Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 10 additions & 16 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
21 changes: 8 additions & 13 deletions packages/core/src/vta/tsp-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -94,7 +92,6 @@ export function openReplyOnce(
try {
reply = await unpack(bytes, {
receiverDecryptionKey: holder.encryptionPrivateKey,
senderEncryptionKey: vta.encryptionPublicKey,
senderSigningKey: vta.signingPublicKey,
});
} catch (err) {
Expand Down Expand Up @@ -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;
}

Expand Down
1 change: 0 additions & 1 deletion packages/core/src/vta/tsp-inbound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
12 changes: 6 additions & 6 deletions packages/core/src/vta/tsp-relationship.ts
Original file line number Diff line number Diff line change
@@ -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.
//
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 0 additions & 2 deletions packages/core/tests/tsp.channel.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion packages/core/tests/tsp.inbound.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ async function sealed(payload, { from = executor, senderVid = from.vid } = {}) {
holder.vid,
{
senderSigningKey: from.signSk,
senderEncryptionKey: from.encSk,
receiverEncryptionKey: holder.encPk,
},
);
Expand Down
2 changes: 0 additions & 2 deletions packages/core/tests/vta.outbound-signing.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
},
);
Expand Down
62 changes: 46 additions & 16 deletions packages/tsp-js/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,61 @@ 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
or adopt an accept for an invite it never sent.
- `@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

Expand Down
Loading
Loading