feat(provision): onboard over any transport the VTA advertises - #133
Conversation
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>
🛡️ AI Agentic Security Code Review1 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
🗺️ Scan CoverageModules scanned: 2 · with findings: 2 · files: 5 · findings: 4
Executive Summary
🔒 Security IssuesConfirmed Vulnerabilities (1)🔵 Silent discard of malformed VTA refusal payloads with no forensic logging
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 Vulnerable lines: 110, 133 🔎 Evidence: 💥 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:
⚖️ Triage Factors:
Attack scenario: A malformed or attacker-influenced VTA refusal payload is silently discarded with no logging, weakening forensic visibility into provisioning failures. 🔧 Remediation:
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: Secure code: Additional recommendations:
|
| 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-checksreply.summary.bundleIdHex !== expectedHex(nonce binding) andreply.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 whethersender.sendreimplements thread/sender-identity verification (e.g. checkingfrom/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
transportorpinparameter inSendProvisionIntegrationOptionsorRunProvisionIntegrationOptionsto 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 aboutopenAdminRotationBundle. 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.
Details
🛡️ Threat Model & Affect Analysis — PR #133
| Field | Value |
|---|---|
| Repository | OpenVTC/vta-browser-plugin |
| Branch | feat/provision-over-any-transport → main |
| 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:
- Legacy send.ts explicitly checked
reply.thid !== requestIdandreply.from !== vtaDidinline before accepting the DIDComm reply. - 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. - 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/TrustTaskSenderdo not reimplement the same checks. - 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.
openAdminRotationBundlein 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:
- A malicious or misbehaving VTA replies with a refusal whose
detailspayload hascodeas a non-string or missing (packages/core/src/provision/send.ts, provisionRefusalOf). provisionRefusalOfreturnsundefinedbecausetypeof payload.code !== "string".- 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.
- 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.
- 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:
runProvisionIntegration(run.ts) callssendProvisionIntegrationwith a genericsender: TrustTaskSender(packages/core/src/provision/run.ts) with no channel-pinning parameter.- The comment in send.ts states the transport priority is 'TSP > DIDComm > REST', implying automatic fallback when a preferred transport is unavailable.
- 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.
- 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.
- 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:
runProvisionIntegrationacceptsephemeralSigning: SigningIdentitywhosedidis passed asephemeralDidtosendProvisionIntegration(run.ts).- 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.'
- 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.
- 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.
- 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:
- A malicious or compromised VTA responds to
provision/integrationwith aPROVISION_CONTEXT_REQUIREDrefusal whosedetails.candidatesarray contains attacker-crafted strings (send.ts,provisionRefusalOf). provisionRefusalOffilters only fortypeof c === "string"but performs no length limit, encoding validation, or content sanitization on candidate values.offscreen.ts(not shown in this PR but referenced as the caller) is expected to renderreport.candidatesas a picker UI, per the code comments describing 'the popup's context-required recovery picker'.- 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.
- 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:
- Legacy code explicitly called
bridge.sendAndAwaitReply(outer, requestId, { timeoutMs })with visible timeout enforcement in send.ts. - 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. - A malicious or unresponsive VTA/mediator/relay can withhold a reply indefinitely.
- If the new abstraction does not propagate
timeoutMscorrectly 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. - 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:
packages/core/src/provision/index.tsremoves theProvisionProblemReportErrorclass export andProblemReportPayloadtype export, replacing them withprovisionRefusalOfandProvisionRefusal(diff hunk in index.ts).- 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. - 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
instanceofagainst an undefined import throws or the class no longer exists). - 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.
- 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:
- The response type documents
digestMultibaseas optional, taken 'over the armored bytes exactly as carried in bundle, not over a canonicalization' (send.ts comment block). 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-checksdigestMultibaseagainst an out-of-band pinned digest when the caller supplied one.- A network-position attacker or malicious/compromised VTA substitutes the
bundlefield bytes while leavingdigestMultibaseabsent 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). - 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. - 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:
runProvisionIntegration/sendProvisionIntegrationallowcontextto be omitted, per comments: 'wallet-class callers typically omit; integration-class callers send explicitly' (run.ts).- 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.
- 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.
- 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.
- 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:
runProvisionIntegrationconditionally setscreateContext: trueon the wire only 'when the caller actually asked for an inline create' (run.ts,...(opts.createContext ? { createContext: true } : {})).- This is documented as a super-admin-only capability, meaning its use has elevated, context-creating side effects on the VTA.
- 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
createContextintent, since it is a body-level option, not part of the signed VP). - Because
createContextis a plain boolean outside the signed envelope, ifopts.createContextwere 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. - 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: trueflag, 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:
- The old
ProvisionIntegrationRequestBodyinterface hand-declared snake_case fields (vc_validity_seconds,create_context) matching the wire protocol at the time. - 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. - 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.
- 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.
- 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/vcValiditySecondssilently 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
📊 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 | |
|---|---|
| 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)
The gap
Provisioning was the last VTA operation with no transport chain.
Every other one — vault, contexts, dids, device wake, acl — has gone through
TrustTaskSendersince #79, so it runs over TSP, DIDComm or REST according to what the agent publishes.runProvisionIntegrationstill took aDidcommMessageBridge, andoffscreen.tsbuilt its own mediator session instead of callinggetVtaSession. Two consequences:CONNECTED over TSPat the top,Transport: DIDComm (authcrypt)on the grant panel below it.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_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.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#responseand 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
#responsefragment 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.)sendProvisionIntegrationis nowbuildTrustTask+sender.send, and reads almost exactly likeswapAcl— 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
getWarmSessioncallsloadHolderitself.So
getVtaSessionsplits in two: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_messageisauth_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:keytimed 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
MediatorRequiredErrornow fires only when a mediator is the sole remaining possibility and none was supplied, rather than before any transport is considered.VtaSessiondraws when it falls back one.client.unsupportedbut never on a post-send failure.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
ProvisionProblemReportErroris gone along with the DIDComm-specific error channel.provisionRefusalOfreplaces it, reading the code anddetails.candidatesoffVtaClientError.details— fields, not a parsed message (R3.7).The popup is untouched: offscreen still forwards
{ code, candidates }andonboard-viewstill compares againstPROVISION_CONTEXT_REQUIRED.Depends on
VTI #1204. It makes the dispatcher emit
provision/integration:contextRequiredwith structured candidates. Without it the spine refuses an ambiguous context asmalformedRequestwith 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.jsstill a single bundle, no dynamicimport().provision→vtais a downward import.provision.trust-task.mjs(5 tests): the envelope is addressed ephemeral → VTA under the 0.3 URI withexpectedResponseTypeset to its#response; the option fields go out camelCase while the signed VP is relayed byte-for-byte; andprovisionRefusalOfrecovers the code and candidate list, returns an empty list for a refusal that carries none, andundefinedfor 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-vtais the real check.