feat(inbound): receive executor-initiated requests over TSP as well as DIDComm - #134
Conversation
…s DIDComm
The wallet's inbox was DIDComm-only, and the asymmetry was precise: the VTA
accepts a `task-consent/decision` back over TSP (`tsp_inbound.rs`) while
`didcomm_bridge.rs` is the only thing that *pushes* a consent or step-up
request. Decisions went up over anything; prompts came down over one transport.
Client-side that was enforced by a discard. `onTspFrame` was a FIFO waiter
queue, and a frame with no outstanding request hit
// else: straggler with no outstanding request — discard.
So an executor-initiated request arriving over TSP had nowhere to go — a
silently lost consent prompt, which is a gated action that never got its human
check (guide rule R7.2), and one that never reached `putPendingInbound` either.
Three things had to change together.
**Correlation, before anything else.** FIFO was sound only while replies were
the only thing on the socket. Once pushes share it, a consent request landing
mid-vault-op would be handed to that op's waiter and parsed as its reply, while
the push itself vanished — strictly worse than dropping it. `awaitTspFrame` now
takes a `claims` predicate and offers each frame to the outstanding requests in
turn. The connection layer holds no TSP keys, so only the caller can tell a
reply from a push: `TspChannel` unpacks with the VTA keys it addressed and
matches the Trust-Task `threadId`, which threads to the request `id` exactly as
DIDComm's `thid ?? id` does, and as the VTA's `respond_with` sets it. A frame
nothing claims is unsolicited by definition.
That moves one existing behaviour. A reply sealed by the wrong sender used to
be unpacked and then refused with `unauthorized`; it is now simply not claimed.
On a shared socket that is the right reading — a frame from someone else is not
an error for *this* request — and refusing it there would let any peer with
socket access fail an unrelated in-flight operation. The security property is
unchanged and now sits where it belongs: the frame falls through to
`unpackInboundTsp`, which resolves the claimed sender's own keys and fails the
unpack.
**Verification, at the boundary.** `unpackInboundTsp` (`vta/tsp-inbound.ts`)
turns a sealed frame into the message shape the pipeline already consumes. A
TSP frame names its sender in cleartext — that is how the mediator routes
without keys — so the VID is read only to look up which keys to try; `unpack`
then verifies the Ed25519 signature and the HPKE-Auth sender-binding against
them. Reading the claim and believing it are different acts, and only the first
happens here.
**Adaptation, honestly.** Over DIDComm a request arrives as a binding envelope
whose `body` is the Trust-Task document; over TSP the plaintext *is* the
document. Everything security-bearing downstream reads the document — the proof
check, the enrolled-executor check, the §7.2 item 11 dedup claim — so the two
converge the moment the document is in hand. The adapted message uses the
document's own `id`, which SPEC §7.2 item 11 says to key on anyway, making the
TSP path structurally closer to the spec than the DIDComm one, where
`message.id` is a transport id and dedup has to reach past it into `body`.
Both inboxes are wired: the worker's and the approver's, each as its own
identity. A frame that fails verification throws, which withholds the mediator's
ack (vti-didcomm-js 0.7.0) and leaves it queued — right for a transient failure
such as an unreachable DID document, harmless for a permanent one, since an
unverifiable frame is refused again on every redelivery and reaches no human
either way.
Requires vti-didcomm-js ^0.7.0, which acks TSP frames after awaiting their
consumer. Without it there is no ack to order against and the persist-before-ack
guarantee does not hold for this path.
The VTA still pushes over DIDComm; making it prefer TSP is the next change, and
this is the half that has to land first — a receiver that works before a sender
that relies on it.
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
🛡️ AI Agentic Security Code Review2 findings need a human to review/validate. Mandatory to check: 🔒 Security Code Review Report Details🛡️ Security Code Review Report — PR #134
🗺️ Scan CoverageModules scanned: 3 · with findings: 2 · files: 12 · findings: 5
Executive Summary
🔒 Security Issues
|
| Field | Detail |
|---|---|
| Severity | HIGH |
| Location | packages/core/src/didcomm/index.ts:383 |
| Finding ID | github_pr-0e8124e0418d |
| OWASP | A03:2017 - Sensitive Data Exposure |
| CVSS 4.0 | 8 |
| Exploit Maturity | conceptual |
| Detection Source | mcp_semgrep |
Summary: Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections. — 2 occurrence(s): index.ts:383, index.ts:624
📝 Description:
Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections.
🌱 Root Cause: Detect Insecure Websocket
🔧 Remediation:
⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.
Priority: Immediate
Detect Insecure Websocket: Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections.
🔍 Validation Log
- Verdict:
⚠️ Must-Review-By-Human- Confidence: 30%
- AI Validation Evidence: EVIDENCE FOUND: The evidence block for this finding has an empty code_snippet ('code_snippet":""') at packages/core/src/didcomm/index.ts line 383, so no concrete insecure ws:// usage was shown. Searching the provided didcomm/index.ts content, resolveMediatorEndpoint explicitly guards insecure transports: 'Refuses plaintext (ws:///http://) endpoints unless allowInsecure is set (local dev only)' and connectMediatorSession accepts an 'allowInsecure' option defaulted to false ('allowInsecure: opts.allowInsecure ?? false'). EVIDENCE NOT FOUND: The actual line 383 content is not visible in the provided source excerpt (the file as given does not clearly map line 383 to a hardcoded ws:// URL); the tool-generated evidence is empty, so the specific insecure construct could not be directly quoted. CHANGED VS PRE-EXISTING: didcomm/index.ts is in the changed-files scope for this MR, so this is CHANGED if the sink exists, but the concrete sink line/text wasn't verifiable from given content. VERDICT JUSTIFICATION: Without a quoted line 383 snippet and given that visible code paths show explicit secure-by-default guards (allowInsecure defaults false, explicit doc comment refusing plaintext), I cannot confirm exploitability, but also cannot fully dismiss since some other unseen constructor call could exist — must_review.
- Validation Effort: This finding was validated up to a point, but the available evidence was insufficient for a conclusive automated verdict. A human (developer / security team) must manually review the code and decide. Not dismissed — treat as an open item pending human review.
🔵 Unsafe Formatstring (2 occurrences)
| Field | Detail |
|---|---|
| Severity | LOW |
| Location | packages/extension/src/offscreen.ts:782 |
| Finding ID | github_pr-356b6a98e65a |
| OWASP | A01:2021 - Broken Access Control |
| CVSS 4.0 | 3.5 |
| Exploit Maturity | conceptual |
| Detection Source | mcp_semgrep |
Summary: Detected string concatenation with a non-literal variable in a util.format / console.log function. If an attacker injects a format specifier in the string, it will forge the log message. Try to use co — 2 occurrence(s): offscreen.ts:782, offscreen.ts:791
📝 Description:
Detected string concatenation with a non-literal variable in a util.format / console.log function. If an attacker injects a format specifier in the string, it will forge the log message. Try to use co
🌱 Root Cause: Unsafe Formatstring
🔧 Remediation:
⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.
Priority: Short-term
Unsafe Formatstring: Detected string concatenation with a non-literal variable in a util.format / console.log function. If an attacker injects a format specifier in the string, it will forge the log message. Try to use co
🔍 Validation Log
- Verdict:
⚠️ Must-Review-By-Human- Confidence: 25%
- AI Validation Evidence: EVIDENCE FOUND: The finding references packages/extension/src/offscreen.ts line 782 with an empty code_snippet, and offscreen.ts is not included in the provided source_files, so I cannot inspect the actual console.log/util.format call in question. EVIDENCE NOT FOUND: No content of offscreen.ts was provided to verify whether string concatenation with a non-literal variable is passed to a format-string-sensitive logging function, nor whether any sanitization exists. CHANGED VS PRE-EXISTING: offscreen.ts is referenced in the threat model as 'not fully in scope'/'not shown in this excerpt', and its content is absent from source_files, so I cannot determine changed vs pre-existing status from evidence alone. VERDICT JUSTIFICATION: Insufficient evidence (file not provided) to confirm or dismiss — must_review per the hard rule that missing deciding file means not 'validated', and it's a real code pattern class (not stub) so not 'not_applicable' either.
- Validation Effort: This finding was validated up to a point, but the available evidence was insufficient for a conclusive automated verdict. A human (developer / security team) must manually review the code and decide. Not dismissed — treat as an open item pending human review.
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.
Details
🛡️ Threat Model & Affect Analysis — PR #134
| Field | Value |
|---|---|
| Repository | OpenVTC/vta-browser-plugin |
| Branch | feat/tsp-inbound → main |
| 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
This PR fixes a message-confusion vulnerability in the TSP mediator connection layer where executor-initiated pushes (task-consent, step-up requests) could be silently misdelivered to an unrelated pending request's waiter under the old FIFO reply-matching scheme. It replaces FIFO with a cryptographically-verified, threadId-correlated claim-predicate model and adds a new inbound-verification module (tsp-inbound.ts) so executor-initiated TSP requests can be received and verified alongside DIDComm.
Diff: +245 / -32 lines
Types: security, feature, dependency_upgrade, test
📁 File Classifications
packages/core/src/didcomm/index.ts
- Type: security
packages/core/src/vta/tsp-channel.ts
- Type: security
packages/core/src/vta/tsp-inbound.ts
- Type: security
packages/core/src/vta/index.ts
- Type: config
packages/core/package.json
- Type: config
🛡️ STRIDE Threat Model
Identified Threats (12)
⚪ STRIDE-1: Claim Predicate Bypass via Malformed threadId in awaitTspFrame
| Field | Detail |
|---|---|
| Category | Spoofing, Tampering |
| Severity | High |
| Likelihood | Possible |
| CVSS | 8.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Medium |
| CWE | CWE-345,CWE-697 |
| CAPEC | CAPEC-593,CAPEC-115 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
Description: awaitTspFrame in TspChannel.send allows reply-frame misattribution due to loose equality matching on doc.threadId against envelope.id without type/format validation, resulting in message confusion between concurrent requests
Evidence: packages/core/src/vta/tsp-channel.ts:188-235
if (doc.threadId !== envelope.id) return false; claimedDoc = doc; return true;
Attack Scenario:
- Attacker (malicious or compromised VTA endpoint) crafts a TSP frame that unpacks successfully under the victim's resolved VTA keys (attacker controls or has compromised the VTA identity, or exploits a key-resolution flaw).
- Attacker sets the JSON payload's
threadIdfield to match a currently outstanding request'senvelope.id, e.g. by observing prior traffic or predicting IDs (see tsp-channel.ts claims callback:if (doc.threadId !== envelope.id) return false;). - The claims predicate in tsp-channel.ts iterates tspWaiters in order and the crafted frame satisfies the loose
!==check since JS coerces types loosely if doc.threadId is not strictly a string matching envelope.id's type. - The forged frame is resolved as the legitimate reply to the pending send(), and parseTrustTaskReply() processes attacker-controlled payload as trusted VTA response.
- Application logic proceeds using the forged Trust-Task reply document, potentially approving/denying operations based on attacker data.
🔎 Threat Clue: Derived from COMP-002 via EP-004
- Data Flows: TSP-reply-correlation
Preconditions: Attacker can produce a frame that decrypts/verifies under unpack() with the resolved VTA keys (e.g., VTA compromise, key-resolution spoofing, or a separate MITM on key resolution), envelope.id values are guessable, sequential, or otherwise predictable, Multiple concurrent outstanding requests exist
Existing Controls: unpack() cryptographically verifies sender authenticity (Ed25519 signature + HPKE-Auth) before threadId comparison • reply.sender !== this.vta.vid check enforces sender VID matches addressed VTA
Recommended Mitigations: Use cryptographically random, unguessable envelope.id (UUIDv4 or better) to prevent threadId prediction • Enforce strict type check (typeof doc.threadId === 'string') before comparison • Add nonce or session-bound context in threadId derivation • Log and alert on repeated claim attempts with mismatched threadId patterns
⚪ STRIDE-2: Claim Predicate Exception Swallowing Enables Silent Waiter Skip
| Field | Detail |
|---|---|
| Category | Denial of Service |
| Severity | Medium |
| Likelihood | Likely |
| CVSS | 5.3 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-405,CWE-834 |
| CAPEC | CAPEC-125,CAPEC-130 |
| OWASP | A04:2021 - Insecure Design |
Description: onTspFrame handler in didcomm/index.ts allows denial-of-service against message correlation due to broad try/catch swallowing all claim() exceptions as false, resulting in resource exhaustion or unbounded waiter iteration under crafted frame floods
Evidence: packages/core/src/didcomm/index.ts:696-728
for (let i = 0; i < tspWaiters.length; i++) { const w = tspWaiters[i]!; let claimed = false; try { claimed = await w.claims(bytes); } catch { claimed = false; } ...
Attack Scenario:
- Attacker (malicious VTA or MITM on mediator connection) sends a rapid flood of malformed TSP frames over the shared WebSocket.
- Each frame triggers onTspFrame in didcomm/index.ts, which iterates the full tspWaiters array and calls w.claims(bytes) for every outstanding request.
- Each claims() call performs an unpack() (asymmetric crypto operation) even for garbage frames, since the try/catch only returns false rather than short-circuiting cheaply.
- With many outstanding requests (large tspWaiters array) and many frames, CPU cost scales as O(waiters * frames), exhausting event-loop time.
- Legitimate pending requests time out waiting for their real replies because the event loop is saturated processing bogus unpack attempts.
- Denial of service against wallet's ability to complete VTA-mediated Trust-Task exchanges.
🔎 Threat Clue: Derived from COMP-001 via EP-002, EP-006
- Data Flows: mediator-websocket-inbound
Preconditions: Attacker can inject frames into the shared mediator WebSocket (malicious/compromised mediator or VTA, or network position), Multiple concurrent outstanding TSP requests
Existing Controls: claims() wrapped in try/catch defaults to false, preventing crash • timeout mechanism (setTimeout) eventually rejects stuck waiters
Recommended Mitigations: Add per-connection rate limiting on inbound TSP frames • Add cheap pre-filter (e.g., envelope sender/type check) before invoking expensive unpack() in claims() • Cap the maximum number of concurrent tspWaiters • Add circuit breaker / backpressure when frame processing time exceeds a budget
⚪ STRIDE-3: Ack-Before-Persistence Handler Throw Causes Silent Frame Loss
| Field | Detail |
|---|---|
| Category | Tampering, Repudiation, Denial of Service |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 6.5 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N |
| Residual Severity | Medium |
| CWE | CWE-696,CWE-362 |
| CAPEC | CAPEC-26 |
| OWASP | A04:2021 - Insecure Design |
Description: onInboundTsp handler contract in didcomm/index.ts allows message loss or duplication due to reliance on handler correctness for the ack-before-durable-storage guarantee, resulting in dropped task-consent/step-up requests or duplicate prompts on redelivery
Evidence: packages/core/src/didcomm/index.ts:725-728
if (inboundTspHandler) await inboundTspHandler(bytes);
Attack Scenario:
- VTA sends a legitimate unclaimed TSP frame (task-consent request) which reaches
if (inboundTspHandler) await inboundTspHandler(bytes);in didcomm/index.ts. - The registered handler in the extension (offscreen.ts, not fully in scope) begins persisting the message but throws partway through (e.g., storage quota exceeded, browser IndexedDB error) before durable storage completes.
- Per the documented R1.6 contract, a throw withholds the ack, so the mediator redelivers the same frame.
- If the handler is not idempotent (de-duplication logic is a documented but unenforced requirement — 'handlers must de-duplicate'), redelivery causes a duplicate user consent prompt or double-processing of a step-up request.
- Conversely, if the handler resolves successfully but the actual persistence write fails silently after the await resolves, the frame is acked and permanently lost — no error surfaces to the user or system.
- Attacker exploiting handler race conditions or storage instability can induce duplicate consent prompts, potentially conditioning the user to click through repeated prompts (prompt fatigue) or causing legitimate task-consent requests to be silently dropped.
🔎 Threat Clue: Derived from COMP-001 via EP-003
- Data Flows: executor-push-persistence
Preconditions: Handler implementation is not perfectly idempotent under redelivery, Storage or persistence layer can fail non-atomically relative to the await boundary, Attacker can trigger redelivery timing (e.g., via connection flapping)
Existing Controls: Documented R1.6 ack-before-persist ordering contract enforced by vti-didcomm-js >=0.7.0 • Explicit code comments mandating handler de-duplication
Recommended Mitigations: Enforce idempotency via message/document id-based deduplication ledger at the handler boundary, not just by convention • Add integration tests simulating handler throw mid-persist to verify redelivery behavior • Add telemetry/alerting on redelivery counts per document id to detect anomalies • Perform persistence write and ack signal atomically or with a durable write-ahead log
⚪ STRIDE-4: Sender VID Spoofing in Cleartext CESR Envelope During Key Resolution
| Field | Detail |
|---|---|
| Category | Spoofing, Denial of Service |
| Severity | High |
| Likelihood | Possible |
| CVSS | 7.7 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Medium |
| CWE | CWE-290,CWE-406 |
| CAPEC | CAPEC-141,CAPEC-664 |
| OWASP | A04:2021 - Insecure Design |
Description: unpackInboundTsp in tsp-inbound.ts allows sender VID spoofing during the key-resolution phase due to reliance on cleartext-CESR-declared sender identity before cryptographic proof, resulting in unnecessary key-resolution requests to attacker-chosen VIDs and potential SSRF-like resolution abuse or DoS on resolveSender
Evidence: packages/core/src/vta/tsp-inbound.ts:90-104
claimedSender = decodeEnvelope(bytes).envelope.sender; ... sender = await opts.resolveSender(claimedSender);
Attack Scenario:
- Attacker with access to the mediator's WebSocket path (or a malicious mediator) sends a crafted TSP frame with an arbitrary cleartext sender VID in the CESR envelope (decoded via decodeEnvelope in tsp-inbound.ts).
- unpackInboundTsp calls
opts.resolveSender(claimedSender)with this fully attacker-controlled string BEFORE any cryptographic verification occurs. - If resolveSender performs network resolution (e.g., DID resolution over HTTP/DNS as implied by vtiResolveKeyAgreement elsewhere in the codebase), the attacker can supply a large volume of distinct bogus VIDs to trigger many outbound resolution requests, causing resource exhaustion or exposing the resolver to SSRF against attacker-controlled resolution endpoints.
- Each failed resolution throws and is caught, generating a VtaClientError, but the resolution attempt (and its side effects: network call, cache pollution) already occurred.
- Attacker floods with many unique fake VIDs, exhausting resolver quota, poisoning any DID-resolution cache with garbage entries, or causing outbound requests to attacker-controlled infrastructure that could fingerprint the wallet's IP/user-agent.
🔎 Threat Clue: Derived from COMP-003 via EP-005
- Data Flows: tsp-inbound-key-resolution
Preconditions: resolveSender performs a network-based or otherwise costly resolution of the cleartext VID, Attacker can inject frames onto the shared WebSocket, No rate limiting on resolution attempts
Existing Controls: unpack() cryptographically verifies signature/sender-auth after resolution, preventing acceptance of unauthenticated content • Code comments explicitly acknowledge cleartext VID is only a 'candidate' until proven
Recommended Mitigations: Rate-limit resolveSender calls per connection/session • Cache negative resolution results to avoid repeated network calls for the same bogus VID • Validate VID format/allow-list against known VTA/executor identities before attempting resolution • Add network-level egress controls or resolver sandboxing to mitigate SSRF risk
⚪ STRIDE-5: Supply Chain Trust Extension via Minor Version Bump of @openvtc/vti-didcomm-js
| Field | Detail |
|---|---|
| Category | Tampering |
| 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:L/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-1104,CWE-829 |
| CAPEC | CAPEC-538 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
Description: package.json dependency declaration for @openvtc/vti-didcomm-js allows acceptance of any 0.7.x release due to caret-range versioning (^0.7.0) combined with security-critical ordering-guarantee reliance (R1.6 ack-before-persist), resulting in silent behavioral regression if a future patch/minor release violates the ordering contract this PR's correctness depends on
Evidence: packages/core/package.json:124
"@openvtc/vti-didcomm-js": "^0.7.0",
Attack Scenario:
- The security fix in this PR (deduplication, message correlation integrity) explicitly depends on a new ordering guarantee documented as 'R1.6' provided starting at vti-didcomm-js 0.7.0.
- package.json declares
"@openvtc/vti-didcomm-js": "^0.7.0", which under semver allows automatic adoption of any 0.7.x or 0.8.x, etc. release without a code review gate, since caret ranges permit minor/patch drift. - A compromised or buggy future publish (supply-chain compromise of the @OpenVTC npm scope, or an unintentional regression) could alter or remove the ack-before-persist ordering while remaining semver-compatible on paper.
- npm install in CI/dev silently pulls the new version; package-lock.json is regenerated and the security-critical timing assumption documented throughout tsp-channel.ts and didcomm/index.ts silently breaks.
- De-duplication and reply/push disambiguation guarantees this PR is built on degrade without any code change to the vulnerable repository, undermining the very fix being introduced.
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: build-pipeline-dependency-resolution
Preconditions: A future @openvtc/vti-didcomm-js release regresses the R1.6 ordering guarantee, No integration test explicitly asserts the ack-timing contract at the dependency boundary, CI performs unpinned or auto-updated installs
Existing Controls: package-lock.json pins exact resolved version and integrity hash at commit time • Code comments explicitly document the version dependency (>=0.7.0) for the R1.6 guarantee
Recommended Mitigations: Pin exact version (no caret) for security-critical dependencies with documented ordering contracts • Add a runtime or integration self-test that verifies ack-before-persist ordering behavior against the installed dependency version at startup • Subscribe to security advisories for the @OpenVTC npm scope and gate upgrades through manual review • Use lockfile integrity verification (npm ci) in CI/CD to prevent drift
⚪ STRIDE-6: Missing Replay Protection on threadId Correlation Enables Reply Replay
| Field | Detail |
|---|---|
| Category | Tampering, Spoofing |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 6.9 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-294 |
| CAPEC | CAPEC-60 |
| OWASP | A07:2021 - Identification and Authentication Failures |
Description: claims predicate in TspChannel.send allows replay of a previously-valid VTA reply due to absence of nonce/timestamp/one-time-use enforcement on threadId matching, resulting in a stale or replayed authentic VTA response being accepted as current if the same envelope.id is reused across requests
Evidence: packages/core/src/vta/tsp-channel.ts:215-217
if (doc.threadId !== envelope.id) return false; claimedDoc = doc; return true;
Attack Scenario:
- A legitimate VTA reply for request A (envelope.id = X) is captured by a MITM/malicious mediator (frames traverse mediator infrastructure per COMP-001).
- Later, the wallet issues a new request B that, due to an id-generation bug, non-cryptographic id-space collision, or intentional id reuse elsewhere in the codebase, is assigned the same or a colliding envelope.id X.
- The mediator (malicious or compromised) replays the captured old frame for request A in response to request B's send().
- The claims predicate only checks
doc.threadId !== envelope.id; it does not check freshness, a nonce, or that the reply postdates the request, so the stale frame passes verification (sender is validly the VTA, since it was a real prior reply). - parseTrustTaskReply() processes stale/incorrect data as if it were the current authoritative response to request B, potentially causing the wallet to act on outdated consent/approval state.
🔎 Threat Clue: Derived from COMP-002 via EP-004
- Data Flows: TSP-reply-correlation
Preconditions: envelope.id generation has weak uniqueness guarantees or can collide/repeat, Attacker or malicious mediator can capture and replay previously observed valid TSP frames, No timestamp or nonce freshness check exists in the reply-claim logic
Existing Controls: Sender authenticity is cryptographically verified via unpack(), so only genuine past VTA replies are replayable (not forged ones) • Timeout mechanism limits the window during which a waiter is outstanding
Recommended Mitigations: Include a monotonically increasing nonce or timestamp in the Trust-Task envelope and reject replies older than a freshness threshold • Ensure envelope.id generation uses cryptographically random UUIDs with negligible collision probability • Track consumed threadId values to reject any duplicate reply for a given already-completed request
⚪ STRIDE-7: Insufficient Logging of Rejected/Unclaimed TSP Frames Undermines Forensics
| Field | Detail |
|---|---|
| Category | Repudiation |
| Severity | Low |
| Likelihood | Likely |
| CVSS | 3.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-778 |
| CAPEC | CAPEC-268 |
| OWASP | A09:2021 - Security Logging and Monitoring Failures |
Description: onTspFrame handler and unpackInboundTsp allow undetected probing/attack attempts due to absent audit logging of failed claim/unpack/verification attempts, resulting in an attacker able to repeatedly probe message-correlation and sender-spoofing vectors without detection
Evidence: packages/core/src/vta/tsp-inbound.ts:110-118
throw new VtaClientError("e.p.msg.unauthorized", `tsp inbound: unpack failed for claimed sender ${claimedSender}: ${(err as Error).message}`);
Attack Scenario:
- Attacker sends repeated crafted or malformed TSP frames attempting to trigger STRIDE-1 (threadId collision), STRIDE-4 (VID spoofing), or general fuzzing of the unpack/claims logic.
- Each failed attempt returns false from claims() or throws a VtaClientError from unpackInboundTsp, both of which are handled purely as control flow (return/throw) with no security event logged (no counter, no structured audit log entry referencing claimedSender, frame hash, or failure reason).
- Because no persistent record of failed verification attempts exists, the wallet operator/user has no way to detect that they were targeted or to attribute a later successful compromise (if any control gap is later exploited) to a specific prior probing campaign.
- Attacker can iterate attack payloads indefinitely without triggering any alerting, rate-limiting response, or incident-response trigger.
🔎 Threat Clue: Derived from COMP-001, COMP-003 via EP-002, EP-003, EP-005
- Data Flows: TSP-frame-verification
Preconditions: No SIEM/logging integration currently captures rejected TSP frame events, Attacker has sustained access to inject frames on the shared connection
Existing Controls: Errors are surfaced as thrown VtaClientError objects to the immediate caller, so failures are not entirely silent to the calling code path
Recommended Mitigations: Add structured security audit logging for every rejected claim, unpack failure, and sender-resolution failure, including claimedSender, timestamp, and failure category • Implement anomaly detection/alerting on repeated verification failures from the same mediator session • Retain logs per applicable data-retention and compliance requirements
⚪ STRIDE-8: Race Condition Between Waiter Registration and Frame Arrival in awaitTspFrame
| Field | Detail |
|---|---|
| Category | Denial of Service, Tampering |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 6.3 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-362 |
| CAPEC | CAPEC-26 |
| OWASP | A04:2021 - Insecure Design |
Description: awaitTspFrame/sendBinary pairing in didcomm/index.ts allows a TOCTOU race due to reliance on synchronous call-ordering convention rather than atomic registration, resulting in a frame arriving between waiter registration and send potentially being lost if the two are not strictly synchronous in all call sites
Evidence: packages/core/src/didcomm/index.ts:540-544
awaitTspFrame(timeoutMs: number, claims: TspFrameClaim): Promise<Uint8Array>;
Attack Scenario:
- The documented contract requires callers to call awaitTspFrame(timeoutMs, claims) synchronously immediately before sendBinary(bytes), so 'no frame can arrive between them' per the code comment in didcomm/index.ts.
- This is a caller-discipline convention, not an enforced invariant of the API — nothing in the type system or runtime prevents a caller (current or future) from awaiting something (e.g., an async key-resolution step) between registering the waiter and calling sendBinary.
- If a future code change or a different consumer of MediatorConnection introduces an await between these two calls, an adversarial mediator that can induce out-of-order delivery, or simply an unlucky race with a fast-replying VTA (in a hypothetical alternate transport), could theoretically have a reply-like frame arrive and be missed if registration order assumptions are violated.
- More concretely today: if
packForVta(called before sendAndAwaitReply in tsp-channel.ts's send()) becomes slow/async in a future refactor, an attacker who can trigger extremely fast VTA responses (or who controls the VTA) could send a frame that arrives before the waiter is registered, causing it to be treated as unclaimed and routed to onInboundTsp instead of resolving the pending send(), silently stalling that request until timeout.
🔎 Threat Clue: Derived from COMP-001 via EP-002
- Data Flows: waiter-registration-send
Preconditions: A future code change violates the synchronous-registration-then-send discipline, Attacker or a fast-responding malicious VTA can induce a race window
Existing Controls: Current implementation in tsp-channel.ts appears to maintain synchronous ordering as documented • Explicit code comments warn future maintainers about the ordering requirement
Recommended Mitigations: Refactor the API so registration and send are atomic (e.g., a single combined sendAndAwaitReply-style call that manages ordering internally, as tsp-mediator-transport already does at a higher layer) rather than relying on caller discipline • Add a runtime assertion/test that fails fast if send occurs asynchronously relative to waiter registration • Add regression tests specifically targeting this ordering invariant
⚪ STRIDE-9: Unbounded tspWaiters Growth Enables Memory Exhaustion DoS
| Field | Detail |
|---|---|
| Category | Denial of Service |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.9 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-770 |
| CAPEC | CAPEC-125 |
| OWASP | A04:2021 - Insecure Design |
Description: tspWaiters array in connectMediatorSession allows unbounded memory growth due to absence of a maximum outstanding-request cap, resulting in memory exhaustion if a compromised or buggy caller (or an attacker able to trigger many concurrent send() calls) issues requests faster than replies/timeouts retire them
Evidence: packages/core/src/didcomm/index.ts:672-679
const tspWaiters: Array<{ resolve: (b: Uint8Array) => void; reject: (e: Error) => void; claims: TspFrameClaim; timer: ReturnType<typeof setTimeout>; }> = [];
Attack Scenario:
- Attacker with the ability to trigger wallet-side send() calls (e.g., via a malicious dApp/website integration that the extension exposes an API to, per packages/extension/src/offscreen.ts) issues a very high volume of concurrent Trust-Task requests.
- Each call to send() in tsp-channel.ts creates a new waiter object pushed onto the shared tspWaiters array in didcomm/index.ts with no upper bound check.
- Because timeouts are typically tens of seconds and each request retains an unresolved Promise, closures, and buffers until timeout/reply, an attacker issuing requests faster than the timeout period causes tspWaiters to grow unbounded.
- Combined with STRIDE-2 (O(waiters × frames) cost in onTspFrame), memory and CPU both degrade, eventually crashing the extension's background/offscreen context or making the wallet unresponsive to legitimate user-initiated Trust-Tasks.
🔎 Threat Clue: Derived from COMP-001, COMP-002 via EP-004
- Data Flows: outstanding-request-tracking
Preconditions: A caller-facing API surface (browser extension) allows a webpage or attacker to trigger many concurrent send() calls, No application-level rate limiting or concurrency cap upstream of connectMediatorSession
Existing Controls: Per-request timeout eventually retires each waiter • Single WebSocket per holder DID limits some parallelism
Recommended Mitigations: Enforce a maximum number of concurrent outstanding tspWaiters per connection, rejecting new send() calls beyond the cap with a clear backpressure error • Add global request-rate limiting at the extension's message-passing boundary (offscreen.ts) before requests reach connectMediatorSession • Monitor and alert on abnormal growth of pending request counts
⚪ STRIDE-10: Type Confusion on Trust-Task Document Fields Bypassing Shape Validation
| Field | Detail |
|---|---|
| Category | Tampering |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.1 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-843,CWE-20 |
| CAPEC | CAPEC-153 |
| OWASP | A03:2021 - Injection |
Description: unpackInboundTsp document parsing allows type confusion due to reliance on TypeScript type assertions (as Record<string, unknown>, as typeof doc) rather than runtime schema validation of the decrypted payload, resulting in downstream consumers receiving malformed id/type/body fields that pass superficial checks but violate expected invariants
Evidence: packages/core/src/vta/tsp-inbound.ts:131-140
doc = JSON.parse(fromUtf8.decode(opened.payload)) as Record<string, unknown>; const id = typeof doc.id === "string" ? doc.id : undefined; if (!id || typeof doc.type !== "string") {
Attack Scenario:
- Attacker controlling/compromising the VTA (or exploiting STRIDE-4-style sender confusion) crafts a Trust-Task document payload with unexpected field types, e.g.,
idas a string that is empty, extremely long, or contains control characters, orbodyas an array instead of an object. - unpackInboundTsp in tsp-inbound.ts does
JSON.parse(fromUtf8.decode(opened.payload)) as Record<string, unknown>— a compile-time-only type assertion providing zero runtime guarantee. - The subsequent check
const id = typeof doc.id === "string" ? doc.id : undefined; if (!id || typeof doc.type !== "string")only validates presence and primitive type ofid/type, notbody's shape, nor bounds/format ofid(e.g., could be a 10MB string). - A downstream consumer (visible truncated at 'Not a Trust-Task document...') that assumes
bodyis a well-formed object per the Trust-Task schema may throw unhandled exceptions, misrender consent-prompt UI, or (in the worst case) pass attacker-controlled strings into a rendering context in offscreen.ts without further sanitization. - If offscreen.ts renders
bodyfields directly into a user-consent UI without escaping, this could enable UI-redressing or injection into the consent-prompt display, misleading the user about what they are approving.
🔎 Threat Clue: Derived from COMP-003 via EP-005
- Data Flows: trust-task-document-parsing
Preconditions: Attacker achieves sender authentication (compromised VTA) or exploits an upstream spoofing threat, Downstream UI/consumer code does not perform additional schema validation or output encoding on doc.body fields
Existing Controls: Basic presence/type check on id and type fields • Data-Integrity proof verification is documented as occurring in parseTaskConsentRequest downstream (not shown in this excerpt)
Recommended Mitigations: Apply a strict JSON schema validator (the codebase already depends on @cfworker/json-schema) to the decrypted Trust-Task document immediately after parsing, before any field is trusted • Bound the size/length of id, type, and body fields • Ensure any UI rendering of body content uses context-appropriate output encoding to prevent injection into the consent prompt
⚪ STRIDE-11: Test Coverage Gap Enabling Regression of Correlation Fix
| Field | Detail |
|---|---|
| Category | Repudiation, Tampering |
| Severity | Low |
| Likelihood | Possible |
| CVSS | 3.7 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-1120 |
| CAPEC | CAPEC-124 |
| OWASP | A04:2021 - Insecure Design |
Description: tsp.channel.mjs, tsp.inbound.mjs, and tsp.mediator-transport.mjs test files allow silent regression of the claim/threadId correlation fix due to unverified test coverage (contents not present in analysis scope), resulting in a future refactor potentially reintroducing the original FIFO message-confusion vulnerability without detection
Evidence: packages/core/tests/tsp.channel.mjs:N/A
N/A - file contents not provided in analysis scope
Attack Scenario:
- This PR's core security property (STRIDE-1 mitigation: threadId-bound claim predicate replacing FIFO matching) is only as durable as its test coverage.
- The test files packages/core/tests/tsp.channel.mjs, tsp.inbound.mjs, and tsp.mediator-transport.mjs are referenced in the file tree but their contents were not available for analysis, so it cannot be confirmed whether they assert the specific adversarial scenario (a push arriving mid-request must not be consumed as that request's reply).
- If these tests only cover the happy path (single request, single reply) and not the interleaved push-during-request adversarial case, a future contributor refactoring tsp-channel.ts or didcomm/index.ts could reintroduce FIFO-style shortcuts without any CI failure.
- The original vulnerability (documented extensively in code comments as the motivation for this PR) would silently return, and no automated signal would catch it before release.
🔎 Threat Clue: Derived from COMP-001, COMP-002 via EP-001, EP-002, EP-004
- Data Flows: ci-test-coverage
Preconditions: Test suite lacks an explicit adversarial interleaving test case, Future code changes are made without manual security review of this specific logic
Existing Controls: Extensive inline documentation explains the security rationale, providing human-readable guardrails for reviewers • Test files exist and are presumably run in CI (though content/coverage unconfirmed)
Recommended Mitigations: Add and verify an explicit test case simulating a task-consent push arriving while a send() request is outstanding, asserting the push is NOT consumed as the reply • Add mutation testing or code coverage gates specifically on tsp-channel.ts claims logic and didcomm/index.ts onTspFrame loop • Require security review sign-off for any future changes to these specific files via CODEOWNERS
⚪ STRIDE-12: Handler Replacement Semantics in onInboundTsp Enable Silent Handler Displacement
| Field | Detail |
|---|---|
| Category | Tampering, Repudiation |
| Severity | Low |
| Likelihood | Unlikely |
| CVSS | 4.4 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-696 |
| CAPEC | CAPEC-464 |
| OWASP | A04:2021 - Insecure Design |
Description: onInboundTsp registration in didcomm/index.ts allows silent handler displacement due to the documented 'replaces any previously-registered handler' semantics with no warning or multi-handler support, resulting in a legitimate handler being unintentionally overwritten and inbound task-consent/step-up messages being routed to the wrong (or no) logic if two components both attempt registration
Evidence: packages/core/src/didcomm/index.ts:760-764
onInboundTsp: (handler) => { inboundTspHandler = handler; },
Attack Scenario:
- Two independent code paths within the extension (e.g., a legacy handler during a migration, or two race-condition-prone initialization routines in offscreen.ts) both call onInboundTsp(handler) against the same MediatorConnection instance.
- Per the documented contract ('Replaces any previously-registered handler'), the second registration silently overwrites the first with no error, warning, or composition mechanism.
- If the first handler was responsible for critical logic (e.g., a security-hardened consent-prompt renderer) and the second is a less-privileged or debug/test handler left in accidentally, all subsequent task-consent and step-up pushes are routed to the wrong logic.
- This could result in task-consent requests being silently dropped, logged insecurely, or displayed without proper verification context, depending on what the overwriting handler does — with no signal to developers or operators that displacement occurred.
🔎 Threat Clue: Derived from COMP-001 via EP-003
- Data Flows: handler-registration
Preconditions: Multiple initialization paths or a buggy re-initialization sequence call onInboundTsp more than once, No runtime warning/guard against re-registration
Existing Controls: Single WebSocket per holder DID architecture limits the scope of the connection object • TypeScript interface documents intended single-handler semantics
Recommended Mitigations: Log a warning when onInboundTsp is called while a handler is already registered • Consider supporting explicit handler composition/chaining instead of silent replacement • Add a defensive assertion in development/test builds that flags double-registration as likely a bug
🍝 PASTA Threat Model
Application Purpose
A browser extension wallet implementing the Trust Spanning Protocol (TSP) and DIDComm to mediate cryptographically-verified Trust-Task exchanges (consent requests, step-up authentication) between a holder identity and a Verifiable Trust Agent (VTA), enabling secure decentralized identity and consent workflows.
Inherent Risks
- The wallet operates over a shared, mediator-relayed WebSocket carrying both request/reply traffic and executor-initiated pushes, creating inherent message-correlation complexity.
- Cryptographic key resolution for both DIDComm and TSP peers depends on external/network-based identity resolution infrastructure outside this codebase's direct control.
- The extension surfaces security-critical consent prompts to end users, making UI-layer trust and correct data provenance essential to the overall security guarantee.
Objectives
Risk: Treat any message-correlation ambiguity as a high-priority security defect given its potential to affect consent-flow integrity.; Treat unauthenticated network-triggered resource consumption (key resolution, unpack attempts) as a denial-of-service risk to be bounded.
Business: Provide a trustworthy browser-based wallet for decentralized identity and Trust-Task consent workflows.; Maintain interoperability with the broader OpenVTC ecosystem (VTA, mediator, executor components).
Security: Prevent an executor-initiated push from being misattributed as the reply to an unrelated pending request.; Ensure only cryptographically authenticated senders can influence wallet consent-prompt content or request resolution.
Financial: Avoid costs associated with security incidents, regulatory fines, or reputational damage from a wallet compromise.; Minimize support/engineering cost from message-correlation bugs causing stuck or duplicated user prompts.
Compliance: Maintain auditability of consent-granting flows sufficient to support non-repudiation obligations under relevant digital-identity regulations.; Align cryptographic verification practices with expected DID/TSP interoperability specifications referenced in code comments (SPEC §7.2).
Functional: Correctly correlate TSP replies to outstanding requests without confusing them with executor-initiated pushes.; Cryptographically verify all inbound TSP frames before surfacing them to any consumer or user interface.
Operational: Ensure reliable delivery and acknowledgment semantics (ack-before-durable-storage) across mediator redelivery scenarios.; Support concurrent Trust-Task requests without resource exhaustion or stalled sessions.
Business Impact Analysis (3)
BIA-1: Trust-Task Consent Request Processing (Critical)
End-to-end flow in which a VTA sends a task-consent or step-up request over TSP, the wallet cryptographically verifies and de-duplicates it, and presents it to the user for approval or denial.
MTD: 00 days 04:00 hours | RTO: 00 days 01:00 hours | RPO: 00 days 00:15 hours
- Stakeholders: End Users / Executor Services / Mediator Operators / VTA Operators / Wallet/Extension Maintainers
- Dependencies: @openvtc/vti-didcomm-js library / @openvtc/vti-tsp-js library / Mediator WebSocket infrastructure / VID/DID key-resolution service
- Disruptions: Message-correlation bug causes a push to be consumed as an unrelated request's reply / Ack-before-persist handler failure causes lost or duplicated consent prompts / Mediator or VTA compromise leading to forged/replayed frames
- Impacts: User approves or denies the wrong Trust-Task due to message confusion / Loss of user trust in wallet reliability and correctness / Potential unauthorized action execution if consent is misattributed / Regulatory exposure if non-repudiation guarantees are undermined
BIA-2: TSP Reply Correlation for Outbound Requests (High)
Flow in which the wallet sends a Trust-Task request to the VTA over TSP and must correctly match the returning reply frame to the originating request among other concurrent traffic.
MTD: 00 days 08:00 hours | RTO: 00 days 02:00 hours | RPO: 00 days 00:30 hours
- Stakeholders: End Users / VTA Operators / Wallet/Extension Maintainers
- Dependencies: TspChannel.send implementation / Mediator WebSocket infrastructure / @noble/curves cryptographic primitives
- Disruptions: Claim predicate misfires due to threadId collision or type confusion / Denial of service from flooding of bogus frames during claim evaluation
- Impacts: Incorrect reply data processed as authoritative for a pending request / Stalled or timed-out requests degrading wallet usability / Resource exhaustion under adversarial frame flooding
BIA-3: Dependency and Build Pipeline Integrity (Medium)
Process of maintaining and upgrading core cryptographic and messaging dependencies (@openvtc/vti-didcomm-js, @openvtc/vti-tsp-js) that underpin the security guarantees of the entire messaging layer.
MTD: 07 days 00:00 hours | RTO: 01 days 00:00 hours | RPO: 01 days 00:00 hours
- Stakeholders: Wallet/Extension Maintainers / OpenVTC Ecosystem Package Maintainers
- Dependencies: npm registry / package-lock.json integrity pinning / CI/CD pipeline
- Disruptions: Supply-chain compromise of an @OpenVTC scoped package / Unintentional regression of a documented ordering/timing contract in a dependency update
- Impacts: Silent breakage of the security guarantees this PR introduces / Potential distribution of a compromised wallet build to end users
Technical Scope
Roles (2): RO-1 Wallet Holder · RO-2 VTA/Executor
Actors (3): AC-1 End User · AC-2 VTA Service · AC-3 Mediator Service
Entry Points (5): EP-1 connectMediatorSession Entry · EP-2 awaitTspFrame Entry · EP-3 onInboundTsp Entry · EP-4 TspChannel.send Entry · EP-5 unpackInboundTsp Entry
Threat Actors (3): TA-1 Malicious/Compromised Mediator Operator · TA-2 Malicious/Compromised VTA Operator · TA-3 Network-Positioned Attacker (MITM)
Infrastructure (1): IF-1 Browser Extension Runtime
Trust Boundaries (3): TB-1 Mediator WebSocket Boundary · TB-2 VTA/Executor Trust Boundary · TB-3 Extension-Internal Boundary
External Entities (2): EE-1 Mediator Service · EE-2 Verifiable Trust Agent (VTA)
System Components (4): SC-1 connectMediatorSession / MediatorConnection · SC-2 TspChannel · SC-3 tsp-inbound (unpackInboundTsp) · SC-4 Browser Extension Offscreen Context
Resources And Assets (3): RA-1 Holder Encryption Private Key · RA-2 Trust-Task Document Payload · RA-3 Outstanding Request Waiter State (tspWaiters)
Technologies And Dependencies (3): TD-1 @openvtc/vti-didcomm-js · TD-2 @openvtc/vti-tsp-js · TD-3 @noble/curves
Use Cases (2)
- Trust-Task Request/Reply Exchange: The wallet holder's application sends a Trust-Task request over TSP to the VTA and awaits the cryptographically-verified reply correlated by threadId, while remaining able to distinguish an unrelated
- Executor-Initiated Task-Consent Push Handling: The VTA proactively pushes a task-consent or step-up request over TSP to the wallet; the wallet verifies the sender cryptographically, shapes the frame into a DIDComm-like message, and surfaces it to
📋 Risk Registry (6)
| ID | Title | Severity | Residual | Priority | Effort |
|---|---|---|---|---|---|
| RISK-001 | Message correlation logic could still misattribute a forged or replayed frame as an authentic reply under specific edge conditions (id predictability, replay). | High | Medium | Short-Term | Low |
| RISK-002 | Cleartext sender VID is used to trigger costly key-resolution operations before cryptographic proof, exposing a DoS/SSRF-adjacent surface. | High | Medium | Short-Term | Medium |
| RISK-003 | Ack-before-persist ordering guarantee (R1.6) is a documented convention rather than an enforced/tested invariant, risking silent regression via dependency drift or handler bugs. | Medium | Low | Medium-Term | Medium |
| RISK-004 | Unbounded resource consumption (waiters array growth, per-frame O(n) unpack attempts) creates an availability risk under adversarial or buggy high-concurrency conditions. | Medium | Low | Medium-Term | Medium |
| RISK-005 | Insufficient runtime schema validation and audit logging on decrypted Trust-Task documents and rejected frames reduces defense-in-depth and forensic capability. | Medium | Low | Short-Term | Low |
| RISK-006 | Test coverage for the specific adversarial interleaving scenario (push during outstanding request) is unverified, risking silent regression of the core security fix. | Low | Low | Medium-Term | Low |
⚔️ Attack Scenarios (3)
SC-2: TspChannel
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. Threat Actors"]
direction LR
TA2@{ shape: rect, label: "TA-2: Malicious/Compromised VTA Operator<br><i>Manipulate consent-prompt content or reply data</i>" }
TA1@{ shape: rect, label: "TA-1: Malicious/Compromised Mediator Operator<br><i>Intercept, replay, or manipulate frames</i>" }
end
subgraph SL2["2. Threats"]
direction LR
S1@{ shape: rect, label: "STRIDE-1: Claim Predicate Bypass via Malformed threadId<br><i>High / Possible</i>" }
S6@{ shape: rect, label: "STRIDE-6: Missing Replay Protection on threadId Correlation<br><i>Medium / Possible</i>" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
C593@{ shape: rect, label: "CAPEC-593: Session Hijacking" }
C60@{ shape: rect, label: "CAPEC-60: Reusing Session IDs" }
end
subgraph SL4["4. Weaknesses"]
direction LR
W345@{ shape: rect, label: "CWE-345: Insufficient Verification of Data Authenticity" }
W294@{ shape: rect, label: "CWE-294: Authentication Bypass by Capture-replay" }
end
subgraph SL5["5. System Component"]
direction LR
SC2@{ shape: rect, label: "SC-2: TspChannel" }
end
SC2 --> W345
W345 --> C593
C593 --> S1
S1 --> TA2
SC2 --> W294
W294 --> C60
C60 --> S6
S6 --> TA1
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:#FFA500, stroke-width:2px
linkStyle 5 stroke:#FFA500, stroke-width:2px
linkStyle 6 stroke:#FFA500, stroke-width:2px
linkStyle 7 stroke:#FFA500, stroke-width:2px
SC-1: connectMediatorSession / MediatorConnection
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. Threat Actors"]
direction LR
TA1@{ shape: rect, label: "TA-1: Malicious/Compromised Mediator Operator<br><i>Induce resource exhaustion via frame flooding</i>" }
TA3@{ shape: rect, label: "TA-3: Network-Positioned Attacker (MITM)<br><i>Degrade availability via volumetric attacks</i>" }
end
subgraph SL2["2. Threats"]
direction LR
S2@{ shape: rect, label: "STRIDE-2: Claim Predicate Exception Swallowing<br><i>Medium / Likely</i>" }
S9@{ shape: rect, label: "STRIDE-9: Unbounded tspWaiters Growth<br><i>Medium / Possible</i>" }
S3@{ shape: rect, label: "STRIDE-3: Ack-Before-Persistence Handler Throw<br><i>Medium / Possible</i>" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
C125@{ shape: rect, label: "CAPEC-125: Flooding" }
C26@{ shape: rect, label: "CAPEC-26: Leveraging Race Conditions" }
end
subgraph SL4["4. Weaknesses"]
direction LR
W405@{ shape: rect, label: "CWE-405: Asymmetric Resource Consumption" }
W770@{ shape: rect, label: "CWE-770: Allocation of Resources Without Limits" }
W362@{ shape: rect, label: "CWE-362: Concurrent Execution Using Shared Resource" }
end
subgraph SL5["5. System Component"]
direction LR
SC1@{ shape: rect, label: "SC-1: connectMediatorSession / MediatorConnection" }
end
SC1 --> W405
W405 --> C125
C125 --> S2
S2 --> TA1
SC1 --> W770
W770 --> C125
C125 --> S9
S9 --> TA3
SC1 --> W362
W362 --> C26
C26 --> S3
S3 --> TA1
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
linkStyle 6 stroke:#FFA500, stroke-width:2px
linkStyle 7 stroke:#FFA500, stroke-width:2px
linkStyle 8 stroke:#FFA500, stroke-width:2px
linkStyle 9 stroke:#FFA500, stroke-width:2px
SC-3: tsp-inbound (unpackInboundTsp)
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. Threat Actors"]
direction LR
TA2@{ shape: rect, label: "TA-2: Malicious/Compromised VTA Operator<br><i>Manipulate consent-prompt content</i>" }
TA3@{ shape: rect, label: "TA-3: Network-Positioned Attacker (MITM)<br><i>Probe resolver for SSRF/DoS</i>" }
end
subgraph SL2["2. Threats"]
direction LR
S4@{ shape: rect, label: "STRIDE-4: Sender VID Spoofing During Key Resolution<br><i>High / Possible</i>" }
S10@{ shape: rect, label: "STRIDE-10: Type Confusion on Trust-Task Document Fields<br><i>Medium / Possible</i>" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
C141@{ shape: rect, label: "CAPEC-141: Cache Poisoning" }
C153@{ shape: rect, label: "CAPEC-153: Input Data Manipulation" }
end
subgraph SL4["4. Weaknesses"]
direction LR
W290@{ shape: rect, label: "CWE-290: Authentication Bypass by Spoofing" }
W843@{ shape: rect, label: "CWE-843: Type Confusion" }
end
subgraph SL5["5. System Component"]
direction LR
SC3@{ shape: rect, label: "SC-3: tsp-inbound (unpackInboundTsp)" }
end
SC3 --> W290
W290 --> C141
C141 --> S4
S4 --> TA3
SC3 --> W843
W843 --> C153
C153 --> S10
S10 --> TA2
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:#FFA500, stroke-width:2px
linkStyle 5 stroke:#FFA500, stroke-width:2px
linkStyle 6 stroke:#FFA500, stroke-width:2px
linkStyle 7 stroke:#FFA500, stroke-width:2px
📊 Risk Summary
Total Threats: 12
By Severity: Low: 3 · High: 2 · Medium: 7
By Category: Unknown: 12
🎯 Attack Surface
Kill Chain 1: A malicious or compromised VTA (TA-2) that can produce cryptographically valid frames (via key compromise or a supply-chain-compromised VTA deployment) crafts a threadId that collides with a currently outstanding request's envelope.id (STRIDE-1), causing TspChannel.send (SC-2) to accept a forged or stale reply (STRIDE-6) as authoritative; this reply is then parsed by parseTrustTaskReply and, if surfaced to a consent-prompt UI without independent re-verification, could steer the end user toward approving an action based on falsified data — chaining a message-correlation weakness directly into a human-trust compromise. Kill Chain 2: A network-positioned attacker or malicious mediator (TA-1, TA-3) floods the shared WebSocket with malformed or arbitrary-VID-claiming TSP frames; each frame forces onTspFrame (SC-1) to iterate every outstanding waiter and invoke expensive unpack() operations (STRIDE-2), while simultaneously triggering unpackInboundTsp's resolveSender calls for the executor-push path (STRIDE-4), and combined with unbounded tspWaiters growth from a burst of legitimate-looking concurrent requests (STRIDE-9), the wallet's event loop and resolver quota are driven toward exhaustion — a purely volumetric attack chain requiring no cryptographic compromise at all, only network position. Kill Chain 3: A dependency-level compromise or unreviewed minor-version update of @openvtc/vti-didcomm-js (STRIDE-5) silently regresses the R1.6 ack-before-persist ordering guar
🛡️ Risk Mitigation Strategy
Priority 1: Harden the cryptographic correlation logic in TspChannel and the connection layer by enforcing strict type checking on threadId comparisons, adopting cryptographically random and collision-resistant envelope ids, and adding freshness/nonce validation to close the replay and predictability gaps identified in STRIDE-1 and STRIDE-6; these are low-effort, high-value changes directly protecting the consent-integrity guarantee this PR establishes and should be treated as immediate follow-up commits to this same PR rather than deferred work. Priority 2: Bound resource consumption across the messaging layer by capping concurrent outstanding tspWaiters, adding a cheap pre-filter before invoking unpack() in the claim-evaluation loop, and rate-limiting or caching resolveSender lookups in tsp-inbound.ts, directly addressing the volumetric denial-of-service surface spanning STRIDE-2, STRIDE-4, and STRIDE-9; these changes are medium effort and should be scheduled for the short-term roadmap given the wallet's exposure to a shared, multi-tenant mediator infrastructure it does not control. Priority 3: Close the defense-in-depth and supply-chain gaps by applying strict JSON schema validation to all decrypted Trust-Task documents before any field is trusted, adding structured audit logging for every rejected or failed verification attempt, pinning exact versions for security-critical dependencies with documented ordering contracts, and adding an explicit adversarial interleaving test case to CI; these medium-term investments do not block the current PR's release but are essential to ensuring the security property it introduces remains durable against both future code changes and supply-chain drift.
Generated by Agentic Sec — Threat Model & Affect Analysis Agent
📊 Summary & findings
| ✅ Confirmed | |
|---|---|
| 0 | 2 |
Must-Review-By-Human (2)
- 🟠 Detect Insecure Websocket (2 occurrences)
- 🔵 Unsafe Formatstring (2 occurrences)
The asymmetry
The wallet's inbox was DIDComm-only, and precisely so: the VTA accepts a
task-consent/decisionback over TSP (tsp_inbound.rs), whiledidcomm_bridge.rsis the only thing that pushes a consent or step-up request. Decisions went up over anything; prompts came down over one transport.Client-side that asymmetry was enforced by a discard:
// else: straggler with no outstanding request — discard.onTspFramewas a FIFO waiter queue, so an executor-initiated request arriving over TSP had nowhere to go. A silently lost consent prompt is a gated action that never got its human check (R7.2), and it never reachedputPendingInboundeither, so persist-before-ack did not cover it.Three things had to change together.
1. Correlation, before anything else
FIFO was sound only while replies were the only thing on the socket. Once pushes share it, a consent request landing mid-vault-op would be handed to that op's waiter and parsed as its reply — while the push itself vanished. Strictly worse than dropping it.
awaitTspFramenow takes aclaimspredicate and offers each frame to the outstanding requests in turn. The connection layer holds no TSP keys, so only the caller can tell a reply from a push:TspChannelunpacks with the VTA keys it addressed and matches the Trust-TaskthreadId— which threads to the requestidexactly as DIDComm'sthid ?? iddoes, and as the VTA'srespond_withsets it (thread_id = self.thread_id.or(self.id)). A frame nothing claims is unsolicited by definition.One existing behaviour moves. A reply sealed by the wrong sender used to be unpacked and then refused with
unauthorized; it is now simply not claimed. On a shared socket that is the right reading — a frame from someone else is not an error for this request — and refusing it there would let any peer with socket access fail an unrelated in-flight operation. The security property is unchanged and now sits where it belongs: the frame falls through tounpackInboundTsp, which resolves the claimed sender's own keys and fails the unpack. The test asserts the property in its new location rather than deleting it.2. Verification at the boundary
unpackInboundTsp(vta/tsp-inbound.ts) turns a sealed frame into a verified message.A TSP frame names its sender in cleartext CESR — that is how the mediator routes without keys — so the VID is read only to look up which keys to try.
unpackthen verifies the Ed25519 signature and the HPKE-Auth sender-binding against them, and the proven sender is cross-checked against the claim. Reading a claim and believing it are different acts, and only the first happens here. Same discipline asfetchAgentNametreating a redirect target as a candidate.3. Adaptation, honestly
Over DIDComm a request arrives as a binding envelope whose
bodyis the Trust-Task document; over TSP the plaintext is the document. The two converge the moment the document is in hand, because everything security-bearing downstream reads the document — the proof check, the enrolled-executor check, the §7.2 item 11 dedup claim. Nothing trusts the envelope for anything.The adapted message uses the document's own
id, which §7.2 item 11 says to key on anyway. That makes the TSP path structurally closer to the spec than the DIDComm one, wheremessage.idis the sender's transport id and dedup has to reach past it intobody.Both inboxes are wired — the worker's and the approver's, each as its own identity.
A frame that fails verification throws, which withholds the mediator's ack and leaves it queued. Right for a transient failure (an unreachable DID document); harmless for a permanent one, since an unverifiable frame is refused again on every redelivery and reaches no human either way.
Tests
tsp.inbound.mjs(6, new) — the happy path pins the adapted shape; the other five are refusals, which matter more here: a frame naming a sender whose keys do not verify it, a sender that will not resolve, a non-JSON payload, a payload that is not a Trust-Task document, and bytes that are not a TSP envelope.tsp.channel.mjs— the simulator now honours the claim contract and threads its replies as the real VTA does, so it exercises the correlation instead of just compiling against it. Adds a mis-threaded-reply case: the document is legitimately sealed by the VTA but threaded to another request, which is what a push looked like under FIFO.tsp.mediator-transport.mjs— a guard that sending without a predicate is refused pre-send (e.client.unsupported, so aVtaSessionmoves on rather than treating it as a possibly-applied mutation), and nothing reaches the VTA.npm run lint/build/testgreen — 604 tests. Layering and entry-point guards pass;dist/background.jsis still a single bundle with no dynamicimport().Depends on
vti-didcomm-js ^0.7.0 (OpenVTC/vti-didcomm-js#11, published), which acks TSP frames after awaiting their consumer. Without it there is no ack to order against, and persist-before-ack does not hold for this path.
Next
The VTA still pushes over DIDComm. Making it prefer TSP is the following change, and this is the half that has to land first — a receiver that works before a sender that relies on it.
Not live-tested
No mediator + VTA in this environment. The correlation change touches the proven TSP request/reply path, so first run against
glenn-vtais the real check — a vault op over TSP still round-tripping is the thing to watch.