fix: read the granted notice at the envelope shape the VTA sends - #153
Conversation
Approving a task no longer auto-publishes on the requester's page. The
operator approves on their device, returns to the site, and the banner
still reads "this will publish automatically the moment you approve" —
until they press the manual "Publish now" fallback.
`parseTaskConsentGranted` was reading the pre-spec wire shape, in two ways
that are each independently fatal:
- It compared `message.type` against the *task* type. Over DIDComm that
member is the envelope type, so the check never matched and the function
returned `null` on its first line for every notice ever sent.
- It read `message.body.payloadDigest`. The VTA sends a full Trust Task
document as the body, so the digest is at `body.payload.payloadDigest`.
The VTA moved this notice from a bare `{status, payloadDigest, taskType}`
body to a `task-consent/granted/0.1` document inside a
`TRUST_TASK_ENVELOPE_TYPE` envelope. `parseTaskConsentRequest`, three
hundred lines up the same file, was migrated with it. This was not.
Nothing failed loudly, which is why it survived. The requester's page
listens for the `consentgranted` event to replay its pinned re-submit, and
deliberately runs no timer poll for re-submitting — a blind retry loop
would reopen the wallet's un-skippable confirm on every tick. So a dropped
notice is indistinguishable from an approver who has not answered yet.
The digests were never the problem: the VTA puts `wire_digest` in both the
`consentRequired` refusal the page holds and the granted notice, so the
match succeeds as soon as the event actually fires.
The in-band `issuer` now gets the same treatment as the transport sender —
checked when present, tolerated when absent. The notice is unsigned by
design, so that is a cheap filter, not an authentication; the page's digest
match remains the guard.
Tests: the five that covered this asserted the pre-spec form, so they
passed throughout while the feature was dead. They now build the fixture
the way `push_granted` emits it (vta-service
`trust_tasks/consent_request.rs`), and three of them fail against the old
parser. Added: an envelope carrying a different task is ignored, an
impostor in-band issuer is rejected, and the pre-spec bare body is refused
outright — pinning one wire form so a later edit cannot restore the
dual-shape tolerance that would hide this again.
Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
🛡️ AI Agentic Security Code Review2 AI-confirmed issues. Mandatory to check: 🔒 Security Code Review Report Details🛡️ Security Code Review Report — PR #153
🗺️ Scan CoverageModules scanned: 1 · with findings: 1 · files: 2 · findings: 7
⚖️ Cross-Finding Reconciliation1 root-cause cluster(s) received divergent verdicts across findings that share the same file + weakness. These are surfaced (not auto-resolved) — a reviewer should confirm the verdicts are intentionally different, not an artifact of findings being judged in isolation:
Executive Summary
🔒 Security IssuesConfirmed Vulnerabilities (2)🟡 Unauthenticated task-consent/granted notice accepted without Data-Integrity proof
🧠 AI Triage:
📝 Description: The parser accepts a plaintext, unsigned Trust Task document as authoritative enough to trigger the requester's UI to auto re-submit, relying only on the DIDComm transport sender or an in-band 🌱 Root Cause: No Data-Integrity proof or signature is verified on the granted notice; authentication is reduced to an optional sender/issuer string comparison that is skipped entirely when the field is missing. 🔎 Evidence: 🎯 Attack Scenario: An attacker who can inject or replay a DIDComm message without a
Generated by Agentic Sec — AI Security Validation Agent Details🛡️ Threat Model & Affect Analysis — PR #153
📋 Affect AnalysisChange SummaryFixes parseTaskConsentGranted() in the VTA browser plugin's DIDComm inbound parser, which previously matched a pre-spec bare-body wire shape that the VTA service stopped sending once the task-consent/granted notice gained a Trust Task envelope. This caused the function to silently return null for every real notice ever sent in production. The fix aligns the parser with the actual envelope shape (type=TRUST_TASK_ENVELOPE_TYPE, body=Trust Task document, digest under body.payload.payloadDigest), adds an in-band issuer check, and expands the test suite with envelope-shaped fixtures plus impersonation/regression coverage. Diff: +86 / -27 lines
|
| Component | Impact | Change | What Changed |
|---|---|---|---|
| Task Consent Notice Parser (parseTaskConsentGranted) | medium | modified | Migrated from parsing a pre-spec bare-body DIDComm message to parsing a full Trust Task document nested inside a DIDComm envelope, matching |
| Requester Page Auto-Republish UX (referenced, not in diff) | medium | modified | Not directly changed by this diff, but its behavior changes as a consequence: it will now actually receive consentgranted events instead o |
📁 File Classifications
packages/core/src/inbound/task-consent.ts
- Type: security
packages/core/tests/inbound.task-consent.mjs
- Type: test
🛡️ STRIDE Threat Model
Identified Threats (6)
🟡 STRIDE-1: Sender Spoofing via Missing DIDComm Authcrypt Sender in parseTaskConsentGranted
| Field | Detail |
|---|---|
| Category | Spoofing, Tampering |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.3 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-290,CWE-345 |
| CAPEC | CAPEC-194,CAPEC-151 |
| OWASP | A07:2021 - Identification and Authentication Failures |
Description: parseTaskConsentGranted in task-consent.ts allows sender spoofing due to tolerating a missing from field and unsigned issuer field, resulting in a spoofed granted-notice being accepted as originating from the enrolled VTA
Evidence: packages/core/src/inbound/task-consent.ts:1-40
const from = typeof message.from === "string" ? message.from : null;
if (from && from !== expectedVtaDid) return null;
...
if (typeof doc.issuer === "string" && doc.issuer !== expectedVtaDid) return null;
Attack Scenario:
- Attacker gains ability to inject a DIDComm message to the requester's inbound channel (e.g. malicious relay, compromised mediator, or transport without enforced authcrypt).
- Attacker crafts a message where
message.fromis omitted andmessage.body.issueris omitted, matching the 'tolerated when absent' branches inparseTaskConsentGranted(task-consent.ts lines implementingif (from && from !== expectedVtaDid) return null;andif (typeof doc.issuer === "string" && doc.issuer !== expectedVtaDid) return null;). - Attacker sets
message.type = TRUST_TASK_ENVELOPE_TYPEandmessage.body.type = TASK_CONSENT_GRANTED_TYPEto pass both gating checks. - Attacker supplies an arbitrary
payloadDigestvalue matching or guessing the requester's outstanding approval digest undermessage.body.payload.payloadDigest. - Function returns
{ payloadDigest }non-null, causing the caller to fire aconsentgrantedevent. - Requester's page treats this as a legitimate nudge and attempts to auto-replay its pinned re-submit, even though no real approval occurred.
- Although the final gate is the single-use grant check at re-submit time (out of scope), premature triggering can cause unwanted re-submit attempts, UI state confusion, or race conditions with legitimate flows.
Preconditions: Attacker can inject or spoof a DIDComm message reaching this parser (e.g., compromised or malicious mediator, transport-layer authcrypt bypass, or lack of enforced encryption at the DIDComm layer)., The from transport field and issuer field are both absent or attacker-controlled at the point this function is invoked.
Existing Controls: Design explicitly treats the notice as non-authoritative and non-load-bearing. • The ultimate authorization gate is a separate single-use grant check performed at re-submit time. • Sender check from !== expectedVtaDid when present.
Recommended Mitigations: Enforce that DIDComm authcrypt transport always surfaces a sender at this layer and reject envelopes lacking one. • Add Data-Integrity proof or signature validation on the granted notice despite it being 'non-load-bearing', since it is exposed as an unauthenticated network input. • Log and alert on notices with a missing from or issuer field for anomaly detection. • Rate-limit or debounce triggering of the consentgranted event to prevent notification flooding from spoofed messages.
🟡 STRIDE-2: Unsigned Notice Tampering via Unverified payloadDigest in parseTaskConsentGranted
| Field | Detail |
|---|---|
| Category | Tampering |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-345,CWE-20 |
| CAPEC | CAPEC-153,CAPEC-668 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
Description: body.payload.payloadDigest in TRUST_TASK_ENVELOPE_TYPE processing in task-consent.ts allows notice content tampering due to the notice lacking a Data-Integrity proof by design, resulting in an attacker-controlled digest value flowing into the consentgranted event without cryptographic verification
Evidence: packages/core/src/inbound/task-consent.ts:30-40
const digest = doc.payload?.payloadDigest;
return typeof digest === "string" ? { payloadDigest: digest } : null;
Attack Scenario:
- Attacker intercepts or spoofs a DIDComm message routed to the requester (see STRIDE-1 preconditions for transport access).
- Attacker crafts
message.body.payload.payloadDigestas an arbitrary string value, since the code only type-checks it (typeof digest === "string") with no cryptographic binding to the real Trust Task document (task-consent.ts, final lines:const digest = doc.payload?.payloadDigest; return typeof digest === "string" ? { payloadDigest: digest } : null;). - The function returns this attacker-controlled digest to the caller as if it were authentic.
- Downstream page logic uses this digest to match against 'its outstanding approval' — if that comparison logic has any weakness (e.g., partial match, case-insensitivity, or logging the digest insecurely), tampered data could cause unintended re-submit triggering or information leakage via error/logging paths.
- Even though the design intends this only as a 'nudge', any downstream code that trusts the returned digest without additional validation inherits the tampering risk.
Preconditions: Attacker can inject a DIDComm message reaching parseTaskConsentGranted., Downstream digest-comparison logic has any exploitable weakness (assumed present but out of scope of provided files).
Existing Controls: Explicit design intent that this is a non-load-bearing nudge and the real gate is elsewhere. • Downstream page is documented to re-check the digest against its outstanding approval before acting.
Recommended Mitigations: Require the DIDComm envelope to be delivered over an authenticated and integrity-protected channel (authcrypt) universally, not tolerated as optional. • Add a Data-Integrity proof to the Trust Task document even though currently deemed unnecessary, to defend against transport-layer downgrade or mediator compromise. • Ensure downstream digest comparison uses constant-time, exact string equality and does not log the raw digest in plaintext.
🟡 STRIDE-3: Denial of Service via Silent Parse Failure Masking Real Approval Notices
| Field | Detail |
|---|---|
| Category | Denial of Service |
| Severity | Medium |
| Likelihood | Likely |
| CVSS | 4.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-755,CWE-703 |
| CAPEC | CAPEC-227 |
| OWASP | A04:2021 - Insecure Design |
Description: parseTaskConsentGranted's type-shape mismatch (pre-fix) in task-consent.ts allows silent notification suppression due to schema drift between the sender's wire format and the parser's expected shape, resulting in requesters missing legitimate consent-granted events and relying on manual fallback
Evidence: packages/core/src/inbound/task-consent.ts:18-19
if (message.type !== TRUST_TASK_ENVELOPE_TYPE) return null;
Attack Scenario:
- VTA service (vta-service trust_tasks/consent_request.rs) changes wire format to wrap the granted notice in a Trust Task envelope (TRUST_TASK_ENVELOPE_TYPE) as documented in the migration comment.
- Prior version of parseTaskConsentGranted checks
message.type !== TASK_CONSENT_GRANTED_TYPE, which never matches the new envelope type, causing the function to return null on every legitimate message (as documented: 'this returned null on every notice ever sent'). - The requester's page, which deliberately implements no polling fallback (to avoid reopening the wallet's un-skippable confirm dialog), never receives the
consentgrantedevent. - The operator experiences indefinite silent failure indistinguishable from 'approver has not answered yet', degrading usability to the point of requiring a manual fallback button for every single transaction.
- This represents a systemic availability failure caused purely by an internal schema/version mismatch, unrelated to attacker activity, but demonstrates how any future silent schema drift (e.g., a v0.2 envelope) would reproduce full DoS of the auto-publish UX with no detection mechanism.
- An attacker who can influence protocol versioning or trigger a downstream schema change (e.g., malicious VTA operator or supply-chain compromise of vta-service) could deliberately reintroduce this exact condition to degrade UX and push users toward risky manual workarounds.
Preconditions: A schema/version mismatch exists between the sender (VTA service) and this parser., No automated fallback/polling exists to detect and recover from silent parse failures., No telemetry alerts on a sustained null-return rate from parseTaskConsentGranted.
Existing Controls: Fixed in this PR by aligning the parser to the current envelope shape. • Manual fallback button exists as an operator-facing mitigation. • Regression test added ('rejects the pre-spec bare body') to pin the parser to one wire form.
Recommended Mitigations: Add telemetry/logging when parseTaskConsentGranted returns null for a message whose top-level type matches TRUST_TASK_ENVELOPE_TYPE, to detect future schema drift. • Introduce explicit protocol version negotiation between VTA and requester to fail loudly instead of silently on shape mismatch. • Add contract/integration tests against the actual vta-service wire format (not just local fixtures) to catch drift pre-release.
🔵 STRIDE-4: Repudiation of Notice Origin due to Unsigned and Optionally-Absent Sender/Issuer Fields
| Field | Detail |
|---|---|
| Category | Repudiation |
| Severity | Low |
| Likelihood | Unlikely |
| 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-593 |
| OWASP | A09:2021 - Security Logging and Monitoring Failures |
Description: parseTaskConsentGranted in task-consent.ts allows repudiation of message origin due to accepting notices with no cryptographically verifiable sender or issuer, resulting in inability to prove or disprove which party actually sent a given granted notice
Evidence: packages/core/src/inbound/task-consent.ts:20-23
const from = typeof message.from === "string" ? message.from : null;
if (from && from !== expectedVtaDid) return null;
Attack Scenario:
- A dispute arises about whether the VTA actually sent a task-consent/granted notice at a specific time.
- Because
fromandissuerare both optional/tolerated-when-absent (task-consent.ts), and the notice is explicitly unsigned ('no Data-Integrity proof'), there is no cryptographic evidence binding the notice content to a specific sender. - Neither party can conclusively prove notice authenticity or non-authenticity after the fact, undermining audit/forensic investigations if the nudge is later implicated in an unintended re-submit or user complaint.
- This is amplified if downstream logging does not separately capture DIDComm transport-layer metadata (e.g., raw envelope headers) that could otherwise substantiate origin outside this function's scope.
Preconditions: A dispute or incident investigation requires proof of notice origin., Transport-layer authcrypt metadata is not independently logged elsewhere.
Existing Controls: Design explicitly accepts this tradeoff since the notice is 'non-load-bearing' and the real gate is elsewhere. • Comment states this is 'a cheap filter and not an authentication.'
Recommended Mitigations: Log full DIDComm transport metadata (including raw authcrypt sender key) at the point of message receipt, independent of this parser's business logic. • Consider requiring the notice to be signed if it will ever be used as audit evidence. • Document explicitly in incident response runbooks that this notice type is non-authoritative and unsuitable as sole evidence.
🔵 STRIDE-5: Type Confusion via Untyped Record Input in parseTaskConsentGranted
| Field | Detail |
|---|---|
| Category | Tampering, Denial of Service |
| Severity | Low |
| Likelihood | Unlikely |
| CVSS | 2.3 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 | None |
| CWE | CWE-843,CWE-20 |
| CAPEC | CAPEC-153 |
| OWASP | A03:2021 - Injection |
Description: message parameter typed as Record<string, unknown> in parseTaskConsentGranted allows malformed/nested object injection due to weak runtime type validation of nested fields, resulting in potential unexpected behavior if doc.payload is a non-object type
Evidence: packages/core/src/inbound/task-consent.ts:25-30
const doc = (message.body ?? {}) as {
type?: unknown;
issuer?: unknown;
payload?: { payloadDigest?: unknown };
};
Attack Scenario:
- Attacker or malformed peer sends a DIDComm message where
message.bodyis a non-null, non-object primitive coerced improperly, ordoc.payloadis set to a non-object value (e.g., a string, array, or null prototype object). - Code does
const doc = (message.body ?? {}) as {...}which performs an unchecked type assertion (as) rather than runtime validation, meaning TypeScript's compile-time typing offers no protection against a malicious/malformed runtime shape. doc.payload?.payloadDigestrelies on optional chaining, which is safe againstpayloadbeingnull/undefined, but ifpayloadis e.g. a string or array,.payloadDigestaccess simply returnsundefined— so this particular code path degrades gracefully.- However, this establishes a broader pattern risk: any future maintenance that dereferences additional nested fields without similar optional chaining/type guards could introduce a runtime TypeError, and because this function is invoked directly from untrusted network input, a crash here could destabilize the message-processing pipeline (potential DoS if unhandled exceptions propagate).
Preconditions: Attacker-controlled DIDComm message reaches this parser with deeply malformed nested structures., Future code changes add unguarded property access on nested attacker-controlled fields.
Existing Controls: Optional chaining (doc.payload?.payloadDigest) prevents crashes for the current field access pattern. • typeof digest === "string" guard before use.
Recommended Mitigations: Replace unchecked as type assertions with a runtime schema validator (e.g., zod, io-ts) for all inbound DIDComm message bodies. • Wrap the top-level message-processing dispatch in a try/catch to prevent a single malformed message from crashing the DIDComm listener process. • Add fuzz testing against parseTaskConsentGranted with malformed/adversarial JSON shapes.
🔵 STRIDE-6: Information Disclosure via payloadDigest Exposure to Unauthenticated DIDComm Peers
| Field | Detail |
|---|---|
| Category | Information Disclosure |
| Severity | Low |
| Likelihood | Unlikely |
| CVSS | 3.7 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | None |
| CWE | CWE-200 |
| CAPEC | CAPEC-117 |
| OWASP | A02:2021 - Cryptographic Failures |
Description: body.payload.payloadDigest returned from parseTaskConsentGranted allows salted digest disclosure due to no authentication being required to have the parser process and forward the value, resulting in confirmation of an outstanding approval's existence to a network-position attacker
Evidence: packages/core/src/inbound/task-consent.ts:29-31
payload?: { payloadDigest?: unknown };
Attack Scenario:
- Attacker positioned to observe or inject DIDComm traffic (e.g., malicious mediator relay) sends or observes a task-consent/granted envelope.
- If the transport is not fully encrypted end-to-end (authcrypt tolerance being optional per the 'tolerated when absent' sender check), an attacker could potentially learn that a specific payloadDigest is associated with an active outstanding approval, since the digest is application data that flows through this parsing layer.
- Although the digest is described as 'salted', repeated observation of the same digest across sessions or correlation with other side channels could allow an attacker to fingerprint approval activity patterns for a given requester/VTA relationship.
- This is a lower-severity confirmation/inference risk rather than direct credential theft, but represents metadata leakage about consent-flow timing and state.
Preconditions: Attacker has network-level visibility into DIDComm traffic (compromised mediator, or transport without full E2E encryption)., Digest salting is weak or reused across sessions such that correlation is feasible.
Existing Controls: Digest is described as 'salted'. • DIDComm transport is expected to use authcrypt for confidentiality in the intended deployment.
Recommended Mitigations: Ensure DIDComm transport enforces authcrypt/anoncrypt encryption end-to-end without tolerance for plaintext fallback. • Rotate salts per-session to prevent cross-session correlation of digests. • Avoid logging the payloadDigest value in any persistent or centralized logging system.
🍝 PASTA Threat Model
Application Purpose
The VTA browser plugin implements a decentralized identity/trust-task system using DIDComm messaging, where a VTA (Verified Trust Agent) service notifies a requester's browser page when a data-sharing task has been consented to, enabling automatic re-submission of a pending action instead of manual polling or intervention.
Inherent Risks
- The notice-parsing layer is intentionally non-authoritative, meaning any weakness here shifts security burden entirely onto an out-of-scope re-submit grant check.
- DIDComm sender/issuer authentication is 'tolerated when absent' by design, creating a latent trust gap if the transport layer's encryption guarantees are ever weakened or misconfigured.
- Schema drift between the VTA service (Rust) and the browser plugin (TypeScript) previously caused a total, silent functional failure, indicating fragile cross-language/cross-repo contract enforcement.
- No polling fallback exists by deliberate design, meaning any future silent parsing failure has no automatic recovery path.
Objectives
Risk: Accept residual risk of an unsigned, best-effort nudge given compensating controls at re-submit time.; Treat silent parsing failures as an availability/UX risk rather than a security-critical one, but monitor for recurrence.
Business: Provide a seamless auto-publish user experience for consented trust tasks without requiring manual polling or repeated wallet confirmations.
Security: Ensure the non-authoritative nudge cannot be leveraged to trigger unintended actions beyond a benign re-submit prompt.; Preserve the integrity of the single-use grant check as the sole load-bearing authorization gate.
Financial: Minimize support costs and user drop-off caused by unclear or stalled consent flows.
Compliance: Align with DIDComm messaging protocol specifications for envelope and trust-task document structures.
Functional: Correctly parse and act on task-consent/granted notices matching the current VTA wire format.
Operational: Maintain synchronized wire-format contracts between vta-service (Rust) and vta-browser-plugin (TypeScript) across releases.
Business Impact Analysis (1)
BIA-1: Automated Consent-Granted Notification and Re-submit Trigger (Medium)
The VTA sends a DIDComm envelope notifying the requester's page that consent was granted, which the page parses to automatically replay a pinned re-submit action instead of requiring manual polling or intervention.
MTD: 01 days 00:00 hours | RTO: 00 days 04:00 hours | RPO: 00 days 00:00 hours
- Stakeholders: Browser Plugin Users / Requester Application Operators / VTA Service Operators
- Dependencies: DIDComm Messaging Transport / Trust Task Document Schema / VTA Service (Rust) Consent Request Module / Single-Use Grant Check (Re-submit Gate)
- Disruptions: Schema drift between VTA service and browser plugin parser causing silent notice-parsing failure / Malicious or compromised DIDComm mediator relay spoofing or dropping notices / Transport-layer authcrypt misconfiguration removing sender authenticity guarantees
- Impacts: Degraded user experience requiring manual fallback for every transaction / Potential confusion or mistrust if spoofed nudges trigger unexpected re-submit attempts / Support burden increase due to unexplained stalled consent flows
Technical Scope
Roles (2): RO-1 Requester/Operator · RO-2 VTA Service Operator
Actors (2): AC-1 Browser Plugin Page · AC-2 VTA Service
Entry Points (2): EP-1 parseTaskConsentGranted Function Call · EP-2 Inbound DIDComm Envelope Delivery
Threat Actors (2): TA-1 Malicious DIDComm Mediator Operator · TA-2 Malicious or Compromised VTA-Adjacent Actor
Infrastructure (1): IF-1 Browser Extension Runtime
Trust Boundaries (2): TB-1 DIDComm Transport Boundary · TB-2 Browser Plugin Internal Boundary
External Entities (2): EE-1 Enrolled VTA Service · EE-2 Malicious DIDComm Peer or Mediator
System Components (3): SC-1 Task Consent Notice Parser · SC-2 Requester Page Re-submit Handler · SC-3 VTA Service Consent Notification Publisher
Resources And Assets (2): RA-1 Salted Payload Digest · RA-2 Expected VTA DID
Technologies And Dependencies (2): TD-1 DIDComm Messaging Protocol · TD-2 Node.js Built-in Test Runner
Use Cases (1)
- Automated Trust Task Re-submit on Consent Granted: The VTA service sends a DIDComm envelope notifying the requester's browser plugin that a Trust Task consent was granted, prompting the page to automatically replay its pinned re-submit action instead
📋 Risk Registry (3)
| ID | Title | Severity | Residual | Priority | Effort |
|---|---|---|---|---|---|
| RISK-001 | Spoofed or unsigned consent-granted notices could trigger premature or unwanted re-submit attempts on the requester's page. | Medium | Low | Short-Term | Medium |
| RISK-002 | Silent schema drift between VTA service and browser plugin can cause total, undetected loss of the auto-publish notification feature. | Medium | Low | Medium-Term | Medium |
| RISK-003 | Lack of cryptographic non-repudiation for consent-granted notices complicates dispute resolution and incident forensics. | Low | Low | Long-Term | Low |
⚔️ Attack Scenarios (1)
SC-1: Task Consent Notice Parser
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. Threat Actors"]
direction LR
TA1@{ shape: rect, label: "TA-1: Malicious DIDComm Mediator Operator<br><i>Intercept or manipulate consent-flow messages</i>" }
TA2@{ shape: rect, label: "TA-2: Malicious or Compromised VTA-Adjacent Actor<br><i>Undermine trust via spurious/dropped notifications</i>" }
end
subgraph SL2["2. Threats"]
direction LR
S1@{ shape: rect, label: "STRIDE-1: Sender Spoofing via Missing DIDComm Authcrypt Sender<br><i>Medium / Possible</i>" }
S2@{ shape: rect, label: "STRIDE-2: Unsigned Notice Tampering via Unverified payloadDigest<br><i>Medium / Possible</i>" }
S3@{ shape: rect, label: "STRIDE-3: Denial of Service via Silent Parse Failure<br><i>Medium / Likely</i>" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
C1@{ shape: rect, label: "CAPEC-194: Fake the Source of Data" }
C2@{ shape: rect, label: "CAPEC-153: Input Data Manipulation" }
C3@{ shape: rect, label: "CAPEC-227: Sustained Client Engagement" }
end
subgraph SL4["4. Weaknesses"]
direction LR
W1@{ shape: rect, label: "CWE-290: Authentication Bypass by Spoofing" }
W2@{ shape: rect, label: "CWE-345: Insufficient Verification of Data Authenticity" }
W3@{ shape: rect, label: "CWE-755: Improper Handling of Exceptional Conditions" }
end
subgraph SL5["5. System Component"]
direction LR
SC1@{ shape: rect, label: "SC-1: Task Consent Notice Parser" }
end
TA1 --> S1
TA1 --> S2
TA2 --> S3
S1 --> C1
S2 --> C2
S3 --> C3
C1 --> W1
C2 --> W2
C3 --> W3
W1 --> SC1
W2 --> SC1
W3 --> SC1
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
linkStyle 8 stroke:#FF0000,stroke-width:2px
linkStyle 9 stroke:#FF0000,stroke-width:2px
linkStyle 10 stroke:#FF0000,stroke-width:2px
linkStyle 11 stroke:#FF0000,stroke-width:2px
📊 Risk Summary
Total Threats: 6
By Severity: Low: 3 · Medium: 3
By Category: Spoofing: 1 · Tampering: 3 · Denial of Service: 2 · Repudiation: 1 · Information Disclosure: 1
🎯 Attack Surface
Kill Chain 1: An attacker positioned on the DIDComm transport path (malicious mediator, compromised relay, or misconfigured authcrypt) crafts a fabricated task-consent/granted envelope with TRUST_TASK_ENVELOPE_TYPE and a matching body.type, omitting both the transport from field and the in-band issuer field to exploit the 'tolerated when absent' logic in parseTaskConsentGranted (STRIDE-1), then supplies an arbitrary or guessed payloadDigest (STRIDE-2) to have the parser return a non-null result, triggering the requester page's consentgranted event and an unwanted re-submit replay attempt; while the ultimate single-use grant check at re-submit time is designed to block unauthorized action, this chain still enables notification-layer manipulation, UI confusion, and potential race conditions with legitimate approval flows. Kill Chain 2: Independent of active attack, a passive schema-drift condition (STRIDE-3) — such as the exact pre-fix bug this PR resolves — causes the parser to silently return null for every legitimate notice due to a type-shape mismatch between the VTA service's Rust-side wire format and the TypeScript parser's expectations; because no polling fallback exists by design (to avoid reopening the wallet's un-skippable confirm dialog), this failure is indistinguishable from a pending approval and persists until manually discovered, and a malicious or careless protocol version change on the VTA side could deliberately or accidentally reintroduce this exact denial-of-service condition against the auto-publish UX. Kill Chain 3: The combination of unchecked type assertions (as casts) on nested untrusted fields (STRIDE-5) with the unauthenticated entry point nature of this parser (EP-1, EP-2) means that any future code change adding unguarded property access on attacker-controlled nested structures could introduce a crash-based DoS vector against the message-processing pipeline, compounding the existing silent-failure risk described in Kill Chain 2.
🛡️ Risk Mitigation Strategy
Priority 1: The most urgent control gap is the absence of mandatory sender/issuer authentication at the DIDComm transport layer for this notice type; while the design rationale (non-load-bearing nudge, real gate elsewhere) is sound in principle, the 'tolerated when absent' logic in parseTaskConsentGranted creates an avoidable spoofing surface that should be closed by enforcing authcrypt sender presence at the transport dispatcher before messages ever reach this parser, and by adding anomaly logging for any notice missing both from and issuer fields. Priority 2: Given that this exact PR fixes a total, silent functional failure caused by undetected schema drift between the Rust vta-service and the TypeScript browser plugin, the highest-value systemic investment is cross-repository contract testing and version negotiation — implementing automated tests that validate the parser against the actual vta-service wire format (not just local fixtures) and adding telemetry that alerts on sustained null-return rates would have caught this regression before every historical notice silently failed. Priority 3: Longer-term, the codebase should move away from unchecked as type assertions on untrusted DIDComm message bodies toward runtime schema validation (e.g., zod/io-ts), paired with defensive try/catch boundaries around the message dispatcher, to ensure that future maintenance on this unauthenticated, network-facing entry point cannot introduce unhandled exceptions or type-confusion crashes that degrade availability of the broader messaging pipeline.
Generated by Agentic Sec — Threat Model & Affect Analysis Agent
📊 Summary & findings
| ✅ Confirmed | |
|---|---|
| 2 | 0 |
Confirmed (2)
- 🟡 Tolerated missing sender/issuer weakens spoofing protection on consent-granted notice
- 🟡 Unauthenticated task-consent/granted notice accepted without Data-Integrity proof
Approving a task no longer auto-publishes on the requester's page. The
operator approves on their device, returns to the site, and the banner
still reads "this will publish automatically the moment you approve" —
until they press the manual "Publish now" fallback.
parseTaskConsentGrantedwas reading the pre-spec wire shape, in two waysthat are each independently fatal:
message.typeagainst the task type. Over DIDComm thatmember is the envelope type, so the check never matched and the function
returned
nullon its first line for every notice ever sent.message.body.payloadDigest. The VTA sends a full Trust Taskdocument as the body, so the digest is at
body.payload.payloadDigest.The VTA moved this notice from a bare
{status, payloadDigest, taskType}body to a
task-consent/granted/0.1document inside aTRUST_TASK_ENVELOPE_TYPEenvelope.parseTaskConsentRequest, threehundred lines up the same file, was migrated with it. This was not.
Nothing failed loudly, which is why it survived. The requester's page
listens for the
consentgrantedevent to replay its pinned re-submit, anddeliberately runs no timer poll for re-submitting — a blind retry loop
would reopen the wallet's un-skippable confirm on every tick. So a dropped
notice is indistinguishable from an approver who has not answered yet.
The digests were never the problem: the VTA puts
wire_digestin both theconsentRequiredrefusal the page holds and the granted notice, so thematch succeeds as soon as the event actually fires.
The in-band
issuernow gets the same treatment as the transport sender —checked when present, tolerated when absent. The notice is unsigned by
design, so that is a cheap filter, not an authentication; the page's digest
match remains the guard.
Tests: the five that covered this asserted the pre-spec form, so they
passed throughout while the feature was dead. They now build the fixture
the way
push_grantedemits it (vta-servicetrust_tasks/consent_request.rs), and three of them fail against the oldparser. Added: an envelope carrying a different task is ignored, an
impostor in-band issuer is rejected, and the pre-spec bare body is refused
outright — pinning one wire form so a later edit cannot restore the
dual-shape tolerance that would hide this again.
Signed-off-by: Glenn Gore glenn.g@affinidi.com