Skip to content

fix: read the granted notice at the envelope shape the VTA sends - #153

Merged
stormer78 merged 1 commit into
mainfrom
fix/consent-granted-envelope
Aug 31, 2026
Merged

fix: read the granted notice at the envelope shape the VTA sends#153
stormer78 merged 1 commit into
mainfrom
fix/consent-granted-envelope

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

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

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>
@stormer78
stormer78 merged commit 1e5e988 into main Aug 31, 2026
3 checks passed
@stormer78
stormer78 deleted the fix/consent-granted-envelope branch August 31, 2026 14:42
@affinidi-appsecurity-bot

affinidi-appsecurity-bot commented Aug 31, 2026

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

2 AI-confirmed issues.

Mandatory to check: 🔒 Security Code Review Report

Details

🛡️ Security Code Review Report — PR #153

Field Value
Repository OpenVTC/vta-browser-plugin
Branch fix/consent-granted-envelopemain
Validated 2026-09-05
Scan ID b3d6ae8b
Validator AI Security Validation Agent

🗺️ Scan Coverage

Modules scanned: 1 · with findings: 1 · files: 2 · findings: 7

Module Files scanned Findings
packages/core 2 7

⚖️ Cross-Finding Reconciliation

1 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:

  • packages/core/src/inbound/task-consent.ts (CWE-345) — verdicts: duplicate, validated
    • duplicate: Unsigned task-consent/granted notice accepted without integrity/authenticity verification
    • validated: Unauthenticated task-consent/granted notice accepted without Data-Integrity proof

Executive Summary

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

🔒 Security Issues

Confirmed Vulnerabilities (2)

🟡 Unauthenticated task-consent/granted notice accepted without Data-Integrity proof

Field Detail
Severity MEDIUM
Location packages/core/src/inbound/task-consent.ts:1
Finding ID github_pr-34ec795818bd
CWE CWE-345
OWASP A08:2021 - Software and Data Integrity Failures
Detection Source threat_model

🧠 AI Triage:

  • Triaged severity: MEDIUM
  • The vulnerable code conclusively skips authenticity verification when doc.issuer is absent/non-string, confirming the flaw exists and is reachable via inbound message processing in production. However, exploitation requires an attacker to control or manipulate the DIDComm transport to strip/omit the from field or otherwise inject an unauthenticated envelope — a non-trivial precondition not equivalent to a simple network-facing unauthenticated endpoint. No CVE/EPSS/exploit maturity data exists (first-party logic flaw), scanner confidence is only 55%, and impact is scoped to forged consent triggering unwanted resubmission rather than direct data exfiltration or RCE. This keeps it at medium: real and worth fixing, but conditional on transport-level compromise and lacking confirmed exploit evidence or broad blast radius.
  • Composite score: 5.4
  • Environment: production

📝 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 issuer field, both of which are tolerated when absent.

🌱 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: packages/core/src/inbound/task-consent.ts:1

if (typeof doc.issuer === "string" && doc.issuer !== expectedVtaDid) return null;
  const digest = doc.payload?.payloadDigest;
  return typeof digest === "string" ? { payloadDigest: digest } : null;

🎯 Attack Scenario:

An attacker who can inject or replay a DIDComm message without a from field (e.g., via a compromised or misconfigured transport that drops sender authentication) can forge a task-consent/granted notice with an arbitrary payloadDigest, tricking the requester's page into believing consent was granted and firing the consentgranted event to auto re-submit.

Also flagged at this location (same code, other weakness framings): CWE-20 — Tolerated missing sender/issuer weakens spoofing protection on consent-granted notice

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 55%
  • AI Validation Evidence: EVIDENCE FOUND: In packages/core/src/inbound/task-consent.ts, parseTaskConsentGranted checks 'if (typeof doc.issuer === "string" && doc.issuer !== expectedVtaDid) return null;' and 'const digest = doc.payload?.payloadDigest; return typeof digest === "string" ? { payloadDigest: digest } : null;' — the function accepts the notice based only on type-shape and optional issuer/sender matching, with no signature or Data-Integrity proof verification, unlike parseTaskConsentRequest which calls verifyTru
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.


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

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

🛡️ Threat Model & Affect Analysis — PR #153

Field Value
Repository OpenVTC/vta-browser-plugin
Branch fix/consent-granted-envelopemain
Generated 2026-09-05

ℹ️ 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

Fixes 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
Types: bugfix, security-adjacent, test

⚠️ Security Implications

⚪ Fix restores intended fail-safe notification path without weakening the authoritative security gate

Fix restores intended fail-safe notification path without weakening the authoritative security gate

Action: Confirm via monitoring/telemetry post-deploy that the notification path is now firing correctly in production, and that no downstream logic assumed it would never fire.

🟡 Previously-dead code path reactivated: tolerant-when-absent identity checks now have live exposure

Previously-dead code path reactivated: tolerant-when-absent identity checks now have live exposure

Action: Enforce that the DIDComm transport layer always surfaces an authenticated sender for this message class and reject envelopes lacking one at the dispatcher level, rather than tolerating absence in the application parser. Add anomaly logging when both from and issuer are absent.

🔵 Unsigned payloadDigest continues to lack cryptographic integrity protection

Unsigned payloadDigest continues to lack cryptographic integrity protection

Action: Independently audit the downstream digest-comparison and single-use grant-check logic for exact, constant-time matching and confirm it is not solely reliant on this parser's weak identity checks.

🔵 Unchecked type assertion (as cast) on untrusted nested message body

Unchecked type assertion (as cast) on untrusted nested message body

Action: Adopt a runtime schema validator (zod/io-ts) for all inbound DIDComm message bodies and ensure the message dispatcher wraps parsing in try/catch to isolate failures per-message.

🧩 Affected Components

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:

  1. 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).
  2. Attacker crafts a message where message.from is omitted and message.body.issuer is omitted, matching the 'tolerated when absent' branches in parseTaskConsentGranted (task-consent.ts lines implementing if (from && from !== expectedVtaDid) return null; and if (typeof doc.issuer === "string" && doc.issuer !== expectedVtaDid) return null;).
  3. Attacker sets message.type = TRUST_TASK_ENVELOPE_TYPE and message.body.type = TASK_CONSENT_GRANTED_TYPE to pass both gating checks.
  4. Attacker supplies an arbitrary payloadDigest value matching or guessing the requester's outstanding approval digest under message.body.payload.payloadDigest.
  5. Function returns { payloadDigest } non-null, causing the caller to fire a consentgranted event.
  6. Requester's page treats this as a legitimate nudge and attempts to auto-replay its pinned re-submit, even though no real approval occurred.
  7. 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:

  1. Attacker intercepts or spoofs a DIDComm message routed to the requester (see STRIDE-1 preconditions for transport access).
  2. Attacker crafts message.body.payload.payloadDigest as 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;).
  3. The function returns this attacker-controlled digest to the caller as if it were authentic.
  4. 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.
  5. 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:

  1. 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.
  2. 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').
  3. The requester's page, which deliberately implements no polling fallback (to avoid reopening the wallet's un-skippable confirm dialog), never receives the consentgranted event.
  4. 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.
  5. 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.
  6. 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:

  1. A dispute arises about whether the VTA actually sent a task-consent/granted notice at a specific time.
  2. Because from and issuer are 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.
  3. 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.
  4. 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:

  1. Attacker or malformed peer sends a DIDComm message where message.body is a non-null, non-object primitive coerced improperly, or doc.payload is set to a non-object value (e.g., a string, array, or null prototype object).
  2. 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.
  3. doc.payload?.payloadDigest relies on optional chaining, which is safe against payload being null/undefined, but if payload is e.g. a string or array, .payloadDigest access simply returns undefined — so this particular code path degrades gracefully.
  4. 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:

  1. Attacker positioned to observe or inject DIDComm traffic (e.g., malicious mediator relay) sends or observes a task-consent/granted envelope.
  2. 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.
  3. 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.
  4. 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
Loading

📊 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 ⚠️ Must-Review-By-Human
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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants