blob: cea50e884a2049c4fbe523399e6c4a4ffd794f74 [file] [edit]
@page profiles \emoji :id: Application Profiles
Many real-world specifications are not new cryptography — they are **application
profiles**: an ordinary signed JWT with a particular media type (@c "typ"), a
required set of claims, and a key-binding convention. LibJWT's job is to expose
the primitives; this page shows how to compose them into each profile. Every
recipe below builds @em and verifies a token with the public API only, and is
mirrored by a test in @c tests/jwt_profiles.c.
The reusable pieces are:
Primitive | Function(s) | Used by
:-------- | :---------- | :------
Media-type pinning | @ref jwt_checker_expect_typ, @ref jwt_builder_settyp | all `typ`-bearing profiles
Algorithm allowlist | @ref jwt_checker_setalgs | all (blocks `alg` confusion)
Required claims | @ref jwt_checker_require | at+jwt, VAPID, PASSporT
Confirmation (`cnf`) | @ref jwt_builder_setcnf_jkt, @ref jwt_builder_setcnf, @ref jwt_get_cnf | DPoP, mTLS
Embedded-key verify | @ref jwt_checker_enable_embedded_jwk | DPoP, OpenID4VCI
Token hash | @ref jwt_token_hash | DPoP `ath`
JWK Thumbprint | @ref jwks_item_thumbprint | DPoP, OpenID4VCI
X.509 (`x5c`/`x5t#S256`) | @ref jwks_item_x5c, @ref jwks_item_x5t_s256 | PASSporT, JAdES, mTLS
Detached payload | @ref jwt_builder_set_detached, @ref jwt_checker_verify_detached | JAdES
@note These profiles layer on a signed JWT only. LibJWT is offline: it does not
implement the HTTP/TLS exchanges of DPoP, mTLS, or OpenID4VCI — only the JWT
touchpoints. Pair these with the BCP 240 / RFC 8725 hardening guidance.
@tableofcontents
@section prof_atjwt \emoji :ticket: OAuth 2.0 access tokens — `at+jwt` (RFC 9068)
@rfc{9068} profiles a JWT access token: the header @c "typ" is @c "at+jwt" and a
fixed set of claims (@c iss, @c exp, @c aud, @c sub, @c client_id, @c iat, @c jti)
is **mandatory**. Pin the type and algorithm, and use @ref jwt_checker_require to
assert the mandatory claims are present (LibJWT otherwise validates only the
claims you ask it to compare).
@code{.c}
/* Issue */
jwt_builder_settyp(builder, "at+jwt");
jwt_builder_setkey(builder, JWT_ALG_ES256, signing_key);
/* ...set iss/sub/aud/client_id/jti and an exp offset... */
/* Verify */
const char *must[] = { "iss","sub","aud","exp","iat","jti","client_id" };
const jwt_alg_t algs[] = { JWT_ALG_ES256 };
jwt_checker_expect_typ(checker, "at+jwt");
jwt_checker_setalgs(checker, algs, 1);
jwt_checker_require(checker, must, 7);
jwt_checker_claim_set(checker, JWT_CLAIM_AUD, "https://rs.example");
jwt_checker_setkey(checker, JWT_ALG_ES256, as_key);
if (jwt_checker_verify(checker, token) == 0)
/* a well-formed RFC 9068 access token */;
@endcode
@section prof_vapid \emoji :inbox_tray: Web Push — VAPID (RFC 8292)
@rfc{8292} (VAPID) is the most widely deployed plain JWS-over-P-256 profile on
the web: an @c ES256 token whose @c aud is the push service origin, @c sub a
contact URI, and @c exp at most 24 hours out. There is no special @c "typ"; the
discipline is the fixed algorithm and the required claims.
@code{.c}
const char *must[] = { "aud", "exp", "sub" };
const jwt_alg_t es256[] = { JWT_ALG_ES256 };
jwt_checker_setalgs(checker, es256, 1); /* ES256 only */
jwt_checker_require(checker, must, 3);
jwt_checker_setkey(checker, JWT_ALG_ES256, app_server_pubkey);
jwt_checker_verify(checker, token);
@endcode
@section prof_passport \emoji :telephone_receiver: Caller ID — PASSporT (RFC 8225)
@rfc{8225} PASSporT (the token behind STIR/SHAKEN) signs caller-identity claims
(@c orig, @c dest, @c iat, @c attest) with @c "typ":"passport", typically ES256,
and carries the signing certificate via @c "x5u" or @c "x5c". Validate the type,
algorithm, and required claims here; read the certificate chain with
@ref jwks_item_x5c (chain/trust validation against the SHAKEN CA is the caller's
PKI policy — see @ref prof_jades for reading `x5c`).
@code{.c}
const char *must[] = { "iat", "orig", "dest" };
const jwt_alg_t es256[] = { JWT_ALG_ES256 };
jwt_checker_expect_typ(checker, "passport");
jwt_checker_setalgs(checker, es256, 1);
jwt_checker_require(checker, must, 3);
jwt_checker_setkey(checker, JWT_ALG_ES256, shaken_leaf_key);
jwt_checker_verify(checker, token);
@endcode
@section prof_oid4vci \emoji :credit_card: OpenID4VCI key proof
An OpenID4VCI key proof (`"typ":"openid4vci-proof+jwt"`) is **self-contained**:
the signing key travels in the protected-header @c "jwk" (@rfc{7515,4.1.3}) and
the issuer binds the credential to that key. Because the header key is supplied
by the presenter, it must be confirmed — here against the holder-key thumbprint
the issuer recorded for the request — with @ref jwt_checker_enable_embedded_jwk,
which refuses to trust an embedded key without a pin.
@code{.c}
/* jkt is the thumbprint the issuer bound the credential request to. */
char *jkt = jwks_item_thumbprint(expected_holder_key, JWK_THUMBPRINT_SHA256);
const jwt_alg_t algs[] = { JWT_ALG_ES256 };
jwt_checker_expect_typ(checker, "openid4vci-proof+jwt");
jwt_checker_setalgs(checker, algs, 1);
jwt_checker_enable_embedded_jwk(checker, JWK_THUMBPRINT_SHA256, jkt);
jwt_checker_verify(checker, proof); /* key is the confirmed header "jwk" */
free(jkt);
@endcode
@section prof_dpop \emoji :hand: Proof-of-possession — DPoP (RFC 9449)
@rfc{9449} DPoP binds an access token to a client key. The access token carries a
@c cnf.jkt (set with @ref jwt_builder_setcnf_jkt); each request is accompanied by
a @c "dpop+jwt" proof that carries the client's public key in its header @c "jwk"
and an @c "ath" claim hashing the access token. To verify a proof: pin the
embedded key to the access token's @c cnf.jkt, then check that @c ath matches
@ref jwt_token_hash of the presented access token (and that @c htm/@c htu match
the request).
@code{.c}
/* The access token was bound to the client key: */
jwt_builder_setcnf_jkt(at_builder, client_key); /* -> cnf.jkt */
/* The proof carries the client key in its header and hashes the AT: */
char *ath = jwt_token_hash(access_token, JWK_THUMBPRINT_SHA256);
/* ...set proof header "jwk", claims htm/htu/jti/ath, sign with client_key... */
/* Verify the proof at the resource server: */
char *jkt = jwt_get_cnf(verified_access_token, "jkt"); /* the binding */
jwt_checker_expect_typ(checker, "dpop+jwt");
jwt_checker_enable_embedded_jwk(checker, JWK_THUMBPRINT_SHA256, jkt);
jwt_checker_verify(checker, proof);
/* In a jwt_checker_setcb() callback, compare the proof's "ath" to
* jwt_token_hash(access_token, JWK_THUMBPRINT_SHA256) and check htm/htu. */
free(ath); free(jkt);
@endcode
@note The confirmation is not a shortcut around the signature: even when the
embedded key's thumbprint matches the pin, the proof's signature must still
verify against that key, so a proof that embeds the victim's key but is signed by
another is rejected.
@section prof_mtls \emoji :lock_with_ink_pen: mTLS-bound tokens (RFC 8705)
@rfc{8705} binds an access token to a client TLS certificate via a @c cnf
@c "x5t#S256" member (the certificate's SHA-256 thumbprint). Set it with
@ref jwt_builder_setcnf; at the resource server, read it with @ref jwt_get_cnf
and compare against the thumbprint of the certificate presented in the TLS
handshake (which your TLS terminator provides).
@code{.c}
/* Issue: bind to the client certificate thumbprint. */
jwt_builder_setcnf(builder, "x5t#S256", client_cert_sha256_b64url);
/* Verify (inside a jwt_checker_setcb() callback): */
char *bound = jwt_get_cnf(jwt, "x5t#S256");
if (bound && !strcmp(bound, presented_cert_thumbprint))
/* the token is being used over the bound mTLS connection */;
free(bound);
@endcode
@section prof_jades \emoji :pen: Detached signatures — JAdES (ETSI 119 182-1)
JAdES (the eIDAS JWS profile) commonly signs a **detached** document — the
payload is conveyed out of band, not inside the token — with the signing
certificate chain in the @c "x5c" header. Sign opaque bytes unencoded
(@rfc{7797}) and detached, then verify with @ref jwt_checker_verify_detached,
supplying the document. Read the chain with @ref jwks_item_x5c (parsed from a
JWK's @c x5c); certificate-path validation is the caller's PKI policy.
@code{.c}
/* Sign a detached, unencoded document with the cert chain in x5c. */
jwt_builder_setpayload(builder, document, document_len);
jwt_builder_setb64(builder, 0); /* opaque bytes, not JSON claims */
jwt_builder_set_detached(builder, 1); /* payload conveyed out of band */
/* ...set the "x5c" header to the certificate chain array... */
token = jwt_builder_generate(builder);
/* Verify with the document supplied out of band. */
jwt_checker_setkey(checker, JWT_ALG_ES256, signer_key);
jwt_checker_verify_detached(checker, token, document, document_len);
@endcode