Trust Spanning Protocol (TSP)

The Trust Spanning Protocol is the Trust Over IP Foundation’s messaging protocol for parties identified by verifiable identifiers — in this ecosystem, DIDs. It is the preferred transport of the whole stack (the official order is TSP > DIDComm > REST) and, since the VTI-Eucalyptus release of 2026-10-07 (coordinated-releases), the default one: a new VTA and a new mediator are built with TSP on, and every implementation in the ecosystem speaks TSP specification Rev 3 — version marker YTSP-AAC, the merged specification at trustoverip/tswg-tsp-specification commit f5b8668.

The one-sentence pitch: TSP gives two DIDs an end-to-end authenticated, encrypted channel whose intermediaries — the mediators that relay it — can be kept from learning who is talking to whom, at a message size that grows additively per hop rather than multiplying the way nested DIDComm does. That routing privacy is why the ecosystem adopted it, and (as the September 2026 decision below records) why it is keeping it.

What Rev 3 Is

Rev 3 is not an increment on Rev 2; it is a different wire. The crypto mode, the version byte, the long count-code prefix, the ciphertext layout and every payload type code changed at once, so “there is no frame the two revisions can both classify”. The ecosystem therefore cut over as a flag day with no Rev 2 compatibility on the Rust side — a Rev 2 frame arriving at a Rev 3 peer is reported as TspError::RevisionMismatch rather than misread. The pieces that matter to a reader of this wiki:

  • HPKE-Base, not HPKE-Auth. Rev 2 authenticated the sender inside the key encapsulation. Rev 3 uses RFC 9180’s base mode and carries sender authenticity in the ESSR signature alone (Encrypt-then-Sign with the sender’s VID inside the confidential payload, bound by AEAD associated data of TSP_Version ‖ VID_sndr ‖ VID_rcvr). This is the change with the largest architectural consequence: nothing in the middle can re-sign a message and still be honest about who sent it — which is exactly why the DIDComm↔TSP bridge had to go (below).
  • CESR encoding, tightened: --X##### long count codes, one -E frame whose count covers the ciphertext (so a message past ~12 KB is long-framed and starts --E), a 128-bit nonce, self-addressing TSP_Digest values (§7.2.1) that a receiver can actually recompute, and non-canonical padding rejected outright. New payload codes XSCS / XHOP / XRFI / XRFA / XRFD / XCTL / XPAD.
  • Relationships are enforced (§7.2.2). An endpoint drops — silently, by design — any application message from a VID it holds no relationship with. Relationships are formed by an invite/accept handshake (XRFI / XRFA), with an invite-race tiebreak by digest, three-case cancellation, Reply_Path, Referral_Field, fillable padding and upper-layer control messages. Under Rev 2 the ecosystem recorded relationships; under Rev 3 it must form them before any Trust Task.
  • Direct, Routed and Nested modes survive, with MAX_HOPS raised from 16 to 64 and §5.3.3’s rule that an intermediary must never persist the endpoint-to-endpoint VIDs of a nested message.
  • Post-quantum is now in the specification (§8): a hybrid MLKEM768-X25519 KEM and ML-DSA-65 signatures, with a published test vector. See post-quantum-cryptography for what is wired and what is not.

The version field is worth a footnote because it is a live interop question: the merged spec reads the three characters after YTSP- as MAJOR (one character) . MINOR (two), so Rev 3 is AAC where Rev 2 was AAB; pre-merge drafts printed ABA under a MAJOR.MINOR.PATCH reading. Only MAJOR gates processing in every implementation, so the disagreement (raised upstream on spec PR #63) does not affect whether messages open.

The Flag Day Across the Stack — September 2026

Because nothing a Rev 2 peer packs can be unpacked by a Rev 3 one, every repo had to move in the same window:

  • Affinidi TDK — affinidi-tsp 0.2.0 (#796, 2026-09-15). The hard wire break: HPKE-Auth → HPKE-Base, all the encoding changes, RelationshipPolicy::Gated as the default, key-state re-resolution on failure (§7.4.2), the PQ path behind a pq feature verified against the spec’s own vector, and the specification’s ten Appendix A vectors run as a test suite (checked against the ToIP reference tsp_sdk 0.10.0’s rev3.json — all pass, and five defects in the published appendix were found and reported on the way). Two latent bugs surfaced: long-form counts decoded to the wrong length, and long-framed messages were not recognized as TSP by any ingress classifier. Up the stack the messaging SDK (0.25 → 0.26.7) enforces relationship state on every unpack and grew a durable PersistentRelationshipStore plus a recovery runtime; the mediator (0.25.0) removed the DIDComm↔TSP bridge and collapsed five ACL-revealing refusal codes into one uniform delivery.refused. The libsodium sealed box (§8.3) is implemented for reading legacy peers only.
  • VTI — #1512 (2026-09-16). The workspace’s own inventory (tsp-rev3-migration.md) concluded that “the one thing that breaks silently” is §7.2.2: an unmigrated VTA that does not answer invites looks, from every client, like a transport that accepts connections and never replies. So the VTA and VTC now carry Trust Tasks inside the published tsp/0.1 binding envelope (#1478, #1488), form a relationship before any send or health ping (#1527, #1537, #1540), answer invites instead of ignoring them (#1525), persist relationships across restarts (#1531), and re-form them after a reply timeout on the client (#1544), in the VTC registry client (#1546) and for the VTA’s own server-initiated sends via a single-flight RecoveryCoordinator (#1549). Alongside it: one outbound transport seam chosen from what a peer advertises, replacing four hand-rolled carriages (#1474, with the webvh selector folded onto it in #1483), reply correlation so TSP can initiate (#1482), and the rule that there is one TSP transport and it must carry a mediator (#1507).
  • did-hosting-service — #194. An answering arm (it had only ever sent) plus persisted relationships, so a VTA’s did:webvh host stays reachable over TSP after either side restarts.
  • OpenVTC — #333 (= Eucalyptus-RC-0). Forms the relationship (form_relationship_routed, idempotent) before any VTC Trust Task, recognizes inbound RelationshipControl, persists relationships in ProtectedConfig, and gained a TSP Relationships pane (None / Pending / Invite received / Bidirectional per community) and an openvtc health finding that names the “no relationship → dropped” case — the explanation for a join to a healthy VTC that never gets a reply.
  • Browser wallet — @openvtc/vti-tsp-js 0.3.0 (#253). Pure-TypeScript TSP that packs Rev 3 only and reads Rev 3 and Rev 2 (a frozen decode-only codec, to be deleted once the last Rev 2 peer is gone), with the XRFI/XRFA handshake and §7.2/7.3 state machine. vti-didcomm-js 0.10.0 fixed the demultiplexer that had been sending Rev 3 long frames (--E) to the DIDComm unpacker — a poison frame redelivered forever while the TSP consumer never heard; the mediator’s affinidi_tsp::is_tsp was fixed in step. The mobile core (vta-mobile-core) moved to Rev 3 sealing in the same window.

Relationships: Admission, and Why the Ecosystem Paid for Them

The §7.2.2 relationship cost a visible run of fixes, and VTI wrote a decision note (tsp-vs-didcomm-trust-task-surface.md) to ask honestly whether it was worth it. Its framing is the clearest explanation of what a TSP relationship is: it is not authentication (the envelope proves the sender, as DIDComm’s does, and a Trust Task carries its own signed proof) and not authorization (that is the ACL). It is admission — “will I accept traffic from this VID at all” — and it is silent by design, because an unrelated VID must not be able to learn whether a target VID is live.

The honest cost ledger: a handshake before the first Trust Task; symmetric silent drops that show up only as timeouts (from the client’s side an unresponsive VTA and a VTA that forgot the relationship are indistinguishable); and the recovery machinery that implies. tsp-relationship-recovery.md designs that machinery as D1–D9: a durable store (D1), an idempotent re-establish transition (D2 — a naive re-invite deadlocks the state machine in both directions), recovery-aware send readiness (D3), bounded single-flight jittered recovery (D4), 7-day idle eviction (D5), a coordinator with startup enumeration (D6, D9), an inbound-invite rate limiter (D7) and drop metrics (D8). D1–D6 and D8 landed by 2026-09-18; D7 and D9 hardening remain.

The decision, recorded as decided: multi-hop TSP — Trust Tasks routed where intermediaries never learn the final recipient — is a requirement, so the relationship layer is “the price of a benefit we will use” and the recovery work is “justified investment, not speculative rent”. The nuance the note is careful about: on today’s reference topology (PNM and VTA sharing one mediator) the privacy benefit is marginal, because the delivery mediator must know the recipient anyway; the benefit materializes only with intermediaries before the recipient’s mediator. The first exercise of that benefit landed the day after the RC: cross-mediator sends are nested so the sender’s own mediator never learns the recipient (#1559).

Cross-Implementation Conformance

TSP now has five implementations in this ecosystem’s orbit, and a new repository — github.com/OpenVTC/tsp-conformance (created 2026-09-16) — runs them against each other: one runner, a driver per implementation, and CI. The implementations are affinidi-tsp 0.2.0 (Rust), the ToIP reference tsp_sdk 0.11.0, @openvtc/vti-tsp-js 0.3.0, and two new ports, affinidi-tsp-go 0.1.0 and affinidi-tsp-dart 0.1.0. The suite asks three questions — do the specification’s Appendix A vectors open (and re-pack byte for byte where the vector publishes its ephemeral material), do all pairs interoperate across roughly 35 message shapes, and do they refuse what they should and run the relationship protocol correctly. The 2026-09-16 run:

SuitePassFail
Spec vectors1520
Pairwise interop8930
Relationship protocol2500
Negative cases3778

All eight negative failures are the reference tsp_sdk accepting a message it should refuse (trailing bytes, a non-zero CESR lead byte, a signed-only message whose ESSR sender does not match the envelope); none is in affinidi-tsp. The capability matrix shows vti-tsp-js does not yet implement signed-only messages, the legacy sealed box, or the post-quantum HPKE path. The TDK’s own docs/tsp/interop.md now points at this suite as “the authoritative result” and keeps its older single-graph round-trip harness against tsp_sdk 0.10.0 only as a quick local check — with the standing instruction to re-run the suite on every reference release. One caveat travels with every implementation: the post-quantum CESR code point 1AAQ is provisional (CESR issue #14), which is the main reason pq stays off by default.

Relationship to DIDComm, After the Bridge

Under Rev 2 the mediator could bridge: re-pack a TSP message as DIDComm and back, so a TSP-only sender could reach a DIDComm-only recipient. Rev 3’s ESSR makes that “by construction a point where the message is decrypted and re-signed by someone who is not the sender”, so the bridge was removed and both ends of a conversation must be on one protocol. What interop looks like now:

  • Two carriers on one mediator. The TDK mediator still serves TSP and DIDComm on the same endpoint and the same per-DID WebSocket, sniffing which is which at ingress. Federation across independent mediators works for both (docs/multi-mediator.md): a TSP routed hop is sealed to the next mediator and signed by the previous one, so it is “rewrap-like by construction” and needs no relay-mode choice, and the peer-mediator allowlist applies to it with a stronger guarantee than DIDComm because every hop is authenticated. See didcomm for the DIDComm side of federation.
  • The choice is per peer, from the DID document. A sender picks TSP when the peer advertises a TSPTransport service (matched by type, never by #id fragment) and the sender can carry it; otherwise DIDComm; otherwise REST. VTI’s one outbound seam (#1474) and the fix in #1560 — select by the sender’s live capability, not the build feature — are that rule made concrete. Advertisement is not availability: the browser wallet reports the transport that actually carried a byte.
  • TSP-only is a supported shape. A VTA can run with no DIDComm compiled in; since mediator 0.22.3 the mediator answers its own management Trust Tasks over TSP (#783), so a TSP-only client can set its own ACL; a DIDComm/TSP-only VTA can be backed up over chunked Trust Tasks.
  • What stays DIDComm. Interop with the wider Aries/Credo world (the mediator speaks DIDComm v1 for that), the did:webvh witness, and any peer that has not advertised TSP. On the same socket, TSP carries Trust Tasks while DIDComm carries protocol messages — a split VTI’s decision note calls “load-bearing and understood”, and one it refuses to fragment further with per-surface carve-outs. The stated long-term goal is still to deprecate DIDComm.

How We Got Here — Rev 2, June–August 2026

TSP’s promotion happened in a coordinated burst across three repos in June–July 2026, against Rev 2 (the November 2025 Experimental Implementer’s Draft):

  • The TDK did the heavy lifting (~60 commits, affinidi-tsp 0.1.1 → 0.1.12): a DID-document VID resolver, Routed/Nested modes, ingress sniffing, the mediator’s own TSP identity, routed relay and remote forwarding, the (since-removed) TSP↔DIDComm bridge, TSPTransport advertised in the mediator’s DID document and baked into generated DIDs, CESR/HPKE compliance and full tsp_sdk wire parity, and graduation from experimental to supported with TSP-preferred selection and DIDComm fallback in send_to.
  • VTI first deferred, then reversed — a 2026-06-22 decision record deferred TSP because the mediator could not route it; three days later the TDK work landed and an SDD (tsp-enablement.md) flipped guidance to TSP > DIDComm > REST. TSP shipped as a first-class managed service (pnm services, TSPTransport in DID templates, a TSP health probe) over the same mediator socket as DIDComm. By August it was a selectable transport (TransportChoice::{Tsp, Didcomm, Auto}), a VTA could run TSP-only (#937), the setup wizard offered it, and communities chose and published their transports at setup.
  • did-hosting-service added TSP as a third transport binding in one PR, because every wire operation there is already a transport-agnostic Trust Task.
  • OpenVTC ran the join ceremony over TSP when the community offered it, choosing TSP only when its own mediator could carry it; the browser wallet shipped the pure-TypeScript vti-tsp-js 0.2.0.

Design decisions locked then and still true: DIDs double as TSP VIDs reusing the existing Ed25519/X25519 keys (no new key material — which is also why the VID model is not yet post-quantum); capability discovery is DID-document-driven; a VTA’s #tsp service endpoint is its mediator’s DID, so TSP needs a mediator but does not need DIDComm; and the ecosystem uses upstream routed/nested sends rather than building onion routing itself.

As of Eucalyptus — On by Default

The three weeks between the release candidate and the VTI-Eucalyptus tag (2026-10-07) turned TSP from preferred into default, and made it the carrier of the stack’s new administrative surfaces:

  • On by default. tsp became a default feature of vta-service and vta-enclave (VTI #1622, 2026-09-21): setup pre-ticks TSP and checks that the chosen mediator advertises a TSPTransport service, dropping TSP from the wizard and refusing a --from file if it does not; a newly minted DID advertises TSP when its mediator carries it (#1665). The same day the TDK mediator added tsp to its default features (affinidi-tdk-rs #854), so a self-hosted did:web mediator advertises TSPTransport and mediator-setup mints TSP by default — a request that came in from the Keyring wallet team. Existing VTAs and hand-written configurations that never mention TSP stay as they were.
  • TSP carries the management planes. The mediator’s operator functions became Trust Tasks answered over TSP as well as DIDComm (#850–#853: stats, queue, message list/get/delete, purge; later config patch and reload and a traffic monitor over TSP), and the legacy DIDComm admin protocols and REST admin routes were removed in mediator 0.31.0 (#901). The VTC’s cnm git … and git-namespace reads go over TSP, DIDComm or HTTPS (VTI #1778, #1841); vgi-bridge speaks TSP to its VTC when the VTC advertises it first (VGI #115) and verify-trust reaches the trust registry by TSP, then DIDComm, then HTTPS (VGI #88); the registry itself answers signed registry/* Trust Tasks over TSP under Rev 3 relationships (affinidi-trust-registry-rs 0.18.0 → 0.23.0). Member pushes from a VTC escalate TSP → DIDComm → REST (VTI #1754), and a member who joined over TSP is spoken to over TSP afterwards (openvtc #413).
  • Relationships across mediators. A TSP relationship can now be started with a peer on another mediator and replies reach a sender on another mediator (VTI #1873, #1894 — the first exercise of the routed path beyond one mediator); pnm can list, reset and delete relationships offline (#1812); an idle relationship is re-invited (#1816); a rate-limited reply is retried with backoff (#1906). OpenVTC fixed the bug the Keyring team filed as VTI-61 — invites and replies were kept in two relationship stores, so a formed relationship could look unformed to the sending side — by giving them one (openvtc #415), and records the peer’s mediator before the Rev 3 invite so it is routed, not sent direct (#405).
  • Libraries. affinidi-tsp 0.2.2 (#912, 2026-10-01) is a dependency-only bump; the Rev 3 fixes of this window (#838, #883, #884, #887, #905, #913, #924) are all in 0.2.x. The TDK’s docs/tsp/interop.md names the Go, Dart and JavaScript implementations as external repositories exercised by OpenVTC/tsp-conformance; neither affinidi-tsp-go nor affinidi-tsp-dart is cloned here, and the Dart Trust Tasks binding in dtgwg-trust-tasks-tf depends on the Dart port with Ed25519 signing only.
  • Keyring. The Keyring phone wallet packs TSP Rev 3 direct mode with its own TypeScript stack, framed on the CESR table of the vendored @openvtc/vti-tsp-js 0.3.0 and cross-checked against the specification’s Appendix A vectors; @bifold/credo-tsp-adapter supplies Askar-backed signing, key agreement and VID resolution. Which carriage the peer leg uses is a build-time choice (VTI_PEER_LEG=tsp, default DIDComm), and “TSP over DIDComm v2” — a TSP envelope deliberately double-wrapped inside a DIDComm message — sits behind a developer setting.

Open Items

The VID model does not carry post-quantum keys, so nothing above affinidi-tsp can use the pq path yet; D7/D9 relationship hardening; the browser wallet’s Rev 2 decode-only codec awaits deletion; vti-tsp-js lacks the sealed box and PQ (its README states nothing either way on signed-only messages); the version-field reading is raised upstream; and the multi-hop nested path is exercised on cross-mediator sends but not yet driven from DID-document route discovery.

See also: didcomm, post-quantum-cryptography, affinidi-tdk, verifiable-trust-infrastructure, verifiable-trust-agent, coordinated-releases