HPKE `session_info` requires the session that only decryption can reveal
Nobody has claimed this yet.
- Dominant language
- Shell
- Stars
- 112
- Forks
- 38
- Avg merge
- 12d 19h
- Merged PRs (30d)
- 4
Description
Summary
§8.3.1 makes session_info (client_id ‖ nonce ‖ responseUri) the HPKE info parameter. Decryption therefore requires knowing which session the response belongs to. The only session identifier the response carries, state, is inside the ciphertext.
This is new in 1.1. In 1.0 — and in 1.1 when ECDH-ES is used — decryption needs only the Verifier's private key, so state can be read out of the plaintext afterwards. Making decryption session-bound inverts that dependency.
This means a Verifier that worked against 1.0 breaks the moment it accepts an HPKE response, which is why I think it is worth treating as a compatibility issue rather than only an editorial one.
The flow
What the Verifier issues, and what comes back:
Verifier Wallet
| |
| (1) Authorization Request |
|--------------------------------------------------------------->|
| client_id \ |
| nonce | the three inputs to session_info |
| response_uri / |
| client_metadata.jwks (encryption keys) |
| |
| (2) Authorization Response |
|<---------------------------------------------------------------|
| POST body: response=<JWE> ...and nothing else |
The Verifier is not missing the values — it issued all three itself in (1). It cannot tell which (1) this (2) is answering.
For direct_post the body carries state in the clear, so the match is trivial:
vp_token=%7B%22my_credential%22%3A%5B%22eyJhbGci…%22%5D%7D&state=3b9404401ea74da7
For direct_post.jwt there is nothing to correlate on:
response=eyJhbGciOiJIUEtFLTAiLCJraWQiOiJUT2VuVU5VY0pY…
Why 1.0 was fine
§8.3 gives the Verifier exactly one affordance for finding what it needs:
If the selected public key contains a
kidparameter, the JWE MUST include the same value in thekidJWE Header Parameter … This enables the Verifier to easily identify the specific public key that was used to encrypt the response.
With ECDH-ES that is sufficient, because the private key is the only input to decryption:
receive response (JWE)
│ kid → private key
▼
decrypt
│ plaintext
▼
{ vp_token, state }
│ state
▼
session identified
1.1 kept that kid sentence unchanged, but HPKE added a second prerequisite — the session — and kid identifies a key, not a session.
Why 1.1 + HPKE is circular
decrypt ─────needs info────▶ session_info
▲ │
│ │ needs to know which session
inside the ciphertext ▼
│ session
state ◀────identified by──────────┘
Every entry point into the loop requires one of the others.
Workarounds, none of them written down
- A distinct
response_uriper transaction — the session is then identified by the URL. This is what we implemented. It also strengthens the binding, sinceresponse_uriis part ofsession_info. - A distinct encryption key per session, identified by
kid. Stretcheskidbeyond the role §8.3 gives it, and costs a keypair per presentation. - Trial decryption against every pending session. O(n) HPKE operations per response; not viable at scale.
Interaction with #708
#708 ("Better guidance how the Verifier matches the incoming direct_post request with the user session") was resolved by making state RECOMMENDED, and the discussion explicitly set aside the approach that is the only workable one under HPKE:
I've seen people putting identifiers in the response URI, but I think the state is a much clearer way to do this, so I would recommend state.
The timing suggests the two threads simply did not meet: #692 (defining the HPKE info parameter) closed 2026-04-26, and #708 closed 2026-05-07.
So the current text points implementers at state, which cannot work for direct_post.jwt with HPKE.
This gets worse under the HAIP 1.1 direction
OpenID4VC-HAIP PR #362 (open, resolving HAIP
#356 and
#357) splits HAIP 1.1 into four profiles:
Redirects and DC API, each with ECDH-ES or HPKE. An ecosystem picks one; the two encryption
mechanisms do not coexist within a profile.
In the "OpenID for Verifiable Presentations via Redirects & Response Encryption using HPKE" profile,
every presentation is HPKE. There is no request in that profile whose state the Verifier can
read before decrypting.
Under plain 1.1 an implementer can sidestep this by offering ECDH-ES keys. In that profile they
cannot. So this is not an edge case there — it is every transaction.
HAIP also defers the contents of session_info to VP 1.1 (from #357: "Define contents of info in
VP 1.1 so we don't need a new parameter"), so unless VP addresses it, neither document does.
Suggestion
Add to §8.3.1 that when HPKE is used the Verifier needs a way to identify the session before decrypting, and name at least one workable mechanism. §14.3.2 and §13.3 already discuss protecting the Response URI and using unique URIs, so a cross-reference from §8.3.1 may be enough — but as written, neither section connects that to the decryption ordering.
It may also be worth revisiting #708's guidance so it distinguishes the encrypted case.
If a normative requirement feels too strong, even a non-normative note naming the per-transaction
response_uri approach would save every implementer of the HAIP HPKE profile from rediscovering
this.
Related
This is the operational half of an asymmetry that also has a security half: session_info is fed to
HPKE's info parameter and has no ECDH-ES equivalent, so the fail-closed property §8.3.1 describes
only holds for HPKE.
HAIP addresses the security half by separating the two into different profiles, so they never
coexist — see HAIP #356, where the call notes HPKE "may be more secure (because of the 'info'
parameter) … but that diminishes if you have to implement both". That resolution does not help the
ordering problem above, which is why I am raising this one on its own.
Context
Found while implementing §8.3 / §8.3.1 end to end — a Go wallet encrypting and a TypeScript verifier decrypting, across HPKE-0, HPKE-1, HPKE-2, HPKE-3, HPKE-4 and HPKE-7, checked against the test vectors published with draft-ietf-jose-hpke-encrypt.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with §§8.3 and 8.3.1, then compare the response-URI guidance in §§13.3 and 14.3.2 with the session-matching discussion in issue #708. Review the HPKE test vectors mentioned in the report and the related HAIP references. Done means the specification explains how a verifier identifies the session before HPKE decryption, or clearly records the remaining design decision.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, typescript
- Domain
- backend-api-design, cryptography, security
- Issue type
- Documentation
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100