blob: 95d5891e1d0cfb8c2d2764b89c5d706ee5b1ef4e [file] [edit]
/* Public domain, no copyright. Use at your own risk. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "jwt_tests.h"
/* Same-family algorithm downgrade in the Compact verify path.
*
* On the pinned path of __verify_config_post() -- both config->alg and
* config->key->alg set and equal -- the token's own alg (jwt->alg) was never
* compared against the pinned alg, and the jwt_alg_required_kty() backstop is
* only family-granular (every RSn/PSn alg maps to RSA). So a token presenting
* "RS512" verified against a key pinned as "RS256" -- same family, valid
* signature -- was accepted, i.e. a pinned verifier accepted an algorithm it
* never pinned. Not a forgery; a policy/conformance gap. These tests pin it
* shut and guard the legitimate (matching) cases against over-rejection.
*
* Runs under each crypto backend (SET_OPS); RSA only, so JSON-backend-agnostic.
*/
static jwk_set_t *load(const char *file)
{
char *path;
jwk_set_t *set;
int ret;
ret = asprintf(&path, KEYDIR "/%s", file);
ck_assert_int_gt(ret, 0);
set = jwks_create_fromfile(path);
free(path);
ck_assert_ptr_nonnull(set);
return set;
}
/* Sign a token with the given alg and serialization using a key that carries no
* "alg" hint (so the builder lets us choose any RSA alg for the same modulus). */
static char *sign(const jwk_item_t *key, jwt_alg_t alg, jwt_serialization_t fmt)
{
jwt_builder_auto_t *b = jwt_builder_new();
ck_assert_int_eq(jwt_builder_setkey(b, alg, key), 0);
ck_assert_int_eq(jwt_builder_set_format(b, fmt), 0);
return jwt_builder_generate(b);
}
/* The same downgrade check on both serializations: the Compact path runs
* through __verify_config_post(), the JSON path through verify_entry() /
* try_candidate(). jwt_checker_verify() auto-detects the format. */
START_TEST(test_same_family_downgrade)
{
jwk_set_t *sks, *vks;
const jwk_item_t *skey, *vkey;
jwt_serialization_t fmts[] = { JWT_FORMAT_COMPACT, JWT_FORMAT_JSON_FLAT };
size_t i;
SET_OPS();
/* Same 2048-bit RSA modulus, two views: one with no "alg" (to sign any
* RSA alg), one pinned to RS256 (to verify). */
sks = load("rsa_key_2048_no_alg.json");
skey = jwks_item_get(sks, 0);
ck_assert_ptr_nonnull(skey);
vks = load("rsa_key_2048.json"); /* carries "alg":"RS256" */
vkey = jwks_item_get(vks, 0);
ck_assert_ptr_nonnull(vkey);
for (i = 0; i < ARRAY_SIZE(fmts); i++) {
char_auto *rs512 = NULL, *rs256 = NULL;
jwt_checker_auto_t *c1 = NULL, *c2 = NULL, *c3 = NULL;
/* A genuinely RS512-signed token (valid signature on this key). */
rs512 = sign(skey, JWT_ALG_RS512, fmts[i]);
ck_assert_ptr_nonnull(rs512);
/* THE GAP: pin RS256, present RS512. The signature is otherwise
* valid, so without the alg pin this verifies; it must be rejected. */
c1 = jwt_checker_new();
ck_assert_int_eq(jwt_checker_setkey(c1, JWT_ALG_RS256, vkey), 0);
ck_assert_int_ne(jwt_checker_verify(c1, rs512), 0);
/* Positive control 1: pin RS256, present RS256 -> accepted. */
rs256 = sign(skey, JWT_ALG_RS256, fmts[i]);
ck_assert_ptr_nonnull(rs256);
c2 = jwt_checker_new();
ck_assert_int_eq(jwt_checker_setkey(c2, JWT_ALG_RS256, vkey), 0);
ck_assert_int_eq(jwt_checker_verify(c2, rs256), 0);
/* Positive control 2: legitimately accept RS512 (via the no-alg key,
* which pins no alg) -> the pin does not over-reject. */
c3 = jwt_checker_new();
ck_assert_int_eq(jwt_checker_setkey(c3, JWT_ALG_RS512, skey), 0);
ck_assert_int_eq(jwt_checker_verify(c3, rs512), 0);
}
jwks_free(sks);
jwks_free(vks);
}
END_TEST
static Suite *libjwt_suite(const char *title)
{
Suite *s;
TCase *tc_core;
int i = ARRAY_SIZE(jwt_test_ops);
s = suite_create(title);
tc_core = tcase_create("jwt_alg_downgrade");
tcase_add_loop_test(tc_core, test_same_family_downgrade, 0, i);
tcase_set_timeout(tc_core, 30);
suite_add_tcase(s, tc_core);
return s;
}
int main(void)
{
JWT_TEST_MAIN("LibJWT same-family algorithm downgrade in verify");
}