Skip to content

fix(extension): accept both spellings of a re-cased error code - #126

Merged
stormer78 merged 1 commit into
mainfrom
fix/trust-task-error-code-casing
Aug 26, 2026
Merged

fix(extension): accept both spellings of a re-cased error code#126
stormer78 merged 1 commit into
mainfrom
fix/trust-task-error-code-casing

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

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 retired
specs were deliberately left snake_case and frozen.

The defect

onboard-view.tsx gated the onboarding context picker on a === against
the old spelling:

res.code === "provision/integration:context_required" &&

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:

against a pre-#279 VTA against a post-#279 VTA
keep context_required works picker never appears
swap to contextRequired picker never appears works
accept both works works

A 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, in
packages/core/src/trust-tasks/error-code.ts, exported from the root barrel.

matchesTrustTaskCode(res.code, PROVISION_CONTEXT_REQUIRED)
// true for "provision/integration:contextRequired"
// true for "provision/integration:context_required"

Four properties are deliberate:

  • canonical is always the registry's current spelling. Call sites read as
    the 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.
  • The snake_case form is derived, not a hand-maintained pair. A drifted
    table looks exactly like no fold at all.
  • Only the local part is transformed. #279 changed nothing before the :.
  • Not a fuzzy compare. contextRequired and contextNotFound stay
    distinct, 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: a
declared 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):

site change
packages/extension/src/onboard-view.tsx ===matchesTrustTaskCode(res.code, PROVISION_CONTEXT_REQUIRED); imports both from @openvtc/pnm-core

New:

site change
packages/core/src/trust-tasks/error-code.ts matchesTrustTaskCode, trustTaskCodeSnakeCase
packages/core/src/trust-tasks/index.ts export the above
packages/core/tests/trust-tasks.error-code.mjs 7 tests

Canonical constant:

site change
packages/core/src/provision/send.ts exports PROVISION_CONTEXT_REQUIRED = "provision/integration:contextRequired", following the MEDIATOR_REQUIRED precedent in bridge-protocol.ts; the parse path documents that it passes the code through verbatim
packages/core/src/provision/index.ts re-export

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

site change
packages/extension/src/bridge-protocol.ts RuntimeOnboardConnectResponse.code — re-spelled + verbatim-passthrough rationale
packages/extension/src/offscreen.ts same, at the ProvisionProblemReportError catch

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

site code
packages/core/src/vault/delete.ts vault/delete:versionConflict
packages/core/src/vault/sign-trust-task.ts vault/sign-trust-task:notSignable
packages/core/src/vta/protocol.ts vault/upsert:sealedSecretInvalid
packages/extension/src/onboard-view.tsx state comment above contextCandidates

Verified against the registry

All four re-cased codes confirmed against dtgwg-trust-tasks-tf@main
specs/**/spec.md (543 declared codes), not taken on trust:

provision/integration:contextRequired
vault/delete:versionConflict
vault/sign-trust-task:notSignable
vault/upsert:sealedSecretInvalid

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) — a details.reason
    token, not an extended error code. The registry declares no bare auth:
    namespace at all, and the top-level code for this rejection is the
    framework's taskFailed. That file's own doc comment records that matching
    this against code is a defect the repo already had once.
  • e.p.msg.context_required (provision/run.ts) — a DIDComm
    problem-report code. Different scheme, not governed by §4.10.
  • push/register:bad_token (test fixture) — the registry still declares the
    whole push/* namespace in snake_case, and bad_token isn'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 contextCandidates in React state within a single onboarding
attempt, 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-infrastructure to emit
contextRequired; if that lands first, every wallet in the field loses the
context 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 ci in a clean worktree:

  • npm run lint (tsc -b, all four workspaces) — clean
  • npm run buildclean, MV3 background bundle emitted at 48.16 kB
  • npm test546 passing, 0 failing (was 539; +7 here)
    • core 401, tsp-js 51, extension 85, reviewer-demo 9

Both CI-only guards re-run by hand against the built dist/:

  • MV3 worker is a single self-contained bundle — no dynamic import()
  • No admin task URIs in the extension bundle ✅

Also confirmed the shipped bundle carries the canonical spelling
(provision/integration:contextRequired in assets/send-*.js and
assets/options-*.js) with the snake_case form derived at runtime rather than
duplicated as a literal.

The seven tests

They pin the property, not the string manipulation:

  • both spellings match, for all four re-cased codes
  • the snake_case form is derived, so it can't drift
  • only the local part is re-cased; the namespace is untouched
  • a different code in the same namespace does not match
  • the namespace is part of the identity (vault/upsert:versionConflict
    vault/delete:versionConflict — both are real codes with the same local part)
  • absent/empty code is not a match — a transport failure carries no
    problem-report, and mistaking that for the one code we act on would open the
    picker on a network blip
  • the exported constant is in the registry's current spelling

Checklist (stack guide §9)

  • No new bare fetch(); no network code touched (R1.2)
  • No lock held across a network await (R1.3)
  • No local state committed before its remote effect (R2.1)
  • Every retry bounded + backed off (R1.4)
  • Accept/poll/listen loops survive transient errors (R1.5)
  • Acks/deletes happen only after durable handoff (R1.6)
  • Errors matched on stable machine-readable codes, never strings
    (R3.7)
    — the whole PR; the match is now stable across the registry's
    re-casing rather than pinned to one side of it
  • Contract change coordinated with the Rust side, not guessed (R3.6)
    verified against the registry's declarations and sequenced ahead of the
    service-side emit
  • Config absence = most restrictive (R5.*) — an absent code matches nothing
  • Logs/status claim only what was verified (R6.*)
  • Deviations flagged with rule numbers

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>
@stormer78
stormer78 merged commit e457a55 into main Aug 26, 2026
3 checks passed
@stormer78
stormer78 deleted the fix/trust-task-error-code-casing branch August 26, 2026 08:34
@affinidi-appsecurity-bot

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

3 findings need a human to review/validate.

Mandatory to check: 🔒 Security Code Review Report

Details

🛡️ Security Code Review Report — PR #126

Field Value
Repository OpenVTC/vta-browser-plugin
Branch fix/trust-task-error-code-casingmain
Validated 2026-08-26
Scan ID a71846c0
Validator AI Security Validation Agent

🗺️ Scan Coverage

Modules scanned: 2 · with findings: 2 · files: 11 · findings: 4

Module Files scanned Findings
packages/core 8 1
packages/extension 3 3

Executive Summary

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

⚠️ 3 finding(s) need human review. These could not be conclusively confirmed or dismissed automatically (insufficient evidence). They are not dismissed — a developer / security team member must read and decide.


🔒 Security Issues

⚠️ Must-Review-By-Human (3)

Validated up to a point, but inconclusive — a human must read the code and make the final call. Reported (not dismissed) so developers and the security team receive them.

🟡 Missing runtime schema validation on DIDComm problem-report body (type-assertion only)

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 with typeof body.code === "string", typeof body.comment === "string", and Array.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 on args, no validation of code against an allow-list of known codes, and no bound on comment size. 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 on ctx before it's used as a button label/onClick argument passed to connect(ctx), which ultimately becomes the wire context field 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 a sender.id === chrome.runtime.id check 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.

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

🛡️ Threat Model & Affect Analysis — PR #126

Field Value
Repository OpenVTC/vta-browser-plugin
Branch fix/trust-task-error-code-casingmain
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:

  1. A malicious or compromised VTA/remote DIDComm endpoint replies to a provision/integration request with reply.type === PROBLEM_REPORT_TYPE.
  2. 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).
  3. Attacker sets body.code to an arbitrary string, and body.args to an oversized array or attacker-controlled strings (candidate list) with no length/content limits.
  4. ProvisionProblemReportError is thrown carrying the unvalidated code/comment/args verbatim.
  5. The error propagates through offscreen.ts's catch handler which forwards code/candidates verbatim across the extension message-passing boundary to bridge-protocol.ts's RuntimeOnboardConnectResponse.
  6. onboard-view.tsx renders res.candidates directly 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:

  1. An operator's wallet has not auto-updated and predates the recognition of matchesTrustTaskCode at some call site not covered by this PR (e.g., a future or existing direct === comparison against vault/delete:versionConflict, vault/sign-trust-task:notSignable, or vault/upsert:sealedSecretInvalid).
  2. A VTA that has adopted trust-tasks #279 responds with the lowerCamelCase spelling (e.g., vault/delete:versionConflict).
  3. If any call site (present or future) in packages/core/src/vault/delete.ts or sign-trust-task.ts consumers still compares with === against the snake_case constant, the comparison silently evaluates false.
  4. 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).
  5. 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.
  6. 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:

  1. A future contributor unfamiliar with the design intent (documented only in comments) refactors matchesTrustTaskCode toward a general case-insensitive or fuzzy .toLowerCase() comparison to 'simplify' the helper.
  2. 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.
  3. A VTA sends vault/upsert:version_conflict (a real distinct code in the vault/upsert namespace) while the caller checks against vault/delete:versionConflict (a different namespace, same local part).
  4. 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.
  5. 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.
  6. 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:

  1. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {...}) in packages/extension/src/offscreen.ts registers a listener without visible validation of sender.id === chrome.runtime.id or message origin/schema.
  2. 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 .catch handler path or otherwise inject a crafted ProvisionProblemReportError-shaped response.
  3. The forged response is passed to sendResponse({ ok: false, ... code, candidates }) and forwarded to bridge-protocol.ts.
  4. onboard-view.tsx receives the forged code/candidates and, because matchesTrustTaskCode matches on content not on source authenticity, treats the forged message as a legitimate VTA-originated context-required picker.
  5. 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.
  6. 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:

  1. A malicious or compromised VTA (or spoofed message per STRIDE-4) returns a provision/integration:contextRequired problem-report with args/candidates containing deceptive strings (e.g., visually similar DID identifiers, or strings crafted to look like a trusted context name).
  2. 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 in res.candidates content downstream.
  3. onboard-view.tsx renders res.candidates in the picker UI (per the if (... && res.candidates && res.candidates.length > 0) branch) presumably via a list of selectable options, without shown validation that entries match expected DID/context syntax.
  4. The operator, trusting the wallet UI, selects a deceptive candidate believing it to be the correct integration context.
  5. The wallet proceeds to provision the ephemeral grant into the attacker-influenced context, potentially exfiltrating or misdirecting the trust relationship being established.
  6. 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:

  1. A caller invokes vault/delete with an attacker- or operator-controlled reason string intended to be 'recorded in the audit trail' per the doc comment in delete.ts.
  2. No cryptographic binding (e.g., signed audit record, hash-chained log) between the reason string and the invoking principal's identity is shown in the reduced code.
  3. If the VTA's audit trail merely stores the free-text reason without 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.
  4. This undermines forensic reconstruction of why a sensitive vault entry (e.g., a did-self-issued signing key) was deleted, especially when combined with the expectedVersion optimistic-concurrency check being bypassed via a version-conflict race (see STRIDE-7).
  5. 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:

  1. Two concurrent agents (or an attacker racing a legitimate operator) both read the same vault entry's current version.
  2. Agent A submits vault/delete with expectedVersion = N.
  3. Before the maintainer processes Agent A's request, Agent B updates the entry, bumping the version to N+1 through a legitimate vault/upsert.
  4. 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.
  5. Since this repo is the client library, an attacker crafting rapid parallel vault/delete and vault/upsert calls could exploit any server-side non-atomicity to delete an entry that a concurrent operation had just intentionally preserved/updated.
  6. 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:

  1. Attacker with valid but limited wallet access supplies an entryId pointing to a vault entry of an unintended kind (e.g., an imported raw key or a did-web entry not meant to sign Trust Tasks).
  2. packages/core/src/vault/sign-trust-task.ts forwards 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's notSignable rejection).
  3. 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.
  4. Attacker obtains a validly-signed Trust Task document (issuer = entry.principalDid) using key material intended for a different purpose (e.g., a did-web domain-verification key), enabling cross-protocol signature reuse or spoofed attestations.
  5. 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:

  1. 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 producing dist/) is skipped, cached, or fails silently.
  2. packages/core/tests/trust-tasks.error-code.mjs imports from ../dist/trust-tasks/error-code.js and ../dist/provision/send.js, which still contain the previous (correct) compiled logic.
  3. CI reports all tests passing, giving false confidence that the source-level regression is safe to merge and release.
  4. 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.
  5. 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:

  1. DEFAULT_TIMEOUT_MS = 60_000 bounds a single DIDComm round-trip, but nothing in the shown code enforces a minimum interval or maximum retry count across repeated sendProvisionIntegration calls.
  2. 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.tsx could rapidly re-invoke provisioning after every contextRequired reply.
  3. 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).
  4. 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.
  5. 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:

  1. packages/core/src/provision/index.ts now re-exports PROVISION_CONTEXT_REQUIRED publicly via the package barrel file (export { ..., PROVISION_CONTEXT_REQUIRED, ... }).
  2. Any consumer of the @openvtc/pnm-core npm 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 via trustTaskCodeSnakeCase.
  3. 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 matchesTrustTaskCode for both spellings.
  4. 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.
  5. 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
Loading

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
Loading

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
Loading

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
Loading

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
Loading

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

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