Skip to content

crypto: length-prefix ECDH-1PU cc_tag + dual-KEK fallback (0.5.0) - #3

Merged
stormer78 merged 1 commit into
mainfrom
fix/ecdh-1pu-kdf-length-prefix
May 31, 2026
Merged

crypto: length-prefix ECDH-1PU cc_tag + dual-KEK fallback (0.5.0)#3
stormer78 merged 1 commit into
mainfrom
fix/ecdh-1pu-kdf-length-prefix

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

Problem

The ECDH-1PU Concat KDF (src/concat-kdf.js) fed the content-encryption tag (cc_tag) into SuppPrivInfo raw, without the 32-bit big-endian length prefix that every other OtherInfo field carries. The comment even documented this as intentional ("the convention shared by affinidi-messaging-didcomm, go-jose, jwx").

That convention was a bug. It matched the then-buggy affinidi-messaging-didcomm (our roundtrip-rust vectors were generated against it), so JS↔Rust authcrypt worked because both sides were wrong — but neither interoperated with credo-ts or didcomm-python. The Rust crate fixed this in 0.14 (its issue #322); this is the matching JS fix.

Fix

  • Length-prefix cc_tag per the ECDH-1PU draft (draft-madden-jose-ecdh-1pu, Appendix B): uint32_be(len) || tag. ECDH-1PU+A256KW authcrypt is now spec-correct and interoperable with credo-ts / didcomm-python / affinidi-messaging-didcomm ≥ 0.14. Affects X25519 and P-256; anoncrypt (ECDH-ES) was never affected.
  • Dual-KEK decrypt fallback in unpack(): derive the spec-correct KEK first, and on AES-KW unwrap failure retry with the legacy (unprefixed-tag) KEK — so an upgraded recipient still reads authcrypt from a not-yet-upgraded peer. The result carries legacyKekUsed (a migration signal).
  • concat-kdf / ecdh-1pu gain a legacy / legacyRawSuppPrivInfo option that drives the fallback (and lets tests synthesise a pre-0.5 envelope). Production pack is always spec-correct.

Tests

  • Length-prefix KAT (concat-kdf: prefixed ≠ raw, and prefixed == raw-with-pre-prefixed-tag).
  • End-to-end fallback: a legacy-packed JWE decrypts with legacyKekUsed === true; a spec-correct JWE decrypts with legacyKekUsed === false.
  • Full suite: 195 pass / 7 skipped (the roundtrip-rust tests skip — they need a Rust helper binary).

Migration ⚠️ (breaking authcrypt wire change)

A 0.5 sender's authcrypt cannot be decrypted by an un-upgraded ≤ 0.4.x recipient. Upgrade recipients before senders — the dual-KEK fallback makes upgraded recipients accept both old and new senders. Pair with affinidi-messaging-didcomm ≥ 0.14.

Follow-up: regenerate the roundtrip-rust interop vectors against a Rust helper built from affinidi-messaging-didcomm ≥ 0.14 (until then those tests stay skipped, or would fail against an old 0.13 helper).

The ECDH-1PU Concat KDF fed the content-encryption tag (cc_tag) into
SuppPrivInfo RAW, without the 32-bit big-endian length prefix that
every other OtherInfo field carries. This matched the then-buggy
affinidi-messaging-didcomm (our roundtrip-rust vectors were generated
against it), so JS<->Rust authcrypt worked because BOTH were wrong —
but neither interoperated with credo-ts / didcomm-python.

Length-prefix cc_tag per the ECDH-1PU draft (Appendix B), making
ECDH-1PU+A256KW authcrypt spec-correct. Affects X25519 and P-256;
anoncrypt (ECDH-ES) was never affected.

To stay interoperable during rollout, unpack() now derives the
spec-correct KEK first and, on AES-KW unwrap failure, retries with the
legacy (unprefixed-tag) KEK, surfacing `legacyKekUsed` as a migration
signal. `concat-kdf` and `ecdh-1pu` gain a `legacy` /
`legacyRawSuppPrivInfo` option that drives the fallback (and lets tests
synthesise a pre-0.5 envelope).

Tests: length-prefix KAT (correct != legacy), end-to-end fallback for a
legacy-packed JWE, and spec-correct does-not-use-fallback. Full suite
195 pass / 7 skipped (roundtrip-rust needs a Rust helper).

BREAKING (authcrypt wire): a 0.5 sender's authcrypt cannot be decrypted
by an un-upgraded <= 0.4.x recipient. Upgrade recipients before senders
(the fallback makes upgraded recipients bilingual). Pair with
affinidi-messaging-didcomm >= 0.14; regenerate the roundtrip-rust
vectors against a didcomm >= 0.14 helper.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 merged commit 14acf27 into main May 31, 2026
1 of 2 checks passed
@stormer78
stormer78 deleted the fix/ecdh-1pu-kdf-length-prefix branch May 31, 2026 17:18
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