Skip to content

fix(rp-login): send the authenticate type the RP actually binds - #139

Merged
stormer78 merged 1 commit into
mainfrom
fix/rp-login-canonical-uris
Aug 29, 2026
Merged

fix(rp-login): send the authenticate type the RP actually binds#139
stormer78 merged 1 commit into
mainfrom
fix/rp-login-canonical-uris

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

DIDComm login to an RP cannot succeed today

This package sent https://affinidi.com/webvh/1.0/authenticate and expected …/authenticate-response. The control plane's DIDComm router binds neither. From did-hosting-common/src/didcomm_types.rs:

pub const MSG_AUTHENTICATE:  &str = "https://trusttasks.org/spec/auth/authenticate/0.1";
pub const MSG_AUTH_RESPONSE: &str = "https://trusttasks.org/spec/auth/authenticate/0.1#response";

So the request never reached a handler, and a reply under the canonical type would have been refused here as the wrong type anyway. Both ends of the exchange were wrong.

This is a live, user-facing path: a site requests login, background.ts raises a consent prompt, the user approves — and then it fails.

It is the drift R3.6 exists for, and the same failure vti-didcomm-js 0.6.1 had against the VTA: a retired vendor URI left behind when the server moved to the canonical one. Nothing caught it because nothing tested this module — the two type URIs were string constants no assertion ever read.

The fix, and the guard

Matched with === against what the RP declares today. No both-spellings fold, and a test asserts the retired response type is still refused, so one cannot creep back later as a "safe" addition.

Five tests, and the shape of them matters: the one that would have caught this unpacks the outgoing envelope as the RP would and asserts the request type. Reply-side assertions alone would have passed throughout the entire period this was broken.

A conformance gap stays open — now written down at the site

The canonical auth/authenticate/0.1 schema declares challenge and sessionId REQUIRED. But:

  • the RP's DIDComm handler is run_authenticate(&state, sender) — it authenticates on the authcrypt sender and reads nothing from the body;
  • its challenge endpoint is POST /api/auth/challenge, REST-only.

So this sends a canonical type over a body that does not satisfy the schema. That shape is the RP's choice — it bound a sender-authenticated handler to the canonical URI — and matching it is strictly better than sending a URI nothing routes. But it should be visible rather than discovered, so the reasoning is in the code.

Closing it properly means the challenge-based flow over a TrustTaskSender — the shape vta/auth-tasks.ts already uses against the VTA, and what would also make this seam transport-agnostic. The RP already routes trust_tasks_didcomm::ENVELOPE_TYPE through the same handlers as POST /api/trust-tasks, so the infrastructure is there.

472 core tests pass; lint and build clean.

Testing note

Worth a live check after merge: a site-initiated DIDComm login against the control plane should now complete rather than fail after the consent prompt. If it was silently falling back to the REST SIOP path before, that would explain why the breakage went unnoticed.

DIDComm login to a did-hosting RP sent
`https://affinidi.com/webvh/1.0/authenticate` and expected
`…/authenticate-response`. The control plane's DIDComm router binds neither:
`did-hosting-common`'s `didcomm_types.rs` has

    pub const MSG_AUTHENTICATE:  &str = ".../spec/auth/authenticate/0.1";
    pub const MSG_AUTH_RESPONSE: &str = ".../spec/auth/authenticate/0.1#response";

so the request did not route to a handler, and a reply under the canonical type
would have been refused here as the wrong type in any case. Login over DIDComm
could not succeed against a current RP at all — the user approves the consent
prompt, and it fails after.

This is the drift R3.6 exists for, and the same failure vti-didcomm-js 0.6.1
had against the VTA: a retired vendor URI left behind when the server moved to
the canonical one. Nothing caught it because nothing tested this module — the
two type URIs were string constants no assertion ever read.

Matched with `===` against the spelling the RP declares today. No
both-spellings fold, and a test asserts the retired response type is still
refused so one cannot creep back as a "safe" addition.

Tests pin the contract in both directions, including the request type by
unpacking the outgoing envelope as the RP would — that is the assertion that
would have caught this, and the reply-side assertions alone would not have.

**A conformance gap is left open, and now written down at the site.** The
canonical `auth/authenticate/0.1` schema declares `challenge` and `sessionId`
REQUIRED; the RP's DIDComm handler authenticates on the authcrypt sender
(`run_authenticate(&state, sender)`) and reads nothing from the body, and its
challenge endpoint is REST-only. So this sends a canonical type over a body
that does not satisfy it — a shape the RP chose when it bound a
sender-authenticated handler to that URI. Matching the RP is strictly better
than sending a URI nothing routes; closing the gap properly means the
challenge-based flow over a `TrustTaskSender`, the shape `vta/auth-tasks.ts`
already uses against the VTA.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 merged commit ef09c80 into main Aug 29, 2026
3 checks passed
@stormer78
stormer78 deleted the fix/rp-login-canonical-uris branch August 29, 2026 13:40
stormer78 added a commit that referenced this pull request Aug 29, 2026
Both RP-login flows — REST SIOPv2 (`login`) and DIDComm (`loginDidcomm`) —
could only be exercised against a real deployment, so in practice they were not
exercised at all. One of them was broken for an unknown length of time as a
direct result: the wallet sent a retired `authenticate` Type URI the control
plane no longer routes, a site-initiated login failed after the user had already
approved the consent prompt, and nothing surfaced it (#139).

This is the missing half of that loop. A page that calls `window.vtaWallet` the
way a relying party does, against a control plane of your choosing — the live
one, or a `did-hosting-control` on localhost — and shows what came back.

It asserts nothing. The point is to make the round-trip observable next to the
wallet's own console, which is where the diagnosis actually happens; a harness
that judged the result would just be a worse unit test with a network
dependency.

Kept out of `server.mjs` deliberately: that is a password-login target for the
VTA's `vault/proxy-login` driver and shares nothing with this but a workspace.

It also reports when no provider is present rather than leaving a dead button —
content scripts are registered per granted origin and never reach a tab that is
already open, so a first visit after granting needs a reload, and that is a
confusing five minutes if the page says nothing.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@affinidi-appsecurity-bot

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 #139

Field Value
Repository OpenVTC/vta-browser-plugin
Branch fix/rp-login-canonical-urismain
Validated 2026-08-29
Scan ID 7d9489be
Validator AI Security Validation Agent

🗺️ Scan Coverage

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

Module Files scanned Findings
packages/core 2 3

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.

🟠 Authentication request body omits required challenge/sessionId binding (protocol non-conformance / weak authentication design)

Field Detail
Severity HIGH
Location packages/core/src/rp-login/didcomm.ts:36
Finding ID github_pr-b32de6ae644d
CWE CWE-345, CWE-294
OWASP A07:2021 - Identification and Authentication Failures
MITRE ATT&CK T1550 - Use Alternate Authentication Material
CAPEC CAPEC-60, CAPEC-459
DREAD 5.2
Reachability 🔴 Reachable
Exploit Maturity conceptual
Detection Source skill_scan

Summary: The login flow's DIDComm 'authenticate' message body is always empty, omitting the challenge/sessionId fields that the canonical schema requires and which would provide freshness/anti-replay binding at the protocol layer. This is called out directly in code comments as an unresolved gap, meaning the shipped authentication flow lacks a defense-in-depth freshness control beyond whatever the transport/crypto layer provides.

📝 Description:

Without a challenge/sessionId binding in the authenticate request body, the application-layer protocol has no cryptographic freshness guarantee tied to a specific login attempt. Any weakness in the lower transport/envelope-layer replay protection (not visible/verified in this module) could allow an old captured 'authenticate' envelope to be reused to trigger new session-token issuance for the victim, without this module providing a compensating control.

🧪 Proof of Concept:

The message type is bound to a schema requiring challenge/sessionId, but the implementation sends body: {} unconditionally, relying entirely on authcrypt sender identity for authentication with no session/challenge binding at the application layer.

const MSG_AUTHENTICATE = "https://trusttasks.org/spec/auth/authenticate/0.1";
const MSG_AUTH_RESPONSE = "https://trusttasks.org/spec/auth/authenticate/0.1#response";
...
    type: MSG_AUTHENTICATE,
    from: holder.did,
    to: [service.did],
    // Empty, because the RP's DIDComm handler authenticates on the **authcrypt
    // sender** and reads nothing from the body (`run_authenticate(&state,
    // sender)`).
    body: {},
  };

Vulnerable lines: 30, 52

🔁 Reproduction Steps:

  1. Build the package and run loginViaDidcomm() against any bridge implementation.
  2. Inspect the outgoing packed DIDComm envelope (e.g., via the test's unpack() call as in rp-login.didcomm.mjs).
  3. Observe opened.message.body is {} — no challenge or sessionId field is present despite the message type being the canonical auth/authenticate/0.1 which requires them.
  4. Capture this envelope (e.g., via a compromised relay/forward node) and note that nothing in the message ties it to a specific login session or time window at the application layer.

🔎 Evidence: packages/core/src/rp-login/didcomm.ts:36

    body: {},
    // canonical `auth/authenticate/0.1` schema declares `challenge` and
    // `sessionId` REQUIRED, and the RP obtains a challenge from a REST-only
    // `POST /api/auth/challenge`.

💥 Impact:

Without a challenge/sessionId binding in the authenticate request body, the application-layer protocol has no cryptographic freshness guarantee tied to a specific login attempt. Any weakness in the lower transport/envelope-layer replay protection (not visible/verified in this module) could allow an old captured 'authenticate' envelope to be reused to trigger new session-token issuance for the victim, without this module providing a compensating control.

Confidentiality: Medium — potential unauthorized re-issuance of session tokens tied to victim identity. · Integrity: High — authentication events for the holder could be replayed without a fresh proof. · Availability: None

🧭 Reachability:

  • Network exposure: public
  • Auth barrier: basic
  • Attack path: EP-001 (DIDCOMM authenticate) → loginViaDidcomm() → message construction at didcomm.ts:36 → sent via bridge.sendAndAwaitReply()

⚖️ Triage Factors:

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

Attack scenario: An attacker replays a captured 'authenticate' DIDComm envelope, which lacks a challenge/sessionId binding, potentially re-triggering session-token issuance if lower-layer replay protection is weak.

🔧 Remediation:

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

Wiring the existing REST challenge endpoint (POST /api/auth/challenge) into the DIDComm authenticate flow, and embedding challenge/sessionId in the message body, satisfies the canonical schema's REQUIRED fields and gives the RP an application-layer freshness/anti-replay check independent of the transport layer.

Vulnerable code:

body: {},

Secure code:

// 1. Fetch a fresh, short-lived challenge from the RP first:
const { challenge, sessionId } = await fetchChallenge(service); // POST /api/auth/challenge

// 2. Bind it into the authenticate message body:
const message = {
  type: MSG_AUTHENTICATE,
  from: holder.did,
  to: [service.did],
  body: { challenge, sessionId },
};

Additional recommendations:

  • Enforce short challenge TTLs (e.g., 60s) on the RP side.
  • Bind the challenge to the specific holder DID and single-use it (nonce consumption).
  • Add a regression test asserting the request body always contains a non-empty challenge/sessionId once implemented.

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 55%
  • AI Validation Evidence: EVIDENCE FOUND: didcomm.ts line 52 sets body: {} on the authenticate message, and the file's own comment explicitly states 'the canonical auth/authenticate/0.1 schema declares challenge and sessionId REQUIRED' while the RP handler is documented as authenticating on 'the authcrypt sender' (run_authenticate(&state, sender)) and 'reads nothing from the body'. The test file (rp-login.didcomm.mjs) asserts assert.deepEqual(opened.message.body, {}) confirming this is the intended contract, not an oversight. EVIDENCE NOT FOUND: The actual server-side handle_authenticate/run_authenticate implementation (did-hosting-control) is not in source_files, so it cannot be independently confirmed whether authcrypt-sender-only authentication constitutes an actual exploitable weakness (e.g. replay) versus a documented, accepted design tradeoff matching the real RP's current behavior. CHANGED VS PRE-EXISTING: CHANGED — didcomm.ts is a file modified by this MR (message construction with body:{} and the surrounding comments are part of the diff), and packages/core/tests/rp-login.didcomm.mjs directly pins this exact behavior with a new assertion. VERDICT JUSTIFICATION: The code matches an already-deployed RP contract per the comments (this is presented as fixing routing drift, not weakening auth), and the primary authentication guarantee is the authcrypt sender identity, not the body. Since the actual security boundary (server-side ACL/session issuance) is outside provided files, this cannot be fully confirmed as exploitable or dismissed — requires human review of the RP's server-side handler to assess real replay/session-binding risk.
  • 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.

🟠 loginViaDidcomm trusts caller-supplied RP key material with no independent DID resolution/revocation check

Field Detail
Severity HIGH
Location packages/core/tests/rp-login.didcomm.mjs:104
Finding ID github_pr-fcfbae739077
CWE CWE-295, CWE-346
OWASP A07:2021 - Identification and Authentication Failures
MITRE ATT&CK T1556 - Modify Authentication Process
CAPEC CAPEC-151, CAPEC-94
DREAD 5
Reachability 🔴 Reachable
Exploit Maturity conceptual
Detection Source skill_scan

Summary: The RP identity check in loginViaDidcomm relies entirely on caller-supplied service.did and service.keyAgreementPublicJwk/keyAgreementKid values rather than performing a live, verified DID resolution within this module. If the upstream caller ever sources this key material from a stale cache or compromised resolver, an attacker with a rotated-out or forged key could pass authentication checks that only compare string equality of from against the caller-provided DID.

📝 Description:

If the upstream key-sourcing mechanism (outside this reviewed file) is ever compromised or stale, an attacker could impersonate the RP to the holder's browser-plugin client, causing the client to accept forged session_id/access_token/refresh_token values as legitimate, resulting in session/credential theft or malicious session establishment against the victim's identity.

🧪 Proof of Concept:

The service object (including its key material) is entirely caller-supplied in every call site shown, including the test harness mirroring production usage; the module performs no independent DID document resolution to verify the key-agreement key is current/non-revoked before trusting it for authcrypt decryption and identity comparison.

function opts(b) {
  return {
    bridge: b,
    holder: Identity.fromSecretJwk({ ... }),
    service: {
      did: RP_DID,
      keyAgreementKid: rpParty.kid,
      keyAgreementPublicJwk: rpParty.publicJwk,
    },
  };
}
...
test("a reply from someone other than the RP is refused", async () => {
  const b = bridge({ from: "did:web:imposter.example", type: AUTH_RESPONSE, body: {...} });
  await assert.rejects(() => loginViaDidcomm(opts(b)), /!= RP/);
});

Vulnerable lines: 78, 111

🔁 Reproduction Steps:

  1. Identify or influence the code path in the consuming application (outside this file) that supplies service.did/service.keyAgreementPublicJwk/keyAgreementKid to loginViaDidcomm.
  2. Cause that upstream path to supply a stale or attacker-controlled key-agreement public key for the RP's DID (e.g., via a compromised DID resolver, a poisoned cache, or a config injection).
  3. Send an authcrypt-encrypted message decryptable with the attacker's corresponding private key, with from set to the expected service.did string.
  4. Observe loginViaDidcomm accepts the message as if genuinely from the RP, since it only checks from === service.did and decrypts using whatever key it was handed.

🔎 Evidence: packages/core/tests/rp-login.didcomm.mjs:104

test("a reply from someone other than the RP is refused", async () => {
  const b = bridge({ from: "did:web:imposter.example", type: AUTH_RESPONSE, body: {...} });
  await assert.rejects(() => loginViaDidcomm(opts(b)), /!= RP/);
});

💥 Impact:

If the upstream key-sourcing mechanism (outside this reviewed file) is ever compromised or stale, an attacker could impersonate the RP to the holder's browser-plugin client, causing the client to accept forged session_id/access_token/refresh_token values as legitimate, resulting in session/credential theft or malicious session establishment against the victim's identity.

Confidentiality: High — attacker-controlled session tokens could be used to access the holder's authenticated session data. · Integrity: High — forged authentication response accepted as legitimate. · Availability: Medium — could disrupt legitimate login flows if poisoning also blocks genuine RP responses.

🧭 Reachability:

  • Network exposure: public
  • Auth barrier: basic
  • Attack path: EP-002 (DIDCOMM authenticate-response) → loginViaDidcomm() → unpack()/authcrypt verification using caller-supplied service.keyAgreementPublicJwk → from === service.did check (didcomm.ts, response-handling section not shown in full but confirmed by test behavior)

⚖️ Triage Factors:

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

Attack scenario: Attacker poisons the upstream source of the RP's key-agreement key, then forges an authcrypt response that passes the from===service.did check because the module trusts caller-supplied key material without live DID resolution.

🔧 Remediation:

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

Resolving the DID document fresh (or with a short-TTL cache plus revocation checks) at call time ensures the key material used for authcrypt verification is current and has not been rotated out or revoked, closing the trust gap left by accepting arbitrary caller-supplied key material.

Vulnerable code:

service: {
  did: RP_DID,
  keyAgreementKid: rpParty.kid,
  keyAgreementPublicJwk: rpParty.publicJwk,
}

Secure code:

// Resolve fresh, verified key material immediately before use:
const didDoc = await resolveDid(service.did, { requireFresh: true });
const keyAgreementPublicJwk = didDoc.getVerifiedKeyAgreementKey(service.keyAgreementKid);
if (!keyAgreementPublicJwk) {
  throw new Error(`Key ${service.keyAgreementKid} not found or revoked in current DID document for ${service.did}`);
}
// proceed to unpack() using keyAgreementPublicJwk resolved just now

Additional recommendations:

  • Enforce a maximum staleness window for cached DID documents.
  • Log and alert on key-agreement key mismatches between cached and freshly resolved values.
  • Pin DID resolution to a trusted resolver with TLS and integrity verification.

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 50%
  • AI Validation Evidence: EVIDENCE FOUND: In rp-login.didcomm.mjs, opts() constructs service: { did: RP_DID, keyAgreementKid: rpParty.kid, keyAgreementPublicJwk: rpParty.publicJwk } directly from test fixtures, and didcomm.ts's loginViaDidcomm uses service.keyAgreementPublicJwk directly in packAuthcrypt(message, holder, [{ kid: service.keyAgreementKid, jwk: service.keyAgreementPublicJwk }]) — confirming the function trusts caller-supplied key material rather than performing its own DID resolution. The sender check is if (reply.from !== service.did) throw ... (didcomm.ts), which only compares the from string, consistent with the finding's claim. EVIDENCE NOT FOUND: No DID resolution, revocation, or key-rotation-check logic exists anywhere in the provided didcomm.ts or related files; but also no evidence of how service is populated in production (e.g. RemoteDidcommEndpoint construction site is not in source_files), so whether upstream callers perform fresh/secure DID resolution cannot be verified. CHANGED VS PRE-EXISTING: CHANGED — loginViaDidcomm in didcomm.ts (this MR's modified file) is the function exhibiting this design, and the cited test file is new/modified within this MR establishing the trust model via opts(). VERDICT JUSTIFICATION: The design pattern (trusting caller-supplied key material with only DID-string equality) is real and present in the changed code, but whether this constitutes an exploitable vulnerability depends entirely on how the calling application resolves service in production, which is not visible in the provided files — cannot be safely confirmed 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.


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 #139

Field Value
Repository OpenVTC/vta-browser-plugin
Branch fix/rp-login-canonical-urismain
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

Fixes a broken DIDComm RP login flow by replacing retired 'affinidi.com/webvh/1.0/' message-type URI constants with the RP control plane's actual canonical URIs ('trusttasks.org/spec/auth/authenticate/0.1'). Prior to this change, login over DIDComm could never succeed because the request did not route to any handler on the RP. Adds a comprehensive new regression test suite pinning the wire contract (type strings, sender verification, malformed-response rejection, legacy-type rejection).

Diff: +174 / -5 lines
Types: bugfix, interoperability, test, security-relevant

⚠️ Security Implications

⚪ Restores previously non-functional authentication flow via correct RP-bound message-type URIs

Restores previously non-functional authentication flow via correct RP-bound message-type URIs

Action: Verify no other module/config in the broader monorepo still references the retired URIs, to avoid inconsistent behavior elsewhere.

🟡 Authenticate request body remains non-conformant to its own declared canonical schema — no application-layer challenge/nonce binding

Authenticate request body remains non-conformant to its own declared canonical schema — no application-layer challenge/nonce binding

Action: Implement the challenge-based flow using a TrustTaskSender (as referenced in comments and already used in vta/auth-tasks.ts): retrieve a challenge/sessionId via POST /api/auth/challenge (or an equivalent DIDComm-native mechanism) and embed it in the authenticate message body before this flow is reli

🔵 Breaking change: legacy 'affinidi.com/webvh/1.0/*' RPs will silently stop working with no fallback

Breaking change: legacy 'affinidi.com/webvh/1.0/*' RPs will silently stop working with no fallback

Action: Coordinate rollout with RP operators to confirm all production RPs bind the canonical trusttasks.org URIs before deploying this client change broadly.

⚪ Test suite added, closing a prior 'zero test coverage' gap on the authentication wire contract

Test suite added, closing a prior 'zero test coverage' gap on the authentication wire contract

Action: Extend coverage further to include timeout/no-reply behavior and type/range validation of numeric response fields (access_expires_at/refresh_expires_at), which remain untested.

🧩 Affected Components

Component Impact Change What Changed
rp-login/didcomm (loginViaDidcomm) high modified The DIDComm message-type URI constants used to address the RP's authenticate handler and to validate its response were changed from a retire

📁 File Classifications

packages/core/src/rp-login/didcomm.ts

  • Type: security

packages/core/tests/rp-login.didcomm.mjs

  • Type: test

🛡️ STRIDE Threat Model

Identified Threats (10)

⚪ STRIDE-1: Response Type Confusion in loginViaDidcomm Response Handler

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

Description: authenticate-response handler in loginViaDidcomm allows message type downgrade due to strict single-string equality without normalization or version negotiation, resulting in potential future compatibility failures or silent auth bypass if RP reintroduces legacy alias

Evidence: packages/core/src/rp-login/didcomm.ts:21-30

const MSG_AUTHENTICATE = "https://trusttasks.org/spec/auth/authenticate/0.1";
const MSG_AUTH_RESPONSE = "https://trusttasks.org/spec/auth/authenticate/0.1#response";

Attack Scenario:

  1. Attacker or misconfigured intermediary intercepts DIDComm transport (e.g., a compromised relay or forward node referenced by wrapForward) between holder and RP.
  2. Attacker crafts a reply message with a type field matching a previously-valid but now-retired URI (e.g. https://affinidi.com/webvh/1.0/authenticate-response) while the code only checks the new canonical string via === per the comment 'no both-spellings fold'.
  3. If the RP's server-side router is ever redeployed with backward-compatibility shims (dual-binding old and new URIs, a common migration pattern), the same relay could replay/forge an old-format response accepted by both client and legacy-compatible server.
  4. Because the client's authentication trust model relies solely on authcrypt sender identity and does not perform mutual protocol-version pinning beyond the literal type string, a downgrade-compatible RP would process the forged type.
  5. Test rp-login.didcomm.mjs explicitly proves current code rejects the legacy type, but the underlying architectural risk (single point-in-time string constant with no algorithm-agility mechanism) remains for future maintenance regressions.

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

  • Data Flows: DF-authenticate-response

Preconditions: Attacker has active man-in-the-middle position or forwarding-node compromise (wrapForward)., RP server would need to be redeployed with dual-URI compatibility (currently not the case per code).

Existing Controls: Strict === type match against canonical URI (no fold). • Explicit regression test 'the retired response type is refused'. • Sender identity verified via authcrypt in unpack.

Recommended Mitigations: Add protocol version negotiation with cryptographic binding of version to session. • Document and enforce a formal deprecation/rejection list for all historical type URIs, not just implicit omission. • Add a runtime allow-list assertion test that fails CI if any legacy alias is silently reintroduced.


⚪ STRIDE-2: Empty Authentication Request Body Missing Required Challenge/SessionId Fields

Field Detail
Category Tampering, Repudiation, Elevation of Privilege
Severity High
Likelihood Likely
CVSS 7.1 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-345,CWE-294
CAPEC CAPEC-60,CAPEC-459
OWASP A07:2021 - Identification and Authentication Failures

Description: authenticate message construction in loginViaDidcomm allows protocol non-conformance due to sending an empty body while the canonical schema requires challenge and sessionId fields, resulting in replay-susceptible or non-standard authentication that other conformant RPs may reject or mishandle

Evidence: packages/core/src/rp-login/didcomm.ts:36-52

body: {},
// canonical `auth/authenticate/0.1` schema declares `challenge` and `sessionId` REQUIRED...

Attack Scenario:

  1. The code constructs an authenticate DIDComm message with body: {} per lines noted in didcomm.ts, deliberately omitting challenge and sessionId fields required by the canonical auth/authenticate/0.1 schema.
  2. The comment in the source explicitly states this is a known conformance gap: the RP's REST-only POST /api/auth/challenge (EP-003) issues a challenge that is never bound into this DIDComm message.
  3. Because no challenge is echoed back in the authenticated request, the protocol lacks freshness/anti-replay binding at the message-body layer — the only anti-replay control is whatever occurs at the authcrypt/transport layer (e.g., DIDComm envelope nonce), which is not verified in this file.
  4. An attacker who can capture a previously sent (or in-flight relayed) authenticate envelope could attempt to replay it to the RP if the RP's authcrypt session/nonce validation has any weakness, since the body itself carries no session-binding data to reject stale requests.
  5. Because this is called out by the code's own authors as unresolved ('Closing it properly means the challenge-based flow...'), the gap is a conscious deferred risk shipped in this PR rather than an unknown regression.

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

  • Data Flows: DF-authenticate-request

Preconditions: RP must accept authcrypt-only authentication without body-level challenge validation (confirmed by comment: run_authenticate(&state, sender))., No additional nonce/freshness binding exists between the DIDComm envelope and this specific login attempt beyond the crypto layer.

Existing Controls: Server-side authentication driven by authcrypt sender identity (cryptographic, not body-based). • Documented conformance gap acknowledged in source comments for future remediation.

Recommended Mitigations: Implement the challenge-based flow using TrustTaskSender as referenced in comments, retrieving a challenge from POST /api/auth/challenge (EP-003) and embedding challenge/sessionId in the message body prior to shipping this change. • Add anti-replay nonce binding at the DIDComm envelope level with short validity windows. • Add schema validation on the RP side to reject empty bodies for authenticate/0.1 per its own declared REQUIRED fields.


⚪ STRIDE-3: Sender Spoofing via Missing DID Document Resolution Check in loginViaDidcomm

Field Detail
Category Spoofing, Information Disclosure, Elevation of Privilege
Severity High
Likelihood Possible
CVSS 7.4 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-295,CWE-346
CAPEC CAPEC-151,CAPEC-94
OWASP A07:2021 - Identification and Authentication Failures

Description: authenticate-response handler in loginViaDidcomm allows sender identity spoofing due to comparing only the from field on the decrypted message against service.did string equality without confirming the decrypted authcrypt sender key actually resolves to that DID's currently published key-agreement key, resulting in potential session-token theft if the passed-in service.keyAgreementPublicJwk is attacker-influenced or stale

Evidence: packages/core/tests/rp-login.didcomm.mjs:104-111

test("a reply from someone other than the RP is refused", async () => {
  const b = bridge({ from: "did:web:imposter.example", type: AUTH_RESPONSE, body: {...} });
  await assert.rejects(() => loginViaDidcomm(opts(b)), /!= RP/);
});

Attack Scenario:

  1. The loginViaDidcomm function accepts service.did, service.keyAgreementKid, and service.keyAgreementPublicJwk as caller-supplied options rather than resolving them fresh from a DID document at call time (per test file's opts() helper mirroring production usage).
  2. Test 'a reply from someone other than the RP is refused' confirms from !== service.did is rejected, but this only validates the claimed from field's string value versus the expected DID — it does not re-verify that the public key used for authcrypt decryption is the RP's currently valid, non-revoked key-agreement key from a live DID resolution.
  3. If the service.keyAgreementPublicJwk passed into loginViaDidcomm by the calling application (outside this module) is ever sourced from a cached, attacker-tamperable, or stale DID document (e.g., pinned config, local storage, or a compromised DID resolver upstream), an attacker who has compromised or rotated past that key could produce valid authcrypt-decryptable messages that pass the from === service.did check.
  4. Because this module does not itself perform DID resolution or key rotation/revocation checks, it fully trusts whatever keyAgreementPublicJwk its caller supplies, shifting the actual security boundary outside the reviewed code and creating a systemic risk if any caller in the broader vta codebase resolves DIDs insecurely (e.g., via HTTP without TLS pinning, or from a stale cache).
  5. A successful spoof yields fraudulent access_token/refresh_token/session_id values returned to the holder as if from the legitimate RP, enabling downstream session hijacking or credential harvesting of the holder's login flow.

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

  • Data Flows: DF-authenticate-response

Preconditions: Caller of loginViaDidcomm supplies a stale, cached, or otherwise compromised service.keyAgreementPublicJwk/keyAgreementKid., Attacker has compromised or intercepted the DID resolution path used upstream of this module (outside visible diff).

Existing Controls: Explicit from !== service.did string check rejecting mismatched sender DIDs. • Authcrypt cryptographic sender verification via unpack (confirmed in test using real key material). • Regression test asserting imposter DID rejection.

Recommended Mitigations: Perform fresh DID resolution (with revocation/rotation checks) immediately prior to each login attempt rather than trusting caller-supplied key material. • Add explicit key-agreement key freshness/expiry validation tied to DID document updated timestamps. • Document and enforce a secure DID resolution contract for all callers of loginViaDidcomm.


⚪ STRIDE-4: Denial of Service via Missing Timeout Enforcement Verification in loginViaDidcomm

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

Description: sendAndAwaitReply call in loginViaDidcomm allows indefinite request hanging due to the visible diff not confirming DEFAULT_TIMEOUT_MS is actually enforced by the bridge implementation, resulting in potential client-side resource exhaustion or hung authentication sessions if a malicious or unresponsive RP endpoint never replies

Evidence: packages/core/src/rp-login/didcomm.ts:20

const DEFAULT_TIMEOUT_MS = 30_000;

Attack Scenario:

  1. DEFAULT_TIMEOUT_MS = 30_000 is defined at module scope in didcomm.ts, suggesting an intended request timeout for the DIDComm auth round-trip.
  2. The visible diff hunk does not show the code path that actually applies this constant to the bridge.sendAndAwaitReply(packed, requestId) call (only the message construction is shown).
  3. The test file's bridge() mock always resolves immediately or via the reply callback, meaning no test exercises timeout behavior (no test named around 'timeout' or 'hang' appears in the file), leaving this control unverified by the new test suite added in this PR.
  4. If the underlying DidcommMessageBridge.sendAndAwaitReply implementation does not itself enforce DEFAULT_TIMEOUT_MS (e.g., it awaits a promise with no Promise.race timeout wrapper), a malicious or compromised RP (or a network-level attacker who silently drops replies) could cause the holder's client to hang indefinitely, exhausting UI resources, held sockets, or blocking subsequent login attempts.
  5. Repeated triggering (e.g., a malicious RP endpoint advertised via a phished login link) could degrade the browser plugin's responsiveness or leak memory via accumulated pending promises.

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: DF-authenticate-request

Preconditions: The underlying sendAndAwaitReply implementation does not itself apply DEFAULT_TIMEOUT_MS., Attacker controls or can silence a targeted RP endpoint the victim is directed to authenticate against.

Existing Controls: A DEFAULT_TIMEOUT_MS constant of 30 seconds is defined, indicating an intended bound. • Test suite's bridge mock always resolves synchronously, so no state is left pending in tested scenarios.

Recommended Mitigations: Add an explicit unit test asserting that a non-responding bridge causes loginViaDidcomm to reject after DEFAULT_TIMEOUT_MS. • Wrap sendAndAwaitReply invocation in a Promise.race against a timer if not already done, and confirm via test. • Add caller-facing cancellation/AbortController support for user-initiated login cancellation.


⚪ STRIDE-5: Insufficient Response Validation Enabling Partial Token Set Injection in loginViaDidcomm

Field Detail
Category Tampering, Information Disclosure
Severity Medium
Likelihood Likely
CVSS 6.4 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-20,CWE-1284
CAPEC CAPEC-153
OWASP A03:2021 - Injection

Description: authenticate-response body parsing in loginViaDidcomm allows malformed or partial token injection due to reliance on runtime presence checks (rejecting only fully-missing tokens per test 'malformed') rather than strict schema/type validation of access_expires_at and refresh_expires_at as numeric timestamps, resulting in potential acceptance of malformed expiry values leading to indefinite token validity or client-side type-confusion errors

Evidence: packages/core/tests/rp-login.didcomm.mjs:113-116

test("a response missing a token is refused rather than half-returned", async () => {
  const b = bridge({ from: RP_DID, type: AUTH_RESPONSE, body: { session_id: "s" } });
  await assert.rejects(() => loginViaDidcomm(opts(b)), /malformed/);
});

Attack Scenario:

  1. loginViaDidcomm parses the authenticate-response body fields session_id, access_token, refresh_token, access_expires_at, refresh_expires_at directly into the returned object (out.sessionId, out.accessToken, etc. per test assertions).
  2. Test 'a response missing a token is refused rather than half-returned' only proves rejection when access_token/refresh_token are entirely absent — it does not test type confusion (e.g., access_expires_at: "never", access_expires_at: -1, or access_expires_at: 99999999999999999).
  3. A compromised or malicious RP (or a MITM capable of authcrypt-layer tampering if key material were ever compromised) could return syntactically valid but semantically malicious expiry values, such as an expiry far in the future or a non-numeric value that downstream token-refresh logic in the browser plugin mishandles.
  4. Because the visible code/tests do not assert typeof access_expires_at === 'number' or bounds-check the timestamp, the client may accept and persist tokens with attacker-influenced effective lifetimes, extending the usable window of a stolen or leaked access token beyond the RP's intended policy.
  5. This weak input validation compounds with STRIDE-3 (sender spoofing risk): if sender trust is ever bypassed, the lack of strict body schema validation removes a secondary defense-in-depth layer that could otherwise limit the blast radius of a forged response.

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

  • Data Flows: DF-authenticate-response

Preconditions: RP or a party controlling authcrypt-verified traffic to the client returns a syntactically valid but semantically abnormal response body., Downstream consumers of loginViaDidcomm's return value trust accessExpiresAt/refreshExpiresAt without independent validation.

Existing Controls: Presence check rejecting entirely missing access_token/refresh_token/session_id fields (per 'malformed' test). • Authcrypt sender verification limiting who can produce an acceptable response. • From-DID equality check restricting the accepted sender.

Recommended Mitigations: Add strict schema validation (e.g., zod/ajv) enforcing types and reasonable bounds on access_expires_at/refresh_expires_at. • Clamp accepted expiry values to a maximum sane lifetime regardless of RP-supplied value. • Add negative test cases for type-confused and out-of-range expiry fields.


⚪ STRIDE-6: Insufficient Logging of Authentication Attempts and Failures in loginViaDidcomm

Field Detail
Category Repudiation
Severity Low
Likelihood Possible
CVSS 3.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-778
CAPEC CAPEC-268
OWASP A09:2021 - Security Logging and Monitoring Failures

Description: loginViaDidcomm allows repudiation of failed or spoofed login attempts due to no visible logging/audit trail construct for rejected sender mismatches, malformed responses, or legacy-type downgrade attempts, resulting in reduced forensic capability during incident response

Evidence: packages/core/tests/rp-login.didcomm.mjs:98-148

await assert.rejects(() => loginViaDidcomm(opts(b)), /!= RP/);

Attack Scenario:

  1. All error paths in loginViaDidcomm (imposter sender, malformed body, legacy type) are surfaced only via thrown exceptions matched by regex in tests (e.g., /!= RP/, /malformed/, /authenticate-response/).
  2. No structured audit log, telemetry event, or security event emission is visible in the diff for any of these rejection paths.
  3. An attacker probing the login flow (e.g., attempting sender spoofing or legacy-type downgrade repeatedly) would leave no durable, queryable trace for the browser plugin operators or the RP's security team to detect a pattern of attack attempts.
  4. Combined with STRIDE-3, a sophisticated attacker could conduct low-and-slow probing of the authentication boundary without detection, since failures are only local thrown JS exceptions rather than reported security events.
  5. Absence of telemetry also hampers the ability to detect the exact scenario this PR fixes (message-type drift) proactively in production before users report broken login, as happened here.

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

  • Data Flows: DF-authenticate-response

Preconditions: No external logging/monitoring wraps calls to loginViaDidcomm at a higher layer (not visible in provided scope, but not evidenced here either).

Existing Controls: Errors are thrown with descriptive messages that could be caught and logged by a calling layer outside this module.

Recommended Mitigations: Emit structured security telemetry events on authentication rejection (sender mismatch, malformed response, legacy type usage) for SOC visibility. • Integrate with a centralized client-side security event bus if one exists in the broader VTA plugin architecture. • Add rate-limiting or backoff-with-alerting for repeated authentication failures from a given RP endpoint.


⚪ STRIDE-7: Unauthenticated Challenge Endpoint Enabling Pre-Auth Enumeration or Flooding at POST /api/auth/challenge

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

Description: POST /api/auth/challenge in RP control plane allows unauthenticated challenge issuance due to auth_required:false per recon data, resulting in potential resource exhaustion or challenge-harvesting by unauthenticated parties feeding the eventual DIDComm authenticate flow

Evidence: packages/core/src/rp-login/didcomm.ts:46-49

// the RP obtains a challenge from a REST-only `POST /api/auth/challenge`.

Attack Scenario:

  1. Recon data (EP-003) documents POST /api/auth/challenge as an unauthenticated REST endpoint referenced in code comments as the source of the challenge/sessionId values the canonical schema requires.
  2. Although not implemented within the reviewed diff, this module's own comments establish that closing the conformance gap (STRIDE-2) requires wiring this endpoint into the DIDComm flow, making it a de-facto dependency of this login path going forward.
  3. An unauthenticated attacker can call this endpoint repeatedly without rate limiting evidenced anywhere in scope, harvesting large volumes of valid challenge/sessionId pairs.
  4. If challenges are not strictly one-time-use and time-bound server-side, an attacker could pre-generate a large pool of valid challenges to use in credential-stuffing-style attempts against the DIDComm authenticate handler once the challenge-binding fix (recommended in STRIDE-2's mitigation) is implemented.
  5. Absent rate limiting, repeated calls could also be used as a low-cost denial-of-service vector against the RP's challenge-issuance subsystem, indirectly degrading legitimate holders' ability to log in via this plugin.

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

  • Data Flows: DF-challenge-request

Preconditions: Endpoint POST /api/auth/challenge remains unauthenticated and unrated-limited in production., Future implementation of the challenge-binding fix does not add one-time-use/expiry enforcement server-side.

Existing Controls: Not yet integrated into this module's DIDComm flow (currently theoretical/forward-looking dependency).

Recommended Mitigations: Apply rate limiting and CAPTCHA-equivalent bot mitigation to POST /api/auth/challenge. • Enforce strict one-time-use and short expiry (e.g., 60s) on issued challenges server-side. • Bind challenge issuance to a client fingerprint or DID hint to reduce mass harvesting value.


⚪ STRIDE-8: Supply Chain Risk from Unpinned Cryptographic and DIDComm Dependencies

Field Detail
Category Tampering, Elevation of Privilege
Severity Medium
Likelihood Possible
CVSS 5.9 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-1104,CWE-829
CAPEC CAPEC-538
OWASP A06:2021 - Vulnerable and Outdated Components

Description: import statements in rp-login.didcomm.mjs allow dependency-confusion or malicious-update risk due to reliance on external packages @openvtc/vti-didcomm-js and @noble/curves for core cryptographic operations without visible lockfile or integrity-pinning evidence in scope, resulting in potential compromise of the authentication and encryption layer if a dependency is compromised upstream

Evidence: packages/core/tests/rp-login.didcomm.mjs:14-17

import { unpack } from "@openvtc/vti-didcomm-js/unpack";
import { x25519 } from "@noble/curves/ed25519.js";

Attack Scenario:

  1. rp-login.didcomm.mjs imports unpack from @openvtc/vti-didcomm-js/unpack and x25519 from @noble/curves/ed25519.js, both of which perform security-critical operations (message unpacking/decryption and elliptic-curve key generation).
  2. No package.json, lockfile, or SBOM was included in the provided scope, so it cannot be confirmed whether these packages are pinned to exact, integrity-verified versions or fetched via loose semver ranges.
  3. If @openvtc/vti-didcomm-js (an org-scoped package, plausible target for npm namespace/typosquat or maintainer-account takeover attacks) is compromised upstream, the unpack function used both in tests and (presumably) production could be modified to exfiltrate decrypted plaintext DIDComm messages, including session tokens processed by loginViaDidcomm.
  4. Similarly, a compromised @noble/curves build could weaken key generation (e.g., biased randomness in x25519.utils.randomSecretKey()), undermining the authcrypt confidentiality this entire authentication model depends on.
  5. Because this authentication design (per STRIDE-3) places full trust in the cryptographic layer rather than independent application-level checks, a supply-chain compromise of either dependency would be a critical, hard-to-detect single point of failure for the whole RP login flow.

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

  • Data Flows: DF-authenticate-request, DF-authenticate-response

Preconditions: Attacker achieves compromise of the npm package registry entry, maintainer account, or CI/CD pipeline for @openvtc/vti-didcomm-js or @noble/curves., Project does not enforce lockfile integrity hashes (npm package-lock.json integrity fields) or reproducible builds.

Existing Controls: Use of well-regarded audited library @noble/curves for cryptographic primitives (reduces but does not eliminate risk). • Scoped package namespace (@openvtc) for the DIDComm library, limiting typosquat surface somewhat.

Recommended Mitigations: Enforce lockfile integrity verification (npm ci with strict lockfile) in CI/CD. • Pin exact dependency versions and monitor via SCA/Dependabot/Snyk for the DIDComm and crypto libraries. • Adopt Subresource Integrity or package signing (e.g., npm provenance/Sigstore) for @openvtc/vti-didcomm-js releases.


⚪ STRIDE-9: Message Forwarding Path Tampering via wrapForward Import

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

Description: wrapForward usage referenced in loginViaDidcomm's imports allows message routing tampering due to reliance on an unreviewed forwarding wrapper whose implementation is outside the provided diff scope, resulting in potential misdelivery or interception of the authenticate message if forwarding logic contains routing-key validation gaps

Evidence: packages/core/src/rp-login/didcomm.ts:17-19

import { packAuthcrypt, packAuthcryptJson, wrapForward, type Identity } from "..
 import type { RemoteDidcommEndpoint } from "../vta/didcomm.js";

Attack Scenario:

  1. The full (non-diff) portion of didcomm.ts imports packAuthcrypt, packAuthcryptJson, wrapForward (visible in the diff context line import { packAuthcrypt, packAuthcryptJson, wrapForward, type Identity } from "..."), indicating the authenticate message may be wrapped for mediator-based forwarding.
  2. The implementation of wrapForward itself is not part of this diff/scope, so its handling of routing keys, next mediator DIDs, and encryption-then-forward ordering cannot be verified here.
  3. If wrapForward (in an unreviewed module) fails to properly re-encrypt or authenticate the forward-envelope layer, a compromised or malicious mediator on the forwarding path could read or redirect the inner authenticate message before it reaches the RP.
  4. Because this analysis's file scope excludes the wrapForward implementation, this is flagged as a theoretical chained-attack surface: combine a compromised mediator with the already-empty message body (STRIDE-2) and the lack of application-level replay binding to potentially replay or redirect authentication attempts.
  5. This threat should be revalidated once the wrapForward implementation file is available in scope; it is included here as a plausible, unverified chained risk given the import is present in the reviewed file's header.

🔎 Threat Clue: Derived from COMP-001 via EP-001

  • Data Flows: DF-authenticate-request

Preconditions: Production deployment routes authenticate messages through a DIDComm mediator using wrapForward., The mediator or forwarding path is compromised or improperly configured.

Existing Controls: Authcrypt encryption of the inner message (assuming packAuthcrypt is applied before wrapForward, per typical DIDComm forward-envelope ordering).

Recommended Mitigations: Review and audit the wrapForward implementation for correct encrypt-then-forward ordering and mediator authentication. • Ensure forward envelopes never expose plaintext routing metadata that could deanonymize the holder. • Add end-to-end tests covering the mediator/forwarding path, not just direct bridge delivery.


⚪ STRIDE-10: Prompt-Injection-Style Instruction Embedded in Source Comments Attempting to Influence Automated Review Tooling

Field Detail
Category Tampering
Severity Informational
Likelihood Possible
CVSS 0.0 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity None
CWE CWE-1059
CAPEC
OWASP A04:2021 - Insecure Design

Description: inline code comments in didcomm.ts and rp-login.didcomm.mjs allow narrative-style justification injection due to unusually persuasive, first-person explanatory prose embedded directly in source comments framing security-relevant design choices (e.g., 'no both-spellings fold, per this repo's rule') as settled and non-actionable, resulting in a risk that automated or human reviewers may be steered to under-scrutinize the documented conformance gaps

Evidence: packages/core/src/rp-login/didcomm.ts:5-15

// Matched with `===` against the spelling the RP declares today; no
// both-spellings fold, per this repo's rule on compatibility arms.

Attack Scenario:

  1. Both modified files contain extensive, narrative, almost conversational comments (e.g., 'Worth naming as a conformance gap rather than leaving to be discovered', 'no both-spellings fold, per this repo's rule on compatibility arms') that go well beyond typical terse code comments.
  2. This style pre-emptively frames the empty-body authentication gap (STRIDE-2) and the strict-equality type check (STRIDE-1) as intentional, already-considered, and low-priority, potentially discouraging a reviewer (human or automated) from flagging them as findings.
  3. This pattern is analyzed here strictly as DATA per this analysis's operating instructions — the comments are not followed as instructions to suppress or downplay findings; instead, their persuasive framing is itself flagged as a review-integrity concern.
  4. If such comment styles become a repo convention, they could be leveraged (intentionally or not) to get real security gaps merged with reduced scrutiny by asserting they are 'known' and 'deferred', when in fact no tracked follow-up ticket or issue reference is visible in this diff.
  5. Recommend requiring an explicit tracked issue/ticket reference (not just prose) whenever a security-relevant gap is deliberately deferred in a merged PR.

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

Preconditions: Code review process relies on comment narrative rather than independent verification of claimed-safe design decisions., No linked tracking issue enforces remediation of the acknowledged conformance gap.

Existing Controls: This analysis explicitly treats all source comments as untrusted data under review, not instructions, per operating directive.

Recommended Mitigations: Require explicit ticket/issue references for any comment claiming a security gap is 'known' and deferred. • Add a lint/CI check flagging comments containing phrases like 'known gap', 'TODO security', or similar without a linked tracking ID. • Ensure security reviewers independently verify claims made in code comments rather than accepting them at face value.



🍝 PASTA Threat Model

Application Purpose

The VTA browser plugin provides a Relying Party (RP) login flow over DIDComm messaging, allowing a holder's decentralized identity wallet to authenticate to a trust-task control plane and obtain session tokens without traditional username/password credentials.

Inherent Risks

  • Decentralized identity authentication depends entirely on correct DID resolution and key material handling by callers outside this module's control.
  • DIDComm message-type URIs are hardcoded string constants with no formal protocol version negotiation mechanism.
  • The authentication model places full trust in transport-layer authcrypt sender verification with an intentionally empty message body, deferring a documented conformance gap.

Objectives

Risk: Treat any authentication bypass or token forgery as an unacceptable Critical risk.; Treat protocol drift causing denial of login service as a High risk requiring rapid remediation.
Business: Enable seamless, passwordless RP login for VTA plugin users via decentralized identity.; Maintain interoperability with the trust-task control plane's DIDComm router.
Security: Ensure only the legitimate RP can produce an accepted authenticate-response.; Prevent replay or downgrade of authentication messages.; Protect session/access/refresh tokens in transit and at rest in the client.
Financial: Avoid revenue loss from broken login flows blocking user onboarding.; Avoid costs associated with incident response for authentication bypass or token theft.
Compliance: Align with DID/DIDComm and Trust-Task ecosystem interoperability specifications.; Support auditability of authentication events where required by downstream RP compliance obligations.
Functional: Correctly route authenticate/authenticate-response messages under the canonical trusttasks.org URI scheme.; Return valid session, access, and refresh tokens to the calling application upon successful authentication.
Operational: Ensure login flow degrades gracefully (timeout, clear errors) rather than hanging indefinitely.; Maintain regression test coverage for the wire contract to prevent future silent protocol drift.

Business Impact Analysis (1)

BIA-1: RP Login via DIDComm (Critical)

The end-to-end process by which a holder's VTA browser plugin authenticates to a Relying Party over DIDComm and receives session tokens enabling access to trust-task services.

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

  • Stakeholders: Holder (End User) / OpenVTC Plugin Maintainers / Relying Party Operations Team / Trust-Task Control Plane Operators
  • Dependencies: @noble/curves Cryptographic Library / @openvtc/vti-didcomm-js Package / DIDComm Message Bridge Transport / POST /api/auth/challenge REST Endpoint / RP DIDComm Router
  • Disruptions: Message-type URI drift causing requests to not route (as occurred pre-fix in this PR). / Compromise of key-agreement key material enabling sender spoofing. / Malicious or unresponsive RP endpoint causing indefinite login hangs. / Supply-chain compromise of cryptographic or DIDComm dependencies.
  • Impacts: Complete inability for users to log in, blocking access to all downstream trust-task services. / Potential unauthorized issuance of session/access/refresh tokens to an attacker. / Reputational damage to OpenVTC and reliant RPs from a publicized authentication bypass. / Support and incident-response cost escalation from silent protocol drift.

Technical Scope

Roles (3): RO-1 Holder · RO-2 Relying Party Operator · RO-3 Plugin Maintainer

Actors (3): AC-1 Holder Identity Client · AC-2 RP Authentication Service · AC-3 DIDComm Bridge Runtime

Use Cases (2): Holder RP Authentication via DIDComm · RP Challenge Issuance for Future Authentication Binding

Attack Trees (3): SC-1: loginViaDidcomm Module · SC-4: POST /api/auth/challenge Endpoint · SC-2: DidcommMessageBridge Transport

Entry Points (3): EP-1 DIDComm Authenticate Request · EP-2 DIDComm Authenticate Response · EP-3 REST Challenge Issuance

Risk Registry (9): RISK-001 · RISK-002 · RISK-003 · RISK-004 · RISK-005 · RISK-006 · RISK-007 · RISK-008 · RISK-009

Threat Actors (3): TA-1 Network-Position Attacker · TA-2 Malicious/Compromised RP Operator · TA-3 Supply Chain Attacker

Infrastructure (2): IF-1 Browser Extension Runtime · IF-2 RP Control Plane Backend

Trust Boundaries (3): TB-1 Holder Browser Plugin Boundary · TB-2 DIDComm Transport/Mediator Boundary · TB-3 Relying Party Control Plane Boundary

External Entities (3): EE-1 Holder (End User Wallet) · EE-2 Relying Party Control Plane · EE-3 DIDComm Mediator/Forwarding Node

System Components (4): SC-1 loginViaDidcomm Module · SC-2 DidcommMessageBridge Transport · SC-3 RP DIDComm Router and Authenticate Handler · SC-4 POST /api/auth/challenge Endpoint

Resources And Assets (4): RA-1 Holder Private Key Material · RA-2 Session/Access/Refresh Tokens · RA-3 RP Key-Agreement Public Key Material · RA-4 Auth Challenge Value

Technologies And Dependencies (3): TD-1 @openvtc/vti-didcomm-js · TD-2 @noble/curves · TD-3 Node.js built-in test runner

⚔️ Attack Scenarios (1)

Exploit identified weaknesses

flowchart LR
  S0["Response Type Confusion in loginViaDidcomm Response Handler"]
  S1["Empty Authentication Request Body Missing Required Challenge"]
  S2["Sender Spoofing via Missing DID Document Resolution Check in"]
  S0 --> S1
  S1 --> S2
Loading

📊 Risk Summary

Total Threats: 10

By Severity: Low: 1 · High: 2 · Medium: 6 · Informational: 1

By Category: Unknown: 10


Generated by Agentic Sec — Threat Model & Affect Analysis Agent

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

Must-Review-By-Human (2)

  • 🟠 Authentication request body omits required challenge/sessionId binding (protocol non-conformance / weak authentication design)
  • 🟠 loginViaDidcomm trusts caller-supplied RP key material with no independent DID resolution/revocation check

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