decentralized-identity / decentralized-identity/veramo
`packDIDCommMessage` omits the required `apv` protected header (anoncrypt and authcrypt)
- Dominant language
- TypeScript
- Stars
- 543
- Forks
- 137
- Avg merge
- 12m
- Merged PRs (30d)
- 2
Description
### Describe the bug
`packDIDCommMessage()` never computes `apv`. The JWE protected header of both
anoncrypt and authcrypt envelopes contains only `alg`, `typ`, `epk`, `enc`
(plus `skid` for authcrypt). Strict DIDComm v2 implementations treat `apv` as
a required protected-header field and reject the envelope during parsing,
before any decryption is attempted.
### What the spec says
DIDComm v2, [ECDH-ES section](https://identity.foundation/didcomm-messaging/spec/v2.0/#ecdh-es-key-wrapping-and-common-protected-headers)
(same definition in the ECDH-1PU section):
> `apv`: this represents the recipients' `kid` list. The list must be
> alphanumerically sorted, `kid` values will then be concatenated with a `.`
> and the final result MUST be base64 URL (no padding) encoding of the SHA256
> hash of concatenated list.
Reference implementations agree: in didcomm-rust the field is non-optional —
`ProtectedHeader.apv` is a plain `&str` (not `Option`) in
[`src/jwe/envelope.rs`](https://github.com/sicpa-dlab/didcomm-rust/blob/main/src/jwe/envelope.rs),
and `ParsedJWE::verify_didcomm()` in
[`src/jwe/parse.rs`](https://github.com/sicpa-dlab/didcomm-rust/blob/main/src/jwe/parse.rs)
recomputes the hash from the recipient kids and rejects the message on
mismatch ("APV mismatch").
### Observed behavior
Sending an anoncrypt message (`ECDH-ES+A256KW`, tested with both `A256GCM`
and `XC20P`) packed by `packDIDCommMessage()` to a didcomm-rust-based
receiver fails with HTTP 400:
```
DIDCommMalformed: Malformed: Unable parse protected header: missing field `apv` at line 1 column 166
```
Re-packing the same message with `apv = base64url(sha256(sortedKids.join('.')))`
added to the protected header *and* passed into the encrypters is accepted
(HTTP 202), and the resulting envelope also passes didcomm-rust's
`verify_didcomm()` check locally.
### Where in the code
- `packages/did-comm/src/didcomm.ts` — `packDIDCommMessageJWE()` has the full
recipient list before creating encrypters, but never computes `apv`; the
encrypter factory calls (e.g. `xc20pAnonEncrypterX25519WithA256KW(recipient.publicKeyBytes, recipient.kid)`)
omit the optional `apv` argument.
- The low-level layer is already prepared: every anon encrypter in
`packages/did-comm/src/encryption/a256kw-encrypters.ts` and
`xc20pkw-encrypters.ts` accepts `apv?: string` and forwards it to the
Concat KDF via `createFullEncrypter()`.
### Suggested fix
After collecting recipients in `packDIDCommMessageJWE()`:
```ts
const kids = recipients.map((r) => r.kid).sort()
const apv = bytesToBase64url(sha256(stringToUtf8Bytes(kids.join('.'))))
protectedHeader = { ...protectedHeader, apv }
// and pass `apv` to each encrypter so it participates in the KDF
```
Backwards compatibility: did-jwt ≥ 7.x merges the protected header into each
recipient header before KEK computation on decrypt (`decryptJWE`), so older
Veramo receivers can still decrypt envelopes that carry `apv`.
Contributor guide
Assessment
This issue has not been assessed yet.