Skip to content

Commit a05da90

Browse files
authored
Merge pull request #318 from benmcollins/307-jwk-thumbprint
jwks: RFC 7638 JWK Thumbprint and RFC 9278 Thumbprint URI
2 parents 3c7bd2a + 590beb2 commit a05da90

20 files changed

Lines changed: 886 additions & 58 deletions

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ if (CHECK_FOUND)
407407
408408
# JWKS Tests
409409
list (APPEND UNIT_TESTS jwt_jwks jwt_jwks_errors
410-
jwt_ec jwt_rsa jwt_hs jwt_jwks_pem)
410+
jwt_ec jwt_rsa jwt_hs jwt_jwks_pem jwt_thumbprint)
411411
412412
# Checker and Builder
413413
list (APPEND UNIT_TESTS jwt_builder jwt_checker jwt_flipflop)

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Standard | RFC
1414
``JWK`` | :page_facing_up: [RFC-7517](https://datatracker.ietf.org/doc/html/rfc7517) | JSON Web Keys and Sets
1515
``JWA`` | :page_facing_up: [RFC-7518](https://datatracker.ietf.org/doc/html/rfc7518) | JSON Web Algorithms
1616
``JWT`` | :page_facing_up: [RFC-7519](https://datatracker.ietf.org/doc/html/rfc7519) | JSON Web Token
17+
``JWK Thumbprint`` | :page_facing_up: [RFC-7638](https://datatracker.ietf.org/doc/html/rfc7638) / [RFC-9278](https://datatracker.ietf.org/doc/html/rfc9278) | JWK Thumbprint and Thumbprint URI
1718

1819
> [!NOTE]
1920
> Throughout this documentation you will see links such as the ones

include/jwt.h

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2211,8 +2211,10 @@ jwk_set_t *jwks_create_fromurl(const char *url, int verify);
22112211
*/
22122212
typedef enum {
22132213
JWK_KEY_NONE = 0x0000, /**< No options */
2214-
JWK_KEY_GEN_KID = 0x0001, /**< Generate a random (uuidv4)
2215-
"kid" for each imported key */
2214+
JWK_KEY_GEN_KID = 0x0001, /**< Generate a deterministic
2215+
"kid" (the @rfc{7638} JWK
2216+
SHA-256 thumbprint) for each
2217+
imported key */
22162218
JWK_KEY_TRY_HMAC = 0x0002, /**< If the input does not parse
22172219
as a PEM/DER key, treat the
22182220
raw bytes as an "oct" (HMAC)
@@ -2553,6 +2555,103 @@ char *jwks_item_export(const jwk_item_t *item, int priv);
25532555
JWT_EXPORT
25542556
char *jwks_export(const jwk_set_t *jwk_set, int priv);
25552557

2558+
/**
2559+
* @brief Hash algorithm for a JWK Thumbprint
2560+
*
2561+
* Selects the digest used by jwks_item_thumbprint() and
2562+
* jwks_item_thumbprint_uri(). SHA-256 is the value 0, so it is the default for
2563+
* a zero-initialized argument and is what virtually all deployments use.
2564+
*
2565+
* @since 3.6.0
2566+
*/
2567+
typedef enum {
2568+
JWK_THUMBPRINT_SHA256 = 0, /**< SHA-256 (default) */
2569+
JWK_THUMBPRINT_SHA384, /**< SHA-384 */
2570+
JWK_THUMBPRINT_SHA512, /**< SHA-512 */
2571+
} jwk_thumbprint_alg_t;
2572+
2573+
/**
2574+
* @brief Compute the JWK Thumbprint of a key
2575+
*
2576+
* @rfc{7638,3}
2577+
*
2578+
* Produces the base64url-encoded SHA-2 digest of the key's canonical JWK form:
2579+
* a JSON object containing only the members required for the key type, with no
2580+
* whitespace and the member names in lexicographic order. The result is a
2581+
* stable, deterministic fingerprint of the (public) key parameters, commonly
2582+
* used as a key id (@c "kid") or as the @c "jkt" confirmation value.
2583+
*
2584+
* The thumbprint is computed over public parameters and is identical whether
2585+
* the item was loaded from a JWK or from a PEM/DER key.
2586+
*
2587+
* @param item A JWK Item
2588+
* @param alg The thumbprint hash algorithm (see @ref jwk_thumbprint_alg_t);
2589+
* @ref JWK_THUMBPRINT_SHA256 is the default.
2590+
* @return A newly allocated, nil-terminated base64url string the caller must
2591+
* free with free(), or NULL on error (an unusable key, a missing required
2592+
* member, or an invalid @p alg).
2593+
* @since 3.6.0
2594+
*/
2595+
JWT_EXPORT
2596+
char *jwks_item_thumbprint(const jwk_item_t *item, jwk_thumbprint_alg_t alg);
2597+
2598+
/**
2599+
* @brief Compute the JWK Thumbprint URI of a key
2600+
*
2601+
* @rfc{9278}
2602+
*
2603+
* As jwks_item_thumbprint(), but returns the RFC 9278 URI form:
2604+
* @c "urn:ietf:params:oauth:jwk-thumbprint:sha-256:<thumbprint>" (or
2605+
* @c sha-384 / @c sha-512 to match @p alg).
2606+
*
2607+
* @param item A JWK Item
2608+
* @param alg The thumbprint hash algorithm (see @ref jwk_thumbprint_alg_t);
2609+
* @ref JWK_THUMBPRINT_SHA256 is the default.
2610+
* @return A newly allocated, nil-terminated URI string the caller must free
2611+
* with free(), or NULL on error.
2612+
* @since 3.6.0
2613+
*/
2614+
JWT_EXPORT
2615+
char *jwks_item_thumbprint_uri(const jwk_item_t *item, jwk_thumbprint_alg_t alg);
2616+
2617+
/**
2618+
* @brief Find a key in a set by its JWK Thumbprint
2619+
*
2620+
* @rfc{7638}
2621+
*
2622+
* Returns the first item in @p jwk_set whose thumbprint (for the given hash)
2623+
* equals @p thumbprint. Unlike jwks_find_bykid(), this matches on the key's
2624+
* canonical, deterministic identity rather than the advisory @c "kid", so it
2625+
* works even when keys carry no (or an inconsistent) @c "kid" — e.g. matching a
2626+
* proof-of-possession @c "cnf"/@c "jkt" value against a set of known keys.
2627+
*
2628+
* @param jwk_set An existing jwk_set_t
2629+
* @param alg The hash used to produce @p thumbprint (see @ref jwk_thumbprint_alg_t)
2630+
* @param thumbprint A base64url JWK thumbprint, as from jwks_item_thumbprint()
2631+
* @return The matching jwk_item_t, or NULL if none matches or on bad input
2632+
* @since 3.6.0
2633+
*/
2634+
JWT_EXPORT
2635+
jwk_item_t *jwks_find_bythumbprint(jwk_set_t *jwk_set, jwk_thumbprint_alg_t alg,
2636+
const char *thumbprint);
2637+
2638+
/**
2639+
* @brief Find a key in a set by its JWK Thumbprint URI
2640+
*
2641+
* @rfc{9278}
2642+
*
2643+
* As jwks_find_bythumbprint(), but takes the RFC 9278 URI form
2644+
* (@c "urn:ietf:params:oauth:jwk-thumbprint:sha-256:<thumbprint>"); the hash
2645+
* is taken from the URI's @c sha-NNN label.
2646+
*
2647+
* @param jwk_set An existing jwk_set_t
2648+
* @param uri A JWK Thumbprint URI, as from jwks_item_thumbprint_uri()
2649+
* @return The matching jwk_item_t, or NULL if none matches or on bad input
2650+
* @since 3.6.0
2651+
*/
2652+
JWT_EXPORT
2653+
jwk_item_t *jwks_find_bythumbprint_uri(jwk_set_t *jwk_set, const char *uri);
2654+
25562655
/**
25572656
* @brief Retrieve binary octet data of a key
25582657
*

libjwt/gnutls/sign-verify.c

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,34 @@ static int gnutls_verify_sha_pem(jwt_t *jwt, const char *head,
454454
return jwt->error;
455455
}
456456

457+
/* @rfc{7638} One-shot SHA-2 digest used by the JWK thumbprint. */
458+
static int gnutls_sha(int sha_bits, const unsigned char *in, size_t in_len,
459+
unsigned char *out, unsigned int *out_len)
460+
{
461+
gnutls_digest_algorithm_t alg;
462+
463+
switch (sha_bits) {
464+
case 256:
465+
alg = GNUTLS_DIG_SHA256;
466+
break;
467+
case 384:
468+
alg = GNUTLS_DIG_SHA384;
469+
break;
470+
case 512:
471+
alg = GNUTLS_DIG_SHA512;
472+
break;
473+
default:
474+
return 1; // LCOV_EXCL_LINE
475+
}
476+
477+
if (gnutls_hash_fast(alg, in, in_len, out))
478+
return 1; // LCOV_EXCL_LINE
479+
480+
*out_len = gnutls_hash_get_len(alg);
481+
482+
return 0;
483+
}
484+
457485
/* Export our ops */
458486
struct jwt_crypto_ops jwt_gnutls_ops = {
459487
.name = "gnutls",
@@ -475,6 +503,8 @@ struct jwt_crypto_ops jwt_gnutls_ops = {
475503
/* Native-key -> JWK conversion, done natively by GnuTLS. */
476504
.key2jwk_params = gnutls_key2jwk_params,
477505

506+
.sha = gnutls_sha,
507+
478508
.jwe_implemented = 1,
479509
.rng = gnutls_rng,
480510
.encrypt_aes_gcm = gnutls_encrypt_aes_gcm,

libjwt/jwk-export.c

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,20 @@
2424
#include <jwt.h>
2525
#include "jwt-private.h"
2626

27-
/* Generate an RFC 4122 version 4 UUID string into the caller's buffer (which
28-
* must be at least 37 bytes), using the active backend's CSPRNG. Returns 0 on
29-
* success. */
30-
static int uuidv4(char out[37])
27+
/* Set a deterministic @rfc{7638} "kid" (the SHA-256 JWK thumbprint) on @jwk
28+
* when JWK_KEY_GEN_KID is requested. @jwk must already carry the key's members
29+
* (its kty plus the type's public parameters). A no-op if the thumbprint
30+
* cannot be computed (e.g. a key missing a required public member). */
31+
static void gen_kid(jwt_json_t *jwk, jwk_key_type_t kty, unsigned int flags)
3132
{
32-
uint8_t b[16];
33+
char_auto *tp = NULL;
3334

34-
if (jwt_ops->rng == NULL || jwt_ops->rng(b, sizeof(b)))
35-
return -1; // LCOV_EXCL_LINE
35+
if (!(flags & JWK_KEY_GEN_KID))
36+
return;
3637

37-
/* version 4 and RFC 4122 variant */
38-
b[6] = (b[6] & 0x0F) | 0x40;
39-
b[8] = (b[8] & 0x3F) | 0x80;
40-
41-
snprintf(out, 37,
42-
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
43-
b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
44-
b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]);
45-
46-
return 0;
38+
tp = jwt_jwk_thumbprint(jwk, kty, 256);
39+
if (tp != NULL)
40+
jwt_json_obj_set(jwk, "kid", jwt_json_create_str(tp));
4741
}
4842

4943
/* For HMAC keys: treat the raw bytes as an "oct" key, guessing the alg from the
@@ -103,7 +97,6 @@ int jwt_key2jwk(const char *key, size_t len, unsigned int flags,
10397
jwk_export_t kp;
10498
jwt_json_t *jwk, *ops;
10599
const char *kty;
106-
char kid[37];
107100
int r, i;
108101

109102
memset(&kp, 0, sizeof(kp));
@@ -135,13 +128,11 @@ int jwt_key2jwk(const char *key, size_t len, unsigned int flags,
135128
jwt_json_obj_set(jwk, "key_ops", ops);
136129
}
137130

138-
if ((flags & JWK_KEY_GEN_KID) && uuidv4(kid) == 0)
139-
jwt_json_obj_set(jwk, "kid", jwt_json_create_str(kid));
140-
141131
/* HMAC fallback for unparseable input. */
142132
if (r != 0) {
143133
jwk_export_clear(&kp);
144134
process_hmac_key(jwk, (const unsigned char *)key, len);
135+
gen_kid(jwk, JWK_KEY_TYPE_OCT, flags);
145136
jwt_json_arr_append(out_array, jwk);
146137
return 0;
147138
}
@@ -175,6 +166,8 @@ int jwt_key2jwk(const char *key, size_t len, unsigned int flags,
175166
}
176167
}
177168

169+
gen_kid(jwk, kp.kty, flags);
170+
178171
jwk_export_clear(&kp);
179172
jwt_json_arr_append(out_array, jwk);
180173

0 commit comments

Comments
 (0)