fix(extension): accept both spellings of a re-cased error code - #126
Conversation
trust-tasks #279 re-cased ~200 extended error codes to the lowerCamelCase SPEC §4.10 rule 4 requires: `provision/integration:context_required` became `provision/integration:contextRequired`. Only the local part after the `:` moved; namespaces are unchanged, and retired specs were deliberately left snake_case. `onboard-view.tsx` gated the onboarding context picker on a `===` against the old spelling. Against a VTA that has taken #279 that comparison goes false and nothing else happens: no error, no crash, no log — the picker just stops appearing, and an operator with a multi-context VTA is dead-ended at Connect with a message about a context they were never offered a way to choose. Swapping the literal would move the defect rather than fix it. This wallet is on the matching side of the wire, never the declaring side, and it updates on the Chrome Web Store's schedule while the VTA updates on its own. A wallet installed today still talks to a months-old agent next year; a wallet that has not auto-updated meets an agent that took #279 this morning. Either literal is correct for exactly half the deployed fleet, so both spellings have to match. `matchesTrustTaskCode(actual, canonical)` (`trust-tasks/error-code.ts`) takes the canonical camelCase code and accepts either spelling, deriving the snake_case form rather than carrying a hand-maintained pair. Call sites read as the code the registry declares today and the compatibility lives in one place, so retiring the fold is one edit rather than a sweep — its TODO names the condition: a declared minimum vta-service floor at or above the release carrying the re-cased codes, the way #125 made 0.18.0 a floor for the dispatcher path. It is deliberately not a fuzzy compare; codes differing by more than the §4.10 re-casing stay distinct, so a rename cannot become a collision. `PROVISION_CONTEXT_REQUIRED` is exported from `provision/send.ts` in the registry's current spelling, following the `MEDIATOR_REQUIRED` precedent in `bridge-protocol.ts`. The code crosses two message-passing hops (offscreen → background → popup) and is forwarded verbatim at each: normalising in transit would make the hops need redeploying in lockstep with the agent, and would hide from the popup which side of the rename its peer is on. The other three codes this repo names — `vault/delete:versionConflict`, `vault/sign-trust-task:notSignable`, `vault/upsert:sealedSecretInvalid` — all verified against the registry, and all appear only in doc comments. Those are re-spelled to the current declaration with a note pointing at the matcher, so the next site that starts branching on one starts from the right shape. Left alone deliberately: `auth:consent_required` is a `details.reason` token, not an extended error code (the registry declares no bare `auth:` namespace), `e.p.msg.context_required` is a DIDComm problem-report code from a different scheme, and `push/register:*` is still snake_case in the registry. Seven new tests pin the dual-accept property itself, since that is what makes the deploy order safe and a comment cannot hold it: an equality that goes false raises nothing. Lint (`tsc -b`) clean across all workspaces; build clean; 546 tests passing, 0 failing (was 539). Both CI bundle guards re-run by hand and pass. **Ship this before the vta-service PR that emits the new spelling** (R3.7). Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
🛡️ AI Agentic Security Code Review3 findings need a human to review/validate. Mandatory to check: 🔒 Security Code Review Report Details🛡️ Security Code Review Report — PR #126
🗺️ Scan CoverageModules scanned: 2 · with findings: 2 · files: 11 · findings: 4
Executive Summary
🔒 Security Issues
|
| Field | Detail |
|---|---|
| Severity | MEDIUM |
| Location | packages/core/src/provision/send.ts:178 |
| Finding ID | github_pr-22eb78ec3471 |
| CWE | CWE-20, CWE-1284 |
| OWASP | A03:2021 - Injection, A08:2021 - Software and Data Integrity Failures |
| MITRE ATT&CK | T1204, T1565.002 |
| CAPEC | CAPEC-153, CAPEC-588 |
| DREAD | 5.2 |
| Reachability | 🔴 Reachable |
| Exploit Maturity | poc |
| Detection Source | skill_scan |
Summary: The wallet trusts a remote DIDComm problem-report body using only a TypeScript type assertion, with no runtime validation, allowing an attacker-controlled endpoint to inject arbitrary code/comment/args values that flow into the operator-facing onboarding UI.
📝 Description:
An attacker who controls or intercepts a DIDComm endpoint can inject arbitrary 'candidate context' strings that the wallet's onboarding UI presents to the operator as legitimate choices, potentially causing the operator to provision the wallet's identity into an attacker-chosen integration context.
🧪 Proof of Concept:
The as Partial<ProblemReportPayload> cast only affects TypeScript's static type checker; at runtime reply.body can contain any JSON shape the remote peer sends, including oversized arrays or unexpected types, and none of it is checked before being wrapped into the thrown error and eventually rendered in the UI.
if (reply.type === PROBLEM_REPORT_TYPE) {
// Throw a typed error so callers can branch on the code without
// re-parsing the message string...
const body = (reply.body ?? {}) as Partial<ProblemReportPayload>;
throw new ProvisionProblemReportError({
code: typeof body.code === "string" ? body.code : "(no code)",
// comment, args assembled similarly below (not shown)
});
}
Vulnerable lines: 178, 189
🔎 Evidence: packages/core/src/provision/send.ts:178
const body = (reply.body ?? {}) as Partial<ProblemReportPayload>;
throw new ProvisionProblemReportError({
code: typeof body.code === "string" ? body.code : "(no code)",
💥 Impact:
An attacker who controls or intercepts a DIDComm endpoint can inject arbitrary 'candidate context' strings that the wallet's onboarding UI presents to the operator as legitimate choices, potentially causing the operator to provision the wallet's identity into an attacker-chosen integration context.
Confidentiality: Low — no direct data exfiltration, but can be used to influence which context/identity the wallet provisions into, an integrity-adjacent trust decision. · Integrity: Medium — attacker-controlled strings reach the picker UI and influence operator selection, potentially causing wrong-context provisioning. · Availability: Low
🧭 Reachability:
- Network exposure: public
- Auth barrier: basic
- Attack path: EP-002 (DIDComm problem-report reply) → send.ts:178 body cast → ProvisionProblemReportError → offscreen.ts:248 catch handler → bridge-protocol.ts RuntimeOnboardConnectResponse → onboard-view.tsx candidate picker render
⚖️ Triage Factors:
| Factor | Value |
|---|---|
| Fixable | ✅ Yes |
| Exploitability | medium |
| Business impact | medium |
| Public exploit | None known |
| Environment | unknown |
Attack scenario: A malicious/compromised DIDComm VTA endpoint sends a crafted problem-report body that is never runtime-validated, letting attacker-controlled strings reach the onboarding picker UI.
🔧 Remediation:
⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.
Replacing the compile-time-only as cast with a runtime schema (zod) enforces bounds on string length, array size, and code format before the value is trusted anywhere downstream, closing the injection/DoS surface at the trust boundary.
Vulnerable code:
const body = (reply.body ?? {}) as Partial<ProblemReportPayload>;
throw new ProvisionProblemReportError({
code: typeof body.code === "string" ? body.code : "(no code)",
Secure code:
import { z } from "zod";
const ProblemReportBodySchema = z.object({
code: z.string().max(256).regex(/^[a-z0-9/_-]+:[a-zA-Z0-9]+$/),
comment: z.string().max(1024).optional(),
args: z.array(z.string().max(512)).max(20).optional(),
});
const parsed = ProblemReportBodySchema.safeParse(reply.body);
const body = parsed.success ? parsed.data : { code: "(invalid)" as const };
throw new ProvisionProblemReportError({
code: body.code ?? "(no code)",
comment: body.comment ?? "",
args: body.args ?? [],
});
Additional recommendations:
- Cap args/candidates array length and per-item length before they ever leave core.
- Add Content-Security-Policy to the extension popup to reduce impact if any candidate is ever rendered as markup.
- Log and rate-limit malformed problem-reports per DIDComm endpoint.
🔍 Validation Log
- Verdict:
⚠️ Must-Review-By-Human- Confidence: 55%
- AI Validation Evidence: EVIDENCE FOUND: In packages/core/src/provision/send.ts the reply body is coerced via
const body = (reply.body ?? {}) as Partial<ProblemReportPayload>;and then each field is individually runtime-checked withtypeof body.code === "string",typeof body.comment === "string", andArray.isArray(body.args) ? body.args.filter((a) => typeof a === "string") : []. This is field-level runtime validation, not schema-less blind trust, contrary to the finding's claim of 'type-assertion only'. EVIDENCE NOT FOUND: No zod/ajv schema validation exists, and there is no length/count cap onargs, no validation ofcodeagainst an allow-list of known codes, and no bound oncommentsize. Whether these gaps constitute a security-relevant issue depends on downstream consumption (rendered as picker candidates in onboard-view.tsx) which does perform its own matching via matchesTrustTaskCode but not content sanitization of candidates. CHANGED VS PRE-EXISTING: send.ts is directly named in the finding's evidence and is part of this MR's diff context (PROVISION_CONTEXT_REQUIRED comment discusses matchesTrustTaskCode usage introduced by this fix); the type-assertion+per-field check pattern itself predates this specific casing fix but the surrounding code was touched. VERDICT JUSTIFICATION: Since per-field runtime type checks DO exist (contradicting the claim of 'never validated at runtime'), and the impact chain (candidate rendering) is only partially confirmed, this is not a clean true positive nor a clean false positive — a human should assess whether the per-field typeof/Array.isArray checks are sufficient mitigation for CWE-20/1284.- 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.
🟡 Unsanitized attacker-influenced candidate list rendered in onboarding context picker
| Field | Detail |
|---|---|
| Severity | MEDIUM |
| Location | packages/extension/src/onboard-view.tsx:307 |
| Finding ID | github_pr-dcb1606bd741 |
| CWE | CWE-451, CWE-20 |
| OWASP | A03:2021 - Injection, A04:2021 - Insecure Design |
| MITRE ATT&CK | T1566, T1204 |
| CAPEC | CAPEC-98, CAPEC-163 |
| DREAD | 4.6 |
| Reachability | 🔴 Reachable |
| Exploit Maturity | poc |
| Detection Source | skill_scan |
Summary: The onboarding context picker renders VTA-supplied candidate strings without validating their format or origin, allowing a malicious/compromised VTA to present deceptive context options to the operator.
📝 Description:
An operator could be tricked into selecting an attacker-influenced context during onboarding, causing the wallet to establish a trust relationship (DID/context) that the attacker controls or benefits from, undermining the product's core security guarantee.
🧪 Proof of Concept:
The correctness of the code-matching (matchesTrustTaskCode) is not in question, but the trust extended to res.candidates content — sourced ultimately from an untrusted remote peer — is unconditional; no format check, length cap, or origin-authenticity check precedes rendering.
// candidates as a picker rather than bouncing the operator
// back to a re-prepare cycle. The ephemeral grant is still
// valid for its 1h TTL so picking immediately retries.
if (
matchesTrustTaskCode(res.code, PROVISION_CONTEXT_REQUIRED) &&
res.candidates &&
res.candidates.length > 0
) {
// candidates set into picker state here (not shown)
}
Vulnerable lines: 303, 320
🔎 Evidence: packages/extension/src/onboard-view.tsx:307
if (
matchesTrustTaskCode(res.code, PROVISION_CONTEXT_REQUIRED) &&
res.candidates &&
res.candidates.length > 0
) {
💥 Impact:
An operator could be tricked into selecting an attacker-influenced context during onboarding, causing the wallet to establish a trust relationship (DID/context) that the attacker controls or benefits from, undermining the product's core security guarantee.
Confidentiality: Low · Integrity: Medium — operator may select a spoofed/deceptive candidate leading to mis-provisioning of trust relationship · Availability: None
🧭 Reachability:
- Network exposure: public
- Auth barrier: basic
- Attack path: EP-002 → send.ts (body.args) → offscreen.ts (candidates) → bridge-protocol.ts → onboard-view.tsx:307 render
⚖️ Triage Factors:
| Factor | Value |
|---|---|
| Fixable | ✅ Yes |
| Exploitability | medium |
| Business impact | medium |
| Public exploit | None known |
| Environment | unknown |
Attack scenario: Attacker-influenced candidate strings from a malicious VTA reach the onboarding picker unsanitized, risking operator selection of a spoofed context.
🔧 Remediation:
⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.
Validating each candidate against expected DID syntax before rendering, capping array size, and displaying the full un-truncated identifier prevents deceptive/injected strings from being presented as trustworthy options.
Vulnerable code:
if (
matchesTrustTaskCode(res.code, PROVISION_CONTEXT_REQUIRED) &&
res.candidates &&
res.candidates.length > 0
) {
setCandidates(res.candidates);
}
Secure code:
const DID_CONTEXT_RE = /^did:[a-z0-9]+:[A-Za-z0-9._:%-]+$/;
if (
matchesTrustTaskCode(res.code, PROVISION_CONTEXT_REQUIRED) &&
res.candidates &&
res.candidates.length > 0
) {
const safeCandidates = res.candidates
.filter((c) => typeof c === "string" && c.length <= 512 && DID_CONTEXT_RE.test(c))
.slice(0, 20);
setCandidates(safeCandidates);
// Render full DID string + fingerprint, never a friendly label alone.
}
Additional recommendations:
- Add an explicit confirmation step showing raw DID + fingerprint before final provisioning commit.
- Consider signing the candidates list at the protocol layer so the wallet can verify origin authenticity, not just format.
🔍 Validation Log
- Verdict:
⚠️ Must-Review-By-Human- Confidence: 50%
- AI Validation Evidence: EVIDENCE FOUND: In onboard-view.tsx:
if (matchesTrustTaskCode(res.code, PROVISION_CONTEXT_REQUIRED) && res.candidates && res.candidates.length > 0) { setContextCandidates(res.candidates); return; }and rendering:{contextCandidates.map((ctx) => (<button key={ctx} onClick={() => void connect(ctx)} ... >{ctx}</button>))}. Candidates are rendered as plain React children ({ctx}), which React auto-escapes, mitigating classic XSS/HTML injection, but there is no DID-syntax validation or allow-listing before display, matching the finding's core claim about lack of validation/disambiguating metadata. EVIDENCE NOT FOUND: No allow-list or DID-format check onctxbefore it's used as a button label/onClick argument passed toconnect(ctx), which ultimately becomes the wirecontextfield sent back to the VTA. CHANGED VS PRE-EXISTING: onboard-view.tsx is directly modified by this MR (the matchesTrustTaskCode integration is the whole point of this fix), so this candidate-rendering code path is CHANGED/in-scope. VERDICT JUSTIFICATION: The rendering itself is XSS-safe (React escaping) but the deeper concern (spoofed/deceptive candidate content misleading the operator, CWE-451) cannot be fully confirmed or dismissed without knowing server-side candidate generation constraints — kept for human 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.
🟡 chrome.runtime.onMessage listener lacks explicit sender/origin validation (as shown in reduced snippet)
| Field | Detail |
|---|---|
| Severity | MEDIUM |
| Location | packages/extension/src/offscreen.ts:248 |
| Finding ID | github_pr-83b24c72da5c |
| CWE | CWE-346, CWE-862 |
| OWASP | A01:2021 - Broken Access Control, A07:2021 - Identification and Authentication Failures |
| MITRE ATT&CK | T1176, T1204 |
| CAPEC | CAPEC-98, CAPEC-141 |
| DREAD | 3.8 |
| Reachability | 🔴 Reachable |
| Exploit Maturity | conceptual |
| Detection Source | skill_scan |
Summary: The offscreen document's chrome.runtime.onMessage listener, as shown in the reduced source, does not visibly validate the sender before processing and responding to messages, which — if truly absent — would let other browser extensions or compromised content scripts spoof provisioning responses.
📝 Description:
Potential for a locally co-installed malicious extension to inject forged provisioning-failure/candidate responses into the wallet's trusted UI flow, steering the operator toward an attacker-controlled context — this is a SUSPECTED finding pending confirmation against the complete file.
🧪 Proof of Concept:
No sender.id check is visible in the excerpt provided; the listener callback signature includes sender but it is unused in the shown code path, which is the pattern associated with CWE-346 (Origin Validation Error) in browser extension message handlers.
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
.catch((e: unknown) => {
// Preserve the structured problem-report fields when the VTA
// replied with one. The popup branches on `code` to surface
// recovery UX (e.g. the contextRequired picker); without
// these fields the message string would have to be regex-
// parsed, which is fragile. Forwarded verbatim — the popup
// does the spelling-tolerant compare, so nothing here has to
// know which side of trust-tasks #279 the VTA is on.
if (e instanceof ProvisionProblemReportError) {
sendResponse({
ok: false,
Vulnerable lines: 248, 260
🔎 Evidence: packages/extension/src/offscreen.ts:248
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
...
.catch((e: unknown) => {
if (e instanceof ProvisionProblemReportError) {
sendResponse({
ok: false,
💥 Impact:
Potential for a locally co-installed malicious extension to inject forged provisioning-failure/candidate responses into the wallet's trusted UI flow, steering the operator toward an attacker-controlled context — this is a SUSPECTED finding pending confirmation against the complete file.
Confidentiality: Low · Integrity: Medium — forged provisioning responses could be injected into the trusted pipeline · Availability: Low
🧭 Reachability:
- Network exposure: internal
- Auth barrier: none
- Attack path: EP-005 (any extension/content script capable of chrome.runtime.sendMessage to this extension) → offscreen.ts:248 onMessage listener → sendResponse forwarded to background/popup → bridge-protocol.ts → onboard-view.tsx
⚖️ Triage Factors:
| Factor | Value |
|---|---|
| Fixable | ✅ Yes |
| Exploitability | low |
| Business impact | medium |
| Public exploit | None known |
| Environment | unknown |
Attack scenario: A co-installed malicious extension or exploited content script sends forged runtime messages to the wallet's offscreen document, which may lack sender-origin validation, to inject spoofed provisioning responses.
🔧 Remediation:
⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.
Explicitly checking sender.id === chrome.runtime.id ensures only the extension's own background/popup/offscreen contexts can trigger this listener, preventing other installed extensions or externally_connectable misconfigurations from injecting forged messages.
Vulnerable code:
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// ... handling without visible sender check ...
});
Secure code:
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (sender.id !== chrome.runtime.id) {
// Reject messages not originating from this extension's own contexts.
return false;
}
// Optionally also validate message.__proto__ shape via a schema (zod) here.
// ... existing handling ...
});
Additional recommendations:
- Audit manifest.json for externally_connectable and unnecessary permissions.
- Add a per-flow correlation nonce so unsolicited/replayed responses are ignored.
- Validate message payload shape with a runtime schema before use.
🔍 Validation Log
- Verdict:
⚠️ Must-Review-By-Human- Confidence: 40%
- AI Validation Evidence: EVIDENCE FOUND: The finding cites
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {...})in offscreen.ts at lines 248-260, but offscreen.ts source is not included in source_files (only referenced in the finding's evidence snippet, not provided in full). EVIDENCE NOT FOUND: The full offscreen.ts file was not provided in source_files, so I cannot verify or deny whether asender.id === chrome.runtime.idcheck exists elsewhere in the listener or file. CHANGED VS PRE-EXISTING: offscreen.ts is referenced but not confirmed in the MR's changed-file list from the provided data; cannot determine scope with certainty. VERDICT JUSTIFICATION: Per the rules, absence of the deciding file in provided source_files means this cannot be validated nor dismissed — must_review, consistent with the finding's own self-declared 'SUSPECTED/POTENTIAL' status.- 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 #126
| Field | Value |
|---|---|
| Repository | OpenVTC/vta-browser-plugin |
| Branch | fix/trust-task-error-code-casing → main |
| Generated | 2026-08-26 |
ℹ️ 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 wire-protocol compatibility bug: a Trust-Task extended error code registry rename (trust-tasks#279, SPEC §4.10 rule 4) re-cased ~200 error codes from snake_case to lowerCamelCase, but the wallet's onboard-view.tsx compared the VTA's provision/integration:context_required code with strict equality (===), which silently fails to match against agents on either side of the rename. The change introduces a matchesTrustTaskCode/trustTaskCodeSnakeCase compatibility shim (packages/core/src/trust-tasks/error-code.ts), a new PROVISION_CONTEXT_REQUIRED exported constant, updates doc comments across vault/vta modules to reference the new spelling and the fold, and swaps the brittle equality check in onboard-view.tsx for the tolerant comparator.
Diff: +245 / -32 lines
Types: bugfix, protocol-compatibility, test
📁 File Classifications
packages/core/src/trust-tasks/error-code.ts
- Type: security
packages/core/src/provision/send.ts
- Type: security
packages/core/src/provision/index.ts
- Type: security
packages/core/src/trust-tasks/index.ts
- Type: config
packages/core/src/vault/delete.ts
- Type: security
packages/core/src/vault/sign-trust-task.ts
- Type: security
packages/core/src/vta/protocol.ts
- Type: security
packages/core/tests/trust-tasks.error-code.mjs
- Type: test
packages/extension/src/bridge-protocol.ts
- Type: security
packages/extension/src/offscreen.ts
- Type: security
🛡️ STRIDE Threat Model
Identified Threats (11)
⚪ STRIDE-1: Unvalidated DIDComm Problem-Report Body Injection in sendProvisionIntegration
| Field | Detail |
|---|---|
| Category | Tampering, Information Disclosure |
| Severity | Medium |
| Likelihood | Likely |
| CVSS | 5.3 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Medium |
| CWE | CWE-20,CWE-1284 |
| CAPEC | CAPEC-153,CAPEC-588 |
| OWASP | A03:2021 - Injection, A08:2021 - Software and Data Integrity Failures |
Description: provision/integration reply handling in send.ts allows malformed/malicious problem-report body injection due to missing runtime schema validation (only as Partial<ProblemReportPayload> type assertion), resulting in unvalidated code, comment, args propagating to the extension UI.
Evidence: packages/core/src/provision/send.ts:178-189
const body = (reply.body ?? {}) as Partial<ProblemReportPayload>;
throw new ProvisionProblemReportError({
code: typeof body.code === "string" ? body.code : "(no code)",
Attack Scenario:
- A malicious or compromised VTA/remote DIDComm endpoint replies to a
provision/integrationrequest withreply.type === PROBLEM_REPORT_TYPE. - In
packages/core/src/provision/send.ts,const body = (reply.body ?? {}) as Partial<ProblemReportPayload>;performs a compile-time-only type assertion with no runtime validation (no zod/ajv schema check). - Attacker sets
body.codeto an arbitrary string, andbody.argsto an oversized array or attacker-controlled strings (candidate list) with no length/content limits. ProvisionProblemReportErroris thrown carrying the unvalidatedcode/comment/argsverbatim.- The error propagates through
offscreen.ts's catch handler which forwardscode/candidatesverbatim across the extension message-passing boundary tobridge-protocol.ts'sRuntimeOnboardConnectResponse. onboard-view.tsxrendersres.candidatesdirectly as picker UI options without sanitization, enabling injected strings (e.g. HTML-like or misleading identifiers) to be shown to the operator as legitimate context choices.
🔎 Threat Clue: Derived from COMP-001, COMP-006 via EP-002, EP-006
- Data Flows: VTA problem-report reply -> offscreen -> bridge-protocol -> onboard-view
Preconditions: Attacker controls or has compromised a DIDComm endpoint the wallet provisions against, No TLS-independent message integrity check beyond DIDComm envelope
Existing Controls: DIDComm envelope encryption/signing (assumed, not shown in reduced code) • TypeScript compile-time interface ProblemReportPayload
Recommended Mitigations: Add runtime schema validation (zod/ajv) for the problem-report body in send.ts before constructing ProvisionProblemReportError • Cap and sanitize args/candidates length and content before UI rendering • Encode/escape candidate strings when rendered in onboard-view.tsx
⚪ STRIDE-2: Silent Branch Failure via Strict Equality Spoofing of Trust-Task Error Codes
| Field | Detail |
|---|---|
| Category | Tampering, Denial of Service |
| Severity | Medium |
| Likelihood | Likely |
| CVSS | 5.1 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-697,CWE-354 |
| CAPEC | CAPEC-267 |
| OWASP | A04:2021 - Insecure Design |
Description: Any remaining === comparison against a Trust-Task extended error code (outside the fixed onboard-view.tsx call site) in vulnerable call sites across the codebase allows silent-branch-bypass due to a fleet permanently split across the SPEC §4.10 re-casing (trust-tasks #279), resulting in denial of the intended recovery UX/availability degradation without any raised error.
Evidence: packages/core/src/vault/delete.ts:22-30
* the maintainer rejects with `vault/delete:versionConflict` on
* mismatch... An agent that predates SPEC §4.10 rule 4 (trust-tasks #279) sends the same rejection as `vault/delete:version_conflict`; match either with `matchesTrustTaskCode` rather than `===`.
Attack Scenario:
- An operator's wallet has not auto-updated and predates the recognition of
matchesTrustTaskCodeat some call site not covered by this PR (e.g., a future or existing direct===comparison againstvault/delete:versionConflict,vault/sign-trust-task:notSignable, orvault/upsert:sealedSecretInvalid). - A VTA that has adopted trust-tasks #279 responds with the lowerCamelCase spelling (e.g.,
vault/delete:versionConflict). - If any call site (present or future) in
packages/core/src/vault/delete.tsorsign-trust-task.tsconsumers still compares with===against the snake_case constant, the comparison silently evaluates false. - No exception is raised; the code simply fails to take the intended recovery branch (e.g., version-conflict retry using
details.currentVersion, or not-signable rejection UX). - The operator observes a UI feature (retry prompt, context picker) that 'just doesn't appear', with no error message to aid diagnosis — a business-logic denial-of-service on the recovery path.
- Because this failure mode is silent, it can persist undetected in production for the entire deployment window of the mismatched wallet/VTA pair.
🔎 Threat Clue: Derived from COMP-003 via EP-003, EP-004
- Data Flows: VTA vault/delete or vault/sign-trust-task rejection -> core consumer branch logic
Preconditions: A caller in the ecosystem (wallet or agent) uses === instead of matchesTrustTaskCode against an extended error code, The peer sends the opposite-casing spelling
Existing Controls: matchesTrustTaskCode helper introduced and applied at onboard-view.tsx line ~314 • Unit tests in trust-tasks.error-code.mjs verifying both spellings match
Recommended Mitigations: Grep/lint rule to forbid direct ===/!== comparison against any string literal matching the <namespace>:<localPart> Trust-Task code pattern • Add integration/contract tests exercising each vault/provision call site against both pre- and post-#279 spellings • Centralize all error-code comparisons through matchesTrustTaskCode and consider a typed wrapper that prevents raw string comparison entirely
⚪ STRIDE-3: Code-Collision Spoofing via Loose Fuzzy Matching Regression in matchesTrustTaskCode
| Field | Detail |
|---|---|
| Category | Spoofing, Tampering |
| Severity | Low |
| Likelihood | Unlikely |
| CVSS | 3.7 CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-697 |
| CAPEC | CAPEC-141 |
| OWASP | A04:2021 - Insecure Design |
Description: matchesTrustTaskCode in error-code.ts allows future maintenance regressions toward general case-insensitive matching due to the deliberately narrow two-spelling design being fragile to well-intentioned refactors, resulting in cross-namespace or cross-code collision producing incorrect recovery UX for a semantically different rejection.
Evidence: packages/core/src/trust-tasks/error-code.ts:44-63
export function matchesTrustTaskCode(
actual: string | null | undefined,
canonical: string,
): boolean {
if (typeof actual !== "string" || actual.length === 0) return false;
return actual === canonical || actual === trustTaskCodeSnakeCase(canonical);
}
Attack Scenario:
- A future contributor unfamiliar with the design intent (documented only in comments) refactors
matchesTrustTaskCodetoward a general case-insensitive or fuzzy.toLowerCase()comparison to 'simplify' the helper. - This regression is not caught because the existing negative tests (
trust-tasks.error-code.mjs) only assert against the four known RENAMED pairs and specific negative cases, not against an exhaustive future-code fuzz corpus. - A VTA sends
vault/upsert:version_conflict(a real distinct code in thevault/upsertnamespace) while the caller checks againstvault/delete:versionConflict(a different namespace, same local part). - Under the current (correct) implementation this returns false (verified in test 'the namespace is part of the identity'), but under a regressed fuzzy implementation it could incorrectly match if the regression collapses namespace boundaries.
- The wallet takes the wrong recovery branch (e.g., displaying a version-conflict retry for a vault/upsert operation while believing it is a vault/delete rejection), potentially executing the wrong corrective action against vault state.
- This could result in mis-attributed audit trail entries or destructive vault actions taken under a mistaken rejection interpretation.
🔎 Threat Clue: Derived from COMP-003 via EP-003, EP-004
- Data Flows: Vault rejection code -> matchesTrustTaskCode -> recovery branch decision
Preconditions: A future code change to matchesTrustTaskCode removes the namespace-boundary check, No CI/lint gate enforcing the exact two-branch design invariant beyond the existing unit tests
Existing Controls: Explicit unit test 'the namespace is part of the identity' asserting non-collision • Explicit unit test 'a different code in the same namespace does not match' • Extensive code comments explaining the design rationale to deter regression
Recommended Mitigations: Add a code review checklist item / CODEOWNERS gate on error-code.ts changes • Expand test suite with property-based fuzzing across many namespace/local-part combinations • Add a runtime assertion or type-level guard preventing bare string literals from bypassing the helper
⚪ STRIDE-4: Unauthenticated chrome.runtime.onMessage Listener in Offscreen Bridge Allows Cross-Extension-Component Spoofing
| Field | Detail |
|---|---|
| Category | Spoofing, Tampering, Elevation of Privilege |
| Severity | High |
| Likelihood | Possible |
| CVSS | 7.1 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Medium |
| CWE | CWE-346,CWE-862 |
| CAPEC | CAPEC-98,CAPEC-141 |
| OWASP | A01:2021 - Broken Access Control, A07:2021 - Identification and Authentication Failures |
Description: chrome.runtime.onMessage listener in offscreen.ts allows message spoofing due to missing sender validation (no sender.id/origin check shown) on the offscreen -> background message channel, resulting in forged problem-report responses or provisioning requests being injected into the trusted extension pipeline.
Evidence: packages/extension/src/offscreen.ts:248-260
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
...
.catch((e: unknown) => {
if (e instanceof ProvisionProblemReportError) {
sendResponse({
ok: false,
Attack Scenario:
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {...})inpackages/extension/src/offscreen.tsregisters a listener without visible validation ofsender.id === chrome.runtime.idor message origin/schema.- If any other extension or a compromised content script co-located in the same browser profile can send messages matching the expected shape, it can trigger the
.catchhandler path or otherwise inject a craftedProvisionProblemReportError-shaped response. - The forged response is passed to
sendResponse({ ok: false, ... code, candidates })and forwarded tobridge-protocol.ts. onboard-view.tsxreceives the forgedcode/candidatesand, becausematchesTrustTaskCodematches on content not on source authenticity, treats the forged message as a legitimate VTA-originated context-required picker.- The operator is shown attacker-supplied 'candidates' (arbitrary strings) in the onboarding picker UI and may select a malicious candidate, causing the wallet to provision into an attacker-chosen context or DID.
- This achieves elevation of privilege over the provisioning flow purely through the extension's internal message-passing trust assumption.
🔎 Threat Clue: Derived from COMP-005 via EP-005
- Data Flows: offscreen -> background -> popup message channel
Preconditions: Attacker can register a chrome extension or exploit another extension/content-script capable of sending runtime messages to this extension's offscreen document, No sender/origin validation present in the reduced offscreen.ts snippet
Existing Controls: Chrome extension messaging is scoped by chrome.runtime.id implicitly in many configurations (not confirmed absent/present in reduced snippet) • Manifest V3 offscreen document isolation (assumed)
Recommended Mitigations: Explicitly validate sender.id === chrome.runtime.id and reject all other senders in the onMessage listener • Add a nonce/request-correlation token per provisioning flow so unsolicited responses are ignored • Validate message schema strictly before processing in offscreen.ts
⚪ STRIDE-5: Unsanitized Candidate List Rendering Enables UI Spoofing in Onboarding Picker
| Field | Detail |
|---|---|
| Category | Spoofing, Tampering |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.7 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Medium |
| CWE | CWE-451,CWE-20 |
| CAPEC | CAPEC-98,CAPEC-163 |
| OWASP | A03:2021 - Injection, A04:2021 - Insecure Design |
Description: OnboardView candidate picker in onboard-view.tsx allows deceptive/spoofed candidate rendering due to lack of visible input sanitization or allow-listing on res.candidates before display, resulting in social-engineering-driven mis-selection of a malicious provisioning context by the operator.
Evidence: packages/extension/src/onboard-view.tsx:307-320
if (
matchesTrustTaskCode(res.code, PROVISION_CONTEXT_REQUIRED) &&
res.candidates &&
res.candidates.length > 0
) {
Attack Scenario:
- A malicious or compromised VTA (or spoofed message per STRIDE-4) returns a
provision/integration:contextRequiredproblem-report withargs/candidatescontaining deceptive strings (e.g., visually similar DID identifiers, or strings crafted to look like a trusted context name). matchesTrustTaskCode(res.code, PROVISION_CONTEXT_REQUIRED)passes because the code is legitimate/correctly spelled — the vulnerability is not in the fixed comparator but in the trust placed inres.candidatescontent downstream.onboard-view.tsxrendersres.candidatesin the picker UI (per theif (... && res.candidates && res.candidates.length > 0)branch) presumably via a list of selectable options, without shown validation that entries match expected DID/context syntax.- The operator, trusting the wallet UI, selects a deceptive candidate believing it to be the correct integration context.
- The wallet proceeds to provision the ephemeral grant into the attacker-influenced context, potentially exfiltrating or misdirecting the trust relationship being established.
- Because the 1-hour ephemeral grant TTL noted in comments is still valid at selection time, the attack window is broad enough for realistic exploitation.
🔎 Threat Clue: Derived from COMP-006 via EP-006
- Data Flows: problem-report candidates -> onboard-view picker -> operator selection -> provisioning
Preconditions: Attacker controls or spoofs the VTA-side response content, No client-side validation/allow-listing of candidate identifiers before rendering
Existing Controls: 1-hour TTL limits exposure window somewhat • matchesTrustTaskCode ensures only genuine contextRequired codes trigger the picker
Recommended Mitigations: Validate each candidate against expected DID/context syntax before rendering • Display disambiguating metadata (e.g., full DID, fingerprint) rather than a bare string to reduce spoofing potential • Add explicit operator confirmation step showing the raw context identifier before final provisioning commit
⚪ STRIDE-6: Missing Sender Repudiation Protection for Vault Delete Rejection Audit Trail
| Field | Detail |
|---|---|
| Category | Repudiation |
| Severity | Low |
| Likelihood | Possible |
| CVSS | 4.0 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-778,CWE-345 |
| CAPEC | CAPEC-268 |
| OWASP | A09:2021 - Security Logging and Monitoring Failures |
Description: vault/delete reason field in delete.ts allows repudiation of destructive vault actions due to the human-readable rationale being client-supplied and unverified against a tamper-evident audit log, resulting in an agent later denying or falsifying the recorded justification for a vault entry deletion.
Evidence: packages/core/src/vault/delete.ts:25-30
/** Human-readable rationale recorded in the audit trail. */
reason?: string;
Attack Scenario:
- A caller invokes
vault/deletewith an attacker- or operator-controlledreasonstring intended to be 'recorded in the audit trail' per the doc comment indelete.ts. - No cryptographic binding (e.g., signed audit record, hash-chained log) between the
reasonstring and the invoking principal's identity is shown in the reduced code. - If the VTA's audit trail merely stores the free-text
reasonwithout linking it to a signed Trust-Task or DIDComm message envelope, the deleting party can later claim a different rationale was used, or deny having supplied a malicious/incorrect reason. - This undermines forensic reconstruction of why a sensitive vault entry (e.g., a
did-self-issuedsigning key) was deleted, especially when combined with theexpectedVersionoptimistic-concurrency check being bypassed via a version-conflict race (see STRIDE-7). - Absent non-repudiation guarantees, an insider or compromised agent can delete critical vault material and later dispute responsibility.
🔎 Threat Clue: Derived from COMP-003 via EP-003
- Data Flows: vault/delete request -> audit trail
Preconditions: Audit trail storage does not cryptographically bind reason to a signed request, Attacker has legitimate (or hijacked) credentials to issue vault/delete
Existing Controls: expectedVersion optimistic concurrency check reduces accidental double-deletion • DIDComm message signing at the transport layer (assumed, not shown)
Recommended Mitigations: Require the reason field to be embedded inside the signed DIDComm message body rather than a separately trusted parameter • Hash-chain or digitally sign each audit trail entry including the requesting principal's DID • Log the raw signed request alongside the parsed reason for later verification
⚪ STRIDE-7: TOCTOU Race in Optimistic Concurrency Version Check for Vault Delete
| Field | Detail |
|---|---|
| Category | Tampering |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.9 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-367 |
| CAPEC | CAPEC-25 |
| OWASP | A04:2021 - Insecure Design |
Description: expectedVersion optimistic-concurrency parameter in delete.ts allows time-of-check-to-time-of-use race conditions due to the version check and delete operation not being shown as atomic (client reads version, then races to submit delete before another writer updates it), resulting in unintended deletion of a vault entry that has since been legitimately modified.
Evidence: packages/core/src/vault/delete.ts:22-28
expectedVersion?: number;
/** Human-readable rationale recorded in the audit trail. */
reason?: string;
Attack Scenario:
- Two concurrent agents (or an attacker racing a legitimate operator) both read the same vault entry's current
version. - Agent A submits
vault/deletewithexpectedVersion = N. - Before the maintainer processes Agent A's request, Agent B updates the entry, bumping the version to N+1 through a legitimate
vault/upsert. - If the maintainer-side check-then-delete is not atomic (single serialized transaction) at the VTA, a race window allows Agent A's delete to be applied against stale expectations, or the rejection (
vault/delete:versionConflict) to be delayed enough that Agent A's stale request is retried and eventually succeeds against a since-changed entry. - Since this repo is the client library, an attacker crafting rapid parallel
vault/deleteandvault/upsertcalls could exploit any server-side non-atomicity to delete an entry that a concurrent operation had just intentionally preserved/updated. - Result: loss of newly-written vault material (e.g., a freshly rotated signing key) presumed safe because it 'passed' the just-prior upsert.
🔎 Threat Clue: Derived from COMP-003 via EP-003
- Data Flows: vault/delete <-> vault/upsert concurrent operations
Preconditions: Server-side (VTA) version-check-then-delete is not implemented as a single atomic transaction, Attacker or racing agent has valid credentials to issue concurrent vault operations
Existing Controls: Optimistic concurrency via expectedVersion and version_conflict/versionConflict rejection with details.currentVersion for retry
Recommended Mitigations: Ensure server-side atomic compare-and-delete transaction (out of scope of this client repo but should be verified against VTA implementation) • Client should re-fetch and re-verify version immediately before retry rather than blindly retrying with stale expectedVersion • Add idempotency keys to prevent duplicate delete execution on retry
⚪ STRIDE-8: Insufficient Restriction on Signable Entry Kind Enables Cross-Kind Signing Abuse
| Field | Detail |
|---|---|
| Category | Elevation of Privilege, Tampering |
| Severity | High |
| Likelihood | Possible |
| CVSS | 7.3 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Medium |
| CWE | CWE-863,CWE-284 |
| CAPEC | CAPEC-122 |
| OWASP | A01:2021 - Broken Access Control |
Description: entryId parameter in sign-trust-task.ts allows selection of any vault entry kind due to the client not enforcing the did-self-issued/didcomm-peer restriction locally (relying entirely on server-side notSignable rejection), resulting in potential attempted misuse of non-signing-capable key material if the server-side check has any bypass path.
Evidence: packages/core/src/vault/sign-trust-task.ts:36-45
entryId: string;
/** The Trust Task document to sign. MUST have no `proof` field.
* MUST set `issuer = <entry.principalDid>`. The VTA refuses to
Attack Scenario:
- Attacker with valid but limited wallet access supplies an
entryIdpointing to a vault entry of an unintended kind (e.g., an imported raw key or adid-webentry not meant to sign Trust Tasks). packages/core/src/vault/sign-trust-task.tsforwards the sign request to the VTA without any client-side pre-validation of entry kind (only documented, not enforced, per the doc comment referencing the server'snotSignablerejection).- If the server-side kind check has a bug, is missing for a newly added entry kind, or is itself vulnerable to the same #279 casing confusion (rejection code spelled differently than expected by an older/newer matcher elsewhere in the stack), the sign operation could proceed against an entry kind that was never intended to produce Trust-Task signatures.
- Attacker obtains a validly-signed Trust Task document (
issuer = entry.principalDid) using key material intended for a different purpose (e.g., adid-webdomain-verification key), enabling cross-protocol signature reuse or spoofed attestations. - This signed artifact, bearing a legitimate-looking
issuer, is then submitted into the Trust-Task ecosystem to third parties who trust the signature without knowing the entry kind restriction was bypassed.
🔎 Threat Clue: Derived from COMP-003 via EP-004
- Data Flows: vault/sign-trust-task request -> VTA entry-kind check -> signature issuance
Preconditions: Server-side entry-kind enforcement has a gap, race, or casing-related matching bug, Attacker holds valid vault access to an entry of a restricted kind
Existing Controls: Server-side rejection vault/sign-trust-task:notSignable (and legacy not_signable) documented as the enforcement point • Doc-level requirement that entryId MUST point to did-self-issued or didcomm-peer
Recommended Mitigations: Add client-side pre-flight validation of entry kind before issuing the sign request, defense-in-depth against server gaps • Ensure server-side rejection logic is covered by the same dual-spelling test rigor as this PR's client-side matcher • Cryptographically bind entry kind into the signature context (e.g., key-usage extension) so misuse is detectable downstream
⚪ STRIDE-9: Build-Time Dependency of Tests on ../dist Enables Stale-Artifact Test Spoofing
| Field | Detail |
|---|---|
| Category | Tampering, Repudiation |
| Severity | Low |
| Likelihood | Unlikely |
| CVSS | 3.1 CVSS:4.0/AV:L/AC:H/AT:P/PR:L/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-1104,CWE-345 |
| CAPEC | CAPEC-698 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
Description: trust-tasks.error-code.mjs test file allows false-positive CI validation due to importing from ../dist rather than ../src, resulting in a stale/pre-fix build artifact silently passing tests while the actual regression in source is not caught.
Evidence: packages/core/tests/trust-tasks.error-code.mjs:16-20
import {
matchesTrustTaskCode,
trustTaskCodeSnakeCase,
} from "../dist/trust-tasks/error-code.js";
import { PROVISION_CONTEXT_REQUIRED } from "../dist/provision/send.js";
Attack Scenario:
- A developer or CI pipeline step modifies
packages/core/src/trust-tasks/error-code.ts(e.g., introduces the fuzzy-matching regression described in STRIDE-3) but the build step (tsc/bundler producingdist/) is skipped, cached, or fails silently. packages/core/tests/trust-tasks.error-code.mjsimports from../dist/trust-tasks/error-code.jsand../dist/provision/send.js, which still contain the previous (correct) compiled logic.- CI reports all tests passing, giving false confidence that the source-level regression is safe to merge and release.
- The regressed source ships in the next build once
dist/is eventually regenerated (e.g., in a release pipeline that does build fresh), at which point the previously-passing tests no longer reflect what was actually verified. - This creates an audit-trail/repudiation gap: the test run 'green' checkmark on the PR does not truthfully represent the source under review, and a reviewer relying on CI status could approve a regression.
🔎 Threat Clue: Derived from COMP-003 via N/A
- Data Flows: CI build -> dist artifact -> test import
Preconditions: CI or local dev workflow does not force a clean rebuild before running tests, Stale dist/ artifacts persist across test runs
Existing Controls: Tests exist at all with strong negative-case coverage • Presumed CI pipeline likely runs build before test (not confirmed in reduced artifact set)
Recommended Mitigations: Change test imports to ../src with a ts-node/tsx loader, or enforce pretest script that always runs a clean build • Add a CI step verifying dist/ is up to date (git diff --exit-code after build) before running tests • Add build-freshness assertion inside the test file itself (e.g., compare file mtimes or content hash)
⚪ STRIDE-10: Missing Rate Limiting on Provision Retry Loop Enables Resource Exhaustion DoS
| Field | Detail |
|---|---|
| Category | Denial of Service |
| Severity | Low |
| Likelihood | Possible |
| CVSS | 4.5 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,CWE-799 |
| CAPEC | CAPEC-125 |
| OWASP | A04:2021 - Insecure Design |
Description: sendProvisionIntegration in send.ts allows unrestricted retry against a contextRequired reject due to DEFAULT_TIMEOUT_MS bounding only a single request without visible backoff/rate-limit tracking across repeated calls, resulting in resource exhaustion of the local wallet or the remote VTA when the operator/picker loop is scripted or abused.
Evidence: packages/core/src/provision/send.ts:12-14
const DEFAULT_TIMEOUT_MS = 60_000;
Attack Scenario:
DEFAULT_TIMEOUT_MS = 60_000bounds a single DIDComm round-trip, but nothing in the shown code enforces a minimum interval or maximum retry count across repeatedsendProvisionIntegrationcalls.- A malicious local script (e.g., a compromised page able to trigger extension messages, chained with STRIDE-4's spoofing) or a buggy automated picker-selection flow in
onboard-view.tsxcould rapidly re-invoke provisioning after everycontextRequiredreply. - Each retry opens a new DIDComm exchange to the remote VTA endpoint, consuming VTA-side resources (connection handling, DIDComm crypto operations) and local extension resources (offscreen document message queue).
- Absent backoff, this can degrade the VTA's availability for legitimate users or exhaust the local browser extension's message-passing capacity, especially if combined with a scripted candidate-picker loop that always re-triggers
contextRequired. - Result: denial of service either to the local extension UI (unresponsive picker) or to the shared remote VTA endpoint.
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: Repeated sendProvisionIntegration retries -> VTA endpoint
Preconditions: No visible rate limiting/backoff logic beyond the single-request timeout, Attacker able to trigger repeated provisioning attempts (locally or via STRIDE-4 spoofing)
Existing Controls: Single-request timeout (DEFAULT_TIMEOUT_MS) prevents indefinite hangs per call • Ephemeral grant TTL (1h) limits some retry windows per comments
Recommended Mitigations: Implement exponential backoff and a maximum retry count for provisioning attempts • Rate-limit provisioning requests per origin/session in the background/offscreen bridge • Add server-side rate limiting on the VTA endpoint (out of scope of this client repo, but should be confirmed)
⚪ STRIDE-11: Public Export of Internal Constant Increases Attack-Surface Discoverability for Provisioning Flow
| Field | Detail |
|---|---|
| Category | Information Disclosure |
| Severity | Informational |
| Likelihood | Unlikely |
| CVSS | 1.8 CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | None |
| CWE | CWE-200 |
| CAPEC | CAPEC-116 |
| OWASP | A01:2021 - Broken Access Control |
Description: PROVISION_CONTEXT_REQUIRED export in provision/index.ts allows external package consumers/attackers full visibility of internal protocol constants due to the barrel file re-exporting the string literal at the package's public API surface, resulting in easier reconnaissance for crafting targeted problem-report spoofing payloads (feeds STRIDE-1/STRIDE-4/STRIDE-5).
Evidence: packages/core/src/provision/index.ts:20-27
export {
sendProvisionIntegration,
ProvisionProblemReportError,
PROVISION_CONTEXT_REQUIRED,
type ProblemReportPayload,
Attack Scenario:
packages/core/src/provision/index.tsnow re-exportsPROVISION_CONTEXT_REQUIREDpublicly via the package barrel file (export { ..., PROVISION_CONTEXT_REQUIRED, ... }).- Any consumer of the
@openvtc/pnm-corenpm package (including an attacker building a malicious VTA or a competing wallet) can trivially discover the exact wire-level code string and its lowerCamelCase/snake_case dual forms viatrustTaskCodeSnakeCase. - While this is largely already documented in the public trust-tasks spec (#279), bundling it as a typed, importable constant lowers the effort for an attacker to programmatically generate perfectly-formed spoofed problem-report payloads that will pass
matchesTrustTaskCodefor both spellings. - Combined with STRIDE-1 (no runtime body validation) and STRIDE-4 (no sender validation on the message bridge), this constant export slightly increases the ease of crafting an exploit payload that reliably triggers the onboarding picker code path with attacker-controlled candidates.
- This is a minor reconnaissance-enablement issue rather than a standalone vulnerability, since the underlying spec is already public.
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: Package export -> public API surface
Preconditions: Attacker has access to inspect the published npm package or extension source, Underlying protocol spec is already public (largely mitigates novelty of this disclosure)
Existing Controls: The Trust-Task spec/registry itself is public, so this constant's value is not truly secret • TypeScript export is intentional and documented for legitimate cross-package reuse
Recommended Mitigations: No action strictly required since the spec is public; optionally document that this constant is not a security boundary • Ensure no security decisions ever rely on the secrecy of this string
🍝 PASTA Threat Model
Application Purpose
A browser extension wallet ('VTA browser plugin') that establishes and manages DIDComm-based trust relationships (provisioning, vault key management, and Trust-Task signing) between a user's browser and remote Verifiable-Trust-Agent (VTA) services, delivering decentralized identity and verifiable-credential capabilities to end users.
Inherent Risks
- The extension bridges an untrusted remote network protocol (DIDComm/Trust-Task) directly into privileged browser-extension UI and vault operations.
- The wallet and the remote VTA services it talks to are independently versioned and deployed, creating a permanently fragmented compatibility surface.
- Vault operations manage cryptographic key material whose misuse has irreversible real-world trust consequences.
- Browser extension message-passing architecture (offscreen/background/popup) is a well-known attack surface for spoofing and privilege confusion.
Objectives
Risk: Accept residual risk from protocol versioning skew as inherent to the DIDComm/Trust-Task ecosystem, provided it degrades gracefully rather than silently.; Treat any silent (non-erroring) failure mode as higher risk than a loud one, given the diagnosis cost.
Business: Provide a trustworthy, low-friction onboarding and provisioning experience for operators connecting wallets to VTA services.; Maintain interoperability across a fleet of independently-versioned wallets and VTAs.
Security: Ensure all remote protocol input is validated before influencing UI or vault state.; Prevent cross-component message spoofing within the extension's internal message-passing architecture.; Preserve non-repudiation of destructive vault operations.
Financial: Avoid costly incident response and reputational damage from a compromised vault or provisioning flow.; Minimize support burden caused by silent, hard-to-diagnose compatibility failures.
Compliance: Align with the SPEC §4.10 rule 4 lowerCamelCase requirement of the trust-tasks registry.; Maintain auditability of vault deletion and signing operations for accountability requirements applicable to identity-wallet software.
Functional: Correctly provision integrations into the right context, including human-in-the-loop disambiguation.; Support vault entry lifecycle management (create, sign, delete) with safe concurrency semantics.; Tolerate protocol evolution (SPEC re-casing) without breaking either side of the deployed fleet.
Operational: Ensure error-code compatibility logic is derived, tested, and centrally maintained rather than duplicated.; Keep browser-extension messaging pipeline resilient to partial failures and malformed peer responses.
Business Impact Analysis (3)
BIA-1: Provisioning and Onboarding Flow (High)
End-to-end process by which an operator connects the wallet to a VTA service, including context disambiguation via the candidate picker.
MTD: 01 days 00:00 hours | RTO: 00 days 04:00 hours | RPO: 00 days 00:00 hours
- Stakeholders: Extension Maintainers / Operators (End Users) / VTA Service Operators
- Dependencies: DIDComm Messaging Channel / Offscreen/Background/Popup Message Bridge / Trust-Task Error-Code Registry
- Disruptions: Malformed or spoofed problem-report responses / Silent branch failures from casing mismatches / Cross-extension message spoofing
- Impacts: Operator provisions into an attacker-controlled context / Operator sees no recovery UX and abandons onboarding (support burden) / Reputational damage from a publicized wallet-onboarding compromise
BIA-2: Vault Key Lifecycle Management (Critical)
Process by which vault entries (signing keys, DID material) are created, signed with, and deleted, including optimistic-concurrency-protected deletion.
MTD: 00 days 04:00 hours | RTO: 00 days 01:00 hours | RPO: 00 days 00:00 hours
- Stakeholders: Extension Maintainers / Operators (End Users) / Relying Parties (Trust-Task Verifiers)
- Dependencies: VTA Vault Storage Service / Optimistic Concurrency Versioning / Trust-Task Signing Endpoint
- Disruptions: TOCTOU race during concurrent delete/upsert / Cross-kind signing abuse of restricted entries / Repudiation of destructive delete actions
- Impacts: Irrecoverable loss of active signing key material / Fraudulent Trust-Task signatures accepted by relying parties / Inability to attribute a destructive action to a responsible principal
BIA-3: Cross-Fleet Protocol Compatibility Maintenance (Medium)
Ongoing process of ensuring wallet and VTA components on both sides of the SPEC #279 re-casing continue to interoperate without silent failures.
MTD: 07 days 00:00 hours | RTO: 01 days 00:00 hours | RPO: N/A
- Stakeholders: Extension Maintainers / VTA Service Operators / Chrome Web Store Release Pipeline
- Dependencies: matchesTrustTaskCode Helper / CI Test Suite / Build Artifact Freshness
- Disruptions: Regression of the dual-spelling matcher / Stale dist/ build artifacts masking regressions in CI
- Impacts: Recovery UX silently disappears for a subset of the fleet / False confidence from green CI on stale artifacts
Technical Scope
Roles (3): RO-1 Wallet Operator · RO-2 VTA Service Principal · RO-3 Extension Internal Process
Actors (3): AC-1 Operator · AC-2 VTA Agent · AC-3 Offscreen Document Process
Entry Points (6): EP-1 Provision Integration Send · EP-2 Problem-Report Reply · EP-3 Vault Delete · EP-4 Vault Sign Trust Task · EP-5 Offscreen Runtime Message Listener · EP-6 Onboard Candidate Picker UI
Threat Actors (4): TA-1 Malicious/Compromised VTA Operator · TA-2 Co-Installed Malicious Extension · TA-3 Insider / Compromised Operator Credential · TA-4 Careless Future Contributor
Infrastructure (2): IF-1 Browser Extension Runtime · IF-2 Remote VTA Deployment
Trust Boundaries (3): TB-1 Remote VTA / DIDComm Network Boundary · TB-2 Browser Extension Internal Message Bus · TB-3 Operator UI Boundary
External Entities (2): EE-1 Remote VTA Service · EE-2 Chrome Web Store Update Channel
System Components (7): SC-1 Provision Send Module (core) · SC-2 Trust-Task Error-Code Matcher (core) · SC-3 Vault Operations Module (core) · SC-4 VTA Protocol Types (core) · SC-5 Offscreen Bridge (extension) · SC-6 Bridge Protocol Types (extension) · SC-7 Onboard View (extension UI)
Resources And Assets (4): RA-1 Vault Entry Key Material · RA-2 Provisioning Ephemeral Grant · RA-3 Problem-Report Payload (code/args/candidates) · RA-4 Vault Audit Trail Reason
Technologies And Dependencies (5): TD-1 TypeScript · TD-2 React · TD-3 Chrome Extension Manifest V3 APIs · TD-4 DIDComm / Trust-Task Protocol · TD-5 node:test
Use Cases (2)
- Provisioning Integration with Context Disambiguation: An operator initiates provisioning of a new integration; if the VTA cannot infer the target context, the wallet presents a candidate picker so the operator can select the correct one before completing
- Vault Entry Deletion with Optimistic Concurrency: A wallet operator deletes a vault entry, supplying an expected version and rationale so the VTA can safely reject stale or conflicting deletion attempts.
📋 Risk Registry (7)
| ID | Title | Severity | Residual | Priority | Effort |
|---|---|---|---|---|---|
| RISK-001 | Unvalidated remote problem-report data flows into wallet UI and decision logic | Medium | Medium | Short-Term | Medium |
| RISK-002 | Internal extension message bus lacks sender authentication | High | Medium | Immediate | Low |
| RISK-003 | Vault delete/sign operations lack defense-in-depth against race conditions and entry-kind misuse | High | Medium | Short-Term | High |
| RISK-004 | Silent compatibility failures across the trust-tasks #279 re-casing fleet split | Medium | Low | Medium-Term | Low |
| RISK-005 | Non-repudiation gap on destructive vault delete actions | Low | Low | Medium-Term | Medium |
| RISK-006 | CI test artifacts may validate stale build output rather than current source | Low | Low | Medium-Term | Low |
| RISK-007 | Unbounded provisioning retry loop enables resource exhaustion | Low | Low | Long-Term | Medium |
⚔️ Attack Scenarios (5)
SC-1: Provision Send Module (core)
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. System Component"]
direction LR
SC1@{ shape: rect, label: "SC-1: Provision Send Module (core)" }
end
subgraph SL2["2. Weaknesses"]
direction LR
CWE20@{ shape: rect, label: "CWE-20: Improper Input Validation" }
CWE1284@{ shape: rect, label: "CWE-1284: Improper Validation of Specified Quantity in Input" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAPEC153@{ shape: rect, label: "CAPEC-153: Input Data Manipulation" }
CAPEC588@{ shape: rect, label: "CAPEC-588: DOM-Based XSS" }
end
subgraph SL4["4. Threats"]
direction LR
STRIDE1@{ shape: rect, label: "STRIDE-1: Unvalidated Problem-Report Injection<br><i>Medium / Likely</i>" }
end
subgraph SL5["5. Threat Actors"]
direction LR
TA1@{ shape: rect, label: "TA-1: Malicious/Compromised VTA Operator<br><i>Hijack provisioning</i>" }
end
CWE20 --> CAPEC153
CWE1284 --> CAPEC588
CAPEC153 --> STRIDE1
CAPEC588 --> STRIDE1
STRIDE1 --> TA1
SC1 --> CWE20
SC1 --> CWE1284
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
SC-3: Vault Operations Module (core)
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. System Component"]
direction LR
SC3@{ shape: rect, label: "SC-3: Vault Operations Module (core)" }
end
subgraph SL2["2. Weaknesses"]
direction LR
CWE367@{ shape: rect, label: "CWE-367: TOCTOU Race Condition" }
CWE863@{ shape: rect, label: "CWE-863: Incorrect Authorization" }
CWE778@{ shape: rect, label: "CWE-778: Insufficient Logging" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAPEC25@{ shape: rect, label: "CAPEC-25: Forced Deadlock / Race Window" }
CAPEC122@{ shape: rect, label: "CAPEC-122: Privilege Abuse" }
CAPEC268@{ shape: rect, label: "CAPEC-268: Audit Log Manipulation" }
end
subgraph SL4["4. Threats"]
direction LR
STRIDE7@{ shape: rect, label: "STRIDE-7: TOCTOU Race in Vault Delete<br><i>Medium / Possible</i>" }
STRIDE8@{ shape: rect, label: "STRIDE-8: Cross-Kind Signing Abuse<br><i>High / Possible</i>" }
STRIDE6@{ shape: rect, label: "STRIDE-6: Missing Repudiation Protection<br><i>Low / Possible</i>" }
end
subgraph SL5["5. Threat Actors"]
direction LR
TA3@{ shape: rect, label: "TA-3: Insider / Compromised Operator Credential<br><i>Delete or misuse vault key material</i>" }
end
CWE367 --> CAPEC25
CWE863 --> CAPEC122
CWE778 --> CAPEC268
CAPEC25 --> STRIDE7
CAPEC122 --> STRIDE8
CAPEC268 --> STRIDE6
STRIDE7 --> TA3
STRIDE8 --> TA3
STRIDE6 --> TA3
SC3 --> CWE367
SC3 --> CWE863
SC3 --> CWE778
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
SC-5: Offscreen Bridge (extension)
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. System Component"]
direction LR
SC5@{ shape: rect, label: "SC-5: Offscreen Bridge (extension)" }
end
subgraph SL2["2. Weaknesses"]
direction LR
CWE346@{ shape: rect, label: "CWE-346: Origin Validation Error" }
CWE862@{ shape: rect, label: "CWE-862: Missing Authorization" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAPEC98@{ shape: rect, label: "CAPEC-98: Phishing via Trusted UI" }
CAPEC141@{ shape: rect, label: "CAPEC-141: Cache Poisoning / Message Injection" }
end
subgraph SL4["4. Threats"]
direction LR
STRIDE4@{ shape: rect, label: "STRIDE-4: Unauthenticated Message Listener Spoofing<br><i>High / Possible</i>" }
end
subgraph SL5["5. Threat Actors"]
direction LR
TA2@{ shape: rect, label: "TA-2: Co-Installed Malicious Extension<br><i>Escalate privilege via message injection</i>" }
end
CWE346 --> CAPEC98
CWE862 --> CAPEC141
CAPEC98 --> STRIDE4
CAPEC141 --> STRIDE4
STRIDE4 --> TA2
SC5 --> CWE346
SC5 --> CWE862
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
SC-7: Onboard View (extension UI)
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. System Component"]
direction LR
SC7@{ shape: rect, label: "SC-7: Onboard View (extension UI)" }
end
subgraph SL2["2. Weaknesses"]
direction LR
CWE451@{ shape: rect, label: "CWE-451: User Interface Misrepresentation" }
CWE20b@{ shape: rect, label: "CWE-20: Improper Input Validation" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAPEC98b@{ shape: rect, label: "CAPEC-98: Phishing via Trusted UI" }
CAPEC163@{ shape: rect, label: "CAPEC-163: Spoof Trust Indicator" }
end
subgraph SL4["4. Threats"]
direction LR
STRIDE5@{ shape: rect, label: "STRIDE-5: Unsanitized Candidate List UI Spoofing<br><i>Medium / Possible</i>" }
end
subgraph SL5["5. Threat Actors"]
direction LR
TA1b@{ shape: rect, label: "TA-1: Malicious/Compromised VTA Operator<br><i>Hijack provisioning</i>" }
end
CWE451 --> CAPEC98b
CWE20b --> CAPEC163
CAPEC98b --> STRIDE5
CAPEC163 --> STRIDE5
STRIDE5 --> TA1b
SC7 --> CWE451
SC7 --> CWE20b
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
SC-2: Trust-Task Error-Code Matcher (core)
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. System Component"]
direction LR
SC2@{ shape: rect, label: "SC-2: Trust-Task Error-Code Matcher (core)" }
end
subgraph SL2["2. Weaknesses"]
direction LR
CWE697@{ shape: rect, label: "CWE-697: Incorrect Comparison" }
CWE354@{ shape: rect, label: "CWE-354: Improper Validation of Integrity Check Value" }
CWE1104@{ shape: rect, label: "CWE-1104: Use of Unmaintained Third Party Components" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAPEC267@{ shape: rect, label: "CAPEC-267: Analytic Attacks on Comparison Logic" }
CAPEC141b@{ shape: rect, label: "CAPEC-141: Cache Poisoning" }
CAPEC698@{ shape: rect, label: "CAPEC-698: Install Malicious Automated Software Update" }
end
subgraph SL4["4. Threats"]
direction LR
STRIDE2@{ shape: rect, label: "STRIDE-2: Silent Branch Failure via Strict Equality<br><i>Medium / Likely</i>" }
STRIDE3@{ shape: rect, label: "STRIDE-3: Fuzzy Matching Regression Collision<br><i>Low / Unlikely</i>" }
STRIDE9@{ shape: rect, label: "STRIDE-9: Stale-Artifact Test Spoofing<br><i>Low / Unlikely</i>" }
end
subgraph SL5["5. Threat Actors"]
direction LR
TA4@{ shape: rect, label: "TA-4: Careless Future Contributor<br><i>Reintroduce fixed vulnerability</i>" }
end
CWE697 --> CAPEC267
CWE354 --> CAPEC141b
CWE1104 --> CAPEC698
CAPEC267 --> STRIDE2
CAPEC267 --> STRIDE3
CAPEC698 --> STRIDE9
STRIDE2 --> TA4
STRIDE3 --> TA4
STRIDE9 --> TA4
SC2 --> CWE697
SC2 --> CWE354
SC2 --> CWE1104
linkStyle 0 stroke:#FFA500,stroke-width:2px
linkStyle 1 stroke:#00FF00,stroke-width:2px
linkStyle 2 stroke:#00FF00,stroke-width:2px
linkStyle 3 stroke:#FFA500,stroke-width:2px
linkStyle 4 stroke:#00FF00,stroke-width:2px
linkStyle 5 stroke:#00FF00,stroke-width:2px
linkStyle 6 stroke:#FFA500,stroke-width:2px
linkStyle 7 stroke:#00FF00,stroke-width:2px
linkStyle 8 stroke:#00FF00,stroke-width:2px
linkStyle 9 stroke:#FFA500,stroke-width:2px
linkStyle 10 stroke:#00FF00,stroke-width:2px
linkStyle 11 stroke:#00FF00,stroke-width:2px
📊 Risk Summary
Total Threats: 11
By Severity: Low: 4 · High: 2 · Medium: 4 · Informational: 1
By Category: Unknown: 11
🎯 Attack Surface
Kill Chain 1: An attacker controlling or spoofing a remote VTA endpoint (TA-1) exploits the missing runtime validation in send.ts (STRIDE-1) to inject a crafted problem-report body; this payload flows unvalidated through offscreen.ts and bridge-protocol.ts into onboard-view.tsx's candidate picker (STRIDE-5), where the operator is socially engineered into selecting a malicious context, ultimately provisioning the ephemeral grant into attacker-controlled infrastructure. Kill Chain 2: A co-installed malicious extension (TA-2) exploits the unauthenticated chrome.runtime.onMessage listener in offscreen.ts (STRIDE-4) to inject a forged response entirely without any real VTA involvement, chaining directly into the same candidate-picker UI spoofing weakness (STRIDE-5) to achieve full onboarding hijack without ever compromising the network protocol. Kill Chain 3: An insider or compromised operator credential (TA-3) races a legitimate vault/upsert against a vault/delete call exploiting the non-atomic optimistic-concurrency window (STRIDE-7), destroying freshly-rotated key material, then separately abuses the vault/sign-trust-task endpoint against a restricted entry kind (STRIDE-8) if server-side kind enforcement has any gap, producing a fraudulently signed Trust-Task artifact that downstream relying parties accept as legitimate — compounded by the audit-trail non-repudiation gap (STRIDE-6) that prevents attributing either action to the responsible principal. Kill Chain 4: A
🛡️ Risk Mitigation Strategy
Priority 1 (Immediate): Close the authentication gap on the internal extension message bus by validating sender.id in every chrome.runtime.onMessage listener and introducing per-flow correlation nonces, since this is the cheapest control to add and it is the root enabler of the most severe cross-component spoofing kill chain (STRIDE-4, RISK-002). Priority 2 (Short-Term): Add runtime schema validation for all DIDComm problem-report bodies before they influence UI state or vault decisions, and sanitize/validate candidate identifiers rendered in the onboarding picker, directly closing the injection surface that both the network-based and extension-based kill chains converge on (STRIDE-1, STRIDE-5, RISK-001). Priority 3 (Short-Term): Strengthen vault operation integrity by adding client-side entry-kind pre-validation ahead of sign-trust-task calls and verifying the VTA's server-side delete transaction is truly atomic against concurrent upserts, since these are the only threats in this review capable of irrecoverable, high-severity impact to cryptographic trust material (STRIDE-7, STRIDE-8, RISK-003). Priority 4 (Medium-Term): Harden the error-code compatibility fold itself against regression by adding a lint rule prohibiting direct equality comparisons on Trust-Task codes, expanding property-based fuzz tests, and fixing the test suite to import from source rather than a potentially stale dist/ directory, preventing this PR's own fix from silently eroding over time (STRIDE-2, STRIDE-3, STRIDE-9, RISK-004, RISK-006). Priority 5 (Medium-to-Long-Term): Improve non-repudiation of destructive vault actions by cryptographically binding the audit-trail reason field to the signed request, and add backoff/rate-limiting to the provisioning retry path to reduce availability risk under abuse (STRIDE-6, STRIDE-10, RISK-005, RISK-007).
Generated by Agentic Sec — Threat Model & Affect Analysis Agent
📊 Summary & findings
| ✅ Confirmed | |
|---|---|
| 0 | 3 |
Must-Review-By-Human (3)
- 🟡 Missing runtime schema validation on DIDComm problem-report body (type-assertion only)
- 🟡 Unsanitized attacker-influenced candidate list rendered in onboarding context picker
- 🟡 chrome.runtime.onMessage listener lacks explicit sender/origin validation (as shown in reduced snippet)
Consumer-side follow-up to trustoverip/dtgwg-trust-tasks-tf#279, which re-cased
~200 extended error codes to the lowerCamelCase that SPEC §4.10 rule 4 requires.
Only the local part after the
:moved; namespaces are unchanged, and retiredspecs were deliberately left snake_case and frozen.
The defect
onboard-view.tsxgated the onboarding context picker on a===againstthe old spelling:
Against a VTA that has taken #279, that comparison goes false — and then
nothing else happens. No error, no crash, no log line. The picker simply
stops appearing, and an operator with a multi-context VTA is dead-ended at
Connect, told a context is required and given no way to choose one. This is the
failure mode R3.7 exists to prevent, arriving through the front door: a real
machine-readable code, matched in a way that isn't stable.
Why dual-accept, and not a swap
Swapping the literal would move the defect, not fix it.
This wallet is on the matching side of this wire and never the declaring
side — it reads codes an agent sends it. And it updates on the Chrome Web
Store's schedule while the VTA updates on its own:
context_requiredcontextRequiredA wallet installed today still talks to a months-old agent next year; a wallet
that hasn't auto-updated meets an agent that took #279 this morning. Both
halves of that fleet are live simultaneously, so either literal alone is
correct for exactly half of it. Accepting both is the only spelling-independent
option — and it's the rule #124 already set for wire field names on the read
path ("emitting the canonical spelling and accepting either are separate
moves").
The helper
matchesTrustTaskCode(actual, canonical)— new, inpackages/core/src/trust-tasks/error-code.ts, exported from the root barrel.Four properties are deliberate:
canonicalis always the registry's current spelling. Call sites read asthe code declared today; the compatibility is confined to one module. That
is what makes retiring the fold one edit rather than a sweep across ~200
possible codes.
table looks exactly like no fold at all.
:.contextRequiredandcontextNotFoundstaydistinct, so a rename can't become a collision that surfaces the wrong
recovery UX for a different rejection.
It carries a
// TODO:naming the condition for dropping the snake_case arm: adeclared minimum vta-service floor at or above the release carrying the
re-cased codes — the way #125 made 0.18.0 a hard floor for the dispatcher path.
Until a floor is declared, an old agent is still a supported peer.
Sites changed
Matcher (behaviour):
packages/extension/src/onboard-view.tsx===→matchesTrustTaskCode(res.code, PROVISION_CONTEXT_REQUIRED); imports both from@openvtc/pnm-coreNew:
packages/core/src/trust-tasks/error-code.tsmatchesTrustTaskCode,trustTaskCodeSnakeCasepackages/core/src/trust-tasks/index.tspackages/core/tests/trust-tasks.error-code.mjsCanonical constant:
packages/core/src/provision/send.tsPROVISION_CONTEXT_REQUIRED = "provision/integration:contextRequired", following theMEDIATOR_REQUIREDprecedent inbridge-protocol.ts; the parse path documents that it passes the code through verbatimpackages/core/src/provision/index.tsMessage-passing path — the code crosses offscreen → background → popup, and
is forwarded verbatim at each hop. Normalising in transit would make the
hops need redeploying in lockstep with the agent, and would hide from the popup
which side of the rename its peer is on. Both are documented so nobody
"helpfully" folds it upstream later:
packages/extension/src/bridge-protocol.tsRuntimeOnboardConnectResponse.code— re-spelled + verbatim-passthrough rationalepackages/extension/src/offscreen.tsProvisionProblemReportErrorcatchDoc comments naming the other three codes — all three appear only in
prose, so there was nothing to fold, but they're now spelled as the registry
declares them with a pointer at the matcher, so the next site that starts
branching on one starts from the right shape:
packages/core/src/vault/delete.tsvault/delete:versionConflictpackages/core/src/vault/sign-trust-task.tsvault/sign-trust-task:notSignablepackages/core/src/vta/protocol.tsvault/upsert:sealedSecretInvalidpackages/extension/src/onboard-view.tsxcontextCandidatesVerified against the registry
All four re-cased codes confirmed against
dtgwg-trust-tasks-tf@mainspecs/**/spec.md(543 declared codes), not taken on trust:Left alone, deliberately
Swept every namespaced snake_case string in the repo. Three classes are
correct as-is and would be bugs to "fix":
auth:consent_required(vta/request-task.ts) — adetails.reasontoken, not an extended error code. The registry declares no bare
auth:namespace at all, and the top-level
codefor this rejection is theframework's
taskFailed. That file's own doc comment records that matchingthis against
codeis a defect the repo already had once.e.p.msg.context_required(provision/run.ts) — a DIDCommproblem-report code. Different scheme, not governed by §4.10.
push/register:bad_token(test fixture) — the registry still declares thewhole
push/*namespace in snake_case, andbad_tokenisn't declared at all;the test only asserts pass-through into an error string, with no matching.
No test fixtures, mock responses, string unions/enums or persisted state carry a
governed code. Persisted state was checked specifically: the only stored code is
the transient
contextCandidatesin React state within a single onboardingattempt, so nothing can hold a stale spelling across sessions.
Deploy order
This must ship before the vta-service change that emits the new spelling.
A sibling PR is updating
verifiable-trust-infrastructureto emitcontextRequired; if that lands first, every wallet in the field loses thecontext picker until it auto-updates. With this merged and released first, the
service-side rename needs no coordination at all — which is the whole point of
the fold, and why it's pinned by a test rather than by a comment.
Verification
Real output, from a cold
npm ciin a clean worktree:npm run lint(tsc -b, all four workspaces) — cleannpm run build— clean, MV3 background bundle emitted at 48.16 kBnpm test— 546 passing, 0 failing (was 539; +7 here)Both CI-only guards re-run by hand against the built
dist/:import()✅Also confirmed the shipped bundle carries the canonical spelling
(
provision/integration:contextRequiredinassets/send-*.jsandassets/options-*.js) with the snake_case form derived at runtime rather thanduplicated as a literal.
The seven tests
They pin the property, not the string manipulation:
vault/upsert:versionConflict≠vault/delete:versionConflict— both are real codes with the same local part)codeis not a match — a transport failure carries noproblem-report, and mistaking that for the one code we act on would open the
picker on a network blip
Checklist (stack guide §9)
fetch(); no network code touched (R1.2)(R3.7) — the whole PR; the match is now stable across the registry's
re-casing rather than pinned to one side of it
verified against the registry's declarations and sequenced ahead of the
service-side emit