Skip to content

feat(did): advertise both receive transports on the holder DID - #135

Merged
stormer78 merged 1 commit into
mainfrom
feat/holder-advertises-tsp
Aug 29, 2026
Merged

feat(did): advertise both receive transports on the holder DID#135
stormer78 merged 1 commit into
mainfrom
feat/holder-advertises-tsp

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

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:2 carried exactly one service:

...(opts?.mediatorDid ? { service: { serviceEndpoint: opts.mediatorDid } } : {})

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-consent request 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 DIDCommMessaging service and a TSPTransport service, 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.

createDidPeer2 now takes services rather than service, emitting one .S element 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:

resolver "tsp" resolves to
affinidi-did-common (Rust, VTA side) TSPTransport
vti-didcomm-js (JS, wallet + relay) "tsp" — passed through verbatim

Publishing "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-js to add tsp to its map, after which the compact form becomes safe.

accept is emitted only for a DIDComm service. didcomm/v2 is 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. 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.

Tests

did.peer-services.mjs (4, new), asserted against the resolver that actually reads these DIDs rather than a decoder of my own:

  • both services resolve with the right types and the did:peer:2 ids (#service, #service-1);
  • the "tsp" abbreviation divergence, pinned as described above;
  • no services means no .S element;
  • accept on the DIDComm entry, absent on the TSP one.

npm run lint / build / test green — 608 tests. dist/background.js still a single bundle with no dynamic import().

One thing worth noting for review: createDidPeer2's field rename from service to services did 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 TSPTransport when it is published, fall back to DIDComm — with the kill-switch flag on top rather than in place of the negotiation.

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>
@stormer78
stormer78 merged commit 4f4f5f5 into main Aug 29, 2026
3 checks passed
@stormer78
stormer78 deleted the feat/holder-advertises-tsp branch August 29, 2026 12:38
stormer78 added a commit that referenced this pull request Aug 29, 2026
)

#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>
@affinidi-appsecurity-bot

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

1 AI-confirmed issue.

Mandatory to check: 🔒 Security Code Review Report

Details

🛡️ Security Code Review Report — PR #135

Field Value
Repository OpenVTC/vta-browser-plugin
Branch feat/holder-advertises-tspmain
Validated 2026-08-29
Scan ID e34999ff
Validator AI Security Validation Agent

🗺️ Scan Coverage

Modules scanned: 1 · with findings: 1 · files: 3 · findings: 1

Module Files scanned Findings
packages/core 3 1

Executive Summary

Category Confirmed Must-Review-By-Human
Security Issues 1 0

🔒 Security Issues

Confirmed Vulnerabilities (1)

🟡 Ambiguous service type abbreviation causes cross-resolver interpretation mismatch (parser differential)

Field Detail
Severity MEDIUM
Location packages/core/src/did/peer.ts:1
Finding ID github_pr-bc314a804865
CWE CWE-346
OWASP A08:2021-Software and Data Integrity Failures
Detection Source threat_model

📝 Description:

The type field of a DidPeerService is encoded verbatim (unless it's exactly "dm") into the did:peer:2 document. Different resolvers in the ecosystem (affinidi-did-common vs vti-didcomm-js) expand abbreviations like "tsp" differently — one expands it to "TSPTransport", the other passes it through unchanged. This is explicitly acknowledged and even tested (did.peer-services.mjs, test "the TSP entry is spelled out...") as a real, un-mitigated trap: the library does not validate or normalize the type value, relying entirely on caller discipline (the code comment says 'Spell a non-DIDComm type out in full') rather than enforcing it in code.

🌱 Root Cause: No validation/whitelist is applied to the caller-supplied type string before embedding it into the DID document; interpretation of ambiguous values is left to whichever resolver parses the DID later, which can behave inconsistently (CWE-346: origin validation error / inconsistent interpretation, CWE-20: improper input validation).

🔎 Evidence: packages/core/src/did/peer.ts:1

const isDidcomm = type === "dm" || type === "DIDCommMessaging";
const accept = s.accept ?? (isDidcomm ? ["didcomm/v2"] : undefined);
const abbreviated: Record<string, unknown> = {
  t: type,
  s: s.serviceEndpoint,
  ...(s.routingKeys && s.routingKeys.length > 0 ? { r: s.routingKeys } : {}),
  ...(accept ? { a: accept } : {}),
};

🎯 Attack Scenario:

An application constructs a DidPeerService with type: "tsp" believing it is a safe abbreviation. When the resulting DID is resolved by a different implementation (e.g., a wallet using vti-didcomm-js vs. a VTA using affinidi-did-common), one side sees type "TSPTransport" and the other sees literal "tsp". This causes the two parties to disagree on whether a service is a TSP transport, silently breaking transport negotiation — a holder might believe TSP messages will be routed/accepted when the mediator's demux or the executor's negotiation logic does not recognize the un-expanded string, resulting in messages being silently dropped or misrouted (as explicitly called out in the holder-identity.ts comment about consent requests going unhandled).

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 82%
  • AI Validation Evidence: EVIDENCE FOUND: peer.ts lines confirm the ambiguous type encoding: const type = s.type ?? "dm"; const isDidcomm = type === "dm" || type === "DIDCommMessaging"; ... const abbreviated: Record<string, unknown> = { t: type, s: s.serviceEndpoint, ... }. The type string is encoded verbatim into the base64url-encoded .S segment with no allow-list or ambiguity check. The file's own JSDoc explicitly documents the exact risk: 'affinidi-did-common's peer resolver expands "tsp" to TSPTransport, vti-didcomm-js's expands only "dm" and passes everything else through verbatim. So "tsp" resolves to two different service types depending on which side reads the DID.' This is a documentation-only mitigation, not an enforced code-level control — no runtime validation rejects ambiguous abbreviations like 'tsp'. EVIDENCE NOT FOUND: No allow-list, deny-list, or throw/reject logic for known-ambiguous abbreviations (e.g. 'tsp') was found anywhere in createDidPeer2 — only a comment advising callers to spell types out in full. No cross-resolver conformance test enforcing this at the createDidPeer2 level (only a canary test in a separate test file per the threat model, not shown as authoritative source here). CHANGED VS PRE-EXISTING: peer.ts is the sole source file provided and is directly implicated by 'feat/holder-advertises-tsp' branch name and holder-identity.ts usage of TSPTransport type per STRIDE-2 evidence; the vulnerable construct (type/abbreviated encoding) is the code under this MR's feature addition, so this is CHANGED. VERDICT JUSTIFICATION: The vulnerable construct is directly quoted and reachable — any caller passing type:'tsp' (as done via holder-identity.ts's TSPTransport-related feature) will have it encoded verbatim with no validation, confirming the parser differential is real and unmitigated in code (only documented in comments).
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.


Generated by Agentic Sec — AI Security Validation Agent
This report includes full scan data + AI validation evidence. Feed to engineering copilots for automated fix deployment.

Complementary: 🛡️ **Threat Model & Affect Analysis**
Details

🛡️ Threat Model & Affect Analysis — PR #135

Field Value
Repository OpenVTC/vta-browser-plugin
Branch feat/holder-advertises-tspmain
Generated 2026-08-29

ℹ️ This report contains theoretical threats and impact analysis for the MR.
Unlike the Security Code Review Report (which contains confirmed, materialised issues),
these are potential risks that may or may not be exploitable. Use this for defence-in-depth planning.


📋 Affect Analysis

Change Summary

Refactors 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
Types: feature, security, refactor, test

🧩 Affected Components

Component Impact Change What Changed
did-peer-service-builder (createDidPeer2) high modified Public API changed from accepting a single optional service object to an ordered services array, enabling multiple .S DID document ele
holder-identity-store (generateOrLoadHolderIdentity) critical modified Now unconditionally publishes both a DIDComm and a TSPTransport service (same mediator endpoint) whenever a mediatorDid is configured, versu
did-peer-services-test-suite low new New test suite added validating the multi-service encoding against the real external @openvtc/vti-didcomm-js resolver, including a canary

📁 File Classifications

packages/core/src/did/peer.ts

  • Type: security

packages/core/src/store/holder-identity.ts

  • Type: security

packages/core/tests/did.peer-services.mjs

  • Type: test

🛡️ STRIDE Threat Model

Identified Threats (10)

⚪ STRIDE-1: Cross-Resolver Type Confusion via Unspelled Abbreviation in createDidPeer2

Field Detail
Category Tampering, Denial of Service
Severity High
Likelihood Likely
CVSS 7.1 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-436,CWE-697,CWE-704
CAPEC CAPEC-153,CAPEC-267
OWASP A08:2021 - Software and Data Integrity Failures

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

const isDidcomm = type === "dm" || type === "DIDCommMessaging";
const abbreviated: Record<string, unknown> = { t: type, s: s.serviceEndpoint, ... };

Attack Scenario:

  1. A wallet or malicious peer constructs a DidPeerService with type: "tsp" (the abbreviated, non-spelled-out form) and passes it into createDidPeer2's services array in packages/core/src/did/peer.ts.
  2. The library encodes it verbatim as t: "tsp" inside the base64url-encoded .S element of the resulting did:peer:2 string (see const abbreviated: Record<string, unknown> = { t: type, ... }).
  3. A VTA/executor using affinidi-did-common's resolver expands "tsp" to TSPTransport and treats the endpoint as TSP-capable, while a wallet using vti-didcomm-js's resolver (per the test in did.peer-services.mjs) passes "tsp" through verbatim and treats it as an unknown/opaque service type.
  4. This causes the two sides of the ecosystem to disagree about whether the peer accepts TSP inbound traffic.
  5. An executor that believes TSP is supported (via the affinidi-did-common resolver) sends a TSP-framed consent request to a wallet mediator that, per the vti-didcomm-js interpretation, does not recognize the service as TSP.
  6. The consent request is accepted at the mediator hop (transport-level ack) but never routed to a handler that performs the human consent check (R7.2 gated action), because the wallet-side resolver did not recognize the service type as actionable.
  7. The request is silently dropped/stored at the mediator, producing a fail-open-looking transport ack with no delivered consent prompt — a consent bypass by omission rather than by cryptographic forgery.

🔎 Threat Clue: Derived from COMP-001, COMP-002 via EP-001, EP-002

  • Data Flows: did:peer:2 service block encoding/decoding across ecosystem resolvers

Preconditions: Attacker or misconfigured integrator controls or influences the type field passed to createDidPeer2 (e.g., a wallet-selection UI, SDK default, or copy-pasted example using the abbreviated form)., Two different DID resolver implementations from different ecosystem members are used on the two sides of a DIDComm/TSP exchange., No end-to-end acknowledgment or timeout-based consent completion protocol exists to detect the silent drop.

Existing Controls: JSDoc comment instructing consumers to 'Spell a non-DIDComm type out in full.' • did.peer-services.mjs contains a canary test (the TSP entry is spelled out...) that fails if the JS resolver ever starts expanding tsp, surfacing drift at test time only.

Recommended Mitigations: Enforce at the type-system/runtime-validation level in createDidPeer2 that type must not be a known-ambiguous abbreviation other than dm; throw/reject on tsp, t, or other single/short tokens not in an explicit allow-list. • Publish and version a shared, canonical abbreviation table across all ecosystem resolvers (affinidi-did-common, vti-didcomm-js) rather than relying on documentation-only conventions. • Add an end-to-end application-level acknowledgment for consent requests so a request accepted at the transport hop but never surfaced to a human triggers a detectable timeout/alert. • Add interop/cross-resolver conformance tests run in CI against all consumer resolver implementations, not just the JS one.


⚪ STRIDE-2: Silent Consent-Bypass via Unrouted TSP Push in generateOrLoadHolderIdentity

Field Detail
Category Tampering, Repudiation, Denial of Service
Severity High
Likelihood Likely
CVSS 7.6 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-345,CWE-354,CWE-693
CAPEC CAPEC-664,CAPEC-593
OWASP A04:2021 - Insecure Design

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

...(opts?.mediatorDid ? { services: [ { serviceEndpoint: opts.mediatorDid }, { type: "TSPTransport", serviceEndpoint: opts.mediatorDid } ] } : {}),

Attack Scenario:

  1. generateOrLoadHolderIdentity in packages/core/src/store/holder-identity.ts unconditionally publishes a second services entry { type: "TSPTransport", serviceEndpoint: opts.mediatorDid } whenever opts?.mediatorDid is set, regardless of whether the wallet's runtime actually has a TSP message handler wired up.
  2. An executor resolves the wallet's did:peer:2 document, observes the published TSPTransport service, and — per the documented negotiation model (TSP > DIDComm > REST) — prefers TSP and sends a consent request framed as a TSP push to the shared mediator DID.
  3. The mediator, which per the comment 'demultiplexes on the TSP magic byte', accepts and stores the TSP-framed message because the transport-level hop succeeded.
  4. If the wallet's application layer has not implemented/enabled a TSP inbound handler (a runtime/config state not verifiable from the published DID document), the stored message is never dequeued or surfaced to the user.
  5. The consent request — a gated action requiring human approval per requirement R7.2 — silently expires or sits unhandled with no error surfaced to either party.
  6. The executor sees a successful transport-level delivery (mediator ack) and may proceed as if the request is pending/being processed, while the wallet user never sees a prompt, resulting in an availability/integrity failure of the consent workflow with no attacker action required beyond normal protocol use — or, adversarially, an attacker-controlled executor can deliberately exploit this gap to send actions expecting them to be silently dropped and later claim they were sent while denying responsibility for the wallet's non-response.

🔎 Threat Clue: Derived from COMP-002 via EP-002

  • Data Flows: Executor -> Mediator (TSP push) -> Wallet handler (never invoked)

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 mediatorDid being present. • Implement mediator-side delivery receipts/read-receipts so the executor can detect non-delivery and fall back to DIDComm or alert. • Add wallet-side self-test at startup verifying the TSP handler is actually registered before advertising the service. • Define and enforce a consent-request timeout/expiry protocol visible to the executor so silent drops are observable rather than indefinite.


⚪ STRIDE-3: Unvalidated serviceEndpoint Injection in DidPeerService Construction

Field Detail
Category Tampering, Spoofing
Severity Medium
Likelihood Possible
CVSS 5.3 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-20,CWE-346
CAPEC CAPEC-148
OWASP A03:2021 - Injection

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

const abbreviated: Record<string, unknown> = { t: type, s: s.serviceEndpoint, ... };
const encoded = base64url.encode(new TextEncoder().encode(JSON.stringify(abbreviated)));

Attack Scenario:

  1. A caller (e.g. a compromised extension UI, or a mediatorDid option sourced from an untrusted configuration channel) supplies a crafted serviceEndpoint string — e.g. a did: value pointing to an attacker-controlled mediator, or a non-DID URI — to services in createDidPeer2 or via opts.mediatorDid in generateOrLoadHolderIdentity.
  2. No validation of serviceEndpoint's format (DID syntax, URI scheme allow-list) occurs in packages/core/src/did/peer.ts before it is embedded via s: s.serviceEndpoint into the abbreviated object and base64url-encoded into the did:peer:2 string.
  3. The resulting DID document is published/shared and resolved by executors that trust the embedded serviceEndpoint for routing DIDComm/TSP traffic.
  4. If the endpoint has been substituted for an attacker-controlled mediator DID, subsequent consent requests or DIDComm messages intended for the legitimate wallet mediator are instead routed to the attacker's mediator, enabling interception or replay.
  5. Because the DID itself is self-certifying only for the key material (.E/.V elements), not for the service endpoints, any executor that resolves the DID and blindly trusts the service block without additional authentication of the mediator is exposed to endpoint substitution.

🔎 Threat Clue: Derived from COMP-001, COMP-002 via EP-001, EP-002

  • Data Flows: Caller input -> DidPeerService.serviceEndpoint -> encoded DID string -> resolver/executor trust

Preconditions: Caller-supplied or configuration-sourced mediatorDid/serviceEndpoint is not independently validated or pinned by the consuming application before being passed to createDidPeer2., Executors trust resolved serviceEndpoint values without additional mediator authentication (e.g. TLS pinning, mediator DID allow-list).

Existing Controls: did:peer:2's .E/.V key elements are used for authcrypt/keyAgreement, providing message-level confidentiality/integrity independent of transport routing, partially limiting the blast radius of endpoint substitution to availability/interception of ciphertext rather than plaintext disclosure.

Recommended Mitigations: Add basic format validation (DID syntax or explicit URI scheme allow-list) for serviceEndpoint in createDidPeer2 before encoding. • Document and encourage callers to pin/validate mediatorDid against a known-good mediator registry before calling generateOrLoadHolderIdentity. • Rely on DIDComm message-level authcrypt (already using .E/.V) as defense-in-depth so a substituted mediator cannot read message content, and surface this expectation explicitly in documentation.


⚪ STRIDE-4: Unbounded Services Array Causing DID String Bloat / Resolver DoS

Field Detail
Category Denial of Service
Severity Low
Likelihood Possible
CVSS 3.7 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-400,CWE-770
CAPEC CAPEC-130
OWASP A04:2021 - Insecure Design

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 args.services, resulting in an arbitrarily long did:peer:2 string that can degrade resolver performance or exceed downstream storage/transport limits.

Evidence: packages/core/src/did/peer.ts:~91-109

for (const s of args.services ?? []) {
  ...
  did += `.S${encoded}`;
}

Attack Scenario:

  1. A caller invokes createDidPeer2 with an args.services array containing an excessive number of entries (e.g. hundreds), each contributing its own .S<base64url> segment via the for (const s of args.services ?? []) loop in packages/core/src/did/peer.ts.
  2. No maximum length check exists on args.services or on the resulting concatenated did string.
  3. The resulting did:peer:2 string becomes extremely large and is published (e.g. stored in holder-identity.ts, transmitted in DIDComm handshakes, or persisted in a DID document store).
  4. Downstream resolvers (affinidi-did-common, vti-didcomm-js) that parse every .S segment must decode and process each one, causing elevated CPU/memory usage proportional to attacker-controlled input size.
  5. If the identity generation flow accepts caller-controlled service lists from an untrusted source (e.g. imported wallet backup or malicious extension configuration), this becomes a resource-exhaustion vector against any party that resolves the resulting DID.

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: args.services -> did string concatenation loop

Preconditions: Untrusted or attacker-influenced input can reach the services array passed to createDidPeer2 (e.g., import/restore flow, misconfigured integration)., Downstream resolvers do not independently cap the number of .S segments they will parse.

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 args.services in createDidPeer2 (e.g. reject or truncate above a small sane bound such as 5-10 entries). • Validate/cap array length at any future public API surface (e.g. import/restore) that allows caller-supplied service lists before reaching createDidPeer2. • Document the maximum supported service count as part of the public interface contract.


⚪ STRIDE-5: Ambiguous accept-Field Omission Enabling DIDComm Media-Type Misrepresentation

Field Detail
Category Tampering, Information Disclosure
Severity Low
Likelihood Unlikely
CVSS 3.1 CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-843,CWE-707
CAPEC CAPEC-693
OWASP A04:2021 - Insecure Design

Description: accept field defaulting logic in createDidPeer2 in packages/core/src/did/peer.ts allows a caller to force-set accept on a non-DIDComm service type (or omit it on a DIDComm one) due to the caller-suppliable s.accept value taking precedence over the isDidcomm inference, resulting in a resolved service document that misrepresents the media types the endpoint actually supports.

Evidence: packages/core/src/did/peer.ts:~97-104

const isDidcomm = type === "dm" || type === "DIDCommMessaging";
const accept = s.accept ?? (isDidcomm ? ["didcomm/v2"] : undefined);

Attack Scenario:

  1. A caller passes { type: "TSPTransport", serviceEndpoint: ..., accept: ["didcomm/v2"] } into services.
  2. In packages/core/src/did/peer.ts, const accept = s.accept ?? (isDidcomm ? ["didcomm/v2"] : undefined); honors the caller-supplied accept even though isDidcomm is false for a TSPTransport type, embedding a DIDComm media-type claim on a non-DIDComm service.
  3. A resolving executor sees a TSPTransport service claiming accept: ["didcomm/v2"] and may attempt to negotiate/send DIDComm-framed messages to an endpoint that cannot actually process them, or misclassify the endpoint's capabilities in downstream routing logic.
  4. This can produce delivery failures for gated (consent) messages routed based on the incorrect accept claim, mirroring the same silent-drop pattern as STRIDE-2 but reachable purely through caller-supplied metadata rather than transport-layer ambiguity.

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: Caller-supplied accept field -> abbreviated.a -> resolved DID service

Preconditions: Caller of createDidPeer2 supplies an inconsistent type/accept combination, either accidentally (copy-paste from a DIDComm example) or intentionally., Downstream resolver/executor logic trusts accept metadata for routing decisions without cross-checking it against type.

Existing Controls: Default behavior (when accept is omitted) correctly infers based on isDidcomm, limiting exposure to only the case where callers explicitly override it. • JSDoc comment explains the intended semantics, providing documentation-level guidance against misuse.

Recommended Mitigations: Validate that accept is only honored (or only allowed) when isDidcomm is true; ignore or reject accept on non-DIDComm service types regardless of caller input. • Add a unit test asserting that supplying accept on a non-DIDComm type either throws or is stripped.


⚪ STRIDE-6: Service Ordering Dependency Causing Index-Based Identifier Confusion

Field Detail
Category Tampering, Repudiation
Severity Low
Likelihood Unlikely
CVSS 2.3 CVSS:4.0/AV:N/AC:H/AT:P/PR:H/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity None
CWE CWE-706
CAPEC CAPEC-141
OWASP A04:2021 - Insecure Design

Description: services array ordering in createDidPeer2 in packages/core/src/did/peer.ts allows service-id/index misassignment due to #service, #service-1, … ids being derived purely from array position rather than an explicit stable identifier, resulting in downstream references to a specific service id (e.g. routingKeys keyed by #service-1) silently resolving to a different service after a reordering.

Evidence: packages/core/src/did/peer.ts:~91-109

for (const s of args.services ?? []) { ... did += `.S${encoded}`; }

Attack Scenario:

  1. An application persists or communicates a reference to a specific service by its positional id (e.g. #service-1 for the TSPTransport entry) based on the current services array order in holder-identity.ts ([ {DIDComm}, {TSPTransport} ]).
  2. A future code change, configuration change, or caller-supplied reordering of the services array (createDidPeer2 has no id pinning, only positional numbering per the did:peer:2 convention) causes the DIDComm entry to become #service-1 and TSPTransport to become #service instead.
  3. Any stored reference, cached routing decision, or audit log entry that assumed the old id-to-type mapping now silently refers to the wrong service type.
  4. An executor or mediator using a stale cached mapping could route a DIDComm-only message to what is now the TSP endpoint id, or vice versa, without any error being raised, since both services share the same underlying serviceEndpoint (the mediator DID) in the current implementation and the mismatch is purely semantic (type/accept mismatch), not a routing failure that would be visibly rejected.

🔎 Threat Clue: Derived from COMP-001, COMP-002 via EP-001, EP-002

  • Data Flows: services array order -> positional #service id assignment -> downstream cached references

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 services array passed to createDidPeer2 changes between DID generation/publication and consumption.

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 type, never by cached positional id. • Consider adding a stable, explicit fragment identifier option in DidPeerService rather than relying solely on positional did:peer:2 numbering, if the underlying spec/resolver ecosystem allows extension.


⚪ STRIDE-7: Supply-Chain Trust in External did:peer:2 Resolver Packages

Field Detail
Category Tampering, Information Disclosure
Severity Medium
Likelihood Possible
CVSS 5.9 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-1104,CWE-829
CAPEC CAPEC-437
OWASP A08:2021 - Software and Data Integrity Failures

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

import { resolve as resolveDidPeer } from "@openvtc/vti-didcomm-js/did-peer";
import { ed25519, x25519 } from "@noble/curves/ed25519.js";

Attack Scenario:

  1. The test suite and (by extension) production consumers depend on @openvtc/vti-didcomm-js's resolve function and @noble/curves's Ed25519/X25519 primitives, imported directly in did.peer-services.mjs and used transitively by createDidPeer2 consumers.
  2. An attacker compromises the @openvtc/vti-didcomm-js package (e.g., via npm account takeover, malicious version publish, or dependency confusion) and modifies its abbreviation-expansion table or service parsing logic.
  3. Because this repository's threat model explicitly relies on the current divergent behavior between resolvers as a documented (if fragile) safety property (see the canary test 'if this now reads TSPTransport... tsp became safe to publish'), an upstream change to the resolver silently invalidates the security assumption encoded in application logic and tests without any change to this repository's own code.
  4. Similarly, a compromised @noble/curves release could weaken or backdoor key generation (ed25519.utils.randomSecretKey(), x25519.getPublicKey), directly compromising the wallet's cryptographic identity at the root of trust.
  5. Because dependency updates are typically consumed via semver ranges and CI, a malicious minor/patch version could be pulled in automatically without manual review, propagating the compromise to all consumers of packages/core.

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: npm dependency resolution -> resolver/crypto behavior -> DID trust root

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 + npm audit signatures or equivalent) for @openvtc/vti-didcomm-js and @noble/curves. • Vendor or checksum-pin the specific resolver logic this code's security assumptions depend on, or add a local regression test independent of the live external package that encodes the expected byte-level output. • Monitor upstream releases of both dependencies for unexpected behavioral changes via automated diffing in CI, separate from routine SCA/version scanning.


⚪ STRIDE-8: Missing Type Field Presence Check Enabling Deceptive Default-Type Spoofing

Field Detail
Category Spoofing, Tampering
Severity Low
Likelihood Possible
CVSS 3.8 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-1188,CWE-284
CAPEC CAPEC-148
OWASP A04:2021 - Insecure Design

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 s.type ?? "dm" applying even to services whose serviceEndpoint is not actually DIDComm-capable, resulting in downstream executors treating an arbitrary/incompatible endpoint as a trusted DIDComm mediator.

Evidence: packages/core/src/did/peer.ts:92-93

const type = s.type ?? "dm";
const isDidcomm = type === "dm" || type === "DIDCommMessaging";

Attack Scenario:

  1. A caller (or a data path fed by less-trusted configuration) constructs a DidPeerService object with a serviceEndpoint pointing to an arbitrary or attacker-influenced endpoint but omits type entirely.
  2. In createDidPeer2, const type = s.type ?? "dm"; defaults this to "dm", and isDidcomm becomes true, causing accept: ["didcomm/v2"] to be attached automatically.
  3. The resulting DID document asserts, with no independent verification, that the arbitrary endpoint is a legitimate DIDComm mediator accepting didcomm/v2 messages.
  4. An executor resolving this DID and trusting the DIDCommMessaging type/accept claims may send sensitive DIDComm-encrypted consent-flow messages to an endpoint that was never vetted as an actual DIDComm-capable mediator, with no code-level control preventing this beyond caller diligence.
  5. This is a lower-severity variant of STRIDE-3/STRIDE-5 specific to the default path being just as unguarded as the explicit-override path, meaning the 'safe by default' assumption of dm is only safe if the endpoint itself is trustworthy, which the library does not verify.

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: Caller-supplied serviceEndpoint (type omitted) -> implicit dm default -> DIDCommMessaging claim

Preconditions: Caller supplies a serviceEndpoint without setting type, and the endpoint is not independently vetted as DIDComm-capable before being passed to createDidPeer2., Downstream executor trusts the resolved DIDCommMessaging type/accept claim without additional handshake-level capability verification.

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 serviceEndpoint values must be pre-vetted as capable of the resulting default/explicit type before being passed to createDidPeer2. • Consider requiring explicit type for any serviceEndpoint not already known/allow-listed as a DIDComm mediator, removing the implicit default for externally-sourced endpoints.


⚪ STRIDE-9: Absence of Non-Repudiation Logging for Service-List Construction Decisions

Field Detail
Category Repudiation
Severity Informational
Likelihood Unlikely
CVSS 1.0 CVSS:4.0/AV:N/AC:H/AT:P/PR:H/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity None
CWE CWE-778
CAPEC CAPEC-81
OWASP A09:2021 - Security Logging and Monitoring Failures

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 services array used, resulting in an inability to later prove or disprove what capabilities a specific wallet DID advertised at a given point in time (relevant for post-incident investigation of STRIDE-1/STRIDE-2).

Evidence: packages/core/src/store/holder-identity.ts:179-201

const peer = createDidPeer2({ ed25519PublicKey: edPublic, x25519PublicKey: x25519Public, ...(opts?.mediatorDid ? { services: [...] } : {}) });

Attack Scenario:

  1. A wallet's DID is generated via generateOrLoadHolderIdentity with a particular mediatorDid/services configuration at time T0.
  2. No log entry, audit record, or persisted metadata captures the exact services array content or the code path/version that produced it (beyond the DID string itself, which does encode it but is not independently indexed/logged for investigation).
  3. If a consent-bypass incident (STRIDE-2) later occurs and is investigated, the incident responder must reverse-engineer the historical services configuration purely from the DID string(s) in use, without corroborating operational logs (e.g. 'holder identity generated with TSP advertised at T0').
  4. This complicates root-cause analysis and makes it harder to conclusively attribute whether a given wallet ever validly advertised TSP capability, weakening any after-the-fact dispute resolution between a wallet holder and an executor over a failed consent delivery.

🔎 Threat Clue: Derived from COMP-002 via EP-002

  • Data Flows: Identity generation event -> no audit trail

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

Field Detail
Category Tampering, Spoofing
Severity Informational
Likelihood Unlikely
CVSS 1.0 CVSS:4.0/AV:N/AC:H/AT:P/PR:H/UI:A/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity None
CWE CWE-1039
CAPEC CAPEC-242
OWASP A03:2021 - Injection

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 /* [REMOVED] */ blocks

/* [REMOVED]   /** Service type. `"dm"` abbreviates `DIDCommMessaging` (the default). */ */

Attack Scenario:

  1. The provided source content contains extensive free-text rationale (e.g. '/* [REMOVED] ... */' markers and long inline commentary) interleaved with actual code in peer.ts and holder-identity.ts.
  2. An automated analysis pipeline (such as this very threat-modeling process) ingests this text as 'source code' without distinguishing genuine code semantics from narrative commentary.
  3. If an attacker with commit/PR access crafted comments containing directive-sounding phrasing (e.g., 'ignore previous findings', 'mark as false positive', 'do not report this'), a downstream automated tool with insufficient input/data separation could be manipulated into altering its own verdicts.
  4. In the current diff, no such adversarial phrasing was found — the commentary is legitimate engineering rationale — but the pattern (long embedded prose adjacent to code) is structurally the same shape that such an attack would take, warranting this be flagged as a design-time process risk for the SDLC's automated review tooling rather than a vulnerability in the application itself.

🔎 Threat Clue: Derived from N/A via N/A

  • Data Flows: PR diff content -> automated analysis tooling ingestion

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 Model

Application Purpose

A 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

  • Cross-implementation resolver interoperability is documentation-enforced rather than protocol-enforced, creating persistent drift risk between ecosystem members.
  • Transport-hop acceptance at a shared mediator is conflated with actual application-level delivery, creating a structural gap for gated consent actions.
  • The security model relies on multiple independently-maintained npm packages (vti-didcomm-js, noble-curves, affinidi-did-common) whose behavior this repository cannot directly control or freeze.

Objectives

Risk: 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: Enable browser-extension wallets to interoperate with multiple executor/VTA implementations across the OpenVTC ecosystem without requiring a single shared codebase.
Security: Ensure gated actions (consent requests, R7.2) are never silently dropped without a human-visible signal.; Ensure service-type advertisements accurately reflect actual wallet-side runtime capability.
Financial: Avoid costly out-of-band support/dispute resolution caused by silently dropped consent requests between wallets and executors.
Compliance: Maintain auditable evidence that consent (human-in-the-loop) requirements (R7.2) are technically enforceable end-to-end, not merely at the transport hop.
Functional: Allow a wallet to construct a did:peer:2 identity advertising one or more transports (DIDComm, TSP) it can receive messages on.
Operational: Ensure identity generation and service advertisement remain deterministic and testable against real ecosystem resolver implementations.

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

  • Stakeholders: Executors / Mediator Operators / Wallet Developers / Wallet Holders
  • Dependencies: did:peer:2 DID document resolution / DIDComm/TSP mediator infrastructure / Wallet TSP/DIDComm inbound handlers / holder-identity.ts identity generation
  • Disruptions: TSP push accepted by mediator but never routed to wallet handler (STRIDE-2) / Cross-resolver type-confusion causing executor/wallet capability mismatch (STRIDE-1)
  • Impacts: Consent requests silently expire with no human review, functionally equivalent to either an unauthorized default-deny or an undetected default-approve depending on executor-side fallback logic / Erosion of trust in the OpenVTC ecosystem's consent-gating guarantee (R7.2), with reputational and compliance exposure

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

  • Stakeholders: Wallet Developers / Wallet Holders / Executors
  • Dependencies: createDidPeer2 (packages/core/src/did/peer.ts) / generateOrLoadHolderIdentity (packages/core/src/store/holder-identity.ts) / @noble/curves key generation / secretWrap key storage
  • Disruptions: Malformed or excessive services array causing resolver performance degradation (STRIDE-4) / Type/accept field inconsistency misrepresenting endpoint capability (STRIDE-5, STRIDE-8)
  • Impacts: Executors build inaccurate capability models of a wallet, degrading negotiation reliability / Potential resource exhaustion at resolving parties for pathological inputs

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

  • Stakeholders: OpenVTC Ecosystem Maintainers / Wallet Developers / Executor Developers
  • Dependencies: affinidi-did-common package (external) / vti-didcomm-js package (external) / did:peer:2 specification conventions
  • Disruptions: An external resolver package silently changes its abbreviation-expansion table (supply-chain drift, STRIDE-7)
  • Impacts: Long-tail interoperability failures across the ecosystem not attributable to any single party's code change

Technical Scope

Roles (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)

  • Wallet Identity Generation With Multi-Transport Advertisement: A wallet generates its did:peer:2 identity and advertises both DIDComm and TSP services backed by the same mediator so executors can negotiate the preferred transport.
  • Executor Resolves Wallet DID and Sends Consent Request: An executor resolves a wallet's published did:peer:2 document, negotiates a preferred transport, and routes a gated consent request through the shared mediator to the wallet holder for approval.

📋 Risk Registry (5)

ID Title Severity Residual Priority Effort
RISK-001 Gated consent requests can be silently dropped when a wallet advertises a TSP transport it cannot actually route to a human-review handler. High Medium Immediate Medium
RISK-002 Divergent resolver abbreviation-expansion tables across ecosystem members can cause service-type misinterpretation. High Medium Short-Term Medium
RISK-003 Unvalidated serviceEndpoint values can be substituted to redirect executor traffic to an attacker-controlled mediator. Medium Low Medium-Term Low
RISK-004 Reliance on independently-maintained external resolver and cryptography packages introduces supply-chain risk to DID resolution and key generation. Medium Low Medium-Term Medium
RISK-005 Unbounded service list construction could be leveraged for resource-exhaustion against resolving parties. Low Low Long-Term Low

⚔️ 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
Loading

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
Loading

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
Loading

📊 Risk Summary

Total Threats: 10

By Severity: Low: 4 · High: 2 · Medium: 2 · Informational: 2

By Category: Unknown: 10

🎯 Attack Surface

Kill Chain 1: A misconfigured or malicious integrator passes an abbreviated, non-portable service type such as "tsp" into createDidPeer2 (STRIDE-1), producing a did:peer:2 identifier that two different ecosystem resolvers (affinidi-did-common vs vti-didcomm-js) decode inconsistently; an executor relying on the resolver that expands the abbreviation believes the wallet accepts TSP and sends a gated consent request over that transport. Kill Chain 2: Independently of the abbreviation issue, generateOrLoadHolderIdentity unconditionally advertises a TSPTransport service whenever a mediatorDid is configured (STRIDE-2), without verifying the wallet's runtime actually has a TSP handler wired up; the shared mediator accepts the TSP-framed push at the transport hop (demultiplexing on the magic byte) and stores it, but because no application-level handler processes it, the consent request is never surfaced to the human holder — chaining STRIDE-1's type-confusion with STRIDE-2's capability/runtime gap produces a compound failure where an executor's message can be transport-accepted yet human-invisible, defeating the R7.2 consent-gating control entirely without any cryptographic compromise. Kill Chain 3: An attacker who additionally controls or influences the serviceEndpoint/mediatorDid value (STRIDE-3, STRIDE-8) could redirect the advertised mediator to an endpoint they control, compounding the delivery-integrity problem with a routing-integrity problem — though DIDComm's authcrypt

🛡️ Risk Mitigation Strategy

Priority 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 type values — and by establishing a shared, versioned abbreviation table with automated cross-resolver conformance tests run against all ecosystem-member resolver packages in CI, so drift is caught before it reaches production DID documents. Priority 3 (Medium-Term): Reduce the trust placed in unvalidated caller-supplied data and external packages (RISK-003, RISK-004) by adding format validation for serviceEndpoint/mediatorDid inputs, pinning and integrity-checking the @openvtc/vti-didcomm-js and @noble/curves dependencies, and adding regression tests that do not solely depend on live external package behavior for their assertions. Priority 4 (Long-Term): Harden the library against resource-exhaustion and observability gaps (RISK-005, and the related repudiation/logging gap) by bounding the services array length and adding structured, non-secret audit logging of identity-generation and service-advertisement events to support forensic reconstruction of capability claims during future incident investigations.


Generated by Agentic Sec — Threat Model & Affect Analysis Agent

📊 Summary & findings
✅ Confirmed ⚠️ Must-Review-By-Human
1 0

Confirmed (1)

  • 🟡 Ambiguous service type abbreviation causes cross-resolver interpretation mismatch (parser differential)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants