Skip to content

feat(rp-login): login() and loginDidcomm() sign in as the per-site persona - #146

Merged
stormer78 merged 2 commits into
mainfrom
feat/per-site-persona-login
Aug 30, 2026
Merged

feat(rp-login): login() and loginDidcomm() sign in as the per-site persona#146
stormer78 merged 2 commits into
mainfrom
feat/per-site-persona-login

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

login() — the REST SIOPv2 path, and the first one an RP page reaches for — signed as the wallet's holder DID for every site, unconditionally and with no signal. It read no vault entry and took no parameter that could change that, while signin-flow.tsx told the operator, on screen:

A different identity for every site — a site sees an identity you use only there, never your wallet's own address.

That was true of proxyLogin and false of the button beside it. This closes the gap on both RP-facing login paths. The explainer becomes true.

What the operator sees

First sign-in at a site raises the picker; every sign-in after is the ordinary consent prompt.

First sign-in at admin.vtc.example

Sign in as
┌──────────────────────────────┐
│ did:webvh:…:personal         │
│ did:webvh:…:work             │
│ My wallet's own identity · … │   ← last, and labelled with what it costs
└──────────────────────────────┘

The holder is an offered answer, not a silent fallback

The RP's ACL is checked against whichever DID signs in, and every enrolment made before personas existed names the holdervtc admin invite --did <your-did> is what put it there. Dropping the route would break those sites on the next sign-in, with an RP refusal as the only clue.

So it stays, chosen explicitly with the consequence on screen (every site holding that DID can recognise the same person), and remembered per origin in site-identity.ts. It is offered only where it can be honouredlogin() self-issues from a key the browser holds; proxyLogin and walletProfile mint through a vault entry, and the holder is not one, so allowHolder gates the option rather than presenting a choice that fails after it is made.

A persona always beats a recorded holder choice. It is the more specific statement about the site and the one visible and revocable in the vault. Reading the local record first would let a stale choice mask an entry bound later through proxyLogin, and the sign-in would use an identity the vault contradicts. Binding a persona clears the record, so there is never a second answer sitting behind the first.

Two mechanisms, one for each path

REST — the id_token source became a parameter. SiopIdTokenMinter, with selfIssuedMinter() for the holder and a VTA-backed one in the offscreen for a persona. That keeps siop at its layer (the persona mint needs vault, two layers up) and loginViaSiop stops taking a SigningIdentity it could only use one way.

DIDComm/TSP — the channel can sign as a persona. signOutboundTask takes a TaskSigner ({ did, sign(envelope) }) instead of a private key. localTaskSigner for the holder's key; vaultTaskSigner asks the VTA via vault/sign-trust-task/0.2. The channel cannot tell them apart, and neither can the RP — it verifies a proof against a DID and never learns where the bytes were produced.

The signer stays REQUIRED: widening the type does not weaken the rule CLAUDE.md records, since a channel with no signer would still silently send unsigned documents. Only the key's location changed. Channel options accept SigningIdentity | TaskSigner and normalise once at construction — the fourteen deprecated *Rest helpers thread an identity through their own public options types, and rewriting all of them would say something none of their callers need to know.

Transport sender and document signer are now separate

Because the RP treats them as separate: handle_authenticate establishes the caller from the signature (session.did != input.signer_did in vti-common), not from who delivered the frame. A persona login rides the wallet's own transport — there is no second mediator session, and the persona has no local key to open one — while the documents are issued by and signed as the persona.

SessionIdentity gains documentSigner rather than widening signing, which would have handed a keyless signer to tspHolderIdentityFromSecret and failed far from the cause.

The guard that moved

loginViaTrustTask's signing.did !== holder.did check is gone. It could not survive a channel signing as a persona, and it was checking the wrong thing anyway — that function never signs. The rule it stood for is enforced where the signature happens, on every outbound document, by signOutboundTask's existing issuer check.

Its test now pins the behaviour that replaced it: that a named subject issues both auth documents. Requesting the challenge for the persona and issuing the authenticate as the holder would be refused by the RP on the signer check — after the operator had approved the sign-in — so the test asserts both envelopes, not just the first.

holderDid in both results is now the DID that actually signed. Reporting the wallet's own for a persona login would tell the page it is talking to an identity that signed nothing in that flow.

Operational note

A site whose ACL names your holder DID keeps working — pick "My wallet's own identity" once. A site you give a persona needs that persona on its ACL; the first failed sign-in says so and names the DID.

Pre-merge checklist

  • npm run lint clean (tsc -b, not --noEmit)
  • npm run build clean
  • npm test — 666 tests, 0 failures (8 new; 2 rewritten where the guard moved)
  • MV3 invariants: dist/background.js single bundle, no dynamic import(), no chrome.cookies, no static content_scripts, no cookies permission
  • Module boundaries + entry points hold — vault/task-signer.ts imports vta (layer 5 → 4, downward); the persona orchestration stays in the extension so no sideways import is needed
  • R1.2 — no new outbound fetch; persona minting and signing reuse the existing VTA session
  • R3.7 — no matching on message text
  • No compatibility fold — ChannelSigner's two arms are both live local shapes, not a legacy wire spelling

`login()` — the REST SIOPv2 path, and the one an RP page reaches for
first — signed as the wallet's holder DID for **every site**,
unconditionally and with no signal. It read no vault entry and took no
parameter that could change that, while `signin-flow.tsx` told the
operator, on screen, that "each site gets its own identity" and "a site
sees an identity you use only there — never your wallet's own address".
That claim was true of `proxyLogin` and false of the button next to it.

Now the identity is resolved per origin: a bound persona if this site
has one, the wallet's own identity if the operator chose it for this
site, and otherwise a prompt that asks. The explainer becomes true.

**The holder stays as an offered answer, not a silent fallback.** The
RP's ACL is checked against whichever DID signs in, and every enrolment
made before personas existed names the holder — `vtc admin invite --did
<your-did>` puts it there. Dropping the route would break those sites on
the next sign-in with an RP refusal as the only clue. So the picker
lists "My wallet's own identity" last, labelled with what it costs
(every site holding it can recognise the same person), and the choice is
remembered per origin in `site-identity.ts`.

It is offered **only** where it can be honoured. `login()` self-issues
from a key the browser holds; `proxyLogin` and `walletProfile` mint
through a vault entry at the VTA, and the holder is not one — so
`allowHolder` gates the option rather than presenting a choice that
fails after it is made.

**A persona always beats a recorded holder choice.** It is the more
specific statement about the site and the one the operator can see and
revoke in the vault; reading the local record first would let a stale
choice mask an entry bound later through `proxyLogin`, and the sign-in
would use an identity the vault contradicts. Binding a persona clears
the record, so there is never a second answer sitting behind the first.

The id_token source is now a parameter rather than a branch:
`SiopIdTokenMinter` with `selfIssuedMinter()` for the holder and a
VTA-backed one in the offscreen for a persona. That keeps `siop` at its
layer — the persona mint needs `vault`, which is two layers up — and
`loginViaSiop` stops taking a `SigningIdentity` it can only use one way.

`minter.did` is load-bearing beyond the signature: the challenge is
requested for it, and the RP refuses unless the DID it issued the
challenge to is the one that signed (`session.did != input.signer_did`
in vti-common's `handle_authenticate`). The persona minter therefore
reads `principalDid` back from the entry rather than assuming it — it is
maintainer-derived, and an entry rotated at the VTA signs as something
the wallet never chose.

`holderDid` in the result is now the DID that actually signed. Reporting
the wallet's own for a persona login would tell the page it is talking
to an identity that signed nothing in that flow.

`loginDidcomm` is unchanged and still authenticates as the holder; it
needs the channel to sign as a persona, which is the next commit.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
The other half. `loginDidcomm` authenticates by the **proof on the
document**, and the channel puts that proof there — so signing in as a
per-site persona meant a channel that could sign as one, and a persona's
key never leaves the VTA.

`signOutboundTask` now takes a `TaskSigner` — `{ did, sign(envelope) }` —
instead of a private key. Two implementations: `localTaskSigner` for the
holder's own key, and `vaultTaskSigner`, which asks the VTA to sign via
`vault/sign-trust-task/0.2`. The channel cannot tell them apart, and
neither can the RP: it verifies a proof against a DID and never learns
where the bytes were produced.

**The signer stays REQUIRED.** Widening the type does not weaken the
rule CLAUDE.md records — a channel that could be built without one would
still silently send unsigned documents. What changed is only where the
key is. Channel options accept `SigningIdentity | TaskSigner` and
normalise once at construction, because the fourteen deprecated `*Rest`
helpers thread an identity straight through their own public options
types and rewriting all of them would say something none of their
callers need to know.

**Transport sender and document signer are now separate, because the RP
treats them as separate.** `handle_authenticate` establishes the caller
from the signature (`session.did != input.signer_did`), not from who
delivered the frame. So a persona login rides the wallet's own transport
— there is no second mediator session, and the persona has no local key
to open one — while the documents are issued by and signed as the
persona. `SessionIdentity` gains `documentSigner` rather than widening
`signing`, which would have handed a keyless signer to
`tspHolderIdentityFromSecret` and failed far from the cause.

**One identity across both steps.** `loginViaTrustTask` takes `subject`
and uses it for the challenge AND the authenticate. Requesting the
challenge for the persona and issuing the authenticate as the holder
would be refused by the RP on the signer check — after the operator had
already approved the sign-in — so a test pins both envelopes, not just
the first.

The `signing.did !== holder.did` guard is gone from `loginViaTrustTask`.
It could not survive a channel signing as a persona, and it was checking
the wrong thing anyway: that function never signs. The rule it stood for
is enforced where the signature happens, on every outbound document, by
`signOutboundTask`'s existing issuer check. Its test now pins the
behaviour that replaced it rather than the guard that moved.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 merged commit e836cc4 into main Aug 30, 2026
3 checks passed
@stormer78
stormer78 deleted the feat/per-site-persona-login branch August 30, 2026 22:04
@affinidi-appsecurity-bot

affinidi-appsecurity-bot commented Aug 31, 2026

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

2 findings need a human to review/validate.

Mandatory to check: 🔒 Security Code Review Report

Details

🛡️ Security Code Review Report — PR #146

Field Value
Repository OpenVTC/vta-browser-plugin
Branch feat/per-site-persona-loginmain
Validated 2026-09-05
Scan ID 71fb65bd
Validator AI Security Validation Agent

🗺️ Scan Coverage

Modules scanned: 2 · with findings: 2 · files: 18 · findings: 3

Module Files scanned Findings
packages/core 11 2
packages/extension 7 1

Executive Summary

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

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


🔒 Security Issues

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

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.

🔵 Unsafe Formatstring (3 occurrences)

Field Detail
Severity LOW
Location packages/extension/src/offscreen.ts:629
Finding ID github_pr-71cc68495d27
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 — 3 occurrence(s): offscreen.ts:629, offscreen.ts:1201, offscreen.ts:1210

📝 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: 85%
  • AI Validation Evidence: EVIDENCE FOUND: The evidence code_snippet is empty (''), and offscreen.ts is not included in source_files, so no concrete format-string/log-injection sink could be located or quoted. EVIDENCE NOT FOUND: packages/extension/src/offscreen.ts is absent from source_files entirely; no util.format/console.log call or tainted variable concatenation could be verified at line 629 or elsewhere. CHANGED VS PRE-EXISTING: offscreen.ts is not in the visible diff/changed-file list provided in this context, and
  • 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.

🟡 SigningIdentity/ChannelSigner abstraction moves trust boundary to remote VTA without validating persona binding before session/document construction

Field Detail
Severity MEDIUM
Location packages/core/src/vault/task-signer.ts:1
Finding ID github_pr-2023b3bfbbd4
CWE CWE-345
OWASP A08:2021 - Software and Data Integrity Failures
Detection Source threat_model

📝 Description:

vaultTaskSigner delegates signing of a document destined for a relying party to a remote VTA session, and only checks that a proof field is present on the returned envelope — it does not verify that the returned proof actually verifies against opts.did, nor that the signedEnvelope's issuer/other fields were not tampered with by the VTA response before mutating the original envelope in place.

🌱 Root Cause: The signer trusts the shape of the VTA's response (presence of proof) without cryptographically or structurally validating that the proof corresponds to the expected DID or that other envelope fields were not altered in transit/by the VTA.

🔎 Evidence: packages/core/src/vault/task-signer.ts:1

export function vaultTaskSigner(opts: VaultTaskSignerOptions): TaskSigner {
  return {
    did: opts.did,
    sign: async (envelope) => {
      const { signedEnvelope } = await vaultSignTrustTask(opts.session, {
        holder: opts.holder,
        service: opts.service,
        entryId: opts.entryId,
        unsignedEnvelope: envelope as unknown as Record<string, unknown>,
      });
      const proof = (signedEnvelope as { proof?: unknown }).proof;
      if (!proof) {
        throw new Error(
          `vault/sign-trust-task: the VTA returned an envelope with no proof for ${opts.did}`,
        );
      }
      (envelope as { proof?: unknown }).proof = proof;
    },
  };
}

🎯 Attack Scenario:

A compromised or malicious VTA session returns a signedEnvelope with a proof object that is malformed, belongs to a different key, or the VTA silently changes envelope fields (like issuer) before returning it; since only truthiness of proof is checked here, the tampered envelope is used and dispatched to the RP, which independently verifies the proof — but the local code has weakened defense-in-depth by trusting the VTA response blindly for structural validation.

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 50%
  • AI Validation Evidence: EVIDENCE FOUND: vaultTaskSigner in task-signer.ts only checks if (!proof) { throw ... } before assigning (envelope as { proof?: unknown }).proof = proof; — no validation that the proof's verificationMethod DID matches opts.did. The doc comment claims 'The VTA refuses with envelope_issuer_mismatch when the envelope's issuer is not the entry's principalDid... which signOutboundTask checks before calling this' implying signOutboundTask enforces envelope.issuer === signer.did (opts.did) prior to
  • 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 #146

Field Value
Repository OpenVTC/vta-browser-plugin
Branch feat/per-site-persona-loginmain
Generated 2026-09-05

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


📋 Affect Analysis

Change Summary

This PR introduces per-site persona login support: loginViaTrustTask() and loginViaSiop() can now authenticate as a DID other than the wallet's holder identity, with the persona's private key held remotely at a VTA vault and never exposed to the browser. This required replacing concrete SigningIdentity types with pluggable TaskSigner/ChannelSigner/SiopIdTokenMinter abstractions across the signing/transport stack, and removing a hard local invariant check that previously guaranteed signer==holder.

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

📁 File Classifications

packages/core/src/rp-login/trust-task.ts

  • Type: security

packages/core/src/siop/login-client.ts

  • Type: security

packages/core/src/vault/index.ts

  • Type: config

packages/core/src/vault/task-signer.ts

  • Type: security

packages/core/src/vta/auth-tasks.ts

  • Type: security

packages/core/src/vta/didcomm.ts

  • Type: security

🛡️ STRIDE Threat Model

Identified Threats (10)

🔴 STRIDE-1: Persona Impersonation via Unverified entryId-to-DID Binding in vaultTaskSigner

Field Detail
Category Spoofing, Tampering, Elevation of Privilege
Severity Critical
Likelihood Likely
CVSS 8.7 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 High
CWE CWE-346,CWE-863,CWE-345
CAPEC CAPEC-151,CAPEC-196
OWASP A01:2021 - Broken Access Control, A07:2021 - Identification and Authentication Failures

Description: vaultTaskSigner in packages/core/src/vault/task-signer.ts allows signing as any persona DID due to missing local verification that the supplied entryId's principalDid matches the caller-supplied did before dispatch, resulting in cross-persona impersonation if the VTA-side check is bypassed or misconfigured.

Evidence: packages/core/src/vault/task-signer.ts:48-77

export function vaultTaskSigner(opts: VaultTaskSignerOptions): TaskSigner {
  return {
    did: opts.did,
    sign: async (envelope) => {
      const { signedEnvelope } = await vaultSignTrustTask(opts.session, {
        holder: opts.holder,
        service: opts.service,
        entryId: opts.entryI

Attack Scenario:

  1. Attacker who controls or compromises a caller of vaultTaskSigner (e.g. malicious extension code, compromised background.ts) supplies an entryId belonging to persona A but a did value belonging to persona B.
  2. vaultTaskSigner (packages/core/src/vault/task-signer.ts, opts.entryId, opts.did) constructs a TaskSigner with did: opts.did without validating opts.entryId's principalDid against opts.did locally.
  3. sign() calls vaultSignTrustTask(opts.session, { entryId, unsignedEnvelope }) which dispatches a Trust Task to the VTA relying entirely on server-side enforcement of 'envelope_issuer_mismatch'.
  4. If the VTA-side check is absent, misconfigured, or has a logic flaw (not visible in this diff, referenced only in comments), the VTA signs the envelope with entry A's key while returning a proof that the local code trusts.
  5. The returned proof is mutated into envelope.proof (task-signer.ts, (envelope as { proof?: unknown }).proof = proof;) without validating the proof's underlying DID against opts.did.
  6. The resulting envelope, signed by entry A's key but carrying issuer=opts.did (persona B), is sent to the RP, which cannot detect the mismatch since it exclusively verifies the Data Integrity proof against the issuer DID document.
  7. Attacker successfully authenticates as persona B at the RP despite having only rights to persona A's vault entry.

🔎 Threat Clue: Derived from COMP-003 via EP-004, EP-005

  • Data Flows: vaultTaskSigner->VTA vault/sign-trust-task/0.2

Preconditions: Attacker can control or influence the entryId/did pairing passed into vaultTaskSigner (e.g., via a compromised or overly-trusting caller in the extension), The VTA-side 'envelope_issuer_mismatch' check is not authoritative, is bypassable, or has an edge-case flaw, No client-side assertion exists that entryId's principalDid equals opts.did before making the vault call

Existing Controls: VTA is documented to refuse with envelope_issuer_mismatch when envelope.issuer does not match the entry's principalDid (server-side, unverified in this codebase slice) • signOutboundTask performs a local issuer==signer.did check before invoking signer.sign, catching some but not all mismatch classes

Recommended Mitigations: Add a local assertion in vaultTaskSigner or its caller that fetches/validates the vault entry's principalDid and confirms it equals opts.did before dispatching the sign request • Cross-validate the returned proof's verificationMethod DID against opts.did on the client before mutating envelope.proof • Add integration tests asserting VTA-side envelope_issuer_mismatch enforcement, and pin behavior with a security regression test • Require entryId and did to be fetched together from a single trusted source (never independently supplied by disparate call sites)


🟠 STRIDE-2: Removal of Client-Side Holder-Equals-Signer Guard in loginViaTrustTask

Field Detail
Category Spoofing, Elevation of Privilege
Severity High
Likelihood Likely
CVSS 7.4 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-863,CWE-284
CAPEC CAPEC-151,CAPEC-115
OWASP A01:2021 - Broken Access Control

Description: loginViaTrustTask in packages/core/src/rp-login/trust-task.ts allows a caller to request an authentication challenge and session for an arbitrary subject DID due to the removal of the local opts.signing.did !== holder.did guard without an equivalent replacement check at this call site, resulting in reliance entirely on remote/downstream enforcement for identity-consistency and loss of defense-in-depth.

Evidence: packages/core/src/rp-login/trust-task.ts:77-110

const subject = opts.subject ?? holder.did;
// The guard that used to live here — "the signing identity must be the
// holder" — has moved to where it can actually be checked.
const challenge = await requestAuthChallenge(sender, { holder, service, issuer: subject, subject, purpose: "login" });

Attack Scenario:

  1. A caller (compromised extension code, malicious library dependency, or logic bug in a consuming component) invokes loginViaTrustTask with opts.subject set to a DID the caller does not control the signing key for.
  2. The removed guard (previously if (opts.signing.did !== holder.did) throw) no longer exists in trust-task.ts; only a comment explains the check 'moved' to signOutboundTask.
  3. requestAuthChallenge is called with issuer: subject (rp-login/trust-task.ts), successfully obtaining a challenge bound to the attacker-chosen subject from the RP, since the RP only refuses at authentication time, not challenge time.
  4. authenticateSession is invoked with issuer: subject; if the channel's underlying signer (sender) is misconfigured, buggy, or itself compromised (e.g., a rogue TaskSigner implementation slipped in via dependency confusion or supply chain), it may sign with a mismatched key while unintentionally satisfying the local issuer==signer.did check with a spoofed signer.did value.
  5. Because the safety invariant now spans two separate modules (trust-task.ts and signOutboundTask) with no single authoritative assertion, a bug in the channel's signer implementation (e.g. a custom TaskSigner that lies about its own did field) allows challenge and authentication requests for an arbitrary subject to proceed further than the previous single hard-fail guard allowed.
  6. Result: increased surface for identity confusion; a single defense layer replaces what was previously two independent layers (local check in trust-task.ts + local check in signOutboundTask).

🔎 Threat Clue: Derived from COMP-001, COMP-004 via EP-001, EP-006

  • Data Flows: loginViaTrustTask->requestAuthChallenge->authenticateSession

Preconditions: A custom or compromised TaskSigner/ChannelSigner implementation that reports an incorrect did, No additional validation between the caller-supplied subject and the vault entry actually used for signing

Existing Controls: signOutboundTask performs envelope.issuer !== signer.did check before signing (packages/core/src/vta/trust-task.ts) • RP performs authoritative server-side check (session.did != input.signer_did in handle_authenticate, per comments)

Recommended Mitigations: Reinstate a defense-in-depth local assertion in loginViaTrustTask comparing the sender's resolved/expected signer DID against opts.subject prior to network calls, where feasible • Add unit/integration tests specifically covering the removed guard's prior failure case to ensure regression cannot silently occur • Document and enforce a strict contract that any TaskSigner implementation's did MUST be cryptographically derived/verifiable, not a freely settable string field • Add telemetry/alerting for challenge requests where subject != holder.did to detect anomalous persona usage patterns


🟡 STRIDE-3: Type-Erasure Bypass via as unknown as Casts in signOutboundTask and vaultTaskSigner

Field Detail
Category Tampering, Information Disclosure
Severity Medium
Likelihood Possible
CVSS 5.3 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-704,CWE-20
CAPEC CAPEC-153
OWASP A03:2021 - Injection, A08:2021 - Software and Data Integrity Failures

Description: signOutboundTask and vaultTaskSigner in packages/core/src/vta/trust-task.ts and packages/core/src/vault/task-signer.ts allow malformed or malicious envelope shapes to bypass TypeScript's compile-time type checking due to repeated as unknown as Record<string, unknown> and (envelope as { proof?: unknown }) casts, resulting in potential runtime type confusion where a crafted envelope with unexpected proof or issuer field types is accepted and forwarded.

Evidence: packages/core/src/vault/task-signer.ts:55-72

const proof = (signedEnvelope as { proof?: unknown }).proof;
if (!proof) { throw new Error(...); }
(envelope as { proof?: unknown }).proof = proof;

Attack Scenario:

  1. A caller constructs or receives (e.g. via a compromised upstream dependency or a deserialization step) a TrustTask envelope object with a proof field that is not the expected proof-object shape, or an issuer field set to an object rather than a string.
  2. vaultTaskSigner casts envelope as unknown as Record<string, unknown> (task-signer.ts) losing all type safety, then passes it to vaultSignTrustTask.
  3. Upon response, the code checks only if (!proof) (task-signer.ts) — a falsy check — rather than validating the proof's structural shape (e.g. type, verificationMethod, proofValue fields), so a malformed but truthy proof value (e.g. an empty object {}) passes the check.
  4. The malformed proof is written back with (envelope as { proof?: unknown }).proof = proof;, silently mutating shared envelope state with unchecked data.
  5. Downstream consumers (RP, VTA reply parsers) that expect a well-formed Data Integrity proof object may throw unhandled exceptions, mishandle the malformed structure, or — in a worse case — treat a permissive/empty proof as valid if their own validation is similarly loose, enabling a request to be treated as signed when it is not.

🔎 Threat Clue: Derived from COMP-003 via EP-004, EP-005

  • Data Flows: vaultTaskSigner->VTA->envelope.proof mutation

Preconditions: A caller or dependency can influence the shape of the proof object returned from vaultSignTrustTask, Downstream proof verification is not strictly schema-validated

Existing Controls: Truthiness check on proof before continuing (task-signer.ts) • signOutboundTask's issuer/signer.did string equality check

Recommended Mitigations: Replace as unknown as double-casts with proper runtime schema validation (e.g. zod/io-ts) for both outbound envelopes and inbound proof objects • Validate proof object shape (required DI proof fields: type, created, verificationMethod, proofValue) before accepting it as non-empty • Add strict TypeScript noImplicitAny/strict in these modules combined with branded types to prevent unsafe casts from being introduced in future changes


🟠 STRIDE-4: Pluggable SiopIdTokenMinter Allows Arbitrary id_token Minting Source Substitution

Field Detail
Category Spoofing, Elevation of Privilege
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-345,CWE-346,CWE-829
CAPEC CAPEC-151,CAPEC-593
OWASP A08:2021 - Software and Data Integrity Failures, A07:2021 - Identification and Authentication Failures

Description: loginViaSiop in packages/core/src/siop/login-client.ts allows substitution of the id_token issuance mechanism via the pluggable SiopIdTokenMinter interface due to lack of validation that opts.minter.did corresponds to a minter actually authorized/entitled to sign for that DID, resulting in potential minting of id_tokens for DIDs the caller does not legitimately control.

Evidence: packages/core/src/siop/login-client.ts:84-119

const idToken = await opts.minter.mint({ audience: opts.rpDid, nonce: challenge.challenge });
const envelope = { id: `urn:uuid:${globalThis.crypto.randomUUID()}`, type: TASK_AUTH_AUTHENTICATE, issuer: opts.minter.did, ... };

Attack Scenario:

  1. A caller supplies a custom SiopIdTokenMinter implementation to loginViaSiop where did is set to a victim persona's DID but mint() is implemented to call an attacker-controlled signing backend or reuse a cached/stolen token.
  2. loginViaSiop requests a challenge using opts.minter.did (login-client.ts, body: JSON.stringify({ did: opts.minter.did })) without validating that this minter is bound/scoped to only mint for DIDs it is entitled to.
  3. idToken = await opts.minter.mint({ audience: opts.rpDid, nonce: challenge.challenge }) is invoked; the function trusts the minter's return value completely — there is no verification in loginViaSiop that the returned JWT's iss/sub actually equals opts.minter.did.
  4. The resulting envelope sets issuer: opts.minter.did (login-client.ts) directly from the minter's claimed did, not from parsing/validating the token's actual claims.
  5. If the RP's validation of id_token signature-vs-claimed-issuer has any leniency or if the minter is compromised (e.g., malicious npm dependency implementing SiopIdTokenMinter), an attacker can present forged authentication for the victim persona's DID.
  6. Because this is the first version where minting is externally pluggable (previously hardcoded to selfIssuedMinter/local signing), this widens the trusted computing base to any code implementing the interface.

🔎 Threat Clue: Derived from COMP-002 via EP-002, EP-003

  • Data Flows: loginViaSiop->SiopIdTokenMinter.mint->RP /auth/challenge

Preconditions: Attacker can supply, inject, or compromise a SiopIdTokenMinter implementation used by the wallet (e.g. via supply-chain attack, malicious extension update, or a permissive plugin architecture), No independent verification of the minted token's actual iss claim against opts.minter.did prior to trusting it as the envelope issuer

Existing Controls: RP performs authoritative check that the DID that signed matches the DID the challenge was issued to (session.did != input.signer_did), described in comments but not verifiable in this code slice • selfIssuedMinter's did is directly derived from signing.did, reducing risk for the default/built-in minter

Recommended Mitigations: Decode and verify the minted id_token's iss and sub claims client-side match opts.minter.did before constructing the envelope and sending it, failing fast on mismatch • Restrict which SiopIdTokenMinter implementations can be registered/loaded (e.g. an allowlist, or requiring cryptographic attestation of the minter's origin) • Add supply-chain controls (dependency pinning, SRI/checksums, code signing) for any package providing custom minter implementations • Add explicit unit tests asserting loginViaSiop rejects a minter whose minted token's issuer does not match minter.did


🟡 STRIDE-5: Optional issuer Parameter Default Fallback Creates Silent Persona Confusion in auth-tasks.ts

Field Detail
Category Tampering, Repudiation
Severity Medium
Likelihood Possible
CVSS 5.9 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-671,CWE-697
CAPEC CAPEC-115
OWASP A04:2021 - Insecure Design

Description: requestAuthChallenge, authenticateSession, refreshAuthSession, startPasskeyLogin, and finishPasskeyLogin in packages/core/src/vta/auth-tasks.ts allow silent issuer substitution due to the params.issuer ?? params.holder.did fallback pattern applied uniformly across five distinct auth flows, resulting in inconsistent or unexpected issuer values if a caller forgets to pass a matching issuer across a multi-step flow (e.g. challenge with issuer=personaA, authenticate with issuer=holder default).

Evidence: packages/core/src/vta/auth-tasks.ts:81,126,160,218,255

issuer: params.issuer ?? params.holder.did,

Attack Scenario:

  1. A calling component invokes requestAuthChallenge with issuer: subject (a persona DID) to get a challenge, as in loginViaTrustTask.
  2. Due to a coding error, race condition, or inconsistent state management elsewhere in a consuming application, a subsequent call to authenticateSession omits issuer, causing params.issuer ?? params.holder.did to silently default to holder.did instead of the persona subject used for the challenge.
  3. The envelope sent to authenticateSession now has issuer = holder.did while the challenge was issued and bound to the persona DID.
  4. signOutboundTask's issuer==signer.did check may still pass locally if the underlying channel's signer.did happens to equal holder.did (i.e., using the default holder signer), masking the fact that the wrong identity is authenticating relative to the original challenge intent.
  5. The RP either refuses (if it strictly checks the challenge's bound subject against the authenticate issuer) — causing a confusing failure with no direct indication that a caller-side default fallback was the root cause — or, in a lenient RP implementation, incorrectly authenticates the holder identity for a session initiated as a persona, causing session/identity confusion in audit logs and downstream authorization decisions.
  6. Because five separate call sites duplicate this exact ?? fallback pattern instead of sharing one enforced invariant, a fix or validation added to one is not guaranteed to be applied to the others, increasing risk of divergent behavior.

🔎 Threat Clue: Derived from COMP-004 via EP-006

  • Data Flows: requestAuthChallenge->authenticateSession->refreshAuthSession issuer propagation

Preconditions: A caller uses different auth-tasks.ts functions across a multi-step flow without consistently threading the same issuer argument, No shared validation ensures issuer consistency across the challenge -> authenticate -> refresh lifecycle

Existing Controls: RP's session.did != input.signer_did check (server-side, described in comments) provides some authoritative backstop • signOutboundTask's local issuer==signer.did equality check catches issuer/signer mismatches, though not issuer/challenge-subject mismatches

Recommended Mitigations: Introduce a session/context object that threads a single resolved issuer value through the entire login flow rather than re-deriving/defaulting it independently at each call site • Add explicit runtime assertions comparing the challenge's bound subject to the authenticate/refresh issuer before dispatch • Add comprehensive integration tests covering multi-step flows with a persona subject to catch silent holder-default fallback regressions • Emit warning-level logs when params.issuer is omitted for a call following a prior call in the same logical session that specified a persona issuer


🟡 STRIDE-6: ChannelSigner Type Widening in DidcommVtaTransport and RestChannel Removes Compile-Time Key-Identity Binding

Field Detail
Category Spoofing, Tampering
Severity Medium
Likelihood Possible
CVSS 5.1 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-345,CWE-284
CAPEC CAPEC-151
OWASP A08:2021 - Software and Data Integrity Failures

Description: DidcommVtaTransport and RestChannel in packages/core/src/vta/didcomm.ts and packages/core/src/vta/rest-channel.ts allow arbitrary ChannelSigner implementations to be substituted for the previously concrete SigningIdentity type due to the asTaskSigner(opts.signing) conversion accepting the new broader ChannelSigner type, resulting in reduced compile-time guarantees that the signer used for every outbound Trust Task actually corresponds to a locally-held, verifiable key.

Evidence: packages/core/src/vta/didcomm.ts:48,75-79,238

signing: ChannelSigner;
...
this.signer = asTaskSigner(opts.signing);
...
await signOutboundTask(envelope, this.signer);

Attack Scenario:

  1. Prior to this change, signing: SigningIdentity in DidcommVtaTransportOptions and RestChannelOptions enforced (at least at the type level) that a concrete, locally-verifiable key was supplied to the transport constructor.
  2. This diff changes the field type to ChannelSigner (didcomm.ts, rest-channel.ts) and immediately wraps it via asTaskSigner(opts.signing), broadening what implementations are accepted at both compile time and runtime.
  3. A malicious or buggy dependency, or a misconfigured DI/plugin registration, supplies a ChannelSigner object whose sign() implementation does not actually perform cryptographic signing correctly (e.g., a no-op stub used accidentally in production due to environment misconfiguration) or that always resolves without producing a genuine proof, and whose did field is attacker/developer-controlled.
  4. Every outbound message from send/notify in DidcommVtaTransport (await signOutboundTask(envelope, this.signer)) and RestChannel is silently signed (or 'signed') by this broadened signer without any additional runtime assertion that the signer is a genuine key-holder for its claimed did beyond the string-equality check in signOutboundTask.
  5. Because the check in signOutboundTask only verifies envelope.issuer === signer.did (both attacker/developer-controlled string fields) rather than verifying possession of key material, a misconfigured or malicious ChannelSigner can pass this check trivially while producing invalid, forged, or absent proofs, which is only caught downstream by the RP's cryptographic proof verification (outside this codebase).
  6. This shifts a class of configuration/dependency errors from a compile-time-shape mismatch (previously requiring a true SigningIdentity) to a runtime string-equality check with no cryptographic backing at the client.

🔎 Threat Clue: Derived from COMP-006, COMP-007 via EP-007, EP-008

  • Data Flows: DidcommVtaTransport.send/notify->signOutboundTask, RestChannel.send->signOutboundTask

Preconditions: A ChannelSigner implementation that is not a genuine key-holder is registered/wired into DidcommVtaTransport or RestChannel (via misconfiguration, dependency injection error, or compromised plugin), Reliance solely on the RP's downstream proof verification to catch the resulting invalid signatures

Existing Controls: signOutboundTask's issuer/signer.did equality check provides a naming consistency guard • RP-side Data Integrity proof cryptographic verification is the ultimate backstop (external to this codebase)

Recommended Mitigations: Add a runtime self-test/handshake for any registered ChannelSigner (e.g. sign-and-verify a known test payload against the signer's claimed DID document) before it is used operationally • Restrict which modules/environments are permitted to construct DidcommVtaTransport/RestChannel with a non-default ChannelSigner via configuration allowlisting • Add typed marker/brand types to distinguish 'verified local key' signers from 'remote/pluggable' signers, and require explicit opt-in for the latter • Add structured logging/telemetry for every signOutboundTask invocation recording signer type/origin for post-hoc audit


🟡 STRIDE-7: Missing Rate Limiting on Unauthenticated /auth/challenge Endpoint Enables DID Enumeration and Flooding

Field Detail
Category Information Disclosure, Denial of Service
Severity Medium
Likelihood Likely
CVSS 6.5 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-307,CWE-799
CAPEC CAPEC-112,CAPEC-125
OWASP A04:2021 - Insecure Design, A07:2021 - Identification and Authentication Failures

Description: POST ${baseUrl}/auth/challenge in packages/core/src/siop/login-client.ts allows unauthenticated, unlimited challenge requests for arbitrary DID values due to the client sending { did: opts.minter.did } with no client-side or evidenced server-side throttling, resulting in potential DID enumeration, resource exhaustion, and nonce-harvesting for downstream replay or brute-force attempts against the RP.

Evidence: packages/core/src/siop/login-client.ts:84-90

const challengeRes = await fetchFn(`${base}/auth/challenge`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ did: opts.minter.did }),
});

Attack Scenario:

  1. An attacker directly calls the RP's /auth/challenge endpoint (identified as EP-003, unauthenticated per recon) with a rapid sequence of { did: <guessed-or-enumerated-DID> } bodies, since login-client.ts shows this is the exact unauthenticated request shape the legitimate client sends.
  2. Because the endpoint is unauthenticated (per recon auth_required: false) and the client code shows no rate-limiting, backoff, or CAPTCHA logic, an attacker can send a very high volume of requests.
  3. Differential response behavior (e.g., timing differences, distinct error codes for 'DID not found' vs 'DID found, challenge issued') could allow the attacker to enumerate which DIDs are registered/known to the RP.
  4. Repeated challenge issuance for a victim's DID also lets an attacker harvest many valid nonces tied to that DID, increasing the attack surface for any weakness in nonce lifecycle/expiry handling at the RP.
  5. At sufficient volume, this also serves as a resource-exhaustion vector against the RP's challenge-issuance subsystem (each request presumably requires state creation/storage for sessionId/challenge), degrading availability for legitimate users.

🔎 Threat Clue: Derived from COMP-002 via EP-003

  • Data Flows: Client->RP /auth/challenge

Preconditions: RP does not implement server-side rate limiting, CAPTCHA, or anomaly detection on /auth/challenge (unverifiable from this code slice, but no client-side mitigation exists and the endpoint is unauthenticated by design), Attacker has network access to the RP's auth API

Existing Controls: None visible in the provided client code; any mitigation would need to exist server-side at the RP, which is out of scope for this repository

Recommended Mitigations: Recommend server-side rate limiting, per-IP and per-DID throttling, and CAPTCHA/proof-of-work challenges on /auth/challenge (RP-side, outside this repo but should be flagged to RP maintainers) • Ensure uniform response timing/shape regardless of DID existence to prevent enumeration • Set short expiry and single-use enforcement on issued challenges to limit the value of harvested nonces • Add client-side backoff/jitter for repeated challenge requests to reduce accidental self-inflicted load, though this does not stop a malicious direct caller


🔵 STRIDE-8: Non-Constant-Time / Non-Cryptographic did String Comparisons Enable Confusable DID Spoofing

Field Detail
Category Spoofing, Tampering
Severity Low
Likelihood Unlikely
CVSS 4.5 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-178,CWE-697
CAPEC CAPEC-627
OWASP A04:2021 - Insecure Design

Description: signOutboundTask in packages/core/src/vta/trust-task.ts allows potential confusable-DID bypass due to relying purely on JavaScript string equality (!==) for issuer/signer DID comparison without unicode normalization or canonical DID-syntax validation, resulting in a theoretical risk that visually or byte-level distinct but semantically ambiguous DID strings (e.g., differing only by trailing whitespace, homoglyphs, or unnormalized percent-encoding) could confuse this check or downstream consumers.

Evidence: packages/core/src/vta/trust-task.ts:105-121

if (envelope.issuer !== undefined && envelope.issuer !== signer.did) {
  throw new VtaClientError("e.client.identity", ...);
}
await signer.sign(envelope);

Attack Scenario:

  1. An attacker or a buggy upstream component supplies an envelope.issuer string that is byte-different from signer.did but resolves (per a lenient or non-canonicalizing DID resolver elsewhere in the stack) to the same underlying DID document — e.g. via trailing whitespace, differing percent-encoding of special characters, or case differences in method-specific-id where the method permits case-insensitivity.
  2. signOutboundTask's check envelope.issuer !== signer.did (packages/core/src/vta/trust-task.ts) treats these as different values and throws — this is the safe default — but the inverse risk exists: a component upstream of this check (not shown, potentially in buildTrustTask or a caller) might construct envelope.issuer from a differently-encoded but resolver-equivalent string to signer.did, causing the strict check to incorrectly ALLOW two strings that a human or a downstream RP might treat as different identities, or REJECT two strings a downstream system treats as the same, producing inconsistent authorization decisions across the trust boundary.
  3. This class of bug is most exploitable if any DID-consuming component elsewhere applies normalization while this check does not, creating a confused-deputy style mismatch between what the wallet 'signs as' and what the RP 'authenticates as'.

🔎 Threat Clue: Derived from COMP-004, COMP-006, COMP-007 via EP-006, EP-007, EP-008

  • Data Flows: signOutboundTask issuer/signer.did comparison

Preconditions: A DID resolver or downstream consumer applies different normalization rules than the raw string comparison used here, Attacker can supply or influence a byte-distinct but resolver-equivalent DID string into either envelope.issuer or a signer's did field

Existing Controls: Strict !== string comparison is a fail-closed default for exact mismatches • DID method specifications generally define canonical forms, reducing but not eliminating ambiguity risk

Recommended Mitigations: Apply DID canonicalization/normalization consistently before any equality comparison in signOutboundTask and all issuer-setting call sites • Add unit tests with edge-case DID strings (differing case, trailing whitespace, encoded characters) to confirm consistent accept/reject behavior matching the RP's own normalization • Document the exact expected DID string canonical form as a type-level invariant (e.g., branded CanonicalDid type) enforced at construction time


🟡 STRIDE-9: Insufficient Repudiation Controls for Persona-Signed Trust Tasks

Field Detail
Category Repudiation
Severity Medium
Likelihood Possible
CVSS 5.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-778,CWE-223
CAPEC CAPEC-593
OWASP A09:2021 - Security Logging and Monitoring Failures

Description: vaultTaskSigner and signOutboundTask across packages/core/src/vault/task-signer.ts and packages/core/src/vta/trust-task.ts allow an authenticated holder to later deny having authorized a persona-signed action due to the absence of any visible local audit log correlating which holder session requested which VTA-signed persona envelope, resulting in weakened non-repudiation for actions taken under a persona identity distinct from the holder.

Evidence: packages/core/src/vault/task-signer.ts:48-77

sign: async (envelope) => {
  const { signedEnvelope } = await vaultSignTrustTask(opts.session, { holder: opts.holder, service: opts.service, entryId: opts.entryId, unsignedEnvelope: envelope });
  ...
}

Attack Scenario:

  1. A holder's session initiates vaultTaskSigner.sign(envelope) for a persona DID via the VTA (task-signer.ts), producing a signed Trust Task sent to an RP under the persona's identity.
  2. No code in the reviewed files persists a local, tamper-evident record (e.g., an append-only log entry binding holder session ID, timestamp, entryId, target RP, and resulting envelope ID) at the point the signing request is made.
  3. If the persona later performs an action the holder disputes having authorized (e.g., in a shared-device or multi-user browser profile scenario), there is no client-side evidence trail to corroborate or refute the holder's claim, shifting the entire evidentiary burden to the VTA's own (unverified in this codebase) server-side logs.
  4. This is compounded by the fact that persona signing keys 'live at the VTA and never leave it' (per code comments), meaning the wallet/browser extension is the only party positioned to record contextual intent (why this action, which UI flow triggered it) that the VTA alone cannot reconstruct.

🔎 Threat Clue: Derived from COMP-003 via EP-004, EP-005

  • Data Flows: Holder session->vaultTaskSigner->VTA sign-trust-task

Preconditions: Dispute arises over whether a persona-signed action was authorized by the legitimate holder, No local audit trail exists to corroborate intent at time of signing

Existing Controls: VTA presumably logs sign-trust-task requests server-side (unverified, out of scope for this repo) • holder session authentication to the VTA (vault/sign-trust-task/0.2) provides some level of accountability at the transport layer

Recommended Mitigations: Add local, tamper-evident audit logging in vaultTaskSigner capturing holder DID, entryId, target service, envelope type/id, and timestamp prior to dispatch • Surface a user-visible confirmation/consent UI step (referencing packages/extension/src/confirm.tsx) specifically for persona-signing actions, with the confirmation decision logged • Coordinate with VTA maintainers to ensure sign-trust-task requests are logged with sufficient correlation IDs to reconcile with client-side records


🟠 STRIDE-10: Extension Message Bridge Lacks Sender Authentication for Trust Task Dispatch

Field Detail
Category Spoofing, Elevation of Privilege
Severity High
Likelihood Possible
CVSS 7.6 CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-346,CWE-862
CAPEC CAPEC-664,CAPEC-115
OWASP A01:2021 - Broken Access Control

Description: background.ts message bridge (chrome.runtime messaging, EP-009) allows any extension context or content script to trigger authenticated login/signing flows due to unauthenticated inbound message handling (auth_required: false per recon) at the extension messaging boundary, resulting in potential unauthorized invocation of loginViaTrustTask, loginViaSiop, or vaultTaskSigner-backed signing flows by malicious or compromised page content.

Evidence: packages/extension/src/background.ts:N/A

N/A — background.ts not included in reduced source slice; entry point derived from recon EP-009 (chrome.runtime messaging, inferred, auth_required: false)

Attack Scenario:

  1. A malicious or compromised website injects content that sends a chrome.runtime.sendMessage-style message (or window.postMessage relayed by bridge-protocol.ts) attempting to reach background.ts's message handler.
  2. Because EP-009 is marked unauthenticated in recon and no sender-origin/sender-id validation is evidenced in the provided files, background.ts may process the message as if it originated from the legitimate confirm.tsx / offscreen.ts UI flow.
  3. If the message schema allows triggering loginViaTrustTask/loginViaSiop/persona-signing flows with attacker-influenced parameters (e.g., target RP service DID, subject persona), the extension could be coerced into signing or authenticating against an RP chosen by the attacker.
  4. Combined with STRIDE-1/STRIDE-2 (persona/subject confusion), a successful message-spoofing attack could chain into unauthorized cross-persona authentication without the user's confirm.tsx consent screen being genuinely shown, if the confirmation step can be bypassed or auto-approved under attacker-controlled conditions.
  5. Result: a website-level attacker escalates to invoking wallet-privileged operations that should require explicit, authenticated user/extension-UI consent.

🔎 Threat Clue: Derived from COMP-008 via EP-009

  • Data Flows: Web page/content script->background.ts message bridge->loginViaTrustTask/loginViaSiop/vaultTaskSigner

Preconditions: background.ts message handler does not verify sender.id/sender.origin matches expected extension/page context, Message schema permits invoking sensitive login/signing operations directly from inbound messages, No mandatory user-interactive confirmation gate before wallet-privileged actions execute

Existing Controls: Chrome extension messaging APIs provide sender.tab/sender.origin metadata that COULD be checked (not confirmed present in this file subset) • confirm.tsx suggests a user-consent UI step exists somewhere in the flow, which may mitigate silent exploitation if enforced consistently

Recommended Mitigations: Enforce strict sender.id/sender.origin allowlisting in background.ts for every message type that can trigger login or signing flows • Require explicit, non-bypassable user confirmation (confirm.tsx) before any persona-signing or login operation proceeds, with no default-approve code path • Apply the principle of least privilege to the message schema: separate low-risk status queries from high-risk signing/login triggers, with different authorization requirements • Add integration tests simulating malicious postMessage/sendMessage payloads to confirm they are rejected



🍝 PASTA Threat Model

Application Purpose

A browser-extension-based digital wallet implementing DIDComm and SIOP-based decentralized identity authentication, enabling users to log into relying parties either as their holder identity or as per-site personas whose signing keys are managed remotely by a Vault/Trust-Task-Agent (VTA).

Inherent Risks

  • The wallet holds and delegates cryptographic signing authority for both a primary holder identity and multiple remotely-keyed per-site personas.
  • Authentication correctness depends on distributed invariant enforcement across client, VTA, and RP components that are not all visible/auditable within this repository.
  • Browser extension architecture inherently exposes a message-passing attack surface to untrusted web content.

Objectives

Risk: Do not allow a single missing local assertion to be the sole barrier between a benign refactor and full persona impersonation.; Treat any newly pluggable interface (TaskSigner, ChannelSigner, SiopIdTokenMinter) as an expansion of the trusted computing base requiring compensating controls.
Business: Enable privacy-preserving per-site persona logins without requiring the holder's primary key to be exposed to every relying party.; Support pluggable identity/signing backends (self-issued vs. VTA-hosted) to broaden interoperability with different relying party ecosystems.
Security: Ensure that a signed document's issuer DID always matches the DID of the party whose private key actually produced the signature.; Prevent any caller — malicious or buggy — from causing the wallet to sign or mint credentials for an identity the current session is not entitled to use.; Ensure the browser extension's message-passing surface only accepts privileged instructions from trusted, authenticated senders.
Financial: Avoid liability from account takeover or persona impersonation incidents that could trigger regulatory fines or user compensation claims.
Compliance: Maintain non-repudiation properties adequate for identity-assurance frameworks (e.g. eIDAS-like assurance levels) governing decentralized identity wallets.; Preserve auditability of signing operations sufficient for dispute resolution and regulatory inquiry.
Functional: Support both self-issued (selfIssuedMinter) and VTA-minted (vaultTaskSigner) identity token issuance paths.; Support login via Trust Task protocol and SIOP protocol against relying parties with differing capability support.
Operational: Maintain reliable challenge-response authentication flows across DIDComm and REST transport channels.; Ensure consistent behavior of the auth-tasks.ts issuer-default pattern across all five auth flow functions.

Business Impact Analysis (3)

BIA-1: Persona-Based Relying Party Authentication (Critical)

End-to-end flow where a user authenticates to a relying party either as their holder identity or as a VTA-hosted persona, producing a session the RP trusts for subsequent authorization decisions.

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

  • Stakeholders: Extension End Users / Relying Parties (RPs) / VTA Operators / Wallet/Extension Maintainers
  • Dependencies: DIDComm Transport (DidcommVtaTransport) / REST Channel (RestChannel) / Trust Task Protocol Envelope/Proof System / VTA Vault Service (sign-trust-task endpoint) / Relying Party Auth API (/auth/challenge, handle_authenticate)
  • Disruptions: Persona impersonation allowing cross-account access at an RP / Removal or regression of the holder/signer identity-consistency guard / VTA outage preventing persona-signed authentication entirely / Malicious extension message triggering unauthorized login/signing
  • Impacts: Unauthorized account access at one or more relying parties, potentially exposing PII or enabling fraudulent transactions / Loss of user trust in the wallet's core security guarantee (that a persona cannot sign as another persona) / Regulatory scrutiny/incident reporting obligations if unauthorized authentication is confirmed in production / Support/incident-response cost to investigate and remediate identity-confusion incidents

BIA-2: Vault-Backed Persona Document Signing (High)

Flow where the wallet requests the VTA to sign an outbound Trust Task envelope using a persona's vault-held key, returning a proof that is attached to the envelope before it is sent to a relying party.

MTD: 02 days 00:00 hours | RTO: 00 days 08:00 hours | RPO: 00 days 01:00 hours

  • Stakeholders: Extension End Users / VTA Operators / Relying Parties
  • Dependencies: Vault Entry Storage (VTA-side) / vault/sign-trust-task/0.2 protocol / TaskSigner abstraction
  • Disruptions: entryId/did mismatch allowing wrong-key signing / VTA returning malformed or empty proof objects / VTA service unavailability blocking all persona-based signing
  • Impacts: Cross-persona signature forgery enabling impersonation at an RP / Failed logins for all persona users during VTA outage, degrading service availability / Increased support burden triaging 'proof missing' errors

BIA-3: Extension Message-Bridge Mediated User Actions (High)

Flow where UI or page-context messages are relayed through background.ts to trigger wallet operations such as login initiation, confirmation display, and signing dispatch.

MTD: 01 days 00:00 hours | RTO: 00 days 04:00 hours | RPO: N/A

  • Stakeholders: Extension End Users / Website Operators (benign and malicious) / Wallet Maintainers
  • Dependencies: chrome.runtime messaging APIs / bridge-protocol.ts message schema / confirm.tsx consent UI / offscreen.ts execution context
  • Disruptions: Unauthenticated or unauthorized message triggering privileged wallet operations / Consent UI bypass allowing silent authorization
  • Impacts: Unauthorized login/signing operations triggered by malicious web content / Erosion of user trust in extension security boundary

Technical Scope

Roles (4): RO-1 Wallet Holder · RO-2 Per-Site Persona · RO-3 VTA Operator · RO-4 Relying Party

Actors (4): AC-1 Extension End User · AC-2 Wallet Core Library · AC-3 VTA Service Process · AC-4 Relying Party Auth Service

Entry Points (9): EP-001 loginViaTrustTask · EP-002 loginViaSiop · EP-003 RP Auth Challenge Endpoint · EP-004 vaultTaskSigner.sign · EP-005 VTA sign-trust-task Endpoint · EP-006 Auth Task Envelope Builders · EP-007 REST Trust Task Dispatch · EP-008 DIDComm Message Send/Notify · EP-009 Extension Message Bridge

Threat Actors (4): TA-1 Malicious Website Operator · TA-2 Compromised Dependency / Supply-Chain Actor · TA-3 Malicious or Compromised VTA-Side Insider · TA-4 Network-Positioned Attacker

Infrastructure (3): IF-1 Browser Extension Runtime · IF-2 VTA Hosted Service · IF-3 Relying Party Hosted Service

Trust Boundaries (4): TB-1 Browser Extension Boundary · TB-2 Wallet-to-VTA Trust Boundary · TB-3 Wallet-to-RP Trust Boundary · TB-4 Internal Core Library Boundary

External Entities (3): EE-1 Relying Party (RP) · EE-2 VTA Vault Service · EE-3 Untrusted Web Page / Content Script

System Components (10): SC-1 loginViaTrustTask / rp-login module · SC-2 loginViaSiop / SIOP login client · SC-3 vaultTaskSigner / Vault Module · SC-4 auth-tasks.ts Auth Task Callers · SC-5 trust-task.ts signOutboundTask / TaskSigner Core · SC-6 DidcommVtaTransport · SC-7 RestChannel · SC-8 Extension Background Message Bridge · SC-9 VTA Vault Service · SC-10 Relying Party Auth API

Resources And Assets (5): RA-1 Holder Signing Identity · RA-2 Persona Signing Key (VTA Vault Entry) · RA-3 Trust Task Envelope / Proof · RA-4 Authentication Challenge/Nonce/SessionId · RA-5 SIOP id_token

Technologies And Dependencies (1):

Use Cases (3)

  • Holder Self-Issued RP Login via Trust Task: The wallet holder authenticates directly to a relying party using their own browser-held signing key, obtaining an authenticated RP session without involving any persona or the VTA.
  • Per-Site Persona Login via VTA-Backed Signing: The wallet holder authenticates to a relying party as a per-site persona, whose signing key is held remotely at the VTA vault service and never leaves it, with the wallet requesting the VTA to sign th
  • SIOP Login with Pluggable id_token Minter: The wallet authenticates to a relying party using the SIOP protocol, obtaining a challenge nonce and minting a self-issued or VTA-minted id_token that is delivered to the RP as proof of identity.

📋 Risk Registry (6)

ID Title Severity Residual Priority Effort
RISK-001 Cross-persona impersonation via unverified vault entryId-to-DID binding Critical High Immediate Medium
RISK-002 Loss of defense-in-depth from removed holder-equals-signer client guard High Medium Short-Term Low
RISK-003 Untrusted pluggable id_token minting source widening trusted computing base High Medium Short-Term Medium
RISK-004 Unauthenticated challenge endpoint enabling enumeration and flooding Medium Medium Medium-Term Low
RISK-005 Unauthenticated extension message bridge exposing privileged operations to web content High Medium Immediate Medium
RISK-006 Silent issuer default fallback causing persona/holder confusion across multi-step auth flows Medium Low Medium-Term Medium

⚔️ Attack Scenarios (4)

SC-3: vaultTaskSigner / Vault Module

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC3@{ shape: rect, label: "SC-3: vaultTaskSigner / Vault Module" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE346@{ shape: rect, label: "CWE-346: Origin Validation Error" }
    CWE863@{ shape: rect, label: "CWE-863: Incorrect Authorization" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC151@{ shape: rect, label: "CAPEC-151: Identity Spoofing" }
    CAPEC196@{ shape: rect, label: "CAPEC-196: Session Credential Falsification" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    S1@{ shape: rect, label: "STRIDE-1: Persona Impersonation via Unverified entryId-to-DID Binding<br><i>Critical / Likely</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA3@{ shape: rect, label: "TA-3: Malicious or Compromised VTA-Side Insider<br><i>Abuse loose entryId/did binding</i>" }
  end
  SC3 --> CWE346
  SC3 --> CWE863
  CWE346 --> CAPEC151
  CWE863 --> CAPEC196
  CAPEC151 --> S1
  CAPEC196 --> S1
  S1 --> TA3
  linkStyle 0 stroke:#A50000, stroke-width:2px
  linkStyle 1 stroke:#A50000, stroke-width:2px
  linkStyle 2 stroke:#A50000, stroke-width:2px
  linkStyle 3 stroke:#A50000, stroke-width:2px
  linkStyle 4 stroke:#A50000, stroke-width:2px
  linkStyle 5 stroke:#A50000, stroke-width:2px
  linkStyle 6 stroke:#A50000, stroke-width:2px
Loading

SC-1: loginViaTrustTask / rp-login module

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC1@{ shape: rect, label: "SC-1: loginViaTrustTask / rp-login module" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE863B@{ shape: rect, label: "CWE-863: Incorrect Authorization" }
    CWE284@{ shape: rect, label: "CWE-284: Improper Access Control" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC151B@{ shape: rect, label: "CAPEC-151: Identity Spoofing" }
    CAPEC115@{ shape: rect, label: "CAPEC-115: Authentication Bypass" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    S2@{ shape: rect, label: "STRIDE-2: Removal of Client-Side Holder-Equals-Signer Guard<br><i>High / Likely</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA2@{ shape: rect, label: "TA-2: Compromised Dependency / Supply-Chain Actor<br><i>Inject malicious signer implementation</i>" }
  end
  SC1 --> CWE863B
  SC1 --> CWE284
  CWE863B --> CAPEC151B
  CWE284 --> CAPEC115
  CAPEC151B --> S2
  CAPEC115 --> S2
  S2 --> TA2
  linkStyle 0 stroke:#FF0000, stroke-width:2px
  linkStyle 1 stroke:#FF0000, stroke-width:2px
  linkStyle 2 stroke:#FF0000, stroke-width:2px
  linkStyle 3 stroke:#FF0000, stroke-width:2px
  linkStyle 4 stroke:#FF0000, stroke-width:2px
  linkStyle 5 stroke:#FF0000, stroke-width:2px
  linkStyle 6 stroke:#FF0000, stroke-width:2px
Loading

SC-2: loginViaSiop / SIOP login client

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC2@{ shape: rect, label: "SC-2: loginViaSiop / SIOP login client" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE345@{ shape: rect, label: "CWE-345: Insufficient Verification of Data Authenticity" }
    CWE307@{ shape: rect, label: "CWE-307: Improper Restriction of Excessive Authentication Attempts" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC593@{ shape: rect, label: "CAPEC-593: Session Hijacking" }
    CAPEC112@{ shape: rect, label: "CAPEC-112: Brute Force" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    S4@{ shape: rect, label: "STRIDE-4: Pluggable SiopIdTokenMinter Allows Arbitrary id_token Minting<br><i>High / Possible</i>" }
    S7@{ shape: rect, label: "STRIDE-7: Missing Rate Limiting on /auth/challenge<br><i>Medium / Likely</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA2B@{ shape: rect, label: "TA-2: Compromised Dependency / Supply-Chain Actor<br><i>Inject malicious minter</i>" }
    TA4@{ shape: rect, label: "TA-4: Network-Positioned Attacker<br><i>Enumerate or flood challenge endpoint</i>" }
  end
  SC2 --> CWE345
  SC2 --> CWE307
  CWE345 --> CAPEC593
  CWE307 --> CAPEC112
  CAPEC593 --> S4
  CAPEC112 --> S7
  S4 --> TA2B
  S7 --> TA4
  linkStyle 0 stroke:#FF0000, stroke-width:2px
  linkStyle 1 stroke:#FFA500, stroke-width:2px
  linkStyle 2 stroke:#FF0000, stroke-width:2px
  linkStyle 3 stroke:#FFA500, stroke-width:2px
  linkStyle 4 stroke:#FF0000, stroke-width:2px
  linkStyle 5 stroke:#FFA500, stroke-width:2px
  linkStyle 6 stroke:#FF0000, stroke-width:2px
  linkStyle 7 stroke:#FFA500, stroke-width:2px
Loading

SC-8: Extension Background Message Bridge

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC8@{ shape: rect, label: "SC-8: Extension Background Message Bridge" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE346B@{ shape: rect, label: "CWE-346: Origin Validation Error" }
    CWE862@{ shape: rect, label: "CWE-862: Missing Authorization" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC664@{ shape: rect, label: "CAPEC-664: Server Side Request Forgery (message analog)" }
    CAPEC115B@{ shape: rect, label: "CAPEC-115: Authentication Bypass" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    S10@{ shape: rect, label: "STRIDE-10: Extension Message Bridge Lacks Sender Authentication<br><i>High / Possible</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA1@{ shape: rect, label: "TA-1: Malicious Website Operator<br><i>Trigger unauthorized wallet operations</i>" }
  end
  SC8 --> CWE346B
  SC8 --> CWE862
  CWE346B --> CAPEC664
  CWE862 --> CAPEC115B
  CAPEC664 --> S10
  CAPEC115B --> S10
  S10 --> TA1
  linkStyle 0 stroke:#FF0000, stroke-width:2px
  linkStyle 1 stroke:#FF0000, stroke-width:2px
  linkStyle 2 stroke:#FF0000, stroke-width:2px
  linkStyle 3 stroke:#FF0000, stroke-width:2px
  linkStyle 4 stroke:#FF0000, stroke-width:2px
  linkStyle 5 stroke:#FF0000, stroke-width:2px
  linkStyle 6 stroke:#FF0000, stroke-width:2px
Loading

📊 Risk Summary

Total Threats: 10

By Severity: Low: 1 · High: 3 · Medium: 5 · Critical: 1

By Category: Spoofing: 6 · Tampering: 5 · Elevation of Privilege: 4 · Information Disclosure: 2 · Repudiation: 2 · Denial of Service: 1


Generated by Agentic Sec — Threat Model & Affect Analysis Agent

📊 Summary & findings
✅ Confirmed ⚠️ Must-Review-By-Human
0 2

Must-Review-By-Human (2)

  • 🔵 Unsafe Formatstring (3 occurrences)
  • 🟡 SigningIdentity/ChannelSigner abstraction moves trust boundary to remote VTA without validating persona binding before session/document construction

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