feat(keys): RFC 7638 JWK thumbprint#143
Merged
Merged
Conversation
Closes #140. A key's stable identifier — the value used as a kid, inside a cnf claim, and as a DPoP jkt — was not computable anywhere in the library. The only prior mention of a thumbprint is the mdoc_jwk_thumbprint parameter openid4vp takes, a value the caller had to derive themselves because openvc could not do it for them. jwk_thumbprint returns the base64url form, jwk_thumbprint_bytes the raw digest. Both cover EC, OKP, RSA and oct. The canonical form is built from an explicit allow-list of the members RFC 7638 §3.2 — and RFC 8037 §2 for OKP — declares required, not by filtering a deny-list of private ones. That inversion is the security-relevant part: kid, use, alg, x5c and every private member (d, p, q, dp, dq, qi) are excluded by construction, so a private key and its public half hash identically and a member nobody anticipated can never leak into a digest. Fails closed rather than digesting a partial key: an unknown or non-string kty, a missing required member, or a non-string member value raises InvalidKey. The canonical form is defined over string members, so coercing anything else would silently produce a digest for a key the caller does not hold. Serialization follows RFC 7638 §3 exactly — sort_keys for the lexicographic code-point order, compact separators for the forbidden whitespace, and ensure_ascii=False so the UTF-8 form is digested rather than a \u-escaped variant. Pinned by both published golden vectors: RFC 7638 §3.1 (RSA) and RFC 8037 §2 (Ed25519). Both carry alg and kid in the spec's own example, so they double as the exclusion test — a filtering bug would move the digest off the published value. Forty tests in total, covering the vectors, exclusion, canonical ordering, input-order independence, hash selection, and every fail-closed path. dcapi_session_transcript's docstring now points at jwk_thumbprint_bytes, which returns the raw digest that handover expects. Prerequisite for the OpenID4VCI key-proof verifier (#141), which must reject a batch where two proofs share a thumbprint — but useful on its own for cnf/kid construction and did:jwk work.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #140.
A key's stable identifier — the value used as a
kid, inside acnfclaim, and as aDPoP
jkt— was not computable anywhere in the library. The only prior mention of athumbprint is the
mdoc_jwk_thumbprintparameteropenid4vptakes: a value thecaller had to derive themselves, because openvc could not do it for them.
Covers
EC,OKP,RSAandoct.The design decision that matters
The canonical form is built from an explicit allow-list of the members RFC 7638
§3.2 (and RFC 8037 §2 for
OKP) declares required — not by filtering out a deny-listof private ones.
That inversion is the security-relevant part.
kid,use,alg,x5cand everyprivate member (
d,p,q,dp,dq,qi) are excluded by construction, so:A deny-list would have to be kept exhaustive forever, and would fail silently the first
time it was not.
Fail-closed
An unknown or non-string
kty, a missing required member, or a non-string member valueraises
InvalidKeyrather than digesting a partial key. The canonical form is definedover string members, so coercing anything else would silently produce a digest for a key
the caller does not hold.
Serialization follows RFC 7638 §3 exactly:
sort_keysfor the lexicographiccode-point order, compact separators for the forbidden whitespace, and
ensure_ascii=Falseso the UTF-8 form is digested rather than a\u-escaped variant.Golden vectors — real, published
Both spec worked examples are pinned:
NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9XskPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4kBoth carry
algandkidin the specs' own examples, so they double as the exclusiontest — a filtering bug moves the digest off the published value rather than passing
quietly. This is one of the rare cases where the spec hands us exact expected output, so
no self-made fixtures were needed.
40 tests in total: the vectors, exclusion, canonical ordering, input-order independence,
cross-curve non-collision, hash selection, every fail-closed path, and a round-trip
through every software backend including
P256KeyAgreementKey(whoseuse: "enc"tagmust not leak in).
Also
dcapi_session_transcript's docstring now points atjwk_thumbprint_bytes, whichreturns the raw digest that ISO 18013-7 handover expects — closing the wart where the
caller was told to compute it themselves.
Why now
Prerequisite for the OpenID4VCI key-proof verifier (#141), which must reject a batch
where two proofs share a thumbprint (ADR-0007 D6).
Useful on its own for
cnf/kidconstruction anddid:jwkwork.Verification
flake8clean ·mypyclean (60 source files) ·pytest1423 passed, 22 skipped(+41) ·
gitlintclean. No new runtime dependency — stdlibhashlib+json.