spec(10.1.6): the curve is part of the identity, not just an algorithm detail - #13
Open
maoryeh wants to merge 1 commit into
Open
spec(10.1.6): the curve is part of the identity, not just an algorithm detail#13maoryeh wants to merge 1 commit into
maoryeh wants to merge 1 commit into
Conversation
…m detail
§10.1.6 makes the RFC 7638 thumbprint the platform identifier but never requires verifiers to pin `crv`,
and verifying the signature does not imply it. That gap is reachable with an off-the-shelf JOSE library.
The thumbprint's canonical input for an OKP key is {crv, kty, x}, so `crv` is inside the digest. Libraries in
wide use accept an OKP JWK whose `crv` is any of Ed25519, Ed448, X25519 or X448 without validating `x`'s
length against it, and then verify an EdDSA signature against those bytes regardless. So one key's own bytes,
merely re-labelled, yield a different `jkt` that still verifies against that same header JWK.
Because the thumbprint *is* the identifier, that hands one key several identities. It lands hardest on the
trust-on-first-use binding this section leans on: a principal already rejected for claiming a second profile
re-presents under another `crv` and binds cleanly as a first contact, and the "MAY flag or rate-limit" signal
the section recommends is escaped by editing one header member. X25519 and X448 are key-agreement keys
besides, which RFC 8037 §3.1 requires be rejected for signing.
What this is *not*: the cross-key replay defence still holds, since an attacker cannot produce a variant
whose thumbprint matches a victim's recorded `cnf.jkt`. What breaks is identity canonicalization — which is
what everything downstream keys off.
Changes:
- Added a MUST to the Algorithms paragraph: reject a `crv` other than the one the accepted `alg` requires,
and never treat thumbprints differing only in `crv` as distinct principals.
- Folded the curve into step 5 of the normative order rather than adding a step, since `alg` is already
checked there and the two are one decision about the same header.
- Added vector 011-crv-substitution and cited it in specification.md. It follows the 006 model — a rejection
case a plausible implementation would accept: K1's own published bytes labelled crv: X448, with a genuine
signature. A library strict enough to refuse at parse passes it too, since the outcome is the same
rejection.
- Defined [RFC 8037] in the reference block, now that §3.1 is cited.
Found while implementing this section in usp-impl, and reproduced against a real verifier before writing any
of the above: the same Ed25519 key produced four verifying thumbprints, one per curve label.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
yahalomran
approved these changes
Aug 25, 2026
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.
§10.1.6 makes the RFC 7638 thumbprint the platform identifier, but never requires a verifier to pin
crv— and verifying the signature does not imply it. That gap is reachable with an off-the-shelf JOSE library, so the thumbprint is not actually canonical today.The mechanism
The thumbprint's canonical input for an
OKPkey is{crv, kty, x}, socrvis inside the digest. Libraries in wide use accept anOKPJWK whosecrvis any ofEd25519,Ed448,X25519orX448without validatingx's length against it, then verify anEdDSAsignature against those bytes regardless.So one key's own bytes, merely re-labelled, yield a different
jktthat still verifies against that same header JWK. Reproduced against a real verifier before writing any of this — the same Ed25519 key produced four verifying thumbprints, one per curve label. The new vector carries two of them:004-proof-issuance-no-athpublishes K1 asUDDReOZl…, and011-crv-substitutionpresents the identicalxasLSW0kh4c….Why it matters here specifically
Because the thumbprint is the identifier, this hands one key several identities, and it lands hardest on the trust-on-first-use binding this section leans on:
crvand binds cleanly as a first contact.X25519/X448are key-agreement keys besides, which [RFC 8037] §3.1 requires be rejected for signing — so this is key-type confusion as well as an identity break.What this is not: the cross-key replay defence still holds. An attacker cannot produce a variant whose thumbprint matches a victim's recorded
cnf.jkt, so006still passes either way. What breaks is identity canonicalization — which is what everything downstream keys off.Changes
crvother than the one the acceptedalgrequires, and never treat thumbprints differing only incrvas distinct principals.algis already checked there, and the two are one decision about the same header.011-crv-substitution, cited inspecification.md. It follows the006model the vectors README asks for — a rejection case a plausible implementation would accept: K1's own published bytes labelledcrv: X448, with a genuine signature. A library strict enough to refuse at parse passes it too, since the outcome is the same rejection.[RFC 8037]in the reference block, now that §3.1 is cited.Checks
vectors,refsandschemasall pass, withcryptographyinstalled so signatures are verified for real rather than taken on trust:authorityreports 45 failures, but identically on a clean checkout — it wants a builtsite/, which a fresh clone doesn't have. Not touched by this change.One incidental note for whoever owns the tooling:
usp_check.pyverifies signatures withEd25519PublicKey.from_public_bytes(jwk["x"])and ignorescrv, which is why this vector's signature checks out. That's defensible for a vector validator — it isn't a conformance verifier — but it does mean the checker cannot catch a curve-confused vector, so the property has to be asserted in the spec text rather than relied on from tooling.Found while implementing §10.1.6 in
usp-impl; the check is in place there.