blob: 94544544ef6fe15c2d0ca0ff95c86f688929a845 [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 the RFC 7638 JWK Thumbprint and RFC 9278 Thumbprint URI:
* jwks_item_thumbprint() and jwks_item_thumbprint_uri(). The expected values
* are deterministic across every crypto and JSON backend, so each test runs
* under the full jwt_test_ops[] loop. */
/* RFC 7638 Section 3.1 example JWK and its published thumbprint. The extra
* members ("alg", "kid") are NOT part of the thumbprint and must be ignored. */
static const char rfc7638_jwk[] =
"{\"kty\":\"RSA\","
"\"n\":\"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86z"
"wu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9y"
"BXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7"
"d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2N"
"cRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw\","
"\"e\":\"AQAB\",\"alg\":\"RS256\",\"kid\":\"2011-04-29\"}";
static const char rfc7638_thumb[] =
"NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs";
static const char *URN_PREFIX = "urn:ietf:params:oauth:jwk-thumbprint:";
static jwk_set_t *load_set(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;
}
START_TEST(test_rfc7638_vector)
{
jwk_set_t *set;
const jwk_item_t *item;
char_auto *tp = NULL;
char_auto *tp0 = NULL;
char_auto *uri = NULL;
SET_OPS();
set = jwks_create(rfc7638_jwk);
ck_assert_ptr_nonnull(set);
ck_assert(!jwks_error(set));
item = jwks_item_get(set, 0);
ck_assert_ptr_nonnull(item);
/* Explicit SHA-256 and the zero (default) selector must both equal the
* RFC value (JWK_THUMBPRINT_SHA256 is 0). */
tp = jwks_item_thumbprint(item, JWK_THUMBPRINT_SHA256);
ck_assert_ptr_nonnull(tp);
ck_assert_str_eq(tp, rfc7638_thumb);
tp0 = jwks_item_thumbprint(item, 0);
ck_assert_ptr_nonnull(tp0);
ck_assert_str_eq(tp0, rfc7638_thumb);
uri = jwks_item_thumbprint_uri(item, JWK_THUMBPRINT_SHA256);
ck_assert_ptr_nonnull(uri);
ck_assert_str_eq(uri,
"urn:ietf:params:oauth:jwk-thumbprint:sha-256:"
"NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs");
jwks_free(set);
}
END_TEST
/* Known SHA-256 thumbprints for the standard fixtures, one per key type. */
static const struct {
const char *file;
const char *thumb;
} known[] = {
{ "ec_key_prime256v1.json", "dcqGJi_pttovbuO9Lefa_iDKSnhrviqfEWPhjKkzTgs" },
{ "rsa_key_2048.json", "Fqs2Gji6uQfnAew4-CpkIQVlUVqLMRCmecl5ddMwVaA" },
{ "oct_key_256.json", "CqF3NtBIm-AK4Ik0wgHcKk5vLKjxQMrLFuiF4BvHcho" },
{ "eddsa_key_ed25519.json", "yP8YONwTmFovdfeOfp9y5u_lxzP7Y3JAmpUgeNnXFS8" },
};
START_TEST(test_known_values)
{
size_t i;
SET_OPS();
for (i = 0; i < ARRAY_SIZE(known); i++) {
jwk_set_t *set = load_set(known[i].file);
const jwk_item_t *item = jwks_item_get(set, 0);
char_auto *tp = NULL;
ck_assert_ptr_nonnull(item);
ck_assert(!jwks_item_error(item));
tp = jwks_item_thumbprint(item, JWK_THUMBPRINT_SHA256);
ck_assert_ptr_nonnull(tp);
ck_assert_str_eq(tp, known[i].thumb);
jwks_free(set);
}
}
END_TEST
/* The thumbprint covers only public members, so a private key and its public
* counterpart must produce the identical thumbprint. */
static const struct {
const char *priv;
const char *pub;
} pairs[] = {
{ "ec_key_prime256v1.json", "ec_key_prime256v1_pub.json" },
{ "eddsa_key_ed25519.json", "eddsa_key_ed25519_pub.json" },
};
START_TEST(test_public_equals_private)
{
size_t i;
SET_OPS();
for (i = 0; i < ARRAY_SIZE(pairs); i++) {
jwk_set_t *sp = load_set(pairs[i].priv);
jwk_set_t *su = load_set(pairs[i].pub);
char_auto *tp = jwks_item_thumbprint(jwks_item_get(sp, 0),
JWK_THUMBPRINT_SHA256);
char_auto *tu = jwks_item_thumbprint(jwks_item_get(su, 0),
JWK_THUMBPRINT_SHA256);
ck_assert_ptr_nonnull(tp);
ck_assert_ptr_nonnull(tu);
ck_assert_str_eq(tp, tu);
jwks_free(sp);
jwks_free(su);
}
}
END_TEST
START_TEST(test_seed_only_okp_null)
{
jwk_set_t *set;
const jwk_item_t *item;
SET_OPS();
/* The Ed448 fixture is a seed-only private OKP key (it has "d" but no
* "x"). The thumbprint requires the public "x" member, so it must fail
* cleanly (return NULL, never crash). */
set = load_set("eddsa_key_ed448.json");
item = jwks_item_get(set, 0);
ck_assert_ptr_nonnull(item);
ck_assert_ptr_null(jwks_item_thumbprint(item, JWK_THUMBPRINT_SHA256));
ck_assert_ptr_null(jwks_item_thumbprint_uri(item, JWK_THUMBPRINT_SHA256));
jwks_free(set);
}
END_TEST
START_TEST(test_hash_sizes)
{
jwk_set_t *set;
const jwk_item_t *item;
char_auto *t256 = NULL;
char_auto *t384 = NULL;
char_auto *t512 = NULL;
char_auto *u384 = NULL;
char_auto *u512 = NULL;
size_t plen = strlen(URN_PREFIX);
SET_OPS();
set = load_set("ec_key_prime256v1.json");
item = jwks_item_get(set, 0);
ck_assert_ptr_nonnull(item);
t256 = jwks_item_thumbprint(item, JWK_THUMBPRINT_SHA256);
t384 = jwks_item_thumbprint(item, JWK_THUMBPRINT_SHA384);
t512 = jwks_item_thumbprint(item, JWK_THUMBPRINT_SHA512);
ck_assert_ptr_nonnull(t256);
ck_assert_ptr_nonnull(t384);
ck_assert_ptr_nonnull(t512);
/* base64url without padding: 32 -> 43, 48 -> 64, 64 -> 86 chars. */
ck_assert_uint_eq(strlen(t256), 43);
ck_assert_uint_eq(strlen(t384), 64);
ck_assert_uint_eq(strlen(t512), 86);
/* A different hash size yields a different value. */
ck_assert(strcmp(t256, t384) != 0);
u384 = jwks_item_thumbprint_uri(item, JWK_THUMBPRINT_SHA384);
u512 = jwks_item_thumbprint_uri(item, JWK_THUMBPRINT_SHA512);
ck_assert_ptr_nonnull(u384);
ck_assert_ptr_nonnull(u512);
ck_assert(!strncmp(u384, URN_PREFIX, plen));
ck_assert(!strncmp(u384 + plen, "sha-384:", 8));
ck_assert(!strncmp(u512 + plen, "sha-512:", 8));
/* The URI must carry the bare thumbprint after the "sha-NNN:" label. */
ck_assert_str_eq(u384 + plen + 8, t384);
jwks_free(set);
}
END_TEST
START_TEST(test_errors)
{
jwk_set_t *set;
const jwk_item_t *item;
char_auto *tp = NULL;
SET_OPS();
/* NULL item */
ck_assert_ptr_null(jwks_item_thumbprint(NULL, JWK_THUMBPRINT_SHA256));
ck_assert_ptr_null(jwks_item_thumbprint_uri(NULL, JWK_THUMBPRINT_SHA256));
set = load_set("ec_key_prime256v1.json");
item = jwks_item_get(set, 0);
ck_assert_ptr_nonnull(item);
/* Out-of-range hash selector. */
ck_assert_ptr_null(jwks_item_thumbprint(item, (jwk_thumbprint_alg_t)100));
ck_assert_ptr_null(jwks_item_thumbprint_uri(item,
(jwk_thumbprint_alg_t)100));
/* A valid call still works after the rejected ones. */
tp = jwks_item_thumbprint(item, JWK_THUMBPRINT_SHA256);
ck_assert_ptr_nonnull(tp);
jwks_free(set);
}
END_TEST
START_TEST(test_find_bythumbprint)
{
jwk_set_t *set;
char *p_ec, *p_ed;
const jwk_item_t *found, *ec_item, *ed_item;
char_auto *tp_ec = NULL;
char_auto *uri_ed = NULL;
int r;
SET_OPS();
/* Build a two-key set: EC P-256 + Ed25519 (loaded into one set). */
r = asprintf(&p_ec, KEYDIR "/ec_key_prime256v1.json");
ck_assert_int_gt(r, 0);
r = asprintf(&p_ed, KEYDIR "/eddsa_key_ed25519.json");
ck_assert_int_gt(r, 0);
set = jwks_load_fromfile(NULL, p_ec);
ck_assert_ptr_nonnull(set);
ck_assert_ptr_nonnull(jwks_load_fromfile(set, p_ed));
free(p_ec);
free(p_ed);
ck_assert(!jwks_error(set));
ck_assert_uint_eq(jwks_item_count(set), 2);
ec_item = jwks_item_get(set, 0);
ed_item = jwks_item_get(set, 1);
/* Find the EC key by its bare SHA-256 thumbprint. */
tp_ec = jwks_item_thumbprint(ec_item, JWK_THUMBPRINT_SHA256);
ck_assert_ptr_nonnull(tp_ec);
found = jwks_find_bythumbprint(set, JWK_THUMBPRINT_SHA256, tp_ec);
ck_assert_ptr_eq(found, ec_item);
/* Find the Ed25519 key by its (SHA-384) thumbprint URI. */
uri_ed = jwks_item_thumbprint_uri(ed_item, JWK_THUMBPRINT_SHA384);
ck_assert_ptr_nonnull(uri_ed);
found = jwks_find_bythumbprint_uri(set, uri_ed);
ck_assert_ptr_eq(found, ed_item);
/* Right value, wrong hash -> no match. */
ck_assert_ptr_null(jwks_find_bythumbprint(set, JWK_THUMBPRINT_SHA512,
tp_ec));
/* Negative and bad-input paths. */
ck_assert_ptr_null(jwks_find_bythumbprint(set, JWK_THUMBPRINT_SHA256,
"nomatch"));
ck_assert_ptr_null(jwks_find_bythumbprint(NULL, JWK_THUMBPRINT_SHA256,
tp_ec));
ck_assert_ptr_null(jwks_find_bythumbprint(set, JWK_THUMBPRINT_SHA256,
NULL));
ck_assert_ptr_null(jwks_find_bythumbprint_uri(NULL, uri_ed));
ck_assert_ptr_null(jwks_find_bythumbprint_uri(set, NULL));
/* Wrong URN prefix, unknown hash label, and valid-but-unmatched. */
ck_assert_ptr_null(jwks_find_bythumbprint_uri(set, "not-a-uri"));
ck_assert_ptr_null(jwks_find_bythumbprint_uri(set,
"urn:ietf:params:oauth:jwk-thumbprint:sha-999:abc"));
ck_assert_ptr_null(jwks_find_bythumbprint_uri(set,
"urn:ietf:params:oauth:jwk-thumbprint:sha-256:nomatch"));
jwks_free(set);
}
END_TEST
#ifdef LIBJWT_HAVE_ML_DSA
/* Known SHA-256 thumbprints for the ML-DSA (AKP) fixtures. The digest is over
* the public members ("alg","kty","pub") and is backend independent. */
static const struct {
const char *file;
const char *thumb;
} akp_known[] = {
{ "mldsa_key_44.json", "xxGwBakMZvovG1w3N7H6HRyppdaD7RkeKhsjtRZsyeA" },
{ "mldsa_key_44_pub.json", "xxGwBakMZvovG1w3N7H6HRyppdaD7RkeKhsjtRZsyeA" },
{ "mldsa_key_65.json", "ZlqlfBlPyndAYX9Bav1t7QPDoguca-O5n5FW5Kv9HXs" },
{ "mldsa_key_87.json", "9ljbLP6hu3Z-N3QsolhMGSUwctjOuM4graYlTXgB_xQ" },
};
START_TEST(test_akp_values)
{
size_t i;
SET_OPS();
for (i = 0; i < ARRAY_SIZE(akp_known); i++) {
jwk_set_t *set = load_set(akp_known[i].file);
const jwk_item_t *item = jwks_item_get(set, 0);
char_auto *tp = NULL;
ck_assert_ptr_nonnull(item);
/* A backend that cannot import ML-DSA at runtime (e.g. a GnuTLS
* without leancrypto) flags the item; skip it. Any backend that
* does import the key yields the same backend-independent value. */
if (jwks_item_error(item)) {
jwks_free(set);
continue;
}
tp = jwks_item_thumbprint(item, JWK_THUMBPRINT_SHA256);
ck_assert_ptr_nonnull(tp);
ck_assert_str_eq(tp, akp_known[i].thumb);
jwks_free(set);
}
}
END_TEST
#endif
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_thumbprint");
tcase_add_loop_test(tc_core, test_rfc7638_vector, 0, i);
tcase_add_loop_test(tc_core, test_known_values, 0, i);
tcase_add_loop_test(tc_core, test_public_equals_private, 0, i);
tcase_add_loop_test(tc_core, test_seed_only_okp_null, 0, i);
tcase_add_loop_test(tc_core, test_hash_sizes, 0, i);
tcase_add_loop_test(tc_core, test_errors, 0, i);
tcase_add_loop_test(tc_core, test_find_bythumbprint, 0, i);
#ifdef LIBJWT_HAVE_ML_DSA
tcase_add_loop_test(tc_core, test_akp_values, 0, i);
#endif
tcase_set_timeout(tc_core, 30);
suite_add_tcase(s, tc_core);
return s;
}
int main(void)
{
JWT_TEST_MAIN("LibJWT JWK Thumbprint (RFC 7638 / RFC 9278)");
}