Skip to content

feat(relationships)!: issue the VRC under the relationship DID, not the persona - #255

Merged
stormer78 merged 1 commit into
mainfrom
feat/pairwise-vrc-issuer
Aug 23, 2026
Merged

feat(relationships)!: issue the VRC under the relationship DID, not the persona#255
stormer78 merged 1 commit into
mainfrom
feat/pairwise-vrc-issuer

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

Closes the last open half of #241 — the one the issue's own triage identified as
blocked upstream.

What was still persona-attributed

Relationship communication has defaulted to a pairwise R-DID since #254. The
credential it produces did not. prepare_accept_vrc_request minted the VRC with
issuer = persona_did, signed with the persona key. The subject was already the
peer's R-DID — so only the issuer half leaked, and that was enough: the pairwise
channel led straight back to the persona.

It is also the worst place to leak it, for the reason #241 gave:

The handshake persona DID is observed once; a published VRC correlates
indefinitely.

The handshake DIDs are seen once, by the mediator, at establishment. The VRC is
the durable artifact both parties retain and may publish to a community trust
graph.

Why it could not be fixed before

The VTC pinned a published VRC's issuer to the authenticated session DID, so a
VRC issued under a relationship DID was rejected outright — the only
enforcement that existed forbade pairwise identifiers rather than requiring
them
. Lifted in OpenVTC/verifiable-trust-infrastructure#1061, which separates
membership (proven by the session) from the identifier the credential carries.

Both ends, because they must move together

  • Issuance signs as whichever identity the relationship actually uses, keyed
    on is_persona_did — the same discriminator listener_id_for_did routes
    sends on. A relationship established without a dedicated R-DID still signs
    with the persona key, unchanged.
  • vet_vrc_issued gate 2 now requires the issuer to be the sender's DID in
    this relationship rather than their persona DID.

Worth noting the gate change is strictly tighter, not merely more private.
Gate 1 already matched from_did — the authenticated DIDComm sender — against
the same field, so the credential is now pinned to the channel it arrived on
rather than to a persona that could have issued it anywhere.

Three tests: the pairwise form passes, a persona-issued VRC on an R-DID
relationship is refused with a specific reason, and a relationship with no R-DID
still accepts its persona-issued VRC (remote_did == remote_p_did there).

Incidental cleanup

The #key-1 verification-method convention that generate_did_peer_from_secrets
imposes was already load-bearing in two places and spelled out longhand in both.
Now named (relationship_signing_vm_id), with the secret lookup beside it in
openvtc-core so the TUI crate does not reach into the secrets resolver
directly.

Docs

docs/relationships-vrcs.md carried a note deferring "full pairwise operation,
including the VRC issuer field" to upstream work. Replaced with what actually
happens, plus why the issuer field mattered more than the handshake routing
does. The example credential showed a did:webvh issuer; it now shows the
did:peer one it would really carry.

Breaking

A VRC from a peer on an older build carries their persona DID and is now
refused; one from this build is refused by older peers. Nothing is published, so
no compatibility window — flagged rather than papered over.

What #241 has left after this

Only the handshake routing, which is intrinsic: the mediator must route
request/accept/finalise before a pairwise channel exists. I'd close that as
accepted rather than leave it reading as outstanding work — and it is worth
restating acceptance criterion 4 against what pairwise identifiers actually
protect here, which the triage comment already argued is the trust graph and the
initiator's identity rather than mutual recognition.

Workspace green: 16 suites, clippy clean.

…he persona

The last half of #241, and the half that mattered most.

Relationship communication has defaulted to a pairwise R-DID since #254, but
the credential it produces did not. `prepare_accept_vrc_request` minted the VRC
with `issuer = persona_did`, signed with the persona key. The subject was
already the peer's R-DID, so only the issuer half was persona-attributed — and
that was enough: the pairwise channel led straight back to the persona.

It is the worst place to leak it. The handshake DIDs are observed once, by the
mediator, at establishment. The VRC is the durable artifact both parties retain
and may publish to a community trust graph, where it correlates every
relationship a persona holds for as long as anyone keeps a copy. #241 said the
same: "The handshake persona DID is observed once; a published VRC correlates
indefinitely."

This could not be fixed here before now. The VTC pinned a published VRC's
issuer to the authenticated session DID, so a VRC issued under a relationship
DID was rejected outright — the only enforcement that existed forbade pairwise
identifiers rather than requiring them. Lifted in
OpenVTC/verifiable-trust-infrastructure#1061, which separates membership (proven
by the session) from the identifier the credential carries.

Both ends move together, so this is breaking on the wire:

- **Issuance** signs as whichever identity the relationship uses, keyed on
  `is_persona_did` — the same discriminator `listener_id_for_did` routes sends
  on. A relationship established without a dedicated R-DID still signs with the
  persona key, unchanged.
- **`vet_vrc_issued` gate 2** now requires the issuer to be the sender's DID in
  *this* relationship rather than their persona DID. That is strictly tighter
  as well as more private: gate 1 already matched the authenticated DIDComm
  sender against the same field, so the credential is pinned to the channel it
  arrived on instead of to a persona that could have issued it anywhere.
  Relationships with no R-DID are unaffected — `remote_did` equals
  `remote_p_did` there, and a test pins that.

The `#key-1` verification-method convention that `generate_did_peer_from_secrets`
imposes was already load-bearing in two places and spelled out longhand in
both. It is now named — `relationship_signing_vm_id` — with the secret lookup
beside it in `openvtc-core`, so the TUI crate does not reach into the secrets
resolver directly.

Docs: the note deferring "full pairwise operation, including the VRC issuer
field" to upstream work is replaced by what actually happens, including why the
issuer field mattered more than the handshake routing does. The example
credential showed a `did:webvh` issuer; it now shows the `did:peer` one it
would really carry.

What #241 leaves open after this is the handshake routing itself, which is
intrinsic — the mediator must route request/accept/finalise before a pairwise
channel exists — and worth closing as accepted rather than outstanding.

BREAKING CHANGE: a VRC issued by a peer running an older build carries their
persona DID and is now refused, and one issued by this build is refused by
older peers. Nothing is published yet, so no compatibility window is provided.

Refs #241, OpenVTC/verifiable-trust-infrastructure#1054

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 requested a review from a team as a code owner August 23, 2026 10:28
@stormer78
stormer78 merged commit 56f5234 into main Aug 23, 2026
14 checks passed
@stormer78
stormer78 deleted the feat/pairwise-vrc-issuer branch August 23, 2026 10:40
@affinidi-appsecurity-bot

Copy link
Copy Markdown

🛡️ AI Agentic Security Code Review

2 AI-confirmed issues, 2 findings need a human to review/validate.

Mandatory to check: 🔒 Security Code Review Report

Details

🛡️ Security Code Review Report — PR #255

Field Value
Repository OpenVTC/openvtc
Branch feat/pairwise-vrc-issuermain
Validated 2026-08-24
Scan ID d12f07bc
Validator AI Security Validation Agent

🗺️ Scan Coverage

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

Module Files scanned Findings
openvtc-core 2 2
docs 1 0
openvtc 1 2

Executive Summary

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

Confirmed Vulnerabilities (2)

🟡 Selective secrets-store eviction causes deterministic denial of VRC issuance per relationship (CWE-404)

Field Detail
Severity MEDIUM
Location openvtc/src/state_handler/inbox_actions.rs:754
Finding ID github_pr-49ae55e4f80b
CWE CWE-404, CWE-703
OWASP A04:2021 - Insecure Design
MITRE ATT&CK T1499 - Endpoint Denial of Service (conceptual analogue)
CAPEC CAPEC-125
DREAD 4.6
Reachability 🔴 Reachable
Exploit Maturity poc
Detection Source skill_scan

Summary: The relationship-DID signing path aborts VRC issuance with a hard error whenever the SecretsResolver lacks the expected '#key-1' secret for that relationship, with no automatic recovery, making the flow susceptible to selective denial by anyone who can remove/corrupt that specific secret entry (e.g., local access or storage eviction).

📝 Description:

Selective removal/corruption of a relationship's signing secret deterministically blocks that relationship's ability to issue VRCs until manual re-establishment, disrupting the trust-establishment feature for targeted users/relationships.

🧪 Proof of Concept:

The ? after ok_or_else immediately propagates a hard error up the call stack with no retry, backoff, or automatic re-establishment attempt, meaning any condition that clears the secrets-store entry deterministically blocks this code path.

} else {
    let secret = openvtc_core::relationships::relationship_signing_secret(tdk, &our_r_did)
        .await
        .ok_or_else(|| {
            anyhow::anyhow!(
                "no signing secret held for relationship DID {our_r_did} \
                     — the relationship needs re-establishing"
            )
        })?;
    vrc.sign(&secret, None).await?;
}

Vulnerable lines: 748, 762

🔎 Evidence: openvtc/src/state_handler/inbox_actions.rs:754

let secret = openvtc_core::relationships::relationship_signing_secret(tdk, &our_r_did)
    .await
    .ok_or_else(|| anyhow::anyhow!("no signing secret held for relationship DID {our_r_did} — the relationship needs re-establishing"))?;

💥 Impact:

Selective removal/corruption of a relationship's signing secret deterministically blocks that relationship's ability to issue VRCs until manual re-establishment, disrupting the trust-establishment feature for targeted users/relationships.

Confidentiality: none · Integrity: none · Availability: low

🧭 Reachability:

  • Network exposure: internal
  • Auth barrier: basic
  • Attack path: EP-002 prepare_accept_vrc_request → EP-003 relationship_signing_secret returns None → hard error abort, no retry/self-heal

⚖️ Triage Factors:

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

Attack scenario: An attacker with local device access or a storage-eviction primitive can delete/corrupt a specific relationship's signing secret to deterministically block that relationship's VRC issuance until manual re-establishment.

🔧 Remediation:

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

Rather than a bare hard failure, detect the missing-secret condition, emit a monitored security event (to catch repeated/targeted patterns indicating an eviction attack), and kick off automatic re-establishment where the protocol supports it, reducing both the operational impact and providing detection capability for abuse.

Vulnerable code:

let secret = relationship_signing_secret(tdk, &our_r_did).await.ok_or_else(|| anyhow::anyhow!("no signing secret held for relationship DID {our_r_did} — the relationship needs re-establishing"))?;

Secure code:

let secret = match relationship_signing_secret(tdk, &our_r_did).await {
    Some(s) => s,
    None => {
        // Trigger automatic, rate-limited re-establishment flow instead of a bare hard error,
        // and emit a structured security/ops event for monitoring.
        metrics::increment_counter("vrc.signing_secret_missing");
        relationships::request_reestablishment(tdk, &our_r_did).await?;
        return Err(anyhow::anyhow!("relationship {our_r_did} re-establishment triggered — VRC issuance deferred"));
    }
};

Additional recommendations:

  • Add alerting/rate-limiting on repeated relationship_signing_secret misses per relationship or per time window to detect targeted eviction attacks.
  • Consider key backup/escrow consistent with the trust model to reduce single-point key loss impact.

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 85%
  • AI Validation Evidence: EVIDENCE FOUND: In inbox_actions.rs, prepare_accept_vrc_request contains: let secret = openvtc_core::relationships::relationship_signing_secret(tdk, &our_r_did).await.ok_or_else(|| anyhow::anyhow!("no signing secret held for relationship DID {our_r_did} — the relationship needs re-establishing"))?; — this is a hard ? propagation that aborts VRC issuance entirely when the secret is missing, with no retry/fallback/repair logic in this function. EVIDENCE NOT FOUND: No automatic re-establishment or repair trigger is invoked inline; the only referenced repair mechanism (repair_key_info_ids, needs_reestablishment) is mentioned in comments but not exercised by this code path. CHANGED VS PRE-EXISTING: CHANGED — prepare_accept_vrc_request and its ok_or_else error path are new/modified as part of this PR's R-DID signing feature. VERDICT JUSTIFICATION: The code demonstrably hard-fails VRC issuance for a relationship whose signing secret is absent, exactly as described — this is a real availability limitation directly visible in the quoted and full source, confirming CWE-404/CWE-703 applicability as a design tradeoff, not a false claim.
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

🟡 Verification-method-id derivation for relationship signing key relies on unenforced naming convention (CWE-706)

Field Detail
Severity MEDIUM
Location openvtc-core/src/relationships.rs:148
Finding ID github_pr-b991e539c0a1
CWE CWE-706, CWE-863
OWASP A04:2021 - Insecure Design
MITRE ATT&CK T1552.004 - Unsecured Credentials: Private Keys (indirect)
CAPEC CAPEC-122
DREAD 4
Reachability 🔴 Reachable
Exploit Maturity theoretical
Detection Source skill_scan

Summary: relationship_signing_vm_id/relationship_encryption_vm_id hardcode the '#key-1'/'#key-2' verification-method suffixes without validating them against the actual DID document, making secret retrieval dependent entirely on an externally-documented (not code-enforced) minting convention.

📝 Description:

Could cause signing to silently fail (denial of VRC issuance) or, in a worst-case resolver design, use key material intended for a different purpose/DID, undermining the per-relationship key isolation the entire feature depends on.

🧪 Proof of Concept:

The function performs no validation that '{r_did}#key-1' is actually the signing verification method declared in the DID document; it is purely a string convention documented in comments, making it fragile to any deviation in DID minting.

pub fn relationship_signing_vm_id(r_did: &str) -> String {
    format!("{r_did}#key-1")
}
pub fn relationship_encryption_vm_id(r_did: &str) -> String {
    format!("{r_did}#key-2")
}
...
pub async fn relationship_signing_secret(
    tdk: &affinidi_tdk::TDK,
    r_did: &str,
) -> Option<affinidi_tdk::secrets_resolver::secrets::Secret> {
    use affinidi_tdk::secrets_resolver::SecretsResolver;
    tdk.get_shared_state()
        .secrets_resolver()
        .get_secret(&relationship_signing_vm_id(r_did))
        .await
}

Vulnerable lines: 148, 176

🔎 Evidence: openvtc-core/src/relationships.rs:148

pub fn relationship_signing_vm_id(r_did: &str) -> String {
    format!("{r_did}#key-1")
}
pub fn relationship_encryption_vm_id(r_did: &str) -> String {
    format!("{r_did}#key-2")
}

💥 Impact:

Could cause signing to silently fail (denial of VRC issuance) or, in a worst-case resolver design, use key material intended for a different purpose/DID, undermining the per-relationship key isolation the entire feature depends on.

Confidentiality: low · Integrity: low · Availability: low

🧭 Reachability:

  • Network exposure: internal
  • Auth barrier: basic
  • Attack path: EP-003 relationship_signing_secret → relationship_signing_vm_id(r_did) string construction → SecretsResolver.get_secret() lookup with no DID-document cross-validation

⚖️ Triage Factors:

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

Attack scenario: A relationship DID created via a non-standard path (import, future protocol version, migration bug) could cause signing operations to silently target the wrong or a nonexistent verification method, weakening key isolation guarantees.

🔧 Remediation:

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

Resolve the signing verification-method id from the DID document's authentication section rather than hardcoding a suffix. This removes the implicit dependency on a convention enforced only by comments and by DID::generate_did_peer_from_secrets, making the code robust to any DID that doesn't follow that exact minting path.

Vulnerable code:

pub fn relationship_signing_vm_id(r_did: &str) -> String {
    format!("{r_did}#key-1")
}

Secure code:

pub async fn relationship_signing_vm_id(
    resolver: &impl DidResolver,
    r_did: &str,
) -> Result<String, DidError> {
    let doc = resolver.resolve(r_did).await?;
    doc.authentication
        .first()
        .cloned()
        .ok_or(DidError::NoAuthenticationMethod)
}

Additional recommendations:

  • Add a runtime assertion comparing the resolved secret's public key against the DID document's declared key before use in signing.
  • Consolidate the #key-1/#key-2 convention into a single well-tested shared helper used by all four dependent call sites, with a unit test enforcing the invariant across generate_profiles and repair_key_info_ids.

🔍 Validation Log

  • Verdict: ✅ Confirmed True Positive
  • Confidence: 75%
  • AI Validation Evidence: EVIDENCE FOUND: relationships.rs is not in source_files, but the finding's own quoted code shows pub fn relationship_signing_vm_id(r_did: &str) -> String { format!("{r_did}#key-1") } and relationship_encryption_vm_id similarly hardcodes #key-2 — this is a plain string-formatting convention with no lookup against an actual DID document's declared verification methods, matching the description. inbox_actions.rs confirms consumption of this convention: relationship_signing_secret(tdk, &our_r_did) is used directly as the signing secret lookup key without any DID-document cross-check visible in the provided code. EVIDENCE NOT FOUND: No DID-document verification-method validation code was found anywhere in the provided files (relationships.rs full source not included, but no cross-check call appears in the consuming code in inbox_actions.rs either). CHANGED VS PRE-EXISTING: CHANGED — relationship_signing_vm_id/relationship_encryption_vm_id are new functions introduced as part of this PR's R-DID signing-key scheme (confirmed by their direct use in the new prepare_accept_vrc_request logic in inbox_actions.rs). VERDICT JUSTIFICATION: The hardcoded suffix convention with no DID-document cross-validation is a genuine design weakness (CWE-706/CWE-863) confirmed by the visible calling code; the risk is real though the severity/exploitability depends on whether other code paths could violate the convention, which is not verifiable, but the core weakness itself is directly observable.
  • Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.

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

🟠 Non-atomic key-branch selection may sign VRC with mismatched issuer identity (TOCTOU / CWE-362)

Field Detail
Severity HIGH
Location openvtc/src/state_handler/inbox_actions.rs:731
Finding ID github_pr-ba9842655f54
CWE CWE-362, CWE-347
OWASP A08:2021 - Software and Data Integrity Failures
MITRE ATT&CK T1552 - Unsecured Credentials (indirect, via key confusion)
CAPEC CAPEC-115
DREAD 4.2
Reachability 🔴 Reachable
Exploit Maturity conceptual
Detection Source skill_scan

Summary: A race between relationship-state mutation (re-establishment/key repair) and the persona-vs-relationship key branch selection in prepare_accept_vrc_request could result in a VRC being signed with one identity's key while declaring a different identity as issuer, undermining the newly added pairwise-privacy protection.

📝 Description:

If exploited, a published VRC could carry a persona-attributed cryptographic signature while claiming a relationship-DID issuer (or vice versa), allowing observers of the community trust graph to correlate a victim's relationships back to a single persona — precisely the privacy leak this PR was designed to prevent.

🧪 Proof of Concept:

our_r_did is read multiple times across an async boundary (config.is_persona_did, config.get_persona_keys/relationship_signing_secret, and later vrc.message). If underlying relationship state changes between these reads (e.g., due to concurrent repair_key_info_ids or re-establishment), the branch taken for signing may not correspond to the identity ultimately declared in the VRC.

let valid_from = Utc::now();
let mut vrc = DTGCredential::new_vrc(
    our_r_did.to_string(),
    their_r_did.to_string(),
    valid_from,
    None,
);
if config.is_persona_did(&our_r_did) {
    let persona_keys = config.get_persona_keys(tdk).await?;
    vrc.sign(&persona_keys.signing.secret, None).await?;
} else {
    let secret = openvtc_core::relationships::relationship_signing_secret(tdk, &our_r_did)
        .await
        .ok_or_else(|| anyhow::anyhow!("no signing secret held for relationship DID {our_r_did}"))?;
    vrc.sign(&secret, None).await?;
}
let msg = vrc.message(&our_r_did, &their_r_did, Some(&task_id))?;

Vulnerable lines: 731, 763

🔎 Evidence: openvtc/src/state_handler/inbox_actions.rs:731

if config.is_persona_did(&our_r_did) {
    let persona_keys = config.get_persona_keys(tdk).await?;
    vrc.sign(&persona_keys.signing.secret, None).await?;
} else {
    let secret = openvtc_core::relationships::relationship_signing_secret(tdk, &our_r_did)
        .await
        .ok_or_else(|| anyhow::anyhow!("no signing secret ..."))?;
    vrc.sign(&secret, None).await?;
}

💥 Impact:

If exploited, a published VRC could carry a persona-attributed cryptographic signature while claiming a relationship-DID issuer (or vice versa), allowing observers of the community trust graph to correlate a victim's relationships back to a single persona — precisely the privacy leak this PR was designed to prevent.

Confidentiality: low · Integrity: high · Availability: none

🧭 Reachability:

  • Network exposure: internal
  • Auth barrier: basic
  • Attack path: EP-002 (prepare_accept_vrc_request) → relationship state re-establishment/key-repair race → is_persona_did(&our_r_did) branch → vrc.sign() with mismatched key → EP-004 DIDComm send

⚖️ Triage Factors:

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

Attack scenario: An attacker who can trigger relationship re-establishment concurrently with VRC issuance may cause a signing key / declared issuer mismatch, potentially reintroducing persona correlation across relationships.

🔧 Remediation:

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

Use a single immutable local binding for the DID that determines both which key is used to sign and what is declared as issuer/subject, eliminating any window where relationship state could be re-read differently between the two steps. Add a post-signature self-verification step that resolves the issuer DID document and confirms the signing key matches, catching any residual mismatch before the message is sent.

Vulnerable code:

if config.is_persona_did(&our_r_did) {
    let persona_keys = config.get_persona_keys(tdk).await?;
    vrc.sign(&persona_keys.signing.secret, None).await?;
} else {
    let secret = relationship_signing_secret(tdk, &our_r_did).await.ok_or_else(...)?;
    vrc.sign(&secret, None).await?;
}

Secure code:

// Snapshot the DID once and derive both the branch decision and the
// message fields from that single snapshot value.
let signing_did = our_r_did.clone();
let is_persona = config.is_persona_did(&signing_did);
let secret = if is_persona {
    config.get_persona_keys(tdk).await?.signing.secret
} else {
    relationship_signing_secret(tdk, &signing_did).await.ok_or_else(|| anyhow::anyhow!("no signing secret for {signing_did}"))?
};
vrc.sign(&secret, None).await?;
// Post-sign self-check: resolve signing_did's DID document and assert
// the key used to sign resolves to its declared verification method
// before constructing/sending the message with the same signing_did.
debug_assert!(verify_proof_matches_issuer(&vrc, &signing_did).await?);
let msg = vrc.message(&signing_did, &their_r_did, Some(&task_id))?;

Additional recommendations:

  • Add an integration test that forces a relationship re-establishment concurrently with prepare_accept_vrc_request and asserts the resulting VRC's issuer always matches the key used to sign it.
  • Consider making relationship state reads transactional/snapshot-based at the point of task creation.

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 40%
  • AI Validation Evidence: EVIDENCE FOUND: prepare_accept_vrc_request in openvtc/src/state_handler/inbox_actions.rs reads relationship fields once into local vars (our_r_did, their_p_did, their_r_did) via let (our_r_did, their_p_did, their_r_did) = { let relationship = config.private.relationships.get(&remote_p_did)...; (Arc::clone(&relationship.our_did), ...) } — this is a single synchronous read inside the loop-thread context (no async gap between the read and the branch if config.is_persona_did(&our_r_did) { ... } else { ... }), which is not obviously TOCTOU-vulnerable since Config is single-writer on the loop thread per the file's architecture comments. EVIDENCE NOT FOUND: No evidence of concurrent mutation of relationship.our_did between this snapshot and the sign() call — the function is async but the snapshot+branch occurs before any .await on a different mutable borrow of config; no lock/mutex code was provided to confirm exclusivity. CHANGED VS PRE-EXISTING: CHANGED — prepare_accept_vrc_request and its signing branch are directly present in inbox_actions.rs, a file at the center of this PR's diff. VERDICT JUSTIFICATION: The claimed TOCTOU window is plausible in theory but the provided code shows a single-threaded synchronous snapshot immediately followed by the branch, and no evidence of a race condition primitive or interleaving point was found; cannot definitively confirm or dismiss without deeper concurrency model visibility, so must_review.
  • Validation Effort: This finding was validated up to a point, but the available evidence was insufficient for a conclusive automated verdict. A human (developer / security team) must manually review the code and decide. Not dismissed — treat as an open item pending human review.

🟠 Gate 2 issuer-binding tightening (messaging.rs) is a single-point control with no independent verification-method cross-check (CWE-346/CWE-290 residual)

Field Detail
Severity HIGH
Location openvtc-core/src/messaging.rs:699
Finding ID github_pr-5508d1786a80
CWE CWE-346, CWE-290, CWE-668
OWASP A07:2021 - Identification and Authentication Failures
MITRE ATT&CK T1550 - Use Alternate Authentication Material (conceptual analogue)
CAPEC CAPEC-151, CAPEC-194
Detection Source skill_scan

🔎 Evidence: openvtc-core/src/messaging.rs:699

if vrc.issuer() != relationship.remote_did.as_str() {
    return Err(format!(
        "credential issuer ({}) is not the DID the sender uses in this \
         relationship ({})",
        vrc.issuer(),
        relationship.remote_did
    ));
}

🔍 Validation Log

  • Verdict: ⚠️ Must-Review-By-Human
  • Confidence: 35%
  • AI Validation Evidence: EVIDENCE FOUND: messaging.rs is not included in source_files, so the exact Gate 2 code if vrc.issuer() != relationship.remote_did.as_str() { ... } cannot be independently verified beyond the finding's own quoted snippet. The finding's concern is about whether OTHER verification entry points besides vet_vrc_issued apply the same check. EVIDENCE NOT FOUND: No other verification entry points, no evidence of a second/legacy verifier, and no messaging.rs source in source_files to confirm or refute cross-path consistency. CHANGED VS PRE-EXISTING: CHANGED — openvtc-core/src/messaging.rs and the Gate 2 issuer check are core to this PR's stated intent (binding VRC issuer to R-DID). VERDICT JUSTIFICATION: Without the full messaging.rs file or other verifier code, it's impossible to confirm whether independent verification-method cross-checks exist elsewhere or whether this is the sole enforcement point; insufficient evidence to validate or dismiss, so must_review.
  • Validation Effort: This finding was validated up to a point, but the available evidence was insufficient for a conclusive automated verdict. A human (developer / security team) must manually review the code and decide. Not dismissed — treat as an open item pending human review.


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

Field Value
Repository OpenVTC/openvtc
Branch feat/pairwise-vrc-issuermain
Generated 2026-08-24

ℹ️ 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 changes how Verifiable Relationship Credentials (VRCs) are issued and verified: the credential issuer/subject is now bound to the pairwise relationship DID (R-DID) instead of always being the persona DID. This closes a privacy weakness where a durable, publishable credential named a persona-scoped identifier shared across every relationship that persona held, enabling cross-relationship correlation. The change updates the verification gate (Gate 2) in messaging.rs, the signing logic in inbox_actions.rs, adds relationship key-lookup helpers in relationships.rs, and updates documentation accordingly.

Diff: +137 / -15 lines
Types: security, feature, docs

🧩 Affected Components

Component Impact Change What Changed
VRC Verification Gate (vet_vrc_issued) critical modified Gate 2's issuer-binding check now compares the credential issuer against relationship.remote_did instead of the persona DID (remote_p_did).
VRC Issuance / Signing (prepare_accept_vrc_request) critical modified VRC issuer/subject construction now uses our_r_did instead of config.persona_did(); signing key selection now branches between persona keys
Relationship Key Management medium new Three new public functions added: relationship_signing_vm_id, relationship_encryption_vm_id, relationship_signing_secret.
VRC Protocol Documentation low modified Documentation updated to describe the new issuer-binding behavior and remove a stale blocked-on-external-issue note.

📁 File Classifications

openvtc-core/src/messaging.rs

  • Type: security

openvtc-core/src/relationships.rs

  • Type: security

openvtc/src/state_handler/inbox_actions.rs

  • Type: security

docs/relationships-vrcs.md

  • Type: docs

🛡️ STRIDE Threat Model

Identified Threats (11)

⚪ STRIDE-1: Persona-Issuer VRC Replay Across Relationships in vet_vrc_issued

Field Detail
Category Spoofing, Tampering, Information Disclosure
Severity High
Likelihood Possible
CVSS 8.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-290,CWE-346,CWE-668
CAPEC CAPEC-151,CAPEC-194
OWASP A07:2021 - Identification and Authentication Failures

Description: Gate 2 in vet_vrc_issued in openvtc-core/src/messaging.rs allows acceptance of a persona-issued VRC on a relationship using a dedicated R-DID if the tightened check is not applied consistently across all code paths, due to reliance on comparing relationship.remote_did rather than cryptographically distinguishing relationship-scoped from persona-scoped keys, resulting in cross-relationship correlation of a persona via a durable published credential.

Evidence: openvtc-core/src/messaging.rs:699-716

if vrc.issuer() != relationship.remote_did.as_str() {
    return Err(format!(
        "credential issuer ({}) is not the DID the sender uses in this \
         relationship ({})",
        vrc.issuer(),
        relationship.remote_did
    ));
}

Attack Scenario:

  1. Attacker (a malicious or compromised relationship counterparty) establishes a legitimate pairwise relationship with the victim persona, obtaining a dedicated R-DID (relationship.remote_did != remote_p_did).
  2. Attacker crafts or replays a VRC where vrc.issuer() is deliberately set to remote_p_did (the persona DID) instead of relationship.remote_did, exploiting any code path that still performs the old Gate 2 comparison or a downstream consumer that does not re-validate.
  3. If the network-facing VRC exchange (EP-004, DIDComm message via listener_id_for_did) or another verifier component does not apply the same tightened Gate 2 logic as messaging.rs, the persona-issued VRC is accepted and stored/published.
  4. The published VRC, which is a durable artifact per the code comments ('publish to the community trust graph'), now names the persona DID as issuer, allowing any observer of the trust graph to correlate this relationship with every other relationship where the same persona issued a VRC.
  5. Attacker or third-party observer aggregates multiple published VRCs bearing the same persona-DID issuer field to deanonymize the pairwise relationships and build a social graph of the victim persona, defeating the privacy purpose of R-DIDs.

Preconditions: A relationship established with a dedicated R-DID (remote_did != remote_p_did)., At least one code path (e.g., a legacy verifier, cached logic, or a system depending on the older behavior in verifiable-trust-infrastructure prior to #1061) that has not adopted the new Gate 2 check., Attacker or victim publishes the VRC to a shared/public trust graph.

Existing Controls: Gate 2 in vet_vrc_issued now compares vrc.issuer() against relationship.remote_did instead of remote_p_did. • Unit tests vrc_issued_under_the_senders_persona_did_is_refused and vrc_issued_under_the_persona_passes_when_that_is_the_relationship_did encode the expected new/old behavior.

Recommended Mitigations: Ensure all VRC verification entry points (not just vet_vrc_issued) enforce the same issuer-binding rule. • Add a schema/type-level invariant (e.g., a distinct VRC type per issuance context) rather than relying solely on a runtime string comparison. • Audit downstream trust-graph publication code to reject or flag VRCs whose issuer equals a known persona DID when a relationship DID exists. • Add integration tests covering the full DIDComm message round-trip (EP-004) rather than only the local vet_vrc_issued unit tests.


⚪ STRIDE-2: Signing-Key Confusion Between Persona and Relationship DID in prepare_accept_vrc_request

Field Detail
Category Tampering, Repudiation
Severity High
Likelihood Possible
CVSS 7.5 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-347,CWE-362
CAPEC CAPEC-115
OWASP A08:2021 - Software and Data Integrity Failures

Description: The branching logic in prepare_accept_vrc_request in openvtc/src/state_handler/inbox_actions.rs allows signing a VRC with the wrong key (persona vs relationship-scoped) due to reliance on the is_persona_did(&our_r_did) discriminator being perfectly synchronized with listener_id_for_did routing decisions, resulting in issuance of a VRC whose issuer field does not match the key that actually signed it, which downstream verification (Gate 2/Gate 4) may then incorrectly accept or reject.

Evidence: openvtc/src/state_handler/inbox_actions.rs:731-763

if config.is_persona_did(&our_r_did) {
    let persona_keys = config.get_persona_keys(tdk).await?;
    vrc.sign(&persona_keys.signing.secret, None).await?;
} else {
    let secret = openvtc_core::relationships::relationship_signing_secret(tdk, &our_r_did)
        .await
        .ok_or_else(|| { ... 

Attack Scenario:

  1. A relationship is established and, due to a race condition, config drift, or the relationship being re-established (see needs_reestablishment reference), our_r_did briefly reflects a stale or ambiguous state where is_persona_did(&our_r_did) evaluates inconsistently with what listener_id_for_did assumes elsewhere.
  2. prepare_accept_vrc_request in inbox_actions.rs signs the VRC using persona_keys.signing.secret (branch taken because is_persona_did returned true) while the VRC.issuer/subject fields were constructed using our_r_did, which in another code path is treated as a relationship DID.
  3. The resulting VRC message has a mismatch between the cryptographic signer and the semantic identity declared in the credential body, which a verifier trusting only the issuer field (without independently verifying the proof's key against the expected DID document) could accept as valid.
  4. This produces a credential that appears bound to the relationship DID but is actually signed with persona key material, reintroducing the correlation risk this PR set out to fix, or causing legitimate future verification failures (denial of the relationship functioning) if the mismatch is instead caught downstream.
  5. Repeated occurrence for many relationships allows an attacker observing multiple such mis-signed VRCs to correlate them back to a single persona via the shared signing key/verification method, even though the declared issuer DIDs differ.

Preconditions: Relationship state transitions (establishment, re-establishment, key repair via repair_key_info_ids) occurring concurrently with VRC issuance., No independent, mandatory validation that the DID document of the declared issuer resolves to the actual key that produced the signature.

Existing Controls: is_persona_did(&our_r_did) discriminator selects the correct key branch under normal conditions. • relationship_signing_secret returns None if the relationship key was lost, forcing an explicit error path rather than silent fallback to the wrong key.

Recommended Mitigations: After signing, verify the resulting VRC signature resolves correctly against the issuer DID's DID document before sending, as a self-check. • Ensure our_r_did state used for signing key selection and message routing (listener_id_for_did) is derived from a single, atomically-read relationship state snapshot. • Add integration test asserting that vrc.sign() always uses the key matching the DID that will appear as issuer in vrc.message().


⚪ STRIDE-3: Missing Signing Secret Denial of Service in relationship_signing_secret Lookup

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

Description: relationship_signing_secret in openvtc-core/src/relationships.rs in the SecretsResolver Data Store allows a legitimate relationship counterparty to trigger a hard error and abort VRC issuance due to lost or never-registered relationship key material, resulting in denial of the VRC-issuance workflow for the affected relationship until manual re-establishment.

Evidence: openvtc-core/src/relationships.rs:162-176

pub async fn relationship_signing_secret(
    tdk: &affinidi_tdk::TDK,
    r_did: &str,
) -> Option<affinidi_tdk::secrets_resolver::secrets::Secret> {
    use affinidi_tdk::secrets_resolver::SecretsResolver;
    tdk.get_shared_state()
        .secrets_resolver()
        .get_secret(&relationship_sig

Attack Scenario:

  1. A relationship's signing secret is lost (e.g., due to storage corruption, device reset, or an attacker deliberately corrupting/deleting the TDK secrets store if they gain any local access) as referenced in the code comment 'Relationship::needs_reestablishment'.
  2. The counterparty sends a legitimate relationship-request finalize message, causing prepare_accept_vrc_request in inbox_actions.rs to be invoked.
  3. relationship_signing_secret(tdk, &our_r_did) in relationships.rs queries SecretsResolver.get_secret(&relationship_signing_vm_id(r_did)) and returns None because the secret is absent.
  4. The ok_or_else branch in inbox_actions.rs returns an anyhow::anyhow! error, aborting the VRC issuance and requiring the relationship to be re-established before any further legitimate trust-credential issuance can proceed.
  5. An attacker who can selectively corrupt or evict specific relationship secrets (e.g., via a storage-exhaustion or eviction-triggering side channel) can perform a targeted DoS against a victim's ability to issue VRCs for chosen relationships without needing to break any cryptography.

Preconditions: Attacker has some means to cause loss/corruption of a specific relationship's signing secret (local access, storage eviction, or an unrelated bug)., No self-healing/re-key mechanism exists that resolves without manual intervention.

Existing Controls: Explicit error path (ok_or_else) with a descriptive message rather than a silent failure or panic. • Reference to Relationship::needs_reestablishment suggesting an existing recovery workflow is expected.

Recommended Mitigations: Implement automatic detection and user-facing prompt to re-establish the relationship when a signing secret is found missing. • Add monitoring/alerting on repeated relationship_signing_secret lookup failures to detect targeted key-eviction attacks. • Consider redundant key backup/escrow strategies consistent with the trust model to reduce single-point key loss impact.


⚪ STRIDE-4: Verification-Method ID Convention Collision in relationship_signing_vm_id

Field Detail
Category Spoofing, Tampering
Severity Medium
Likelihood Possible
CVSS 6.4 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-706,CWE-863
CAPEC CAPEC-122
OWASP A04:2021 - Insecure Design

Description: relationship_signing_vm_id in openvtc-core/src/relationships.rs allows verification-method-id collisions or mismatches due to the hardcoded, string-formatted #key-1 convention not being cryptographically bound to the DID document, resulting in retrieval of an incorrect or attacker-substituted secret from the SecretsResolver if the naming convention is violated elsewhere (e.g., by generate_did_peer_from_secrets changing conventions or a malicious peer DID crafted to alias another id).

Evidence: openvtc-core/src/relationships.rs:137-160

pub fn relationship_signing_vm_id(r_did: &str) -> String {
    format!("{r_did}#key-1")
}
pub fn relationship_encryption_vm_id(r_did: &str) -> String {
    format!("{r_did}#key-2")
}

Attack Scenario:

  1. The convention that #key-1 is always the signing key and #key-2 is always the key-agreement key is enforced only by the DID-generation code path (DID::generate_did_peer_from_secrets) and is duplicated by string formatting in relationship_signing_vm_id and relationship_encryption_vm_id.
  2. If any relationship DID is created or reloaded through a different code path (e.g., an imported DID, a future did:peer version, or a migration bug in generate_profiles' reload path) that does not follow the #key-1/#key-2 convention, relationship_signing_vm_id(r_did) constructs a verification-method id that does not correspond to the actual signing key.
  3. get_secret() on the SecretsResolver either returns None (triggering the DoS in STRIDE-3) or, in a worse case, returns whatever secret happens to be registered under that id string if the resolver's storage is keyed loosely (e.g., populated by a different, attacker-influenced import routine).
  4. The wrong secret is used to sign the VRC, producing a credential whose signature does not correspond to the DID's actual key-1, which either fails verification (availability impact) or, if the resolver is shared/misconfigured, could leak use of an unintended key for signing operations outside their intended scope.
  5. This weakens the isolation the R-DID scheme is designed to provide, since the correctness of key selection depends entirely on an unenforced naming convention repeated across multiple modules (documented in the code comment as 'load-bearing in two places...so it is named here rather than spelled out a fourth time').

Preconditions: A code path that creates or repairs relationship DIDs without following the #key-1/#key-2 convention., SecretsResolver implementation that does not strictly scope secrets per-DID-document.

Existing Controls: Convention is documented extensively in code comments referencing generate_profiles and repair_key_info_ids as the two other load-bearing usages. • repair_key_info_ids exists specifically to re-key secrets to this convention, implying an active repair mechanism.

Recommended Mitigations: Resolve the verification-method id from the DID document itself (e.g., by inspecting the authentication/keyAgreement sections) rather than hardcoding #key-1/#key-2. • Add a runtime assertion that the resolved secret's public key matches the DID document's declared key-1 before signing. • Consolidate the convention into a single shared constant/function used by all four dependent call sites, with compile-time or test-time enforcement.


⚪ STRIDE-5: Insufficient Non-Repudiation Evidence for Issuer-Binding Change in vet_vrc_issued Error Path

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

Description: The rejection path in vet_vrc_issued in openvtc-core/src/messaging.rs allows an attacker who repeatedly submits mismatched-issuer VRCs to leave no persistent, tamper-evident audit trail beyond a transient Err(String) due to the absence of structured security-event logging, resulting in reduced ability to detect and later prove repeated probing/impersonation attempts against the VRC issuer gate.

Evidence: openvtc-core/src/messaging.rs:709-716

return Err(format!(
    "credential issuer ({}) is not the DID the sender uses in this \
     relationship ({})",
    vrc.issuer(),
    relationship.remote_did
));

Attack Scenario:

  1. Attacker repeatedly sends crafted VRCs with mismatched vrc.issuer() values (e.g., cycling through persona DID, arbitrary DIDs) to a victim's vet_vrc_issued entry point (EP-001).
  2. Each attempt returns a formatted error string but the code shown does not indicate this is persisted to an append-only, tamper-evident security log distinguishable from generic application logs.
  3. Without correlated logging (timestamps, sender DID, attempted issuer value, relationship id), the victim or an auditor cannot reliably reconstruct how many attempts were made, from whom, or whether the same attacker cycled through multiple relationships.
  4. This absence of forensic evidence lets an attacker deny having probed the gate, and hampers incident response when trying to determine whether the previously-vulnerable behavior (accepting persona-issued VRCs) was exploited before the fix landed.

Preconditions: No centralized structured security-event logging around vet_vrc_issued failures., Multi-attempt probing behavior by an attacker.

Existing Controls: Descriptive Err(String) return value that includes both the received and expected DID values, useful if logged by the caller.

Recommended Mitigations: Emit a structured, tamper-evident security audit log entry (sender DID, relationship id, expected vs. actual issuer, timestamp) on every Gate 2 rejection. • Add rate-limiting/alerting on repeated Gate-2 failures from the same sender or relationship. • Retain historical rejection records to support forensic reconstruction of pre-fix exploitation attempts.


⚪ STRIDE-6: Downgrade to Persona-DID Fallback via Forced Relationship-Without-R-DID Establishment

Field Detail
Category Spoofing, Elevation of Privilege
Severity Medium
Likelihood Possible
CVSS 6.9 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-656,CWE-286
CAPEC CAPEC-159
OWASP A04:2021 - Insecure Design

Description: The fallback branch in prepare_accept_vrc_request and the corresponding acceptance in vet_vrc_issued allows an adversarial peer to intentionally negotiate a relationship without a dedicated R-DID (remote_did == remote_p_did) due to the protocol permitting persona-only relationships, resulting in the victim's VRC issuer being forced back to the persona DID for that relationship and defeating the privacy improvement this PR introduces.

Evidence: openvtc/src/state_handler/inbox_actions.rs:748-763

if config.is_persona_did(&our_r_did) {
    let persona_keys = config.get_persona_keys(tdk).await?;
    vrc.sign(&persona_keys.signing.secret, None).await?;
} else { ... }

Attack Scenario:

  1. Attacker, as the initiating or accepting party in a relationship-establishment handshake (handshake messages routed by persona DIDs per docs/relationships-vrcs.md), deliberately avoids generating/negotiating a dedicated R-DID for the relationship.
  2. The relationship is established with remote_did == remote_p_did (the 'no dedicated R-DID' case explicitly supported by the code, confirmed by the test vrc_issued_under_the_persona_passes_when_that_is_the_relationship_did).
  3. When the victim later calls prepare_accept_vrc_request, config.is_persona_did(&our_r_did) evaluates true (since our_r_did equals the persona DID in this configuration), so the VRC is signed and issued under the victim's persona DID rather than a pairwise identity.
  4. The attacker, who controls or observes this relationship, now possesses a durable, publishable VRC bearing the victim's persona DID as issuer, and can repeat this technique across multiple relationships with the same victim (or coordinate with colluding peers) to rebuild the exact cross-relationship correlation this PR intends to prevent.
  5. Because relationship-DID negotiation is optional/peer-influenced rather than mandatory, this attack requires no exploitation of a bug — it abuses the legitimate fallback path as a downgrade vector.

Preconditions: Protocol/UX allows establishing a relationship without a dedicated R-DID., Victim's client does not warn or restrict VRC issuance when no R-DID exists., Attacker can influence or is the counterparty during relationship establishment.

Existing Controls: The no-R-DID case is an intentional, documented fallback rather than a bug; it is tested explicitly. • Users retain the choice of whether to establish a dedicated R-DID per docs/relationships-vrcs.md.

Recommended Mitigations: Default to always negotiating a dedicated R-DID for new relationships, requiring explicit user opt-out (and a clear warning) to fall back to persona-only. • Warn users in the UI/log when a VRC is about to be issued under the persona DID due to a missing R-DID. • Track and alert when a single persona has issued VRCs to many distinct relationships without R-DIDs, indicating potential correlation exposure.


⚪ STRIDE-7: Trust Graph Publication of Persona-Correlating VRC Metadata

Field Detail
Category Information Disclosure
Severity High
Likelihood Likely
CVSS 7.1 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
Residual Severity Medium
CWE CWE-200,CWE-203
CAPEC CAPEC-118
OWASP A01:2021 - Broken Access Control

Description: The community trust graph publication path referenced in code comments in openvtc/src/state_handler/inbox_actions.rs and docs/relationships-vrcs.md allows information disclosure due to VRCs being durable, publishable artifacts whose issuer/subject fields may still leak persona correlation via out-of-band metadata (timing, validFrom/validUntil clustering, or co-published credential sets) even after switching to R-DID issuance, resulting in partial re-identification of personas through side-channel graph analysis.

Evidence: docs/relationships-vrcs.md:78-90

relationship is established, and the VRC is issued under it — so the durable
credential, which is the artifact that would otherwise correlate you across
every relationship you hold, names only the pairwise identifier.

Attack Scenario:

  1. Multiple VRCs, now correctly issued under distinct R-DIDs per relationship, are published by their holders to the community trust graph as described in the code comments.
  2. An adversary with graph-analysis capability collects all published VRCs and correlates them not by issuer DID (which now differs per relationship) but by side-channel metadata: nearly identical validFrom timestamps (same establishment burst), shared validUntil expiry windows, or co-occurrence patterns (e.g., several R-DID VRCs published within seconds of each other by the same physical node/IP).
  3. Because the underlying signing infrastructure (TDK, SecretsResolver) and node identity are otherwise shared across all relationships for a single user, timing and infrastructure-level correlation can still link multiple R-DID-issued VRCs back to one persona, even though the cryptographic issuer field no longer directly reveals it.
  4. This achieves the same de-anonymization goal the PR sought to prevent, through a side channel outside the scope of the issuer-binding fix, since the fix addresses only the direct field-level correlation and not timing/graph metadata correlation.

Preconditions: Adversary has broad visibility into the published trust graph., VRCs are published in a way that preserves timing metadata or publication batching., No jitter/anonymization applied to publication timing.

Existing Controls: Issuer field correlation (the primary, most direct vector) is now mitigated by this PR. • Pairwise R-DIDs reduce the most obvious form of correlation.

Recommended Mitigations: Introduce random publication delay/jitter for VRCs to reduce timing-correlation signal. • Avoid batch-publishing multiple relationship VRCs from the same session/connection. • Document and evaluate metadata-level correlation risk (timestamps, expiry windows) as a follow-on privacy hardening task. • Consider unlinkable validity-period ranges (e.g., rounding validFrom/validUntil to coarse buckets shared across many unrelated users).


⚪ STRIDE-8: Gate 1/Gate 2 Ordering Dependency Enabling Relationship-Confusion Spoofing

Field Detail
Category Spoofing, Elevation of Privilege
Severity Medium
Likelihood Possible
CVSS 6.3 CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
Residual Severity Low
CWE CWE-367,CWE-362
CAPEC CAPEC-25
OWASP A04:2021 - Insecure Design

Description: vet_vrc_issued in openvtc-core/src/messaging.rs allows an authenticated but wrong-relationship sender to pass Gate 2 due to relationship lookup being keyed by from_did without also validating that the resolved relationship object corresponds to the specific task/request context expected, resulting in potential cross-relationship confusion if two relationships happen to share overlapping DID material (e.g., during re-establishment transition windows).

Evidence: openvtc-core/src/messaging.rs:696-717

let remote_p_did = Arc::clone(&relationship.remote_p_did);
if vrc.issuer() != relationship.remote_did.as_str() { ... }

Attack Scenario:

  1. Gate 1 (not shown in the diff but referenced as matching from_did against a relationship) resolves a Relationship record keyed on the authenticated sender's DID.
  2. During a relationship re-establishment window (referenced via Relationship::needs_reestablishment), it is possible for stale and new relationship records to transiently coexist or for remote_did to be updated asynchronously relative to in-flight VRC processing.
  3. An attacker who has just re-established a relationship (obtaining a new R-DID) races the delivery of a VRC signed under the old identity before the victim's relationship store fully commits the transition, exploiting a TOCTOU window between relationship-state lookup and the Gate 2 comparison.
  4. If the comparison in Gate 2 (vrc.issuer() != relationship.remote_did.as_str()) reads a relationship object captured before the transition completes, a VRC that should be rejected under the new binding may be validated against stale state, weakening the intended tightening.
  5. This does not require breaking cryptography — it exploits a state-consistency assumption between relationship lookup and the issuer-binding check that the diff does not visibly guard with any transactional/locking mechanism.

Preconditions: Concurrent relationship re-establishment and VRC processing for the same relationship., No atomic/transactional guarantee between relationship-state read and Gate 2 comparison.

Existing Controls: Gate 1 (referenced) already authenticates from_did against the relationship before Gate 2 executes, narrowing the window. • Relationship::needs_reestablishment implies re-establishment is a deliberate, tracked state rather than silent.

Recommended Mitigations: Ensure relationship state used across Gate 1 and Gate 2 is read from a single consistent snapshot (e.g., one lock acquisition or immutable clone) for the duration of vet_vrc_issued. • Add explicit state-machine guards preventing VRC processing during an in-progress re-establishment transition. • Add regression tests simulating concurrent re-establishment and VRC delivery.


⚪ STRIDE-9: Documentation-Embedded Narrative Manipulation Attempt in relationships-vrcs.md

Field Detail
Category Tampering
Severity Informational
Likelihood Very Unlikely
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-1039
CAPEC CAPEC-242
OWASP A03:2021 - Injection

Description: docs/relationships-vrcs.md in the documentation content contains narrative prose referencing external issue trackers and justification text that could be mistaken for analyzer instructions due to being embedded directly in the diff/data under analysis, resulting in a potential prompt-injection style manipulation attempt against automated review/analysis tooling if not explicitly treated as inert data.

Evidence: docs/relationships-vrcs.md:78-84

relationship is established. Full pairwise operation, including the VRC issuer
field, depends on protocol work tracked in
[verifiable-trust-infrastructure#1054](https://github.com/OpenVTC/verifiable-trust-infrastructure/issues/1054).

Attack Scenario:

  1. The documentation diff in docs/relationships-vrcs.md contains removed and added prose referencing '[verifiable-trust-infrastructure#1054]' and general narrative claims about why the issuer field 'mattered more than the routing does'.
  2. An automated analysis or review pipeline ingesting this diff as untrusted input could, if insufficiently isolated, misinterpret embedded prose as directives rather than descriptive content.
  3. This finding documents that the content was correctly treated as data-under-analysis in this review, per the security directive, and flags the pattern (narrative justification embedded in diffs referencing external trackers) as a potential future vector for prompt-injection against LLM-based or automated code-review tooling if such tooling is used in the OpenVTC CI/CD pipeline without strict input/output separation.

Preconditions: An automated LLM-based or heuristic review tool processes PR diffs/docs without strict data/instruction separation.

Existing Controls: This analysis explicitly treats all diff/doc content as data, not instructions, per the security directive.

Recommended Mitigations: Ensure any automated review tooling in the OpenVTC CI/CD pipeline enforces strict separation between analyzed content and operator instructions. • Sanitize or clearly delimit PR description/doc content before feeding it to any LLM-based review automation.


⚪ STRIDE-10: Supply-Chain Trust in affinidi_tdk SecretsResolver Implementation

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

Description: relationship_signing_secret in openvtc-core/src/relationships.rs and vrc.sign() in inbox_actions.rs allow full trust delegation to the external affinidi_tdk dependency's SecretsResolver and cryptographic signing implementation due to the absence of any local integrity check on the returned secret or produced signature, resulting in a supply-chain risk where a compromised or vulnerable affinidi_tdk release could silently weaken or bypass the entire VRC issuer-binding security model this PR implements.

Evidence: openvtc/src/state_handler/inbox_actions.rs:749-763

let secret = openvtc_core::relationships::relationship_signing_secret(tdk, &our_r_did)
    .await
    .ok_or_else(|| { ... })?;
vrc.sign(&secret, None).await?;

Attack Scenario:

  1. openvtc-core and openvtc depend on the external affinidi_tdk crate for SecretsResolver, DID resolution, and cryptographic signing (vrc.sign()), none of which are re-verified locally after use.
  2. An attacker compromises the affinidi_tdk supply chain (e.g., via a malicious crate update, typosquatting, or a compromised maintainer account) and ships a version that, for example, returns a fixed/predictable secret from get_secret(), or signs with a weak/backdoored algorithm.
  3. Because relationship_signing_secret and the sign() call trust the dependency's return value implicitly, this PR's entire pairwise-privacy fix (binding VRC issuer to R-DID) is undermined at the cryptographic layer regardless of the correctness of the Rust-level issuer-binding logic.
  4. All VRCs signed after the compromised dependency is deployed could be forged, replayed, or have their intended privacy guarantees silently defeated, without any visible change to the messaging.rs/inbox_actions.rs logic reviewed in this diff.
  5. Detection would be difficult since the vulnerability lives entirely outside the reviewed files, in a transitive dependency not included in this analysis's source set.

Preconditions: Attacker achieves compromise of the affinidi_tdk crate supply chain or a transitive dependency., No dependency pinning/verification (e.g., cargo-vet, cargo-audit, lockfile hash verification) enforced in CI.

Existing Controls: Use of a dedicated TDK abstraction (SecretsResolver trait) which at least centralizes secret access behind one interface, easing future auditing.

Recommended Mitigations: Pin and verify affinidi_tdk versions via Cargo.lock with reproducible builds and checksum verification. • Integrate cargo-audit/cargo-vet or equivalent supply-chain scanning into CI for all TDK-related dependencies. • Add cryptographic self-checks (e.g., verify the produced VRC signature against the expected public key immediately after signing) independent of the TDK's internal correctness.


⚪ STRIDE-11: Test-Only Coverage Gap Enabling Regression of Gate 2 Tightening

Field Detail
Category Tampering
Severity Low
Likelihood Possible
CVSS 4.0 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-1120,CWE-1077
CAPEC CAPEC-267
OWASP A04:2021 - Insecure Design

Description: The unit tests added in openvtc-core/src/messaging.rs for vet_vrc_issued allow future silent regression of the Gate 2 tightening due to coverage being limited to two hand-picked scenarios without property-based or exhaustive DID-combination testing, resulting in a risk that a future refactor could reintroduce the persona-DID acceptance bug without failing CI.

Evidence: openvtc-core/src/messaging.rs:1988-2019

fn vrc_issued_under_the_senders_persona_did_is_refused() { ... }
fn vrc_issued_under_the_persona_passes_when_that_is_the_relationship_did() { ... }

Attack Scenario:

  1. Developers rely on vrc_issued_under_the_senders_persona_did_is_refused and vrc_issued_under_the_persona_passes_when_that_is_the_relationship_did as the sole regression guards for Gate 2 behavior.
  2. A future refactor (e.g., merging Gate 1 and Gate 2, or changing the Relationship struct's field names/semantics) could alter comparison logic in a way that both existing tests still pass by coincidence (e.g., if test fixtures always set remote_did distinctly) while a real-world edge case (e.g., empty string DID, case-sensitivity mismatch, or Unicode normalization difference in DID strings) reintroduces acceptance of an incorrect issuer.
  3. Because there is no fuzz/property-based test asserting the invariant 'Gate 2 accepts iff vrc.issuer() == relationship.remote_did' across a wide range of DID string forms, such a regression would ship silently.
  4. An attacker discovering the regression (e.g., via a DID string edge case such as trailing whitespace, differing percent-encoding, or mixed case in a did:peer identifier) could bypass Gate 2 despite the tests passing in CI.

Preconditions: A future code change alters DID comparison semantics., No fuzz/property-based testing of DID string equality edge cases.

Existing Controls: Two explicit regression tests directly encode the intended before/after behavior for the primary cases. • Tests include clear, descriptive failure messages aiding triage.

Recommended Mitigations: Add property-based tests (e.g., using proptest) asserting the Gate 2 invariant across a broad space of DID string permutations, including case and encoding variants. • Add a test asserting DID string comparison is exact byte-for-byte (no normalization) to guard against homograph/normalization-based bypass. • Include a regression test for empty/malformed DID strings in both issuer and remote_did fields.



🍝 PASTA Threat Model

Application Purpose

OpenVTC issues and verifies Verifiable Relationship Credentials (VRCs) over DIDComm to attest pairwise trust relationships between personas while minimizing cross-relationship correlation, providing privacy-preserving decentralized trust attestation for community/professional networks.

Inherent Risks

  • VRCs are durable, potentially publishable artifacts whose issuer/subject fields can persistently correlate identities if not carefully scoped.
  • The system depends on an external cryptographic/DID toolkit (affinidi_tdk) whose internal correctness is not independently verified.
  • Relationship establishment optionally omits a dedicated relationship DID, creating an intentional but risk-bearing fallback to persona-scoped identifiers.
  • Key material for relationship DIDs can be lost, requiring re-establishment and creating availability dependencies on secret storage durability.

Objectives

Risk: Accept residual risk of side-channel (timing/metadata) correlation as lower priority than direct issuer-field correlation.; Treat loss of relationship signing key material as a recoverable availability event, not a security compromise.
Business: Provide privacy-preserving, verifiable trust relationship attestations for community and professional networks.; Prevent persona correlation across relationships from durable published credentials.
Security: Bind VRC issuer/subject identity strictly to the identity used within a specific relationship channel.; Prevent cross-relationship correlation of a persona via published trust-graph artifacts.; Ensure the key used to sign a VRC always matches the DID declared as its issuer.
Financial: Avoid reputational and remediation costs associated with a privacy-breaking credential design flaw.
Compliance: Align with data minimization and unlinkability principles found in privacy-by-design frameworks applicable to decentralized identity systems.
Functional: Support pairwise relationship DIDs (R-DIDs) as the default identity used for VRC issuance and verification.; Support fallback to persona DID issuance only when no dedicated R-DID exists for a relationship.
Operational: Ensure VRC issuance and verification remain available even when relationship key material must be repaired or re-established.; Maintain consistent enforcement of issuer-binding rules across all verification entry points.

Business Impact Analysis (1)

BIA-1: VRC Issuance and Verification (High)

The end-to-end process of accepting a relationship request, signing a Verifiable Relationship Credential under the correct identity, transmitting it over DIDComm, and verifying its issuer binding on receipt.

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

  • Stakeholders: Community Trust Graph Operators / End Users (Personas) / OpenVTC Core Maintainers / Relationship Counterparties
  • Dependencies: affinidi_tdk SecretsResolver / DIDComm Mediator/Routing Infrastructure / DTGCredential Library / TDK DID Resolver
  • Disruptions: Loss of relationship signing secret preventing VRC issuance. / Regression reintroducing persona-DID acceptance in Gate 2. / Compromise of affinidi_tdk supply chain affecting signing integrity. / Concurrent re-establishment causing stale relationship-state reads.
  • Impacts: Cross-relationship correlation of personas via published VRCs, undermining the platform's core privacy value proposition. / Inability for affected users to issue or accept new VRCs until relationship re-establishment. / Loss of user trust in the platform's privacy guarantees if correlation incidents become public.

Technical Scope

Roles (3): RO-1 Persona Owner · RO-2 Relationship Counterparty · RO-3 Trust Graph Observer

Actors (3): AC-1 End User Persona · AC-2 Remote Peer Node · AC-3 TDK Secrets Resolver Service

Entry Points (4): EP-1 Inbound VRC Verification · EP-2 Relationship Signing Secret Lookup · EP-3 Accept Relationship Request / Issue VRC · EP-4 Outbound VRC DIDComm Message

Threat Actors (4): TA-1 Malicious Relationship Counterparty · TA-2 Trust Graph Analyst · TA-3 Supply Chain Attacker · TA-4 Local Access Attacker

Infrastructure (2): IF-1 OpenVTC Node Runtime · IF-2 DIDComm Mediator Service

Trust Boundaries (3): TB-1 DIDComm Network Boundary · TB-2 Local Secrets/Key Store Boundary · TB-3 Community Trust Graph Boundary

External Entities (3): EE-1 Relationship Counterparty Node · EE-2 DIDComm Mediator · EE-3 Trust Graph Observer

System Components (5): SC-1 VRC Verification Gate (vet_vrc_issued) · SC-2 Relationship Key Management (relationships.rs) · SC-3 VRC Issuance Handler (prepare_accept_vrc_request) · SC-4 TDK Secrets Resolver · SC-5 Community Trust Graph

Resources And Assets (4): RA-1 Verifiable Relationship Credential (VRC) · RA-2 Relationship Signing Secret · RA-3 Persona Signing Keys · RA-4 Relationship State Record

Technologies And Dependencies (3): TD-1 affinidi_tdk · TD-2 DTGCredential · TD-3 did:peer / did:webvh

Use Cases (2)

  • Establish Pairwise Relationship and Issue VRC: A persona owner accepts a relationship request, causing the node to optionally generate a dedicated relationship DID, then sign and send a Verifiable Relationship Credential to the counterparty under
  • Verify Inbound VRC Issuer Binding: Upon receiving a VRC from an authenticated relationship counterparty, the receiving node validates that the credential's issuer matches the identity the sender uses within that specific relationship b

📋 Risk Registry (8)

ID Title Severity Residual Priority Effort
RISK-001 Cross-relationship correlation of a persona via mismatched or regressed VRC issuer binding High Medium Short-Term Medium
RISK-002 VRC signed with a key that does not match its declared issuer DID due to signing-branch logic error High Medium Short-Term Medium
RISK-003 Side-channel correlation of personas via VRC publication timing/metadata despite issuer-field fix High Medium Medium-Term Medium
RISK-004 Users or counterparties bypassing pairwise privacy protections by omitting a dedicated relationship DID Medium Medium Medium-Term Low
RISK-005 Verification-method naming convention (#key-1/#key-2) not enforced against DID document, risking wrong-key retrieval Medium Low Long-Term Medium
RISK-006 Loss of relationship signing secret causing denial of VRC issuance for affected relationships Medium Low Medium-Term Low
RISK-007 Supply-chain compromise of affinidi_tdk silently undermining the entire issuer-binding security model Medium Medium Long-Term High
RISK-008 Insufficient security-event logging around Gate 2 rejections hampering detection and forensic reconstruction Low Low Long-Term Low

⚔️ Attack Scenarios (4)

SC-1: VRC Verification Gate (vet_vrc_issued)

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC1@{ shape: rect, label: "SC-1: VRC Verification Gate" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE290@{ shape: rect, label: "CWE-290: Authentication Bypass by Spoofing" }
    CWE367@{ shape: rect, label: "CWE-367: TOCTOU Race Condition" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC151@{ shape: rect, label: "CAPEC-151: Identity Spoofing" }
    CAPEC25@{ shape: rect, label: "CAPEC-25: Forced Deadlock/Race Exploitation" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    STRIDE1@{ shape: rect, label: "STRIDE-1: Persona-Issuer VRC Replay Across Relationships<br><i>High / Possible</i>" }
    STRIDE8@{ shape: rect, label: "STRIDE-8: Gate 1/Gate 2 Ordering Dependency<br><i>Medium / Possible</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA1@{ shape: rect, label: "TA-1: Malicious Relationship Counterparty<br><i>Deanonymize personas across relationships</i>" }
  end
  SC1 --> CWE290
  CWE290 --> CAPEC151
  CAPEC151 --> STRIDE1
  STRIDE1 --> TA1
  SC1 --> CWE367
  CWE367 --> CAPEC25
  CAPEC25 --> STRIDE8
  STRIDE8 --> 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:#FFA500, stroke-width: 2px
  linkStyle 5 stroke:#FFA500, stroke-width: 2px
  linkStyle 6 stroke:#FFA500, stroke-width: 2px
  linkStyle 7 stroke:#FFA500, stroke-width: 2px
Loading

SC-3: VRC Issuance Handler (prepare_accept_vrc_request)

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC3@{ shape: rect, label: "SC-3: VRC Issuance Handler" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE347@{ shape: rect, label: "CWE-347: Improper Verification of Cryptographic Signature" }
    CWE656@{ shape: rect, label: "CWE-656: Reliance on Security Through Obscurity" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC115@{ shape: rect, label: "CAPEC-115: Authentication Bypass" }
    CAPEC159@{ shape: rect, label: "CAPEC-159: Redirect Access to Weaker Configuration" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    STRIDE2@{ shape: rect, label: "STRIDE-2: Signing-Key Confusion Between Persona and Relationship DID<br><i>High / Possible</i>" }
    STRIDE6@{ shape: rect, label: "STRIDE-6: Downgrade to Persona-DID Fallback<br><i>Medium / Possible</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA1@{ shape: rect, label: "TA-1: Malicious Relationship Counterparty<br><i>Deanonymize personas across relationships</i>" }
  end
  SC3 --> CWE347
  CWE347 --> CAPEC115
  CAPEC115 --> STRIDE2
  STRIDE2 --> TA1
  SC3 --> CWE656
  CWE656 --> CAPEC159
  CAPEC159 --> STRIDE6
  STRIDE6 --> 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:#FFA500, stroke-width: 2px
  linkStyle 5 stroke:#FFA500, stroke-width: 2px
  linkStyle 6 stroke:#FFA500, stroke-width: 2px
  linkStyle 7 stroke:#FFA500, stroke-width: 2px
Loading

SC-2: Relationship Key Management (relationships.rs)

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC2@{ shape: rect, label: "SC-2: Relationship Key Management" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE706@{ shape: rect, label: "CWE-706: Use of Incorrectly-Resolved Name or Reference" }
    CWE404@{ shape: rect, label: "CWE-404: Improper Resource Shutdown or Release" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC122@{ shape: rect, label: "CAPEC-122: Privilege Abuse via Naming Convention" }
    CAPEC125@{ shape: rect, label: "CAPEC-125: Flooding / Resource Exhaustion" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    STRIDE4@{ shape: rect, label: "STRIDE-4: Verification-Method ID Convention Collision<br><i>Medium / Possible</i>" }
    STRIDE3@{ shape: rect, label: "STRIDE-3: Missing Signing Secret Denial of Service<br><i>Medium / Likely</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA4@{ shape: rect, label: "TA-4: Local Access Attacker<br><i>Trigger targeted denial of service</i>" }
  end
  SC2 --> CWE706
  CWE706 --> CAPEC122
  CAPEC122 --> STRIDE4
  STRIDE4 --> TA4
  SC2 --> CWE404
  CWE404 --> CAPEC125
  CAPEC125 --> STRIDE3
  STRIDE3 --> TA4
  linkStyle 0 stroke:#FFA500, stroke-width: 2px
  linkStyle 1 stroke:#FFA500, stroke-width: 2px
  linkStyle 2 stroke:#FFA500, stroke-width: 2px
  linkStyle 3 stroke:#FFA500, stroke-width: 2px
  linkStyle 4 stroke:#FFA500, stroke-width: 2px
  linkStyle 5 stroke:#FFA500, stroke-width: 2px
  linkStyle 6 stroke:#FFA500, stroke-width: 2px
  linkStyle 7 stroke:#FFA500, stroke-width: 2px
Loading

SC-5: Community Trust Graph

---
config:
  layout: dagre
  look: classic
  theme: dark
---
flowchart LR
  subgraph SL1["1. System Component"]
    direction LR
    SC5@{ shape: rect, label: "SC-5: Community Trust Graph" }
  end
  subgraph SL2["2. Weaknesses"]
    direction LR
    CWE200@{ shape: rect, label: "CWE-200: Exposure of Sensitive Information" }
  end
  subgraph SL3["3. Attack Patterns"]
    direction LR
    CAPEC118@{ shape: rect, label: "CAPEC-118: Data Collection via Aggregation" }
  end
  subgraph SL4["4. Threats"]
    direction LR
    STRIDE7@{ shape: rect, label: "STRIDE-7: Trust Graph Publication of Persona-Correlating VRC Metadata<br><i>High / Likely</i>" }
  end
  subgraph SL5["5. Threat Actors"]
    direction LR
    TA2@{ shape: rect, label: "TA-2: Trust Graph Analyst<br><i>Aggregate metadata to build correlation graph</i>" }
  end
  SC5 --> CWE200
  CWE200 --> CAPEC118
  CAPEC118 --> STRIDE7
  STRIDE7 --> TA2
  linkStyle 0 stroke:#FF0000, stroke-width: 2px
  linkStyle 1 stroke:#FF0000, stroke-width: 2px
  linkStyle 2 stroke:#FF0000, stroke-width: 2px
Loading

📊 Risk Summary

Total Threats: 11

By Severity: Low: 2 · High: 3 · Medium: 5 · Informational: 1

By Category: Unknown: 11

🎯 Attack Surface

Kill Chain 1: An attacker who is or becomes a legitimate relationship counterparty (TA-1) exploits the interaction between the intentional no-R-DID fallback (STRIDE-6) and the signing-branch logic in prepare_accept_vrc_request (STRIDE-2) to force or receive a VRC issued under the victim's persona DID rather than a pairwise R-DID; if this VRC is later published to the community trust graph (SC-5), the attacker or a passive Trust Graph Analyst (TA-2) can directly correlate it with every other persona-issued VRC the victim has produced, fully reconstructing the victim's relationship graph despite the Gate 2 tightening in vet_vrc_issued. Kill Chain 2: Even where Gate 2's tightened issuer-binding check (STRIDE-1, STRIDE-8) is correctly enforced end-to-end, a Trust Graph Analyst (TA-2) can fall back to a metadata-level correlation attack (STRIDE-7) by clustering VRCs based on publication timing, validity-period windows, or co-occurrence patterns, chaining a purely passive observational technique onto the residual risk left by the primary fix to partially defeat the unlinkability goal. Kill Chain 3: A Local Access Attacker (TA-4) or an adversary able to influence relationship state during a re-establishment window chains a targeted secret-eviction or TOCTOU race (STRIDE-3, STRIDE-8) against a specific relationship's signing key, forcing either a denial-of-service on VRC issuance or a window in which stale relationship state is validated against a new VRC, potentially reintroducing a

🛡️ Risk Mitigation Strategy

Priority 1: Immediately harden and expand test coverage for the Gate 2 issuer-binding invariant (RISK-001) with property-based/fuzz testing of DID string comparisons and ensure the tightened rule is enforced consistently at every VRC verification entry point in the codebase, not solely within vet_vrc_issued, since any missed consumer reopens the exact correlation vulnerability this PR fixes. Priority 2: Add a post-signing self-verification step in prepare_accept_vrc_request (RISK-002) that confirms the produced signature cryptographically resolves against the DID declared as the VRC issuer, closing the signing-branch logic gap before it can produce mismatched credentials, and pair this with atomic reads of relationship state across the signing-key-selection and message-routing decision points to eliminate the TOCTOU exposure identified in Gate 1/Gate 2 ordering. Priority 3: Treat the no-R-DID fallback (RISK-004) as a security-relevant default rather than a neutral configuration choice — require explicit user acknowledgment before establishing a relationship without a dedicated R-DID, and surface a clear warning whenever a VRC is about to be issued under the persona DID, since this fallback is the most direct way the fix in this PR can be silently bypassed. Priority 4: Address residual privacy leakage via publication metadata (RISK-003) over the medium term by introducing timing jitter and avoiding batch publication of multiple VRCs, recognizing that the issuer-field fix alone does not achieve full unlinkability against a determined trust-graph analyst. Priority 5: Longer term, reduce reliance on unenforced naming conventions (RISK-005) by resolving verification-method ids from DID documents directly, improve operational resilience against relationship-key loss (RISK-006) with automated re-establishment prompts, strengthen supply-chain assurance for affinidi_tdk (RISK-007) via dependency pinning and automated vetting, and add structured security-event logging around


Generated by Agentic Sec — Threat Model & Affect Analysis Agent

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

Confirmed (2)

  • 🟡 Verification-method-id derivation for relationship signing key relies on unenforced naming convention (CWE-706)
  • 🟡 Selective secrets-store eviction causes deterministic denial of VRC issuance per relationship (CWE-404)

Must-Review-By-Human (2)

  • 🟠 Non-atomic key-branch selection may sign VRC with mismatched issuer identity (TOCTOU / CWE-362)
  • 🟠 Gate 2 issuer-binding tightening (messaging.rs) is a single-point control with no independent verification-method cross-check (CWE-346/CWE-290 residual)

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