Skip to content

httpsig 2.0: track draft-hardt-httpbis-signature-key-07 - #65

Merged
dickhardt merged 6 commits into
mainfrom
v2
Aug 5, 2026
Merged

httpsig 2.0: track draft-hardt-httpbis-signature-key-07#65
dickhardt merged 6 commits into
mainfrom
v2

Conversation

@dickhardt

@dickhardt dickhardt commented Aug 3, 2026

Copy link
Copy Markdown
Member

Starts the 2.x line, tracking draft-hardt-httpbis-signature-key-08 (published to the datatracker 2026-08-05). Version 2.0.0-alpha.2, published under the alpha dist-tag so npm install @hellocoop/httpsig keeps giving 1.x.

145 tests pass. Full caller-facing detail in MIGRATING-2.0.md.

-08 is not wire compatible with -05/-06/-07 in either direction, and the protocol has no version negotiation, so both ends of a deployment move together. 1.x continues on the 1.x branch and keeps latest.

Algorithm determination, inverted

The algorithm now comes from the JWK alg member and is never derived from kty/crv.

  • alg is REQUIRED on every conveyed key and must be fully specified (RFC 9864). Polymorphic EdDSA is rejected in favour of Ed25519/Ed448.
  • A key whose kty or crv disagrees with its alg is rejected rather than resolved in favour of either reading.
  • RSA is newly supported: PS256/384/512, RS256/384/512.
  • Symmetric material (oct, HS*) is rejected outright.
  • ML-DSA and the AKP key type are declined as unsupported_algorithm, not treated as malformed — absence of support is a reason to decline, not a parse failure.
  • generateKeyPair() stamps alg, which WebCrypto does not.

Worth noting kty+crv genuinely does determine the algorithm for OKP and EC. The requirement is uniform anyway, for negotiation totality and forward compatibility — see the draft's Design Rationale A.11.

Wire changes

1.x 2.x
hwk sig=hwk;kty="OKP";crv="Ed25519";x="..." sig=hwk;alg="Ed25519";kty="OKP";crv="Ed25519";x="..."
hwk kid tolerated rejected — key is inline, an identifier selects nothing
negotiation ;sigkey=jkt parameter Accept-Signature-Scheme / Accept-Signature-Alg header fields
error algorithms supported_algorithms member Accept-Signature-Alg
unknown scheme invalid_key unsupported_scheme

Enforcement

  • Discovery metadata must carry a matching issuer. New issuer_missing / issuer_mismatch. 1.x followed jwks_uri wherever the document pointed, so a subdomain takeover or shared-hosting misconfiguration could get requests attributed to an identity that never signed them. Byte equality, no normalization — a trailing slash is a different identifier, and a test pins that.
  • jwt scheme validates exp, which it previously ignored entirely.
  • strictAAuth removed — covering signature-key is a specification requirement, not a profile choice.
  • The alg signature parameter is ignored. Signers never emit it; verifiers ignore it but still reproduce it in the signature base, since @signature-params is covered. Tests sign by hand with a misleading alg to prove the key decides.
  • supportedAlgorithms lets a verifier declare what it accepts, reported back as acceptSignatureAlg for an Accept-Signature-Alg response header — deliberately not inside SignatureError, whose supported_algorithms member -08 removed.
  • Unusable keys elsewhere in a JWKS are skipped. Already the behaviour; now specified and tested. Without it no issuer could ever add a post-quantum key alongside a classical one.
  • Errors are typedSignatureVerificationError carries the code instead of leaving it to message matching.

Known gaps

  • jwks, self-jwt, x509 not implemented. Additive, not breaking.
  • Assertion caching deliberately omitted — the draft calls it a straw man.
  • RFC 9421 and Go interop vectors no longer verify: they predate this specification and do not cover signature-key. They now assert rejection, which is correct but weaker. Replacement coverage tracked in Publish test vectors for each Signature-Key scheme dickhardt/signature-key#25.

Follow-ups

dickhardt and others added 2 commits August 3, 2026 18:06
BREAKING CHANGE: 2.0 is not wire-compatible with 1.x in either
direction, and the protocol has no version negotiation.

Algorithm determination is inverted. The algorithm is taken from the
JWK alg member and never derived from kty and crv, which underdetermine
it: an RSA key has no crv and leaves both padding and hash free, and an
EC curve does not fix the hash. alg must be fully specified per RFC
9864, so the polymorphic EdDSA identifier is rejected in favour of
Ed25519 and Ed448. A key whose kty or crv disagrees with its alg is
rejected rather than resolved in favour of either reading.

RSA is newly supported (PS256/384/512, RS256/384/512). Symmetric
material -- oct, HS*, hmac-sha256 -- is rejected outright, since every
scheme here distributes a public key. ML-DSA and the AKP key type are
recognized and declined as unsupported_algorithm rather than failing as
malformed; absence of support is a reason to decline, not a parse error.

The hwk scheme emits and requires alg, and rejects kid: the key is
inline, so an identifier selects nothing. sigkey is replaced by the
Accept-Signature-Scheme and Accept-Signature-Alg header fields, which
are Lists of Tokens and so can name more than one value.
supported_algorithms is removed from Signature-Error in favour of
Accept-Signature-Alg, which works on a challenge as well as an error.
The unsupported_scheme code is added.

The jwt scheme now validates exp, which it previously ignored entirely.
exp is what bounds how long the confirmation key an assertion carries
stays acceptable; without it that key is acceptable indefinitely.

The strictAAuth option is removed. Covering signature-key is a
requirement rather than a profile choice, because an uncovered
Signature-Key header can be substituted without invalidating the
signature, so it can no longer be disabled.

Errors are typed. SignatureVerificationError carries the Signature-Error
code directly instead of leaving it to be recovered by matching on
message text.

The RFC 9421 and Go interop vectors predate this specification and do
not cover signature-key, so they can no longer verify; their assertions
now check that they are rejected with invalid_input. Restoring
cryptographic agreement with the RFC vectors, by testing the signature
base directly, is follow-up work.

Assertion caching is deliberately not implemented: the draft carries an
Editor's Note calling it a straw man.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LXExbWHeem2SNb7tZ2LGnc
RFC 9421 Section 1.4 gives three ways to establish the signature
algorithm; Section 3.3.7 develops the second for JOSE algorithms and
states that "the explicit alg signature parameter is not used at all
when using JOSE signing algorithms". This package was already on that
path -- fetch() never emitted alg and verify() took the algorithm from
the key -- but neither was stated as deliberate, and nothing proved it.

State it in both places and cover it with tests.

The distinction that matters is between ignoring the parameter and
discarding it. alg lives inside @signature-params, which is covered by
the signature, so it must still be reproduced verbatim when the base is
reconstructed; dropping it would change the base and fail an otherwise
valid signature. Ignoring it means only that it does not select the
algorithm.

The tests sign requests by hand so the covered parameters can include an
alg the library would never emit: one declaring ed25519, one declaring
rsa-pss-sha512 against an Ed25519 key, and one declaring the symmetric
hmac-sha256. All three verify, and the algorithm used is the key's. A
verifier that honoured the parameter would attempt the wrong operation
on the second and fail. A final test signs with one key and presents
another to confirm the suite can still produce a failure, so the others
cannot pass vacuously.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LXExbWHeem2SNb7tZ2LGnc
dickhardt and others added 4 commits August 3, 2026 19:22
-07 was published to the datatracker on 2026-07-05 and was editorial --
a paragraph in the Introduction citing AAuth and Email Verification.
Everything this branch implements lands in -08.

The hwk alg parameter in particular was forbidden in -06 and remained
forbidden in -07, so the break this package straddles is -07 to -08.
Every reference saying otherwise was off by one revision.

Bumped to 2.0.0-alpha.1 rather than correcting in place, because
2.0.0-alpha.0 is published and its tarball embeds the wrong claim in
README.md and MIGRATING-2.0.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LXExbWHeem2SNb7tZ2LGnc
A verifier could not say which algorithms it accepts. It implemented
eleven and accepted all of them, so a deployment wanting Ed25519 only,
or wanting to refuse RSASSA-PKCS1-v1_5 while still implementing it, had
no way to express that. An unknown alg happened to produce
unsupported_algorithm by falling out of the lookup table rather than by
policy, and nothing ever reported what would have worked.

verify() now takes supportedAlgorithms, defaulting to everything the
library implements, exported as SUPPORTED_ALGORITHMS. A key outside the
set is rejected before any signature verification -- there is no point
verifying with an algorithm that will be declined either way -- and the
set comes back on the result as acceptSignatureAlg for the caller to
send in an Accept-Signature-Alg response header.

It travels on the result rather than inside SignatureError deliberately:
-08 removed the supported_algorithms member from Signature-Error, and
what a verifier accepts now belongs in Accept-Signature-Alg. A test
asserts it does not appear in the error.

Also covered the rule that a verifier resolving a key from a JWKS must
select by kid without requiring any other member to be usable. The
library already behaved this way -- keys.find() only reads kid -- but
nothing proved it, and it is one refactor away from being lost by
someone validating inside the predicate. Tests place an unparseable
ML-DSA key before and after the usable one, and confirm that selecting
the ML-DSA key itself declines cleanly rather than crashing.

Each JWKS test uses its own issuer origin. The JWKS cache is
module-level and keyed by URL, so sharing one origin serves a previous
test's keys.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LXExbWHeem2SNb7tZ2LGnc
draft-hardt-httpbis-signature-key-08 requires the metadata document at
{id}/.well-known/{dwk} to carry an issuer member equal to the identity
it was fetched under. This library followed jwks_uri without checking.

Without the check, a document served at that location -- through
misconfigured shared hosting, a subdomain takeover, or any other means
-- can point jwks_uri at keys that do not belong to id, and the verifier
attributes the request to the identity in the header. It is the same
check RFC 8414 Section 3.3 requires of authorization server metadata.

Reject a document with no issuer as issuer_missing, and one whose
issuer differs from id as issuer_mismatch, both new error codes in -08.
The comparison is byte equality as presented, with no normalization, so
a trailing slash is a different identifier and is rejected; a test
pins that.

Two existing jwks_uri fixtures served metadata without an issuer and
now fail, correctly. Added it to them.

This is the only normative change in -08 the library had not tracked.
The rest of the revision was already implemented on this branch before
it merged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LXExbWHeem2SNb7tZ2LGnc
alpha.1 predates supportedAlgorithms, the JWKS tolerance tests, and the
discovery issuer verification of -08.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LXExbWHeem2SNb7tZ2LGnc
@dickhardt
dickhardt merged commit 3a21937 into main Aug 5, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant