Skip to content

fix(server)!: attestation x5c key bounds, HAIP anchor interop, resource and embedder-binding fixes - #775

Merged
osanderson merged 8 commits into
mainfrom
fix/attestation-standards
Nov 2, 2026
Merged

osanderson merged 8 commits into
mainfrom
fix/attestation-standards

Conversation

@osanderson

Copy link
Copy Markdown
Collaborator

Attestation, resource indicator and embedder-binding fixes from the 2026-11-02 review.

Breaking (UPGRADING.md ## v0.69.0):

  • Attestation x5c RSA keys are bounded. X5CAttesterChain path-built over the caller's x5c certificates before anything in the request was authenticated, and crypto/x509 bounds neither an RSA modulus nor its exponent: a 31,744-bit modulus with e = 2^31-1 cost about 60 ms of CPU per unauthenticated token request. Every x5c certificate's key is now held to the existing caller-key limits from internal/jose (at most 4096 bits, exponent 65537) before isSelfSigned or chain verification. Configured anchors are not limited. Other certificate paths were checked: mTLS client authentication only verifies a single leaf against configured pools, federation and JWK x5c are only thumbprinted, and JWS x5c headers aren't used for key resolution.
  • IssueAccessToken / IssueRefreshToken refuse edited embedder inputs. TokenBinding now keeps an unexported copy of the thumbprint it verified, and AttestedClient of the attestation expiry, so an edited Thumbprint or AttestationExpiresAt is refused, as the doc comments already said. The exported fields are unchanged.

Non-breaking:

  • x5c intermediate that is also an anchor. HAIP 1.0 §4.4.1 puts the "exclude the trust anchor" rule on the wallet. A conformant x5c = [leaf, intermediate] was refused once the server also listed the intermediate as an anchor. An attestation is now refused only when every verified chain ends at an anchor x5c itself carries (same subject + SPKI comparison as before). A self-signed certificate below the leaf is still refused. The native wallet guide is corrected, and the v0.66.0 and v0.68.0 upgrade notes each get a short "relaxed in v0.69.0" note.
  • Attestation alongside client_assertion. This is still refused (RFC 6749 §2.3). The docs now say that draft-12 §7.6's use of an attestation as an additional signal isn't supported, and the error_description says the attestation can't be combined with another client authentication method.
  • Resource indicators. An empty resource= is treated as omitted (RFC 6749 §3.1/§3.2) at PAR and the token endpoint, including next to non-empty values. With resource indicators off, resource is ignored however many times it appears. Before, two copies gave invalid_request.
  • Claims value/values equality. Numbers are compared exactly by the value their text denotes, not via float64, so integers above 2^53 that differ no longer match.
  • Docs. Config.Extensions and Config.RAR now say unregistered parameters are ignored and stripped, not rejected.

Regression tests cover each item and fail without the fix.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.36620% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/claimvalues/claimvalues.go 91.30% 2 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

osanderson and others added 8 commits November 2, 2026 14:48
X5CAttesterChain parsed the certificates of an attestation's x5c and
path-built over them before anything in the request was authenticated.
crypto/x509 bounds neither an RSA modulus nor its exponent, so a
certificate with a 31,744-bit modulus and exponent 2^31-1 cost about
60 ms of CPU per unauthenticated token request.

Every x5c certificate's key is now held to the limits v0.63.0 set for
any other RSA key a caller supplies, at most 4096 bits and exponent
65537, through the new internal jose.ValidateCallerCertificateKey,
before isSelfSigned or chain verification uses it. Configured anchors
are not limited.

BREAKING CHANGE: X5CAttesterChain refuses with invalid_client an
attestation whose x5c carries an RSA key over 4096 bits or with an
exponent other than 65537, on the signing certificate or an
intermediate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HAIP 1.0 §4.4.1 has the wallet leave the trust anchor out of x5c; it
asks nothing of the verifier. X5CAttesterChain refused any x5c that
carried a certificate for the anchor a verified chain ended at, so a
conformant x5c [leaf, intermediate] verified against anchors {root} but
was refused once the server also listed the intermediate as an anchor,
which a wallet has no way to know.

An attestation is now refused only when every verified chain ends at an
anchor x5c carries itself (compared by subject and public key, as
before); one chain ending at an anchor the wallet didn't send is enough.
A self-signed certificate below the leaf is still refused. The native
wallet guide no longer says HAIP requires the server to refuse, and the
v0.66.0 and v0.68.0 upgrade notes say what changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… auth method

A request carrying attestation headers together with a client_assertion
or client_assertion_type stays refused with invalid_request (RFC 6749
§2.3), so an assertion naming another client is never ignored in
favour of the attestation. The attestation-based client authentication
draft-12 §7.6 also allows an attestation as an additional signal beside
another method; that use is not supported, and the docs now say so:
Config.AttestationBasedClientAuthentication, authenticateClient and the
native wallet guide. The error_description now says the attestation
can't be combined with another client authentication method, rather
than only that the request uses more than one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hen unsupported

RFC 6749 §3.1 and §3.2: "Parameters sent without a value MUST be
treated as if they were omitted from the request". An empty "resource"
parameter at PAR or the token endpoint was refused with invalid_target;
it is now dropped, alone or beside non-empty ones, as every other empty
parameter already is. TokenEndpointRequest.Resources drops it too.

With resource indicators disabled (neither Config.Resources nor a
ResourcePolicy), one "resource" was ignored but a repeated one was
refused with invalid_request as a duplicate parameter. RFC 8707 §2 lets
a client repeat it, and an unrecognized parameter is ignored (RFC 6749
§3.1, §3.2), so it is now ignored however many times it appears.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… expiry

IssueAccessToken documented that it accepts only a TokenBinding
VerifyTokenRequestBinding returned, but checked only the client it was
verified for: an embedder that changed the exported Thumbprint got an
access token whose cnf named a key that never proved possession. In the
same way, an AttestedClient whose exported AttestationExpiresAt was
moved on passed the check that refuses a Client kept from an earlier
request.

TokenBinding now keeps an unexported copy of the thumbprint it
verified, and AttestedClient of the expiry it verified. IssueAccessToken
refuses a binding whose Thumbprint differs, and IssueAccessToken and
IssueRefreshToken refuse a Client whose AttestationExpiresAt differs and
judge the attestation's expiry by the verified copy. The exported
fields are unchanged.

BREAKING CHANGE: IssueAccessToken refuses with server_error a
TokenBinding whose Thumbprint was changed after VerifyTokenRequestBinding
returned it, and IssueAccessToken and IssueRefreshToken refuse an
AttestedClient whose AttestationExpiresAt was changed after
AuthenticateAttestedClient returned it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Config.Extensions said a parameter without a registered Definition is
rejected, and Config.RAR described the same default-reject stance. The
server ignores and strips such a parameter, as RFC 6749 §3.1 requires
and extension.Registry.Parse documents. Only "authorization_details"
is refused without Config.RAR, with invalid_authorization_details.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OIDC Core §5.5.1 releases a claim requested with "value" or "values"
only when it equals one of them. internal/claimvalues decoded both
sides into float64, so integers above 2^53 that differ compared equal,
and a claim was released for a value the client didn't request.

Numbers are now compared by the value their text denotes, reduced to
sign, significant digits and a big.Int decimal exponent, so "100",
"1e2" and "100.0" stay equal while 9007199254740993 and
9007199254740992 differ, without expanding a large exponent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The binding_message section's code block shared its closing fence with
the next section's when both were added under v0.69.0, and lost it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@osanderson
osanderson force-pushed the fix/attestation-standards branch from 66c5d1b to a1248f3 Compare November 2, 2026 06:49
@sonarqubecloud

sonarqubecloud Bot commented Nov 2, 2026

Copy link
Copy Markdown

@osanderson
osanderson merged commit 78c6fde into main Nov 2, 2026
27 checks passed
@osanderson
osanderson deleted the fix/attestation-standards branch November 2, 2026 06:59
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