crypto: length-prefix ECDH-1PU cc_tag + dual-KEK fallback (0.5.0) - #3
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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(ourroundtrip-rustvectors 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
cc_tagper the ECDH-1PU draft (draft-madden-jose-ecdh-1pu, Appendix B):uint32_be(len) || tag.ECDH-1PU+A256KWauthcrypt 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.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 carrieslegacyKekUsed(a migration signal).concat-kdf/ecdh-1pugain alegacy/legacyRawSuppPrivInfooption that drives the fallback (and lets tests synthesise a pre-0.5 envelope). Productionpackis always spec-correct.Tests
concat-kdf: prefixed ≠ raw, and prefixed == raw-with-pre-prefixed-tag).legacyKekUsed === true; a spec-correct JWE decrypts withlegacyKekUsed === false.roundtrip-rusttests 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-rustinterop vectors against a Rust helper built fromaffinidi-messaging-didcomm≥ 0.14 (until then those tests stay skipped, or would fail against an old 0.13 helper).