Skip to content

feat: fetch response headers, Accept-Language, and ResourceClient.UseDPoPNonce - #691

Merged
osanderson merged 2 commits into
mainfrom
feat/fetch-headers-nonce
Oct 20, 2026
Merged

osanderson merged 2 commits into
mainfrom
feat/fetch-headers-nonce

Conversation

@osanderson

Copy link
Copy Markdown
Collaborator

Lets a wallet use the DPoP nonce an OpenID4VCI Nonce Response carries, and ask for issuer metadata in the user's language. Both changes are additive; nothing breaks.

Why

DPoP nonce. OpenID4VCI 1.0 §7.2 lets the Credential Issuer's Nonce Response carry a DPoP-Nonce header, which "the Wallet uses ... when presenting an access token at the Credential Endpoint". A FAPIgo-based wallet couldn't do that, for two reasons:

  • fapihttp never returned response headers, so the only way to read one was to bypass its hardening.
  • ResourceClient had no way to take a nonce it was handed elsewhere: seeding its cache meant copying the unexported key format.

So the first Credential Request was always challenged with use_dpop_nonce and retried.

Accept-Language. OpenID4VCI 1.0 §12.2.2 recommends sending Accept-Language on the issuer-metadata request, so display metadata comes in the user's language. fapihttp had no way to send it.

Changes

  • fapihttp.FetchResponse.Header: the response's header fields, the final response's after any redirects, as a copy. Filled by both Fetch and Post.
  • FetchRequest.AcceptLanguage and PostRequest.AcceptLanguage: sent as Accept-Language (RFC 9110 §12.5.4), under Accept's rules: printable ASCII, at most 1024 bytes.
    • It's a named field rather than a general header map. The headers fapihttp sends, and the absence of the rest (cookies, credentials), are part of what it hardens.
  • client.ResourceClient.UseDPoPNonce(ctx, resource, nonce): the next Do to that origin presents the nonce.

Testing

  • fapihttp:
    • Accept-Language is sent on every redirect hop and on a POST;
    • Header holds the final response's headers, as a copy;
    • an Accept-Language with CRLF, or over 1024 bytes, is refused before any request.
  • client: after UseDPoPNonce, the first Do presents the nonce and isn't challenged; a stale one gets one challenge and a retry.
  • Checks:
    • go vet, go test ./..., and go test -race on Go 1.26.9 for fapihttp and client;
    • golangci-lint (0 issues);
    • arm vet;
    • every demo module.

🤖 Generated with Claude Code

osanderson and others added 2 commits October 20, 2026 10:24
…nguage

FetchResponse had only the body and status code, so a caller couldn't
read a header a server sends with its answer without bypassing
fapihttp's hardening: the DPoP-Nonce an OpenID4VCI Nonce Response
carries (OpenID4VCI 1.0 §7.2, RFC 9449 §8), say. FetchResponse.Header
now holds the response's header fields, the final response's after any
redirects, as a copy.

FetchRequest and PostRequest gain AcceptLanguage, sent as the
Accept-Language header (RFC 9110 §12.5.4): OpenID4VCI 1.0 §12.2.2
recommends a Wallet send it when fetching Credential Issuer Metadata,
so display metadata comes in the user's language. It follows Accept's
rules (printable ASCII, at most 1024 bytes). It's a named field rather
than a header map: the headers fapihttp sends, and the absence of the
rest (cookies, credentials), are part of what it hardens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpenID4VCI 1.0 §7.2 lets a Credential Issuer's Nonce Response carry a
DPoP nonce in its DPoP-Nonce header, which "the Wallet uses ... when
presenting an access token at the Credential Endpoint". The Nonce
Endpoint isn't called through ResourceClient, so its nonce never
reached Do, whose first request was challenged with use_dpop_nonce.
Seeding the nonce cache by hand meant copying its unexported key
format.

UseDPoPNonce has the next Do to a resource's origin present a nonce it
was given, through the nonce cache every client has had since it
became the default. A wrong or stale nonce costs only the challenge and
retry Do already handles. It does nothing under SenderConstrainMTLS.

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

codecov Bot commented Oct 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.10345% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
client/resource.go 50.00% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@sonarqubecloud

Copy link
Copy Markdown

@osanderson
osanderson merged commit 60fdc7c into main Oct 20, 2026
19 checks passed
@osanderson
osanderson deleted the feat/fetch-headers-nonce branch October 20, 2026 03:17
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