blob: ac2e55e37fc872e077b70a115b96d9c1774e60db [file] [edit]
/* Public domain, no copyright. Use at your own risk. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "jwt_tests.h"
/* Tests for embedded-JWK verification (RFC 7515 4.1.3): jwt_checker_enable_
* embedded_jwk() (thumbprint pin) and _keyring() (allowlist). The header "jwk"
* is attacker-supplied, so the security contract is that it is accepted ONLY
* after the key is confirmed against a caller-supplied pin/allowlist AND its
* signature verifies against that confirmed key. This is the DPoP / OpenID4VCI
* key-proof mechanism. The proof carries the P-256 public key in its protected
* header and is signed by the matching private key. */
/* The public half of tests/keys/ec_key_prime256v1.json (key "A"). */
#define PUB_JWK_P256 \
"{\"kty\":\"EC\",\"crv\":\"P-256\"," \
"\"x\":\"Y--DdSpCZ5oF3j__h-SdNJIwvB5aI4AXzpRErGUjWrM\"," \
"\"y\":\"_bSTCXlDeU-pZZbOKDUVLANspSIeuKZfTM8rtXFG_RU\"}"
static jwk_set_t *load_key_a(void)
{
jwk_set_t *set = jwks_create_fromfile(KEYDIR "/ec_key_prime256v1.json");
ck_assert_ptr_nonnull(set);
return set;
}
/* A self-contained proof: header "jwk" = @jwk_hdr (or none), signed with
* @signing_key, typ @typ. Caller frees the token. */
static char *make_proof(const jwk_item_t *signing_key, char *jwk_hdr,
const char *typ)
{
jwt_builder_auto_t *b = jwt_builder_new();
jwt_value_t v;
ck_assert_ptr_nonnull(b);
ck_assert_int_eq(jwt_builder_setkey(b, JWT_ALG_ES256, signing_key), 0);
if (typ != NULL)
ck_assert_int_eq(jwt_builder_settyp(b, typ), 0);
if (jwk_hdr != NULL) {
jwt_set_SET_JSON(&v, "jwk", jwk_hdr);
ck_assert_int_eq(jwt_builder_header_set(b, &v),
JWT_VALUE_ERR_NONE);
}
jwt_set_SET_STR(&v, "htm", "POST");
ck_assert_int_eq(jwt_builder_claim_set(b, &v), JWT_VALUE_ERR_NONE);
return jwt_builder_generate(b);
}
START_TEST(test_pin_confirms)
{
jwk_set_t *set;
const jwk_item_t *key;
jwt_checker_auto_t *checker = NULL;
char_auto *jkt = NULL;
char_auto *token = NULL;
SET_OPS();
set = load_key_a();
key = jwks_item_get(set, 0);
jkt = jwks_item_thumbprint(key, JWK_THUMBPRINT_SHA256);
ck_assert_ptr_nonnull(jkt);
token = make_proof(key, PUB_JWK_P256, "dpop+jwt");
ck_assert_ptr_nonnull(token);
checker = jwt_checker_new();
ck_assert_int_eq(jwt_checker_expect_typ(checker, "dpop+jwt"), 0);
{
const jwt_alg_t algs[] = { JWT_ALG_ES256 };
ck_assert_int_eq(jwt_checker_setalgs(checker, algs, 1), 0);
}
ck_assert_int_eq(jwt_checker_enable_embedded_jwk(checker,
JWK_THUMBPRINT_SHA256, jkt), 0);
ck_assert_int_eq(jwt_checker_verify(checker, token), 0);
/* The confirmed embedded key is borrowable through the introspection API. */
ck_assert_uint_eq(jwt_checker_sig_count(checker), 1);
ck_assert_int_eq(jwt_checker_sig_verified(checker, 0), 1);
ck_assert_ptr_nonnull(jwt_checker_sig_key(checker, 0));
jwks_free(set);
}
END_TEST
START_TEST(test_pin_mismatch_rejected)
{
jwk_set_t *set;
const jwk_item_t *key;
jwt_checker_auto_t *checker = NULL;
char_auto *token = NULL;
SET_OPS();
set = load_key_a();
key = jwks_item_get(set, 0);
token = make_proof(key, PUB_JWK_P256, "dpop+jwt");
ck_assert_ptr_nonnull(token);
checker = jwt_checker_new();
/* Pin to a thumbprint the embedded key does not match. */
ck_assert_int_eq(jwt_checker_enable_embedded_jwk(checker,
JWK_THUMBPRINT_SHA256,
"not-the-right-thumbprint-value-000000000000"), 0);
ck_assert_int_ne(jwt_checker_verify(checker, token), 0);
jwks_free(set);
}
END_TEST
START_TEST(test_missing_header_jwk_rejected)
{
jwk_set_t *set;
const jwk_item_t *key;
jwt_checker_auto_t *checker = NULL;
char_auto *jkt = NULL;
char_auto *token = NULL;
SET_OPS();
set = load_key_a();
key = jwks_item_get(set, 0);
jkt = jwks_item_thumbprint(key, JWK_THUMBPRINT_SHA256);
/* A token with no "jwk" header at all. */
token = make_proof(key, NULL, "dpop+jwt");
ck_assert_ptr_nonnull(token);
checker = jwt_checker_new();
ck_assert_int_eq(jwt_checker_enable_embedded_jwk(checker,
JWK_THUMBPRINT_SHA256, jkt), 0);
ck_assert_int_ne(jwt_checker_verify(checker, token), 0);
jwks_free(set);
}
END_TEST
/* The crux: even when the embedded key's thumbprint matches the pin, the
* signature must verify against THAT key. A proof that embeds key A's public
* key but is signed by a different key B must be rejected. */
START_TEST(test_signature_must_match_embedded)
{
jwk_set_t *set, *bset;
const jwk_item_t *akey, *bkey;
jwt_checker_auto_t *checker = NULL;
char_auto *jkt = NULL;
char_auto *token = NULL;
SET_OPS();
set = load_key_a();
akey = jwks_item_get(set, 0);
jkt = jwks_item_thumbprint(akey, JWK_THUMBPRINT_SHA256);
bset = jwks_create_generate(JWK_KEY_TYPE_EC, "P-256", JWT_ALG_ES256,
JWK_KEY_NONE);
ck_assert_ptr_nonnull(bset);
bkey = jwks_item_get(bset, 0);
if (bkey == NULL || jwks_item_error(bkey)) {
/* A backend with no EC keygen: nothing to test here. */
jwks_free(bset);
jwks_free(set);
return;
}
/* header jwk = A (matches the pin), but signed by B. */
token = make_proof(bkey, PUB_JWK_P256, "dpop+jwt");
ck_assert_ptr_nonnull(token);
checker = jwt_checker_new();
ck_assert_int_eq(jwt_checker_enable_embedded_jwk(checker,
JWK_THUMBPRINT_SHA256, jkt), 0);
ck_assert_int_ne(jwt_checker_verify(checker, token), 0);
jwks_free(bset);
jwks_free(set);
}
END_TEST
START_TEST(test_keyring_allowlist)
{
jwk_set_t *set, *other;
const jwk_item_t *key;
jwt_checker_auto_t *c1 = NULL, *c2 = NULL;
char_auto *token = NULL;
SET_OPS();
set = load_key_a();
key = jwks_item_get(set, 0);
token = make_proof(key, PUB_JWK_P256, "dpop+jwt");
ck_assert_ptr_nonnull(token);
/* The allowlist contains key A: accepted. */
c1 = jwt_checker_new();
ck_assert_int_eq(jwt_checker_enable_embedded_jwk_keyring(c1,
JWK_THUMBPRINT_SHA256, set), 0);
ck_assert_int_eq(jwt_checker_verify(c1, token), 0);
/* An allowlist of some other key: rejected. */
other = jwks_create_fromfile(KEYDIR "/ec_key_secp256k1.json");
ck_assert_ptr_nonnull(other);
c2 = jwt_checker_new();
ck_assert_int_eq(jwt_checker_enable_embedded_jwk_keyring(c2,
JWK_THUMBPRINT_SHA256, other), 0);
ck_assert_int_ne(jwt_checker_verify(c2, token), 0);
jwks_free(other);
jwks_free(set);
}
END_TEST
/* A malformed/incomplete header "jwk" is rejected, not crashed on (the header
* key is fully attacker-controlled). */
START_TEST(test_malformed_embedded_jwk)
{
jwk_set_t *set;
const jwk_item_t *key;
char_auto *jkt = NULL;
const char *bad[] = {
"{\"kty\":\"EC\",\"crv\":\"P-256\"}", /* missing x/y */
"{\"kty\":\"frobnicate\"}", /* unknown kty */
"{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"@\",\"y\":\"@\"}",
"{}", /* empty object */
};
size_t n;
SET_OPS();
set = load_key_a();
key = jwks_item_get(set, 0);
jkt = jwks_item_thumbprint(key, JWK_THUMBPRINT_SHA256);
for (n = 0; n < ARRAY_SIZE(bad); n++) {
jwt_checker_auto_t *cc = jwt_checker_new();
char *t = make_proof(key, (char *)bad[n], "dpop+jwt");
ck_assert_ptr_nonnull(t);
ck_assert_int_eq(jwt_checker_enable_embedded_jwk(cc,
JWK_THUMBPRINT_SHA256, jkt), 0);
/* A malformed header key is rejected, never crashed on. */
ck_assert_int_ne(jwt_checker_verify(cc, t), 0);
free(t);
}
jwks_free(set);
}
END_TEST
/* One embedded-JWK checker verifies two proofs in a row: the confirmed key from
* the first verify must be released before the second borrows its own. */
START_TEST(test_checker_reuse)
{
jwk_set_t *set;
const jwk_item_t *key;
jwt_checker_auto_t *checker = NULL;
char_auto *jkt = NULL;
char_auto *t1 = NULL, *t2 = NULL;
SET_OPS();
set = load_key_a();
key = jwks_item_get(set, 0);
jkt = jwks_item_thumbprint(key, JWK_THUMBPRINT_SHA256);
t1 = make_proof(key, PUB_JWK_P256, "dpop+jwt");
t2 = make_proof(key, PUB_JWK_P256, "dpop+jwt");
ck_assert_ptr_nonnull(t1);
ck_assert_ptr_nonnull(t2);
checker = jwt_checker_new();
ck_assert_int_eq(jwt_checker_enable_embedded_jwk(checker,
JWK_THUMBPRINT_SHA256, jkt), 0);
ck_assert_int_eq(jwt_checker_verify(checker, t1), 0);
ck_assert_int_eq(jwt_checker_verify(checker, t2), 0);
jwks_free(set);
}
END_TEST
START_TEST(test_enable_errors)
{
jwk_set_t *set;
const jwk_item_t *key;
jwt_checker_auto_t *checker = NULL;
char_auto *jkt = NULL;
SET_OPS();
set = load_key_a();
key = jwks_item_get(set, 0);
jkt = jwks_item_thumbprint(key, JWK_THUMBPRINT_SHA256);
checker = jwt_checker_new();
/* No "trust whatever is embedded" mode: a NULL/empty pin must fail. */
ck_assert_int_ne(jwt_checker_enable_embedded_jwk(NULL,
JWK_THUMBPRINT_SHA256, jkt), 0);
ck_assert_int_ne(jwt_checker_enable_embedded_jwk(checker,
JWK_THUMBPRINT_SHA256, NULL), 0);
ck_assert_int_ne(jwt_checker_enable_embedded_jwk(checker,
JWK_THUMBPRINT_SHA256, ""), 0);
/* An out-of-range thumbprint selector. */
ck_assert_int_ne(jwt_checker_enable_embedded_jwk(checker,
(jwk_thumbprint_alg_t)42, jkt), 0);
/* The keyring form: NULL checker, bad alg, and a missing keyring all fail. */
ck_assert_int_ne(jwt_checker_enable_embedded_jwk_keyring(NULL,
JWK_THUMBPRINT_SHA256, set), 0);
ck_assert_int_ne(jwt_checker_enable_embedded_jwk_keyring(checker,
(jwk_thumbprint_alg_t)42, set), 0);
ck_assert_int_ne(jwt_checker_enable_embedded_jwk_keyring(checker,
JWK_THUMBPRINT_SHA256, NULL), 0);
jwks_free(set);
}
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_embedded_jwk");
tcase_add_loop_test(tc_core, test_pin_confirms, 0, i);
tcase_add_loop_test(tc_core, test_pin_mismatch_rejected, 0, i);
tcase_add_loop_test(tc_core, test_missing_header_jwk_rejected, 0, i);
tcase_add_loop_test(tc_core, test_signature_must_match_embedded, 0, i);
tcase_add_loop_test(tc_core, test_keyring_allowlist, 0, i);
tcase_add_loop_test(tc_core, test_malformed_embedded_jwk, 0, i);
tcase_add_loop_test(tc_core, test_checker_reuse, 0, i);
tcase_add_loop_test(tc_core, test_enable_errors, 0, i);
tcase_set_timeout(tc_core, 30);
suite_add_tcase(s, tc_core);
return s;
}
int main(void)
{
JWT_TEST_MAIN("LibJWT embedded-JWK verify (RFC 7515 4.1.3)");
}