diff --git a/CLAUDE.md b/CLAUDE.md index 23db2a40..59e260f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,22 +32,16 @@ Rules that bite hardest here: issue rather than absorbing the old shape here. This is also why a request to another repo should not ask for a deprecation window on this repo's behalf. - **The one exception is TSP Rev 2, and the reason it is one is worth reading - before citing it for anything else.** The rule above rests on "our peers cut - over with us", which is true of the VTA and the mediator and false of the - Trust Spanning Protocol: it is a ToIP specification with implementations - nobody here controls, and the reference `tsp_sdk` shipped Rev 3 in 0.10.0 - while others are still on Rev 2. So `@openvtc/vti-tsp-js` **reads** both and - **packs** only Rev 3. That asymmetry is what keeps it from being the pattern - this rule bans: there is no dual-accept *arm* anywhere on the Rev 3 path, no - `if (rev === 2)` branch, and no negotiation. Rev 2 is a frozen decode-only - codec in its own directory, reached by dispatching on a version marker the - wire actually carries — and it is deleted whole, with its arm of the - dispatcher, the day the last Rev 2 peer is gone. A fold you can delete in one - `rm` is not a fold. **Nothing else in this repo has that shape**; if you are - reaching for this paragraph to justify a second version arm, the honest test - is whether a peer you do not control is on the far side, and for the VTA and - the mediator the answer is no. + **TSP is not an exception either.** The Trust Spanning Protocol is a ToIP + specification with implementations nobody here controls, and + `@openvtc/vti-tsp-js` once kept a frozen decode-only Rev 2 codec for exactly + that reason. It was deleted when the specification released 1.0 + (`YTSP-BAA`, October 2026): the package now speaks 1.0 only and refuses every + other MAJOR — Rev 2 and Rev 3 included — as `E_TSP_REVISION`, in step with + affinidi-tsp 0.3 and the reference `tsp_sdk` 0.16. MAJOR is the + specification's own "not interoperable" signal, so honouring it is the + protocol, not a fold. Do not reintroduce a draft reader; a lagging TSP peer + upgrades. - **R3.7 — match errors on stable machine-readable codes, never on strings, and parse error *bodies* before throwing on status.** Any condition this diff --git a/package-lock.json b/package-lock.json index bb83f1bc..3aee19f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7981,7 +7981,7 @@ "@noble/curves": "^2.4.0", "@openvtc/trust-tasks": "^0.27.1", "@openvtc/vti-didcomm-js": "^0.12.0", - "@openvtc/vti-tsp-js": "^0.3.0", + "@openvtc/vti-tsp-js": "^0.4.0", "@scure/base": "^2.2.0", "cbor-x": "^1.6.6" }, @@ -8055,7 +8055,7 @@ }, "packages/tsp-js": { "name": "@openvtc/vti-tsp-js", - "version": "0.3.0", + "version": "0.4.0", "license": "Apache-2.0", "dependencies": { "@noble/ciphers": "^2.0.0", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index a174bbc3..d8c801cc 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -36,6 +36,13 @@ For history before this file, see `git log` on `packages/core`. ### Changed +- **TSP 1.0 (breaking on the wire).** Requires `@openvtc/vti-tsp-js` ^0.4.0, + which speaks TSP 1.0 (`YTSP-BAA`) and refuses the Rev 2 and Rev 3 drafts as + `E_TSP_REVISION`. The TSP channel and inbound path therefore cannot exchange + messages with a VTA on `affinidi-tsp` 0.2.x or earlier; the VTA moves to + `affinidi-tsp` 0.3 (`affinidi-messaging-sdk` 0.39) in the same cutover. + `unpack` no longer takes the sender's X25519 key, which only Rev 2 used. + - **What the executors push and reply is verified under `authentication`.** `verifyTrustTaskProof` with `expectedProofPurpose` now also requires the verification method to be listed under that relationship in the signer's DID diff --git a/packages/core/package.json b/packages/core/package.json index e1441b74..f21190da 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -144,7 +144,7 @@ "@noble/curves": "^2.4.0", "@openvtc/trust-tasks": "^0.27.1", "@openvtc/vti-didcomm-js": "^0.12.0", - "@openvtc/vti-tsp-js": "^0.3.0", + "@openvtc/vti-tsp-js": "^0.4.0", "@scure/base": "^2.2.0", "cbor-x": "^1.6.6" }, diff --git a/packages/core/src/vta/tsp-channel.ts b/packages/core/src/vta/tsp-channel.ts index dec1663e..bbe51dff 100644 --- a/packages/core/src/vta/tsp-channel.ts +++ b/packages/core/src/vta/tsp-channel.ts @@ -17,12 +17,10 @@ // packed bytes is an injected `TspTransport` (mediator-backed in production, a // simulator in tests). // -// **We send spec Rev 3 and read Rev 3 or Rev 2.** The revision is not -// negotiated and cannot be: an inbound message carries a version marker that -// says what it is, an outbound one has nothing to read. So `unpack` here still -// passes the VTA's X25519 public key — which Rev 3 ignores and Rev 2 needs to -// open a message at all — and `pack` does not. A VTA still on affinidi-tsp -// 0.1.x cannot read what we send; that is the cutover, not a bug. +// **We speak TSP 1.0 (`YTSP-BAA`) and nothing else.** The version is not +// negotiated: a message from a VTA still on a draft (affinidi-tsp 0.2.x or +// earlier) is refused by `unpack` as `E_TSP_REVISION`, and that VTA refuses +// ours. The VTA and this wallet cut over together. import { pack, unpack } from "@openvtc/vti-tsp-js"; @@ -94,7 +92,6 @@ export function openReplyOnce( try { reply = await unpack(bytes, { receiverDecryptionKey: holder.encryptionPrivateKey, - senderEncryptionKey: vta.encryptionPublicKey, senderSigningKey: vta.signingPublicKey, }); } catch (err) { @@ -129,15 +126,13 @@ export interface TspHolderIdentity { signingPrivateKey: Uint8Array; /** X25519 private key — decrypts replies sealed to us. * - * Under Rev 3 that is all it does: HPKE-Base does not put the sender's key - * in the KEM, so this is no longer half of our outbound authenticity. */ + * That is all it does: HPKE-Base does not put the sender's key in the KEM, + * so it is no part of our outbound authenticity. */ encryptionPrivateKey: Uint8Array; /** X25519 public key. * - * A Rev 3 counterparty never needs it — it is kept because it is the key a - * peer resolves from our DID, and because a Rev 2 peer's `unpack` cannot - * open our messages without it. Rev 2 is read-only here, so nothing in this - * package sends under it. */ + * The key a peer resolves from our DID and seals its replies to. Nothing + * here uses it to open a message. */ encryptionPublicKey: Uint8Array; } diff --git a/packages/core/src/vta/tsp-inbound.ts b/packages/core/src/vta/tsp-inbound.ts index c3b42ebf..f414df0d 100644 --- a/packages/core/src/vta/tsp-inbound.ts +++ b/packages/core/src/vta/tsp-inbound.ts @@ -117,7 +117,6 @@ export async function unpackInboundTsp( try { opened = await unpack(bytes, { receiverDecryptionKey: opts.holder.encryptionPrivateKey, - senderEncryptionKey: sender.encryptionPublicKey, senderSigningKey: sender.signingPublicKey, }); } catch (err) { diff --git a/packages/core/src/vta/tsp-relationship.ts b/packages/core/src/vta/tsp-relationship.ts index 5dc3f9d8..c733160d 100644 --- a/packages/core/src/vta/tsp-relationship.ts +++ b/packages/core/src/vta/tsp-relationship.ts @@ -1,10 +1,10 @@ -// Forming the TSP relationship a Rev 3 peer requires before it will accept +// Forming the TSP relationship a TSP peer requires before it will accept // anything (7.2.2). // -// Rev 3 turned relationships from bookkeeping into a precondition: "if an -// endpoint receives an application message destined to one of its legitimate -// VIDs, but it has not established a relationship from the source VID in the -// message to its own VID, it SHOULD drop the message." The message is dropped, +// Rev 3, and 1.0 after it, turned relationships from bookkeeping into a +// precondition: "if an endpoint receives an application message destined to +// one of its legitimate VIDs, but it has not established a relationship from +// the source VID in the message to its own VID, it SHOULD drop the message." The message is dropped, // not refused -- nothing comes back -- so a wallet that skips this does not get // an error, it gets a 30-second timeout and a transport that looks broken. // @@ -290,7 +290,7 @@ export type InboundControlOutcome = * and `resolveCancel` ignores a cancellation naming a relationship we do not * hold — answering that would let anyone probe which relationships we hold. * An invite is ignored: this wallet initiates its relationships and does not - * take inbound ones, and a Rev 3 peer may send application messages over an + * take inbound ones, and a TSP peer may send application messages over an * invite it is still waiting on (§3.6), so ignoring it loses nothing. */ export async function applyInboundControl( diff --git a/packages/core/tests/tsp.channel.mjs b/packages/core/tests/tsp.channel.mjs index 37948d97..90ad35bb 100644 --- a/packages/core/tests/tsp.channel.mjs +++ b/packages/core/tests/tsp.channel.mjs @@ -45,7 +45,6 @@ function simulatedVtaTransport(vta, holder, dispatch, replySenderVid) { async sendAndAwaitReply(packed, options = {}) { const req = await unpack(packed, { receiverDecryptionKey: vta.encSk, - senderEncryptionKey: holder.encPk, senderSigningKey: holder.signPk, }); assert.equal(req.sender, holder.vid); @@ -77,7 +76,6 @@ function simulatedVtaTransport(vta, holder, dispatch, replySenderVid) { // what's exercised, not a crypto failure. const sealed = await pack(utf8.encode(wrapTspEnvelope(replyDoc)), replySenderVid ?? vta.vid, holder.vid, { senderSigningKey: vta.signSk, - senderEncryptionKey: vta.encSk, receiverEncryptionKey: holder.encPk, }); // Honour the transport contract: the channel decides which frame is its diff --git a/packages/core/tests/tsp.inbound.mjs b/packages/core/tests/tsp.inbound.mjs index 9cd19425..6e15dba5 100644 --- a/packages/core/tests/tsp.inbound.mjs +++ b/packages/core/tests/tsp.inbound.mjs @@ -61,7 +61,6 @@ async function sealed(payload, { from = executor, senderVid = from.vid } = {}) { holder.vid, { senderSigningKey: from.signSk, - senderEncryptionKey: from.encSk, receiverEncryptionKey: holder.encPk, }, ); diff --git a/packages/core/tests/vta.outbound-signing.mjs b/packages/core/tests/vta.outbound-signing.mjs index a21424d1..316d56a4 100644 --- a/packages/core/tests/vta.outbound-signing.mjs +++ b/packages/core/tests/vta.outbound-signing.mjs @@ -177,7 +177,6 @@ test("TSP: the sealed document carries a proof, distinct from the outer signatur async sendAndAwaitReply(bytes, options = {}) { const opened = await unpack(bytes, { receiverDecryptionKey: vtaEncSk, - senderEncryptionKey: x25519.getPublicKey(holderEncSk), senderSigningKey: ed25519.getPublicKey(holderSignSk), }); // Opened through the binding: what the wallet seals is the envelope, and @@ -200,7 +199,6 @@ test("TSP: the sealed document carries a proof, distinct from the outer signatur holderVid, { senderSigningKey: vtaSignSk, - senderEncryptionKey: vtaEncSk, receiverEncryptionKey: x25519.getPublicKey(holderEncSk), }, ); diff --git a/packages/tsp-js/CHANGELOG.md b/packages/tsp-js/CHANGELOG.md index e99e6867..da06a722 100644 --- a/packages/tsp-js/CHANGELOG.md +++ b/packages/tsp-js/CHANGELOG.md @@ -8,8 +8,47 @@ For history before this file, see `git log` on `packages/tsp-js`. ## [Unreleased] +## [0.4.0] — TSP 1.0 + +**Breaking on the wire and in the API.** Moves to the released TSP 1.0 +specification +([tswg-tsp-specification@ccda6b3](https://github.com/trustoverip/tswg-tsp-specification/commit/ccda6b33159d860f05c328eb436dabe83548b705), +PR #86), in step with `affinidi-tsp` 0.3.0 and the ToIP reference `tsp_sdk` +0.16.0. A peer still on Rev 3 or Rev 2 cannot exchange messages with this +release in either direction; upgrade both ends together. + +### Changed + +- Packs `YTSP-BAA` (1.0), was `YTSP-AAC` (0.2, Rev 3). `TSP_VERSION` is + `{ major: 1, minor: 0 }`; `SUPPORTED_MAJOR` is 1. +- `unpack`, `decodeEnvelope` and `peekRevision` refuse every MAJOR but 1 with + `TspRevisionError` (`E_TSP_REVISION`), naming the version found — including + Rev 3 (`AAC`), its pre-merge draft (`ABA`) and Rev 2 (`AAB`), all of which + this package used to read. +- A payload whose ESSR sender field is the NULL VID is refused: 1.0 requires + the sender's VID in every confidential payload (§3.2, §3.7 step 7), where + Rev 3 allowed `4BAA` under HPKE-Base. Packing is unchanged — this package + always wrote the VID. +- Appendix A vectors replaced with 1.0's (`tests/fixtures/spec-v1-vectors.json`, + identical to `affinidi-tsp` 0.3.0's). The new `control-rfi-referral` vector + is opened and its `Signature_new` verified through `referralSignedData`. +- The Rust interop vector is now one `affinidi-tsp` 0.3.0 packed, and a message + packed here was opened by that crate with a matching thread digest. +- `src/rev3/` is `src/v1/`. + +### Removed + +- The Rev 2 reader (`src/rev2/`), its dispatcher arm, and with them: + `UnpackKeys.senderEncryptionKey`, the `revision` field on `UnpackedMessage` + and `DecodedEnvelope`, the `Revision` type, `PeekedRevision.revision`, + `KNOWN_MINORS` (now `KNOWN_VERSIONS`, keyed `"MAJOR.MINOR"`), and the + `cesr` exports `REV2_MINOR`, `LONG_COUNT_REV2`, `LONG_COUNT_REV3`, + `TSP_HPKEAUTH_CIPHERTEXT` and `TSP_TMP`. `decodeCount` no longer takes a + long-form selector. + ### Added +- `describeVersion(major, minor)`. - `resolveAccept(state, answeredDigest, ourInviteDigest)`: whether a received accept answers the invite we have outstanding (§7.2.2). `transition` sees only the state, so until now every client had to compare the digests itself @@ -17,22 +56,13 @@ For history before this file, see `git log` on `packages/tsp-js`. - `@openvtc/vti-tsp-js/unsafe-testing`, a **test-only** subpath for byte-reproducible packing: `__unsafeDeterministicPack`, `…PackInvite`, `…PackAccept`, `…PackCancel`, `…PackNested` and `…PackRouted` take an - `__unsafeIkmE` (RFC 9180 DeriveKeyPair input) for the HPKE-Base ephemeral and - can write the NULL VID in the ESSR sender field. A fixed ephemeral key breaks - confidentiality; the subpath exists so the Appendix A vectors can be - reproduced and is not part of the documented API. The main entry point's - packers are unchanged and never take either knob. With it, all six HPKE-Base - vectors (`direct-hpke-base`, `control-rfi-direct`, `control-rfa-direct`, - `control-rfd`, `nested-direct`, `routed`) re-pack byte for byte. - -### Changed - -- The Appendix A test vectors are the merged specification's - ([tswg-tsp-specification@f5b8668](https://github.com/trustoverip/tswg-tsp-specification/commit/f5b8668952aabe8e541b535fcbdf589484ffc4f4)), - which carry `YTSP-AAC` — the marker this package packs — in place of the - pre-merge `YTSP-ABA` set. Every message and the control vectors' digests - changed; all still open and verify. A pre-merge `ABA` message stays pinned in - the tests: reading it is unchanged. Test-only; no library behaviour changed. + `__unsafeIkmE` (RFC 9180 DeriveKeyPair input) for the HPKE-Base ephemeral. A + fixed ephemeral key breaks confidentiality; the subpath exists so the + Appendix A vectors can be reproduced and is not part of the documented API. + The main entry point's packers are unchanged and never take the knob. With + it, the six HPKE-Base vectors this package can pack (`direct-hpke-base`, + `control-rfi-direct`, `control-rfa-direct`, `control-rfd`, `nested-direct`, + `routed`) re-pack byte for byte. ### Fixed diff --git a/packages/tsp-js/README.md b/packages/tsp-js/README.md index f1e18499..ab94b8e6 100644 --- a/packages/tsp-js/README.md +++ b/packages/tsp-js/README.md @@ -18,32 +18,26 @@ it natively; on React Native, import [`react-native-get-random-values`](https://github.com/LinusU/react-native-get-random-values) once at app startup. -## Revisions: packs Rev 3, reads Rev 3 and Rev 2 +## Version: TSP 1.0 -The specification's **Rev 3** changed the crypto mode, the version byte, the -long count-code prefix, the ciphertext code and layout, the `-E` count's -meaning, the signature code and every payload layout — at once. Nothing a Rev 2 -peer packs can be unpacked by a Rev 3 one or the reverse, and there is no -negotiation in the protocol. +This package speaks the released **TSP 1.0** specification (`YTSP-BAA`) and +nothing else, in step with [`affinidi-tsp`](https://crates.io/crates/affinidi-tsp) +0.3 and the ToIP reference `tsp_sdk` 0.16. -So this package is deliberately asymmetric: +`unpack` and `decodeEnvelope` read the version marker first — it sits at a fixed +offset and needs no keys — and refuse any MAJOR but 1 with a `TspRevisionError` +(`code: "E_TSP_REVISION"`) that names the version found. That includes the two +drafts: Rev 2 (`YTSP-AAB`, 0.1) and Rev 3 (`YTSP-AAC`, 0.2). There is no +negotiation in the protocol; a peer still on a draft has to upgrade. -- **`unpack` dispatches on the version marker**, which sits at a fixed offset - and needs no keys. A Rev 2 message is read by a frozen, decode-only codec in - `src/rev2/`, and the result reports `revision: "rev2"`. -- **`pack` does not dispatch on anything**, because there is nothing to - dispatch on. An inbound message says what it is; an outbound one has to be - decided before a byte exists, and no field on the wire says what a peer can - read. We pack Rev 3. A Rev 2 peer cannot read it, and nothing here retries or - falls back. - -`unpack`'s `revision` is how a caller learns what a peer actually speaks. -Remembering that per peer belongs above this package — a codec has no business -holding state about who it has talked to. +1.0 is Rev 3's wire with one payload rule changed: the ESSR sender field must +carry the sender's VID in every confidential payload, where Rev 3 let HPKE-Base +write the NULL VID `4BAA`. This package always wrote it; on receive, a NULL +sender is now refused. ## Relationships -Rev 3 §7.2.2: an endpoint SHOULD **drop** an application message from a VID it +§7.2.2: an endpoint SHOULD **drop** an application message from a VID it holds no relationship with. Dropped, not refused — nothing comes back — so a client that skips the handshake sees a timeout, not an error. @@ -89,36 +83,35 @@ package owns the wire layer — CESR encode/decode, the `-E` envelope, HPKE seal/open, Ed25519 sign/verify, and `pack`/`unpack` for Direct, Nested, and Routed messages. -- **HPKE-Base** (Rev 3) — RFC 9180, `DHKEM(X25519, HKDF-SHA256)` + - `HKDF-SHA256` + `ChaCha20Poly1305`. The fixed code `YTSP-` is the HPKE - `info`, and `TSP_Version ‖ VID_sndr ‖ VID_rcvr` is real AAD. The sender's - key no longer enters the KEM — sender authenticity is the ESSR sender field - plus the outer signature. **HPKE-Auth** remains for reading Rev 2, where the - envelope frame was the `info` and the AAD was empty. +- **HPKE-Base** — RFC 9180, `DHKEM(X25519, HKDF-SHA256)` + `HKDF-SHA256` + + `ChaCha20Poly1305`. The fixed code `YTSP-` is the HPKE `info`, and + `TSP_Version ‖ VID_sndr ‖ VID_rcvr` is real AAD. The sender's key does not + enter the KEM — sender authenticity is the ESSR sender field plus the outer + signature. - **CESR** — binary `qb2` framing (selectors `-E`, `-Z`, `-A`, `-J`, `B`, `F`, `I`; markers `YTSP`, `XSCS`/`XHOP`, `XRFI`/`XRFA`/`XRFD`/`XCTL`/`XPAD`). - **Message modes** — Direct, Nested (metadata privacy), and Routed (multi-hop through a relay/mediator). - **Relationships** — `XRFI` / `XRFA` / `XRFD`, the §7.2.1 self-addressing - digest, and the §7.2/§7.3 state machine. Rev 3 gates application messages on + digest, and the §7.2/§7.3 state machine. TSP gates application messages on a relationship, so this is a precondition for sending anything, not an optional extra. Byte-compatibility is proven in three ways, because a round trip proves none of it — encoder and decoder agree with each other whatever they both get wrong, -which is exactly the failure mode Rev 3's one-character changes produce: +which is exactly the failure mode one-character wire changes produce: -- **The specification's own Appendix A vectors** (`YTSP-AAC`, as merged at - [tswg-tsp-specification@f5b8668](https://github.com/trustoverip/tswg-tsp-specification/commit/f5b8668952aabe8e541b535fcbdf589484ffc4f4)) +- **The specification's own Appendix A vectors** (`YTSP-BAA`, TSP 1.0 at + [tswg-tsp-specification@ccda6b3](https://github.com/trustoverip/tswg-tsp-specification/commit/ccda6b33159d860f05c328eb436dabe83548b705)) run as a test suite, fixed and external and produced by the ToIP reference - implementation. Every HPKE-Base vector both opens - (`tests/interop.spec-vectors.mjs`, `tests/control.spec-vectors.mjs`) and - **re-packs byte for byte** from its published `ikmE` - (`tests/interop.spec-vectors-repack.mjs`). Every published vector is either - exercised or named as uncovered, so the list cannot quietly shrink. -- **Both directions against `affinidi-tsp`** — its Rev 2 vector unpacks here - (`tests/interop.rust-vector.mjs`), and a message packed here unpacks there, - thread digest included. + implementation. Every HPKE-Base vector opens + (`tests/interop.spec-vectors.mjs`, `tests/control.spec-vectors.mjs`), and + every one this package can pack **re-packs byte for byte** from its published + `ikmE` (`tests/interop.spec-vectors-repack.mjs`). Every published vector is + either exercised or named as uncovered, so the list cannot quietly shrink. +- **Both directions against `affinidi-tsp` 0.3** — a message it packed unpacks + here (`tests/interop.rust-vector.mjs`), and a message packed here unpacks + there, thread digest included. - **Pinned bytes** for the deterministic parts of what we emit, since the sealed message itself is not reproducible (HPKE draws a fresh ephemeral key). The vector re-pack fixes that key through a test-only subpath, @@ -151,28 +144,22 @@ const packed = await pack(payloadBytes, senderDid, recipientDid, { receiverEncryptionKey, // X25519 public — HPKE-Base recipient (seal to) }); // packed.bytes: the qb2 TSP message. First byte 0xF8, or 0xFB past ~12 KB — -// Rev 3's `-E` count covers the ciphertext, so large messages are long-framed. +// The `-E` count covers the ciphertext, so large messages are long-framed. // `isTsp()` accepts both; a classifier that knows only 0xF8 silently drops them. const msg = await unpack(packed.bytes, { receiverDecryptionKey, // X25519 private — our key senderSigningKey, // Ed25519 public — outer-signature verification - senderEncryptionKey, // X25519 public — OPTIONAL, Rev 2 only (see below) }); -// msg.sender / msg.receiver (proven VIDs), msg.payload, and msg.revision. +// msg.sender / msg.receiver (proven VIDs), msg.payload. ``` -`senderEncryptionKey` is Rev 2's alone: HPKE-Auth puts the sender's static key -in the KEM, so without it a Rev 2 message cannot be *opened*, let alone -verified. Omit it and a Rev 2 message is refused by name rather than by a -decryption failure. Rev 3 has no use for it at all. - ```ts import { peekRevision, isRevisionError } from "@openvtc/vti-tsp-js"; -// Keyless, from the version marker — for routing, metrics, or deciding whether -// you hold the extra key a Rev 2 message needs. -const { revision, minor } = peekRevision(bytes); // "rev3" | "rev2" +// Keyless, from the version marker. Throws TspRevisionError for any MAJOR but +// 1 — e.g. a peer still on a draft — before a key is touched. +const { major, minor } = peekRevision(bytes); // 1, 0 ``` Multi-hop routing (seal end-to-end to the final recipient, wrap a routing layer @@ -189,9 +176,9 @@ import { packRouted } from "@openvtc/vti-tsp-js"; | `pack` / `unpack` | Direct message seal+sign / verify+open | | `packWithHops` | Lower-level pack with an explicit hop list | | `packRouted` / `packNested` / `nextHop` | Routed (multi-hop) + Nested (metadata-privacy) messages | -| `decodeEnvelope` | The `-E` cleartext envelope, dispatching on revision — keyless, for relays | -| `peekRevision` / `describeRevision` | Which revision framed a message, from its version marker alone | -| `TspRevisionError` / `isRevisionError` | A revision that could not be established or is not spoken; `code: "E_TSP_REVISION"` | +| `decodeEnvelope` | The `-E` cleartext envelope of a 1.0 message — keyless, for relays | +| `peekRevision` / `describeRevision` / `describeVersion` | The version a message declares, from its marker alone; refuses any MAJOR but 1 | +| `TspRevisionError` / `isRevisionError` | A version that could not be established or is not spoken; `code: "E_TSP_REVISION"` | | `isTsp` | Ingress classifier — accepts both `0xF8` and `0xFB` framings | | `sha256` | Thread-digest helper | | `cesr` | Binary CESR frame primitives | @@ -212,7 +199,7 @@ omission: | The libsodium sealed box (§8.3) | §8 tells new implementations not to use it. A `C`-coded ciphertext is *recognised* and refused by name, so it never reads as a corrupt `F`. | | Composing a referral (§7.2.5) | A referral's `Signature_new` covers the invite's digest, so composing one needs the *introduced* VID's signing key at pack time. A wallet holding another VID's private key is not a shape this package should invite. Referrals are decoded and exposed unverified, with `referralSignedData` for a caller that can resolve `VID_new` and check. | | Fillable padding (§7.5) | The field is always written, always empty. Conformant, and leaves the traffic-analysis defence unimplemented rather than half-implemented. | -| Packing Rev 2 | See *Revisions* above. | +| Reading or packing the Rev 2 / Rev 3 drafts | See *Version* above. | ## Test diff --git a/packages/tsp-js/package.json b/packages/tsp-js/package.json index ebac1667..7e48ae32 100644 --- a/packages/tsp-js/package.json +++ b/packages/tsp-js/package.json @@ -1,7 +1,7 @@ { "name": "@openvtc/vti-tsp-js", - "version": "0.3.0", - "description": "Pure-TypeScript TSP (Trust Spanning Protocol) primitives \u2014 byte-compatible with affinidi-tsp. Packs spec Rev 3 (HPKE-Base on RFC 9180 + binary CESR framing), reads Rev 3 and Rev 2. No WebCrypto, no WASM: runs in browsers, Node, and React Native.", + "version": "0.4.0", + "description": "Pure-TypeScript TSP (Trust Spanning Protocol) primitives \u2014 byte-compatible with affinidi-tsp. Speaks TSP 1.0 (HPKE-Base on RFC 9180 + binary CESR framing). No WebCrypto, no WASM: runs in browsers, Node, and React Native.", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/tsp-js/src/cesr/wire.ts b/packages/tsp-js/src/cesr/wire.ts index 7353354f..92e76c40 100644 --- a/packages/tsp-js/src/cesr/wire.ts +++ b/packages/tsp-js/src/cesr/wire.ts @@ -16,18 +16,12 @@ // - count code `encodeCount(id, count)` — a `-`-framed group header // carrying a quadlet count. // -// ── Two revisions ── +// ── One version ── // -// The frame *primitives* — fixed data, variable data, the short count code — -// are identical in spec Rev 2 and Rev 3, so they live here once. Exactly two -// things in this file are revision-dependent, and both are decode-side only -// because this package packs Rev 3 and nothing else (see `../revision.ts`): -// -// 1. The long count code's second selector: Rev 2 spelled it `-0X#####`, -// Rev 3 spells it `--X#####`. `decodeCount` takes the form to expect. -// 2. The code table. Rev 3 struck HPKE-Auth's `G` ciphertext and the `X` -// trailing marker and added `F`, `C`, `-A`, `XCTL` and `XPAD`. Both sets -// are named below; the revision modules pick. +// This package speaks TSP 1.0 and nothing else (see `../revision.ts`). The +// code table is the one Rev 3 introduced and 1.0 kept: Rev 2's HPKE-Auth `G` +// ciphertext, its `X` trailing marker and its `-0X#####` long count are gone, +// and `F`, `C`, `-A`, `XCTL` and `XPAD` are in. // CESR base64url selector values (index of the char in the base64url alphabet). const D0 = 52; // '0' @@ -44,31 +38,14 @@ const DASH = 62; // '-' * `DATA_LIMIT = 3 * (1 << 24)`, ~48 MiB). Guards against hostile size headers. */ export const MAX_FIELD_SIZE = 3 * (1 << 24); -/** Which spelling of the *long* count code a decoder should expect. - * - * The value is the second selector of the six-byte header. Rev 2 used `0` - * (D0), from a superseded draft of the CESR v2 tables; Rev 3 pins the master - * table for genus `-_AAACAA`, which carries only the double-dash form. One - * character, and encoder and decoder agree with themselves either way — which - * is why `cesr.wire.mjs` pins the bytes rather than round-tripping them. */ -export const LONG_COUNT_REV2 = D0; -/** @see LONG_COUNT_REV2 */ -export const LONG_COUNT_REV3 = DASH; - -/** TSP version `(major, minor)` this package **packs** — Rev 3, `YTSP-AAC`. +/** TSP version `(major, minor)` this package packs and reads — 1.0, + * `YTSP-BAA`, the released specification. * - * MAJOR.MINOR, two components rather than three: MINOR occupies the whole - * 12-bit count. Pre-merge drafts of §9.1 read the three characters as MAJOR, - * MINOR, PATCH and gave `YTSP-ABA`; affinidi-tsp deliberately does not follow - * that reading, and neither do we — see that crate's `TSP_VERSION` for the - * argument. The merged specification's Appendix A vectors carry `YTSP-AAC`, - * the marker packed here. Nothing about interoperating depends on the choice: only MAJOR - * gates processability, it is the same character either way, and no - * implementation refuses a message on MINOR. */ -export const TSP_VERSION = { major: 0, minor: 2 } as const; - -/** The MINOR value Rev 2 carried (`YTSP-AAB`). Read, never written. */ -export const REV2_MINOR = 1; + * MAJOR.MINOR, two components: one character of MAJOR, then MINOR across the + * whole 12-bit count (§9.1). The drafts before it were 0.1 (Rev 2, `AAB`) and + * 0.2 (Rev 3, `AAC`); none interoperates with the next, and only MAJOR gates + * processability, so a 0.x frame is refused at the version marker. */ +export const TSP_VERSION = { major: 1, minor: 0 } as const; /** Interpret a base64url string as a big-endian integer of its 6-bit symbols. * Only used on ASCII base64url constants ≤ 4 chars (≤ 24 bits), so a JS number @@ -117,9 +94,6 @@ function triplet(stream: Uint8Array, i: number): number | undefined { export const TSP_PLAINTEXT = cesrInt("B"); // 1 export const TSP_VID = cesrInt("B"); // 1 export const ED25519_SIGNATURE = cesrInt("B"); // 1 -/** `G`: var-data HPKE-Auth ciphertext. **Rev 2 only** — Rev 3 strikes the `G` - * codes from the table along with HPKE-Auth itself. */ -export const TSP_HPKEAUTH_CIPHERTEXT = cesrInt("G"); // 6 /** `F`: var-data HPKE-Base ciphertext (Rev 3 §9.4). */ export const TSP_HPKE_BASE_CIPHERTEXT = cesrInt("F"); // 5 /** `C`: var-data libsodium sealed-box ciphertext (Rev 3 §8.3). @@ -129,11 +103,8 @@ export const TSP_HPKE_BASE_CIPHERTEXT = cesrInt("F"); // 5 * sealed box: §8 tells new implementations not to use it, and our only TSP * peers are the VTA and the mediator, which do not send one. */ export const TSP_SEALED_BOX_CIPHERTEXT = cesrInt("C"); // 2 -/** `X`: 2-byte fixed-data marker emitted after the envelope VIDs. **Rev 2 - * only** — Rev 3 deletes it and always writes the receiver-VID field. */ -export const TSP_TMP = cesrInt("X"); // 23 -/** `A`: fixed-data id for a relationship nonce (32 bytes in Rev 2, 16 in - * Rev 3 — the code follows from the payload length). */ +/** `A`: fixed-data id for a relationship nonce (16 bytes — the code follows + * from the payload length). */ export const TSP_NONCE = cesrInt("A"); // 0 /** `I`: fixed-data id for a SHA-256 digest (32 bytes). */ export const TSP_SHA256 = cesrInt("I"); // 8 @@ -236,15 +207,14 @@ export function encodeVariableData(identifier: number, payload: Uint8Array, out: /** Encode a count-code group header carrying `count` quadlets. * - * Always the Rev 3 long spelling `--X#####`, because this package packs Rev 3 - * and nothing else. A Rev 2 message is read, never written. */ + * The long spelling is `--X#####`; see {@link decodeCount}. */ export function encodeCount(identifier: number, count: number, out: number[]): void { if (count < 4096) { const word = (DASH << 18) | (bits(identifier, 6) << 12) | bits(count, 12); for (const b of beBytes(word)) out.push(b); } else { const word1 = - (DASH << 18) | (LONG_COUNT_REV3 << 12) | (bits(identifier, 6) << 6) | bits(count >>> 24, 6); + (DASH << 18) | (DASH << 12) | (bits(identifier, 6) << 6) | bits(count >>> 24, 6); const word2 = bits(count, 24); for (const b of beBytes(word1)) out.push(b); for (const b of beBytes(word2)) out.push(b); @@ -282,13 +252,15 @@ export interface Cursor { /** Decode a count-code group header for `identifier`. Advances `cur` and * returns the quadlet count, or undefined on mismatch. * - * `longForm` is the second selector to accept for the six-byte long header — - * {@link LONG_COUNT_REV3} (the default) or {@link LONG_COUNT_REV2}. */ + * The six-byte long header is `--X#####` (§9): a second DASH selector. Rev 2 + * spelled it `-0X#####`, from a superseded draft of the CESR v2 tables; that + * form is refused. One character, and encoder and decoder agree with + * themselves either way — which is why `cesr.wire.mjs` pins the bytes rather + * than round-tripping them. */ export function decodeCount( identifier: number, stream: Uint8Array, cur: Cursor, - longForm: number = LONG_COUNT_REV3, ): number | undefined { const word = triplet(stream, cur.pos); if (word === undefined) return undefined; @@ -296,7 +268,7 @@ export function decodeCount( const expected = ((DASH << 18) | (bits(identifier, 6) << 12) | bits(index, 12)) >>> 0; const expectedLong = - ((DASH << 18) | (longForm << 12) | (bits(identifier, 6) << 6) | bits(index & 0x3f, 6)) >>> 0; + ((DASH << 18) | (DASH << 12) | (bits(identifier, 6) << 6) | bits(index & 0x3f, 6)) >>> 0; if (word === expected) { cur.pos += 3; return index; diff --git a/packages/tsp-js/src/index.ts b/packages/tsp-js/src/index.ts index 8e96e421..28bdf18e 100644 --- a/packages/tsp-js/src/index.ts +++ b/packages/tsp-js/src/index.ts @@ -3,24 +3,21 @@ // on @noble. No WebCrypto dependency: runs identically in browser, Node, and // React Native (only `crypto.getRandomValues` is required of the runtime). // -// ── Revisions ── +// ── Version ── // -// This package **packs spec Rev 3** (`YTSP-AAC`: HPKE-Base, `F` ciphertext, -// ESSR sender field, `--X` long counts, indexed signatures) and **reads Rev 3 -// and Rev 2**. `unpack` dispatches on the version marker every message carries; -// `pack` has nothing to dispatch on and does not try. See `revision.ts` for why -// that asymmetry is the whole design, and `rev2/reader.ts` for what a Rev 2 -// message costs us to read. +// This package speaks **TSP 1.0** (`YTSP-BAA`: HPKE-Base, `F` ciphertext, the +// ESSR sender field carrying the sender in every confidential payload, `--X` +// long counts, indexed signatures), and refuses every other MAJOR, the Rev 2 +// and Rev 3 drafts included. See `revision.ts`. // // Layers: -// cesr/wire — binary CESR frame primitives, shared by both revisions -// revision — the keyless version-marker discriminator +// cesr/wire — binary CESR frame primitives +// revision — the keyless version gate // crypto/hpke — HPKE Base + Auth seal/open via @noble // crypto/sign — Ed25519 sign/verify via @noble // relationship — the §7.2/§7.3 state machine, pure and storage-free -// rev3/ — Rev 3 envelope, fields, payload frame, control, pack/unpack -// rev2/ — Rev 2 reader; frozen, decode-only -// message/ — the public API and the dispatcher +// v1/ — envelope, fields, payload frame, control, pack/unpack +// message/ — the public API export * as cesr from "./cesr/wire.js"; export * as hpke from "./crypto/hpke.js"; @@ -32,13 +29,13 @@ export { } from "./cesr/wire.js"; export { describeRevision, + describeVersion, isRevisionError, peekRevision, - KNOWN_MINORS, + KNOWN_VERSIONS, SUPPORTED_MAJOR, TspRevisionError, type PeekedRevision, - type Revision, } from "./revision.js"; export { decodeEnvelope, @@ -77,8 +74,8 @@ export { type RelationshipEvent, type RelationshipState, } from "./relationship.js"; -export { referralSignedData, type Referral } from "./rev3/control.js"; -export { generateNonce } from "./rev3/fields.js"; +export { referralSignedData, type Referral } from "./v1/control.js"; +export { generateNonce } from "./v1/fields.js"; export { packRouted, packNested, diff --git a/packages/tsp-js/src/message/direct.ts b/packages/tsp-js/src/message/direct.ts index ca47bd3b..b88bf409 100644 --- a/packages/tsp-js/src/message/direct.ts +++ b/packages/tsp-js/src/message/direct.ts @@ -1,97 +1,51 @@ -// The public message API: pack Rev 3, unpack either. +// The public message API: pack and unpack TSP 1.0. // -// ── The asymmetry is the design ── -// -// `unpack` dispatches on the version marker a message carries; `pack` does not -// dispatch on anything, because there is nothing to dispatch on. An inbound -// message *says* what revision it is. An outbound one has to be decided before -// a byte exists, and the wire carries no field that would tell us what a peer -// can read — so a dual *packer* could only ever be a guess dressed as a -// protocol. We pack Rev 3. -// -// What that costs is exact and worth stating plainly: a Rev 2 peer cannot read -// what we send. Nothing here softens that, retries, or falls back. What it buys -// is that the Rev 3 path has no revision branch in it anywhere — the word -// "rev2" appears in this file and in `../rev2/`, and nowhere else. -// -// `revision` on the result is how a caller learns what a peer actually speaks. -// Persisting that per peer belongs above this package: a codec has no business -// holding state about who it has talked to. +// `unpack` gates on the version marker a message carries and refuses any MAJOR +// but 1 — including the Rev 2 and Rev 3 drafts this package used to read — as +// a `TspRevisionError`, before a key is touched. `pack` has nothing to decide: +// it writes 1.0. import { sha256 } from "@noble/hashes/sha2.js"; -import { unpack as unpackRev2, type Rev2UnpackKeys } from "../rev2/reader.js"; import { - pack as packRev3, - packAccept as packAcceptRev3, - packCancel as packCancelRev3, - packInvite as packInviteRev3, - packWithHops as packWithHopsRev3, - unpack as unpackRev3, + pack as packV1, + packAccept as packAcceptV1, + packCancel as packCancelV1, + packInvite as packInviteV1, + packWithHops as packWithHopsV1, + unpack as unpackV1, type PackKeys, type PackedMessage, -} from "../rev3/direct.js"; -import type { ApplicationKind, ControlMessage, ControlType } from "../rev3/payload.js"; -import type { MessageType } from "../rev3/payload.js"; -import { describeRevision, peekRevision, TspRevisionError, type Revision } from "../revision.js"; - -export type { ApplicationKind, ControlMessage, ControlType, MessageType, PackKeys, PackedMessage }; - -/** Keys needed to unpack a message of either revision. - * - * `senderEncryptionKey` is Rev 2's alone: HPKE-Auth puts the sender's static - * key in the KEM, so a Rev 2 message cannot be *opened* without it, let alone - * verified. It is optional because the Rev 3 path has no use for it at all, - * and a required field that one whole revision ignores teaches the wrong thing - * about what authenticates a Rev 3 sender. Omit it and a Rev 2 message is - * refused by name rather than by a decryption failure. */ -export interface UnpackKeys { - /** Receiver's X25519 private key. */ - receiverDecryptionKey: Uint8Array; - /** Sender's Ed25519 public key (outer signature verification). */ - senderSigningKey: Uint8Array; - /** Sender's X25519 public key. **Rev 2 only** — HPKE-Auth sender - * authentication. */ - senderEncryptionKey?: Uint8Array; -} + type UnpackKeys, + type UnpackedMessage, +} from "../v1/direct.js"; +import type { ApplicationKind, ControlMessage, ControlType } from "../v1/payload.js"; +import type { MessageType } from "../v1/payload.js"; +import { describeRevision, describeVersion, peekRevision, TspRevisionError } from "../revision.js"; +import { TSP_VERSION } from "../cesr/wire.js"; -export interface UnpackedMessage { - /** The decrypted message body. For Direct it is the upper-layer payload; for - * Nested/Routed the opaque inner message (the route is in `hops`). */ - payload: Uint8Array; - /** Sender VID, from the cleartext envelope. */ - sender: string; - /** Receiver VID, from the cleartext envelope. */ - receiver: string; - /** The message kind recovered from the payload frame. */ - messageType: MessageType; - /** The recovered relationship-forming message, when there is one (§7.2). - * - * Rev 3 only: Rev 2 messages never decode to one here. Its self-addressing - * digest has already been verified against the frame, so a `control` that is - * present identified itself correctly — but *what to do about it* is the - * caller's, via `relationship.ts`. */ - control?: ControlMessage; - /** Remaining route for a Routed message (empty otherwise). */ - hops: string[]; - /** SHA-256 of the payload frame — the TSP thread digest. */ - threadDigest: Uint8Array; - /** Which revision framed this message. A caller that tracks what a peer - * speaks reads it here. */ - revision: Revision; -} +export type { + ApplicationKind, + ControlMessage, + ControlType, + MessageType, + PackKeys, + PackedMessage, + UnpackKeys, + UnpackedMessage, +}; -/** Pack a direct TSP message (Rev 3). */ +/** Pack a direct TSP message. */ export function pack( body: Uint8Array, senderVid: string, receiverVid: string, keys: PackKeys, ): Promise { - return packRev3(body, senderVid, receiverVid, keys); + return packV1(body, senderVid, receiverVid, keys); } -/** Pack a message of any kind (Rev 3), carrying a routing `hops` list in the +/** Pack a message of any kind, carrying a routing `hops` list in the * payload frame. `hops` must be empty for Direct/Nested. */ export function packWithHops( body: Uint8Array, @@ -101,7 +55,7 @@ export function packWithHops( receiverVid: string, keys: PackKeys, ): Promise { - return packWithHopsRev3(body, kind, hops, senderVid, receiverVid, keys); + return packWithHopsV1(body, kind, hops, senderVid, receiverVid, keys); } /** @@ -118,7 +72,7 @@ export function packInvite( keys: PackKeys, opts: { route?: string[]; nonce?: Uint8Array } = {}, ): Promise { - return packInviteRev3(senderVid, receiverVid, keys, opts); + return packInviteV1(senderVid, receiverVid, keys, opts); } /** Pack a relationship-forming accept (`XRFA`) answering `inviteDigest`. */ @@ -128,7 +82,7 @@ export function packAccept( receiverVid: string, keys: PackKeys, ): Promise { - return packAcceptRev3(inviteDigest, senderVid, receiverVid, keys); + return packAcceptV1(inviteDigest, senderVid, receiverVid, keys); } /** Pack a relationship cancellation (`XRFD`) naming either half of the @@ -139,17 +93,18 @@ export function packCancel( receiverVid: string, keys: PackKeys, ): Promise { - return packCancelRev3(relationshipDigest, senderVid, receiverVid, keys); + return packCancelV1(relationshipDigest, senderVid, receiverVid, keys); } /** - * Unpack a TSP message of either revision, dispatching on its version marker. + * Unpack a TSP 1.0 message. * - * A parse failure against a frame whose MINOR we do not recognise is re-reported - * as a {@link TspRevisionError} naming both revisions and carrying the - * underlying error. Without that, a frame from a revision we have never seen - * dies wherever its layout first disagrees with ours — which is almost never - * where the actual problem is. + * A frame at any other MAJOR is refused as a {@link TspRevisionError} naming + * both versions. A parse failure against a 1.0 frame whose MINOR we do not + * recognise is re-reported the same way, carrying the underlying error: + * otherwise a frame from a version we have never seen dies wherever its layout + * first disagrees with ours — which is almost never where the actual problem + * is. */ export async function unpack( wireBytes: Uint8Array, @@ -158,34 +113,11 @@ export async function unpack( const peeked = peekRevision(wireBytes); try { - if (peeked.revision === "rev2") { - if (keys.senderEncryptionKey === undefined) { - throw new TspRevisionError( - "tsp: message is Rev 2 (YTSP-AAB), which needs the sender's X25519 public key to open (HPKE-Auth); pass senderEncryptionKey", - peeked.major, - peeked.minor, - ); - } - const rev2Keys: Rev2UnpackKeys = { - receiverDecryptionKey: keys.receiverDecryptionKey, - senderEncryptionKey: keys.senderEncryptionKey, - senderSigningKey: keys.senderSigningKey, - }; - const out = await unpackRev2(wireBytes, rev2Keys); - return { ...out, revision: "rev2" }; - } - - const out = await unpackRev3(wireBytes, { - receiverDecryptionKey: keys.receiverDecryptionKey, - senderSigningKey: keys.senderSigningKey, - }); - return { ...out, revision: "rev3" }; + return await unpackV1(wireBytes, keys); } catch (err) { - // A revision error is already about the revision; re-wrapping would bury it. - if (err instanceof TspRevisionError) throw err; if (!peeked.recognised) { throw new TspRevisionError( - `tsp: could not parse a message declaring ${describeRevision(peeked)}; this implementation packs Rev 3 (YTSP-AAC) and reads Rev 2 (YTSP-AAB). Underlying error: ${ + `tsp: could not parse a message declaring ${describeRevision(peeked)}; this implementation speaks ${describeVersion(TSP_VERSION.major, TSP_VERSION.minor)}. Underlying error: ${ err instanceof Error ? err.message : String(err) }`, peeked.major, diff --git a/packages/tsp-js/src/message/envelope.ts b/packages/tsp-js/src/message/envelope.ts index 7cec5427..7f80b1dc 100644 --- a/packages/tsp-js/src/message/envelope.ts +++ b/packages/tsp-js/src/message/envelope.ts @@ -1,58 +1,41 @@ -// The public, revision-dispatching envelope decode. +// The public envelope decode. // // A relay routes on the cleartext envelope and never opens the message, so this -// has to work for both revisions with no keys at all. It reads the version -// marker first (`../revision.ts`) and hands the frame to the matching codec. +// works with no keys at all. It reads the version marker first +// (`../revision.ts`), refuses a MAJOR we do not speak, and hands the frame to +// the codec. // -// There is no public envelope *encode* here: we pack Rev 3 and nothing else, so -// the Rev 3 codec's `encodeFields`/`finalizeFrame` pair is the only way to -// build one, and it is deliberately not reachable through a name that suggests -// a revision-neutral envelope exists. +// There is no public envelope *encode* here: the codec's +// `encodeFields`/`finalizeFrame` pair is the only way to build one, and it is +// deliberately not reachable through a name that suggests a standalone +// envelope exists. -import { decodeEnvelope as decodeRev3 } from "../rev3/envelope.js"; -import { decodeRev2Envelope } from "../rev2/reader.js"; -import { peekRevision, type Revision } from "../revision.js"; +import { decodeEnvelope as decodeV1 } from "../v1/envelope.js"; +import { peekRevision } from "../revision.js"; export interface Envelope { sender: string; - /** Empty string is Rev 3's NULL VID (`4BAA`) — "no receiver named". Rev 2 - * had no such spelling and always names one. */ + /** Empty string is the NULL VID (`4BAA`) — "no receiver named". */ receiver: string; } export interface DecodedEnvelope { envelope: Envelope; - /** Bytes consumed by the envelope fields. - * - * The number means different things per revision and is reported for - * diagnostics, not for arithmetic across them: in Rev 2 it is the whole `-E` - * frame, which is also the HPKE `info`; in Rev 3 it is the offset at which - * the ciphertext field begins, and the AAD is those bytes minus the count - * code. Code that needs either should use the revision's own codec. */ + /** Offset at which the ciphertext field begins. The AAD is the bytes before + * it, minus the `-E` count code; code that needs it should use the codec. */ headerLen: number; - /** Which revision framed this message. */ - revision: Revision; /** MINOR as carried, unjudged. */ minor: number; } -/** Decode the cleartext envelope of a TSP message of either revision. */ +/** Decode the cleartext envelope of a TSP 1.0 message. Throws + * `TspRevisionError` for any other MAJOR. */ export function decodeEnvelope(data: Uint8Array): DecodedEnvelope { - const peeked = peekRevision(data); - if (peeked.revision === "rev2") { - const rev2 = decodeRev2Envelope(data); - return { - envelope: { sender: rev2.sender, receiver: rev2.receiver }, - headerLen: rev2.headerLen, - revision: "rev2", - minor: peeked.minor, - }; - } - const rev3 = decodeRev3(data); + peekRevision(data); + const decoded = decodeV1(data); return { - envelope: rev3.envelope, - headerLen: rev3.headerLen, - revision: "rev3", - minor: rev3.minor, + envelope: decoded.envelope, + headerLen: decoded.headerLen, + minor: decoded.minor, }; } diff --git a/packages/tsp-js/src/message/routed.ts b/packages/tsp-js/src/message/routed.ts index d1355902..7f9c86ce 100644 --- a/packages/tsp-js/src/message/routed.ts +++ b/packages/tsp-js/src/message/routed.ts @@ -6,7 +6,7 @@ // addressed intermediary reads it. Nested mode is the degenerate wrapper: an // inner packed message carried opaquely to a single intermediary. // -// Rev 3 only, like everything on the packing side. Two changes from Rev 2 reach +// Two changes Rev 3 made, and 1.0 kept, reach // callers: the inner message is carried **raw** rather than inside a `B` // var-data field, which makes it a caller error to hand one that is not // quadlet-aligned; and the `-J` count is the hop group's byte length rather diff --git a/packages/tsp-js/src/rev2/reader.ts b/packages/tsp-js/src/rev2/reader.ts deleted file mode 100644 index 13a26aca..00000000 --- a/packages/tsp-js/src/rev2/reader.ts +++ /dev/null @@ -1,212 +0,0 @@ -// Rev 2 — read only. -// -// This module decodes messages framed under spec Rev 2 and **cannot pack one**. -// That asymmetry is the whole design: a Rev 2 peer cannot read anything we -// send, so emitting Rev 2 would buy nothing and would leave a legacy encoder -// alive to rot. Reading one costs this file and ends a class of unreadable -// failure — a Rev 2 frame handed to the Rev 3 codec dies at the ciphertext -// selector with "missing F ciphertext field", which points at the crypto layer -// for a problem that is nothing of the sort. -// -// It is deliberately frozen. Nothing here should ever need to change, because -// Rev 2 is finished: when the last Rev 2 peer is gone this file and its arm of -// the dispatcher are deleted together, and nothing else moves. -// -// -E envelope (= HPKE info): YTSP · B sender · B receiver · X 00 00 -// ciphertext = ct ‖ tag(16) ‖ enc(32) -// -C22 -K22 sig(64) Ed25519 over envelope ‖ ciphertext -// -// The encrypted plaintext is itself a CESR payload frame: -// -Z [B sender-VID] XSCS body -// -// Differences from Rev 3, each of which is why this cannot be a flag on the -// Rev 3 codec: HPKE-**Auth** (so the sender's X25519 *public* key is needed to -// open at all), the envelope frame as HPKE `info` with empty AAD, `enc` at the -// tail rather than the head, the `G` ciphertext code, the `X 00 00` marker, an -// `-E` count that covers only the header, a non-indexed signature code, a `-J` -// count that counts VIDs rather than bytes, an inner message wrapped in a `B` -// field, and the long count code spelled `-0X`. - -import { sha256 } from "@noble/hashes/sha2.js"; - -import * as wire from "../cesr/wire.js"; -import * as hpke from "../crypto/hpke.js"; -import * as sign from "../crypto/sign.js"; - -const ENC_LEN = 32; -const TAG_LEN = 16; -const SIG_LEN = 64; -const SIG_QUADLETS = 22; -const EMPTY = new Uint8Array(0); - -const fromUtf8 = new TextDecoder("utf-8", { fatal: true }); - -/** Rev 2's long count code spelling, from a superseded draft of the CESR v2 - * tables. Every `decodeCount` in this file passes it. */ -const LONG = wire.LONG_COUNT_REV2; - -/** Keys needed to open a Rev 2 message. Note the third: HPKE-Auth puts the - * sender's static key in the KEM, so without the sender's X25519 **public** - * key a Rev 2 message cannot be opened at all — not merely left unverified. */ -export interface Rev2UnpackKeys { - receiverDecryptionKey: Uint8Array; - senderEncryptionKey: Uint8Array; - senderSigningKey: Uint8Array; -} - -export interface Rev2UnpackedMessage { - payload: Uint8Array; - sender: string; - receiver: string; - messageType: "direct" | "nested" | "routed"; - hops: string[]; - threadDigest: Uint8Array; -} - -function bytesEqual(a: Uint8Array, b: Uint8Array): boolean { - if (a.length !== b.length) return false; - for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; - return true; -} - -/** Decode the Rev 2 envelope, reporting the frame length — which is the HPKE - * `info` byte length, because Rev 2 bound the ciphertext by passing the whole - * envelope frame as `info`. */ -export function decodeRev2Envelope( - data: Uint8Array, -): { sender: string; receiver: string; headerLen: number } { - const cur: wire.Cursor = { pos: 0 }; - - if (wire.decodeCount(wire.TSP_ETS_WRAPPER, data, cur, LONG) === undefined) { - throw new Error("tsp(rev2): missing -E envelope wrapper"); - } - const version = wire.readVersion(data, cur); - if (version === undefined) throw new Error("tsp(rev2): missing or malformed version marker"); - - const senderBytes = wire.decodeVariableData(wire.TSP_VID, data, cur); - if (senderBytes === undefined) throw new Error("tsp(rev2): missing sender VID"); - const receiverBytes = wire.decodeVariableData(wire.TSP_VID, data, cur); - if (receiverBytes === undefined) throw new Error("tsp(rev2): missing receiver VID"); - - let sender: string; - let receiver: string; - try { - sender = fromUtf8.decode(senderBytes); - receiver = fromUtf8.decode(receiverBytes); - } catch { - throw new Error("tsp(rev2): invalid VID encoding"); - } - - // The 2-byte TMP marker, emitted unconditionally for encrypted messages. - wire.decodeFixedData(wire.TSP_TMP, 2, data, cur); - - return { sender, receiver, headerLen: cur.pos }; -} - -/** Decode a Rev 2 hop list: a `-J` group whose count is the number of VIDs. */ -function decodeHops(stream: Uint8Array, cur: wire.Cursor): Uint8Array[] { - const count = wire.decodeCount(wire.TSP_HOP_LIST, stream, cur, LONG); - if (count === undefined) throw new Error("tsp(rev2): malformed hop list"); - if (count > wire.MAX_HOPS) throw new Error("tsp(rev2): too many hops"); - const hops: Uint8Array[] = []; - for (let i = 0; i < count; i++) { - const hop = wire.decodeVariableData(wire.TSP_VID, stream, cur); - if (hop === undefined) throw new Error("tsp(rev2): malformed hop VID"); - hops.push(hop); - } - return hops; -} - -function decodePayloadFrame(frame: Uint8Array): { - kind: "direct" | "nested" | "routed"; - hops: string[]; - body: Uint8Array; -} { - const cur: wire.Cursor = { pos: 0 }; - if (wire.decodeCount(wire.TSP_PAYLOAD, frame, cur, LONG) === undefined) { - throw new Error("tsp(rev2): missing -Z payload frame"); - } - // Optional ESSR sender-VID: the reference omits it for HPKE-Auth, where the - // KEM already authenticates the sender. A non-VID marker will not match a `B` - // var-data field, so this is a tolerant skip — which is exactly what Rev 3 - // replaced with a required field and a mandatory cross-check. - wire.decodeVariableData(wire.TSP_VID, frame, cur); - - const marker = frame.slice(cur.pos, cur.pos + 3); - if (bytesEqual(marker, wire.XSCS)) { - cur.pos += 3; - const body = wire.decodeVariableData(wire.TSP_PLAINTEXT, frame, cur); - if (body === undefined) throw new Error("tsp(rev2): missing payload plaintext"); - return { kind: "direct", hops: [], body }; - } - if (bytesEqual(marker, wire.XHOP)) { - cur.pos += 3; - const hopBytes = decodeHops(frame, cur); - let hops: string[]; - try { - hops = hopBytes.map((h) => fromUtf8.decode(h)); - } catch { - throw new Error("tsp(rev2): hop VID not UTF-8"); - } - const body = wire.decodeVariableData(wire.TSP_PLAINTEXT, frame, cur); - if (body === undefined) throw new Error("tsp(rev2): missing payload plaintext"); - return { kind: hops.length === 0 ? "nested" : "routed", hops, body }; - } - throw new Error("tsp(rev2): unsupported payload type marker"); -} - -/** Unpack a Rev 2 message: verify the Ed25519 signature over - * envelope ‖ ciphertext, split `enc` off the **tail**, and HPKE-Auth open with - * the envelope as `info` and empty AAD. */ -export async function unpack( - wireBytes: Uint8Array, - keys: Rev2UnpackKeys, -): Promise { - if (wireBytes.length < 48) throw new Error("tsp(rev2): message too short"); - - const { sender, receiver, headerLen } = decodeRev2Envelope(wireBytes); - const envelopeBytes = wireBytes.slice(0, headerLen); - - const cur: wire.Cursor = { pos: headerLen }; - const ctRange = wire.decodeVariableDataRange(wire.TSP_HPKEAUTH_CIPHERTEXT, wireBytes, cur); - if (ctRange === undefined) throw new Error("tsp(rev2): missing G ciphertext frame"); - const signedEnd = cur.pos; // the signature covers envelope ‖ ciphertext - - const gLen = ctRange.end - ctRange.begin; - if (gLen > wire.MAX_FIELD_SIZE) throw new Error("tsp(rev2): ciphertext too large"); - if (gLen < ENC_LEN + TAG_LEN) throw new Error("tsp(rev2): ciphertext truncated"); - - const attach = wire.decodeCount(wire.TSP_ATTACH_GRP, wireBytes, cur, LONG); - const group = wire.decodeCount(wire.TSP_INDEX_SIG_GRP, wireBytes, cur, LONG); - if (attach !== SIG_QUADLETS || group !== SIG_QUADLETS) { - throw new Error("tsp(rev2): unexpected signature group size"); - } - const signature = wire.decodeFixedData(wire.ED25519_SIGNATURE, SIG_LEN, wireBytes, cur); - if (signature === undefined) throw new Error("tsp(rev2): missing Ed25519 signature"); - if (cur.pos !== wireBytes.length) throw new Error("tsp(rev2): trailing bytes after signature"); - if (!sign.verify(wireBytes.slice(0, signedEnd), signature, keys.senderSigningKey)) { - throw new Error("tsp(rev2): signature verification failed"); - } - - const gPayload = wireBytes.slice(ctRange.begin, ctRange.end); - const encStart = gPayload.length - ENC_LEN; - const payloadFrame = await hpke.open( - gPayload.slice(0, encStart), - EMPTY, - gPayload.slice(encStart), - keys.receiverDecryptionKey, - keys.senderEncryptionKey, - envelopeBytes, - ); - - const threadDigest = sha256(payloadFrame); - const frame = decodePayloadFrame(payloadFrame); - return { - payload: frame.body, - sender, - receiver, - messageType: frame.kind, - hops: frame.hops, - threadDigest, - }; -} diff --git a/packages/tsp-js/src/revision.ts b/packages/tsp-js/src/revision.ts index 663a218e..c3310fb6 100644 --- a/packages/tsp-js/src/revision.ts +++ b/packages/tsp-js/src/revision.ts @@ -1,63 +1,45 @@ -// Which revision of the TSP specification framed this message. +// The version gate: which TSP version framed this message, and do we speak it. // -// The whole dual-revision design rests on one property: **the revision is -// readable without any keys, at a fixed offset, before anything else is -// parsed.** A TSP message opens with the `-E` count code and then the version -// marker: +// The version is readable without any keys, at a fixed offset, before anything +// else is parsed. A TSP message opens with the `-E` count code and then the +// version marker: // // f8 40 13 -E count (short form, 3 bytes) ← or `fb …` long form, 6 bytes // 61 34 8f YTSP genus marker -// f8 00 01 version count code: MAJOR=0, MINOR=1 → Rev 2 -// f8 00 02 version count code: MAJOR=0, MINOR=2 → Rev 3 +// f8 10 00 version count code: MAJOR=1, MINOR=0 → 1.0 (`YTSP-BAA`) // -// So `peekRevision` reads at most nine bytes and never touches a key. That is -// what makes "receive both, pack one" honest rather than a guess: an inbound -// message *says* what it is, and we dispatch on what it says. +// So `peekRevision` reads at most nine bytes and never touches a key. // -// ── Why the two MINOR values are not symmetric ── +// ── One version ── // -// Rev 2 is the only MINOR we match exactly. Everything else at MAJOR 0 is read -// as Rev 3, because §9.1 makes MAJOR the field that gates processability and -// MINOR one that no implementation may refuse a message on — the ToIP reference -// discards MINOR entirely. Pre-merge drafts of Rev 3 shipped `YTSP-ABA`, which -// is MINOR 64 under this (MAJOR.MINOR) reading and MINOR 1 / PATCH 0 under the -// three-component one; the merged specification's Appendix A vectors, like -// affinidi-tsp and this package, carry `AAC` = 2. Both must parse as Rev 3, -// and so must whatever the resolution of that argument turns out to be, so -// enumerating known-good MINORs would be the wrong shape. See -// `KNOWN_MINORS` for what the list is actually for. +// This package speaks TSP 1.0 and refuses everything at another MAJOR, which +// is what §9.1 makes MAJOR for. That includes both drafts it used to read: +// Rev 2 (`YTSP-AAB`, 0.1) and Rev 3 (`YTSP-AAC`, 0.2). Rev 3 is the same wire +// as 1.0 bar one payload rule — 1.0 requires the sender VID in every +// confidential payload, where Rev 3 let HPKE-Base write the NULL VID — but a +// draft frame is refused here rather than read under rules it was not written +// to, in step with affinidi-tsp 0.3 and the ToIP reference `tsp_sdk` 0.16. // -// ── What this is not ── -// -// This is not negotiation. We pack Rev 3 unconditionally; a Rev 2 peer cannot -// read what we send, and nothing here pretends otherwise. `peekRevision` exists -// so that a Rev 2 message we are *given* is read correctly and reported as -// such, rather than dying at the ciphertext selector with "missing F -// ciphertext field" — a crypto-layer error for a problem that is nothing of the -// sort. +// A different MINOR at MAJOR 1 is carried, not refused: no implementation may +// refuse a message on MINOR. `KNOWN_VERSIONS` exists so that a refusal, or a +// later parse failure, can name what the frame declared. import * as wire from "./cesr/wire.js"; -/** A revision of the TSP specification, as carried by a message's version - * marker. */ -export type Revision = "rev2" | "rev3"; - /** The MAJOR version this package implements. MAJOR is the only component that * gates processability (§9.1). */ -export const SUPPORTED_MAJOR = 0; +export const SUPPORTED_MAJOR = wire.TSP_VERSION.major; -/** MINOR values we can name, for diagnostics only — never for admission. - * - * A MINOR absent from here still parses (as Rev 3); what the list buys is the - * ability to say "this frame declared an unrecognised MINOR" when a parse then - * fails, instead of blaming the ciphertext. */ -export const KNOWN_MINORS: Readonly> = Object.freeze({ - 1: "Rev 2 (YTSP-AAB)", - 2: "Rev 3 (YTSP-AAC)", - 64: "Rev 3 as published upstream (YTSP-ABA)", +/** Versions we can name, keyed `"MAJOR.MINOR"`, for diagnostics only — never + * for admission. */ +export const KNOWN_VERSIONS: Readonly> = Object.freeze({ + "0.1": "Rev 2 (YTSP-AAB)", + "0.2": "Rev 3 (YTSP-AAC)", + "0.64": "Rev 3 pre-merge draft (YTSP-ABA)", + "1.0": "TSP 1.0 (YTSP-BAA)", }); -/** A frame whose revision could not be established, or is not one we speak. +/** A frame whose version could not be established, or is not one we speak. * * Carries a machine-readable `code` because the caller's decision — drop, * report, or ask the peer to upgrade — must not be made by matching on a @@ -87,22 +69,31 @@ export function isRevisionError(err: unknown): err is TspRevisionError { /** What {@link peekRevision} found. */ export interface PeekedRevision { - /** Which codec should parse this frame. */ - revision: Revision; /** MAJOR as carried. Always {@link SUPPORTED_MAJOR} — a mismatch throws. */ major: number; /** MINOR as carried, unjudged. */ minor: number; - /** Whether {@link KNOWN_MINORS} names this MINOR. A `false` here is not an - * error; it is the note a later parse failure should cite. */ + /** Whether {@link KNOWN_VERSIONS} names this version. A `false` here is not + * an error; it is the note a later parse failure should cite. */ recognised: boolean; } +/** Human-readable name for a version, for error text and diagnostics. */ +export function describeVersion(major: number, minor: number): string { + return KNOWN_VERSIONS[`${major}.${minor}`] ?? `MAJOR ${major}, MINOR ${minor} (unrecognised)`; +} + +/** Human-readable name for a peeked version. */ +export function describeRevision(peeked: PeekedRevision): string { + return describeVersion(peeked.major, peeked.minor); +} + /** - * Read a message's revision from its version marker, without keys. + * Read a message's version from its marker, without keys. * * Throws {@link TspRevisionError} if the bytes are not a TSP frame at all, if - * the version marker is malformed, or if MAJOR is one we do not implement. + * the version marker is malformed, or if MAJOR is one we do not implement — + * which includes every pre-1.0 draft. */ export function peekRevision(bytes: Uint8Array): PeekedRevision { if (!wire.isTsp(bytes)) { @@ -111,8 +102,6 @@ export function peekRevision(bytes: Uint8Array): PeekedRevision { // The version marker sits immediately after the `-E` count code, whose width // is the one thing the leading byte tells us: short is 3 bytes, long is 6. - // Both revisions' long spellings lead with 0xFB, so this is revision-agnostic - // — which it has to be, since it runs before the revision is known. const cur: wire.Cursor = { pos: bytes[0] === wire.TSP_MAGIC_BYTE_LONG ? 6 : 3 }; const version = wire.readVersion(bytes, cur); @@ -121,21 +110,15 @@ export function peekRevision(bytes: Uint8Array): PeekedRevision { } if (version.major !== SUPPORTED_MAJOR) { throw new TspRevisionError( - `tsp: message declares MAJOR ${version.major}; this implementation speaks MAJOR ${SUPPORTED_MAJOR}`, + `tsp: message is ${describeVersion(version.major, version.minor)}; this implementation speaks ${describeVersion(wire.TSP_VERSION.major, wire.TSP_VERSION.minor)}`, version.major, version.minor, ); } return { - revision: version.minor === wire.REV2_MINOR ? "rev2" : "rev3", major: version.major, minor: version.minor, - recognised: Object.hasOwn(KNOWN_MINORS, version.minor), + recognised: Object.hasOwn(KNOWN_VERSIONS, `${version.major}.${version.minor}`), }; } - -/** Human-readable name for a peeked revision, for error text and diagnostics. */ -export function describeRevision(peeked: PeekedRevision): string { - return KNOWN_MINORS[peeked.minor] ?? `MAJOR ${peeked.major}, MINOR ${peeked.minor} (unrecognised)`; -} diff --git a/packages/tsp-js/src/unsafe-testing.ts b/packages/tsp-js/src/unsafe-testing.ts index 3bed4617..85f4279c 100644 --- a/packages/tsp-js/src/unsafe-testing.ts +++ b/packages/tsp-js/src/unsafe-testing.ts @@ -3,10 +3,10 @@ // // Every function here packs exactly what its namesake in the main entry point // packs, except that the HPKE-Base ephemeral key is derived from a caller-fixed -// `ikmE` (RFC 9180 §7.1.3 DeriveKeyPair) instead of drawn at random, and the -// ESSR sender field may be written as the NULL VID. Ed25519 signatures are -// already deterministic, so with those two fixed — and the invite nonce, which -// `packInvite` already takes — the whole message is reproducible byte for byte. +// `ikmE` (RFC 9180 §7.1.3 DeriveKeyPair) instead of drawn at random. Ed25519 +// signatures are already deterministic, so with that fixed — and the invite +// nonce, which `packInvite` already takes — the whole message is reproducible +// byte for byte. // // ── Why this is unsafe ── // @@ -32,7 +32,7 @@ import { type PackKeys, type PackedMessage, type UnsafeDeterministicPack, -} from "./rev3/direct.js"; +} from "./v1/direct.js"; export type { PackKeys, PackedMessage, UnsafeDeterministicPack }; diff --git a/packages/tsp-js/src/rev3/control.ts b/packages/tsp-js/src/v1/control.ts similarity index 99% rename from packages/tsp-js/src/rev3/control.ts rename to packages/tsp-js/src/v1/control.ts index 42e22fd0..8a03b59b 100644 --- a/packages/tsp-js/src/rev3/control.ts +++ b/packages/tsp-js/src/v1/control.ts @@ -6,7 +6,7 @@ // wallet that cannot send an invite cannot send anything. // // On the wire they are not a serialized body inside a generic payload. Each is -// its own CESR payload-frame variant, and like every Rev 3 layout each begins +// its own CESR payload-frame variant, and like every layout each begins // with the ESSR sender-VID field and ends with the padding field: // // Invite XRFI sndr Digest Nonce Reply_Path Referral pad @@ -164,7 +164,7 @@ function inviteDigestAfter(nonce: Uint8Array, route: string[], referral?: Referr /** * The bytes `Signature_new` is made over (§9.3): - * `{XRFI, VID_sndr | 4BAA, Digest, Nonce, Reply_Path, VID_new}`. + * `{XRFI, VID_sndr, Digest, Nonce, Reply_Path, VID_new}`. * * Exported so a caller that has resolved `VID_new` can verify a referral it was * sent — see {@link Referral.signature} for why this cannot happen during diff --git a/packages/tsp-js/src/rev3/direct.ts b/packages/tsp-js/src/v1/direct.ts similarity index 93% rename from packages/tsp-js/src/rev3/direct.ts rename to packages/tsp-js/src/v1/direct.ts index 2d3229a8..6f1c1ca4 100644 --- a/packages/tsp-js/src/rev3/direct.ts +++ b/packages/tsp-js/src/v1/direct.ts @@ -1,13 +1,15 @@ -// Rev 3 pack and unpack — the revision this package speaks. +// TSP 1.0 pack and unpack — the only version this package speaks. // // -E one frame; the count covers everything below -// YTSP `YTSP-AAC` +// YTSP `YTSP-BAA` // sender-VID // receiver-VID `4BAA` when absent // enc ‖ ct HPKE-Base ciphertext, AEAD tag inside ct // -C23 -K22 B0 sig(64) indexed Ed25519 signature over the above // -// ── What moved from Rev 2 ── +// 1.0 is Rev 3's wire with the sender VID required in every confidential +// payload. What Rev 3 moved from Rev 2, and so what this codec has never +// spoken: // // * **HPKE-Base, not HPKE-Auth.** The sender's KEM key no longer participates, // so packing and unpacking each take one fewer key than they did. Sender @@ -106,17 +108,8 @@ export interface UnsafeDeterministicPack { /** RFC 9180 §7.1.3 `DeriveKeyPair` input for the HPKE-Base ephemeral — the * `ikmE` Appendix A prints. At least 32 bytes. */ __unsafeIkmE: Uint8Array; - /** Write the NULL VID `4BAA` in the ESSR sender field instead of the sender's - * VID. §9.2 permits it under HPKE-Base, and the published vectors use it; - * this package's own stance is to always write the VID (see `payload.ts`), - * so it is offered only here, for reproducing those vectors. */ - nullPayloadSender?: boolean; } -/** The ESSR sender field's content for this pack. */ -const payloadSender = (senderVid: string, unsafe?: UnsafeDeterministicPack): string => - unsafe?.nullPayloadSender === true ? "" : senderVid; - /** Encode the signature attachment: `-C23 -K22 B0 sig(64)`. */ function encodeSignatureFrame(signature: Uint8Array, out: number[]): void { wire.encodeCount(wire.TSP_ATTACH_GRP, ATTACH_GROUP_QUADLETS, out); @@ -186,7 +179,7 @@ async function sealFrame( return new Uint8Array(out); } -/** Pack a Rev 3 application message of any kind. */ +/** Pack an application message of any kind. */ export async function packWithHops( body: Uint8Array, kind: ApplicationKind, @@ -197,7 +190,7 @@ export async function packWithHops( unsafe?: UnsafeDeterministicPack, ): Promise { const fields = encodeFields(senderVid, receiverVid); - const { frame, threadDigest } = encodePayloadFrame(body, kind, hops, payloadSender(senderVid, unsafe)); + const { frame, threadDigest } = encodePayloadFrame(body, kind, hops, senderVid); return { bytes: await sealFrame(fields, frame, keys, unsafe), threadDigest }; } @@ -218,7 +211,7 @@ export async function packControl( unsafe?: UnsafeDeterministicPack, ): Promise { const fields = encodeFields(senderVid, receiverVid); - const { frame, threadDigest } = encodeControlFrame(control, payloadSender(senderVid, unsafe), fields); + const { frame, threadDigest } = encodeControlFrame(control, senderVid, fields); return { bytes: await sealFrame(fields, frame, keys, unsafe), threadDigest }; } @@ -292,7 +285,7 @@ export function packCancel( ); } -/** Pack a Rev 3 direct message. */ +/** Pack a direct message. */ export function pack( body: Uint8Array, senderVid: string, @@ -303,7 +296,8 @@ export function pack( return packWithHops(body, "direct", [], senderVid, receiverVid, keys, unsafe); } -/** Unpack a Rev 3 message. */ +/** Unpack a TSP 1.0 message. The caller has already gated the version + * (`../revision.ts`). */ export async function unpack( wireBytes: Uint8Array, keys: UnpackKeys, diff --git a/packages/tsp-js/src/rev3/envelope.ts b/packages/tsp-js/src/v1/envelope.ts similarity index 97% rename from packages/tsp-js/src/rev3/envelope.ts rename to packages/tsp-js/src/v1/envelope.ts index d621fcb1..7bb6c5fd 100644 --- a/packages/tsp-js/src/rev3/envelope.ts +++ b/packages/tsp-js/src/v1/envelope.ts @@ -1,4 +1,4 @@ -// The Rev 3 `-E` envelope frame (spec Rev 3 §9.1). +// The `-E` envelope frame (§9.1). // // The envelope is the cleartext outer frame: TSP version, sender VID, receiver // VID. Two things changed from Rev 2 and both reach right through the packing @@ -72,7 +72,7 @@ export function finalizeFrame(fields: Uint8Array, body: Uint8Array): Uint8Array return new Uint8Array(out); } -/** Decode a Rev 3 envelope and report the offsets needed to open and verify +/** Decode an envelope and report the offsets needed to open and verify * the message. Throws on a malformed frame. */ export function decodeEnvelope(data: Uint8Array): DecodedEnvelope { const cur: wire.Cursor = { pos: 0 }; diff --git a/packages/tsp-js/src/rev3/fields.ts b/packages/tsp-js/src/v1/fields.ts similarity index 96% rename from packages/tsp-js/src/rev3/fields.ts rename to packages/tsp-js/src/v1/fields.ts index d8fdeead..4d97a24f 100644 --- a/packages/tsp-js/src/rev3/fields.ts +++ b/packages/tsp-js/src/v1/fields.ts @@ -1,11 +1,11 @@ -// Rev 3 payload *field* codecs — the pieces every payload layout is built from. +// Payload *field* codecs — the pieces every payload layout is built from. // // These live apart from both `payload.ts` (which dispatches on the type code) // and `control.ts` (which composes the relationship-forming layouts) because // both need them and neither owns them. Splitting them out is also what keeps // the two from importing each other in a cycle. // -// §9.2 gives every Rev 3 layout the same skeleton — type code, ESSR sender VID, +// §9.2 gives every layout the same skeleton — type code, ESSR sender VID, // type-specific fields, padding — so most of what differs between a generic // message and an invite is which of these appear and in what order. @@ -33,7 +33,7 @@ export function generateNonce(): Uint8Array { return nonce; } -/** The ESSR sender-VID field. Always written; the NULL VID is the empty string. */ +/** The ESSR sender-VID field. Always the sender's VID: 1.0 requires it in every confidential payload. */ export function encodeSenderField(senderVid: string, out: number[]): void { wire.encodeVariableData(wire.TSP_VID, utf8.encode(senderVid), out); } diff --git a/packages/tsp-js/src/rev3/payload.ts b/packages/tsp-js/src/v1/payload.ts similarity index 88% rename from packages/tsp-js/src/rev3/payload.ts rename to packages/tsp-js/src/v1/payload.ts index 0569d95b..f5c6b38b 100644 --- a/packages/tsp-js/src/rev3/payload.ts +++ b/packages/tsp-js/src/v1/payload.ts @@ -1,6 +1,6 @@ -// The Rev 3 CESR payload frame — the plaintext that gets sealed (§9.2/§9.3). +// The CESR payload frame — the plaintext that gets sealed (§9.2/§9.3). // -// Every Rev 3 layout has the same shape: type code, ESSR sender VID, +// Every layout has the same shape: type code, ESSR sender VID, // type-specific fields, padding. Rev 2 had none of the first two and none of // the last. // @@ -17,13 +17,13 @@ // // ── The ESSR sender field ── // -// Rev 3 moved sender authenticity out of the KEM: HPKE-Base does not -// authenticate a sender, so the binding is the AAD plus this field plus the -// outer signature. Under HPKE-Base the field MAY be the NULL VID; when it is -// not, it MUST equal the envelope sender, and §3.7 step 7 has the receiver -// check exactly that. We always write it and always check it — the spec's own -// security considerations note the two bindings are then independent, which is -// the argument for not resting sender authenticity on one mechanism. +// HPKE-Base does not authenticate a sender, so the binding is the AAD plus +// this field plus the outer signature. 1.0 requires the field to carry the +// sender's VID in every confidential payload, and §3.7 step 7 has the receiver +// check it against the envelope; Rev 3 let HPKE-Base write the NULL VID +// instead. Every payload this package reads is confidential, so a NULL sender +// field is refused outright. The two bindings are independent, which is the +// argument for not resting sender authenticity on one mechanism. // // ── Padding ── // @@ -72,7 +72,7 @@ export interface DecodedFrame { /** The plaintext body: the upper-layer payload for Direct, the raw inner * message for Nested/Routed, empty for a control or padding frame. */ body: Uint8Array; - /** The ESSR sender VID as carried, or `""` for the NULL VID. */ + /** The ESSR sender VID as carried. Never the NULL VID: decoding refuses it. */ senderVid: string; /** The thread digest: SHA-256 over the whole `-Z` frame for an application * message, and the carried `TSP_Digest` for a control one. */ @@ -154,9 +154,10 @@ export function encodeControlFrame( /** * Decode a payload frame. * - * `envelopeSender` is checked against the ESSR sender field: a non-NULL field - * that disagrees with the envelope is a message claiming two senders, which is - * a verification failure and not a parse one. `envelopeFields` is needed to + * `envelopeSender` is checked against the ESSR sender field, which must name + * it: a NULL field is a confidential payload without its sender (§3.7 step 7), + * and one that disagrees is a message claiming two senders. Both are + * verification failures, not parse ones. `envelopeFields` is needed to * recompute a control message's self-addressing digest. */ export function decodePayloadFrame( @@ -177,7 +178,7 @@ export function decodePayloadFrame( const typeCode = frame.slice(cur.pos, cur.pos + 3); cur.pos += 3; - // Every Rev 3 layout carries the ESSR sender field next. + // Every layout carries the ESSR sender field next. const senderFieldBegin = cur.pos; const senderBytes = wire.decodeVariableData(wire.TSP_VID, frame, cur); if (senderBytes === undefined) throw new Error("tsp: missing ESSR sender VID field"); @@ -188,7 +189,10 @@ export function decodePayloadFrame( } catch { throw new Error("tsp: ESSR sender VID is not UTF-8"); } - if (senderVid.length > 0 && senderVid !== envelopeSender) { + if (senderVid.length === 0) { + throw new Error("tsp: ESSR sender VID is the NULL VID; a confidential payload must name its sender"); + } + if (senderVid !== envelopeSender) { throw new Error("tsp: ESSR sender VID does not match the envelope sender"); } diff --git a/packages/tsp-js/tests/cesr.wire.mjs b/packages/tsp-js/tests/cesr.wire.mjs index c33690a5..cb29afe5 100644 --- a/packages/tsp-js/tests/cesr.wire.mjs +++ b/packages/tsp-js/tests/cesr.wire.mjs @@ -21,8 +21,6 @@ test("code identifiers match the reference", () => { assert.equal(w.TSP_HPKE_BASE_CIPHERTEXT, 5); // Rev 3 `F` assert.equal(w.TSP_SEALED_BOX_CIPHERTEXT, 2); // Rev 3 `C` (§8.3, recognised only) assert.equal(w.TSP_GENERIC_STREAM, 0); // Rev 3 `-A` - assert.equal(w.TSP_HPKEAUTH_CIPHERTEXT, 6); // Rev 2 `G`, struck from the Rev 3 table - assert.equal(w.TSP_TMP, 23); // Rev 2 only assert.equal(w.TSP_ETS_WRAPPER, 4); assert.equal(w.TSP_PAYLOAD, 25); assert.equal(w.TSP_ATTACH_GRP, 2); @@ -44,20 +42,21 @@ test("encodeCount(-E, 19) = f8 40 13 and round-trips", () => { assert.equal(cur.pos, 3); }); -test("encodeVersion = 61 34 8f f8 00 02 — YTSP-AAC, the Rev 3 marker", () => { - // Rev 2 was `f8 00 01` (`YTSP-AAB`). One byte, and it is the byte the whole - // dual-revision dispatch turns on, so it is pinned rather than round-tripped. +test("encodeVersion = 61 34 8f f8 10 00 — YTSP-BAA, TSP 1.0", () => { + // Rev 2 was `f8 00 01` (`YTSP-AAB`) and Rev 3 `f8 00 02` (`YTSP-AAC`); 1.0 + // moves MAJOR, in the middle byte. It is the byte the version gate turns on, + // so it is pinned rather than round-tripped. const buf = write((out) => w.encodeVersion(out)); - assert.equal(hex(buf), "61348ff80002"); + assert.equal(hex(buf), "61348ff81000"); const cur = { pos: 0 }; - assert.deepEqual(w.readVersion(buf, cur), { major: 0, minor: 2 }); + assert.deepEqual(w.readVersion(buf, cur), { major: 1, minor: 0 }); assert.equal(cur.pos, 6); }); -test("readVersion reads a Rev 2 marker without judging it", () => { - // The discriminator has to parse a revision it does not implement the codec - // for — that is the entire job. MINOR comes back unjudged; `peekRevision` - // decides, and `readVersion` does not get an opinion. +test("readVersion reads a draft marker without judging it", () => { + // The gate has to parse a version it refuses, so it can say which one it + // refused. MAJOR and MINOR come back unjudged; `peekRevision` decides, and + // `readVersion` does not get an opinion. const rev2 = bytes([0x61, 0x34, 0x8f, 0xf8, 0x00, 0x01]); const cur = { pos: 0 }; assert.deepEqual(w.readVersion(rev2, cur), { major: 0, minor: 1 }); @@ -84,11 +83,14 @@ test("variable data — 21-byte VID (0 lead bytes, D4 selector)", () => { assert.deepEqual(w.decodeVariableData(w.TSP_VID, buf, cur), vid); }); -test("fixed data — 2-byte TMP marker = 5c 00 00", () => { - const buf = write((out) => w.encodeFixedData(w.TSP_TMP, bytes([0, 0]), out)); +test("fixed data — 2 bytes under `X` = 5c 00 00 (a one-byte header)", () => { + // Rev 2's trailing `X 00 00` marker, kept as a golden vector for the + // one-byte fixed-data header now that nothing emits the marker itself. + const X = w.cesrInt("X"); + const buf = write((out) => w.encodeFixedData(X, bytes([0, 0]), out)); assert.equal(hex(buf), "5c0000"); const cur = { pos: 0 }; - assert.deepEqual(w.decodeFixedData(w.TSP_TMP, 2, buf, cur), bytes([0, 0])); + assert.deepEqual(w.decodeFixedData(X, 2, buf, cur), bytes([0, 0])); assert.equal(cur.pos, 3); }); @@ -101,10 +103,8 @@ test("fixed data — 64-byte Ed25519 signature header = d0 10", () => { }); // Hop lists are no longer here: Rev 3 §9.2 made the `-J` count the group's byte -// length where Rev 2 counted VIDs, so the same bytes mean two different things -// and neither codec can share one implementation. Each revision owns its own — -// `rev3/payload.ts` and `rev2/reader.ts` — and they are exercised through -// `message.routed.mjs` and the published `routed` vector respectively. +// length where Rev 2 counted VIDs. The codec owns its own in `v1/fields.ts`, +// exercised through `message.routed.mjs` and the published `routed` vector. test("count long-form (≥ 4096) — Rev 3 spells it --X, and the count is exact", () => { // Two things changed here and both were invisible to a round trip. @@ -133,20 +133,14 @@ test("count long-form (≥ 4096) — Rev 3 spells it --X, and the count is exact assert.equal(cur0.pos, buf0.length); }); -test("count long-form — a Rev 2 header decodes only under the Rev 2 form", () => { - // `-0Z` + 5000. Hand-built, because nothing in this package emits one any - // more: the Rev 2 reader is the only caller that passes LONG_COUNT_REV2, and - // the Rev 3 decoder must *not* accept these bytes — a decoder loose enough to - // take either spelling would read a Rev 2 frame as Rev 3 and then fail - // somewhere that says nothing about why. +test("count long-form — Rev 2's `-0Z` spelling is refused", () => { + // `-0Z` + 5000, hand-built: nothing emits it. A decoder loose enough to take + // either spelling would read a Rev 2 frame's counts and then fail somewhere + // that says nothing about why. const rev2 = bytes([0xfb, 0x46, 0x40, 0x00, 0x13, 0x88]); - const asRev2 = { pos: 0 }; - assert.equal(w.decodeCount(w.TSP_PAYLOAD, rev2, asRev2, w.LONG_COUNT_REV2), 5000); - assert.equal(asRev2.pos, 6); - - const asRev3 = { pos: 0 }; - assert.equal(w.decodeCount(w.TSP_PAYLOAD, rev2, asRev3), undefined); - assert.equal(asRev3.pos, 0, "a refused decode advances nothing"); + const cur = { pos: 0 }; + assert.equal(w.decodeCount(w.TSP_PAYLOAD, rev2, cur), undefined); + assert.equal(cur.pos, 0, "a refused decode advances nothing"); }); test("isTsp accepts both framings, and nothing else", () => { @@ -173,5 +167,5 @@ test("variable data round-trips across all lead-byte alignments", () => { test("wrong identifier fails to decode", () => { const buf = write((out) => w.encodeVariableData(w.TSP_VID, enc.encode("x"), out)); const cur = { pos: 0 }; - assert.equal(w.decodeVariableData(w.TSP_HPKEAUTH_CIPHERTEXT, buf, cur), undefined); + assert.equal(w.decodeVariableData(w.TSP_HPKE_BASE_CIPHERTEXT, buf, cur), undefined); }); diff --git a/packages/tsp-js/tests/control.spec-vectors.mjs b/packages/tsp-js/tests/control.spec-vectors.mjs index a25ed3f4..ae462205 100644 --- a/packages/tsp-js/tests/control.spec-vectors.mjs +++ b/packages/tsp-js/tests/control.spec-vectors.mjs @@ -2,10 +2,10 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; -import { unpack, packInvite, packAccept, packCancel } from "../dist/index.js"; -import { decodeEnvelope as decodeRev3Envelope } from "../dist/rev3/envelope.js"; -import { deriveSaid } from "../dist/rev3/control.js"; -import { senderFieldBytes } from "../dist/rev3/fields.js"; +import { unpack, packInvite, packAccept, packCancel, referralSignedData } from "../dist/index.js"; +import { decodeEnvelope as decodeV1Envelope } from "../dist/v1/envelope.js"; +import { deriveSaid } from "../dist/v1/control.js"; +import { senderFieldBytes } from "../dist/v1/fields.js"; import * as wire from "../dist/cesr/wire.js"; import { ed25519, x25519 } from "@noble/curves/ed25519.js"; @@ -19,7 +19,7 @@ import { ed25519, x25519 } from "@noble/curves/ed25519.js"; // Nothing weaker would do. Encoder and decoder built from the same wrong // reading agree perfectly, and so do two implementations that share it. const VECTORS = JSON.parse( - readFileSync(new URL("./fixtures/spec-rev3-vectors.json", import.meta.url), "utf8"), + readFileSync(new URL("./fixtures/spec-v1-vectors.json", import.meta.url), "utf8"), ); const b64u = (s) => new Uint8Array(Buffer.from(s, "base64url")); @@ -58,13 +58,12 @@ test("the invite's digest is derived exactly as the reference derives it", async // "TSP_Digest does not match" from three layers down. const v = VECTORS.vectors["control-rfi-direct"]; const wireBytes = b64u(v.message); - const decoded = decodeRev3Envelope(wireBytes); + const decoded = decodeV1Envelope(wireBytes); const envelopeFields = wireBytes.slice(decoded.aad.begin, decoded.aad.end); - // The vector carries the NULL VID in its ESSR sender field, which §9.2 permits - // under HPKE-Base. We write the real VID; both are conformant, and the - // derivation has to accept whichever the message actually carries. - const nullSenderField = senderFieldBytes(""); + // The ESSR sender field is inside the derivation, and 1.0 makes it the + // sender's VID (at `YTSP-AAC` this vector carried the NULL VID there). + const senderField = senderFieldBytes(id("alice").id); const nonceOut = []; wire.encodeFixedData(wire.TSP_NONCE, new Uint8Array(16).fill(0x11), nonceOut); @@ -72,7 +71,7 @@ test("the invite's digest is derived exactly as the reference derives it", async wire.encodeCount(wire.TSP_HOP_LIST, 0, emptyList); const after = new Uint8Array([...nonceOut, ...emptyList, ...emptyList]); // nonce, Reply_Path, Referral - const said = deriveSaid(envelopeFields, wire.XRFI, nullSenderField, after); + const said = deriveSaid(envelopeFields, wire.XRFI, senderField, after); const out = await openVector("control-rfi-direct"); assert.deepEqual(said, out.control.digest, "recomputed SAID matches the one on the wire"); @@ -121,6 +120,38 @@ test("control-rfd — a cancel names the message it ends and carries no nonce", assert.equal(cancel.control.nonce, undefined); }); +test("control-rfi-referral — the referral decodes, and Signature_new verifies under the new VID's key", async () => { + // An invite over an existing relationship introducing a new VID (§7.2.5). + // `unpack` checks the SAID, which covers `VID_new` but not `Signature_new`; + // the signature is the caller's to check once it has resolved `VID_new`, + // through `referralSignedData`. In 1.0 its input starts with the sender's + // VID — a Rev 3 reference sender wrote the NULL VID there instead. + const v = VECTORS.vectors["control-rfi-referral"]; + const out = await openVector("control-rfi-referral"); + assert.equal(out.control.controlType, "invite"); + assert.deepEqual(out.threadDigest, out.control.digest); + + const introduced = id(v.newVid); + const { referral } = out.control; + assert.ok(referral, "the referral field is populated"); + assert.equal( + referral.newVid, + introduced.longForm, + "a VID introduced for the first time travels in its long form", + ); + assert.deepEqual(referral.signature, b64u(v.signatureNew), "Signature_new as published"); + + const signed = referralSignedData( + out.sender, + out.control.digest, + out.control.nonce, + out.control.route, + referral.newVid, + ); + assert.equal(ed25519.verify(referral.signature, signed, b64u(introduced.pkS)), true); + assert.equal(ed25519.verify(referral.signature, signed, b64u(id("alice").pkS)), false); +}); + test("a tampered control digest is refused as verification, not parsed as valid", async () => { // The whole value of a self-addressing digest is that a receiver checks it. // Flip one byte of the invite's carried digest and the recomputation must diff --git a/packages/tsp-js/tests/fixtures/spec-rev3-vectors.json b/packages/tsp-js/tests/fixtures/spec-v1-vectors.json similarity index 71% rename from packages/tsp-js/tests/fixtures/spec-rev3-vectors.json rename to packages/tsp-js/tests/fixtures/spec-v1-vectors.json index ec73718b..10639623 100644 --- a/packages/tsp-js/tests/fixtures/spec-rev3-vectors.json +++ b/packages/tsp-js/tests/fixtures/spec-v1-vectors.json @@ -1,90 +1,101 @@ { "_source": { "spec": "trustoverip/tswg-tsp-specification", - "commit": "f5b8668952aabe8e541b535fcbdf589484ffc4f4", + "commit": "ccda6b33159d860f05c328eb436dabe83548b705", "section": "Appendix A: Test Vectors", - "note": "Appendix A as merged, which moved the TSP version to YTSP-AAC. Every message changed with it, and so did the control vectors' SAID digests, which cover the version; keys, ikmE and the direct vectors' printed payloads are unchanged. Supersedes the pre-merge YTSP-ABA vectors of PR #63 (commit 66a158002eb2cc6acf5ac62f9aa23224d433e6cc). Extracted mechanically from the specification (tsp-conformance fixtures/spec-vectors.json, tools/extract_spec_vectors.py); nested-direct and routed additionally carry innerPayload, the plaintext payload frame of the inner message. Values are qb64 (CESR text domain), which is a direct base64url transcode of the qb2 binary domain this package works in. The private keys are published in the spec and must never be used for anything but checking these vectors." + "note": "Appendix A of the released specification, TSP 1.0 (YTSP-BAA). Every message changed with the version, and eight of the eleven also gained the sender VID at the head of the payload, which 1.0 requires in every confidential payload; identifiers, keys, ikmE and nonces are unchanged, and control-rfi-referral (with the alice_referred identifier) is new. Identical to affinidi-tsp 0.3.0's tests/vectors/rev3.json, which was checked value by value against the specification text and tsp_sdk 0.16.0's test_vectors/rev3.json. newVid and signatureNew on control-rfi-referral name the introduced identifier and its published Signature_new. Values are qb64 (CESR text domain), a direct base64url transcode of the qb2 binary domain this package works in. The private keys are published in the spec and must never be used for anything but checking these vectors.", + "reference": "openwallet-foundation-labs/tsp v0.16.0" }, "identifiers": { "alice": { "id": "did:peer:4zQmUL61Nc1F7ioiKxHNqwnJXX4srhFsKKPo6TrCmhM3dfpq", - "longForm": "did:peer:4zQmUL61Nc1F7ioiKxHNqwnJXX4srhFsKKPo6TrCmhM3dfpq:z25NRJMKpQKwnUm6k9FbTSp1eqANorDtFimHW9nGdLSgAbN1UhsQANFAnhij8VXrrwnDDG5Rb7HGrYpBGZRy3EbgmxirqJi3WuNfiajavps6kAfnvk9ykxGqcxH4u7P2TJohfbvigGMTMGxAhqkU4JDkWYi1sBp266n8tzTzD3DfRUAMspuGDnE6KM7r55U1tmXkDtRRWy97BFdh9HKfA3CPhA3gFdwRRV45CP1kGiaYU2rfwYAncrd1vfsAsZS8CoqbDWBgm7wGQNBXkjE8SuhKA8zv64cwpvbjJZioW7tx6Jo986sMDshig678dSR8whhc1zq4ZeaeDzGB6SbUZrpXXpd4aHy5AQQF7fwmStceXXEsNa4zL8xAhTszp2yFhLF399Q83hTdka8gUWyyQ26LUFDXCUivSfPxFRyUZZEQEK54sZZFP44MQoC4dDRrDqd2tyKMjTkdxVTg1Tvk1k9CNRUoHzdrboeESKmmdN39Ms3xRCVTN7cU6qGFW7Uz9xqhCinaKpzgRLp6ZSvmX4NcCTbruQeb3vMvHvJLCkMghQp", "sigKeyType": "Ed25519", "pkS": "b5DCBaChqWVVGJOsGWcKuKti0SKfm53pTfOwuj-hm9g", "skS": "MqiUK5zsViOi9QxQlyMXvPdraQghVOASyFiAvlFXp4U", "encKeyType": "X25519", "pkE": "4ZKJH0k2hjrBnFa83B8ZWx2dQYjQmbnc2nJW6urQSWg", - "skE": "0G2OObWQl0Nz9fR8LyBvzVGVLezU3wu5Y7payTJY2BE" + "skE": "0G2OObWQl0Nz9fR8LyBvzVGVLezU3wu5Y7payTJY2BE", + "longForm": "did:peer:4zQmUL61Nc1F7ioiKxHNqwnJXX4srhFsKKPo6TrCmhM3dfpq:z25NRJMKpQKwnUm6k9FbTSp1eqANorDtFimHW9nGdLSgAbN1UhsQANFAnhij8VXrrwnDDG5Rb7HGrYpBGZRy3EbgmxirqJi3WuNfiajavps6kAfnvk9ykxGqcxH4u7P2TJohfbvigGMTMGxAhqkU4JDkWYi1sBp266n8tzTzD3DfRUAMspuGDnE6KM7r55U1tmXkDtRRWy97BFdh9HKfA3CPhA3gFdwRRV45CP1kGiaYU2rfwYAncrd1vfsAsZS8CoqbDWBgm7wGQNBXkjE8SuhKA8zv64cwpvbjJZioW7tx6Jo986sMDshig678dSR8whhc1zq4ZeaeDzGB6SbUZrpXXpd4aHy5AQQF7fwmStceXXEsNa4zL8xAhTszp2yFhLF399Q83hTdka8gUWyyQ26LUFDXCUivSfPxFRyUZZEQEK54sZZFP44MQoC4dDRrDqd2tyKMjTkdxVTg1Tvk1k9CNRUoHzdrboeESKmmdN39Ms3xRCVTN7cU6qGFW7Uz9xqhCinaKpzgRLp6ZSvmX4NcCTbruQeb3vMvHvJLCkMghQp" + }, + "alice_referred": { + "id": "did:peer:4zQmRFRNUTarZmMebvvqNyADP7rkUgh5NFFhxqWvx3eqqgS3", + "sigKeyType": "Ed25519", + "pkS": "TkDQONscMVauhQQE0TfgAhXQSoG_SGVNLOlz6VFapoc", + "skS": "pA42dzrFSFimN_v0W5yx9gZJHYbEAOH5VKy2cLftzIk", + "encKeyType": "X25519", + "pkE": "Ql8Xuy4ejZdHGWrfehc5aTtCGF0fY1jUlh_NPY0DiQg", + "skE": "lDy7QQoN-PR0PhyZAthCSZ8OvOZ1oqiaQfDHr0F6DWk", + "longForm": "did:peer:4zQmRFRNUTarZmMebvvqNyADP7rkUgh5NFFhxqWvx3eqqgS3:z25NRJMKpQKwnUm6k9FbTSp1eqANorDtFimHW9nGdLSgAbN1UhsQANFAnhij8VXrrwnDDG5Rb7HGrYpBGZRy3EbgmxirqJi3WuNfiajavps6kAfnvk9ykxGqcxH4u7P2TJohfbvigGMTMGxAhqkU4JDkWYi1sBp266n8tzTzD3DfRU9FbvHS99WbQ3rd6KbNqFfoUzchbb9bU8CCfwNrF8Z1diU8mdjbfbQXkzsau4aexTFxBgCcbEeMUs6fcMABJqQmX7Dzb3HdME9efPvQRQH3JXHvL2jkbPuBpj2nqG3juyRWvothcQuwUaoc1Crp19Yvxw8w2mTiV1y2xiAutpg4RbEu5gN94cFRF4wEWUwhYFgzFYoLWwqMGJmLbTA95V7jAfYAATnUEtWKFcwg2bYYaM2QmFV67Bmi43aGvQMY8qJCXcRDPFZtEuxefQAm916V6DF9JB6oTRajzx7vJvG6VgUKv8hncTrCaMNmvV22upbfU5duZK4qBTrXo1crJShNem547Fg92FHvadwHULk49ZsVBMbPETQpEEatkjBtgNg" }, "bob": { "id": "did:peer:4zQmZmCAsG7j1ewTjXjtddwujik33CE2cMbYSPagpMiYnt1A", - "longForm": "did:peer:4zQmZmCAsG7j1ewTjXjtddwujik33CE2cMbYSPagpMiYnt1A:z25NRJMKpQKwnUm6k9FbTSp1eqANorDtFimHW9nGdLSgAbN1UhsQANFAnhij8VXrrwnDDG5Rb7HGrYpBGZRy3EbgmxirqJi3WuNfiajavps6kAfnvk9ykxGqcxH4u7P2TJohfbvigGMTMGxAhqkU4JDkWYi1sBp266n8tzTzD3DfRUDuzmWt547vK52UYFjqvYuqeZke48ht1Tn7veKyU9an3DBDEK5iNnMZuTwssYn9WboW5tdbdA3dyKiqwbBwrTUQsaAbpAaMzKCsFouHetYm3r6ft4Ewh5F6sQeWtVzScGmrWr2M9rcyLyRC4nC711Sfh3q6tNeb393SqeLs1uMVD3AJ7mLMWGmaYSuMBewWRcaJZdEiDe4NjZa12WSuZ9hZ9UYigEMByhwiT8WVsgiNEjFwcQLdofowERUKhYYNv1sgDfZ4K7MGRtdkJw3eZRbP1T9NPwyDsiBycnKipqn4MfcnDMWVMSsiQCg1xprkVvtgtgjhYkG1A7w9UGk5497ytKuFMpzwJbu8cAxLqTFZ1phe8cft61Euqj1eoCQ1X5E", "sigKeyType": "Ed25519", "pkS": "-ezxE2yQV7bti34Ofz1Cgy_Ykf3_t6ob2zc4evBKZS4", "skS": "8sDXUkHnFe7exy1sGdCZl9kQQb-Xz-wuMBcGKXkVetw", "encKeyType": "X25519", "pkE": "c2fBIIJWZgJFNKZ0SS7k63Q_73kW1nnwaAf2vIb-mGA", - "skE": "j952DlpES4WEaSrY72PZwOi1j035m9iFxsZh_OKfcOE" + "skE": "j952DlpES4WEaSrY72PZwOi1j035m9iFxsZh_OKfcOE", + "longForm": "did:peer:4zQmZmCAsG7j1ewTjXjtddwujik33CE2cMbYSPagpMiYnt1A:z25NRJMKpQKwnUm6k9FbTSp1eqANorDtFimHW9nGdLSgAbN1UhsQANFAnhij8VXrrwnDDG5Rb7HGrYpBGZRy3EbgmxirqJi3WuNfiajavps6kAfnvk9ykxGqcxH4u7P2TJohfbvigGMTMGxAhqkU4JDkWYi1sBp266n8tzTzD3DfRUDuzmWt547vK52UYFjqvYuqeZke48ht1Tn7veKyU9an3DBDEK5iNnMZuTwssYn9WboW5tdbdA3dyKiqwbBwrTUQsaAbpAaMzKCsFouHetYm3r6ft4Ewh5F6sQeWtVzScGmrWr2M9rcyLyRC4nC711Sfh3q6tNeb393SqeLs1uMVD3AJ7mLMWGmaYSuMBewWRcaJZdEiDe4NjZa12WSuZ9hZ9UYigEMByhwiT8WVsgiNEjFwcQLdofowERUKhYYNv1sgDfZ4K7MGRtdkJw3eZRbP1T9NPwyDsiBycnKipqn4MfcnDMWVMSsiQCg1xprkVvtgtgjhYkG1A7w9UGk5497ytKuFMpzwJbu8cAxLqTFZ1phe8cft61Euqj1eoCQ1X5E" }, "nested_alice": { "id": "did:peer:4zQmehsjMuPkPjzg7WF2tH5X1SGuFMxacJB9RETLFwjgMDqX", - "longForm": "did:peer:4zQmehsjMuPkPjzg7WF2tH5X1SGuFMxacJB9RETLFwjgMDqX:z25NRJMKpQKwnUm6k9FbTSp1eqANorDtFimHW9nGdLSgAbN1UhsQANFAnhij8VXrrwnDDG5Rb7HGrYpBGZRy3EbgmxirqJi3WuNfiajavps6kAfnvk9ykxGqcxH4u7P2TJohfbvigGMTMGxAhqkU4JDkWYi1sBp266n8tzTzD3DfRUDxc1EGZZ8oysc2HpdT4rEuGHZoobGpKmo4XJSoDPKF6CfUWCr2UyzMv8dbCQWeugCqsUqQuASukLHjNPpzRx8cuSRSA7kDNG9civ4UoFrWhgvdSrPgBjeQYQFREA9ctDXCG7QkoYfNrpLhHXZ3LK3C1Jm7hQtmX926LQ75V82SSRPmwXBYXGx153XZuCm98znoU8cWADwCtJ6GhVrUSZPkkaPgrZqE1pFukaCvZf4gstRoZEwGrtDhyDdk5L6SpoLxEYCYfgFGmYpo7a1PojKqFFqp1NLzgfimW8XEuq9sL3BXraAz6kxRXCnbeEmSNHxzgJxe9S17WFsTn2jtFHLQFHgRxzpjhdXeysKxYJSDng8Cjjk8WdZeN5YkmKsve5z", "sigKeyType": "Ed25519", "pkS": "_-E1m_BLThHwhUuYhV0YovargFtQUkox6LnwhHx6Nnc", "skS": "gOpqJhU4I8_MNADm8rODdfaMP4dv61uP8VwY8e0mjNw", "encKeyType": "X25519", "pkE": "NJwm8WK_zTmq60S-bjrbr1OjURO0FBfEjaTlZhmDm0Y", - "skE": "e2EIWSYsqFudoECVTglwKwX_i_t36CGjjDCCRF7uQ7A" + "skE": "e2EIWSYsqFudoECVTglwKwX_i_t36CGjjDCCRF7uQ7A", + "longForm": "did:peer:4zQmehsjMuPkPjzg7WF2tH5X1SGuFMxacJB9RETLFwjgMDqX:z25NRJMKpQKwnUm6k9FbTSp1eqANorDtFimHW9nGdLSgAbN1UhsQANFAnhij8VXrrwnDDG5Rb7HGrYpBGZRy3EbgmxirqJi3WuNfiajavps6kAfnvk9ykxGqcxH4u7P2TJohfbvigGMTMGxAhqkU4JDkWYi1sBp266n8tzTzD3DfRUDxc1EGZZ8oysc2HpdT4rEuGHZoobGpKmo4XJSoDPKF6CfUWCr2UyzMv8dbCQWeugCqsUqQuASukLHjNPpzRx8cuSRSA7kDNG9civ4UoFrWhgvdSrPgBjeQYQFREA9ctDXCG7QkoYfNrpLhHXZ3LK3C1Jm7hQtmX926LQ75V82SSRPmwXBYXGx153XZuCm98znoU8cWADwCtJ6GhVrUSZPkkaPgrZqE1pFukaCvZf4gstRoZEwGrtDhyDdk5L6SpoLxEYCYfgFGmYpo7a1PojKqFFqp1NLzgfimW8XEuq9sL3BXraAz6kxRXCnbeEmSNHxzgJxe9S17WFsTn2jtFHLQFHgRxzpjhdXeysKxYJSDng8Cjjk8WdZeN5YkmKsve5z" }, "nested_bob": { "id": "did:peer:4zQmSMw413keKhgpTpYm1q8jzmpqNYwddVGPubkHNgp3qQei", - "longForm": "did:peer:4zQmSMw413keKhgpTpYm1q8jzmpqNYwddVGPubkHNgp3qQei:z25NRJMKpQKwnUm6k9FbTSp1eqANorDtFimHW9nGdLSgAbN1UhsQANFAnhij8VXrrwnDDG5Rb7HGrYpBGZRy3EbgmxirqJi3WuNfiajavps6kAfnvk9ykxGqcxH4u7P2TJohfbvigGMTMGxAhqkU4JDkWYi1sBp266n8tzTzD3DfRUAgR84nfSphZZkXtF4LsDaKz5V52HNNgWgz1k8aFkjSCzTgyVcS7f2RwomT1HfyLVrW3UqCszF7iJ9vpfMviins6mHyfBkcA3bJbpeKoVTqemLZ9XgkhhGxnszeDAr8HAGrwTysiGmZeJUoweCk2RyP5f1QyWBqcNcRyDRjkShHQgpcVPy2iKceZYQbZsMRVTJveWDuzbAMzAeX2XsXDGE8y6r2dEKLikSRqFFYwGTrNEBjmkfQ9DnkRbQAK3ZoKKT94khAhbw8tZkDK3w48Vjb6XZkEcpWbpwsrAM4RDPcS7UJq3fH3WCoeSDQWoXyxncPoYmNaBK6PtwQe3PhaMoUBwUUFtEsDCuVCrAz6vpvMyZaeMv4EbmBGYwtMunWhxG", "sigKeyType": "Ed25519", "pkS": "eGi5FU5ZXN-kavLZRr_ozjAXFzUPsv14y8CyPCOuR84", "skS": "6X6bCFv20GqJXHcnIBba3cILwFv9eawgwOL2HgOp6ig", "encKeyType": "X25519", "pkE": "5tBNodlJELEWNo9d-rgDxcYf1ldc9fruci9PtWqUgAo", - "skE": "0xi9nim9P3GuGznEHJJFL2JGpuCPqtCYFJ0EvBGj9Qo" + "skE": "0xi9nim9P3GuGznEHJJFL2JGpuCPqtCYFJ0EvBGj9Qo", + "longForm": "did:peer:4zQmSMw413keKhgpTpYm1q8jzmpqNYwddVGPubkHNgp3qQei:z25NRJMKpQKwnUm6k9FbTSp1eqANorDtFimHW9nGdLSgAbN1UhsQANFAnhij8VXrrwnDDG5Rb7HGrYpBGZRy3EbgmxirqJi3WuNfiajavps6kAfnvk9ykxGqcxH4u7P2TJohfbvigGMTMGxAhqkU4JDkWYi1sBp266n8tzTzD3DfRUAgR84nfSphZZkXtF4LsDaKz5V52HNNgWgz1k8aFkjSCzTgyVcS7f2RwomT1HfyLVrW3UqCszF7iJ9vpfMviins6mHyfBkcA3bJbpeKoVTqemLZ9XgkhhGxnszeDAr8HAGrwTysiGmZeJUoweCk2RyP5f1QyWBqcNcRyDRjkShHQgpcVPy2iKceZYQbZsMRVTJveWDuzbAMzAeX2XsXDGE8y6r2dEKLikSRqFFYwGTrNEBjmkfQ9DnkRbQAK3ZoKKT94khAhbw8tZkDK3w48Vjb6XZkEcpWbpwsrAM4RDPcS7UJq3fH3WCoeSDQWoXyxncPoYmNaBK6PtwQe3PhaMoUBwUUFtEsDCuVCrAz6vpvMyZaeMv4EbmBGYwtMunWhxG" }, "p": { "id": "did:peer:4zQmXuYx5quNpAYvu1syaoHpJWxHceBM8PnAS1J84mjEo118", - "longForm": "did:peer:4zQmXuYx5quNpAYvu1syaoHpJWxHceBM8PnAS1J84mjEo118:z25NRJMKpQKwnUm6k9FbTSp1eqANorDtFimHW9nGdLSgAbN1UhsQANFAnhij8VXrrwnDDG5Rb7HGrYpBGZRy3EbgmxirqJi3WuNfiajavps6kAfnvk9ykxGqcxH4u7P2TJohfbvigGMTMGxAhqkU4JDkWYi1sBp266n8tzTzD3DfRUDEQsmpmjBHzq8cijG6sryWW5nVPht6pYsbuY4ccUdfcph7owJRDqYg99MemT1i7k3HvTxvnop9E89WNwWZiDLmJk5tssb7rQSatDkq2kvRhgW8eVyFnsoTdg2h87tsGpE8p6tQNyZpzAnTNVUe886osyyavxqQccEcm8idNnFEXEqtLL1oENMQvaWr8CsYUPy1rUpmv9xFPBay42n54ssn99hEMCahvW1sLFjHwcxGGco6PSMZT5DCw9yhUVSAeYpJ6eC4a2tjXB3tjcFeWifhxkp1u2yLJ1bcdxmJDvbKPghPVeCYYEm7g7RjrdFKKX1534YVEvwgeTi6H6ujmSvvs6xLwDjkmRwYpgPAucS9TMgVGJgDpEXD1brQZUP3PBJ", "sigKeyType": "Ed25519", "pkS": "4aeb8EZ5rJciXQvEjl1LDcZaST67KdYnqvZ2BYWU5Y8", "skS": "WW-aIOkC8AcowW-6H-MmjFoUA92mzz1XYmp5DsLpxE0", "encKeyType": "X25519", "pkE": "ajg137oz_TtbqqxihWdMYGblF9NLtFpDg8mv06leNVQ", - "skE": "ivpbTN8fMgb3oj1S3tQ5SC5UAjDQ5oLuOlmrkbJKy38" + "skE": "ivpbTN8fMgb3oj1S3tQ5SC5UAjDQ5oLuOlmrkbJKy38", + "longForm": "did:peer:4zQmXuYx5quNpAYvu1syaoHpJWxHceBM8PnAS1J84mjEo118:z25NRJMKpQKwnUm6k9FbTSp1eqANorDtFimHW9nGdLSgAbN1UhsQANFAnhij8VXrrwnDDG5Rb7HGrYpBGZRy3EbgmxirqJi3WuNfiajavps6kAfnvk9ykxGqcxH4u7P2TJohfbvigGMTMGxAhqkU4JDkWYi1sBp266n8tzTzD3DfRUDEQsmpmjBHzq8cijG6sryWW5nVPht6pYsbuY4ccUdfcph7owJRDqYg99MemT1i7k3HvTxvnop9E89WNwWZiDLmJk5tssb7rQSatDkq2kvRhgW8eVyFnsoTdg2h87tsGpE8p6tQNyZpzAnTNVUe886osyyavxqQccEcm8idNnFEXEqtLL1oENMQvaWr8CsYUPy1rUpmv9xFPBay42n54ssn99hEMCahvW1sLFjHwcxGGco6PSMZT5DCw9yhUVSAeYpJ6eC4a2tjXB3tjcFeWifhxkp1u2yLJ1bcdxmJDvbKPghPVeCYYEm7g7RjrdFKKX1534YVEvwgeTi6H6ujmSvvs6xLwDjkmRwYpgPAucS9TMgVGJgDpEXD1brQZUP3PBJ" }, "q": { "id": "did:peer:4zQmTKaSRexnzX2ecu1eYRusFNeWbrT5jbjr3YjtfgciZc1Q", - "longForm": "did:peer:4zQmTKaSRexnzX2ecu1eYRusFNeWbrT5jbjr3YjtfgciZc1Q:z25NRJMKpQKwnUm6k9FbTSp1eqANorDtFimHW9nGdLSgAbN1UhsQANFAnhij8VXrrwnDDG5Rb7HGrYpBGZRy3EbgmxirqJi3WuNfiajavps6kAfnvk9ykxGqcxH4u7P2TJohfbvigGMTMGxAhqkU4JDkWYi1sBp266n8tzTzD3DfRUBoRT26HWqurFNeBrp2Z5RXkhxkhyxAnpoB6ZS1CthQZKhEgZz8ubr2Q29a9C2ZYyuCNNuWCCUhfm8AASng7btZiV16P3qyFpbMk1dpwabuguNC2hBAycwQ1pniBp3yDGdTrSDg7Mvd62zkzDJfCXhxTQJQ5SQvAu1bb5ZGonmjMfu3fU877FfvbYL1gqYDKcH7hZ7NnifTQP345XsY2tTav6vR2vUAzDiWwvpVJ9gApPWJExs3A8EBw3wke2m2C9uQpdu4XRaxVzWgtGd1NL3LFwcf34gEksj2VsURD4VNhdjQ4MkKSFy7uxgDSyqTXUdr1GEGeQGAtcFTq5anuPnN9kDHm48WihypUQ48jo3zM5JXE1fhL1Xypi5rWn3tkip", "sigKeyType": "Ed25519", "pkS": "qUfbbaVzKhhddujMaJNSl9C8eGWCOBjA3WT4UGzXNOU", "skS": "GolUele6xQTKisWaDZT9Q6dUNGg93_y__h-J3X_m5aU", "encKeyType": "X25519", "pkE": "M5Qk6RSrDUckHiQlmduCMbu9HW6AZbJCfkQEQqWShSI", - "skE": "GHZ-LhpY_K1f_PvDW6loXWLvFIcc6majVAR8_jWgL4c" + "skE": "GHZ-LhpY_K1f_PvDW6loXWLvFIcc6majVAR8_jWgL4c", + "longForm": "did:peer:4zQmTKaSRexnzX2ecu1eYRusFNeWbrT5jbjr3YjtfgciZc1Q:z25NRJMKpQKwnUm6k9FbTSp1eqANorDtFimHW9nGdLSgAbN1UhsQANFAnhij8VXrrwnDDG5Rb7HGrYpBGZRy3EbgmxirqJi3WuNfiajavps6kAfnvk9ykxGqcxH4u7P2TJohfbvigGMTMGxAhqkU4JDkWYi1sBp266n8tzTzD3DfRUBoRT26HWqurFNeBrp2Z5RXkhxkhyxAnpoB6ZS1CthQZKhEgZz8ubr2Q29a9C2ZYyuCNNuWCCUhfm8AASng7btZiV16P3qyFpbMk1dpwabuguNC2hBAycwQ1pniBp3yDGdTrSDg7Mvd62zkzDJfCXhxTQJQ5SQvAu1bb5ZGonmjMfu3fU877FfvbYL1gqYDKcH7hZ7NnifTQP345XsY2tTav6vR2vUAzDiWwvpVJ9gApPWJExs3A8EBw3wke2m2C9uQpdu4XRaxVzWgtGd1NL3LFwcf34gEksj2VsURD4VNhdjQ4MkKSFy7uxgDSyqTXUdr1GEGeQGAtcFTq5anuPnN9kDHm48WihypUQ48jo3zM5JXE1fhL1Xypi5rWn3tkip" }, "pq_alice": { "id": "did:peer:4zQmbLqu3weZtsnNesBzecCZm5FA6uaGkgCWYR5Wndr6Gzzd", - "longForm": "did:peer:4zQmbLqu3weZtsnNesBzecCZm5FA6uaGkgCWYR5Wndr6Gzzd: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", "sigKeyType": "MlDsa65", "pkS": "-XNPEJoCD0SXNM9Z1Ek_Ah3JXU94GgMiqsGZ9VlhtD-ibb_VSt7Kft0622kvaA7nYI8A1STW6uYfs4EQwElWrjLKUAxyIRGj490atsxFZdlpBJhDzY5GugJfhnT_MTHlM2R1OJHRNVpL7rqAQCFhFMv_s3n_nu6ERT07HE0v4-IV7BzADacxQvizGxUiBMD5_xIBOGFJRsHXyWnxFZkZUlYlMiiA7Czj_ja6qPeNhFQtQJgkCp1hrIteqAXGyBK9ngctYOHypXkwMAG8rO74UW53CHyrCU7FuL13nDmiXxOaEDzZ4e1fbLm_wcmgl7KTCWFsoFyRyZiEHY1IPYXlTlCZjP5s2FzCpoTXO_XvQK-iOcTMesC1-_xkXYJPjjhhyq4aR6Y_gQUBTHG0EROH7NzKRAOCJ0905kxpP5zKv0EYWkMSJcigfgXhREC7Iq6JDyRw0VLpu3czeF4xG0VdojBUJKQChDzkDWC9PzgZKLvktmN9W7-yFw573qgeWY4oWnl4ZPrKoqgcGGcAMMRGG12iEdjvy0kqxg7SUUIho2dUl_lN9FivvNOBGrVfDoL2HBCuEawJA9cVhgNtMxD1OV1QRRrm0MuKzSVOd7M8DH-cRLBb4n_5wSApn53h_5OM6T_uCdrfRjl9FWgiazeXwGb9ivlJR0OjRKQS9aKiU4oP4QK4tzm9IVf7fbmOH66sUh4Cgy2JCjt8Sqhvhp4sySpA_WIUzvhdm52hKFoRx-_FJc35nGMia7tvsgPpc-GlHCdMQXB873Lfrn2yZgIXz1Ji1UIAlqtKFRqVBCRiBlwhAXT9xjrPmpD7PcLXUWe_8pTNI-1t8QqcOl_S9GbQTAnyhkoSaApimo3EzxRyjxoksn0DnX9FKEeguxGEZ0dhXuUT9Et-9FylvkKgd0ZTU_RzMWgdWfS0JEhqUE2uMgJrKYaCzZO5C84o2gSbIyXcyvKukoQ2YVA3qF6qBoj5fiL5atJ2VDxqgg2D2Q2RWL2QUeAbpw50odvjKLQUbW2YLAHuPMnxZCn9MyeXwVjoasyELSemUurKHsxWwnonMAmQ46kxjgv4s5asGxMLrP_-fKtwMEt1epJ3Anicdt4FpD0Rf2EbyCW5KH7jPPz8mq3Tr7Dx5er8jqfT46Zi3Wlx0fIwryB5ON8yQrMTtBRvSMSQndty2JHKoKhylYc4KoVszqx8d8Ia4IkjwNNrvQAQomPUM2s8Wf9HuQwHwwGjhuGQefHiUBnOqbn057HXKuWrFYY-enzKBLaSnwVD0zatyIrkPfocI0PxoCSQIlUrriyAUbOs53drxBdF7bh7urVZjPL2KrPyaC-evLUrFpTl8F1BCUlREpmDSx55LjbcbHQyf9WZY2835gMT096InMv3JrLbuwHyz8hd6oy_FqdJpjrYQK9REajLxmZNVCTPhqxdoUn0cJvnq8uOgn8qVa9AFkxUWT3eLe-IwTo3EnuLxoyvl657a0JsBofp1roFkt0DPlAiNK12ffdclWql6c6S1-W__6um0L_ZcnIMznII7demQ0RQVKBd1ycs9wlJ66GLgxgS40OIZL8OuKp1Ht9vkl5DScgkPt_zKSEdbnEnfXlo70eKAb4fkJaJyTYT4r-wPskiBqUZW8NmJrh8ioK7FyImJYxHXOBgkOD7bReyZJr5kkWD2WWrB-jPr70W_Glk7b7PdwS9Hf3leB8MfZCfOUFbRksbPaz89qOsHcBOM3_F5UthxqUboxREizv41xkN88IHQhmpAyvS3ZXqApr3x2mjkzp7kCUdJFVY2Gk4BX3h7o732Um6UW_TXE1HPvUnqQo2OxfLzw-9xcM0AQbJSrTeC-hD3ZHpJyEHue-4srYvloWjoZqbwcXJUcMiPdNFDfSDiysqwAuN9VUFSCMAzGlyL-sytuTrr1rYEY3KshQ3vjFLxiiiCa6ZBn5Je-s6IHBP-OFTAIylZVLtgq4WDKo9Q1zoulzk6SGHZJ1QF0Q-AxGpiA9We2kq2B040q1jLewPkFLq2dVH45UAzTlvUiUXWg1_sKPhHQHy5gf8S-z6jg5DnDhCik9W2lDMicBYJe1b4_ykBDbPD9Nzabt6L5JcDH-Y9NaYEzHfcx6DqkHy1XDQ6mgxaBLDueY1hAY4YPOuNDoyxhkliEpyc7_Jlogu4RXYWoDft822kyto0vlnyPgEE12Cc2jrpZiixy6_hb8euWXtpznMmHHN44qBCTIpcbmq1_uPbC_oxOSp_Tel90TQev9fyMqFVCBYMgb4Ar9YWKnz2FT1hV7RrGoqFGH6lL62wUelyIa6kQFdBUw31gEXfs3TLyQKL_ZTSh7bXNZS79sfdrh5FekYQS9UpXpnMRq4Gfe_GMdk6Qn7aVRddFrVJviHnTE0cSyEZb8aU3MfTPVVmj5aIDN7TuNG8lI7OzAbZXU-rJpHygn7_6upjNkWJzO88Y2eNuidcrrwtnLL7aGOQsSHIcjIGF3JgB2XJciVmSfxnTSvTtV-ZZRoXwp3mYZsqY3JrfUn-URIo7QTrLO1vlIBCMgxoJeIeDGG4_0wq-YG4202WKVB45US_fgGGhLDcfpsdHm5eZU_pNKYl-uCN89pb1vDrEA", "skS": "-XNPEJoCD0SXNM9Z1Ek_Ah3JXU94GgMiqsGZ9VlhtD_kAdF67qjgAqtaOSUMDFnsYqIrfi6KACV4hnd82xS-9eniQETgp6ws5_-nsj1FHN8IWQMhm1VBcY1xBkejH4s02jxt8376jgka4Uqf0q_QlX_Skzpg9TfQT_FZD1FSGF0QMYGBgYOHU3UWU3NycoN4SIaDKBBCd2QxVAZWggV4coc1RCESNXhEFGF3JWcUURdYMkchhGc4RniHA2IUJmB3YGMINgRIcxgzgYAmYoQTB4MgNzcGAAJVF4BIAFhXVwUTR4YEYXIQVleGdFNDJndVeFdzZDcBZQAWZ4NGJAIAdgiEQGKEEjdQV4OHhCSEEhBndDNFNzeBhSQVMTUGUycFh2QHdwEUIhRlABB1M4UXIgEYeGUhVRVCZlR0ISiIMBNhERIHECYkVydYMIYoYgdWRnaIJGUYJTdlWBIIAggjR3SGhXZyAFZTJgiGVWgCNIhjAmgGBSByUihIUhhidEdQAYgDVoUYgmBgJIcHh1eFcQZ1cEQVEEAQVwBUA0hkYRNVRgUTU3hDYIUoFCdhYBIBQ3NlFUdih2Z2EVZjUTNXU0hQc3BQMlOCY4hwAEczR3ZwOFBXhgQXNINXg2EYgURHIkgCWAZxRGWDESYjV1QRImMxU2QDZSSFZ3NkEVUTBXNCg1gGZRQFdjI2QhUVZTQTVlh2ZDM2g4MIVCCIGFgkAnBkV1FjhYAkV3ATdDYGElSDYRR3NYUBCBdhMWaBJoJydSJDZSMWMIhDVnMhN3IWFlKDaEMnMoEUYHJ3dEM4BnY2c3UBMGiER3AWJVUIZ3IARmEYcXERAYQFg3B0FyI2c0RxMoU3MlWBNDZWIwBnQYYxcCV4RQVoFYBAKGhQIyYWFoZYIyAXJUhwg4N4JAVFaHgnRzVzNXNTEIZWJBU4JIBIZBYDMzd1FkOCMQAAYIiAMDdhBmR4dBGEI0RUGDE0KAcAQTZxUAhwcDFlIggnIBFRQyAVcjYQc4EUZ2RkMnJAJ3hjCHQIY3dRVoODB1MYQGMYJYMHdUY3REZYhHE4MoFUgDNESIclhVg4dFdQJDZjJjAAIWMnOCA3AWRhQShAhkUXMiZGIUJQJhRjI4VxQIUGdhEGZxYAYhYRhkRBEYQVF2dmMQNVJHUYZGgDQ2EkKGJEI3ZQI4M3YFJHKFJ0hHYwhlcoZ1goM4JXcAFIFCRFhwRWRUMkcVZjYXeDNASIIxRIeDchgXQ2MBdEcSYIU1VodCdGRgcRdxOCOIBoQSGAZIh0RQdgNyZUIkAGF2AmY3QTdxKDJIQxKBEHQ0dFZ3UkEkIiZmZRh1gIRXIYAhdVFzEjICECQmgxN1hlUgQGBBVYARBYAXIxZnE2gocTJBYoB3I4BRUzVFMDVSMRNiNIVwOHWFBBdwYEIGh4hzZiFnVHNYhyJIUSQFgDUkhXBVhBIDYQFSNyc3h0CHiAc1dGNmZxCGV4EQYTCHYUWEAiJDAhAXcVM4USEEIYQ1AFYkIVaIUmFUJzdDMzBRN4NRiHgjc4JjYgYjIRFWQnQyg2czZAYIZoBYJSiAODMwJxECRxYTVoVINyYFdYIkBDdCBkNCYgZTcUcxFoJFhwVHU4MjYIY0VlYlE3VocREodUYUgTYHYmdDVYI0ZxNHEjOIYiQiIyZmFSZDMnNiIVeEIhZ2ZVgQQhhVKIcUImUjKIhRQEdUcCMYcwRGJ3AhAzZUVidQZUaHFDAgWCIHYoFkY1gkUVFkcCQXFXiFUkQVMBMzIVWHNxBIUmATUmJgFRgDZoJgdoRWUVgREIMoBhMVEoEociAlaEg3iDFRMwV3A2NQWBASR4hWQxQGEEhBRkdSVURUNCN4ZBQUeHcBBBYxUWQHUwcFJDFIEWcoByhIMlIWM4FYACFGd3VRIYdINgAQRFZEIDUCQTCGhnIxRXBkcVZIESZgRWYjZUVRRYeFN4BEdDVgh3B4YTMkIwF4AHSGIiJTA0VRQQNoU3gBhCRxgXRxFRAHJ1RIEAZRNlBDIXYHFTJlZnZQQIQTAgdzQRNwYXdgEa7MwV9I0hxYbZm1dij9JjeuVueMp9v3iXXJLFSeeNeOEgtx__f5jXsq1bW7PIMuvslJmvvQtJLgB434F_Gmdx7z3zee30OzRQWa257gETLsDtgFwux8KNV43FcDVudsf7jh9C9102AlmOCRmbqE3NR92yGY-PkpHb1zpsK_CKpThiStJtpqGHfrxuF9WWrL_HO7QQhOHIawt-u8_KFDX-x1uTR8PH75PARzDAfvGp8UZOw8pdocNXXYdPrVan4AzRfdKTUMW-XjkKd-TrEhkq1QGkIBosojve_BhJDdHymDtoT7HQPRdzd-pKPNSl6lP4TwQYqdOqvmcoELhKVtAjxAAQIhuhL4AzU8fEarsOvkQq1oC4ewNI81NbSSZ0A5Z7iJGo-pPn0LsS_clkxjAQBD5ht3_XRc3yMT7I1h-AEGYxa_UfzYnlqDaWh-GQMd0hSRTkVf2SGSIsSp4USPHPZR2SURtpBHp659F6Zm5L5j-ROemWvk0PkhArmY71acQFpFAYK6ddoyxZhIWEJm9LDj68cuStJV2p2RTS7MDrLXN6iVSFmV4cyqoOZXLxGeNfrZ5QGRgTJqpRbaxhBC9LrVCLjT-0aodjRoqONUCfj4ADgz8qjrQo9Nkx_UjX7HWU_fDfh0eS2UjUqnGJHY0ATaeQ-7IE_WtU5ayqdK0_wlh6ZABAkFR-ShaY76KKNmTcdrq2cjp9A45cfUhv0fOkJqOBQBD8jDxS-cD_L6SGGt11zcELxObDADc89r_4Deeq9HsLbT36vHvZrZ5wMeZUPsJUVjkGvVbmUTi5544X6WZu2lX1JVuIdfrunzlYc1hWh7M8MNwSSlbqt38ImXtel1c0X993uMnLcHTISrM6FBsEcfFZ3P7t_GT-iImrLKTNBwahPJMFw0XXyz8CUuJjuVIdl1UBNBqs4NO0gp-qxU0A_anAk-ia6jlk440a2EsnB9T99pYFu1yu5ucYtb_171nK1NErw_KKRQqRl9RLbJN32GTR81lSWAyVmhVwP0_3-fhT9HfSALGvYbBu2ZMX7-PmM9_LossI8c-mfH7OngGhO5M55FfMAYnceKZeDUZxgWbijMmH3O-Bw7VUraYPniJQvBpjaqg2TOD1tph41P1SiHsvJ2q-OY_-r8kf8c8wRCWsgUemf2rbLwuWnrKWufcrgQm_AnQG2peAbyCHXXLi4X6hZRy_DogdTcXIdivgvE_0WwDcdVYEy5S9q2WvsY2uKTHvqiabK-h8B_f_v21HszG5TZDFt38zT0GhBt9eeoywR1TOViKxgh_M7cLg6P0UUUehw4jyt8TqC2u6Q-0l4IA8wO4l8Fe3LqKZBAwvkEY1q01ZCOf9cqLLf0Z3KyuudyFs4zco2AFQw9EnWgI5l17V2kcm5ji3AWpieqiXZj5f9YjR5XDX1NbOYcBJ9Zl7XpvS-Ktb4rlgppHy3kCfX3GkvfxYeatevZNvDtvnsPW9Up3zwVcf8N7KzcrAcUNmePUnP2jlaWBXTx2KytqOeqTb5gE6KTi9hXI2YM2JTT7I3oO1hL5QMDdaALseviir5-Vv0UHmWagUmAQZ6azbvDMhXzmRb4AqIX2Si6dZCrHbwZB0qYVIcQ9piXInOamryzIkOnx7A4w2YKD2cCpmdJuL0p-VrbuaMIOg1HFfD6fYugFV2X4l7-vdcC2JDCp6UWWMdbSJdgQW6IcmETG1lYjB8rbvh_5W45lqHwTxbftQNadIU4ep-uyMgu98-ZSsvI2n36yqrObPHTTw2_micQqATm5ejI_W3dOeTt6xbFgAAn-9i3yTeEFGU1N3BDbo7ndnf5KcxeiZGep6vkX2WSKmaITO0TkQmvMXx3FN7u6ATxOO6go-B79O5p9UMI9cG2Tw21A4sp2HQ8ejUyIm6xp1oMQncOv74VlTwmvgmgQHOqC6EUvnZ5E4VKva8ewNDoSVvcSoCiOD6FF5xd7CKKukkgFkPO53y9l_aXvbtgQ4EL76V5FPziJiAVclvDdc0Xh_pmg8JkssVR2IS-fQqhAtG688UwTOWXqeZjS85uGAabOQsvjzy1_Wx4dC7bV6NWHko8VN8-CeTlpdc0iLyHwQ_V2lgTlysCtenAvDQGGAZ2kZ2LWeWVJfg1zFCkKnZ0El5ismIzlloVoPXs_u0q5krVw1tmr3TUdu9kVj69wwvggHe28m9ceQMfoZW42ltQYRDf7Io7pHufW27mCpJdGHmpFUmj3OFm6eJVt2ybHxNWBIybAEQsoDCT853y-Jd-Z2qq-uA1iJ95jtweT38FeUbRq2kLFXN1iKwLoPqSuuUKfudWhzs-2Spb1gYJHlxQ_HqkNfM8I0qtRj3gJY5lOKLZB3pFFgDDWgHH0ALPxgiEI6FbcIMvPcaEJzMBJww1eZ5tssn130LcN0ImDOpG9SHY9WFXocuLYhcCwdpSE91uibnwoVEbJ4oyZNRhn7E5yAPZ3UyA-aSqj4ZhN2MdDdDPFMTIwBxzn6-8FJNYZPo-zD0hL0dcFEBaOTVQpKi_Bm2VQpaNoLva5BVL1g40M5Xkj_R6PL1JMAAjIn5y6gdXu104uWriMmv0x9EUHOb_LlOrXXHe-MPyjoe3jOIZSBKQD7zAtF_qBQFSdpmz63BiIBuG2PK1kjZ117tRSPzIZrgxiY4l-1fqtY1jzBIZzG_kykNC0eLPMB-NuMdQSaUPLvMA0k4PpYWMAzqCVU2JwPha7TTVHGbn77qZSfx4KnOdVQFdj9rQ9U2idFmZY7jMAQnVYHefGJTDghCyTJBRrXyOQS-IVkDbr8af1ZtHKGf2gWYQ3lZS-IBLc3YSb6qlTc-nYOjB6j1k7xj_1BMgv8_P1ZXQKkQg4GyEi-FTG_DfTlo0V0kASIcztDp84tG_iswh9ov_OmJ9MWGq7JH2NPiLZMHPWlKNRuoXcwZJq-ftydxxOZTWO4R41YMlI5n__osufkfEgqwJwZkCGuiTti0qRhPw-eQ_WkOwEVZ5FaK1w1vWmwGnuHw79JA7aCUIY-e4WUsp3oEjvl5jIgmsGfE_TzeawF_OeGZw4P5Ek0NXfVBYDQHKMPoYH58SZ3Jyj0UXNP1ApMPUZFgbxENfpneAwzqnlWXUFYu4dfFs4FLX9zyBDRsdS73qAZZpIe8_vtMdnHyRf0NhFxHXZrWInReuoDbp490SeAPI2lwD19AZSNLEHGfkQ90IJBtHMQYaTw3XBaZjfGvNH8nB8BOGWqnNgeIkjj84oiQGZmfXYOLx8VtNz3-_XrOWwHz47IP67FRHwCPKgPcnty9UL2B6eLpnV_9Dn487pwESVXJW1pUZSOk0C47xuc", "encKeyType": "MLKEM768-X25519", "pkE": "LDlJhENyVToDWclSyYcaamqm-AMGyZfM04FVB7Gk4UISujKxtGTNJ9IL0EiEy7iGMjai_hFlTfZR-WkJZ5Fyc3SSlndE-MJK5CIpM9sT8aeKetJ60nKzpoMI_xIYSQC_eWlwCgW9qzs019oOUebC6KOJMNhitrRY8QFFOds_TlouG2NxKgLPK1Y5XnaIrZGC9SWEVJW-7WMj-0iWrSEGibxn0RYRR_o8crt0mDMZvZAgGgGaQuRfUueUDtB4HNfBkMgoN3tPhvxwytRXqIQlB-opY5dgk9lgVLpEGXdN5UGkZZJHN-nOU2eD1nMqFHIW5hd6gEUyUwTDRdiX9ppPuwFdCBFbVJE6Gip7qWpK2MZVVJIXt0iRyfB00apPFsQ3ffdE74FQnCIDILhR-OI0zmbA2fNOEviXugeJZdTEyRViJqh8CtsFnjEwASxLxTQQ2kROE0AHwMAbK3Rq-aGciRmjkmFjcIFInAyAPSJxs4B924qV3QEjAeCvPdg7XiWcelC4MymZTsDPoJYMSAFS9vMdL6ksIeGaNUM_07rDh1Bu7FRiCYCO6ncP1Rc1jsYdTftq55uor0OFdoXH0DIuwOdVg5BnixWU4ctSbgM7hORDH5M2AERFFPe4exgcwgKi3BFUicySVrsRrjrAL-JP5GY-n-Km9tByCQRH0xCDDLIVa0pjzYGCi9cdnLkwjJIdy_RhUieIDraW78nAC1kcliWqEPJxVfx3LhyX8ILDocXCTKERjCUAtZFa0sclFvwB__p0Sdu9qZNWyCUj_9g5efxRG4aHO-EVHap608m6J4SZuQLPbBmzDhx_ixdvrIZjXSWNjDVqpPl_J6EdxCKZUWcBfAxOPNqvSHAnsxa5fuFbMLgbcIKXgmPNDRgb5yJJNpBC9ElxPANAQzgSxFJ7mFO6X3cYqPqaAqMDWWgrx9FBdxZjzhKkHKVIRthZM7mUprZsLLCrXeim0Uh-MMwhc9QafKg5I_m7YWYiEZrBFSxyErFg43NpKvQE0CQzBpG3ATYD-eAXfKKmj2wG_9SU3GQrXMTISNRRXDSHFUmOivV-A8QbYFkLZlGZe3EX2EOU4xl_u3WrB2PIn8gIlcCkhSJ7t_d0YUFJQRxw8vWDsLqD39usXJdhLCoI-CYi1Ay3HaKwFahJRuoybPa6tnKx_spngaHGIwOayalmyalRIssG3ll3k3m5NZhQ6nwsSKKj-EnCEAklUnltTPBqSZlmZYU93XKClQdlWxOxFPUetpdgxftsGxxv2QnA3YhkyseyBOBaY3rJagFLooA2a4i4-hNt5KRZveDG4jGqiSQxvjNDDIHBHWMftDonTnFTgbQDIzHMYfVvTWnDDna7pOEJWzK8FJSgBaW88tU8X5sGlYNOxPSD9ZeGZGJzxRAlwjfCjVQ64-aCPjUJWnhFtOKxmscQKqISe1ui_CpTnlauL0Gvh0pHlNzFPxPF6jHLVZE3DPU78FNnRXwVi_COqVM9_ruemamWHZshksI7owuMVYolTmud7uaHENqLAbodwGAvSuQdbRAN18ZMSFKCAwtBcV6TArgCjxMta2YeSPbOuy7Q8p1TRYfghQ3pfur2LLUQb8yYRQDSIsP_hb-ON7AMWQ", - "skE": "U18eGAUfzboX6GC7iBsTD4kOqA9pmCX0DH36kQBZYAA" + "skE": "U18eGAUfzboX6GC7iBsTD4kOqA9pmCX0DH36kQBZYAA", + "longForm": "did:peer:4zQmbLqu3weZtsnNesBzecCZm5FA6uaGkgCWYR5Wndr6Gzzd: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" }, "pq_bob": { "id": "did:peer:4zQmZsKmffnwYVbGLb6ZmZ8gkgyTs8D76YQRBt5YnFNK4Sno", - "longForm": "did:peer:4zQmZsKmffnwYVbGLb6ZmZ8gkgyTs8D76YQRBt5YnFNK4Sno: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", "sigKeyType": "MlDsa65", "pkS": "E3fHwPPFvz4VSA_VgU5wZEsUlBUk8TgMt8NVYFf0MyDTsbWUhMbwZgals8GrGYdy-WRRRc3IUudHYAiZonL7XjueeMzM-WV52zZOLVsCEk-G2ZsPGXSfNBGa1B6tVKV-J_ZvgMq2H7_gBcDSeWEKHNbi8B0Pqwq6eVI_plXTkkb2lnyhzhzt839E5_dUBNvCXlvQvqjgdRlaRV8ttcy8n9XrAWiTk87-79lahLCSP7rrTGulnMbv2V2jffRbwyDbAJb5lb1fnh7J9FFKL-LvbmMUN3TTsQvdcSj5UZsnqb7O5FCCKZDvOpBdG6yk9DVgc0Br3E43PNAIkNHafecpvG3jTphxAO2Vwe0W13Vwq-VcAxdY1Gs1aJJSCROY6j8LeJs26t_gBcgYndNQ7-2ExZlK2nUdUAtHex-73NOvWOf9CS5diU9Zp6xDSN4idQyle5LxgPPp0zzFvckKI1pE5r54S4oUf41sxzzayKkUebhLs4gQuCKDnwI3hg9EFaM4z1t3hXg57lbrk9ZlDeYCEEH-597a-BsnOfxzMpiNuj10FMD0u1XLHA7vHm2n1R8QpLfEc4Vsl0gWTUg4zg747Yk3oyO86-sjh6s4lwJWj-OXIxk9kF7Es_k4Sz9Az8BiPPP7MYo4XKaSchqEDhMUxf3TsuCzLHPLJ1Lmk3AjPphJRVa19T6n8BLLbzimeqTD2Q_1Df0j8LUrL364if98TJuXed7Oxr1ZBFayHJ5P9ci0CXe_tcgDag8ZTVmBhldxMIsY92OGvANEyxqad89tco5-11O69TRIJn1I1jSA1TWfCBYPkQh_uBQWe6llkSDIg1wI_2vd1COJCHkYSO7TdgmASEai_ATmp4ZJ0bG9QafDzphcyjRDuTNA1EOFGQVzN0_ffLaRlZRXmvPoDTvuLzpK2hxqkikzg9r8-GunMiXIlB9RIfuABJu-7iEu2LqXjyDOMXz53ghysFjo4ntpZNlliphGA6Z0-RZBW115ZDUMqJ7n328PWm_hjCmiImHYBc3nOR0MrnVHGTKig1d1DonX5-Qkd81aRpN7_mvvJGs7EWijpsXPjrrpPbarOCJxLbVB7idTUFZ8YeJI-p4uZRGQhnOb18XRcs2i6nzHGAkGlt1w0rbf6wLDXlHUiscE1PVXpzeaG95oTX_fEqTB6SASxd3LFmXhAjio1TS82IJmZtiu9XGGaNpxf6EptCY1H0PCriZ0jXMMXcw7c1dytTmzZzmBzdfse8kqZLOEcOjWn8LYYMCqD_S8u9eLlPOIBEjEkm5KCgatx8l03o-qCiAe18YwlW1HUwaVQHlbDOffLjuuOmfwNEEVZT498PejsmiJ-I3WolWYmELmj1gIOBe1dsFYbTnF5WsB8G6Yeu9ICagcBbRbJNPEmSweL6LIEq-W-dazqGrSF_P8YYdCIM1iB3k7VC_oP70_Zet_tdMeOKzNb-C4fcly3WUMVd_DqiyzzANd4JqO_378znOYZF1XWBdh2-VIA1_NTa_I9CBFgJDZepHJnlOPbskq8qc61ZG_WXz7MmdJe1QK8jBFjarJp7q28h4937gjoppnWSbvQNSwnNK3U3QnyT_HGCCepTfLgHU9mNEQ-Y0lBwtLprrOR7x98LfxQrtAQ8XhsKqrHk-k4o5T6QcmAtWcSmGXwRDIGDKT9hjAWI1JtDer_HDw3_GJDpbxUZuhbVniBaDOjSBH6jHUUUJlEnbujSzOdS__CLwehEkbJ8o8CAUcfE1b2D9netchY7w77Pfdg_DcuI22gWWqqcSVshGhG9aJ_RWG87RaxX0OCkKek1JxSbSH38KdrbxqotlLoxJbRgDxAxViEZYUBesBJgomnFgYTpdroOU_lwWv97s9FDoVa-dU1oTDjfrEDdkeZNvZm5ZRF-5gxx4zBytSdh6favFK8xJPCGGrlQcB9HGMDyl5uIgo8BesLnyj-ITsK39RAbDPUP5GGGCxcKNSalO4kwXDtjvPVegOuOdbu8BIyVi43ye-YcOm6b3fuXD3AQ6w-GuVwlJifBtAInauwP6MKXg3jV_Lr8QCb6oIKNpVCL93dJhE_ldoJoVC06NL4MLNYkkQpmLN-Zxc6DBn0z3x0rdJVrIEUQGXurHMW66c7v-uiIGLexvCd4O9lKm_8ZApB6hYet7MPkAXYBOX6SEnpDuQRghZeKuHOlpoi_RBBGlHcy6qN-w4fu_-P5RZk6A9M7XwE4qu4W1gtMdmZOpZgX9T7KY_L39g9VoRpQv1SqfOAn5uX2ZRM5zatWbtbcgr4ZwHQ97BPwIi2KF_w4JhWB4FfzQufjLkxp-WPecCWr9an98mDhwdPJr1-_QSQPjM-ZOl971L1kVT9WJPQc3ErIHQHZwXPvD7wjLt3cmHVRdaspKt9qqxAWBmyZs4zqahyEgS0Z3Ds8DZXwDT_xKQFvSwR3FVfcMtWLs_xCCmIHKkxp8myRpXS9WOBwWD7am6-isL3EVJXlmyS7IKxQlpWWttniwnMpIw80ujyAb9EZpCUkCFu2DpWzEd44B5RMfFrDNO4uZaiyFxsZJyieKv-zTtQs8BI3WR4qdtV2gTQpM7xn53bzpSbBD63emxl6Nb9mQ", "skS": "E3fHwPPFvz4VSA_VgU5wZEsUlBUk8TgMt8NVYFf0MyCcFajzHL3w3t4tiA8DnPHO_LietFcKL1Rn8dSJriXTTOsJwQ2uNPpvZbnxfxBVfk0x7FhsZIsUCMc-M86km5LsYgBxOAkkR-QtJ7K2LOoiCSPFYTgZzwQD45C7gUlwg7NIhlBiJDFhAVOCVzIXMjZyQnhSYAUBZ3iFOBNSUAMRImYxcHY0RSiERAdCEHgCEBUHg4SEUiAXRmGCSHcDUjYTdWMDcRNDiEVgMohyA3B1hmMgIhV1dgA2YiN0JlRkRWNRBEE1SHMkVBBWQlcRdSRVERdVQRV0SBN1eAJkQYckUYRDEFFYNIdlgBEzFQNlMVGAVgYSOGgmNTIIiCZ3MDVEhncVQjQ2ODVIdoaBF1VoVxIDJCR2EhETBkKHQzZmR4MSFhIhYBIyQVeHNzhWdoIzNkdUModxKFiCMGBUFSZCEhIjMyVlMjMGWIUBQQd1FCiFhFAEdYdHI0gSJBJ1Zoh2ByRBczJFZkg3hDgXMFSAJICHMEgCOFd1V4JEWHdhgBQ3MlJmQkdwRAUkADiCIRImRGV0FUGFcheIIFVTgnFQZmc1NAI0aHU4gDchc3MXeFEUNniGB4FBgzUAZ2EThjNSIRRERSFHcoVHJiBIASiDd0hmgndnWGV1EYdBcAgEM2F4MBgYZCFoUYQVZgBlc2dnVkFRARYDdRFVABUnYSMTRzFRh0MDA3USUIYCNQhwRWVxUkVXF3UUYjAgZzU0dmAQEIOHR4hhOEgxBgMWBDcXUkd1CHMUAHFAVXSFFYF1VmA4hDF3V1B3cgR1FYSAdyhFgWRXSEECETdyNFJFc1WDSEJREBUgMEdEQAGGJDEQVQiBVCJoUAZkMEIXcogVeFEQN4NHNjIneCYEASI3GFIyBXYSBVRVIUZUeIczESVFAiEgUihoUhE0FGBYRVAzhjFngmMDcTM1QlBDZjFkQoKGcIhIR3IQRIMDeFSAGBGIdRJxJXB1UYc4c4ISVFJlBXYQZQOBR1AhEkCAeFJScCQzEmIyNTJHMDAxOGRoI3QkcWYYExNBASGCAEA2VFAxGHJgKDN3gVdiQgJzKBRneEgTFIFXI4VUVjNQJjCHcIdkKHQFdXQBKAYlc2eANGUlBHVoB0QScRJ0YAc3VTBxZ1EEEiWFGAQwNmNUFSUQdiYTJXeGJSQBQWZCCFeFYSgBVGZQUhEXNAN1ExYkdHRjAHUYF2IiMGViBjQTRDF3NQdGU3BDWBd3RSRyYFSDE3F2c4Y0YDgEYXMDJjeHYCURYmgmIQNnSDQwFyZFdgA0AhRhQnIkIUIRSAAQVwUwZ1iCiBJECGRnY0IQQQIGEWAAUCcDaDEWIjZ1hkIHAUiDiDIxYHQHhgZkMyU1iDcBQoCBBVBXB2BGQTYIVmeIaCSINTNTcQFANFETMgKBhShYAoZBZ4WEUxMFJTYRAkYiECIQOCGFYwh4gwiHEjA3EBNGEmVRFTdlWIQIZWYUJARjdxIhM1AzIFQWQWNxaHQWeGFxIWaIYHglElQzNQVIAFIyQyiGaBN0goFDBGcAQIATNjB0V1E4NlQQVxBoZYBAgYgCIDQCNSNyIlYBUCADCHYFFwYzYQIwOHIzNmhVRWA4YDdihDFhZlVVZQgFJGdoiIhVMQQ4ZDNTAggkJAEEKBVwEXSAMWYlRSI1ZTZ2QDVFFkVUcyWDFRUScocxVWBhgIiHhYVwSBMhhwiHhwiIdRIFcVU3h0VRQwKFhGJ4NlGHhDJ1JQIgEiY1GFhxN2AIAEOIcDAFSIJwcmgIYIgXdyNDVXgWBwFhZhF1M2ZFcngAM0aBgReBVkEHh1AjcAiBcBMyeBJCU2JRhWd0UndmiFFwQgRXEIFoJWQXBUZEeAFiQjAjAiJhZGATIyFIJAcYAyIoIYKEQII2MQFkMDQXI0ZSRhhjAUIRRCeGgFOGBGECAmAGJngmQXYTJSN2Y4BhRYdhcjgXKAdWhCIDg4M3CGJRQIRXdBgVEQURFFVmNycCVzJmYhgWE0VSJAVoNnZyhxNWEUcGPwOZJ5PpSe0dmUdmL-i4fNu8uQiOIiTy5QagcmszKyUsQ0wlvrq8XugBfeEBAcTq8C4JxlYHw2gJfKe7xEvIDwN_-0RT9BEZ9ie-Hn4LPnNBD2xPzwGYmflfc4-q2Y1gVPQ6nW16Hyy26dY91s5Pf5v3XWVDlpSKn8pAf7iKKle5o-Vasmtsi_ySIETw6N4Iw-OsWb48vwQdQcQEL09vUUxkGoodnfoA751RtQ7aSkyZix3IOlac08Iw3uMvIyK9oKp1xpL3Z8wN0GJCAKiV-te8p1_B3HgQgMXdm452_wcGehu2ZdYkyWEkgZzql8NALm8qDsn8XElFKhdtMiCTpRLkgk_pq_oQFElY-FolMaLbQq-tEjF9vN0EbqFzg0G30-WbFrEj75kUZoHvslCyn2QVDgCT66AlIEIc8TWIm6L0V3p2pEnSGeR6k27sUT7IDkRl-EmlcHh0KQsnWpZX8cYw--ZOX5iM3RpqMvlyAONuE3A4cmBZBovL2Dh6urXfrf48Er0TTv6k80L52M4tX-s31aq88ExWDSs0BO6ZW-MwI21LcHvE4dlwiKFcWCJrjNl5M8nSe2D57xrCSYWXd-QJ6AER0kacN225v5mbZ2aodUFQnn93yB6kd-nPkceEKjMtpEzz_fz7TliJjDbfD34K9p0QixsbudU5BaKy7NKETjTIp68wdygASUJpNZHhak9immFn6mIp_d9jHZKtkKQ2CuXlDgDnG_CwejxC3iZlg_-Ba0hLFDiKi2vPoO0CueToCnCsdRi3MNpF_FKITvH_mxzZHZJ6kFJ9kzbS11R6ZrAEFkZ18OM5z98X1U5xqmkEVKT5_iJx8Pu8fgn-8o8WkbBl96goiY3RKidnEwKRh8ENFfTocH0qbJHDZGfyUVioZxUpaQ8GVNfsUEUmcr8eqW6MNrDu606y54c5Rw4DHywMEvklG6nLd1G3u30QMmmmQ0u1wJG9TY4iFRUK6NavaudfMRaYXw7zHMj_SKzNBNgV5A4wiOvAsdlQoRo5_AKsDkdTH6uKdaKg8EYbjVIzEmghoO9p3sdW6s9UgnePioFPQE_HeI1wE5XB6xYERaLrMHN41qQvLeqjbis6cIojg1gm_PEdM_W5G815b2Xg8R2psr1bAQlRyz5XJq2QkhxPlyS5WkvkGAcU5DqtUOSt_NTbgdqV1eZ79Tvp9_ZbBzSPDq24a7StrHf7FNfooY8J6_egZvP2TYGtwV4rz_QNxI1Dzrh2ltIlZjjLWyPGCulNsf9_YGacEomwK3wERDYMIvOMWyCb9K8cdi8t9KqpySnzfl2ZdMSFGZ_YW6boipY3LvIjCNUtqEdcMAo76aD-UdG-dRN4kaPpIdbtBTKp4HS9qHNyk3Wznpco2zj5gte9J7RrvOlR9OuVwDOm_cfNSuY6URpnkdYbSy3fKyMUQ1tQWujbSd5_fpyDXbsGmdY-zB_zWEhOXScL2lK7KHgJZxJj_G9jjk8Ro2FmAicUK5Vd2MeVMBlke5bIgYjtUgMX4UkobSSyGMAbid6e-ErTAahWeGxoGdC2LqcgJxKGAhEZqvMM0DeUMS4gZmdEA8TXqAGhodTI9xNFecDZoN82pC-QfNHnb7GZJulCgHXLBC-AE2zqx7PY5y0Zcfl6sPrFYBVTnixewwAXes4K0ORO3SO2UFEi0WSTVkMjj4bzZTwlhQ3VV2c1FjznmOCbNJIJ8TWaivkUmYrLCpTWf5muFQ5nMMj32AZ4WP7vmG4kJSiEShPX9QbeFX8_lc6m8XXpd1-9YTGYSRPukYoXUJg9twdx99d84oH7Sth5jSIfSYs2SDNfYQfu0OzaEkRBhn7pZSPtTR0KaGeRQy7zdXXAVp1dA4aMuk4a3OnjvzDcBaE0h9uYOA8YN0rgHzPzMe07f6zUttgxYyW4Yg5dgCknKlt0ocImI-us22r0guisgLuk7efZnPrQl-Jj0pVVituB4g869_9l7_bD4SVynhJoJynGmtBF3yITp_k5LRbvRysW9pRlYFVchWO-PX17ErA7gChnEiTw1IFPMHZiGv5z1_WLq1nk0I1O9hqCmH59t8yOMMbpDMA8eCvDMY-mdg2f_rk5RRhrUCWSps0L4S2OCLS3Cy5ZS4732itmBmvnRWCIOk9rYQKGjN67xLBqAfl7Q1y0jx_7c_kHMV5xbLE6TEXux6--LyZHWKEF3OCH81cthgcyt0yG1UaTp3NEI_v5aWuwUH4l4UsXCcJAwiL9Ed577GNI1YbC6pwPOPcn_8hPO-BdwMW7sAdPTgeB7lKUTT4unvJT_Ie76CmqhathL67pDD-rFQBUMYjcGISyorczAdb2dKN7-MyvGuDpb9ZfMM283gRNpcPEgYGzTtarIEEVyv9diHaIFKjd5nD4ixAqJKBYv8CUOUpB2tCpXyM_kss_HdO8ovToK3QPuGilavKSOgZH1gT4IQAgECb7dMDJ0AwtHVlmme9Orvs4Ksf8tgmT3R9mKWaGFRqglAuTSyOVELXaPVCpGPBn3mmpjSbZLMWFwS8Q5K3imihRMwgUtoqvurrUy6FwnbKOwaPszKo6hmH_9OhY7HXD57Iefzlpd9nlc8seIcrPBY9mBlFOmqIYZj31hJU3Essq6RtzU4qaTREaZVHMTP53ZJzG-bVpPMjpv4XrBPgxfaWyX7mmhJXZlLTseZAm5bXyJ_SntA4sLzxXWIzExaaef8lN0aqsoLd-lA4mw9W4g911Qin1oWmuXIjr34HoOvGUek53jf6unxmkwc7epNWdxxBhjWmKZzHDglaJjB960hECqDXlKUHGXnhcAdV8d7AF-hg6afLXc_hfi1n5ZLDArmTK29Q6VEz3Jtsq6U6GdGDlPVvG_aZqCUVbFuP9Tj-dROMEXe54Iz3oF5OfYQYoicWTynEPXHvmv48EJq-vtY3tMFutsKNKKSGkv8POR63-zFUlmbNueDjRxhRuZ5VYpK5QCRKWi27D3J0iPP5hD1KznMs5UAm7OzqssAB_x_VDzhaWpDhsVL4xVuNi4Sr0am_deFivikUpoKUZMoKtVH9QuzZ88a4BGneW2Xe3HonCIo-Gjdebo6W6vbKafRVAO9OGKziRhDki4XZhEpcYksCOQQgl7K6dHus-_tsfPVqn0asVptnnE-Jz0_WC23H5rpMfJHFuLbbNEn9-bcWdR3nhgRlIF4RrwLVaf4qzR37Z0GFPX0AJrkr4LPS_B8G9u0Ci1A97y6OfTH8BDknSom7dapD3px8_dzvS-vvJRt0fTMfZ9TauM_DtaDbJ9uO8ahZPIKoFsRte2NtzEVaF0jCPq252OcFRv-fi8HaljZkc", "encKeyType": "MLKEM768-X25519", "pkE": "CjuwTpSwD7NFkpXPXZdGLPQB4zwvnVYVKXGYnGljHQk0ojR4nLMQFtuS0ncgAiyXUoaHgGAVFIdCnBeLVIuVdlEf0Jy7xXzORRU6cVmq9rF49OtY4YFb1dor56oe5NJhmvWMbGIMK_cZPGGbYAZYe7A_JjKMUjB8eHkDcMql4bal8ialj8EZRNsdlkI7wPlGw1ocvDNgC9sdgaVU3_HLbqRy3cGAjtLGU5keIBFZBRZ3h3WJDiqFtIgyNMHCRFBwV9ULsNey5XiJKItjJFilF8JUCUapMfJwKZg41utgl2pemzBiHUnHcQNyR2A9ljpGc6SAjJpNVmp4LrQXAaEhoeMb1VqK6XMt4cRVlmCiMfWNsyyY5WZDArYMs_aSN5TPDQJWuxUTSHQG-wxD7asfzXSPZTXPdsR7BRIlahKfazuSZGbObICOcsu02mldPsGts5VmDTsNfvFQycjGjMGTfhcdg6OzfFNO0iaFgNxM-xkEdUaYKZOf6TVWxurKOUWExDqDkZavQigMr-imb2dWltsr-nicngYSQkt-ZGx6Z4kbuIVx4nuH9ValTBM03bvIaMEjc1U0aUODWFzGtmVN0gdqxUYwyPS6-xipzGt3LYY4FoCix8IWCTthIxNCK0ys1MtCFSF_BiPLwvbL7koFzQcbpuq79uOgm8eOJzGTiQEmIDS-CPC3izKlvepQbLLPelYoDyUPyKilHzq8NsiunORnD-N0Vpdq70kT4_OysbyKJrxE95i4valbYxoHmLmovtgdudfIebGVjyJbuYRuZWQwFpq6oHNJrOgugwl9IeKwM8U_8FrP8xeLz1dx_8FwSXhcy4VF4agzpeFLlBSCqMwr6Zmg9DIIpZoFfrPEMBkeZ0An0wKGfsWkOlK3BzCgDiBdCwKA7aJgMTpEp7k-apPNYZEnBmiTjnmkPHQuLJPBJHN17qUH15qjWTG7LWGeTKazMHkeh1JkKEC_hlqWDOVht_Bff-Wmw2IWaIBGR0wOkKoepFBJSgdtEVSo2ClCgoaAZayZneGQXDOUpCAz1fKhx6DAMgUs9zyLOfEHD4xh78WDjlGnrDE_swtkYxQQB4NNV-OW97JicwleFvPK40p3ajNKdCJdUExDRXIdjhmDgLML9KS0JOkDBWdacnFqT1dbZaO3r6eYi3UJ32IxukJSCjVnNMhn-5LAyLwjbwwdDoitSRlF9aUE8VcVE9CmGSpP8nrN7ykLpEeSEsE0b2IURvaVvgg_WTQoJ0pamDrAWcidG5ZpMTNOIidpqxawNjiJm9dJ1pF0Xqc7SdSR9RqPhewCB_yK9consyRuznlH_VAQqSEovJSZQxOHtEq4mGhhHdcOk8GZJgOwtuhMm3CPBoOfaUxTeFV3OQwPeAq3GrwSJ2AYd9gLz5IGrGY0hIp2FEg1jOYYuJmxg5A6xCuZG6VhWUKnWbya47kO7seKGdmN8UAZ3bOjw4qQyQMJE8JsgyM5nfwVqLrBF1aGZJJ_5XYok0w2ErgTNLohapmmDGha7EpRBsBiaOLMgnqwXM_m7jARCM0X6ccg8WDwuDxZndFwXZzkIc5zs0HY4usCp2THOwDodeS0VXLTpwYfo7zyF68l8XetCT-GxlxSEw", - "skE": "CDZNS-gKPeJMsblGZC33c-6rrv-JsuuVfitTnkg6wnw" + "skE": "CDZNS-gKPeJMsblGZC33c-6rrv-JsuuVfitTnkg6wnw", + "longForm": "did:peer:4zQmZsKmffnwYVbGLb6ZmZ8gkgyTs8D76YQRBt5YnFNK4Sno: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" } }, "vectors": { @@ -93,78 +104,88 @@ "receiver": "bob", "skEm": "EgVuWV1WsPbu8JDwzSWiCUkkjCeQUl0PkwIY_wtN3RA", "pkEm": "wfprioKR4GMkMcd24cr7B7sM1_iy88nTHbvkED2YVys", - "message": "-EBYYTSP-AAC4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB4CAtwfprioKR4GMkMcd24cr7B7sM1_iy88nTHbvkED2YVysWC_6GFqzGqxjBQhW05P2nlttvJtG8bu8WM2kZJiLlOeT7b2Wg0URDRYhOFeb4EtB8XpEUW6KqjqW_zwj79vMTAdrf8vq4u27MbDCQpI8qzeiGLN8B-tak3SI1vKAADx68VHVg5TQM-CAX-KAWBADatiGf0keq_Yh5pDYzDpKoicsevHubnS9RNd5rRX8Sn21puLukElsJxroPtCRXFYTcwkJqazpoWpCZAv95sW4M", + "message": "-EBYYTSP-BAA4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB4CAtwfprioKR4GMkMcd24cr7B7sM1_iy88nTHbvkED2YVysWC_6GFqzGqxjBQhW05P2nlttvJtG8bu8WM2kZJiLlOeT7b2Wg0URDRYhOFeb4EtB8XpEUW6KqjqW_zwj79vMTAdrf8vq4u27MbDCQpI8qzeiGLN8B-tak3SI1vKAADx68VHVg5TQM-CAX-KAWBAC8WRVuxtb3BbVZ_koe0_wQYoAUIYuzDOKelTR8u9FwO5USoVI7ekbAlSLevvkUqMvkK0KyfNpdp4VKp7xt1HwL", "payload": "-ZAcXSCS4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BAA-AAF5BAEAGhlbGxvIHdvcmxk" }, "direct-hpke-base": { "sender": "alice", "receiver": "bob", - "ikmE": "zmfPZS31ZecVid-0CvG0x7YPwxkwy_8TLqggDcwUDJg", "pkEm": "1T4oA1pSbehBiIwnoXFGA24kgHowT34VdE95wF9qjSs", - "message": "-EBFYTSP-AAC4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB4FAa1T4oA1pSbehBiIwnoXFGA24kgHowT34VdE95wF9qjStBsok4fIkbu8IKODF2nsZMUAmS5BqxDbbYrvl_TNFj6rHuLymWkvlrkt54-cOq-CAX-KAWBADw48mquyWI0O40nsbcI7jRugSJko0JMZR-4S07YFJq3Mzy6jvAkEsDGAecW9jCUVDdwZScckWUaAzSSGbNunMN", - "payload": "-ZAJXSCS4BAA4BAA-AAF5BAEAGhlbGxvIHdvcmxk" + "ikmE": "zmfPZS31ZecVid-0CvG0x7YPwxkwy_8TLqggDcwUDJg", + "message": "-EBYYTSP-BAA4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB4FAt1T4oA1pSbehBiIwnoXFGA24kgHowT34VdE95wF9qjStBspw4fIkbu9GOQVW07qbNMjem9i6wNJXO6NhuEfPo-jSM2IP6iUTRBAoc96gQHCDKDVyMkTQdM7jaXZx9VD0D6c-dxuJXe4-KNZaQ4MMAYaGCpFQ-B5wfHmsk14WV1x_gDULqviid-CAX-KAWBACcDk1sZY8TSif4hGKZ6IMsQllDLVoEzTuLH3hwyOFjEIV5hyscSeYZd_hfMo7O2SPmhjCw03xBh0qf6ZtLilIO", + "payload": "-ZAcXSCS4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BAA-AAF5BAEAGhlbGxvIHdvcmxk" }, "direct-signed-only": { "sender": "alice", "receiver": "bob", - "message": "-EA3YTSP-AAC4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB-ZAMXSCS4BAA4BAA-AAI5BAHAHB1YmxpYyBhbm5vdW5jZW1lbnQh-CAX-KAWBACLj6MsdBES31rSLHtwvsl399JiRr8guUU--hauBWZ9zftvcY7LWzfK5JyEOcqb2l1F5c32MNtpDsQjmQgn1m8F", + "message": "-EA3YTSP-BAA4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB-ZAMXSCS4BAA4BAA-AAI5BAHAHB1YmxpYyBhbm5vdW5jZW1lbnQh-CAX-KAWBAAOGxn5_YhGpaJYlO5dvRoVDvXvkoXs8MXJu8zyqP13dxaFGADFYq4BRCC-2msX8RqIconwwPGbWFJraQ92RFwO", "payload": "-ZAMXSCS4BAA4BAA-AAI5BAHAHB1YmxpYyBhbm5vdW5jZW1lbnQh" }, "control-rfi-direct": { "sender": "alice", "receiver": "bob", - "ikmE": "ew2j30RF7AyIl6ompEoE-0acQb8yg_Iyk303CTx8HJ8", "pkEm": "U9I24IIKCz-mPGP1IZcfMZohp_eBlv-BBlfLm7vbUWk", - "message": "-EBSYTSP-AAC4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB4FAnU9I24IIKCz-mPGP1IZcfMZohp_eBlv-BBlfLm7vbUWlcL29m6RzNgGaA6llzgwarcJ_sziA26tvIdjNKso7y8wD8jzERbXsSQ_PA0oxKauyhQSKfn8OxuivWKEaP0_r0j9Jfxnh2oanEUeLFG7wJ97xLQfnY-CAX-KAWBACNFhexhFZuxtMYcR0SnzKbkofePpeJJlPsgTyHPUT2YcG2GR5ELoNtquUeu5ltysJtqfDBwXgi2UvPhsSOJrkJ", - "payload": "-ZAWXRFI4BAAIG6HKhYGieW7r7cADGj6gJ0aMB0rNFf6IyDgK_u9jFE60AARERERERERERERERERERER-JAA-JAA4BAA" + "ikmE": "ew2j30RF7AyIl6ompEoE-0acQb8yg_Iyk303CTx8HJ8", + "message": "-EBlYTSP-BAA4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB4FA6U9I24IIKCz-mPGP1IZcfMZohp_eBlv-BBlfLm7vbUWlcL1Bm6RzNgHXE7bpj5WVH5x53A3FX121-2mcZnuPxzZW25Kly2LfpfINIiqkoCZX2I3jF3r2W_0iEVD86cJ5qb6OfAWbXv9NkPtlY2qqu5kEwGGswgTFwm9MLPfYbSoFeex40HyZ2SFTKX_Lsb1D9JFMnkitf1T2x7pWbLWwH7-zEu5IYHrjBRNs0kQ3J-CAX-KAWBAAJOX_3pC4PhLp8iUX8d6OA0g5NjZyif5THck1Ip2ihku1V5bpKIWk7Cc5ljW3lPyQll0G_F7luI-mmrzZRG2YB", + "payload": "-ZApXRFI4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBxINce082jnb_QpUKx6O0LU8JgGHzLjbq2w1zT6LDreHfL0AARERERERERERERERERERER-JAA-JAA4BAA" }, "control-rfa-direct": { "sender": "bob", "receiver": "alice", - "ikmE": "vU7UpVjzd8nDZDHJ1NKaDdwEzUDHI7PRmK1Udx160J4", "pkEm": "qVZSTmoBq-jrBX8d1pa40Yk5J28FT14vIHOh8-GN6VU", - "message": "-EBVYTSP-AAC4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4FAqqVZSTmoBq-jrBX8d1pa40Yk5J28FT14vIHOh8-GN6VXzL1VMQNnLb62gdEZb0jn-MrOyxuuFV3ljbqcDOSS01nS9XMYTzOI8JqthJqS9-0Y7fa3YLQvlZmF2VPPfUxxkzmGMFsguP9GFj-Wy2_jbXRvbLGRyd9n5Iu0x6fZY-CAX-KAWBADOiTPO66uSeX7v1x8LVcVGkiJvoRaLemx6g7torYTWAC2fXuVzBxTpBBWUxqHq-feLHLlPF04nQtVvKjwBY88D", - "payload": "-ZAZXRFA4BAAIG6HKhYGieW7r7cADGj6gJ0aMB0rNFf6IyDgK_u9jFE6IFVD0MQtgrqunFx5ALtyRt4RXR8R4umLVKETH2iu5Z4h4BAA" + "ikmE": "vU7UpVjzd8nDZDHJ1NKaDdwEzUDHI7PRmK1Udx160J4", + "message": "-EBoYTSP-BAA4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4FA9qVZSTmoBq-jrBX8d1pa40Yk5J28FT14vIHOh8-GN6VXzL2BMQNnLb77kc6VLtFoSpTIpC7rkZe6gss50E2Wx5PnKFn5SQzvUAOQrGoxeDCj89WAJEyu2jXJX-q_rFWPyFqv4YMXimtz9rhT3iUk01bLVhWtdW8LBFs_dUx38BYhn5Fdbif-SpSAtfaDYf3_pyZydt52m13WlhfKH8fBS4TzIyiJCjHueTV2eiF3NzC6ULxA7arRz-CAX-KAWBACJdltbnwRnZaCm9IgwOaEiZJIEuAuWJMpot0oAOeIsk9x6xf4Zda7VzDmlVqqdQ6WJBRhuiyzNIiYswfohjokL", + "payload": "-ZAsXRFA4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFBINce082jnb_QpUKx6O0LU8JgGHzLjbq2w1zT6LDreHfLIALaOO20VPlQYNeyfXABltdTgVzuabrHeE4JmsBTmKZs4BAA" }, "control-rfd": { "sender": "alice", "receiver": "bob", - "ikmE": "16WX82PmX4bBjabLDE16md_SNXRJfNa0xPn5_fp6GVE", "pkEm": "OLoHxJkegv3Q68EgUa1HPL_ELnoSkq5XE30aiOOC604", - "message": "-EBKYTSP-AAC4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB4FAfOLoHxJkegv3Q68EgUa1HPL_ELnoSkq5XE30aiOOC606B3B1YEcvuD4SDhkXCqlpn1_EM92pZdHHBbSC9yCTGuTFQpXadBZ21K0ReTbkUqjyX4lDkzJyHnFRHC20C-CAX-KAWBAAP0xHZu9aQ3ipUAO_IlZ25t-YZycYCnzDoep-n707EY-MQNqkRqA00M3KvMwBAV_iV9_jzDJ8TXrKiirrrahwK", - "payload": "-ZAOXRFD4BAAIG6HKhYGieW7r7cADGj6gJ0aMB0rNFf6IyDgK_u9jFE64BAA" + "ikmE": "16WX82PmX4bBjabLDE16md_SNXRJfNa0xPn5_fp6GVE", + "message": "-EBdYTSP-BAA4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB4FAyOLoHxJkegv3Q68EgUa1HPL_ELnoSkq5XE30aiOOC606B3DJYEcvuD5fHgabSzDmLQHCXOjs4Scd3wXTu5EnFh6Qazu7-sFFOFDTmBY2TzYwEJaMmVqzxpwo2CwnXOrT25jMOoZIV5Y7rSJJnw2WiYHzEzRReA1Uyka8ZUY5vXpiMAB4ltQ0uNnweaX9LdSlA6bNmZZHE-CAX-KAWBABNwBSh-lRnUTzdNvYvY875KhmJrJjk7K2KUBCKj7V3E_haKu4EgJC_CwC00peg4kgUpuhkI_cXABesAoKDytAA", + "payload": "-ZAhXRFD4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBxINce082jnb_QpUKx6O0LU8JgGHzLjbq2w1zT6LDreHfL4BAA" }, "control-rfi-sealed-box": { "sender": "alice", "receiver": "bob", "skEm": "Z4ALsIFbWmoNYPFkP6mR5CYbOlwk3Oa07reQ9j9BbYo", "pkEm": "DvRIPYQKw_paPzePKdSbkGiRcRNmjYHhx0gYr0S543I", - "message": "-EBlYTSP-AAC4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB4CA6DvRIPYQKw_paPzePKdSbkGiRcRNmjYHhx0gYr0S543LPuthiS9TykYKBa5GgjpzVaYNucbfQLrI9Q_TfieiYffgOvSAPhO5cAisip55K2b2AbuPDV4Zk5EnAMy7-470OWR2mI3ouzQl4meT26JPSyg_rg5HmXmMJWnalpAWn06Gal8YMCDRbZ0255gZ9Oo_cZ8lsjAt2L_c9YW-tI0x9gtg3b53hlA0RJj1GWRop-CAX-KAWBACZ_SWKSrGs0IJizGHSPHgDFXfwoB4xQFHecjUVbYA8d8jPfVge-5Lj3FW2E9gY1xx4to73eQgj7fc_jiCVuNAA", - "payload": "-ZApXRFI4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBxFCAm2-rAs9Ae4dJYRGoAzEhQMvYDNDqdqfjZW7CD8cjB0AARERERERERERERERERERER-JAA-JAA4BAA" + "message": "-EBlYTSP-BAA4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB4CA6DvRIPYQKw_paPzePKdSbkGiRcRNmjYHhx0gYr0S543KVlMk3yL4W08OWfBVHFyWhaYNucbfQLrI9Q_TfieiYffgOvSAPhO5cAisip55K2b2AbuPDV4Zk5EnAMy7-470OWR2mI3ouzQl4meT26JPSyg_rg_6tE5FNNG_K5ijuTB2B9nrzYn_noJeq1c-EhHqS9tvIjAt2L_c9YW-tI0x9gtg3b53hlA0RJj1GWRop-CAX-KAWBACvVQ2gIAdG9LdKR_Pv07EN9DdfsS9lD5OW9adiBM_MKVdstA4doE98Bp4BV8bRtFnLe0emL2_lPCMffhJeXekL", + "payload": "-ZApXRFI4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBxFE9tlhiE3clxo_8R29YbrfSvWL2_8-COmjEg5UXNYNpl0AARERERERERERERERERERER-JAA-JAA4BAA" + }, + "control-rfi-referral": { + "sender": "alice", + "receiver": "bob", + "pkEm": "-RSFKGq436VjiepfHhVnCCTFmsruYqlGoi6kbkIKqU8", + "ikmE": "WZWlZqGZoqjRKMM35-_QVQyC-s46a7F4uOgaQcn-E7o", + "message": "-EFMYTSP-BAA4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB4FEh-RSFKGq436VjiepfHhVnCCTFmsruYqlGoi6kbkIKqU8V0SLvVgpSNqmnjHxmrlmlkQMjtf6GVCryJ9XcLFYl7nIJhsWUYacqWq3-HH_XtQCPKmuPx1-haBPSqPOP7QHW9ij7TEmZ9rBj1ix7fFineqIrkIsr1U5md7mXF4VOU5Zf3sR4YtMlub7zLxllc16mH8XrmCXMPtGimsqgz4iQh00MQoX-dRBwi2C5J_QhAmWGOLyLablBUGH224zmZKBYW5x1uhmpg2C1cTrby1UAbEI3BWgeDsJ09X16dYw95nEOy4I71n1r1gj789Ee8aVWVFUnvBEx_78IUChzSqAlwL0wa3vs0QT0j4VmJsWrHNejAK6CNGTeDsHkfxd9hCBIeVcEsUbiUH9dvcoEe7XNCkweg2zHzA3rJt9_mMS_03EYzjl_g4YfDsn2HLwP8b1qjww6szBleeGCm4hXPNjZyhtCxC0nSz_NDKc0sj0YDm0gGLFpHZEyZAy_ln_gWio45JUfID9XvCFWxK8tzccdveqlPntV0E7Ylu51hSlmRiWWtfPgMBIDNdHskUmxaHXUN047Cvt7ffhAi4emo63ew7H3cOSgta8SQArm6VYNDbasISrdiLVrS-MimRFFq3W1VDrv85n8z0Bc4HnB1Xwwe_XVdSx_EGf4EUeVT06_2-wlgmhpAdIz5Whxxlouudyzc41S0iqNVikFokkEPf1i1vzLj80lBx66NgTb558SZDtLteipbGG1ryOj9TZSBo1RgHDlRGpzjTFMDOe32p3690Z40EPtLVjnpPfr1GeuaKJUHDjjIbqf9u4C3XTTn4VRyRibXDpZsT_9GBewFPldVsGZ_7hZAEWCJqxUt2hA42r6pAijO7yAB5Z_nvyvClB9N7t_cmzfP_EAs6va5bWv0qAZ2R9yTtRJqU8fAO8-hMBYfWYR9bACiRaR0tw7-7flHYQPGc3yUOtw7w6ogFKEcoLDM896Rsw0oWjsMUR4sgZSSWCdsNnt6dxQlwLOVmMmSenoM3p4oQxrAs3kh1UzLseOL3Yf0VdRQlYMIGbzGr7_9o8Pc-nZOz7krPhTrx5SgRVNqPvrKOlUzcvjswPJI2rgGp3PIqzgHhClYE3IYr3RQYbH7yDrqDrlVDAu8jFtBXo0-CAX-KAWBAAvL7xYttu5pbrnm2zZA6S3gw6rdWc-1pM14rnzrRuSfmF2AeHNUag0Bb6x5khkx97f5MAAGgNbSIyoTJQRN0YJ", + "payload": "-ZEQXRFI4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBxII0fOmLaap2OZ9f6I6cKVbx_1Gah0adRdjHljUZu_0sQ0AARERERERERERERERERERER-JAA-JDn5BDOAGRpZDpwZWVyOjR6UW1SRlJOVVRhclptTWVidnZxTnlBRFA3cmtVZ2g1TkZGaHhxV3Z4M2VxcWdTMzp6MjVOUkpNS3BRS3duVW02azlGYlRTcDFlcUFOb3JEdEZpbUhXOW5HZExTZ0FiTjFVaHNRQU5GQW5oaWo4VlhycnduRERHNVJiN0hHcllwQkdaUnkzRWJnbXhpcnFKaTNXdU5maWFqYXZwczZrQWZudms5eWt4R3FjeEg0dTdQMlRKb2hmYnZpZ0dNVE1HeEFocWtVNEpEa1dZaTFzQnAyNjZuOHR6VHpEM0RmUlU5RmJ2SFM5OVdiUTNyZDZLYk5xRmZvVXpjaGJiOWJVOENDZndOckY4WjFkaVU4bWRqYmZiUVhrenNhdTRhZXhURnhCZ0NjYkVlTVVzNmZjTUFCSnFRbVg3RHpiM0hkTUU5ZWZQdlFSUUgzSlhIdkwyamtiUHVCcGoybnFHM2p1eVJXdm90aGNRdXdVYW9jMUNycDE5WXZ4dzh3Mm1UaVYxeTJ4aUF1dHBnNFJiRXU1Z045NGNGUkY0d0VXVXdoWUZnekZZb0xXd3FNR0ptTGJUQTk1VjdqQWZZQUFUblVFdFdLRmN3ZzJiWVlhTTJRbUZWNjdCbWk0M2FHdlFNWThxSkNYY1JEUEZadEV1eGVmUUFtOTE2VjZERjlKQjZvVFJhanp4N3ZKdkc2VmdVS3Y4aG5jVHJDYU1ObXZWMjJ1cGJmVTVkdVpLNHFCVHJYbzFjckpTaE5lbTU0N0ZnOTJGSHZhZHdIVUxrNDlac1ZCTWJQRVRRcEVFYXRrakJ0Z05n-CAX-KAWBACb7tnYScp9X6HHhI1G9hxZ2iGBIlhUB9gCgkX9jZ-Juwj3gCDm0GghiQPRzt6IL6PzaUhwgn1SYU8mW4dLhY0P4BAA", + "newVid": "alice_referred", + "signatureNew": "m-7Z2EnKfV-hx4SNRvYcWdohgSJYVAfYAoJF_Y2fibsI94Ag5tBoIYkD0c7eiC-j82lIcIJ9UmFPJluHS4WNDw" }, "nested-direct": { "sender": "alice", "receiver": "bob", - "ikmE": "wTSnRd6U5rWz-3yO0ngUXXg_6V5WgUjGbGLk0LrLj0g", "pkEm": "2lwtszl6nTuyUHQPcfgHqTOLcllbgMpL5_tD3myO5ko", - "message": "-ECeYTSP-AAC4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB4FBz2lwtszl6nTuyUHQPcfgHqTOLcllbgMpL5_tD3myO5kqpyGOhlgwX9CKraR5Zqsd5gQo7pmzcqpslHmtpSe0VUGRKNk7lkfZ20A6iBtCP1VqpCenjGyAJ53mdytJaBU4yQlfk6_h_qkKkr8jSQoN13rtqJFnFKtSekDzdd8MtUk82fdLHrNnmk8228jNhzbL2mewQhT_V3x1-FikjdZELf5LksMYsRVdiTbIqnVk0Co_EAAO_fNEtKS-g8X2RFjJ4RHAqC-HmTC9rQg-YRasfnB2d9HuK9nLecMZspuUHPEV7Tw6UDpjSHAqwyxLkQ7LxxmW98egYKdSiaRxh6Js6BBoX_MBnjRKhY6-kPFnkWJgUA2NhQyzZ4QpYOZwiRT6qkzqB1SWU_hgQ_WLLxr1bMoaVuflEN9Dv2tKRmXJx1LMtWUkeP6Z_e5y0Cnl3d0m10M-GJnvVBBtp-CAX-KAWBAAf8fKbSjXyszjAzBQMtY580wZ_F-rhMqGMYbEGBs4te68JmWebtqW0r4cO_jyf4EQxMSV2wDmc00e0XoUo1N4M", - "payload": "-ZBiXHOP4BAA-JAA4BAA-EBFYTSP-AAC4BATZGlkOnBlZXI6NHpRbWVoc2pNdVBrUGp6ZzdXRjJ0SDVYMVNHdUZNeGFjSkI5UkVUTEZ3amdNRHFY4BATZGlkOnBlZXI6NHpRbVNNdzQxM2tlS2hncFRwWW0xcThqem1wcU5Zd2RkVkdQdWJrSE5ncDNxUWVp4FAa18xMvIf8fW5FF3cOHHKPlmJY7CNOU4gMdUw06e03UW_1PVt1O92_IkwQWqsgqb--yVFkjJd6aYMUklWFZRfxcW8kYJv_yPb6pVojLsUg-CAX-KAWBACNI0H7OMKnaJi-UyGHZNAp6gG7SlaA8JKfZxONWL5MWABhfZ2Jh9BUUC2Twe8ziNrziMbT23NJ4WeK45mWqqgJ", - "innerPayload": "-ZAJXSCS4BAA4BAA-AAF5BAEAGhlbGxvIHdvcmxk" + "ikmE": "wTSnRd6U5rWz-3yO0ngUXXg_6V5WgUjGbGLk0LrLj0g", + "message": "-EDEYTSP-BAA4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB4FCZ2lwtszl6nTuyUHQPcfgHqTOLcllbgMpL5_tD3myO5kqpyImhlgwX9DE3kHqDyqLks3Vu6LJJ_9XzPzZuEc8YSW5GDwyZpdZs2yyJNKmJ91m_ENjPfBh5gX-WkuImZW0hdGtkGpvV-HsOqsT_MHvnqfbORRJBU6PAiT2CdZx8WiRvYuvZldnk95fV8yxNkoLA_-gB3VaVtDBxPRQqWqs2Wc_gssIVYUtAZJge33CFf_VAOX1SwPj03Onp5WnrMnETAs6PAtxBBwZInnO0ZK6HSUC29v7G416SyR0YraUo9Q6avtctbI0P7hk5GCuh2nsoVmix8IlvxSV27WlS0OD44nS8VNvqYTBMjhXWElfh1EpHRmIQPT-e_sFr5xjDfO7Eot6GQu9DN9YR5VveSE8qAn4dF6x-PjVLRayvpm8TF0Eu1cHPHs5jfVthaYmdzL5M9b15uVUsG2AutWVUpuu4zzNE8-BUoml60FB5Rk8p90oQLMKpywMYvnJtS2zmaqLxzsrI5kPoYwaU9JSdxJS745XnvzhUmJa1LxlHroq18BJVvuB3r1L48WF1280mo8H-UNwoXsMewGyWv7qPphynHRikF0kojDNWI4CO-CAX-KAWBAB1gxFSqrdBUn2MGzDHTyRlftwBm4QN8CQXz8g-znfsjEKZUVIp5s4EEJwrGwulezHd6rBq3q7osFyxft3kgAoJ", + "payload": "-ZCIXHOP4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx-JAA4BAA-EBYYTSP-BAA4BATZGlkOnBlZXI6NHpRbWVoc2pNdVBrUGp6ZzdXRjJ0SDVYMVNHdUZNeGFjSkI5UkVUTEZ3amdNRHFY4BATZGlkOnBlZXI6NHpRbVNNdzQxM2tlS2hncFRwWW0xcThqem1wcU5Zd2RkVkdQdWJrSE5ncDNxUWVp4FAt18xMvIf8fW5FF3cOHHKPlmJY7CNOU4gMdUw06e03UW_1PU51O92_Il-UI8_i2d8_q29QnqN7YIRHj3eCWRhYaBN79uCs2IPG--bWLEWNtvnOubC74usCMrj-tq7OV7dBpwSGLq88N02BHnnU1-3E4mxG9bDrgqExnpbFtDyaEMJNpxwOnUcB-CAX-KAWBABE7ohf2jB9CmAtv03sVi9pZkh9-3VPqMPLiyGYnuuib0zXz6A9YXe-7-VNg15nPOUoTI5T1ckJS_5bSCKbb8UO", + "innerPayload": "-ZAcXSCS4BATZGlkOnBlZXI6NHpRbWVoc2pNdVBrUGp6ZzdXRjJ0SDVYMVNHdUZNeGFjSkI5UkVUTEZ3amdNRHFY4BAA-AAF5BAEAGhlbGxvIHdvcmxk" }, "routed": { "sender": "alice", "receiver": "p", - "ikmE": "H_LnY626_NNxiT6uU0Cd6029AGyYDUMOKrr3BlRyk4g", "pkEm": "u3jQxhX4ryavA7_XLCdatfNVJssymR8KjHMqFhCjl1c", - "message": "-EDGYTSP-AAC4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVh1WXg1cXVOcEFZdnUxc3lhb0hwSld4SGNlQk04UG5BUzFKODRtakVvMTE44FCbu3jQxhX4ryavA7_XLCdatfNVJssymR8KjHMqFhCjl1fHhCX_UWAqrxNVo7bN-T0MGRTxtZbXHohFBpJBtj56fuw7UmON9OGL5WewM8NS0pCOIVlwvkg3zFasindp0QF9RtKf60Y5ZewPVLKJE6zrYGM2B6fCupmO_qBmBJjXuHWh01-rGzvpFO_-Lldld7PmcFCcTrJDwLb39GhO_MQ8anZkjGz3AOAArmiwDhfVo9HNWj-7OnZ5fsKE5hBbf_2ZGN059sGqofoBGzM3KV9eCcZccBL0gmUCp5pqy_VTzsB19HhzED39WmMiFK67ZwFMmME9yBfMk9hRjyiq7UTulmS6E4X-TJSOhoMq7a3jrDKLzLeFyh6v_Jbu5_aYnz_dohJstsSkhwqCSuQxudCHC9pumoAaO4rE9pg8N1_yMg3bHL7i_SwHELoRSb9zMJy_BDGNeNPqYE7UdwnDanzHVqJ3UHzfkt9mwF1MM_QZdKOvgvfBxE9dxEHPR_J5qlizbf5nGw-4-Nbqt0y1YspyqoHUM7RS8CNS49ihzipCXyaTJQKe8iAamp_Ifkpya_Ufh2-9B0rfUcgRguVt6plFzt_cfp7d8p7yHAIaRGlVeVEi-CAX-KAWBADXtOMgFILitn6czr6D3zzczLT8uAfUBn8x0pBiFu92B-gyI5V8KaYiOvtCqiKVplPFx--C4YAVoNb7mnR2PR4P", - "payload": "-ZCKXHOP4BAA-JAo4BATZGlkOnBlZXI6NHpRbVRLYVNSZXhuelgyZWN1MWVZUnVzRk5lV2JyVDVqYmpyM1lqdGZnY2laYzFR4BATZGlkOnBlZXI6NHpRbVNNdzQxM2tlS2hncFRwWW0xcThqem1wcU5Zd2RkVkdQdWJrSE5ncDNxUWVp4BAA-EBFYTSP-AAC4BATZGlkOnBlZXI6NHpRbWVoc2pNdVBrUGp6ZzdXRjJ0SDVYMVNHdUZNeGFjSkI5UkVUTEZ3amdNRHFY4BATZGlkOnBlZXI6NHpRbVNNdzQxM2tlS2hncFRwWW0xcThqem1wcU5Zd2RkVkdQdWJrSE5ncDNxUWVp4FAa_Gblg2mbPRmyh5lhWb0A-hzL-Neyd0LKd1gGhh-ILyOsKH7KHRVUqhRWKUcoR8U9j2EogB7xFwucVDU959QzKK2G4rFZMNrehW6FfO0x-CAX-KAWBABWD3cFkPXspFtG2yV_hfnwBEx7lavatfw4gv5PitdP_HI2KG60AU1lnhuVcTcxVeThYwBJX0fzY60YST347tMJ", - "innerPayload": "-ZAJXSCS4BAA4BAA-AAF5BAEAGhlbGxvIHdvcmxk" + "ikmE": "H_LnY626_NNxiT6uU0Cd6029AGyYDUMOKrr3BlRyk4g", + "message": "-EDsYTSP-BAA4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVh1WXg1cXVOcEFZdnUxc3lhb0hwSld4SGNlQk04UG5BUzFKODRtakVvMTE44FDBu3jQxhX4ryavA7_XLCdatfNVJssymR8KjHMqFhCjl1fHhB__UWAqrwDJWvoXmUsNAkr_v6LfOf5HTY1P0zMsRNE3YXW_4qKZ6FjZDq5z1YuOHHdsjFVVrU6NjW0Xuw9vUMQEEjS6RK6LLcXXCq20YjxnD8yblIOC_r90SMeeiyKP2E3qKhLiDK3JWFhIeLHDC2OJQKQm3oz28GRl2_kvaFbV-RZzeYTCnki0SKIZ2YJAGWGoZyNwZcKq_E0fUdytEIQ7pOalnOEgPxAnemxOfOJhJyGNn0UH0qZDz__5nPnf8XReYsVvLS6GdeU_HnYSgcBiykidm7MIkBG01ETs8j7ZEprSE6S44Ic7tcSjxx-E54qM5SSS2svq5fKhuyP_izhY9O0V8nAGc5r3r1BhEHtn-WPw8abqf6n1QPFBjdjcCgTMyBvwN1hEB2MqAA_VdQFxK9W2RQ3b_FM8OCUWLTqAC813wQcCffVZh8B8XJvG5Sm-tgHZ_sJAJS_2UkgxciBHT6YAZvBT-V7EQGencJvn2nMqBOV1NRA9PKqBn1O0jIcjVgG7_pfREoX2DCZ2CS43HK6h9CgByefMPCRlGTP9bhZ639ir6uzfiCOYiKyEfBJ7iXgkcK0parug5iezn9qaNTkgGCutnqx-3Vw2BkpPPEq7BE5IcrJnyNMNrONTEDNKLvH4zbi_9AlWMfRDR64DNgzKXdyIeGy16S5G68LcNIaYeSmMtQa93qNwl63XaGyoAoeqAJf2P-9FgpRLc0M8-CAX-KAWBACdgfSSUG5oJvVz_yIDD7CoyYZFAAzkXgP7q9cxzuvbEEHqwkHCBO9aJ2-sv8leAAsWruqPuvyt2f6X9D6q_3cA", + "payload": "-ZCwXHOP4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx-JAo4BATZGlkOnBlZXI6NHpRbVRLYVNSZXhuelgyZWN1MWVZUnVzRk5lV2JyVDVqYmpyM1lqdGZnY2laYzFR4BATZGlkOnBlZXI6NHpRbVNNdzQxM2tlS2hncFRwWW0xcThqem1wcU5Zd2RkVkdQdWJrSE5ncDNxUWVp4BAA-EBYYTSP-BAA4BATZGlkOnBlZXI6NHpRbWVoc2pNdVBrUGp6ZzdXRjJ0SDVYMVNHdUZNeGFjSkI5UkVUTEZ3amdNRHFY4BATZGlkOnBlZXI6NHpRbVNNdzQxM2tlS2hncFRwWW0xcThqem1wcU5Zd2RkVkdQdWJrSE5ncDNxUWVp4FAt_Gblg2mbPRmyh5lhWb0A-hzL-Neyd0LKd1gGhh-ILyOsKGvKHRVUqgfSUCPqN6W87V8ckirwHgzPSRc629uzlcqVoClth3da9VDiPGqg6RutIeITc-Ya6OtFzgTlOa-oplHkrY3i1_3jrJNgflGp6Fl9S7k3fOZ3i6dfT6Q38QVC_5FGIFNM-CAX-KAWBABgVHhPC4Pi1Z8_8I_hnUnlbmQGVOImBIglNbcRixZva1zj_GdTAzZRQPGdjHDdHh0W-wDQmcuav_JAeLaUcnwJ", + "innerPayload": "-ZAcXSCS4BATZGlkOnBlZXI6NHpRbWVoc2pNdVBrUGp6ZzdXRjJ0SDVYMVNHdUZNeGFjSkI5UkVUTEZ3amdNRHFY4BAA-AAF5BAEAGhlbGxvIHdvcmxk" }, "direct-hpke-base-pq": { "sender": "pq_alice", "receiver": "pq_bob", - "message": "-EGwYTSP-AAC4BATZGlkOnBlZXI6NHpRbWJMcXUzd2VadHNuTmVzQnplY0NabTVGQTZ1YUdrZ0NXWVI1V25kcjZHenpk4BATZGlkOnBlZXI6NHpRbVpzS21mZm53WVZiR0xiNlptWjhna2d5VHM4RDc2WVFSQnQ1WW5GTks0U25v5FGFAG_gn35RNoXWKshZoo7uJpjoK4rcKk2jfCWOYB5delcb_W29HoRlEL0dSVeU4hgMITPH3cryDD1lyXErM9gmApU_t3X2VMWTbHMN8i8YqhdzaUnPWWDqRNtFjChapB_euEuUWFXJYvlz7tyudeTDJxbbXFn5s_fzwvfKwPMsvzF0asVGhxKC77xe3cMht126-gHc0xx908WrHrMVUtfLXWQiQmIsnXcbHqPwbHnxDWamgaWPkbwjebDDnJGHwkAg1jBz9_Wo6RYymhGpBGqc2L9IsGfYSzy9PaSb22sg5BtTLO1vf3o_d3KoAks7uHG7RYDQgarMl4pWZUaICod3MK7yZlTR9LOsnXqkn2UXMyNg5Tr0bX6iEIw4bM77HXu2Ic84eF-2DfgXbK2I6DdH-KcH2b2EMmv95kC4WED-jPGoQA2Wd7kBNYlbYouQU4nDmmdVjOVvo2AeD0_aDu3cN2nQyINx34ryTcGjG6sfg6H6FYWT6yET7vpfPpZlx_cs_qq9QC9d5yjZOSzyEmgQE4a0QVGtxdk8a_Bd2O8gGutWbM5fFNLoPEwreECPHIuhuMBXMmIqRS1igha8yN_dvapSle1WrHVy9EADQww4KzR8DHvPVuqbCEKtm5_zA0jYiLtiiCGkfUJnBI5k07acUjSSmWz3idGItf-SmY4latHXAeWF6i18cJZkJjV2MA3s1ufE0xB2QgRMs_zwpDAB94ICwBUcMBuce40rVhamBx4_RPvr-FsX_IUMBOCRf-p_Av3RKqE2KQa83IgWUbgUnvXHsmNOtArVpTD5wtIDB-Pcwq_jvOgyKYupgddkTwo9jY50q4R3u2Nvm47NjurQKJ1jvmVsv-ouSfIYGeUrqWOkbwjfkgenLJHv7ZnXE2bPI-KkkM4n5f2OjCfHuirtQnstKEd4gCbhUSx5BSpjoNLT77emBWb_NjiKdCZ8dugvuu48R9uaAc3PtBQyf1xkyHpUoge7FLHRDC_Kc4mITunie4eC4SjaBpyVwL2PwW-pPr8J1RHOX0KNqxFq_7ZocLPdzAtgaYkLO7kaIBRKH4gGPRsoO4eyk0DrUzIaZBqp4VJRE7EcE48FjtneHHvjPc0eA4v35CWpdPtvEHgeAwsXgfF64Bn_PIMgyMt29Ja-s7Ob4j4TGOBXxxdMUhf67z9nnUARnLej12ARzaz9Wa00fQjfvJQVaxbjeZ9kFDM69vjW997615Oxute1YenKcZqACx9EoghOqj_xwJphXfBIQcy27pYcOPDm5Q1vjLryDv5kyyp0bIni5sIqUQnrJ9InUy2VBtSB0wE_drnoiwQu2_rMb_M0jgnuNZ-N3Y9haW4AMX7a-5uttqK-eyQO0OogmCBr8pLN0ZC7aQz2KbPl_TDxQQgrCn2ra_1VsYn8hl0x_D1FQUeB4y6bA3LnfUuVkYYYoUFQQGy5oCdT-AjqCSqfOoGRjRRBfsOpm67DykdVa1BXIEgfXNPCzfHzPHvAkYdH1aar1fOZINtK9jKJZjz-5RtiE2yvMWqj629LAtbbXg63OW_A6SJ7Y_4N-CRR-KRQ1AAQB4RkRy_co2t-SiuKfXPHlU5es2uIAfVB_WJ8artzk1efysbPS2uf5bdHL7krPJXl1ZV31V-rdxoXv1ESppt27vrwNAd2NnxWkjH9Dnif3SyV8ZY2fxe0X-_hDFVfdst75PK69kAbhCDPaMjczGLVKCFh34fGQVScKIlfsswAuzYPQaTNmt6Zg98lGlWG7oZJ1dXR89NBTYEHk2vJBWS_-qDu28hGUiGpgjK60GNxo0m9JAHvGC_SkURCtRlIydqr1o1sI_ZOIgU43EFYhYrafPrWtx6by6wRSdlRKjKSIDHkuobUsVYruanR6aPdbpjFf31Ez3sFBmn3c1VjqBOl4vBUOBapAVbbj3lnwd6eJC1psRYLgcgqPkLe2sq2i6FhlxDeBX9h9T7qI1hiWmTAwdt3W9CeR00IXIQNE_0OH_THREAv2RE-pBFtg447EwfVpvK1T-3oiPsqrgRq-zoOc5jWqfzY-CLl9lOLE5pO_bAbzzglRNs8oDbfoxSKbhl09GTFbLr5mQ3PGfReVlzL9s-wO4DTYX7zmuVPCEQY5NldKhXHXsPtNrrgedWnpsdIeHsMJ-M_z6gOmhmzctMcf1c8fPluVYmYa6-h5pEIuvmWpkWOVLVZoI7aADx5v1sDXNPokBSo6TY4g8SB2_gcxERbmSp8_BXumw-9RAtwNx0fWkqmJAzZJOLgX-cYOwQyVQvuZv_HhMFtUhhSFEzg6cHQJmbhaTLqVa7iiiJMXU-vPmpro0kf5eKw2nkbrEZ6lnH2-eniGRpycY1PsyRThIkosGwv-9SlMvieR1qMsHxr_p3SfAAvtRupWrFIUZfCECTYx5l9IuV7PEQPxvUiYjAE41NKJ2SX5d_AiX62TYfZDmuRRG5cg7cA6IhD8Uf0sm3_eLErf3dify4uEy7ftvqqQqUHdm-rVaHPp9UdK_7knXvSqXBnfch1ZORlo9e5_nG9KT0zF2Gp6AhcoDYjtXVLVjF_3mmc7SmY6gCNQuqhyrLZLzXOtMmNXM-g_Wg-Wy9KISyfHtOp86HA_mjzGNxOO470A5iQuI-nF_L4kLmhQv1PsVrusUDRNx7dyqBqhIBTetL9uU8a-2dXuBIn-5Qe01XxcnZcmDazq1BPQoeUSgYD30sBAaqEgOZ89nOB7u9nmyB5iirWH9-ejdnx9wkR2wIoB4y9ohwEG3-Gs776uNrTUI73vEHTYrcZNBod432Fnr-iibMsTMa8B_G-EZ4PXIqxzyi7j-SkGK4uVRfsPxhLsC8EvcbtHzyhRczCEhVUFHlikoFUdEoNMze09wq_d8st6OsEobm0WZr6nPlqiJ-gPJWsY0WA7Q_w1Ql5W1M-x0ThkeGxw_RMo_IScHchZ36OodPGm_n0Zjbdh-S_pnlKr8l8YUb6AtIEkOtyhl0ZBHdHTIeJZbSCNNtSaFjKmHwvDdK79CaR4vgN7y2X7aZkVlMwrCmMTydhK_8mvryyhkr6eSQb2A4faz-yzhac09e0abg6_-dXZKr7QWxD3f_3-V42y1AcmemYwKti_RFgE3koDZGY0ghZu851hvRgNzJx0iI6Yz4sdxlzD2GQ_xGKdoZjZLPXeO_Z4PKd6Gb77i-ekutyKDzy3l9g2gaKB8L5T2O9NPWPju3IqFaVzAiwH0qSzxtO6wTk99jVfX6qOBNkgcoJYp1DgQXheDGj5AS7AYWFIrI29uA7PYCEAL_DWT1-nEQFJGZ3vUlsMWRevEg4BnB_64Xw1SvKDpC9dlrot2AWe9Y6K1bcpU2ZObp2kUD7S1hgRGjpjz31Lon4iSdCdhEhXmFU9FQ9zf3K7X_oV8NFYmaUgUk8rFP2jBFG3OmCuMY2lgWN39kh5Vp6KSvvZ4GAcgds5FzUIO3sVfmEWECxnzFPvnJtWaloj2rarXLF50F6FWIJeaJsPks-4D9SB5RiLRE9OT7P-jg9F1asn7D3oE0VfOa1mW1erVL6eHbVV4l_IQCZlRMXzg7V8naaSXXE4mcRC7IA3aq7nHGmTqr2RdzJNpHXebDqL37k7myswdHyX1bHUb--Ku45s9WZwECCXfSfFzKmnGRl_2tOSix5ypPLtJZj4e11usS391NVr7lgUgQBfRAUqrBkx0yhkDc4_ChvfXGdyacRkzmu7kkDZx7PnHGQsvrtSGimxYx9Fa13dRZcW4tH4sFBhc0oUM10yGsjEXhfK0o4zAz94N_Glw5kH-kFtGPkFLTwUEECISWD7xpHVIiNSfM4YtJsNkWXbBtj0UYM57MuGDzbPqDkCnrhHBl2AhHJ5eKz4byR8ouf6i5xQQIG8NU-4cGyiGErUze1QUYQx-10xi1Q8p9Ibz3ejHpSIXmJxUyUMToqTgOA-eQqn073VyLZrc9_AYmSdT9BT1UE56jl7tITcEq0ix_Th0A5zdSoVw5lp_iXrv5V1cy-2Z_rvbjoc6Bw6NikjsKQrX5we9n8hQ-R8ugYW1eIUP-B91inNBWLp8UtY98LE5c8-CulvVJavFjQZXdpC1_Gy8s-GB1QdYfZdoLlbjrojhN-p_HG2L67eakv4hNrgNHJjMs2VvDKuGNZTOF_AFg8QBUWnD5onGwDKt1LhkzCpSgH-qjM79a5AhZPlNrk99jum_TcdKxsBZgjoU3kbs5Nvgb-MHx0tyr35A1r6O18jyBHBH61-KsVu1b3CH6sUVlZ3Vpbl5rGHi02Kv3gf2Z76Oe7rjDmgEtCwp9otovvpBrJxadkdoIOexK45Nw9ZjlArqDxfBAuIeY0VQySHyUQvRJiLC3_1SF8a4d8qnHNlmBEdYnH2bXtnvlBiaF0ZknjCy6IRynUWGYN3kEnE9MjwLWsx1DHdstewkMhq_U5qtxvh6RUmrANXB97GK17vj9E6uCfndQfq_AzSB3WEmWxXHwLDwII7cyN90FwthhxbRM-HfL0Mzj61ljk1fZXtbB9-uRw1S4KqqxJqMxe5giSzZ8A4yhL91ILt5Z6_gvxwCbZEP_BAJut2tCdSu6X6rcCiO0eIyM8MsvXGlBOsY0M_7jvbHJVA86NF8dV6k0uxsN-r1B-0Mg1AOEGt_8gC2YzhN0a6-4PDycJFHsaWFbgoSJZbM_Uy4OEg6QTG4WYaLcjbXES-UzTF1BHsjSO8BEKDHexDlJN_wY2EXJaAoUiPu1JPVjWemHBSNy-HvZGIpdXZ7XSOnBYtCbCVf3khiNNoY6NlhoZtDx5Y92xJlDcVupuq8CBsVZY8ruHd5XVlgFD8QfDKWjzyVqjKQ2yitWOtX6FY443axjZOykFWS_fBwehTV7NVX_yvYiVLFjeJ0XG_9DZqofn8cVg71FBC4AuY1_CdjPnHPlUPPUeEY9NdSyUMDqGDiLTHSWNvhFUbi4La5loNjugGEyvvHmlx6_YFKSJ747WH64AdLSnvNsQ7jgPQtD5rlxUDfdGS_0rE10jfvZGJo_sABL7DIAzi79ecyVCyG0PG3fCfNWHa7iTT6rk8KYRBC4mFhrLmdM3mIe57xu5rMdg1axkXsPOn8heZ-QZyxlSQXXTFEf70S9bgfFT86xs83Cp1k2KaEzlCYnoPxOJOUNdthWv5Skp-OszV5cwUMq5u4Wlei5V1SRzN1ydS27r60I2_JdWg5ymLRlvj5wL1eeaeB7LZF4ROd6uxDmbErX1zPPuSaHMopqEBJOWc-wjFfKF2Hdz4riu6_tYvmbYdrbQVh8ciGOlY4oQX_7EwrCD1fBzDXVtAk59DeyVL6uiFwWRuXTlmYb0yHk4YAnv3pN4BZGX0d6hacYQIbVZFyfRHKE3BmGWamgiLH1-q1OIcpAVfALQcKEjrR666EstrZN_wk2Ydwruu_-Lgj9TPE2eRc-AVYVb-2p29KE1rBkpIkLTHtUqjvIt3YeMv-N2gfVWsPVhr7VtR7vUhzkIASUDBQ3udRiWEQRI2TsFQjgBoNyDCmtBSEJY8eEHd9lA1Jyh2ZLXMVpLNxskklrAz7Xse51jjcbtlHWlHw26pmPHpXfpYxxEQE2JXTbBAvTsa5ei3A7ObX9Cg22ghgv6eMq2zX3-eHurDyZhsSuqd6WSdETenn8HmuEVWybbize-m9AfKQaH5YZN7jUL-nUgbBiOz217AbduaOY9dcnn6-sQPGoik6RxWfHN6r94ox62zRXHxLPwn8A9ycZVAX31Yvi42j435NB5gtwVK3PvykKS0iht2tWkBacQP9R5sFPGgy-mDQ2y8QeRa6rA3Ezyr36wk6OJec9oVd7AqBfXvnI9IiQ3HyeDswRGzukXMncJ0zao4l6hvXP2uE_n6Yhbc1wr_hM2OdQodTirJPaqeS8igmjIeMGhw2RqO1lxI7YII0RswgIDC2SYpsvq-JvP4voJDkFJcXd6kbjd_CcqQFiLEyCS2wAAAAAAAAAAAAAAAAAAAAAABQ4SHSIm", - "payload": "-ZAJXSCS4BAA4BAA-AAF5BAEAGhlbGxvIHdvcmxk" + "message": "-EHDYTSP-BAA4BATZGlkOnBlZXI6NHpRbWJMcXUzd2VadHNuTmVzQnplY0NabTVGQTZ1YUdrZ0NXWVI1V25kcjZHenpk4BATZGlkOnBlZXI6NHpRbVpzS21mZm53WVZiR0xiNlptWjhna2d5VHM4RDc2WVFSQnQ1WW5GTks0U25v5FGYAG_gn35RNoXWKshZoo7uJpjoK4rcKk2jfCWOYB5delcb_W29HoRlEL0dSVeU4hgMITPH3cryDD1lyXErM9gmApU_t3X2VMWTbHMN8i8YqhdzaUnPWWDqRNtFjChapB_euEuUWFXJYvlz7tyudeTDJxbbXFn5s_fzwvfKwPMsvzF0asVGhxKC77xe3cMht126-gHc0xx908WrHrMVUtfLXWQiQmIsnXcbHqPwbHnxDWamgaWPkbwjebDDnJGHwkAg1jBz9_Wo6RYymhGpBGqc2L9IsGfYSzy9PaSb22sg5BtTLO1vf3o_d3KoAks7uHG7RYDQgarMl4pWZUaICod3MK7yZlTR9LOsnXqkn2UXMyNg5Tr0bX6iEIw4bM77HXu2Ic84eF-2DfgXbK2I6DdH-KcH2b2EMmv95kC4WED-jPGoQA2Wd7kBNYlbYouQU4nDmmdVjOVvo2AeD0_aDu3cN2nQyINx34ryTcGjG6sfg6H6FYWT6yET7vpfPpZlx_cs_qq9QC9d5yjZOSzyEmgQE4a0QVGtxdk8a_Bd2O8gGutWbM5fFNLoPEwreECPHIuhuMBXMmIqRS1igha8yN_dvapSle1WrHVy9EADQww4KzR8DHvPVuqbCEKtm5_zA0jYiLtiiCGkfUJnBI5k07acUjSSmWz3idGItf-SmY4latHXAeWF6i18cJZkJjV2MA3s1ufE0xB2QgRMs_zwpDAB94ICwBUcMBuce40rVhamBx4_RPvr-FsX_IUMBOCRf-p_Av3RKqE2KQa83IgWUbgUnvXHsmNOtArVpTD5wtIDB-Pcwq_jvOgyKYupgddkTwo9jY50q4R3u2Nvm47NjurQKJ1jvmVsv-ouSfIYGeUrqWOkbwjfkgenLJHv7ZnXE2bPI-KkkM4n5f2OjCfHuirtQnstKEd4gCbhUSx5BSpjoNLT77emBWb_NjiKdCZ8dugvuu48R9uaAc3PtBQyf1xkyHpUoge7FLHRDC_Kc4mITunie4eC4SjaBpyVwL2PwW-pPr8J1RHOX0KNqxFq_7ZocLPdzAtgaYkLO7kaIBRKH4gGPRsoO4eyk0DrUzIaZBqp4VJRE7EcE48FjtneHHvjPc0eA4v35CWpdPtvEHgeAwsXgfF64Bn_PIMgyMt29Ja-s7Ob4j4TGOBXxxdMUhf67z9nnUARnLej12ARzaz9Wa00fQjfvJQVaxbjeZ9kFDM69vjW997615Oxute1YenKcZqACx9EoghOqj_xwJphXfBIQcy27pYcOPDm5Q1vjLryDv5kyyp0bIni5sIqUQnrJ9InUy2VBtSB0wE_drnoiwQu2_rMb_M0jgnuNZ-N3Y9haW4AMX7a-5uttqK-eyQO0OogmCBr8pLN0ZC7aQz2KbPl_TDxQQgrCn2ra_1VsYn8hl0x_D1FQUeB4y6bA3LnfUuVkYYYoUFQQGy5oCdT-AjqCSqfOoGRjRRBfsOpm67DykdVa1BXIEgfXNPCzfHzPHvAkZJH1aar1eAdWb-IhlIIBALK9y9jHU_-Mzam4lEwcZQ7lN6Xq4HKb5NEPzOFprhTSWuo6u40S7KjnKbbYqoqjY6_vh53PQ_cusZe2cWrT_fSYcCdiwfrwWudTnNuZ4Kuwt3KpYSR-CRR-KRQ1AAQxrqolgJyqX4nFor_R8KcsOIDy-kcTYi24hdyxNV4_UEeV2YrMrNYI3y1_jrMCRcM_2lhFevmgLEdiGbDVRfuK0JeMYblmMezTGwZfrN1OP7N2oJbitrJGvARkelQHNLUp2ayTLv6ZMx7h-zBUtS_iMiWp5Q6PDxQOkNVPHyZMxHiJb5muQOVC5OW74Y6NyTjdOc5kcqrzTpg1i6tsHmrF721EIxdaDfpV43yFg30Z8_qkzja2QrCFjd_bswSfiQX8o0C7P7HwHVQ09_PeRUdeSZsj_qr5yU-FRta-WdXQnQzv0NJL4r2-7KzvFYtxnx6gtarihuucsJ5VxBC33rw-r2Cg8d00p_F7R9FZHPxHwNorD38WywwSeeJM3PCU9nLzKICRZ76EZmlWCU7iNl27GzH9awWKcEkEv8f9q_5kk_VMKwQ-hx_w_yRR7vjJudcvLignKBnkxXcv5HPAItLw8ofWRotQ1YTRhauW9SmNYZhGV_ASrmNojQ5_xh-gysA12owet6WyG95kN0jLE2OWYjtf-BklKXl423cDUkb42HXm7p1UJsLrFcqOGgeXrCXjIj0r6GQckNTp5CHQn9hlaE-uo8qiRJ3aIje-JaYTFExBG4IrQtC1vARskTWJ5-Q7mZSY9gTeaO4l8Jr0wGDsp-5rJFBGeVo86RI_dXLPKh-P5CMysScQuwlR_qqt2QF5snDdO2hKZwQZNflInQHtEqBGveR59tdW196ePY8v_L1G-NVZx72ti9Nzx2zfVHPUKg_xYrQSJxZtUJGLx8lFDratoC0Joqj8jk4j6QnoQ00fuYeUGa1ddsydQNwvmsttHDwO-ZEMKATHNyzwitQF_cTy4SilkB9YHv8shQJnTsNSYsDgfyRJC4w9v6Vs9U-6gS_5XZSB7INHcQpMqK_hBtIkL9qWB6R7hpyKw_WPWGeMq6UaU6_CZO_Ss_qxQRB3uqU47yefisf9EZbJBREvE8QgVA8TDazpNakLorExNLbQs4yl1AAPiH2_UqaipxvJaQatdvOhL1kAIALqLdCNt7OAPT_k3gO1mhOFxYD7BiL0hktxm_NMvNDPlzOzM7IldkS8WluDwhUlhCWyZMMwMYtcubUu19jV08C1rjsmQXSQTCb9m3liI-9YvqOyk9AVsnsuOhFnrvJ3AG0wlp6k3labfw2X0ra6sfLikajKKhZ81WGbiQjOT0TK2nAiyt0Dbe0B7-m0_opFcfdbSrOaombzNj7K9mRxlxFt352UH0Z0xpUEK134I6xRrG6iQEeNEl3ffr398eF20M2IMQ4s7vxn9qVKI7SY5J79tyuFkv2wT-OoNrzaoHdi09yhCutIyCw4pwNZKhAeXhryPQT72V52_lk8moXUj_lHRYBxJ9G6OD15sHTRheeufsG7lgZElb9l2ySU8S2mxavebMjDGaK34fKZ-H0OtR0bgzpgmXU15MrqQw8CSRhcq5c-iggz36SsduQB3JhOaOpY2KxNLm0-5HziXRlURGp3O0doSKsRjsFmyS4hVE878x64plLcF3sz5IxwQLBFls0CPQbyRY-5RoDnRT36XiV22_JQ-2vw0YalGFcIm8zhATW-hrlOz2tx7tqquOWyfhwt_qIvPTOwiZZnX7a-qW2CF8pa-Lg2qH17S0hHeJeGhUGsQm12mu4VzSiEd6vz0Sak0bzB8MfrSNhJleeaCRoNBLhVr3rVbcnX03onqNLz5EGuxw9huLJlDhc3c9azg7RAIJKYDNJdYCPfeq_Oq7CMqaMuIjf9f2HNoxS6IgzOXUlyW2kShCkM4dAR-Mpapv465Jd7Ixe_eWAGF6FuzWw0VqDYthrkCbfdWPpZCIMvP6iVpKkuv-lKmpf2tShpz1ADbvRGfaZWezVnLW-Vvxg2hbWvRia_CGZ5qTC4V__9RGv4sARIz2X6kuxjAJDKPwVz9yX8cJHN9WOt-gjEHyJYmX9_Hr8PJVFHun_xWQ478txsvsrHfNegjDPzcoLzwB-_XY3Ho3CMH6EpvRX3njq1IqRCJoAEZc_xlbiwoOYjaOePhBIcIwKs9A4qnc6MQLu2UHMbqzUobCEinWBExQ4JaK9SSxwNHijhl21TMb2kgO7_Ci7ZquM7omAf8drlKxMtOD2nPQnZPFirrI2HHC_G9HYE3VCTlWF6pE5tAiaFM_keMe22xfucgLSM7wOpChDGpDxJsKzLBNNRDyZWG6vMz3cdX9XEZ71SJp4o3UleOqkvLxwRgWdHB55TqP3PcVRsaPxKTewEpkKe_sKTtt1uHlj4L9zs6qYDX4RbTM9pesWG-xE826qb8gqA2Nv4uesbq0X_SFfcXU8KIDyW5DVWMVpuIy4UHsfUX3_WucLJeYBGpsOdgDhr2CgiWKBHbwFWbNcU84RI-Ftfm-SK1j4ogB9yMdhpi-k2l84OrerUUuRWLARhL0CyMVhj9f1P5LqkIFUhaIR-dWpUUPbOtQ4PhNNTu08vBIIzWEwVBPhIIZKcKo_lRkdjxAWt6ARNqfhlzTRKPy8XnCly2IwDVclaZ-MgldhgC3fO5mbuDJ89hPUBJ8Vy2_9XY3W-r3ne1ZlfCIBRrRo2cpTjQ0Pv92yXTLs5NyykUujF9oxQpwwF3weFMRix3cZgvwmb7J6DBxv_9Kx8WhkVHYtd2qQ7fc0-I8U-1sJJVDCOLa3RhZS4i3Z5nx0MciR5lH7NfOjz_QmdrS7XINUlUo_xu9UN02nXes9Ed98QPIgotE5a_ZaX1CpBHtRLiePl2o6KqF304tsgZwUDFfaegpHMTyXS4tTSGp69sVbdCHWiYNTef1EqnkLgxUCCho5rD6u76CVJMCGWHeJyZmpiW3Zr7PabveQNZiUhS99UHKO20lOfvw-96VIN4qQJcZWP_S92BFPBoB7sMXX0Yp5bnU-ca13KpysdpGt4gygPMtk2KCkuxH23TKGwJ5g2AhNNDNgB9o-DoGicGnh2MDuUxRrQ1RRsaELjnT9j_RuWuNoHWvazQZIh9sFsG3CXFNyUHsUw-HKXaGvJ6R1mSXyla4OnbWp1rz8L28aDsf26ccM_p_PlCzmoJZvtRW2XQmSDClYvKB_Hpn7Du5DpQ_7M3o2ZnOhVkoCC0G-owcM7d_oyaw7w3O1lwuucJlfeJwpqEz8DnSHwS_EjrQ7fmJVn4xsW6IyhA_-xgw4nPbNlf9K1f_FMb4bzmtnf7YojdpxmFN9r00lLajlawiFqcUbhR6EDzyeZh2sKtIXbVYqz8hEKrs_RarwjlioTPemuzLmh0ZsCxGYLQVdIgr24JbPFLMbf6a5hOg3PIYzvarcOTGWsVigKeb205Ggnl8WuzTBI-EovqVY8CoHrjk03ZfDYMCPRIzqsJdqS1OjiHmQnwFMeeKVH4h_qXAQIap_xHXGEdbvMBavr3ADpq7n8YjHVdO5ze5Xy6A6Nf1q12Sv5e-RSXY1TABK5gwLSdMYWjj5hyHTgXyzxHQOtB-qdPst6TNWHDvoUVMS2GWXfwItykAFwcKDV9zafPYqsLgVxzlBjIotuyggAnCwpFpCVFMR3gU5DQvBsok47aj0GmiAibhYEUECioVl21CVo96hnR1sUzbceD2X77n5SlhxRgJupOfFKxi_zNN70JSCqfSfLHCI9npJzXJfxt9MAkoKQw6OoPmvUVs9XrdXywxYAcn-QYyaQcegolL21RZqUKkg-225K5ZZZ50YA-lrQ9JmjOIhaafmnIC3e1eLWGgLgd9WB2asPRMQkC-jPqBwfHHxVAukyZMLtkofvzfghBCQd1JM3g8jTybCoxC2wTeUmML5Q62_vaGjOqbgWh1KaVWhfQ4tr628PoosSHHhIVdSyRGLc4GutCcL1Ub733A3b6VFWdMpZ4J7rzQn9j9VqKhy5Mjh3aZMa2eA0dZ3yI7imcfdes8U7PDifslV0efzrcpLS-2tTtU3MU7zb6N6jkrdRB9thaouhKHjlQ02xPu5NqzGwpDSH4bu2DvEDYviyuKcPFpsj9w-7Z_6RAL-MlmDJMa9F2aUwmIZlp8RQBQDkpBlO5Ce8-Twg46MwLcGhOT8CFsI08EvdNZ3S5dC14DXb_vKnvr6JIgBLRvlVfclsM-yNTY6shUXEDVbvtMNYuC0u4Q19damlbS6SI3FuiMVF7VfQQpBGi5L0iEEhErE20GT8GThCbweDLmWNBNzNaJklRkvn5v1SK_XtcTXSYISb-npkLCA6CyjfzliXwDe5sM-RPkiqYgca8W39LmMdxyn3H7aDz3zBU9lcj_ZQiuK9ALcfK70Pd7UrWEw1YvBdvKW2tN1z4przkmUA8t1h49Lv9C30xQvztFu925gaawKLzo_d6r-UaGuttZOevIPEBskePJDgds1Q2pye4K8zP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAABwwPFRgh", + "payload": "-ZAcXSCS4BATZGlkOnBlZXI6NHpRbWJMcXUzd2VadHNuTmVzQnplY0NabTVGQTZ1YUdrZ0NXWVI1V25kcjZHenpk4BAA-AAF5BAEAGhlbGxvIHdvcmxk" } } } diff --git a/packages/tsp-js/tests/interop.rust-vector.mjs b/packages/tsp-js/tests/interop.rust-vector.mjs index d7cd2517..4e5836a8 100644 --- a/packages/tsp-js/tests/interop.rust-vector.mjs +++ b/packages/tsp-js/tests/interop.rust-vector.mjs @@ -3,29 +3,35 @@ import assert from "node:assert/strict"; import { unpack, pack } from "../dist/index.js"; -// ── Cross-implementation interop vector ── -// Generated from the Rust reference (affinidi-tsp src/message/direct.rs `pack`) -// with FIXED keys: sender_sign = [0x11;32], sender_enc = [0x22;32], -// receiver_enc = [0x33;32], body = "hello from rust tsp", -// alice.example -> bob.example. Unpacking it here proves hpke-js opens -// affinidi-tsp's HPKE-Auth ciphertext byte-for-byte (same RFC 9180 suite, -// same key schedule) and that the CESR framing agrees end-to-end. +// ── Cross-implementation interop, both directions, against affinidi-tsp 0.3.0 ── +// +// Rust → JS: packed by the published affinidi-tsp 0.3.0 (TSP 1.0) with +// `direct::pack` and FIXED keys — sender signing seed [0x11;32], receiver X25519 +// secret [0x33;32], body "hello from rust tsp", did:web:alice.example → +// did:web:bob.example. The ephemeral is random, so these bytes are one sample, +// not a reproduction; opening them proves we read what the crate writes, down +// to the thread digest it computed at seal time. +// +// JS → Rust: a message this package packed with the same keys was handed to +// the same crate's `direct::unpack`, which recovered the sender, the receiver, +// the plaintext and a byte-identical thread digest. That run cannot live in a +// JS suite; what is pinned below instead is the deterministic part of what we +// emit — the envelope fields and the payload frame — so that if either moves, +// the run is known to be stale and has to be redone. const RUST = { wireHex: - "f8401361348ff80001e010076469643a7765623a616c6963652e6578616d706c65e8100700006469643a7765623a626f622e6578616d706c655c0000e0601a5795132915e698a115677334d13dd7154f717eda8791473ccbb360671313f40544e2ae9153559a01d6aa33b93261dd0ab610231bad47e059d0eaa46038cf872ba82a282a431fd391e10f4d3c0603f82016f8a016d0100d308cdcf413984d884ff81ac2308da9d3afc9a0601e9393f664d54f9c37892897e996a0c8949ca8afa643ed39f888312094f6c34c55a1f4c3c0032f969cb707", - senderEncPk: "0faa684ed28867b97f4a6a2dee5df8ce974e76b7018e3f22a1c4cf2678570f20", + "f8403761348ff81000e010076469643a7765623a616c6963652e6578616d706c65e8100700006469643a7765623a626f622e6578616d706c65e05024f063c9148e6c799cbf21842bcfc2327cf1b912834d0f01f11bced07185613a0842fb56e701ea30fe05a3ec417dc4c2cca1eca967b9ba9416c9937dbb49cb98908987e497cb053f8a72652b280ce0f8317a6b7a8433e729af676e9386debb4f06c97bb16c36018ca547fac469f82017f8a0160400dfa9f2e43a3b0bdc2e82ce1f73b45d875a8e12e9bc259321b0b3f6577c044d4e3b1c9626f2c3c7691d41d1708020a01929971f9ce80db9c2b62a044cd6254d0c", senderSignPk: "d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737", receiverEncSk: "3333333333333333333333333333333333333333333333333333333333333333", - threadDigest: "9f374ced683278ee93c75b2e2e57022c166534a9426e395f64282bd81c62593c", + threadDigest: "a83deae9f9ec261f4fd98d36c6b6c5f1531d72de7406ea7c7a359cab633dbeef", }; const fromHex = (h) => new Uint8Array(Buffer.from(h, "hex")); const toHex = (u8) => Buffer.from(u8).toString("hex"); -test("JS unpacks a Rust-packed TSP message byte-for-byte (HPKE-Auth interop)", async () => { +test("JS unpacks a message affinidi-tsp 0.3.0 packed (TSP 1.0)", async () => { const unpacked = await unpack(fromHex(RUST.wireHex), { receiverDecryptionKey: fromHex(RUST.receiverEncSk), - senderEncryptionKey: fromHex(RUST.senderEncPk), senderSigningKey: fromHex(RUST.senderSignPk), }); @@ -43,70 +49,50 @@ test("tampering the Rust vector fails verification", async () => { await assert.rejects( unpack(bytes, { receiverDecryptionKey: fromHex(RUST.receiverEncSk), - senderEncryptionKey: fromHex(RUST.senderEncPk), senderSigningKey: fromHex(RUST.senderSignPk), }), + /signature verification failed/, ); }); test("JS-packed message uses the same fixed keys and self-unpacks", async () => { - // Sanity: the JS pack path with the same receiver key produces a message the - // JS unpack path opens (ephemeral is random, so bytes differ from Rust's, but - // the receiver key + suite are identical). const receiverSk = fromHex(RUST.receiverEncSk); const { x25519, ed25519 } = await import("@noble/curves/ed25519.js"); - const senderEncSk = new Uint8Array(32).fill(0x22); const senderSignSk = new Uint8Array(32).fill(0x11); + assert.equal(toHex(ed25519.getPublicKey(senderSignSk)), RUST.senderSignPk); const packed = await pack( new TextEncoder().encode("hello from js tsp"), "did:web:alice.example", "did:web:bob.example", { senderSigningKey: senderSignSk, - senderEncryptionKey: senderEncSk, receiverEncryptionKey: x25519.getPublicKey(receiverSk), }, ); const unpacked = await unpack(packed.bytes, { receiverDecryptionKey: receiverSk, - senderEncryptionKey: x25519.getPublicKey(senderEncSk), senderSigningKey: ed25519.getPublicKey(senderSignSk), }); assert.equal(new TextDecoder().decode(unpacked.payload), "hello from js tsp"); }); -// ── Rev 3, the other direction ── -// -// The vector above and the published Appendix A vectors in -// `interop.spec-vectors.mjs` both check our *decoder*. Nothing in a JS-only -// suite can check the encoder: a round trip passes whenever pack and unpack -// share a misreading, which is precisely the failure mode the one-character -// changes in Rev 3 produce. -// -// So the encoder was checked by running affinidi-tsp against it. A message -// packed here with the fixed keys below was handed to `direct::unpack` on the -// crate's `tsp-rev3` branch (worktree at fb4e23a), which recovered the sender, -// the receiver, the plaintext and a byte-identical thread digest. -// -// What is pinned here is what that run cannot: the deterministic parts of what -// we emit. The sealed message itself is not reproducible — HPKE draws a fresh -// ephemeral key every time — but the payload frame and the envelope fields are, -// and between them they carry every layout decision Rev 3 changed. If either -// moves, the cross-implementation run above is stale and has to be redone. -test("Rev 3 — the envelope fields we emit are byte-exact", async () => { - const { encodeFields } = await import("../dist/rev3/envelope.js"); +test("the envelope fields we emit are byte-exact", async () => { + const { encodeFields } = await import("../dist/v1/envelope.js"); const fields = encodeFields("did:web:alice.example", "did:web:bob.example"); assert.equal( toHex(fields), "61348f" + // YTSP - "f80002" + // YTSP-AAC + "f81000" + // YTSP-BAA "e010076469643a7765623a616c6963652e6578616d706c65" + // alice, 0 lead "e81007" + "0000" + "6469643a7765623a626f622e6578616d706c65", // bob, 2 lead ); + // And they are the envelope affinidi-tsp wrote: the Rust vector opens with + // `-E` and then exactly these bytes. + assert.equal(RUST.wireHex.slice(6, 6 + toHex(fields).length), toHex(fields)); }); -test("Rev 3 — the payload frame we emit is byte-exact", async () => { - const { encodePayloadFrame } = await import("../dist/rev3/payload.js"); +test("the payload frame we emit is byte-exact", async () => { + const { encodePayloadFrame } = await import("../dist/v1/payload.js"); const { frame, threadDigest } = encodePayloadFrame( new TextEncoder().encode("hello from js tsp rev3"), "direct", @@ -122,7 +108,8 @@ test("Rev 3 — the payload frame we emit is byte-exact", async () => { "f80009" + // -A generic stream, 9 quadlets "e81008" + "0000" + "68656c6c6f2066726f6d206a73207473702072657633", // B: the body ); - // The digest affinidi-tsp independently recomputed from these bytes. + // The digest affinidi-tsp independently recomputed from these bytes. The + // payload frame is unchanged from Rev 3, which already carried the sender. assert.equal( toHex(threadDigest), "625026c6e4a2f5061e2403777cbb2bc3d1595f4c8593d5d88a61f89c9a9b4232", diff --git a/packages/tsp-js/tests/interop.spec-vectors-repack.mjs b/packages/tsp-js/tests/interop.spec-vectors-repack.mjs index 4981b789..f92ae3c4 100644 --- a/packages/tsp-js/tests/interop.spec-vectors-repack.mjs +++ b/packages/tsp-js/tests/interop.spec-vectors-repack.mjs @@ -5,7 +5,7 @@ import { readFileSync } from "node:fs"; import { decodeEnvelope, unpack } from "../dist/index.js"; import * as wire from "../dist/cesr/wire.js"; import { deriveKeyPair } from "../dist/crypto/hpke-noble.js"; -import { decodeDigest, decodeNonce, decodeVidList } from "../dist/rev3/fields.js"; +import { decodeDigest, decodeNonce, decodeVidList } from "../dist/v1/fields.js"; import { __unsafeDeterministicPack, __unsafeDeterministicPackAccept, @@ -29,16 +29,17 @@ import { // `./unsafe-testing` subpath, whose fixed ephemeral must never be used outside a // test (see that module). // -// Every input is derived from the vector, never hard-coded: the NULL-vs-present -// ESSR sender field and the padding from the printed `payload`, the nonce and -// digests and the inner message of a nesting from that same payload. +// Every input is derived from the vector, never hard-coded: the padding from +// the printed `payload`, the nonce and digests and the inner message of a +// nesting from that same payload. The ESSR sender field is checked rather than +// derived: 1.0 makes it the sender's VID, and that is all this packer writes. // -// Of the ten vectors, six are HPKE-Base and all six re-pack. The other four +// Of the eleven vectors, seven are HPKE-Base and six re-pack. The other five // cannot, for reasons that are about what this package packs, not about the // vectors — named below rather than silently skipped. const VECTORS = JSON.parse( - readFileSync(new URL("./fixtures/spec-rev3-vectors.json", import.meta.url), "utf8"), + readFileSync(new URL("./fixtures/spec-v1-vectors.json", import.meta.url), "utf8"), ); const b64u = (s) => new Uint8Array(Buffer.from(s, "base64url")); @@ -48,8 +49,8 @@ const eq = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]); /** * Read the printed payload frame far enough to recover what a packer has to be - * told: the type code, whether the ESSR sender field is the NULL VID, the - * type-specific fields, and the padding. Parsed with the wire primitives rather + * told: the type code, the ESSR sender field, the type-specific fields, and + * the padding. Parsed with the wire primitives rather * than `decodePayloadFrame`, so a mis-reading there cannot hide one here. */ function readPrintedPayload(printed) { @@ -111,10 +112,10 @@ async function repack(name) { // a vector used either differently, re-packing it would be a different claim // and this says so instead of producing bytes that merely fail to match. assert.equal(p.padding.length, 0, `${name}: the packer writes an empty padding field`); - assert.ok(p.sender === "" || p.sender === sender.id, `${name}: ESSR sender is NULL or the sender`); + assert.equal(p.sender, sender.id, `${name}: the ESSR sender names the sender (1.0)`); const keys = { senderSigningKey: b64u(sender.skS), receiverEncryptionKey: b64u(receiver.pkE) }; - const unsafe = { __unsafeIkmE: b64u(v.ikmE), nullPayloadSender: p.sender === "" }; + const unsafe = { __unsafeIkmE: b64u(v.ikmE) }; switch (p.type) { case "scs": @@ -236,11 +237,18 @@ test("vectors that cannot be re-packed here are named, with the reason", () => { // The hybrid KEM draws its own encapsulation randomness and the vector // publishes no ephemeral material; nor does this package pack ML-KEM/ML-DSA. "direct-hpke-base-pq": "ML-KEM-768/X25519 + ML-DSA-65: not implemented, and no published ephemeral", + // HPKE-Base, with a published ikmE, but its Signature_new is made by the + // introduced VID's key and no packer here takes one: referrals are read + // (see `control.spec-vectors.mjs`), not written. + "control-rfi-referral": "referral invites are decode-only", }; assert.deepEqual( new Set([...REPACKED, ...Object.keys(notRepacked)]), new Set(Object.keys(VECTORS.vectors)), "every published vector is either re-packed or named", ); - for (const name of Object.keys(notRepacked)) assert.equal(VECTORS.vectors[name].ikmE, undefined, name); + for (const name of Object.keys(notRepacked)) { + if (name === "control-rfi-referral") continue; // has ikmE; see above + assert.equal(VECTORS.vectors[name].ikmE, undefined, name); + } }); diff --git a/packages/tsp-js/tests/interop.spec-vectors.mjs b/packages/tsp-js/tests/interop.spec-vectors.mjs index 5d20b780..b1928790 100644 --- a/packages/tsp-js/tests/interop.spec-vectors.mjs +++ b/packages/tsp-js/tests/interop.spec-vectors.mjs @@ -3,14 +3,15 @@ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { unpack, peekRevision } from "../dist/index.js"; +import * as wire from "../dist/cesr/wire.js"; // ── The specification's own Appendix A test vectors ── // -// Fixture extracted from Appendix A of the merged specification -// (trustoverip/tswg-tsp-specification f5b8668), which packs `YTSP-AAC`. It -// replaced the pre-merge `YTSP-ABA` vectors from spec commit 66a1580: the -// version is inside the signed envelope, the AAD and every SAID, so every -// message changed with it. +// Fixture from Appendix A of the released specification, TSP 1.0 +// (trustoverip/tswg-tsp-specification ccda6b3), which packs `YTSP-BAA`. It +// replaced the Rev 3 `YTSP-AAC` vectors: the version is inside the signed +// envelope, the AAD and every SAID, so every message changed with it, and the +// confidential ones gained the sender VID in their payload. // // These check something neither a round trip nor a two-implementation interop // run can. A round trip passes whenever the encoder and decoder share a @@ -18,15 +19,15 @@ import { unpack, peekRevision } from "../dist/index.js"; // unpacks with the other passes whenever *both* share one. These bytes are // fixed, external, and were produced by neither of us. // -// Three of the ten are exercised here and the three HPKE-Base control vectors in -// `control.spec-vectors.mjs`. `direct-sealed-box` and `control-rfi-sealed-box` +// Three of the eleven are exercised here and the four HPKE-Base control vectors +// in `control.spec-vectors.mjs`. `direct-sealed-box` and `control-rfi-sealed-box` // need the libsodium sealed box (§8.3), which we deliberately do not implement; // `direct-signed-only` is an unencrypted `-E` frame we never receive; // `direct-hpke-base-pq` needs ML-KEM and ML-DSA. Each is skipped by // name below rather than silently absent, so the list says what is missing // instead of looking complete. const VECTORS = JSON.parse( - readFileSync(new URL("./fixtures/spec-rev3-vectors.json", import.meta.url), "utf8"), + readFileSync(new URL("./fixtures/spec-v1-vectors.json", import.meta.url), "utf8"), ); const b64u = (s) => new Uint8Array(Buffer.from(s, "base64url")); @@ -43,25 +44,20 @@ const openVector = (name) => { }); }; -test("the published vectors declare Rev 3 as we pack it: YTSP-AAC", () => { - // The merged Appendix A carries `YTSP-AAC` — MINOR 2 under the MAJOR.MINOR - // reading this package and affinidi-tsp both use, the same marker our own - // packing emits. Pre-merge drafts carried `ABA` (MINOR 64), which still reads - // as Rev 3 (see `revision.dispatch.mjs`); `peekRevision` matches Rev 2 exactly - // and treats everything else at MAJOR 0 as current. +test("the published vectors declare the version we pack: YTSP-BAA", () => { + // A stale fixture would fail every open on MAJOR anyway; this is the test + // whose failure says so in as many words. for (const [name, v] of Object.entries(VECTORS.vectors)) { const peeked = peekRevision(b64u(v.message)); - assert.equal(peeked.revision, "rev3", name); - assert.equal(peeked.major, 0, name); - assert.equal(peeked.minor, 2, name); + assert.equal(peeked.major, 1, name); + assert.equal(peeked.minor, 0, name); assert.equal(peeked.recognised, true, name); - assert.equal(v.message.slice(4, 12), "YTSP-AAC", `${name} carries YTSP-AAC in qb64`); + assert.equal(v.message.slice(4, 12), "YTSP-BAA", `${name} carries YTSP-BAA in qb64`); } }); test("direct-hpke-base — opens, verifies, and carries the published payload", async () => { const unpacked = await openVector("direct-hpke-base"); - assert.equal(unpacked.revision, "rev3"); assert.equal(unpacked.messageType, "direct"); assert.equal(unpacked.sender, id("alice").id); assert.equal(unpacked.receiver, id("bob").id); @@ -69,20 +65,16 @@ test("direct-hpke-base — opens, verifies, and carries the published payload", assert.deepEqual(unpacked.hops, []); }); -test("direct-hpke-base — the ESSR sender field may be the NULL VID", async () => { - // This vector's payload frame is `-ZAJ XSCS 4BAA 4BAA -AAF 5BAE `: - // the sender field is `4BAA`, the NULL VID. §9.2 permits it under HPKE-Base, - // and affinidi-tsp writes the real VID instead — so a decoder that required - // either spelling would reject half the conformant messages in existence. - // qb64 `-ZAJ XSCS 4BAA 4BAA -AAF 5BAE `, which in the binary - // domain this package works in is the layout below. The third field — the - // ESSR sender — is `e01000`, an empty `B` var-data field: the NULL VID. - const frame = Buffer.from(VECTORS.vectors["direct-hpke-base"].payload, "base64url"); - assert.equal(frame.subarray(0, 3).toString("hex"), "f99009", "-Z count, 9 quadlets"); - assert.equal(frame.subarray(3, 6).toString("hex"), "5d2092", "XSCS type code"); - assert.equal(frame.subarray(6, 9).toString("hex"), "e01000", "ESSR sender: NULL VID"); - assert.equal(frame.subarray(9, 12).toString("hex"), "e01000", "padding: empty field"); - assert.equal(frame.subarray(12, 15).toString("hex"), "f80005", "-A generic stream, 5 quadlets"); +test("direct-hpke-base — the ESSR sender field names the sender", async () => { + // 1.0 requires the sender VID in every confidential payload (§3.2), where + // Rev 3 let HPKE-Base write the NULL VID `4BAA` — which is what this vector + // carried at `YTSP-AAC`. Now: `-ZAc XSCS 4BAT 4BAA -AAF 5BAE `, and the field after the type code is alice's VID. + const frame = b64u(VECTORS.vectors["direct-hpke-base"].payload); + assert.equal(Buffer.from(frame.subarray(3, 6)).toString("hex"), "5d2092", "XSCS type code"); + const cur = { pos: 6 }; + const sender = new TextDecoder().decode(wire.decodeVariableData(wire.TSP_VID, frame, cur)); + assert.equal(sender, id("alice").id, "ESSR sender: the sender's VID, not 4BAA"); await assert.doesNotReject(() => openVector("direct-hpke-base")); }); @@ -94,7 +86,7 @@ test("nested-direct — the inner message is carried raw, not in a B field", asy // carries it raw. Reading the payload as a whole TSP message is what says we // stripped nothing and added nothing: it must still lead with a `-E` frame. const inner = peekRevision(unpacked.payload); - assert.equal(inner.revision, "rev3"); + assert.deepEqual([inner.major, inner.minor], [1, 0]); }); test("routed — the hop list survives the -J byte-length count", async () => { @@ -138,6 +130,7 @@ test("vectors this implementation does not cover are named, not omitted", () => "control-rfi-direct", "control-rfa-direct", "control-rfd", + "control-rfi-referral", ]; assert.deepEqual( new Set([...covered, ...Object.keys(uncovered)]), diff --git a/packages/tsp-js/tests/message.direct.mjs b/packages/tsp-js/tests/message.direct.mjs index 58afb527..d6b2ce8c 100644 --- a/packages/tsp-js/tests/message.direct.mjs +++ b/packages/tsp-js/tests/message.direct.mjs @@ -8,21 +8,18 @@ const enc = new TextEncoder(); const dec = new TextDecoder(); // Mirrors affinidi-tsp src/message/direct.rs tests (random keys, JS-internal -// pack→unpack round-trip). Byte-exact cross-impl vectors against the Rust crate -// are a follow-up (needs fixed-key vectors generated from Rust). +// pack→unpack round-trip). The cross-implementation checks against the Rust +// crate are in `interop.rust-vector.mjs`. function genKeys() { const senderSign = ed25519.utils.randomSecretKey(); - const senderEnc = x25519.utils.randomSecretKey(); const receiverEnc = x25519.utils.randomSecretKey(); return { packKeys: { senderSigningKey: senderSign, - senderEncryptionKey: senderEnc, receiverEncryptionKey: x25519.getPublicKey(receiverEnc), }, unpackKeys: { receiverDecryptionKey: receiverEnc, - senderEncryptionKey: x25519.getPublicKey(senderEnc), senderSigningKey: ed25519.getPublicKey(senderSign), }, }; diff --git a/packages/tsp-js/tests/message.envelope.mjs b/packages/tsp-js/tests/message.envelope.mjs index 71cee869..9415e9d9 100644 --- a/packages/tsp-js/tests/message.envelope.mjs +++ b/packages/tsp-js/tests/message.envelope.mjs @@ -2,26 +2,26 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; -import { decodeEnvelope } from "../dist/index.js"; -import { encodeFields, finalizeFrame, decodeEnvelope as decodeRev3 } from "../dist/rev3/envelope.js"; +import { decodeEnvelope, isRevisionError } from "../dist/index.js"; +import { encodeFields, finalizeFrame, decodeEnvelope as decodeV1 } from "../dist/v1/envelope.js"; const hex = (u8) => Buffer.from(u8).toString("hex"); const b64u = (s) => new Uint8Array(Buffer.from(s, "base64url")); const VECTORS = JSON.parse( - readFileSync(new URL("./fixtures/spec-rev3-vectors.json", import.meta.url), "utf8"), + readFileSync(new URL("./fixtures/spec-v1-vectors.json", import.meta.url), "utf8"), ); -test("Rev 3 envelope fields are byte-exact, and three bytes shorter than Rev 2", () => { +test("envelope fields are byte-exact, and three bytes shorter than Rev 2's", () => { const fields = encodeFields("did:web:bob.example", "did:web:alice.example"); assert.equal( hex(fields.slice(0, 9)), "61348f" + // YTSP - "f80002" + // version count: MAJOR 0, MINOR 2 — `YTSP-AAC` + "f81000" + // version count: MAJOR 1, MINOR 0 — `YTSP-BAA` "e81007", // sender var-data header (19 bytes → 2 lead, D6 selector) ); // 6 (version) + 24 (bob, 2 lead) + 24 (alice, 0 lead) = 54. Rev 2 was 57: - // it ended with a 2-byte `X 00 00` TMP marker that Rev 3 deletes outright. + // it ended with a 2-byte `X 00 00` TMP marker that Rev 3 deleted outright. assert.equal(fields.length, 54); }); @@ -30,13 +30,13 @@ test("the -E count covers the body, which is why it is written last", () => { const body = new Uint8Array(9).fill(0xaa); const frame = finalizeFrame(fields, body); - // Rev 2's count covered only the header and so could be written first. Rev 3's - // covers fields ‖ body — 63 bytes, 21 quadlets — and cannot exist until the + // Rev 2's count covered only the header and so could be written first. Since + // Rev 3 it covers fields ‖ body — 63 bytes, 21 quadlets — and cannot exist until the // ciphertext does. assert.equal(hex(frame.slice(0, 3)), "f84015"); // -E, 21 quadlets assert.equal(frame.length, 3 + 54 + 9); - const decoded = decodeRev3(frame); + const decoded = decodeV1(frame); assert.equal(decoded.contentEnd, frame.length, "the count fixes where signable content ends"); assert.equal(decoded.headerLen, 3 + 54, "the ciphertext field begins after the fields"); }); @@ -46,19 +46,19 @@ test("the AAD is the fields without the count code", () => { // deliberately outside it, which is exactly why encoding splits in two. const fields = encodeFields("did:web:alice.example", "did:web:bob.example"); const frame = finalizeFrame(fields, new Uint8Array(0)); - const decoded = decodeRev3(frame); + const decoded = decodeV1(frame); assert.deepEqual(frame.slice(decoded.aad.begin, decoded.aad.end), fields); assert.equal(decoded.aad.begin, 3, "the AAD starts after the count code, not at 0"); }); -test("Rev 3 envelope round-trips for varied VID lengths", () => { +test("the envelope round-trips for varied VID lengths", () => { for (const [s, r] of [ ["a", "b"], ["did:web:x", "did:web:y"], ["did:key:z6Mkexample", "did:web:host.example:path"], ]) { const frame = finalizeFrame(encodeFields(s, r), new Uint8Array(0)); - const { envelope } = decodeRev3(frame); + const { envelope } = decodeV1(frame); assert.equal(envelope.sender, s); assert.equal(envelope.receiver, r); } @@ -70,10 +70,10 @@ test("an empty receiver is the NULL VID, and an empty sender is refused", () => // the two empty strings are not symmetric and must not be handled as if they // were. const frame = finalizeFrame(encodeFields("did:web:alice", ""), new Uint8Array(0)); - assert.equal(decodeRev3(frame).envelope.receiver, ""); + assert.equal(decodeV1(frame).envelope.receiver, ""); const headless = finalizeFrame(encodeFields("", "did:web:bob"), new Uint8Array(0)); - assert.throws(() => decodeRev3(headless), /NULL VID/); + assert.throws(() => decodeV1(headless), /NULL VID/); }); test("a frame claiming more content than it holds is refused at the count", () => { @@ -84,27 +84,25 @@ test("a frame claiming more content than it holds is refused at the count", () = // failure three layers down. const lying = Uint8Array.from(honest); lying[2] += 1; - assert.throws(() => decodeRev3(lying), /declares more content/); + assert.throws(() => decodeV1(lying), /declares more content/); }); -test("the public decodeEnvelope dispatches, and reports which revision it read", () => { - // Rev 3, from the published vectors. - const rev3 = decodeEnvelope(b64u(VECTORS.vectors["direct-hpke-base"].message)); - assert.equal(rev3.revision, "rev3"); - assert.equal(rev3.minor, 2); // the merged vectors carry `AAC` - assert.equal(rev3.envelope.sender, VECTORS.identifiers.alice.id); - assert.equal(rev3.envelope.receiver, VECTORS.identifiers.bob.id); +test("the public decodeEnvelope reads 1.0, and refuses a draft before reading its VIDs", () => { + const v1 = decodeEnvelope(b64u(VECTORS.vectors["direct-hpke-base"].message)); + assert.equal(v1.minor, 0); + assert.equal(v1.envelope.sender, VECTORS.identifiers.alice.id); + assert.equal(v1.envelope.receiver, VECTORS.identifiers.bob.id); - // Rev 2, from the pinned Rust interop vector in `interop.rust-vector.mjs`. - const rev2Wire = Buffer.from( - "f8401361348ff80001e010076469643a7765623a616c6963652e6578616d706c65e8100700006469643a7765623a626f622e6578616d706c655c0000e0601a5795132915e698a115677334d13dd7154f717eda8791473ccbb360671313f40544e2ae9153559a01d6aa33b93261dd0ab610231bad47e059d0eaa46038cf872ba82a282a431fd391e10f4d3c0603f82016f8a016d0100d308cdcf413984d884ff81ac2308da9d3afc9a0601e9393f664d54f9c37892897e996a0c8949ca8afa643ed39f888312094f6c34c55a1f4c3c0032f969cb707", - "hex", + // The same message with its version count rewritten to Rev 3's `AAC`. The + // VIDs are byte-identical across revisions and would parse; the gate is what + // refuses it. + const draft = b64u(VECTORS.vectors["direct-hpke-base"].message); + draft[7] = 0x00; + draft[8] = 0x02; + assert.throws( + () => decodeEnvelope(draft), + (err) => isRevisionError(err) && err.major === 0 && err.minor === 2, ); - const rev2 = decodeEnvelope(new Uint8Array(rev2Wire)); - assert.equal(rev2.revision, "rev2"); - assert.equal(rev2.minor, 1); - assert.equal(rev2.envelope.sender, "did:web:alice.example"); - assert.equal(rev2.envelope.receiver, "did:web:bob.example"); }); test("truncated and non-TSP input throws", () => { diff --git a/packages/tsp-js/tests/message.routed.mjs b/packages/tsp-js/tests/message.routed.mjs index 71eed4fb..c556eac8 100644 --- a/packages/tsp-js/tests/message.routed.mjs +++ b/packages/tsp-js/tests/message.routed.mjs @@ -20,12 +20,10 @@ function party(vid) { } const packKeys = (sender, receiver) => ({ senderSigningKey: sender.signSk, - senderEncryptionKey: sender.encSk, receiverEncryptionKey: receiver.encPk, }); const unpackKeys = (receiver, sender) => ({ receiverDecryptionKey: receiver.encSk, - senderEncryptionKey: sender.encPk, senderSigningKey: sender.signPk, }); diff --git a/packages/tsp-js/tests/payload.app-stream.mjs b/packages/tsp-js/tests/payload.app-stream.mjs index 0fa167d9..3cd7f6bc 100644 --- a/packages/tsp-js/tests/payload.app-stream.mjs +++ b/packages/tsp-js/tests/payload.app-stream.mjs @@ -2,7 +2,7 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import * as wire from "../dist/cesr/wire.js"; -import { decodePayloadFrame } from "../dist/rev3/payload.js"; +import { decodePayloadFrame } from "../dist/v1/payload.js"; // An XSCS body is exactly one Bytes primitive (tswg-tsp-specification#77): // an -H## group, a second primitive, or data after the stream is refused, @@ -14,7 +14,7 @@ const prim = (s) => { return out; }; const frameOf = (stream, trailing = []) => { - const body = [...wire.XSCS, ...prim(""), ...prim("")]; // NULL sender, no padding + const body = [...wire.XSCS, ...prim("did:example:alice"), ...prim("")]; // sender, no padding wire.encodeCount(wire.TSP_GENERIC_STREAM, stream.length / 3, body); body.push(...stream, ...trailing); const frame = []; diff --git a/packages/tsp-js/tests/revision.dispatch.mjs b/packages/tsp-js/tests/revision.dispatch.mjs deleted file mode 100644 index 31864841..00000000 --- a/packages/tsp-js/tests/revision.dispatch.mjs +++ /dev/null @@ -1,182 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert/strict"; -import { readFileSync } from "node:fs"; - -import { pack, unpack, peekRevision, isRevisionError, isTsp } from "../dist/index.js"; -import { ed25519, x25519 } from "@noble/curves/ed25519.js"; - -const enc = new TextEncoder(); -const dec = new TextDecoder(); -const b64u = (s) => new Uint8Array(Buffer.from(s, "base64url")); - -const VECTORS = JSON.parse( - readFileSync(new URL("./fixtures/spec-rev3-vectors.json", import.meta.url), "utf8"), -); - -// The Rev 2 golden message from `interop.rust-vector.mjs`: packed by -// affinidi-tsp 0.1.x with fixed keys, alice.example → bob.example. -const REV2 = { - wire: new Uint8Array( - Buffer.from( - "f8401361348ff80001e010076469643a7765623a616c6963652e6578616d706c65e8100700006469643a7765623a626f622e6578616d706c655c0000e0601a5795132915e698a115677334d13dd7154f717eda8791473ccbb360671313f40544e2ae9153559a01d6aa33b93261dd0ab610231bad47e059d0eaa46038cf872ba82a282a431fd391e10f4d3c0603f82016f8a016d0100d308cdcf413984d884ff81ac2308da9d3afc9a0601e9393f664d54f9c37892897e996a0c8949ca8afa643ed39f888312094f6c34c55a1f4c3c0032f969cb707", - "hex", - ), - ), - senderEncryptionKey: new Uint8Array( - Buffer.from("0faa684ed28867b97f4a6a2dee5df8ce974e76b7018e3f22a1c4cf2678570f20", "hex"), - ), - senderSigningKey: new Uint8Array( - Buffer.from("d04ab232742bb4ab3a1368bd4615e4e6d0224ab71a016baf8520a332c9778737", "hex"), - ), - receiverDecryptionKey: new Uint8Array( - Buffer.from("3333333333333333333333333333333333333333333333333333333333333333", "hex"), - ), -}; - -const party = (vid) => { - const sk = ed25519.utils.randomSecretKey(); - const xsk = ed25519.utils.toMontgomerySecret(sk); - return { vid, sk, pk: ed25519.getPublicKey(sk), xsk, xpk: x25519.getPublicKey(xsk) }; -}; - -// ── peekRevision: the keyless discriminator ── - -test("peekRevision reads Rev 2 and Rev 3 from the version marker alone", () => { - assert.equal(peekRevision(REV2.wire).revision, "rev2"); - assert.equal(peekRevision(REV2.wire).minor, 1); - assert.equal(peekRevision(b64u(VECTORS.vectors["direct-hpke-base"].message)).revision, "rev3"); -}); - -test("peekRevision refuses what is not a TSP frame, with a matchable code", () => { - for (const junk of [enc.encode('{"protected":"..."}'), enc.encode("eyJhbGciOiJ"), new Uint8Array(0)]) { - assert.throws( - () => peekRevision(junk), - (err) => isRevisionError(err) && err.code === "E_TSP_REVISION", - ); - } -}); - -test("peekRevision refuses an unknown MAJOR and carries it on the error", () => { - // MAJOR is the only component that gates processability (§9.1), so this is - // the one version check that is allowed to refuse a message. - const frame = Uint8Array.from(REV2.wire); - frame[7] = 0x10; // version count code: MAJOR 0 → 1 - assert.throws( - () => peekRevision(frame), - (err) => isRevisionError(err) && err.major === 1, - ); -}); - -test("an unrecognised MINOR at MAJOR 0 still reads as Rev 3", () => { - // Rev 2 is the only MINOR matched exactly; everything else at MAJOR 0 is - // current-generation. A future `YTSP-AAD` must not be refused — §9.1 makes - // MINOR a field no implementation may reject on, and the reference discards - // it entirely. - const frame = Uint8Array.from(b64u(VECTORS.vectors["direct-hpke-base"].message)); - frame[8] = 0x0d; // MINOR 2 → 13, a value nothing has ever shipped - const peeked = peekRevision(frame); - assert.equal(peeked.revision, "rev3"); - assert.equal(peeked.recognised, false, "unrecognised, but not refused"); -}); - -// ── unpack: dispatch ── - -test("unpack routes a Rev 2 message to the Rev 2 reader and says so", async () => { - const out = await unpack(REV2.wire, { - receiverDecryptionKey: REV2.receiverDecryptionKey, - senderEncryptionKey: REV2.senderEncryptionKey, - senderSigningKey: REV2.senderSigningKey, - }); - assert.equal(out.revision, "rev2"); - assert.equal(dec.decode(out.payload), "hello from rust tsp"); - assert.equal(out.sender, "did:web:alice.example"); -}); - -test("a Rev 2 message without the sender's X25519 key is refused by name", async () => { - // HPKE-Auth puts the sender's static key in the KEM, so this is not "we could - // not verify the sender" — the message cannot be *opened* at all. Saying that - // beats an AEAD failure, which reads like tampering. - await assert.rejects( - () => - unpack(REV2.wire, { - receiverDecryptionKey: REV2.receiverDecryptionKey, - senderSigningKey: REV2.senderSigningKey, - }), - (err) => isRevisionError(err) && /Rev 2/.test(err.message), - ); -}); - -test("a pre-merge `YTSP-ABA` message still reads as Rev 3, and opens", async () => { - // Appendix A's `direct-hpke-base` as it stood before the specification merged - // (spec commit 66a1580), when the vectors carried `ABA` — MINOR 64 under the - // MAJOR.MINOR reading. The merged vectors moved to `AAC`, so this is pinned - // here: messages packed against the draft exist, and MINOR must never gate - // processing. Same published keys as the current fixture. - const aba = b64u( - "-EBFYTSP-ABA4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB4FAa1T4oA1pSbehBiIwnoXFGA24kgHowT34VdE95wF9qjStBsok4fIkbu8IKODF2nsZMUAmS5BqxDbbYrvl_TNGpz2omwJgYX4bSYoTeKse4-CAX-KAWBADHuTmj_7jyUCkkalySPOFiy5pTbNtjEODiwwJZlI5DqMk5Wutx4LIOWkAAa3uee2b_0Kh5SXaFq65MedyO5VYJ", - ); - const peeked = peekRevision(aba); - assert.equal(peeked.revision, "rev3"); - assert.equal(peeked.minor, 64); - assert.equal(peeked.recognised, true); - const out = await unpack(aba, { - receiverDecryptionKey: b64u(VECTORS.identifiers.bob.skE), - senderSigningKey: b64u(VECTORS.identifiers.alice.pkS), - }); - assert.equal(out.revision, "rev3"); - assert.equal(dec.decode(out.payload), "hello world"); -}); - -test("a Rev 3 message needs no sender encryption key at all", async () => { - // The clearest statement that HPKE-Base moved sender authenticity out of the - // KEM: the key Rev 2 could not open a message without is simply not passed. - const out = await unpack(b64u(VECTORS.vectors["direct-hpke-base"].message), { - receiverDecryptionKey: b64u(VECTORS.identifiers.bob.skE), - senderSigningKey: b64u(VECTORS.identifiers.alice.pkS), - }); - assert.equal(out.revision, "rev3"); - assert.equal(dec.decode(out.payload), "hello world"); -}); - -test("what we pack is Rev 3, and it round-trips as Rev 3", async () => { - const alice = party("did:web:alice"); - const bob = party("did:web:bob"); - const packed = await pack(enc.encode("hello"), alice.vid, bob.vid, { - senderSigningKey: alice.sk, - receiverEncryptionKey: bob.xpk, - }); - assert.equal(peekRevision(packed.bytes).minor, 2, "we emit YTSP-AAC"); - const out = await unpack(packed.bytes, { - receiverDecryptionKey: bob.xsk, - senderSigningKey: alice.pk, - }); - assert.equal(out.revision, "rev3"); - assert.equal(dec.decode(out.payload), "hello"); -}); - -// ── The long-framed path ── - -test("a message past ~12 KB is long-framed, and still peeks and round-trips", async () => { - // This is the case Rev 2 could never produce: its `-E` count covered only the - // header. Rev 3's covers the ciphertext, so a large message leads with 0xFB - // and its count is a six-byte long form — the exact header whose decode used - // to fold the identifier bits into the length. - const alice = party("did:web:alice"); - const bob = party("did:web:bob"); - const body = new Uint8Array(20_000).map((_, i) => (i * 31 + 7) & 0xff); - - const packed = await pack(body, alice.vid, bob.vid, { - senderSigningKey: alice.sk, - receiverEncryptionKey: bob.xpk, - }); - - assert.equal(packed.bytes[0], 0xfb, "long -E framing"); - assert.equal(isTsp(packed.bytes), true, "an 0xF8-only classifier would drop this"); - assert.equal(peekRevision(packed.bytes).revision, "rev3", "the version is at offset 6, not 3"); - - const out = await unpack(packed.bytes, { - receiverDecryptionKey: bob.xsk, - senderSigningKey: alice.pk, - }); - assert.deepEqual(out.payload, body); -}); diff --git a/packages/tsp-js/tests/revision.gate.mjs b/packages/tsp-js/tests/revision.gate.mjs new file mode 100644 index 00000000..a63bbbec --- /dev/null +++ b/packages/tsp-js/tests/revision.gate.mjs @@ -0,0 +1,173 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; + +import { pack, unpack, peekRevision, isRevisionError, isTsp } from "../dist/index.js"; +import { decodePayloadFrame, encodePayloadFrame } from "../dist/v1/payload.js"; +import { encodeFields } from "../dist/v1/envelope.js"; +import { ed25519, x25519 } from "@noble/curves/ed25519.js"; + +const enc = new TextEncoder(); +const dec = new TextDecoder(); +const b64u = (s) => new Uint8Array(Buffer.from(s, "base64url")); + +const VECTORS = JSON.parse( + readFileSync(new URL("./fixtures/spec-v1-vectors.json", import.meta.url), "utf8"), +); + +// Draft-revision messages, every one of which 1.0 refuses on MAJOR. +// +// Rev 2 (`YTSP-AAB`, 0.1): packed by affinidi-tsp 0.1.x with fixed keys, +// alice.example → bob.example. This package read it until 1.0. +const REV2 = new Uint8Array( + Buffer.from( + "f8401361348ff80001e010076469643a7765623a616c6963652e6578616d706c65e8100700006469643a7765623a626f622e6578616d706c655c0000e0601a5795132915e698a115677334d13dd7154f717eda8791473ccbb360671313f40544e2ae9153559a01d6aa33b93261dd0ab610231bad47e059d0eaa46038cf872ba82a282a431fd391e10f4d3c0603f82016f8a016d0100d308cdcf413984d884ff81ac2308da9d3afc9a0601e9393f664d54f9c37892897e996a0c8949ca8afa643ed39f888312094f6c34c55a1f4c3c0032f969cb707", + "hex", + ), +); + +// Rev 3 (`YTSP-AAC`, 0.2): Appendix A's `direct-hpke-base` as the merged Rev 3 +// specification printed it (spec commit f5b8668). Same published keys as the +// 1.0 fixture, and an ESSR sender field of `4BAA` that 1.0 would refuse anyway. +const REV3_AAC = b64u( + "-EBFYTSP-AAC4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB4FAa1T4oA1pSbehBiIwnoXFGA24kgHowT34VdE95wF9qjStBsok4fIkbu8IKODF2nsZMUAmS5BqxDbbYrvl_TNFj6rHuLymWkvlrkt54-cOq-CAX-KAWBADw48mquyWI0O40nsbcI7jRugSJko0JMZR-4S07YFJq3Mzy6jvAkEsDGAecW9jCUVDdwZScckWUaAzSSGbNunMN", +); + +// The same message as the pre-merge Rev 3 drafts printed it (spec commit +// 66a1580), `YTSP-ABA`: MINOR 64 under the MAJOR.MINOR reading. +const REV3_ABA = b64u( + "-EBFYTSP-ABA4BATZGlkOnBlZXI6NHpRbVVMNjFOYzFGN2lvaUt4SE5xd25KWFg0c3JoRnNLS1BvNlRyQ21oTTNkZnBx4BATZGlkOnBlZXI6NHpRbVptQ0FzRzdqMWV3VGpYanRkZHd1amlrMzNDRTJjTWJZU1BhZ3BNaVludDFB4FAa1T4oA1pSbehBiIwnoXFGA24kgHowT34VdE95wF9qjStBsok4fIkbu8IKODF2nsZMUAmS5BqxDbbYrvl_TNGpz2omwJgYX4bSYoTeKse4-CAX-KAWBADHuTmj_7jyUCkkalySPOFiy5pTbNtjEODiwwJZlI5DqMk5Wutx4LIOWkAAa3uee2b_0Kh5SXaFq65MedyO5VYJ", +); + +const V1 = b64u(VECTORS.vectors["direct-hpke-base"].message); +const v1Keys = { + receiverDecryptionKey: b64u(VECTORS.identifiers.bob.skE), + senderSigningKey: b64u(VECTORS.identifiers.alice.pkS), +}; + +const party = (vid) => { + const sk = ed25519.utils.randomSecretKey(); + const xsk = ed25519.utils.toMontgomerySecret(sk); + return { vid, sk, pk: ed25519.getPublicKey(sk), xsk, xpk: x25519.getPublicKey(xsk) }; +}; + +// ── peekRevision: the keyless version gate ── + +test("peekRevision reads 1.0 from the version marker alone", () => { + const peeked = peekRevision(V1); + assert.deepEqual([peeked.major, peeked.minor, peeked.recognised], [1, 0, true]); +}); + +test("peekRevision refuses what is not a TSP frame, with a matchable code", () => { + for (const junk of [enc.encode('{"protected":"..."}'), enc.encode("eyJhbGciOiJ"), new Uint8Array(0)]) { + assert.throws( + () => peekRevision(junk), + (err) => isRevisionError(err) && err.code === "E_TSP_REVISION", + ); + } +}); + +test("every pre-1.0 draft is refused on MAJOR, named, before a key is touched", async () => { + // MAJOR is the only component that gates processability (§9.1). The drafts + // are MAJOR 0; refusing them here, keyless, is what keeps a Rev 3 frame from + // being read under 1.0's payload rule — or a Rev 2 one from dying at the + // ciphertext selector with an error about crypto. + for (const [frame, minor, name] of [ + [REV2, 1, "Rev 2 (YTSP-AAB)"], + [REV3_AAC, 2, "Rev 3 (YTSP-AAC)"], + [REV3_ABA, 64, "Rev 3 pre-merge draft (YTSP-ABA)"], + ]) { + const isDraft = (err) => + isRevisionError(err) && + err.major === 0 && + err.minor === minor && + err.message.includes(name) && + err.message.includes("TSP 1.0 (YTSP-BAA)"); + assert.throws(() => peekRevision(frame), isDraft, name); + // Keys that would open the message are not enough: the gate comes first. + await assert.rejects(() => unpack(frame, v1Keys), isDraft, name); + } +}); + +test("a MAJOR from the future is refused the same way", () => { + const frame = Uint8Array.from(V1); + frame[7] = 0x20; // version count code: MAJOR 1 → 2 + assert.throws( + () => peekRevision(frame), + (err) => isRevisionError(err) && err.major === 2, + ); +}); + +test("an unrecognised MINOR at MAJOR 1 is carried, not refused", () => { + // A future `YTSP-BAB` must not be refused — §9.1 makes MINOR a field no + // implementation may reject on, and the reference discards it entirely. + const frame = Uint8Array.from(V1); + frame[8] = 0x0d; // MINOR 0 → 13, a value nothing has ever shipped + const peeked = peekRevision(frame); + assert.deepEqual([peeked.major, peeked.minor], [1, 13]); + assert.equal(peeked.recognised, false, "unrecognised, but not refused"); +}); + +// ── The 1.0 payload rule ── + +test("a confidential payload whose ESSR sender is the NULL VID is refused", () => { + // Rev 3 let HPKE-Base write `4BAA` there; 1.0 requires the sender's VID in + // every confidential payload (§3.2, §3.7 step 7). Every payload this package + // reads is confidential. Checked at the payload decoder because nothing here + // packs the NULL form — which is itself the send-side half of the rule. + const fields = encodeFields("did:web:alice", "did:web:bob"); + const nulled = encodePayloadFrame(enc.encode("x"), "direct", [], "").frame; + assert.throws(() => decodePayloadFrame(nulled, "did:web:alice", fields), /NULL VID/); + + const named = encodePayloadFrame(enc.encode("x"), "direct", [], "did:web:alice").frame; + assert.equal(dec.decode(decodePayloadFrame(named, "did:web:alice", fields).body), "x"); + assert.throws( + () => decodePayloadFrame(named, "did:web:mallory", fields), + /does not match the envelope sender/, + ); +}); + +// ── pack ── + +test("what we pack is 1.0, and it round-trips", async () => { + const alice = party("did:web:alice"); + const bob = party("did:web:bob"); + const packed = await pack(enc.encode("hello"), alice.vid, bob.vid, { + senderSigningKey: alice.sk, + receiverEncryptionKey: bob.xpk, + }); + const peeked = peekRevision(packed.bytes); + assert.deepEqual([peeked.major, peeked.minor], [1, 0], "we emit YTSP-BAA"); + const out = await unpack(packed.bytes, { + receiverDecryptionKey: bob.xsk, + senderSigningKey: alice.pk, + }); + assert.equal(dec.decode(out.payload), "hello"); +}); + +// ── The long-framed path ── + +test("a message past ~12 KB is long-framed, and still peeks and round-trips", async () => { + // Rev 2 could never produce this: its `-E` count covered only the header. + // Since Rev 3 the count covers the ciphertext, so a large message leads with + // 0xFB and its count is a six-byte long form — the exact header whose decode + // used to fold the identifier bits into the length. + const alice = party("did:web:alice"); + const bob = party("did:web:bob"); + const body = new Uint8Array(20_000).map((_, i) => (i * 31 + 7) & 0xff); + + const packed = await pack(body, alice.vid, bob.vid, { + senderSigningKey: alice.sk, + receiverEncryptionKey: bob.xpk, + }); + + assert.equal(packed.bytes[0], 0xfb, "long -E framing"); + assert.equal(isTsp(packed.bytes), true, "an 0xF8-only classifier would drop this"); + assert.equal(peekRevision(packed.bytes).major, 1, "the version is at offset 6, not 3"); + + const out = await unpack(packed.bytes, { + receiverDecryptionKey: bob.xsk, + senderSigningKey: alice.pk, + }); + assert.deepEqual(out.payload, body); +});