feat(did): advertise both receive transports on the holder DID - #135
Conversation
Transport negotiation runs in one direction only. The wallet reads a VTA's published services and picks TSP > DIDComm > REST, degrading on an explicit `unsupported`. An executor pushing *to* the wallet has nothing to read: the holder's `did:peer:2` carried exactly one service, DIDComm-typed by convention, so there is no signal that this wallet accepts TSP inbound and no basis on which to prefer it. That gap is what makes a TSP push unsafe rather than merely unimplemented. Hop acceptance is not delivery: a TSP frame pushed to a wallet that cannot route it is accepted and stored by the mediator and then never handled — for a `task-consent` request, a gated action that never got its human check (R7.2). A deployment flag cannot close that, because the thing it would be asserting is a per-peer property. So the holder now publishes what it can receive: a `DIDCommMessaging` service and a `TSPTransport` service, both pointing at its mediator — one deployment, demultiplexed on the TSP magic byte, advertised under two types because they are two things a sender must choose between. `createDidPeer2` takes `services` rather than `service`, emitting one `.S` element each. Both forms (multiple elements, or one element carrying an array) are spec-legal; the multiple-element form is what every resolver in this ecosystem indexes and numbers, and there is nothing to gain from the other. **The TSP type is spelled out, and that is not cosmetic.** The abbreviation table is not shared: `affinidi-did-common`'s peer resolver expands `"tsp"` to `TSPTransport`, while `vti-didcomm-js`'s expands only `"dm"` and passes everything else through verbatim. Publishing `"tsp"` would therefore resolve to two different service types depending on which side read the DID — the VTA seeing a TSP service where the wallet saw none. `"TSPTransport"` is preserved verbatim by both. A test pins the divergence so that if the JS resolver ever learns the abbreviation, it fails and says so rather than leaving a stale comment behind. `accept` is emitted only for a DIDComm service. `didcomm/v2` is a DIDComm media type, and asserting it on a TSP endpoint would advertise something untrue about what that endpoint speaks. **This changes the DID of a freshly-minted holder**, because did:peer:2 encodes services into the identifier. It does not migrate anything: `ensureHolder` returns a persisted holder unchanged, so an existing wallet keeps its DID, its ACL row, and its DIDComm inbox, and simply does not advertise TSP until it is re-onboarded. The capability arrives with new holders and by choice for old ones, rather than as a forced re-enrolment. Nothing reads the new service yet. The VTA-side negotiated push is the next change, and this is the half that has to land first — a peer cannot negotiate against a capability that has not been published. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
) #135 added a `TSPTransport` entry to the holder's `did:peer:2`, to give an executor a capability signal to negotiate a push against. It does nothing, for two independent reasons: 1. **Nothing calls the function it was added to.** Onboarding adopts a VTA-minted holder via `installVtaMintedHolder` (provision-integration, M2C); `ensureHolder`'s self-minting path is the earlier design and is currently unreached. 2. **The adopted holder is a `did:key`**, and that method has no service endpoints at all — its document is derived from the key material alone. So no capability can be published in a holder's DID document while the holder is a did:key, whichever code mints it. The second reason is the one that matters: it rules out the whole approach, not just this call site. Publishing capabilities in the DID document was chosen over announcing them at enrolment on the strength of symmetry — both sides resolving and matching published services — and that symmetry is not available to a did:key holder. Reverted here, with both reasons written down at the site so the next attempt does not rediscover them. `createDidPeer2`'s multi-service support stays: it is the correct shape for a peer DID, it is exercised by tests, and it is what the earlier path would need if a peer-DID holder ever returns. Its doc now says plainly that nothing publishes a second service today, so a reader cannot infer from its existence that holder capabilities are discoverable. The signal a negotiated push needs has to live somewhere a did:key can carry it — announced at enrolment and held against the ACL/device record. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
🛡️ AI Agentic Security Code Review1 AI-confirmed issue. Mandatory to check: 🔒 Security Code Review Report Details🛡️ Security Code Review Report — PR #135
🗺️ Scan CoverageModules scanned: 1 · with findings: 1 · files: 3 · findings: 1
Executive Summary
🔒 Security IssuesConfirmed Vulnerabilities (1)🟡 Ambiguous service type abbreviation causes cross-resolver interpretation mismatch (parser differential)
📝 Description: The 🌱 Root Cause: No validation/whitelist is applied to the caller-supplied 🔎 Evidence: 🎯 Attack Scenario: An application constructs a DidPeerService with
Generated by Agentic Sec — AI Security Validation Agent Details🛡️ Threat Model & Affect Analysis — PR #135
📋 Affect AnalysisChange SummaryRefactors did:peer:2 identity construction from a single optional 'service' field to an ordered 'services' array, and modifies generateOrLoadHolderIdentity to publish both a DIDComm and an explicit 'TSPTransport' service (both pointed at the same mediator) so executors can negotiate TSP delivery to the wallet, closing a one-directional capability-negotiation gap. Adds a new interop-focused test suite validating encoding against an external resolver package. Diff: +118 / -14 lines 🧩 Affected Components
📁 File Classificationspackages/core/src/did/peer.ts
packages/core/src/store/holder-identity.ts
packages/core/tests/did.peer-services.mjs
🛡️ STRIDE Threat ModelIdentified Threats (10)⚪ STRIDE-1: Cross-Resolver Type Confusion via Unspelled Abbreviation in createDidPeer2
Description: serviceEndpoint type field in createDidPeer2 in packages/core/src/did/peer.ts allows cross-resolver type confusion due to non-portable single-character abbreviation tables between did:peer:2 resolvers (affinidi-did-common vs vti-didcomm-js), resulting in a TSP transport service being silently misinterpreted as an unresolved/opaque type on one side, causing routing/consent-negotiation divergence. Evidence: packages/core/src/did/peer.ts:~26-31, ~95-108 Attack Scenario:
🔎 Threat Clue: Derived from COMP-001, COMP-002 via EP-001, EP-002
Preconditions: Attacker or misconfigured integrator controls or influences the Existing Controls: JSDoc comment instructing consumers to 'Spell a non-DIDComm type out in full.' • did.peer-services.mjs contains a canary test ( Recommended Mitigations: Enforce at the type-system/runtime-validation level in createDidPeer2 that ⚪ STRIDE-2: Silent Consent-Bypass via Unrouted TSP Push in generateOrLoadHolderIdentity
Description: mediatorDid option in generateOrLoadHolderIdentity in packages/core/src/store/holder-identity.ts allows advertisement of a TSP transport the wallet may not actually be able to route/handle due to publishing capability claims independent of runtime handler wiring, resulting in gated consent actions (R7.2) never reaching a human reviewer. Evidence: packages/core/src/store/holder-identity.ts:179-201 Attack Scenario:
🔎 Threat Clue: Derived from COMP-002 via EP-002
Preconditions: opts.mediatorDid is set when calling generateOrLoadHolderIdentity, publishing the TSPTransport capability claim., The wallet's actual runtime TSP handler is not implemented, disabled, or misconfigured — a state entirely decoupled from the DID document construction code., No liveness/capability probe exists to verify advertised services are actually backed by working handlers before relying on them for gated actions. Existing Controls: Code comment explicitly documents the risk: 'hop acceptance is not delivery... which for a consent request is a gated action that never got its human check (R7.2).' • Publishing both services together (rather than TSP alone) at least preserves the DIDComm fallback path as an option for executors that choose to use it. Recommended Mitigations: Gate publication of the TSPTransport service entry on an explicit, verified runtime capability flag rather than solely on ⚪ STRIDE-3: Unvalidated serviceEndpoint Injection in DidPeerService Construction
Description: serviceEndpoint field in createDidPeer2 in packages/core/src/did/peer.ts allows injection of arbitrary/malformed URI or DID strings due to absence of format validation before base64url-encoding into the DID string, resulting in propagation of malicious or malformed endpoints to any resolver/executor that trusts the DID document without independent validation. Evidence: packages/core/src/did/peer.ts:~99-108 Attack Scenario:
🔎 Threat Clue: Derived from COMP-001, COMP-002 via EP-001, EP-002
Preconditions: Caller-supplied or configuration-sourced Existing Controls: did:peer:2's Recommended Mitigations: Add basic format validation (DID syntax or explicit URI scheme allow-list) for ⚪ STRIDE-4: Unbounded Services Array Causing DID String Bloat / Resolver DoS
Description: services array parameter in createDidPeer2 in packages/core/src/did/peer.ts allows unbounded service list construction due to absence of a length/size limit on Evidence: packages/core/src/did/peer.ts:~91-109 Attack Scenario:
🔎 Threat Clue: Derived from COMP-001 via EP-001
Preconditions: Untrusted or attacker-influenced input can reach the Existing Controls: Current internal call site in holder-identity.ts only ever constructs a fixed 2-element services array, limiting exposure from that specific call path. Recommended Mitigations: Add an explicit maximum length check on ⚪ STRIDE-5: Ambiguous accept-Field Omission Enabling DIDComm Media-Type Misrepresentation
Description: accept field defaulting logic in createDidPeer2 in packages/core/src/did/peer.ts allows a caller to force-set Evidence: packages/core/src/did/peer.ts:~97-104 Attack Scenario:
🔎 Threat Clue: Derived from COMP-001 via EP-001
Preconditions: Caller of createDidPeer2 supplies an inconsistent Existing Controls: Default behavior (when Recommended Mitigations: Validate that ⚪ STRIDE-6: Service Ordering Dependency Causing Index-Based Identifier Confusion
Description: services array ordering in createDidPeer2 in packages/core/src/did/peer.ts allows service-id/index misassignment due to Evidence: packages/core/src/did/peer.ts:~91-109 Attack Scenario:
🔎 Threat Clue: Derived from COMP-001, COMP-002 via EP-001, EP-002
Preconditions: A consumer of the resolved DID document caches or persists service references by positional id rather than by type., The order of entries in the Existing Controls: Current call site in holder-identity.ts uses a fixed, hardcoded order (DIDComm first, TSPTransport second), limiting real-world exposure to future refactors rather than present-day exploitation. Recommended Mitigations: Document explicitly that consumers must resolve services by ⚪ STRIDE-7: Supply-Chain Trust in External did:peer:2 Resolver Packages
Description: @openvtc/vti-didcomm-js and @noble/curves dependencies in did.peer-services.mjs and peer.ts allow supply-chain compromise due to the security-critical DID resolution and cryptographic key-derivation logic being delegated to external, independently-versioned packages outside this repository's control, resulting in potential silent tampering with DID resolution semantics or key material if either dependency is compromised or subtly altered upstream. Evidence: packages/core/tests/did.peer-services.mjs:9-11 Attack Scenario:
🔎 Threat Clue: Derived from COMP-001 via EP-001
Preconditions: Dependency version pinning/lockfile integrity (e.g. package-lock.json, checksums) is not strictly enforced or audited for these two packages., No subresource integrity or reproducible-build verification exists for the resolved DID logic or cryptographic primitives at build/publish time. Existing Controls: Test suite (did.peer-services.mjs) pins behavioral expectations against the real external resolver rather than a local mock, which would at least cause CI failures if resolver behavior changes unexpectedly (detective control, not preventive). • Use of @noble/curves, a well-audited, widely-used cryptography library, reduces likelihood relative to using a bespoke crypto implementation. Recommended Mitigations: Pin exact versions and verify integrity hashes (npm lockfile + ⚪ STRIDE-8: Missing Type Field Presence Check Enabling Deceptive Default-Type Spoofing
Description: type field defaulting to "dm" in createDidPeer2 in packages/core/src/did/peer.ts allows a caller to implicitly spoof a service as DIDComm-capable due to the fallback Evidence: packages/core/src/did/peer.ts:92-93 Attack Scenario:
🔎 Threat Clue: Derived from COMP-001 via EP-001
Preconditions: Caller supplies a serviceEndpoint without setting Existing Controls: DIDComm protocol itself typically involves a handshake/negotiation step at the application layer that could reveal an incompatible endpoint, providing some out-of-band mitigation not visible in this code. Recommended Mitigations: Document that ⚪ STRIDE-9: Absence of Non-Repudiation Logging for Service-List Construction Decisions
Description: createDidPeer2 and generateOrLoadHolderIdentity in packages/core/src/did/peer.ts and packages/core/src/store/holder-identity.ts allow repudiation of which services were advertised at DID-generation time due to the absence of any audit log or persisted record of the exact Evidence: packages/core/src/store/holder-identity.ts:179-201 Attack Scenario:
🔎 Threat Clue: Derived from COMP-002 via EP-002
Preconditions: An investigation is undertaken after a suspected consent-delivery failure or dispute., No external logging infrastructure independently captures identity-generation events. Existing Controls: The DID string itself is self-describing and could be decoded post-hoc to recover the services configuration, partially mitigating pure non-repudiation concerns if the DID is retained. Recommended Mitigations: Emit a structured audit log entry (without secret material) when generateOrLoadHolderIdentity constructs a new identity, recording the services array and mediatorDid used. • Retain historical DID generations (not just the current active one) to support forensic reconstruction of capability claims over time. ⚪ STRIDE-10: Prompt-Injection-Style Instructional Comments Embedded in Reviewed Diff Content
Description: [REMOVED] comment markers and embedded rationale text in the provided diff/source for packages/core/src/did/peer.ts allow injection of instruction-like natural-language content into automated review/analysis pipelines due to the diff format including free-text developer commentary indistinguishable from tool directives, resulting in a theoretical risk that an automated code-review or LLM-based tool could misinterpret embedded prose as operator instructions rather than analyzed data. Evidence: packages/core/src/did/peer.ts:multiple Attack Scenario:
🔎 Threat Clue: Derived from N/A via N/A
Preconditions: A future PR author (malicious insider or compromised contributor account) embeds instruction-like text in code comments., The automated review/analysis tool does not enforce strict data/instruction separation for ingested diff content. Existing Controls: This analysis pipeline explicitly treats all diff/source content as untrusted data under a stated security directive, mitigating this class of risk at the tooling level. • No directive-like phrasing was actually present in this specific diff, so no exploitation occurred here. Recommended Mitigations: Continue enforcing strict data/instruction separation in all automated code-review and LLM-based analysis tooling. • Add a lightweight static check flagging unusually long or imperative-sounding comment blocks in PRs for human review. 🍝 PASTA Threat ModelApplication PurposeA browser extension wallet (VTA) that constructs did:peer:2 identities advertising multiple transport-negotiable services (DIDComm, TSP) so executors can route gated consent-approval workflows to the correct wallet-side mediator handler. Inherent Risks
ObjectivesRisk: Treat any transport advertised without a verified runtime handler as a high-priority design risk requiring compensating controls.; Treat cross-resolver semantic drift as a supply-chain-adjacent risk requiring interop testing rather than documentation alone. Business Impact Analysis (3)BIA-1: Consent-Gated Action Delivery (Critical)The end-to-end process by which an executor's request for a gated action reaches a human wallet-holder for explicit approval before execution. MTD: 00 days 04:00 hours | RTO: 00 days 01:00 hours | RPO: 00 days 00:00 hours
BIA-2: DID Identity Generation and Service Advertisement (High)The process by which a wallet generates or loads its did:peer:2 identity and advertises the transports/services it can receive messages on. MTD: 01 days 00:00 hours | RTO: 00 days 08:00 hours | RPO: 00 days 00:00 hours
BIA-3: Cross-Ecosystem Resolver Interoperability (Medium)The shared, implicit agreement across independently-maintained resolver packages (affinidi-did-common, vti-didcomm-js) on how did:peer:2 service abbreviations are expanded. MTD: 07 days 00:00 hours | RTO: 03 days 00:00 hours | RPO: N/A
Technical ScopeRoles (3): RO-1 Wallet Holder · RO-2 Executor Operator · RO-3 Ecosystem Maintainer Actors (3): AC-1 Wallet Application · AC-2 Executor Service · AC-3 Mediator Service Entry Points (2): EP-1 createDidPeer2 Invocation · EP-2 generateOrLoadHolderIdentity Invocation Threat Actors (3): TA-1 Malicious Executor Operator · TA-2 Compromised npm Publisher · TA-3 Misconfigured Integrator Infrastructure (2): IF-1 Browser Extension Runtime · IF-2 Mediator Hosting Trust Boundaries (4): TB-1 Wallet Local Runtime · TB-2 Mediator Infrastructure · TB-3 External Executor / VTA · TB-4 External Package Registry External Entities (2): EE-1 Executor / VTA System · EE-2 npm Package Registry System Components (5): SC-1 createDidPeer2 Function · SC-2 generateOrLoadHolderIdentity Function · SC-3 Mediator Service · SC-4 External DID Resolvers · SC-5 Holder Secret Store Resources And Assets (3): RA-1 Ed25519/X25519 Key Pair · RA-2 did:peer:2 Identifier String · RA-3 Mediator-Routed Consent Message Technologies And Dependencies (3): TD-1 @openvtc/vti-didcomm-js · TD-2 @noble/curves · TD-3 affinidi-did-common Use Cases (2)
📋 Risk Registry (5)
⚔️ Attack Scenarios (3)SC-2: generateOrLoadHolderIdentity Function---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. Threat Actors"]
direction LR
TA1@{ shape: rect, label: "TA-1: Malicious Executor Operator<br><i>Exploit consent-flow ambiguity</i>" }
TA3@{ shape: rect, label: "TA-3: Misconfigured Integrator<br><i>Unintentional unsafe config</i>" }
end
subgraph SL2["2. Threats"]
direction LR
S2@{ shape: rect, label: "STRIDE-2: Silent Consent-Bypass via Unrouted TSP Push<br><i>High / Likely</i>" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAP664@{ shape: rect, label: "CAPEC-664: Server Side Request Forgery" }
CAP593@{ shape: rect, label: "CAPEC-593: Session Hijacking" }
end
subgraph SL4["4. Weaknesses"]
direction LR
CWE345@{ shape: rect, label: "CWE-345: Insufficient Verification of Data Authenticity" }
CWE354@{ shape: rect, label: "CWE-354: Improper Validation of Integrity Check Value" }
CWE693@{ shape: rect, label: "CWE-693: Protection Mechanism Failure" }
end
subgraph SL5["5. System Component"]
direction LR
SC2@{ shape: rect, label: "SC-2: generateOrLoadHolderIdentity Function" }
end
SC2 --> CWE345
SC2 --> CWE693
CWE345 --> CAP664
CWE693 --> CAP593
CAP664 --> S2
CAP593 --> S2
S2 --> TA1
S2 --> TA3
linkStyle 0 stroke:#FF0000,stroke-width:2px
linkStyle 1 stroke:#FF0000,stroke-width:2px
linkStyle 2 stroke:#FF0000,stroke-width:2px
linkStyle 3 stroke:#FF0000,stroke-width:2px
linkStyle 4 stroke:#FF0000,stroke-width:2px
linkStyle 5 stroke:#FF0000,stroke-width:2px
linkStyle 6 stroke:#FF0000,stroke-width:2px
linkStyle 7 stroke:#FF0000,stroke-width:2px
SC-1: createDidPeer2 Function---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. Threat Actors"]
direction LR
TA3@{ shape: rect, label: "TA-3: Misconfigured Integrator<br><i>Unintentional unsafe config</i>" }
TA1@{ shape: rect, label: "TA-1: Malicious Executor Operator<br><i>Exploit consent-flow ambiguity</i>" }
end
subgraph SL2["2. Threats"]
direction LR
S1@{ shape: rect, label: "STRIDE-1: Cross-Resolver Type Confusion<br><i>High / Likely</i>" }
S3@{ shape: rect, label: "STRIDE-3: Unvalidated serviceEndpoint Injection<br><i>Medium / Possible</i>" }
S4@{ shape: rect, label: "STRIDE-4: Unbounded Services Array<br><i>Low / Possible</i>" }
S5@{ shape: rect, label: "STRIDE-5: Ambiguous accept-Field Omission<br><i>Low / Unlikely</i>" }
S8@{ shape: rect, label: "STRIDE-8: Deceptive Default-Type Spoofing<br><i>Low / Possible</i>" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAP153@{ shape: rect, label: "CAPEC-153: Input Data Manipulation" }
CAP148@{ shape: rect, label: "CAPEC-148: Content Spoofing" }
CAP130@{ shape: rect, label: "CAPEC-130: Excessive Allocation" }
CAP693@{ shape: rect, label: "CAPEC-693: Protocol Manipulation" }
end
subgraph SL4["4. Weaknesses"]
direction LR
CWE436@{ shape: rect, label: "CWE-436: Interpretation Conflict" }
CWE20@{ shape: rect, label: "CWE-20: Improper Input Validation" }
CWE400@{ shape: rect, label: "CWE-400: Uncontrolled Resource Consumption" }
CWE843@{ shape: rect, label: "CWE-843: Type Confusion" }
CWE1188@{ shape: rect, label: "CWE-1188: Insecure Default Initialization" }
end
subgraph SL5["5. System Component"]
direction LR
SC1@{ shape: rect, label: "SC-1: createDidPeer2 Function" }
end
SC1 --> CWE436
SC1 --> CWE20
SC1 --> CWE400
SC1 --> CWE843
SC1 --> CWE1188
CWE436 --> CAP153
CWE20 --> CAP148
CWE400 --> CAP130
CWE843 --> CAP693
CWE1188 --> CAP148
CAP153 --> S1
CAP148 --> S3
CAP130 --> S4
CAP693 --> S5
CAP148 --> S8
S1 --> TA1
S3 --> TA3
S4 --> TA3
S5 --> TA3
S8 --> TA3
linkStyle 0 stroke:#FF0000,stroke-width:2px
linkStyle 1 stroke:#FF0000,stroke-width:2px
linkStyle 2 stroke:#00FF00,stroke-width:2px
linkStyle 3 stroke:#00FF00,stroke-width:2px
linkStyle 4 stroke:#00FF00,stroke-width:2px
linkStyle 5 stroke:#FF0000,stroke-width:2px
linkStyle 6 stroke:#FFA500,stroke-width:2px
linkStyle 7 stroke:#00FF00,stroke-width:2px
linkStyle 8 stroke:#00FF00,stroke-width:2px
linkStyle 9 stroke:#00FF00,stroke-width:2px
linkStyle 10 stroke:#FF0000,stroke-width:2px
linkStyle 11 stroke:#FFA500,stroke-width:2px
linkStyle 12 stroke:#00FF00,stroke-width:2px
linkStyle 13 stroke:#00FF00,stroke-width:2px
linkStyle 14 stroke:#00FF00,stroke-width:2px
linkStyle 15 stroke:#FF0000,stroke-width:2px
linkStyle 16 stroke:#FFA500,stroke-width:2px
linkStyle 17 stroke:#00FF00,stroke-width:2px
linkStyle 18 stroke:#00FF00,stroke-width:2px
linkStyle 19 stroke:#00FF00,stroke-width:2px
SC-4: External DID Resolvers---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. Threat Actors"]
direction LR
TA2@{ shape: rect, label: "TA-2: Compromised npm Publisher<br><i>Inject malicious resolver/crypto behavior</i>" }
end
subgraph SL2["2. Threats"]
direction LR
S7@{ shape: rect, label: "STRIDE-7: Supply-Chain Trust in External Resolvers<br><i>Medium / Possible</i>" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAP437@{ shape: rect, label: "CAPEC-437: Supply Chain" }
end
subgraph SL4["4. Weaknesses"]
direction LR
CWE1104@{ shape: rect, label: "CWE-1104: Use of Unmaintained Third Party Components" }
CWE829@{ shape: rect, label: "CWE-829: Inclusion of Untrusted Functionality" }
end
subgraph SL5["5. System Component"]
direction LR
SC4@{ shape: rect, label: "SC-4: External DID Resolvers" }
end
SC4 --> CWE1104
SC4 --> CWE829
CWE1104 --> CAP437
CWE829 --> CAP437
CAP437 --> S7
S7 --> TA2
linkStyle 0 stroke:#FFA500,stroke-width:2px
linkStyle 1 stroke:#FFA500,stroke-width:2px
linkStyle 2 stroke:#FFA500,stroke-width:2px
linkStyle 3 stroke:#FFA500,stroke-width:2px
linkStyle 4 stroke:#FFA500,stroke-width:2px
linkStyle 5 stroke:#FFA500,stroke-width:2px
📊 Risk SummaryTotal Threats: 10 By Severity: Low: 4 · High: 2 · Medium: 2 · Informational: 2 By Category: Unknown: 10 🎯 Attack SurfaceKill Chain 1: A misconfigured or malicious integrator passes an abbreviated, non-portable service type such as 🛡️ Risk Mitigation StrategyPriority 1 (Immediate): Close the structural gap between transport-hop acceptance and application-level delivery for gated consent actions (RISK-001) by requiring that any advertised TSPTransport service in generateOrLoadHolderIdentity be gated on a verified, currently-registered runtime handler, and by introducing delivery/read-receipt signaling so an executor can detect non-delivery rather than assuming success from mediator-level acceptance alone; this directly restores the R7.2 human-in-the-loop guarantee that the current design silently undermines. Priority 2 (Short-Term): Eliminate cross-resolver semantic drift (RISK-002) by replacing documentation-only conventions with enforceable runtime validation in createDidPeer2 — rejecting or normalizing ambiguous short-form Generated by Agentic Sec — Threat Model & Affect Analysis Agent 📊 Summary & findings
Confirmed (1)
|
Negotiation only runs one way
The wallet reads a VTA's published services and picks TSP > DIDComm > REST, degrading only on an explicit
unsupported. That is real capability negotiation.An executor pushing to the wallet has nothing to read. The holder's
did:peer:2carried exactly one service:DIDComm-typed by did:peer:2 convention. No signal that this wallet accepts TSP inbound, and no basis on which to prefer it.
Why that blocks the VTA-side push rather than just complicating it
Hop acceptance is not delivery. A TSP frame pushed to a wallet that cannot route TSP inbound is accepted and stored by the mediator, and then never handled. For a
task-consentrequest that is a gated action that never got its human check — R7.2, and precisely the failure class the last three PRs have been closing.A deployment flag cannot substitute, because what it would be asserting ("every wallet talking to this agent handles TSP inbound") is a per-peer property. That is what sent me back here instead of building the flagged push.
What this publishes
A
DIDCommMessagingservice and aTSPTransportservice, both pointing at the holder's mediator — one deployment, demultiplexed on the TSP magic byte, advertised under two types because they are two things a sender must choose between.createDidPeer2now takesservicesrather thanservice, emitting one.Selement each. Both the multiple-element form and the single-element-carrying-an-array form are spec-legal; the multiple-element form is what every resolver here indexes and numbers, and there is nothing to gain from the other.The TSP type is spelled out, and that is not cosmetic
The abbreviation tables are not shared:
"tsp"resolves toaffinidi-did-common(Rust, VTA side)TSPTransportvti-didcomm-js(JS, wallet + relay)"tsp"— passed through verbatimPublishing
"tsp"would resolve to two different service types depending on which side read the DID — the VTA seeing a TSP service where the wallet saw none."TSPTransport"is preserved verbatim by both, so it means the same thing everywhere today with no library change.There is a test that pins the divergence deliberately: if the JS resolver ever learns the abbreviation, it fails and tells you the comment above has stopped being true. Worth a small follow-up in
vti-didcomm-jsto addtspto its map, after which the compact form becomes safe.acceptis emitted only for a DIDComm service.didcomm/v2is a DIDComm media type; asserting it on a TSP endpoint would advertise something untrue about what that endpoint speaks.This changes the holder DID — and deliberately does not migrate
did:peer:2 encodes services into the identifier, so a freshly-minted holder gets a different DID.
Nothing migrates.
ensureHolderreturns a persisted holder unchanged, so an existing wallet keeps its DID, its ACL row and its DIDComm inbox, and simply does not advertise TSP until it is re-onboarded. The capability arrives with new holders, and by choice for old ones, rather than as a forced re-enrolment.Tests
did.peer-services.mjs(4, new), asserted against the resolver that actually reads these DIDs rather than a decoder of my own:#service,#service-1);"tsp"abbreviation divergence, pinned as described above;.Selement;accepton the DIDComm entry, absent on the TSP one.npm run lint/build/testgreen — 608 tests.dist/background.jsstill a single bundle with no dynamicimport().One thing worth noting for review:
createDidPeer2's field rename fromservicetoservicesdid not produce a compile error at the call site, because the spread...(cond ? { service: … } : {})defeats excess-property checking. It compiled while silently advertising nothing. Caught by reading rather than by the type-checker, which is worth knowing about that pattern.Next
Nothing reads the new service yet. The VTA-side negotiated push comes next: resolve the recipient's DID document, prefer
TSPTransportwhen it is published, fall back to DIDComm — with the kill-switch flag on top rather than in place of the negotiation.