Skip to content

feat(provision): onboard over any transport the VTA advertises - #133

Merged
stormer78 merged 1 commit into
mainfrom
feat/provision-over-any-transport
Aug 29, 2026
Merged

feat(provision): onboard over any transport the VTA advertises#133
stormer78 merged 1 commit into
mainfrom
feat/provision-over-any-transport

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

The gap

Provisioning was the last VTA operation with no transport chain.

Every other one — vault, contexts, dids, device wake, acl — has gone through TrustTaskSender since #79, so it runs over TSP, DIDComm or REST according to what the agent publishes. runProvisionIntegration still took a DidcommMessageBridge, and offscreen.ts built its own mediator session instead of calling getVtaSession. Two consequences:

  • A wallet already connected over TSP had to open a DIDComm session just to onboard. The setup screen says so out loud: CONNECTED over TSP at the top, Transport: DIDComm (authcrypt) on the grant panel below it.
  • A VTA advertising no DIDComm could not be onboarded at all.

Why the bespoke path also cost us a bug

The VTA has served this operation through the shared dispatcher for some time — TASK_PROVISION_INTEGRATION_0_3trust_tasks/provision_integration.rs — taking the same request body and returning the same response body as the bespoke DIDComm handler beside it. We were using the bespoke one.

That handler labels its reply from a hand-written version→URI map, and the map was not moved when the router cut over to 0.3. So a provisioning that had fully succeeded — bundle sealed, admin rolled over, secret written — came back labelled provision/integration/0.1#response and was rejected here as an unexpected reply type, with the whole successful response body quoted into the error.

The dispatcher has no such map. It sets the #response fragment on the request URI it just parsed, so that class of bug cannot arise on this path. (VTI #1202 fixes the map itself, for the CLI clients still on it.)

sendProvisionIntegration is now buildTrustTask + sender.send, and reads almost exactly like swapAcl — the template for this shape since the ACL swap was collapsed.

The session speaks as the ephemeral

This is the part that needed care. The holder is what provisioning is about to mint, so it cannot exist yet — and the warm mediator pool authenticates as the holder, because getWarmSession calls loadHolder itself.

So getVtaSession splits in two:

buildVtaSession(vtaDid, who: SessionIdentity, connect: MediatorConnector, opts)
getVtaSession(vtaDid, restBaseUrl?)   // = the holder-over-the-warm-pool case

Onboarding passes the ephemeral and a connector that opens and closes its own connections. Handing the ephemeral a pooled connection would have sent its provisioning request under a DID the operator never granted; the split is what makes that unrepresentable rather than merely avoided.

Authorisation needed no VTA-side work. auth_from_message is auth_from_did(sender), and the dispatcher's trust-task path resolves claims the same way — so the ACL grant the operator just made authenticates the call identically whichever transport carries it. That was the main risk in this migration and it evaporated on inspection.

DIDComm leads for onboarding, deliberately

The chain is otherwise TSP-first, and stays that way everywhere else.

But a TSP reply timeout is a hard failure by design — a mutation may already have applied, and provisioning is as mutating as an operation gets — and TSP delivery to a just-granted ephemeral has never been exercised. The enrolled holder's mailbox is live-validated; the ephemeral's is a transient auth session, and the one headless attempt against a mailbox-less did:key timed out with zero frames.

Leading with DIDComm leaves the proven path exactly as it was, while still letting a VTA that advertises no DIDComm onboard over TSP or REST — the capability that was missing entirely. It is a one-line change to promote TSP once an ephemeral round-trip is confirmed against a live mediator, and the comment at the call site says so.

Three behaviour changes worth calling out

  • A mediator is no longer mandatory. MediatorRequiredError now fires only when a mediator is the sole remaining possibility and none was supplied, rather than before any transport is considered.
  • A mediator that will not connect skips its channel rather than failing the session — mirroring what the TSP branch already did. Safe for the same reason: this is pre-send, so nothing has been dispatched and nothing can have been applied twice. That is the distinction VtaSession draws when it falls back on e.client.unsupported but never on a post-send failure.
  • The request body is lowerCamelCase (createContext), the canonical 0.2+ form the registry declares and the generated payload type now enforces. The Rust struct still carries snake_case aliases so the old spelling would also be accepted — that is a fold on the VTA's side, not a reason to keep sending the legacy form. The signed VP is relayed byte-for-byte, as always.

The error channel

ProvisionProblemReportError is gone along with the DIDComm-specific error channel. provisionRefusalOf replaces it, reading the code and details.candidates off VtaClientError.detailsfields, not a parsed message (R3.7).

The popup is untouched: offscreen still forwards { code, candidates } and onboard-view still compares against PROVISION_CONTEXT_REQUIRED.

Depends on

VTI #1204. It makes the dispatcher emit provision/integration:contextRequired with structured candidates. Without it the spine refuses an ambiguous context as malformedRequest with the candidates rendered into a sentence, and the picker would have nothing to read — trading a working recovery UX for a string match. Merge that first.

Verification

  • npm run lint / npm run build / npm test — green (596 tests, 0 failures).
  • dist/background.js still a single bundle, no dynamic import().
  • Module layering and entry-point guards pass — provisionvta is a downward import.
  • New provision.trust-task.mjs (5 tests): the envelope is addressed ephemeral → VTA under the 0.3 URI with expectedResponseType set to its #response; the option fields go out camelCase while the signed VP is relayed byte-for-byte; and provisionRefusalOf recovers the code and candidate list, returns an empty list for a refusal that carries none, and undefined for errors with no framework payload behind them.

Not live-tested. No mediator + VTA available in this environment. The DIDComm path is the same channel class the rest of the wallet uses daily, but the ephemeral-as-session-identity wiring is new, so first run against glenn-vta is the real check.

Provisioning was the last VTA operation with no transport chain. Every other
one — vault, contexts, dids, device wake, acl — has gone through
`TrustTaskSender` since #79, so it runs over TSP, DIDComm or REST according to
what the agent publishes. `runProvisionIntegration` still took a
`DidcommMessageBridge`, and `offscreen.ts` built its own mediator session
rather than calling `getVtaSession`. A wallet already connected over TSP had to
open a DIDComm session to onboard, and a VTA advertising no DIDComm could not
be onboarded at all.

The VTA has served this operation through the shared dispatcher for some time
(`TASK_PROVISION_INTEGRATION_0_3` → `trust_tasks/provision_integration.rs`),
taking the same request body and returning the same response body as the
bespoke DIDComm handler beside it. We were using the bespoke one, and paid for
it a second way: that handler labels its reply from a hand-written version→URI
map which was not moved when the router cut over to 0.3, so a provisioning that
had fully succeeded — bundle sealed, admin rolled over, secret written — came
back labelled `provision/integration/0.1#response` and was rejected here as an
unexpected reply type. The dispatcher has no such map; it sets the `#response`
fragment on the request URI it just parsed, so the class of bug does not exist
on that path. (VTI #1202 fixes the map itself, for the CLI clients still on it.)

`sendProvisionIntegration` is now `buildTrustTask` + `sender.send`, and reads
almost exactly like `swapAcl`, which has been the template for this shape since
the ACL swap was collapsed.

**The session speaks as the ephemeral.** The holder is what provisioning is
about to mint, so it cannot exist yet, and the warm mediator pool authenticates
as the holder — it calls `loadHolder` itself. `getVtaSession` therefore splits:
`buildVtaSession` takes the identity and a `MediatorConnector`, and
`getVtaSession` is the holder-over-the-warm-pool case of it. Onboarding passes
the ephemeral and connections it opens and closes itself. Handing the ephemeral
a pooled connection would have sent its request under a DID the operator never
granted, which is the failure this split exists to make unrepresentable.

Authorisation is unchanged and needed no VTA-side work: `auth_from_message` is
`auth_from_did(sender)`, and the dispatcher's trust-task path resolves claims
the same way, so the ACL grant the operator just made authenticates the call
identically whichever transport carries it.

**DIDComm leads for onboarding, deliberately.** The chain is otherwise
TSP-first. But a TSP reply timeout is a hard failure by design — a mutation may
already have applied, and provisioning is as mutating as an operation gets —
and TSP delivery to a *just-granted ephemeral* has never been exercised: the
enrolled holder's mailbox is live-validated, the ephemeral's is a transient auth
session, and the one headless attempt against a mailbox-less did:key timed out.
Leading with DIDComm leaves the proven path exactly as it was while still
letting a VTA that advertises no DIDComm onboard over TSP or REST. Promote TSP
once an ephemeral round-trip is confirmed against a live mediator.

Three consequences worth naming:

- **A mediator is no longer mandatory.** `MediatorRequiredError` now fires only
  when a mediator is the sole remaining possibility and none was supplied,
  rather than before any transport is considered.
- **A mediator that will not connect skips its channel** instead of failing the
  session, mirroring what the TSP branch already did. Safe for the same reason:
  pre-send, so nothing has been dispatched and nothing can have been applied
  twice — the distinction `VtaSession` draws when it falls back on
  `e.client.unsupported` but never on a post-send failure.
- **The request body is lowerCamelCase** (`createContext`), the canonical 0.2+
  form the registry declares and the generated payload type now enforces. The
  Rust struct still carries snake_case aliases, so the old spelling would also
  be accepted; that is a fold on the VTA's side, not a reason to keep sending
  the legacy form. The signed VP is relayed byte-for-byte, as always.

`ProvisionProblemReportError` is gone with the DIDComm-specific error channel.
`provisionRefusalOf` replaces it, reading the code and `details.candidates` off
`VtaClientError.details` — fields, not a parsed message (R3.7). The popup's
context picker is untouched: offscreen still forwards `{code, candidates}` and
`onboard-view` still compares against `PROVISION_CONTEXT_REQUIRED`.

Needs VTI #1204, which makes the dispatcher emit that canonical code with
structured candidates. Without it the spine refuses an ambiguous context as
`malformedRequest` with the candidates rendered into a sentence, and the picker
would have nothing to read — trading a working recovery UX for a string match.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 merged commit b342005 into main Aug 29, 2026
3 checks passed
@stormer78
stormer78 deleted the feat/provision-over-any-transport branch August 29, 2026 11:47
@affinidi-appsecurity-bot

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

1 AI-confirmed issue, 3 findings need a human to review/validate.

Mandatory to check: 🔒 Security Code Review Report

Details

🛡️ Security Code Review Report — PR #133

Field Value
Repository OpenVTC/vta-browser-plugin
Branch feat/provision-over-any-transportmain
Validated 2026-08-29
Scan ID ba74411a
Validator AI Security Validation Agent

🗺️ Scan Coverage

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

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

Executive Summary

Category Confirmed Must-Review-By-Human
Security Issues 1 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

Confirmed Vulnerabilities (1)

🔵 Silent discard of malformed VTA refusal payloads with no forensic logging

Field Detail
Severity LOW
Location packages/core/src/provision/send.ts:117
Finding ID github_pr-43ae29bbb6b7
CWE CWE-778, CWE-393, CWE-544
OWASP A09:2021 - Security Logging and Monitoring Failures
MITRE ATT&CK T1562.006 - Impair Defenses: Indicator Blocking (analogous - loss of forensic signal)
CAPEC CAPEC-268
DREAD 5
Reachability 🔴 Reachable
Exploit Maturity poc
Detection Source skill_scan

Summary: This function correctly distinguishes structured refusals from generic errors but discards the raw payload without any diagnostic logging when the payload shape is unexpected, weakening incident response and enabling an attacker probing the protocol to leave no client-side trace.

📝 Description:

Operators and support engineers lose the ability to diagnose why a provisioning attempt failed when the VTA returns an unexpected error shape; a legitimate PROVISION_CONTEXT_REQUIRED refusal with a malformed payload (e.g., due to a VTA-side bug or version mismatch) is silently treated as opaque failure, degrading UX and incident forensics without any compromise of confidentiality/integrity.

🧪 Proof of Concept:

The two early return undefined paths (lines ~111 and ~115) discard potentially security-relevant information — including a hypothetically attacker-manipulated or genuinely-malformed refusal from the VTA — with no logging, console output, or telemetry hook.

export function provisionRefusalOf(e: unknown): ProvisionRefusal | undefined {
  if (!(e instanceof VtaClientError)) return undefined;
  const payload = e.details as
    | { code?: unknown; message?: unknown; details?: { candidates?: unknown } }
    | undefined;
  if (!payload || typeof payload.code !== "string") return undefined;
  const raw = payload.details?.candidates;
  return {
    code: payload.code,
    message: typeof payload.message === "string" ? payload.message : e.message,
    candidates: Array.isArray(raw) ? raw.filter((c): c is string => typeof c === "string") : [],
  };
}

Vulnerable lines: 110, 133

🔎 Evidence: packages/core/src/provision/send.ts:117

export function provisionRefusalOf(e: unknown): ProvisionRefusal | undefined {
  if (!(e instanceof VtaClientError)) return undefined;
  const payload = e.details as ...;
  if (!payload || typeof payload.code !== "string") return undefined;

💥 Impact:

Operators and support engineers lose the ability to diagnose why a provisioning attempt failed when the VTA returns an unexpected error shape; a legitimate PROVISION_CONTEXT_REQUIRED refusal with a malformed payload (e.g., due to a VTA-side bug or version mismatch) is silently treated as opaque failure, degrading UX and incident forensics without any compromise of confidentiality/integrity.

Confidentiality: None · Integrity: Low — could mask a genuine refusal, causing incorrect retry logic downstream. · Availability: Low — degrades incident response time, not a direct outage.

🧭 Reachability:

  • Network exposure: internal
  • Auth barrier: basic
  • Attack path: EP-003 (VTA reply, attacker-influenced if VTA compromised or misbehaving) → sendProvisionIntegration throws VtaClientError → EP-004 (provisionRefusalOf) → undefined return, silently dropped by offscreen.ts caller

⚖️ Triage Factors:

Factor Value
Fixable ✅ Yes
Exploitability low
Business impact low
Public exploit None known
Environment unknown

Attack scenario: A malformed or attacker-influenced VTA refusal payload is silently discarded with no logging, weakening forensic visibility into provisioning failures.

🔧 Remediation:

⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.

Adds a diagnostic log capturing the raw, unparseable error details before returning undefined, preserving forensic evidence without changing the function's public contract or return type.

Vulnerable code:

if (!(e instanceof VtaClientError)) return undefined;
const payload = e.details as ...;
if (!payload || typeof payload.code !== "string") return undefined;

Secure code:

if (!(e instanceof VtaClientError)) return undefined;
const payload = e.details as
  | { code?: unknown; message?: unknown; details?: { candidates?: unknown } }
  | undefined;
if (!payload || typeof payload.code !== "string") {
  // Preserve forensic evidence instead of silently discarding.
  console.warn("provisionRefusalOf: VtaClientError with unparseable details payload", { details: e.details, message: e.message });
  return undefined;
}

Additional recommendations:

  • Emit a structured telemetry event (not just console.warn) distinguishing 'not a VtaClientError' from 'VtaClientError with unparseable payload' for centralized monitoring.
  • Add schema validation against known refusal codes with alerting on unrecognized codes.

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 85%
  • AI Validation Evidence: EVIDENCE FOUND: provisionRefusalOf in send.ts: 'if (!(e instanceof VtaClientError)) return undefined;' and 'if (!payload || typeof payload.code !== "string") return undefined;' — both paths return undefined with no console.log/logger call anywhere in the function or surrounding file. Searched the whole send.ts file body for any logging call (console., logger., trace) — none exists. EVIDENCE NOT FOUND: No forensic logging call in the provided source; offscreen.ts (the presumed caller) is not shown in this diff's relevant section to confirm it independently logs e.details before calling this function, but that does not change what this function itself does. CHANGED VS PRE-EXISTING: CHANGED — provisionRefusalOf is defined in send.ts, one of the files touched by this MR (part of the DIDComm→Trust-Task migration), so the finding is in scope. VERDICT JUSTIFICATION: The function's behavior exactly matches the finding — malformed payloads are silently discarded with zero logging, directly observable in the quoted code, so this is validated as a low-severity logging/observability gap.
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

⚠️ 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.

🟠 Removal of explicit reply correlation (thid) and sender (from/vtaDid) checks with unverified replacement in delegated transport layer

Field Detail
Severity HIGH
Location packages/core/src/provision/send.ts:130
Finding ID github_pr-baed67d502b6
CWE CWE-345, CWE-353, CWE-290
OWASP A08:2021 - Software and Data Integrity Failures, A07:2021 - Identification and Authentication Failures
MITRE ATT&CK T1557 - Adversary-in-the-Middle, T1078 - Valid Accounts
CAPEC CAPEC-593, CAPEC-141
DREAD 5.6
Reachability 🔴 Reachable
Exploit Maturity conceptual
Detection Source skill_scan

Summary: A structural change removes explicit request/reply correlation (thid) and sender-identity (from == vtaDid) validation from the provisioning client and delegates it to an unreviewed transport abstraction. If that abstraction does not reimplement equivalent checks, a network-positioned attacker could cause the wallet to accept a mismatched or forged provisioning reply for the sensitive AdminRotation flow.

📝 Description:

If the replacement transport layer does not reimplement equivalent checks, an attacker positioned on the transport (compromised mediator, TSP relay, or MITM) could cause the wallet to accept a forged or cross-request reply for the AdminRotation provisioning flow, potentially leading to acceptance of an attacker-controlled admin DID/key bundle or a denial-of-service via reply confusion.

🧪 Proof of Concept:

The removed code was the client's last line of defense ensuring a reply was actually correlated to the specific request it sent and originated from the expected VTA identity. Its replacement's internal logic cannot be verified from the files provided in this PR, and the PASTA/STRIDE analysis flags this as an unconfirmed regression risk.

// [REMOVED FROM DIFF]
const reply = await bridge.sendAndAwaitReply(outer, requestId, { timeoutMs });

if (reply.thid !== requestId) {
  throw new Error(`provision-integration: reply thid ${reply.thid ?? "(none)"} != request ${requestId}`);
}
if (reply.from !== vtaDid) {
  throw new Error(`provision-integration: reply from ${reply.from ?? "(none)"} != VTA ${vtaDid}`);
}
// --- replaced in current version by: ---
// buildTrustTask(...) + sender.send(...) — implementation not in scope

Vulnerable lines: 1, 50

🔎 Evidence: packages/core/src/provision/send.ts:130

if (reply.thid !== requestId) {
  throw new Error(`provision-integration: reply thid ${reply.thid ?? "(none)"} != request ${requestId}`);
}
if (reply.from !== vtaDid) {
  throw new Error(`provision-integration: reply from ${reply.from ?? "(none)"} != VTA ${vtaDid}`);
}

💥 Impact:

If the replacement transport layer does not reimplement equivalent checks, an attacker positioned on the transport (compromised mediator, TSP relay, or MITM) could cause the wallet to accept a forged or cross-request reply for the AdminRotation provisioning flow, potentially leading to acceptance of an attacker-controlled admin DID/key bundle or a denial-of-service via reply confusion.

Confidentiality: High — potential exposure of admin DID/private key bundle to unintended recipient if reply/sender validation is genuinely missing downstream. · Integrity: High — acceptance of a wrong/forged reply could corrupt the onboarding state machine or bind the wallet to an unintended admin identity. · Availability: Low — worst case is a failed/hung onboarding, not a broader outage.

🧭 Reachability:

  • Network exposure: internal
  • Auth barrier: basic
  • Attack path: EP-001 (runProvisionIntegration) → EP-002 (sendProvisionIntegration) → sender.send()/buildTrustTask() [unverified] → openAdminRotationBundle

⚖️ Triage Factors:

Factor Value
Fixable ✅ Yes
Exploitability medium
Business impact high
Public exploit None known
Environment unknown

Attack scenario: A transport-layer attacker (compromised mediator/relay) attempts to inject a mismatched or forged provisioning reply, exploiting the unverified removal of explicit thid/from checks now delegated to TrustTaskSender.

🔧 Remediation:

⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.

Re-instate an explicit, visible correlation-id and sender-identity check at the provision/send.ts call site rather than relying entirely on an opaque shared abstraction. This preserves defense-in-depth even if TrustTaskSender's internal implementation is later refactored or has a bug.

Vulnerable code:

// send.ts (current, truncated) — correlation/auth is delegated:
const reply = await sender.send(buildTrustTask(...));
// (no visible thid/from equivalent check in the reviewed portion)

Secure code:

const task = buildTrustTask({ type: PROVISION_INTEGRATION, to: vtaDid, from: ephemeralDid, body });
const reply = await sender.send(task, { timeoutMs });

// Explicitly re-verify correlation and sender identity at the call site,
// even if the transport layer is expected to do so — defense in depth:
if (reply.correlationId !== task.id) {
  throw new VtaClientError(`reply correlation mismatch: ${reply.correlationId} != ${task.id}`);
}
if (reply.from !== vtaDid) {
  throw new VtaClientError(`reply sender mismatch: ${reply.from} != ${vtaDid}`);
}

Additional recommendations:

  • Add unit/integration tests in packages/core/tests/provision.trust-task.mjs that explicitly assert rejection of mismatched correlation IDs and spoofed sender DIDs.
  • Document TrustTaskSender's correlation/authentication contract in its TypeScript interface with explicit invariants.
  • Add server-side (VTA) audit logging of provision/integration replies keyed by request nonce for forensic cross-checking.

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 55%
  • AI Validation Evidence: EVIDENCE FOUND: send.ts shows the new implementation delegates correlation entirely to buildTrustTask()/sender.send(): 'const envelope = buildTrustTask(PROVISION_INTEGRATION, opts.body, { issuer: opts.ephemeralDid, recipient: opts.vtaDid }); return opts.sender.send<...>(envelope, { expectedResponseType: PROVISION_INTEGRATION_RESULT, operationLabel: "provision/integration/0.3", timeoutMs: ... })'. The removed inline checks (reply.thid !== requestId, reply.from !== vtaDid) are indeed absent from this file. run.ts partially compensates: it cross-checks reply.summary.bundleIdHex !== expectedHex (nonce binding) and reply.summary.adminDid !== admin.did, and the HPKE AAD binds the bundle id per comments, providing some equivalent-in-spirit protection at a different layer. EVIDENCE NOT FOUND: packages/core/src/vta/trust-task.ts (buildTrustTask) and packages/core/src/vta/channel.ts (TrustTaskSender/expectedResponseType handling) are not included in source_files, so it cannot be confirmed whether sender.send reimplements thread/sender-identity verification (e.g. checking from/thid equivalent) before returning the payload to the caller. CHANGED VS PRE-EXISTING: CHANGED — send.ts is the file with the finding's exact removed code (the diff shows the old inline checks replaced by buildTrustTask/sender.send in this same file), so the vulnerable construct chain is squarely in the changed file. VERDICT JUSTIFICATION: Since the deciding correlation-check code (inside buildTrustTask/TrustTaskSender) is not present in provided files, exploitability cannot be confirmed, but partial mitigations (bundleIdHex/adminDid cross-checks in run.ts) exist; per rules this must be must_review, not validated or dismissed.
  • 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.

🟡 No transport-tier pinning for admin-rotation provisioning — silent fallback to REST removes prior E2E authcrypt guarantee

Field Detail
Severity MEDIUM
Location packages/core/src/provision/send.ts:1
Finding ID github_pr-053bcc33759e
CWE CWE-757, CWE-300, CWE-326
OWASP A02:2021 - Cryptographic Failures
MITRE ATT&CK T1040 - Network Sniffing, T1557 - Adversary-in-the-Middle
CAPEC CAPEC-610, CAPEC-594
Reachability 🔴 Reachable
Exploit Maturity conceptual
Detection Source skill_scan

Summary: The refactor removes the requirement for a reachable DIDComm mediator (with its authcrypt-inner + authcrypt-forward-outer E2E encryption) and replaces it with an opaque priority-ordered transport chain that can silently fall back to REST for the wallet's most sensitive operation — admin key rotation — without any caller-visible warning or opt-out.

📝 Description:

If REST does not carry equivalent end-to-end confidentiality/integrity protection to the removed authcrypt layer, the signed BootstrapRequest VP and the HPKE-sealed AdminRotation bundle could traverse a network path with a larger observation/tampering surface than the original design assumed, even though HPKE sealing and VP signing provide some independent protection.

🧪 Proof of Concept:

This comment documents the removal of the mandatory DIDComm mediator requirement (and its authcrypt E2E encryption) in favor of an automatic, unpinned transport-priority fallback that includes REST — with no caller-facing control to require a minimum transport tier for this specific high-sensitivity operation.

// This used to be a bespoke DIDComm protocol message: authcrypt-inner +
// authcrypt-forward-outer packed here...
// It is now `buildTrustTask` + `sender.send`, exactly like every other VTA
// operation — which means it runs over whichever transport the VTA
// advertises, priority TSP > DIDComm > REST, rather than requiring a
// reachable DIDComm mediator.

Vulnerable lines: 1, 20

🔎 Evidence: packages/core/src/provision/send.ts:1

// which means it runs over whichever transport the VTA advertises, priority
// TSP > DIDComm > REST, rather than requiring a reachable DIDComm mediator.

💥 Impact:

If REST does not carry equivalent end-to-end confidentiality/integrity protection to the removed authcrypt layer, the signed BootstrapRequest VP and the HPKE-sealed AdminRotation bundle could traverse a network path with a larger observation/tampering surface than the original design assumed, even though HPKE sealing and VP signing provide some independent protection.

🧭 Reachability:

  • Network exposure: internal
  • Auth barrier: basic
  • Attack path: EP-001 (runProvisionIntegration) → EP-002 (sendProvisionIntegration) → sender (TrustTaskSender, transport auto-selected) → REST fallback [if TSP/DIDComm unavailable]

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 45%
  • AI Validation Evidence: EVIDENCE FOUND: The header comment in send.ts states: 'which means it runs over whichever transport the VTA advertises, priority TSP > DIDComm > REST, rather than requiring a reachable DIDComm mediator.' There is no transport or pin parameter in SendProvisionIntegrationOptions or RunProvisionIntegrationOptions to restrict transport tier. EVIDENCE NOT FOUND: The actual REST transport implementation and its confidentiality/authentication properties (packages/core/src/vta/channel.ts or a REST-specific sender) are not in source_files, so whether REST provides equivalent-or-weaker guarantees than the old bespoke DIDComm authcrypt cannot be confirmed. HPKE-sealing of the bundle itself (independent of transport) is documented and provides payload confidentiality regardless of transport, per types.ts / run.ts comments about openAdminRotationBundle. CHANGED VS PRE-EXISTING: CHANGED — send.ts (this MR) removed the DIDComm-only requirement and introduced the multi-transport priority comment; this is a design decision explicit in the changed file. VERDICT JUSTIFICATION: Cannot confirm actual security degradation without seeing REST transport implementation; the HPKE seal mitigates some risk but caller-facing transport pinning is genuinely absent from the reviewed API surface — insufficient evidence to validate or dismiss, so must_review.
  • Validation Effort: This finding was validated up to a point, but the available evidence was insufficient for a conclusive automated verdict. A human (developer / security team) must manually review the code and decide. Not dismissed — treat as an open item pending human review.

🔵 Unsafe Formatstring (2 occurrences)

Field Detail
Severity LOW
Location packages/extension/src/offscreen.ts:781
Finding ID github_pr-cd63f39bb6ba
OWASP A01:2021 - Broken Access Control
CVSS 4.0 3.5
Exploit Maturity conceptual
Detection Source mcp_semgrep

Summary: Detected string concatenation with a non-literal variable in a util.format / console.log function. If an attacker injects a format specifier in the string, it will forge the log message. Try to use co — 2 occurrence(s): offscreen.ts:781, offscreen.ts:790

📝 Description:

Detected string concatenation with a non-literal variable in a util.format / console.log function. If an attacker injects a format specifier in the string, it will forge the log message. Try to use co

🌱 Root Cause: Unsafe Formatstring

🔧 Remediation:

⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.

Priority: Short-term

Unsafe Formatstring: Detected string concatenation with a non-literal variable in a util.format / console.log function. If an attacker injects a format specifier in the string, it will forge the log message. Try to use co

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 20%
  • AI Validation Evidence: EVIDENCE FOUND: The finding references packages/extension/src/offscreen.ts line 781 for an 'Unsafe Formatstring' issue (string concatenation with a non-literal variable passed to console.log/util.format). This file is NOT included in source_files, so no code snippet, evidence_type is 'code' with an empty code_snippet field. EVIDENCE NOT FOUND: The actual offscreen.ts source around line 781 is absent from all provided files (only send.ts, run.ts, types.ts, tests, tsp-js, reviewer-demo/server.mjs were given) — cannot see the alleged log-injection sink or verify attacker-controlled input reaches it. CHANGED VS PRE-EXISTING: Cannot determine — offscreen.ts is plausibly touched by this MR (it's the likely caller of sendProvisionIntegration/provisionRefusalOf per other findings' descriptions), but it is not in the changed-files list visible here nor in source_files, so scope cannot be confirmed either way. VERDICT JUSTIFICATION: With an empty code_snippet and the file entirely absent from provided source, there is no way to confirm or refute the vulnerable construct or its reachability from untrusted input — must_review per the hard rule against validating without seeing the sink.
  • 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 #133

Field Value
Repository OpenVTC/vta-browser-plugin
Branch feat/provision-over-any-transportmain
Generated 2026-08-29

ℹ️ This report contains theoretical threats and impact analysis for the MR.
Unlike the Security Code Review Report (which contains confirmed, materialised issues),
these are potential risks that may or may not be exploitable. Use this for defence-in-depth planning.


📋 Affect Analysis

Change Summary

Migrates the wallet's provision-integration (admin onboarding) round-trip from a bespoke, hand-rolled DIDComm authcrypt/forward protocol to a generic, multi-transport Trust Task abstraction (TrustTaskSender, priority TSP > DIDComm > REST). This removes the hard dependency on a reachable DIDComm mediator and fixes a prior production bug (VTI #1202) where a stale version map mislabeled 0.3 replies as 0.1. The change also replaces a bespoke ProvisionProblemReportError class with a generic VtaClientError + provisionRefusalOf() extractor.

Diff: +245 / -190 lines
Types: security, feature, refactor

📁 File Classifications

packages/core/src/provision/index.ts

  • Type: security

packages/core/src/provision/run.ts

  • Type: security

packages/core/src/provision/send.ts

  • Type: security

packages/core/tests/provision.trust-task.mjs

  • Type: test

packages/extension/src/offscreen.ts

  • Type: security

🛡️ STRIDE Threat Model

Identified Threats (11)

⚪ STRIDE-1: Missing Reply Correlation Check in TrustTaskSender Abstraction

Field Detail
Category Spoofing, Tampering
Severity High
Likelihood Likely
CVSS 8.1 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-345,CWE-353
CAPEC CAPEC-593,CAPEC-141
OWASP A08:2021 - Software and Data Integrity Failures

Description: sendProvisionIntegration in COMP-003 allows reply-spoofing or cross-request confusion due to removal of explicit thid/from cross-checks previously enforced inline, resulting in acceptance of a mismatched or attacker-influenced reply as the legitimate provisioning bundle.

Evidence: packages/core/src/provision/send.ts:N/A (removed code region)

if (reply.thid !== requestId) { throw new Error(...) } if (reply.from !== vtaDid) { throw new Error(...) }

Attack Scenario:

  1. Legacy send.ts explicitly checked reply.thid !== requestId and reply.from !== vtaDid inline before accepting the DIDComm reply.
  2. The new implementation delegates transport and correlation entirely to buildTrustTask + sender.send (packages/core/src/vta/trust-task.ts, packages/core/src/vta/channel.ts), whose correlation/authentication logic is not present in this PR's provided files.
  3. An attacker who can influence the underlying transport (e.g., a malicious relay in the TSP > DIDComm > REST chain) could attempt to inject a reply that lacks equivalent thid/from validation if buildTrustTask/TrustTaskSender do not reimplement the same checks.
  4. If validation is weaker or absent in the new abstraction, the wallet could accept a bundle from an unintended sender or for an unrelated request.
  5. openAdminRotationBundle in run.ts would then process attacker-influenced bundle data, potentially rotating the admin DID with attacker-controlled failure or acceptance semantics.

Preconditions: Attacker has some level of transport-layer access (malicious relay, mediator compromise, or MITM on TSP/DIDComm/REST channel)., buildTrustTask/TrustTaskSender/VtaClientError implementations (not in scope) fail to reimplement thid/from or equivalent nonce/sender checks.

Existing Controls: HPKE-sealed bundle requires the wallet's ephemeral Ed25519 seed to open (openAdminRotationBundle). • bundleIdHex cross-check against the request nonce is still documented as Step 4 of the round-trip in run.ts comments.

Recommended Mitigations: Verify that buildTrustTask/sender.send reimplement equivalent-or-stronger correlation (request-id/thid) and sender-identity (from/vtaDid) checks before merging. • Add unit tests asserting rejection of replies with mismatched correlation id or sender DID. • Document the correlation contract of TrustTaskSender explicitly in its interface.


⚪ STRIDE-2: Silent Downgrade of Structured Error Handling via provisionRefusalOf

Field Detail
Category Repudiation, Denial of Service
Severity Medium
Likelihood Possible
CVSS 5.3 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-393
CAPEC CAPEC-268
OWASP A09:2021 - Security Logging and Monitoring Failures

Description: provisionRefusalOf in COMP-003 allows silent loss of refusal context due to returning undefined for any non-VtaClientError or malformed payload, resulting in callers treating a genuine VTA refusal (e.g. context-required) as an unhandled/unknown error and potentially retrying or bypassing recovery UX.

Evidence: packages/core/src/provision/send.ts:N/A

export function provisionRefusalOf(e: unknown): ProvisionRefusal | undefined {
  if (!(e instanceof VtaClientError)) return undefined;
  ...
  if (!payload || typeof payload.code !== "string") return undefined;
  ...

Attack Scenario:

  1. A malicious or misbehaving VTA replies with a refusal whose details payload has code as a non-string or missing (packages/core/src/provision/send.ts, provisionRefusalOf).
  2. provisionRefusalOf returns undefined because typeof payload.code !== "string".
  3. The offscreen.ts caller (not shown, but implied entry point EP-004) cannot distinguish this from a transport failure and falls back to generic error handling.
  4. The context-required recovery picker UX (candidates list) never renders, and the wallet may either silently fail to provision or the operator retries blindly, potentially against a rate limit or with a now-expired ephemeral grant.
  5. Because there is no explicit logging of the raw refusal payload before it is discarded, the operator/support team cannot audit what the VTA actually said, weakening incident forensics.

Preconditions: VTA returns a malformed or unexpected error payload shape., Caller code (offscreen.ts) does not independently log the raw VtaClientError.details before calling provisionRefusalOf.

Existing Controls: VtaClientError.details is documented as carrying the framework's error payload verbatim (R3.7 guide rule referenced in comments).

Recommended Mitigations: Log the raw e.details payload whenever provisionRefusalOf returns undefined for a VtaClientError, to preserve forensic evidence. • Add a fallback generic-refusal path distinguishing 'not a VtaClientError' from 'VtaClientError with unparseable payload'. • Add schema validation for known refusal codes with alerting on unrecognized ones.


⚪ STRIDE-3: Transport Downgrade Enabling Weaker Channel Selection in TrustTaskSender

Field Detail
Category Spoofing, Tampering, Information Disclosure
Severity High
Likelihood Possible
CVSS 7.1 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-757,CWE-300
CAPEC CAPEC-610,CAPEC-594
OWASP A02:2021 - Cryptographic Failures

Description: sender.send in COMP-003 allows silent selection of a weaker transport due to the documented priority fallback chain TSP > DIDComm > REST being negotiated without explicit caller pinning, resulting in the provisioning round-trip for admin key rotation occurring over a less-authenticated or less-confidential channel than intended.

Evidence: packages/core/src/provision/send.ts:N/A

// which means it runs over whichever transport the VTA advertises, priority TSP > DIDComm > REST, rather than requiring a reachable DIDComm mediator.

Attack Scenario:

  1. runProvisionIntegration (run.ts) calls sendProvisionIntegration with a generic sender: TrustTaskSender (packages/core/src/provision/run.ts) with no channel-pinning parameter.
  2. The comment in send.ts states the transport priority is 'TSP > DIDComm > REST', implying automatic fallback when a preferred transport is unavailable.
  3. An attacker capable of disrupting or blocking the higher-priority transport (e.g., blocking TSP or a DIDComm mediator) can force fallback to REST, which the code's own header notes previously required 'a reachable DIDComm mediator' for confidentiality guarantees the bespoke handler provided.
  4. If REST lacks the equivalent authcrypt-style end-to-end encryption that DIDComm provided (packAuthcrypt/packAuthcryptJson, now removed), the BootstrapRequest VP and reply bundle could traverse a channel with weaker confidentiality/integrity guarantees than the original design assumed.
  5. The wallet completes admin DID rotation believing it used a secure channel, while the actual bytes traveled over a downgraded transport, exposing the signed VP and sealed bundle to a broader observation surface.

Preconditions: Attacker can selectively degrade availability of preferred transports (TSP, DIDComm) to force REST fallback., REST transport implementation (not in scope) lacks equivalent end-to-end authcrypt-style protection that was removed with packAuthcrypt/packAuthcryptJson.

Existing Controls: HPKE-sealed bundle keeps the admin rotation payload confidential regardless of outer transport. • BootstrapRequest VP is signed, giving integrity independent of the transport.

Recommended Mitigations: Allow callers to pin or exclude specific transports for high-sensitivity operations like admin rotation. • Document and enforce a minimum transport security tier for provision/integration requests. • Emit a warning/telemetry event when fallback to a lower-tier transport occurs during provisioning.


⚪ STRIDE-4: Ephemeral DID Reuse Across Transport Migration Without Rotation Enforcement

Field Detail
Category Spoofing, Elevation of Privilege
Severity Medium
Likelihood Possible
CVSS 6.9 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-294,CWE-664
CAPEC CAPEC-60,CAPEC-39
OWASP A07:2021 - Identification and Authentication Failures

Description: ephemeralDid field in SendProvisionIntegrationOptions (COMP-003) allows replay of an operator-granted ephemeral did:key due to lack of visible single-use/TTL enforcement in the provided client code, resulting in potential reuse of the same ephemeral identity for multiple provisioning attempts including by an attacker who obtains the private key material.

Evidence: packages/core/src/provision/run.ts:N/A

ephemeralSigning: SigningIdentity; ... vtaDid: string;

Attack Scenario:

  1. runProvisionIntegration accepts ephemeralSigning: SigningIdentity whose did is passed as ephemeralDid to sendProvisionIntegration (run.ts).
  2. The comment states 'the operator-granted ephemeral did:key' authorizes the call and 'the grant the operator just made is what authorises this call whichever channel carries it.'
  3. If the ephemeral private key material (opts.ephemeralSigning.privateKey) is exposed — e.g., via a compromised offscreen.ts context or extension storage — an attacker can replay a BootstrapRequest VP signed with the same key against the VTA within the grant's TTL window.
  4. Because context inference and create_context defaults are server-side, a second provisioning attempt using the same ephemeral DID before TTL expiry could succeed if the VTA does not enforce single-use nonces per ephemeral grant.
  5. This results in unauthorized admin DID rotation performed by the attacker using the legitimately-granted-but-leaked ephemeral identity, effectively hijacking the onboarding flow.

Preconditions: Attacker obtains the ephemeral Ed25519/X25519 private key material (e.g., via extension memory disclosure, offscreen.ts compromise, or insecure storage)., VTA-side does not enforce single-use consumption of the ephemeral grant.

Existing Controls: Grant is described as time-limited (TTL) per the context-required recovery UX comments. • bundleIdHex nonce cross-check ties a specific reply to a specific request.

Recommended Mitigations: Enforce single-use consumption of the ephemeral grant server-side upon first successful provisioning. • Minimize ephemeral private key exposure window in the browser extension (zeroize after use). • Add client-side detection/alerting if the same ephemeral DID is reused across multiple runProvisionIntegration calls.


⚪ STRIDE-5: Untrusted Candidate List Rendered from VTA Refusal Payload

Field Detail
Category Tampering, Spoofing
Severity Medium
Likelihood Possible
CVSS 5.1 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:P/VC:N/VI:L/VA:N/SC:L/SI:N/SA:N
Residual Severity Low
CWE CWE-79,CWE-20
CAPEC CAPEC-63,CAPEC-85
OWASP A03:2021 - Injection

Description: candidates field in ProvisionRefusal (COMP-003) allows injection of attacker/VTA-controlled string arrays due to unsanitized pass-through from payload.details.candidates into the wallet's recovery picker UI, resulting in potential UI-layer injection or spoofed context names presented to the operator during the context-required recovery flow.

Evidence: packages/core/src/provision/send.ts:N/A

candidates: Array.isArray(raw) ? raw.filter((c): c is string => typeof c === "string") : [],

Attack Scenario:

  1. A malicious or compromised VTA responds to provision/integration with a PROVISION_CONTEXT_REQUIRED refusal whose details.candidates array contains attacker-crafted strings (send.ts, provisionRefusalOf).
  2. provisionRefusalOf filters only for typeof c === "string" but performs no length limit, encoding validation, or content sanitization on candidate values.
  3. offscreen.ts (not shown in this PR but referenced as the caller) is expected to render report.candidates as a picker UI, per the code comments describing 'the popup's context-required recovery picker'.
  4. If the extension's UI renders these strings without escaping (e.g., via innerHTML or a template that does not encode), an attacker-controlled VTA could inject misleading context names (social engineering) or, in a worse case, markup/script if the rendering path is unsafe.
  5. The operator, trusting the wallet UI, could select a spoofed 'context' entry, causing the admin DID to be provisioned into an unintended or attacker-steered maintainer context.

Preconditions: Operator has connected to (or been redirected to) a malicious or compromised VTA., offscreen.ts / popup rendering path does not sanitize or escape candidate strings before display.

Existing Controls: Type filtering ensures only string entries survive (Array.isArray(raw) ? raw.filter(...) : []).

Recommended Mitigations: Sanitize/escape candidate strings before rendering in any HTML-based UI context. • Validate candidate strings against expected DID/context syntax before display. • Cap the number and length of rendered candidates to prevent UI-based denial of service or spoofing via lookalike names.


⚪ STRIDE-6: Loss of Explicit Timeout Enforcement Semantics During Transport Abstraction

Field Detail
Category Denial of Service
Severity Low
Likelihood Possible
CVSS 4.0 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-400,CWE-664
CAPEC CAPEC-227
OWASP A04:2021 - Insecure Design

Description: timeoutMs option in SendProvisionIntegrationOptions (COMP-003) allows indefinite hang or resource exhaustion due to delegation of timeout enforcement to buildTrustTask/sender.send whose implementation is not visible in this PR, resulting in potential denial of service against the extension's offscreen document if the abstraction fails to honor the configured timeout.

Evidence: packages/core/src/provision/send.ts:N/A

timeoutMs?: number; // Request-side timeout. Default 60s

Attack Scenario:

  1. Legacy code explicitly called bridge.sendAndAwaitReply(outer, requestId, { timeoutMs }) with visible timeout enforcement in send.ts.
  2. New code delegates entirely to buildTrustTask + sender.send (packages/core/src/vta/trust-task.ts, packages/core/src/vta/channel.ts), neither of which is included in this PR for review.
  3. A malicious or unresponsive VTA/mediator/relay can withhold a reply indefinitely.
  4. If the new abstraction does not propagate timeoutMs correctly to the underlying transport-specific wait logic across all three transports (TSP, DIDComm, REST), the awaiting Promise in the browser extension's offscreen document may never resolve or reject.
  5. This ties up the offscreen document's execution context indefinitely, potentially blocking subsequent onboarding attempts or leaking a pending promise/listener per attempt (resource exhaustion under repeated attempts).

Preconditions: buildTrustTask/TrustTaskSender implementation does not correctly honor timeoutMs across all supported transports., Attacker or misbehaving VTA can selectively withhold replies.

Existing Controls: Default timeout constant (60s) is defined at the send.ts module level and documented to match the Rust SDK. • Comment states handler work is synchronous inside one handler call, bounding server-side processing time.

Recommended Mitigations: Add explicit unit tests asserting sender.send rejects/times out at timeoutMs across TSP, DIDComm, and REST paths. • Implement a client-side watchdog independent of sender.send for the offscreen document. • Ensure onboarding UI provides a manual cancel path in case of a hang.


⚪ STRIDE-7: API Surface Break Removing Typed Error Export Without Deprecation Path

Field Detail
Category Denial of Service
Severity Low
Likelihood Likely
CVSS 3.1 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:L
Residual Severity Low
CWE CWE-1104
CAPEC CAPEC-664
OWASP A06:2021 - Vulnerable and Outdated Components

Description: index.ts export barrel in COMP-002 allows silent breakage of downstream consumers due to the hard removal of ProvisionProblemReportError and ProblemReportPayload in favor of provisionRefusalOf/ProvisionRefusal without a deprecated compatibility shim, resulting in build-time or runtime failures in any external caller (e.g., other extension modules or third-party integrators) still importing the old symbols.

Evidence: packages/core/src/provision/index.ts:22-27

export {
  sendProvisionIntegration,
  provisionRefusalOf,
  PROVISION_CONTEXT_REQUIRED,
  type ProvisionRefusal,
  ...

Attack Scenario:

  1. packages/core/src/provision/index.ts removes the ProvisionProblemReportError class export and ProblemReportPayload type export, replacing them with provisionRefusalOf and ProvisionRefusal (diff hunk in index.ts).
  2. Any code path (including offscreen.ts, not confirmed updated in this PR's visible files) that still does catch (e) { if (e instanceof ProvisionProblemReportError) ... } will now fail at compile time (TypeScript) or, if using a stale compiled bundle, at runtime with an undefined reference.
  3. If this extension package is consumed by third-party integrators or other internal packages pinned to the previous minor version's type contract, their build breaks or their error-handling branch silently becomes dead code (since instanceof against an undefined import throws or the class no longer exists).
  4. During a live rollout, a partially-deployed extension bundle (old offscreen.ts bundled against new core) could throw unhandled exceptions when a refusal occurs, since the expected class no longer exists, causing the onboarding UI to crash rather than show the context-required picker.
  5. This results in an availability/DoS-class defect for the onboarding flow: legitimate refusals that used to be gracefully handled now crash the caller instead of surfacing recovery UX.

Preconditions: A consumer package or bundle is not fully synchronized with this API change (old offscreen.ts against new core, or an external integrator)., No compatibility shim or deprecation period was provided for the removed symbols.

Existing Controls: This is a coordinated monorepo change (core + extension) reducing the chance of skew within this single repository.

Recommended Mitigations: Provide a temporary deprecated re-export of ProvisionProblemReportError/ProblemReportPayload that wraps provisionRefusalOf for one release cycle. • Add a CHANGELOG/migration note and major/minor version bump signaling the breaking change. • Add integration tests covering offscreen.ts's actual catch-block usage against the new error shape.


⚪ STRIDE-8: Missing Explicit Digest Verification Path for Optional digestMultibase

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:L/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-354,CWE-345
CAPEC CAPEC-463,CAPEC-384
OWASP A08:2021 - Software and Data Integrity Failures

Description: digestMultibase field in ProvisionIntegrationResponseBody (COMP-003) allows acceptance of an unpinned or tampered bundle due to the digest being explicitly OPTIONAL with no code shown enforcing verification when present, resulting in a holder that pinned the bundle out-of-band potentially accepting a substituted bundle if the caller does not actively check the digest.

Evidence: packages/core/src/provision/send.ts:N/A

* self-describing multibase multihash ... It is OPTIONAL: it exists for holders that pinned the bundle out-of-band, and its absence is not a failure.

Attack Scenario:

  1. The response type documents digestMultibase as optional, taken 'over the armored bytes exactly as carried in bundle, not over a canonicalization' (send.ts comment block).
  2. openAdminRotationBundle (open.ts, not in scope for this PR) is responsible for opening the HPKE-sealed bundle; there is no visible code in run.ts or send.ts that cross-checks digestMultibase against an out-of-band pinned digest when the caller supplied one.
  3. A network-position attacker or malicious/compromised VTA substitutes the bundle field bytes while leaving digestMultibase absent or recomputing it to match the substituted bytes, since HPKE sealing does not by itself guarantee sender authenticity against a compromised VTA (the VTA is the one sealing it).
  4. If the wallet's higher-level flow does not explicitly compare an operator-pinned digest before calling openAdminRotationBundle, the substituted bundle is opened and processed as legitimate.
  5. This could result in the wallet adopting attacker-influenced admin key material if the VTA itself (or a MITM with VTA-equivalent capability) is compromised, since the client-side digest check — the one integrity backstop for out-of-band pinning — is optional and its enforcement is not demonstrated in the reviewed files.

Preconditions: Caller (wallet) has an out-of-band pinned digest expectation but the reviewed code path does not enforce comparing it., VTA or a MITM with VTA-level capability can influence the sealed bundle bytes.

Existing Controls: HPKE sealing restricts who can produce a bundle the wallet's ephemeral key can open meaningfully, but does not itself prove the VTA (versus an attacker with VTA-equivalent access) sealed it. • bundleIdHex nonce cross-check (Step 4 in run.ts) binds the reply to the request, partially mitigating pure replay.

Recommended Mitigations: Make digest verification mandatory whenever the caller has a pinned digest, and fail closed if digestMultibase is absent in that case. • Surface a clear API for callers to supply and enforce an expected digest before invoking openAdminRotationBundle. • Add test coverage in provision.trust-task.mjs asserting rejection when digest mismatches.


⚪ STRIDE-9: Unvalidated Context Inference Delegated Entirely Server-Side

Field Detail
Category Elevation of Privilege
Severity Low
Likelihood Possible
CVSS 4.6 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-863,CWE-284
CAPEC CAPEC-122
OWASP A01:2021 - Broken Access Control

Description: context field in ProvisionIntegrationRequestBody (COMP-003) allows ambiguous authorization outcomes due to omission triggering server-side inference rules that are not client-verifiable, resulting in the admin DID potentially being provisioned into an operator-unintended maintainer context when inference disagrees with operator expectation.

Evidence: packages/core/src/provision/run.ts:N/A

// context is optional on the wire. When omitted, the VTA's inference rules pick the target context...

Attack Scenario:

  1. runProvisionIntegration/sendProvisionIntegration allow context to be omitted, per comments: 'wallet-class callers typically omit; integration-class callers send explicitly' (run.ts).
  2. When omitted, the VTA infers the target context from ACL grant state and its own contexts, per the documented rules, none of which the client can independently verify before the round-trip completes.
  3. If the operator's ephemeral grant is ambiguous (e.g., recently modified ACL, multiple eligible contexts) but the VTA's inference logic (server-side, out of scope) picks a context different from what the operator intended, the response returns a bundle scoped to the wrong context.
  4. Because the wallet trusts the returned bundle's scope implicitly (there is no visible client-side assertion that the resulting context matches an expected value), the admin DID ends up provisioned as an administrator of an unintended maintainer context.
  5. This is an authorization-boundary confusion: the operator believes they granted admin rights to context A, but ends up as admin of context B, potentially gaining or losing privileges unexpectedly.

Preconditions: Server-side context inference logic (out of scope) can resolve ambiguously without triggering PROVISION_CONTEXT_REQUIRED., Client does not independently verify the returned bundle's context scope against operator intent.

Existing Controls: PROVISION_CONTEXT_REQUIRED refusal path exists for genuinely ambiguous cases, prompting explicit operator choice. • Comments describe well-defined inference rules (single-context grant, super-admin + single-context VTA) intended to minimize ambiguity.

Recommended Mitigations: Have the client assert the returned bundle's context matches the explicitly requested (or last-displayed-and-confirmed) context before adoption. • Log the inferred context server-side and surface it to the client for confirmation even in the non-ambiguous path. • Add regression tests for edge cases in inference rule precedence.


⚪ STRIDE-10: Insufficient Audit Trail for createContext Privileged Flag

Field Detail
Category Repudiation, Elevation of Privilege
Severity Medium
Likelihood Possible
CVSS 5.5 CVSS:4.0/AV:N/AC:L/AT:P/PR:H/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-778,CWE-863
CAPEC CAPEC-93
OWASP A09:2021 - Security Logging and Monitoring Failures

Description: createContext flag in ProvisionIntegrationRequestBody (COMP-003) allows repudiation of a privileged inline-context-creation action due to no client-side logging or confirmation step surrounding the super-admin-only createContext: true path, resulting in an operator later being unable to prove or disprove they intentionally triggered inline context creation during a disputed provisioning event.

Evidence: packages/core/src/provision/run.ts:N/A

...(opts.createContext ? { createContext: true } : {}),

Attack Scenario:

  1. runProvisionIntegration conditionally sets createContext: true on the wire only 'when the caller actually asked for an inline create' (run.ts, ...(opts.createContext ? { createContext: true } : {})).
  2. This is documented as a super-admin-only capability, meaning its use has elevated, context-creating side effects on the VTA.
  3. No code in the reviewed files logs, timestamps, or requires explicit re-confirmation before this flag is set and transmitted alongside the signed BootstrapRequest VP (which itself does not encode the createContext intent, since it is a body-level option, not part of the signed VP).
  4. Because createContext is a plain boolean outside the signed envelope, if opts.createContext were flipped by a bug or a compromised caller in the extension UI layer (e.g., offscreen.ts) without the operator's explicit action, the resulting privileged action (new context creation) would be indistinguishable after the fact from an operator-intended one.
  5. In a dispute (e.g., 'I never asked to create a new context'), there is no local evidence trail correlating the operator's UI interaction to the transmitted createContext: true flag, weakening non-repudiation for this super-admin action.

Preconditions: Caller is a super-admin whose grant permits inline context creation., No independent audit logging exists at the extension layer correlating UI intent to the transmitted flag.

Existing Controls: Flag defaults to false and is only included when explicitly true, reducing accidental transmission risk. • Server-side super-admin authorization check presumably gates whether the flag has effect (not in scope, but implied).

Recommended Mitigations: Log a client-side audit event (operator id, timestamp, target VTA) whenever createContext: true is included in a request. • Require an explicit UI confirmation step (e.g., re-entered passphrase or confirmation dialog) before setting this flag. • Consider binding createContext intent into the signed VP itself for stronger non-repudiation.


⚪ STRIDE-11: Removed Legacy Wire-Format Compatibility Introduces Field-Naming Mismatch Risk

Field Detail
Category Tampering, Denial of Service
Severity Low
Likelihood Possible
CVSS 3.7 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-354,CWE-20
CAPEC CAPEC-153
OWASP A04:2021 - Insecure Design

Description: ProvisionIntegrationRequestBody type derivation in COMP-003 allows request malformation due to the switch from hand-written snake_case fields to a generated Omit<ProvisionIntegrationPayload,"request"> type without a runtime-level assertion that the generated bindings match the deployed VTA's accepted schema version, resulting in silently dropped or misinterpreted fields if the client and VTA drift on the 0.3 schema.

Evidence: packages/core/src/provision/send.ts:N/A

export type ProvisionIntegrationRequestBody = Omit<ProvisionIntegrationPayload, "request"> & { request: BootstrapRequestVp; };

Attack Scenario:

  1. The old ProvisionIntegrationRequestBody interface hand-declared snake_case fields (vc_validity_seconds, create_context) matching the wire protocol at the time.
  2. The new type is derived via Omit<ProvisionIntegrationPayload, "request"> from generated bindings at @openvtc/trust-tasks/provision/integration/0.3/payload, trusting the generated package to exactly reflect what the deployed VTA accepts.
  3. If the deployed fleet of VTAs has not uniformly upgraded to the exact 0.3 payload shape (the same class of bug noted in the PR's own comments about the prior VTI #1202 version-mismatch incident), a client built against a newer generated binding could send camelCase-only fields to a VTA still expecting some snake_case aliases exclusively, or vice versa.
  4. Because the Rust struct is stated to keep snake_case as serde aliases ('that is a fold on the VTA's side'), forward compatibility is assumed but not verified at runtime by this client — there is no schema-version negotiation or capability check visible in the reviewed files.
  5. A request built against a mismatched schema expectation could be silently misparsed server-side (extra/unknown fields ignored) rather than rejected, causing the operation to proceed with default values the operator did not intend (e.g., vc_validity_seconds/vcValiditySeconds silently defaulting rather than honoring the caller's explicit preference).

Preconditions: Deployed VTA fleet has heterogeneous versions of the provision/integration schema., No runtime capability/version negotiation exists between client and VTA for this task type.

Existing Controls: Generated bindings are versioned per path (0.3/payload), giving some compile-time alignment guarantee for a single VTA version. • Rust-side serde aliases provide backward-compatible parsing for the old snake_case spelling.

Recommended Mitigations: Add a VTA capability/version discovery step before sending provision/integration requests. • Add integration tests against multiple VTA schema versions (0.2 and 0.3) to detect silent field drops. • Fail closed (explicit error) rather than silently default when an expected field is not acknowledged by the VTA's response.



🍝 PASTA Threat Model

Application Purpose

A browser-extension wallet client that onboards itself as a maintainer-context administrator by exchanging a signed Verifiable Presentation for an HPKE-sealed admin-key bundle with a VTA (Verifiable Trust Authority), now migrated from a bespoke DIDComm protocol to a generic multi-transport Trust Task abstraction.

Inherent Risks

  • The onboarding flow transfers long-term admin private key material to the browser extension, making the extension a high-value target regardless of transport.
  • The transport-abstraction migration removes previously inline, auditable security checks (thid/from correlation) in favor of an opaque shared library whose implementation is outside this PR's review scope.
  • Backward-incompatible API changes across a monorepo boundary (core package vs extension package) create a window for version skew during rollout.

Objectives

Risk: Do not silently regress security properties (correlation checks, transport confidentiality) during architectural refactors.
Business: Enable wallets to onboard as maintainer-context admins over any VTA-supported transport, removing the DIDComm-mediator hard dependency.
Security: Preserve reply authenticity, request/reply correlation, and end-to-end confidentiality guarantees previously provided by the bespoke DIDComm implementation.; Protect the ephemeral signing identity and the resulting admin private keys from disclosure or reuse.
Financial: Avoid support costs from onboarding failures caused by mediator unavailability or protocol version mismatches (e.g., VTI #1202).
Compliance: Maintain verifiable, non-repudiable evidence of privileged actions (e.g., inline context creation) for audit purposes.
Functional: Provide a single, transport-agnostic runProvisionIntegration/sendProvisionIntegration API surface for admin bootstrap.
Operational: Ensure the shared Trust-Task dispatcher path (server-side) and client library stay in lockstep across schema versions.

Business Impact Analysis (1)

BIA-1: Wallet Admin Onboarding via Provision Integration (Critical)

The end-to-end process by which a wallet exchanges an ephemeral operator grant for a long-term admin DID and private keys via a signed VP and an HPKE-sealed reply bundle.

MTD: 01 days 00:00 hours | RTO: 00 days 04:00 hours | RPO: 00 days 00:00 hours

  • Stakeholders: Extension End Users / Maintainer Context Operators / VTA Operators / VTA Browser Plugin Maintainers
  • Dependencies: @openvtc/trust-tasks generated bindings / HPKE sealing/opening library / TrustTaskSender / buildTrustTask transport abstraction / VTA provision_integration.rs dispatcher (server-side, out of scope)
  • Disruptions: Reply correlation abstraction fails to validate sender/nonce, allowing spoofed replies. / Transport fallback silently downgrades confidentiality guarantees. / Schema version skew between client bindings and deployed VTA fleet causes silent field drops.
  • Impacts: Unauthorized admin DID rotation compromising a maintainer context. / Loss of operator trust in the onboarding flow. / Support escalations and delayed onboarding (as previously seen with VTI #1202).

Technical Scope

Roles (3): RO-1 Wallet Operator (Ephemeral Grantee) · RO-2 Maintainer Context Super-Admin · RO-3 VTA Service Role

Actors (3): AC-1 Browser Extension (Wallet) · AC-2 Wallet Operator · AC-3 VTA Trust-Task Dispatcher

Use Cases (2): Wallet Admin Onboarding · Context-Required Recovery Selection

Attack Trees (2): SC-2: sendProvisionIntegration / TrustTaskSender Client · SC-1: runProvisionIntegration Orchestrator

Entry Points (4): EP-001 runProvisionIntegration · EP-002 sendProvisionIntegration · EP-003 Provision Integration Trust Task Wire Endpoint · EP-004 provisionRefusalOf

Risk Registry (6): RISK-1 · RISK-2 · RISK-3 · RISK-4 · RISK-5 · RISK-6

Threat Actors (3): TA-1 Malicious/Compromised Transport Relay Operator · TA-2 Malicious or Compromised VTA Operator · TA-3 Local Browser-Based Attacker

Infrastructure (2): IF-1 Browser Extension Runtime (Manifest V3 Offscreen Document) · IF-2 VTA Hosting Infrastructure

Trust Boundaries (3): TB-1 Browser Extension Boundary · TB-2 Wallet-to-VTA Network Boundary · TB-3 VTA Internal Processing Boundary

External Entities (2): EE-1 Remote VTA (Verifiable Trust Authority) · EE-2 Optional DIDComm Mediator / Relay

System Components (5): SC-1 runProvisionIntegration Orchestrator · SC-2 sendProvisionIntegration / TrustTaskSender Client · SC-3 VTA Trust-Task Dispatcher · SC-4 Offscreen Extension Document · SC-5 Sealed Admin Rotation Bundle Store

Resources And Assets (5): RA-1 Ephemeral Signing Identity / Private Key · RA-2 Signed BootstrapRequest VP · RA-3 HPKE-Sealed Admin Rotation Bundle · RA-4 digestMultibase Integrity Value · RA-5 ProvisionRefusal Candidates List

Technologies And Dependencies (4): TD-1 @openvtc/trust-tasks (provision/integration/0.3/payload bindings) · TD-2 TrustTaskSender / buildTrustTask abstraction · TD-3 VtaClientError · TD-4 HPKE Sealed-Bundle Library

⚔️ Attack Scenarios (1)

Exploit identified weaknesses

flowchart LR
  S0["Missing Reply Correlation Check in TrustTaskSender Abstracti"]
  S1["Silent Downgrade of Structured Error Handling via provisionR"]
  S2["Transport Downgrade Enabling Weaker Channel Selection in Tru"]
  S0 --> S1
  S1 --> S2
Loading

📊 Risk Summary

Total Threats: 11

By Severity: Low: 4 · High: 2 · Medium: 5

By Category: Unknown: 11


Generated by Agentic Sec — Threat Model & Affect Analysis Agent

📊 Summary & findings
✅ Confirmed ⚠️ Must-Review-By-Human
1 3

Confirmed (1)

  • 🔵 Silent discard of malformed VTA refusal payloads with no forensic logging

Must-Review-By-Human (3)

  • 🟠 Removal of explicit reply correlation (thid) and sender (from/vtaDid) checks with unverified replacement in delegated transport layer
  • 🟡 No transport-tier pinning for admin-rotation provisioning — silent fallback to REST removes prior E2E authcrypt guarantee
  • 🔵 Unsafe Formatstring (2 occurrences)

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