feat: add the VAC and VDC credential types - #15
Conversation
Adds the two credentials that confer rather than assert. Both track drafts in trustoverip/dtgwg-cred-spec - PR #29 (VAC) and #19 (VDC) - and are marked as such in the API docs so a consumer knows the shape may move. The VAC carries an AuthorityGrant: a scope, the actions permitted within it, and the optional parent and audience that make attenuation work. attenuate() derives a narrower VAC from one the holder already has, without involving the issuer - which is what lets a member equip an agent with four hours of read-only access rather than lending it their own standing authority. authority::verify_chain is the part that matters. Issuing a VAC is a struct and a signature; what stops a holder acquiring authority they were never given is a verifier refusing a chain that widens. Anyone can mint a well-formed VAC naming any scope and any actions and it will verify perfectly as a credential - what makes it worthless is that its chain does not reach the party governing the scope. Seven rules, each closing one way of getting more than was granted, and the tests are mostly attacks: a self-issued grant, an added action, a grafted parent, a leaked audience-bound credential, an expired link under a live parent, a chain past the depth ceiling. Resolution is bearer-side by construction - verify_chain takes the chain as a slice and never dereferences parent. Resolving over the network would make verification depend on availability, turn every id into a request the verifier can be induced to make against an address the holder chooses, and signal credential use to whoever hosts the identifier. Empty actions confers nothing rather than everything, refused both by the constructor and at the deserialization boundary so it cannot be reached either way. DTGCredentialType is non_exhaustive, so the new variants do not break callers matching with a wildcard. Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
🛡️ AI Agentic Security Code Review2 AI-confirmed issues, 3 findings need a human to review/validate. Mandatory to check: 🔒 Security Code Review Report Details🛡️ Security Code Review Report — PR #15
🗺️ Scan CoverageModules scanned: 3 · with findings: 1 · files: 6 · findings: 8
Executive Summary
🔒 Security IssuesConfirmed Vulnerabilities (2)🟡 Validity-window checks trust a fully caller-supplied timestamp, enabling temporal bypass if
|
| Field | Detail |
|---|---|
| Severity | MEDIUM |
| Location | src/authority.rs:216 |
| Finding ID | github_pr-1199860b7fd1 |
| CWE | CWE-294, CWE-367 |
| OWASP | A07:2021 - Identification and Authentication Failures |
| MITRE ATT&CK | T1078 - Valid Accounts, T1553 - Subvert Trust Controls |
| CAPEC | CAPEC-90, CAPEC-462 |
| DREAD | 6.4 |
| Reachability | 🔴 Reachable |
| Exploit Maturity | conceptual |
| Detection Source | skill_scan |
🧠 AI Triage:
- Severity reassessed: HIGH → MEDIUM — The scanner itself rates exploitability as 'medium' and confirms no direct injection vector into
atexists in the visible code — exploitation is 'entirely conditional on caller behavior outside this file.' No public exploit exists, exploit maturity is only 'conceptual', and CVSS is not provided (unable to confirm CVSS≥7 threshold). Per HIGH severity gates, this requires exploitability≥5 OR public_exploit≥7, and while exploitability is borderline-medium, the lack of confirmed production deployment and lack of a demonstrated attacker-controlled path intoatwithin this codebase caps this at medium. This is a legitimate design weakness worth fixing but not an immediately exploitable vulnerability as currently evidenced. - Composite score: 4.4
- Environment: unknown
Summary: verify_chain's temporal validity checks are only as trustworthy as the at parameter passed in by the caller; the function itself performs no freshness or monotonicity enforcement on this value, creating a design-level risk that any integrator sourcing at from client-controlled or cached data creates a replay/rollback vector for expired credentials.
📝 Description:
In deployments where the at timestamp is derived from client-supplied data, a cached authorization decision, or any source outside a trusted, freshly-sampled system clock, an attacker can present an expired VAC/VDC chain together with a stale at value and have the library report it as currently valid, extending access beyond the intended validity window.
🧪 Proof of Concept:
Both comparisons are performed strictly against the at argument as given, with no internal call to Utc::now() and no check that at is recent/fresh relative to any trusted reference. Any caller-controlled or replayed at value that predates the credential's real expiry will cause these checks to pass for an otherwise-expired credential.
let c = link.credential();
if c.valid_from() > at {
return Err(AuthorityError::NotValidNow { index, at });
}
if let Some(until) = c.valid_until()
&& until < at
{
return Err(AuthorityError::NotValidNow { index, at });
}
}
Vulnerable lines: 213, 226
🔎 Evidence: src/authority.rs:216
if c.valid_from() > at {
return Err(AuthorityError::NotValidNow { index, at });
}
if let Some(until) = c.valid_until()
&& until < at
{
return Err(AuthorityError::NotValidNow { index, at });
}
💥 Impact:
In deployments where the at timestamp is derived from client-supplied data, a cached authorization decision, or any source outside a trusted, freshly-sampled system clock, an attacker can present an expired VAC/VDC chain together with a stale at value and have the library report it as currently valid, extending access beyond the intended validity window.
Confidentiality: High — an expired credential that should be denied is instead accepted, extending unauthorized access beyond its intended lifetime · Integrity: Low — does not itself corrupt data, but permits actions under a false authorization state · Availability: None
🧭 Reachability:
- Network exposure: internal
- Auth barrier: none
- Attack path: EP-001 (verify_chain
atparameter) ← caller passes any DateTime, potentially derived from untrusted input
⚖️ Triage Factors:
| Factor | Value |
|---|---|
| Fixable | ✅ Yes |
| Exploitability | medium |
| Business impact | medium |
| Public exploit | None known |
| Environment | unknown |
Attack scenario: If an integrator derives the at argument to verify_chain from anything other than a freshly-sampled trusted clock, an attacker can present an already-expired chain alongside a stale/replayed at value and have it accepted as currently valid.
🔧 Remediation:
⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.
Removing at from the public parameter list and sampling it internally with Utc::now() eliminates any possibility of a caller supplying a stale, replayed, or attacker-influenced timestamp. If deterministic time injection is required for testing, expose it only behind a #[cfg(test)]-gated or explicitly-named verify_chain_at variant, never as the default production entry point.
Vulnerable code:
pub fn verify_chain(
chain: &[DTGCredential],
governing_party: &str,
requested_scope: &str,
requested_action: &str,
presenter: &str,
at: DateTime<Utc>,
) -> Result<VerifiedAuthority, AuthorityError> { ... uses `at` directly for all validity checks ... }
Secure code:
pub fn verify_chain(
chain: &[DTGCredential],
governing_party: &str,
requested_scope: &str,
requested_action: &str,
presenter: &str,
) -> Result<VerifiedAuthority, AuthorityError> {
let at = Utc::now(); // sampled internally; never caller-influenced
// ... rest unchanged, using this trusted `at` ...
}
Additional recommendations:
- If a testable variant with explicit
atmust remain for unit tests, name it distinctly (e.g.verify_chain_at) and mark it#[doc(hidden)]or behind a feature flag so production code paths cannot easily reach for it. - Add monotonic clock / replay-protection guidance for any calling service that must cache authorization decisions.
🔍 Validation Log
- Verdict: ✅ Confirmed True Positive
- Confidence: 90%
- AI Validation Evidence: EVIDENCE FOUND: verify_chain signature is
pub fn verify_chain(chain: &[DTGCredential], governing_party: &str, requested_scope: &str, requested_action: &str, presenter: &str, at: DateTime<Utc>) -> Result<VerifiedAuthority, AuthorityError>—atis a plain caller-supplied parameter, not derived internally viaUtc::now(). The validity checks are:if c.valid_from() > at { return Err(...) }andif let Some(until) = c.valid_until() && until < at { return Err(...) }(lines 216-225 per finding,- Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.
🟡 Non-canonicalized string equality on identity fields (issuer/subject/audience) risks spoofing if identifiers are DIDs/URIs with encoding variance
| Field | Detail |
|---|---|
| Severity | MEDIUM |
| Location | src/authority.rs:296 |
| Finding ID | github_pr-23a5230f7f1c |
| CWE | CWE-178, CWE-697 |
| OWASP | A07:2021 - Identification and Authentication Failures |
| MITRE ATT&CK | T1036 - Masquerading |
| CAPEC | CAPEC-627, CAPEC-165 |
| Reachability | 🔴 Reachable |
| Exploit Maturity | theoretical |
| Detection Source | skill_scan |
🧠 AI Triage:
- Triaged severity: MEDIUM
- CWE-178/697 comparison-normalization issue confirmed present in code (src/authority.rs:296-301) and reachable via unauthenticated EP-001, but exploit maturity is only 'theoretical' with no demonstrated encoding-variance exploit and unknown production status. This does not meet high/critical bar (requires exploit maturity ≥ PoC or confirmed prod exposure with concrete attack demonstration); it exceeds low because the flaw is real, reachable, and in an identity/authorization-adjacent security boundary. Medium is the accurate calibration.
- Composite score: 4.3
- Environment: unknown
🔎 Evidence: src/authority.rs:296
if root.credential().issuer() != governing_party {
return Err(AuthorityError::RootNotGoverning { ... });
}
🧭 Reachability:
- Network exposure: internal
- Auth barrier: none
- Attack path: EP-001 (verify_chain) — issuer/subject/audience/id fields of attacker-supplied DTGCredential objects flow directly into
==/!=comparisons with no canonicalization step
🔍 Validation Log
- Verdict: ✅ Confirmed True Positive
- Confidence: 90%
- AI Validation Evidence: EVIDENCE FOUND:
if root.credential().issuer() != governing_party(RootNotGoverning check),if link.credential().issuer() != parent.credential().subject()(IssuerNotParentSubject),if audience != presenter(WrongAudience), andmatch (&grant.parent, parent.id()) { (Some(named), Some(presented)) if named == presented => {} ... }(BrokenLink) — all confirmed in src/authority.rs as plain Rust!=/==string comparisons with no visible normalization, case-folding, or Unicode canonicalization- Validation Effort: Cloned repo, read source file, verified vulnerability claim against actual code. Confirmed exploitability in scan context.
⚠️ Must-Review-By-Human (3)
Validated up to a point, but inconclusive — a human must read the code and make the final call. Reported (not dismissed) so developers and the security team receive them.
🟠 Hardcoded Secrets: Generic Api Key (2 occurrences in 1 unique secrets)
| Field | Detail |
|---|---|
| Severity | HIGH |
| Location | src/lib.rs:923 |
| Finding ID | github_pr-ecd571317032 |
| CWE | CWE-798 |
| OWASP | A07:2021 - Identification and Authentication Failures |
| CVSS 4.0 | 8 |
| Exploit Maturity | conceptual |
| Detection Source | mcp_gitleaks |
Summary: 1 unique generic api key secret(s) detected across 2 location(s). Locations: lib.rs:923, lib.rs:955
📝 Description:
Detected 1 unique generic api key secret(s) across 2 locations. These hardcoded secrets could expose access to services and sensitive operations.
🌱 Root Cause: Hardcoded Generic Api Key in source code
🔧 Remediation:
⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.
Priority: Immediate
Remove 2 hardcoded generic api key secret(s) and use environment variables or a secure vault instead.
Also flagged at this location (same code, other weakness framings): UNCONFIRMED — Hardcoded Secret: Generic Api Key in lib.rs:923
🔍 Validation Log
- Verdict:
⚠️ Must-Review-By-Human- Confidence: 85%
- AI Validation Evidence: EVIDENCE FOUND: Finding claims a generic API key secret at src/lib.rs:923 (2 occurrences). The src/lib.rs diff hunks provided show additions related to AuthorityGrant, DTGCredentialError variants, DTGCredentialType, and DTGCommon::authority()/authority_mut() — none of the visible hunks contain anything resembling an API key literal. Line 923 is beyond the visible diff hunks (largest visible line reference is ~640s). EVIDENCE NOT FOUND: The actual byte content at src/lib.rs:923 was not included i
- 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.
⚪ UNCONFIRMED — Hardcoded Secret: Generic Api Key in lib.rs:955
| Field | Detail |
|---|---|
| Severity | INFORMATIONAL |
| Location | src/lib.rs:955 |
| Finding ID | github_pr-70c3b18596b7 |
| CWE | CWE-798 |
| OWASP | A07:2021 - Identification and Authentication Failures |
| CAPEC | CAPEC-561 |
| Reachability | 🔴 Reachable |
| Exploit Maturity | theoretical |
| Detection Source | skill_scan |
Summary: Cannot confirm or deny this finding. The tool flagged a 'Generic Api Key' pattern in src/lib.rs at line 955, but src/lib.rs's actual content is not present anywhere in the sourceCode, diff, or priorArtifacts supplied for this task. Per instructions, only confirmed true positives should be reported with full evidence — this finding lacks the code evidence required for confirmation.
📝 Description:
Cannot be assessed without confirming whether a real secret is present. If confirmed, and given this is a publish = true crate per Cargo.toml, any embedded real secret would become permanently public on crates.io upon the next cargo publish, and would already be exposed in git history if committed.
🧪 Proof of Concept:
No PoC can be constructed without the actual source line and surrounding context. Fabricating a code snippet would misrepresent the codebase and violate evidence-based reporting requirements.
[UNAVAILABLE — file not provided in this analysis session; cannot show surrounding context]
Vulnerable lines: 955, 955
🔁 Reproduction Steps:
- Obtain the actual content of src/lib.rs from the repository at the relevant commit.
- Inspect line 955 and surrounding context (±10 lines) for the matched literal.
- Cross-check whether this occurrence and the one at line 923 belong to the same struct/enum definition (e.g., two related fields in a credential subject type, given the crate's domain of DTG credentials) rather than two independent secrets.
- Classify as true or false positive only after direct inspection; if true, treat as an active incident per remediation guidance below.
🔎 Evidence: src/lib.rs:955
[NOT PROVIDED — src/lib.rs content absent from all supplied artifacts]
💥 Impact:
Cannot be assessed without confirming whether a real secret is present. If confirmed, and given this is a publish = true crate per Cargo.toml, any embedded real secret would become permanently public on crates.io upon the next cargo publish, and would already be exposed in git history if committed.
🧭 Reachability:
- Network exposure: unknown
- Auth barrier: unknown
- Attack path: unknown — cannot trace without src/lib.rs content
⚖️ Triage Factors:
| Factor | Value |
|---|---|
| Fixable | ✅ Yes |
| Exploitability | unknown |
| Business impact | unknown |
| Public exploit | None known |
| Environment | unknown |
Attack scenario: Unconfirmed — gitleaks flagged a second pattern-matched string at src/lib.rs:955 as a possible generic API key, but the actual file content was never supplied, so no realistic exploitation scenario can be grounded in evidence.
🔧 Remediation:
⚠️ AI-generated fix. Review, test in staging, and validate against your architecture before applying.
Same remediation path as VULN-MCP-001: confirm via direct inspection first; if real, rotate the credential, purge from history, and replace with environment-variable or secrets-manager-backed configuration.
Vulnerable code:
[UNKNOWN — requires src/lib.rs content]
Secure code:
[UNKNOWN — requires src/lib.rs content]
Additional recommendations:
- Cross-reference both flagged lines (923 and 955) together when inspecting src/lib.rs, since they may share a root cause (e.g., a shared example/test block or a repeated pattern in derive macro output).
- Add/verify gitleaks runs in CI pre-merge so any future reintroduction is caught before merge rather than post-hoc.
- If false positive (e.g., a field named
api_key: Stringin a#[derive(Serialize, Deserialize)]struct with no literal secret value), add a scoped gitleaks allowlist entry with a comment explaining why, rather than disabling the rule globally.
🔍 Validation Log
- Verdict:
⚠️ Must-Review-By-Human- Confidence: 90%
- AI Validation Evidence: EVIDENCE FOUND: This finding self-reports as UNCONFIRMED, noting src/lib.rs content is absent from all provided artifacts at line 955. EVIDENCE NOT FOUND: Full src/lib.rs content at line 955; the diff hunks provided end well before line 955 (visible hunks reference up to roughly line 640s in the new lib.rs additions). CHANGED VS PRE-EXISTING: src/lib.rs is in the changed-files list making it in-scope, but the specific line content cannot be confirmed. VERDICT JUSTIFICATION: Same reasoning as the
- Validation Effort: This finding was validated up to a point, but the available evidence was insufficient for a conclusive automated verdict. A human (developer / security team) must manually review the code and decide. Not dismissed — treat as an open item pending human review.
Generated by Agentic Sec — AI Security Validation Agent
This report includes full scan data + AI validation evidence. Feed to engineering copilots for automated fix deployment.
Details
🛡️ Threat Model & Affect Analysis — PR #15
| Field | Value |
|---|---|
| Repository | OpenVTC/dtg-credentials |
| Branch | feat/vac-vdc-credentials → main |
| Generated | 2026-09-05 |
ℹ️ This report contains theoretical threats and impact analysis for the MR.
Unlike the Security Code Review Report (which contains confirmed, materialised issues),
these are potential risks that may or may not be exploitable. Use this for defence-in-depth planning.
📋 Affect Analysis
Change Summary
This PR introduces two new credential types — Verifiable Authority Credential (VAC) and Verifiable Delegation Credential (VDC) — along with a new authority-chain verification module (src/authority.rs) that enforces attenuation-only (narrowing) delegation semantics. The core security value is verify_chain, which prevents self-issued/escalated authority grants by requiring a chain to trace back to a governing-party-issued root and enforcing seven narrowing invariants at every link.
Diff: +641 / -6 lines
Types: feature, security, docs, config
🧩 Affected Components
| Component | Impact | Change | What Changed |
|---|---|---|---|
| Authority Chain Verifier (authority.rs) | critical | new | Introduces verify_chain, AuthorityError, VerifiedAuthority, and MAX_CHAIN_DEPTH — a complete new authorization-decision subsystem for valida |
| Credential Construction & Attenuation (create.rs) | high | new | Adds new_vac, new_vdc, and attenuate constructors following the existing new_v* pattern, producing DTGCredential objects consumed by verify_ |
📁 File Classifications
src/authority.rs
- Type: security
src/create.rs
- Type: security
CHANGELOG.md
- Type: docs
Cargo.toml
- Type: config
tests/authority_chain.rs
- Type: test
🛡️ STRIDE Threat Model
Identified Threats (12)
🔴 STRIDE-1: Signature Verification Omission in verify_chain Consumer Integration
| Field | Detail |
|---|---|
| Category | Spoofing, Elevation of Privilege |
| Severity | Critical |
| Likelihood | Likely |
| CVSS | 9.3 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | High |
| CWE | CWE-345,CWE-347,CWE-306 |
| CAPEC | CAPEC-115,CAPEC-194 |
| OWASP | A07:2021 - Identification and Authentication Failures, A01:2021 - Broken Access Control |
Description: authority::verify_chain in COMP-001 allows complete authority forgery due to the function explicitly not verifying cryptographic signatures on any credential in the chain, resulting in unauthorized privilege grants if a caller invokes verify_chain without independently verifying signatures first
Evidence: src/authority.rs:~204-220
/// The signature on each credential is *not* checked here. Verify those first, with\n/// [crate::DTGCredential] and the data-integrity suite; ...\npub fn verify_chain(\n chain: &[DTGCredential],\n governing_party: &str,\n ...\n) -> Result<VerifiedAuthority, AuthorityError> {
Attack Scenario:
- Attacker crafts an arbitrary DTGCredential chain in JSON with a fabricated root claiming issuer == governing_party and a leaf granting themselves broad actions/scope, without ever holding a valid private key.
- Attacker's integrating application deserializes the JSON into
Vec<DTGCredential>and passes it directly toverify_chain(chain, governing_party, requested_scope, requested_action, presenter, at)in src/authority.rs without first calling the separate data-integrity/signature verification suite mentioned only in the doc comment ('The signature on each credential is not checked here...'). verify_chainwalks the structural rules only — BrokenLink, IssuerNotParentSubject, WidensScope/Actions, OutlivesParent — none of which touch cryptographic proof, and the attacker-controlled chain satisfies every structural invariant because the attacker constructs both leaf and root to be self-consistent.RootNotGoverningcheck at the bottom comparesroot.credential().issuer() != governing_partyas a plain string equality on an unauthenticated claim field, which the attacker sets verbatim to the expected governing party name.verify_chainreturnsOk(VerifiedAuthority{...}), and the calling application grants the requested access believing the chain was cryptographically valid.- Attacker has now obtained full impersonation of the governing party's issued authority with zero valid signatures.
🔎 Threat Clue: Derived from COMP-001, COMP-002 via EP-001
- Data Flows: DF-authority-chain-verification
Preconditions: Integrating application calls verify_chain without a preceding mandatory signature-verification step, Attacker can supply arbitrary DTGCredential JSON/objects as chain input, No structural or type-level enforcement in this module linking a 'verified' credential type to a 'checked' state
Existing Controls: Extensive doc comment in src/authority.rs explicitly warning integrators that signatures are not checked here and must be verified separately
Recommended Mitigations: Introduce a type-state wrapper (e.g., VerifiedCredential<T>) that can only be constructed after signature verification, and require verify_chain to accept only that wrapped type instead of raw DTGCredential • Add a runtime assertion/callback hook inside verify_chain requiring a signature-verification closure to be passed and invoked per link • Add integration tests and CI checks in tests/authority_chain.rs (not shown) asserting that unsigned or invalid-signature chains are rejected end-to-end • Document this as a hard API contract with a #[must_use]-style compile-time gate rather than doc-comment-only guidance
🟠 STRIDE-2: String-Based Identity Comparison Enabling Homoglyph/Case Spoofing in RootNotGoverning Check
| Field | Detail |
|---|---|
| Category | Spoofing, Elevation of Privilege |
| Severity | High |
| Likelihood | Possible |
| CVSS | 7.1 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Medium |
| CWE | CWE-178,CWE-697 |
| CAPEC | CAPEC-627,CAPEC-165 |
| OWASP | A07:2021 - Identification and Authentication Failures |
Description: verify_chain in COMP-001 allows governing-party impersonation due to plain != string equality checks (RootNotGoverning, IssuerNotParentSubject, WrongAudience) on attacker-supplied identifier strings without normalization, resulting in a spoofed root or issuer bypassing the intended trust boundary if the identity strings are DIDs/URIs subject to case, Unicode, or percent-encoding variance
Evidence: src/authority.rs:~296-301
if root.credential().issuer() != governing_party {\n return Err(AuthorityError::RootNotGoverning { ... });\n}
Attack Scenario:
- Governing party's canonical identifier is registered/expected as e.g.
did:web:Example.comin the caller'sgoverning_partyparameter. - Attacker issues (or claims to issue) a root credential where
issuerfield is a visually or byte-level distinct but application-equivalent string (differing case, trailing slash, percent-encoded characters, or Unicode confusable) that the upstream identity resolver treats as equivalent but Rust's!=treats as different — or conversely, if the comparison side used by the caller is inconsistent, the two-sided mismatch could be exploited to smuggle a distinct-but-equivalent issuer past logging/detection while still passing the literal check used elsewhere in the codebase. if root.credential().issuer() != governing_partyin src/authority.rs performs rawPartialEqon&str/Stringwith no canonicalization (case-folding, Unicode NFC, DID method-specific normalization).- Similarly,
IssuerNotParentSubjectandWrongAudiencechecks use the same raw equality against attacker-controlledissuer/subject/audiencefields propagated from deserialized JSON. - Depending on how the surrounding system resolves/compares these identifiers elsewhere (e.g., DID resolution, case-insensitive lookups in a directory), an attacker can craft a credential chain whose string fields pass
verify_chain's literal comparison while representing a different real-world identity than intended, or vice versa causing legitimate chains to be wrongly rejected (functional/DoS side-effect).
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: DF-authority-chain-verification
Preconditions: Identity strings (issuer/subject/audience/governing_party) are DIDs or URIs with a normalization scheme not enforced before comparison, Attacker controls or influences credential JSON fields prior to verify_chain invocation, Some other component in the system treats two distinct string representations as the same identity
Existing Controls: Use of typed String fields with PartialEq provides deterministic (if strict) comparison • thiserror-based error variants make mismatches visible in logs for post-hoc detection
Recommended Mitigations: Normalize all identity strings (case-folding, NFC Unicode normalization, DID-method-specific canonicalization) before any equality comparison in verify_chain • Introduce a dedicated Did/Identifier newtype with a custom PartialEq enforcing canonical comparison • Add fuzz/property tests feeding Unicode-confusable and case-variant identifiers into verify_chain • Document the exact identifier format contract expected by verify_chain callers
🟡 STRIDE-3: Algorithmic Complexity Amplification via Repeated Nested Action-List Scan in Chain Walk
| 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-405,CWE-1050 |
| CAPEC | CAPEC-131,CAPEC-25 |
| OWASP | A04:2021 - Insecure Design |
Description: verify_chain in COMP-001 allows moderate resource exhaustion due to an O(depth × actions²) nested .contains() scan over attacker-controlled actions Vec at each of up to MAX_CHAIN_DEPTH=8 links, resulting in increased per-call CPU cost when actions lists are maximally sized (bounded but non-trivial amplification, especially if actions strings are also long)
Evidence: src/authority.rs:~275-281
for action in &grant.actions {\n if !parent_grant.actions.contains(action) {\n return Err(AuthorityError::WidensActions { index, action: action.clone() });\n }\n}
Attack Scenario:
- Attacker constructs a chain at the maximum permitted depth (8 links, per MAX_CHAIN_DEPTH) where every link's
actionsVec contains a large number of long strings. - Attacker ensures each
actionslist is crafted so that many entries must be checked before the!parent_grant.actions.contains(action)linear scan concludes (worst-case: no match found until end of list), repeated for every action in every link at every depth level. for action in &grant.actions { if !parent_grant.actions.contains(action) {...} }in src/authority.rs runs this nested linear scan once per link, with.contains()itself doing a String comparison per candidate — total cost scales with (depth × actions_per_link × actions_per_parent × avg_string_length).- Attacker submits many such maximally-sized chains concurrently to a service exposing verify_chain (e.g., an API endpoint gating on EP-001), amplifying CPU consumption disproportionately to network/request cost, since chain size is not otherwise bounded beyond depth.
- Aggregate CPU exhaustion degrades verifier availability for legitimate credential holders.
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: DF-authority-chain-verification
Preconditions: No upper bound enforced on the size of actions Vec per link or on string length within actions, verify_chain is reachable at sufficient request volume by an unauthenticated or lightly-authenticated caller, Attacker can supply crafted DTGCredential objects with large actions lists (constructor in src/create.rs only forbids empty lists, not oversized ones per visible diff)
Existing Controls: MAX_CHAIN_DEPTH = 8 bounds the number of links, limiting the outer multiplier • NoActions check rejects empty lists, but not oversized ones
Recommended Mitigations: Enforce a maximum length on actions Vec and a maximum string length per action at credential construction and/or deserialization boundary • Replace Vec<String> + linear .contains() with a HashSet<String> for O(1) membership checks when actions lists can be large • Add request-level rate limiting and payload-size limits ahead of verify_chain in the hosting service • Add a fuzz/benchmark test asserting verify_chain completes within an SLA bound for maximal-size adversarial input
🟡 STRIDE-4: Missing Duplicate/Cycle Detection Enabling Repeated-ID Chain Confusion
| Field | Detail |
|---|---|
| Category | Tampering |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 6.3 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-706,CWE-841 |
| CAPEC | CAPEC-141,CAPEC-462 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
Description: verify_chain in COMP-001 allows chain-integrity confusion due to the algorithm only checking adjacent-link parent-pointer consistency (BrokenLink) without validating global uniqueness of id values across the presented chain, resulting in potential logical inconsistency if an attacker supplies a chain containing repeated or self-referential id values that satisfy pairwise adjacency checks
Evidence: src/authority.rs:~247-263
match (&grant.parent, parent.id()) {\n (Some(named), Some(presented)) if named == presented => {}\n ...
Attack Scenario:
- Attacker builds a chain where
chain[2].id()equalschain[0].id()(or some other non-adjacent duplication), while every adjacent pair still satisfiesgrant.parent == parent.id()for the immediate neighbor per the BrokenLink check in src/authority.rs. - Because verify_chain only compares
chain[index]againstchain[index+1](immediate neighbor), it never detects that anidreused elsewhere in the chain could represent the attacker inserting a link that structurally 'points backward' into an earlier position once combined with how a consuming system might cache or deduplicate credentials byid. - If the surrounding system (not shown, in src/lib.rs or a credential store) indexes/caches credentials by
idand later re-resolves them by that key rather than relying purely on the presented slice, the duplicateidcould cause it to substitute a different (attacker-controlled) credential object at lookup time than the one verify_chain actually validated in-slice, breaking the 'never dereferences parent' invariant asserted in the module doc. - This creates a verification/enforcement mismatch: verify_chain's guarantees apply only to the exact objects in the slice, but downstream code that re-keys by
idcould inadvertently substitute a different, unverified object, defeating the intended security model.
🔎 Threat Clue: Derived from COMP-001, COMP-002 via EP-001
- Data Flows: DF-authority-chain-verification
Preconditions: A calling system caches, indexes, or re-resolves credentials by id after verify_chain returns success, rather than acting only on the exact validated slice, Attacker can produce two distinct DTGCredential objects sharing the same id value, Chain length ≥ 3 so non-adjacent duplication is structurally possible within MAX_CHAIN_DEPTH
Existing Controls: BrokenLink check enforces strict adjacent parent/id linkage • Module doc explicitly states resolution is bearer-side and parent is never dereferenced by this function itself
Recommended Mitigations: Add an explicit uniqueness check across all id values in the presented chain before/while walking it, rejecting chains with duplicate ids • Document clearly that any caller must use only the exact validated slice object references post-verification and must not re-resolve by id • Add regression tests in tests/authority_chain.rs covering duplicate-id and cyclic chain inputs
🟠 STRIDE-5: Clock-Trust Manipulation via Caller-Supplied at Parameter Bypassing Validity Windows
| Field | Detail |
|---|---|
| Category | Tampering, Elevation of Privilege |
| Severity | High |
| Likelihood | Likely |
| CVSS | 7.6 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-294,CWE-367 |
| CAPEC | CAPEC-90,CAPEC-462 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
Description: verify_chain in COMP-001 allows expired/not-yet-valid credential acceptance due to the validity-window check trusting an entirely caller-supplied at: DateTime<Utc> parameter instead of an authoritative system clock, resulting in bypass of temporal authority constraints (OutlivesParent, NotValidNow) if the calling integration passes an attacker-influenced or stale timestamp
Evidence: src/authority.rs:~215-235
if c.valid_from() > at {\n return Err(AuthorityError::NotValidNow { index, at });\n}\nif let Some(until) = c.valid_until()\n && until < at\n{\n return Err(AuthorityError::NotValidNow { index, at });\n}
Attack Scenario:
- Attacker holds an authority credential that has since expired (
valid_untilin the past relative to true current time) or a chain link whose parent has since expired. - The integrating application derives the
atparameter from an untrusted or attacker-influenceable source — e.g., a client-supplied request timestamp, a replayed cached value, or a server clock vulnerable to NTP manipulation — rather than always usingUtc::now()freshly at verification time. - Attacker submits (or replays) a request causing the caller to invoke
verify_chain(chain, governing_party, scope, action, presenter, at)with anatvalue set to a moment when the now-expired credential was still valid. if c.valid_from() > atandif until < atchecks in src/authority.rs pass becauseatreflects the attacker-favorable past instant rather than true present time.- verify_chain returns
Ok(VerifiedAuthority{...})for a chain that should be expired, and the caller grants access based on stale authority.
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: DF-authority-chain-verification
Preconditions: Calling application does not strictly bind at to a freshly-sampled, trusted Utc::now() at the moment of authorization decision, Attacker can influence or replay the at value supplied to verify_chain (e.g., via a request parameter, cached session state, or clock skew), Credential in question has since expired but was valid at the attacker-chosen at
Existing Controls: Validity window is checked per-link, not just once for the whole chain, narrowing (but not eliminating) the exposure window • OutlivesParent check prevents extending validity beyond the parent regardless of at
Recommended Mitigations: Remove at as a caller-supplied parameter and instead have verify_chain internally call Utc::now(), or clearly document that callers MUST pass only a freshly-sampled trusted timestamp and never persist/replay it • Add monotonic/replay protection at the calling layer to prevent reuse of a stale at value across multiple authorization decisions • Add integration tests asserting that credentials expired relative to true wall-clock time are rejected even under adversarial at manipulation attempts at the calling layer
🟡 STRIDE-6: Insufficient Structured Audit Logging of Rejected Authority Chains at verify_chain Boundary
| Field | Detail |
|---|---|
| Category | Repudiation |
| Severity | Medium |
| Likelihood | Likely |
| CVSS | 4.8 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:L/SA:N |
| Residual Severity | Low |
| CWE | CWE-778,CWE-223 |
| CAPEC | CAPEC-93 |
| OWASP | A09:2021 - Security Logging and Monitoring Failures |
Description: verify_chain in COMP-001 allows undetected repeated escalation attempts due to the function returning a typed Result<_, AuthorityError> with no built-in audit logging, structured event emission, or rate-limited alerting hook, resulting in a lack of non-repudiable evidence and delayed detection if a caller does not independently log every AuthorityError occurrence with sufficient context
Evidence: src/authority.rs:~55-60
/// Each variant names a specific way of acquiring authority that was not granted, rather\n/// than collapsing into one \"invalid\" — a verifier's logs are where an escalation attempt\n/// becomes visible.
Attack Scenario:
- Attacker repeatedly submits crafted chains attempting privilege escalation (e.g., WidensScope, WidensActions, RootNotGoverning) against a service embedding verify_chain.
- Each attempt returns a distinct, well-described
AuthorityErrorvariant (e.g.,RootNotGoverning{root_issuer, expected}) from src/authority.rs, but the module itself performs no logging — it purely returns a value. - If the calling application does not itself log every
Err(AuthorityError::*)return with correlated identity (presenter, requested scope/action, timestamp) to an immutable/append-only audit trail, the attacker's escalation attempts leave no forensic trace beyond possibly a generic application error log. - Attacker can iterate many chain variations (probing for the exact rule triggering rejection, since each error variant reveals precisely which invariant failed) without detection, using the granular error messages themselves as an oracle to refine subsequent forged chains.
- Without structured logging tied to identity, security teams cannot distinguish a single misconfigured legitimate client from a sustained, deliberate escalation-probing campaign.
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: DF-authority-chain-verification
Preconditions: Calling application does not implement mandatory structured audit logging for every AuthorityError outcome, No rate limiting or anomaly detection layered on repeated verify_chain failures for a given presenter/identity
Existing Controls: Highly granular AuthorityError enum (14 variants) provides rich diagnostic detail if logged • Doc comment explicitly notes verifier logs are 'where an escalation attempt becomes visible', signaling intended design reliance on caller-side logging
Recommended Mitigations: Provide an optional tracing/logging hook or callback parameter in verify_chain (or a wrapping function) that emits structured audit events on every Err path with presenter/scope/action context • Document as a mandatory integration requirement that all AuthorityError returns be logged to an append-only, tamper-evident audit store • Add anomaly detection/alerting on repeated distinct AuthorityError types from the same presenter within a short time window
🔵 STRIDE-7: Information Disclosure via Verbose AuthorityError Detail Fields Exposed to Untrusted Presenters
| Field | Detail |
|---|---|
| Category | Information Disclosure |
| Severity | Low |
| Likelihood | Likely |
| CVSS | 3.5 CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-209,CWE-200 |
| CAPEC | CAPEC-54 |
| OWASP | A09:2021 - Security Logging and Monitoring Failures |
Description: AuthorityError variants in COMP-001 allow internal identity/topology disclosure due to error messages embedding raw internal identifiers (root_issuer, expected governing party, parent subject, actual audience) via #[error(...)] thiserror formatting, resulting in information disclosure to an unauthenticated presenter if these error strings are returned verbatim in an API response rather than being sanitized for external consumption
Evidence: src/authority.rs:~110-120
#[error(\n \"chain root was issued by `{root_issuer}`, not by `{expected}` which governs the scope\"\n)]\nRootNotGoverning { root_issuer: String, expected: String },
Attack Scenario:
- Attacker presents a deliberately malformed or mismatched chain to a service wrapping verify_chain (EP-001), e.g., omitting an audience match or naming a wrong parent.
- verify_chain returns
Err(AuthorityError::WrongAudience{index, audience, presenter})orErr(AuthorityError::IssuerNotParentSubject{index, issuer, subject}), whoseDisplay(via thiserror#[error(...)]) embeds the actual internalaudience,subject, orgoverning_partyidentifiers that were expected. - If the calling web service or API surfaces this Display string directly in an HTTP error response body (a common anti-pattern for thiserror-derived errors), the attacker learns legitimate internal identifiers (e.g., the exact DID of the governing party or a legitimate subject) that were not previously known.
- Attacker uses these disclosed identifiers to refine subsequent forged credential chains (e.g., now targeting the correct
governing_partystring discovered from the error) or to conduct social engineering / further enumeration against the disclosed identities.
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: DF-authority-chain-verification
Preconditions: Calling application propagates AuthorityError's Display/Debug output directly into an external-facing response without sanitization, Attacker has any ability to trigger verify_chain with attacker-influenced but distinguishable inputs (e.g., varying the wrong-audience/issuer fields) to enumerate distinct error responses
Existing Controls: Errors are strongly typed enum variants rather than free-form strings, enabling a caller to easily map to sanitized external messages if they choose to • thiserror derives standard Display formatting only, not automatic logging or network exposure
Recommended Mitigations: Document that AuthorityError Display strings are for internal diagnostic/logging use only and MUST NOT be returned verbatim in external API responses • Provide a sanitized external_message() method or a From-conversion to a generic 'access denied' error type for public-facing responses • Add a code-review/lint rule or test asserting no route handler directly serializes AuthorityError text to end-users
🟡 STRIDE-8: Unbounded Attenuation via DTGCredential::attenuate Absent Depth/Rate Enforcement Prior to Verification
| Field | Detail |
|---|---|
| Category | Denial of Service, Tampering |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.9 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-770,CWE-400 |
| CAPEC | CAPEC-125,CAPEC-130 |
| OWASP | A04:2021 - Insecure Design |
Description: DTGCredential::attenuate in COMP-002 allows creation of maximal-depth or resource-heavy chains prior to any verify_chain enforcement due to attenuate itself (per available evidence in src/create.rs diff) not visibly enforcing MAX_CHAIN_DEPTH or actions-list-size limits at construction time, resulting in proliferation of large/deep chains that only fail at the final verify_chain step, wasting resources and enabling scenario STRIDE-3 amplification if attenuate is exposed as a network-facing operation
Evidence: src/create.rs:241-404 (truncated diff)
/// Creates a new Verifiable Authority Credential (VAC) — a chain root.\n/// ... `actions` MUST NOT be empty ...\n(attenuate() body not shown in provided diff/source)
Attack Scenario:
- A legitimate or attacker-controlled holder repeatedly calls
DTGCredential::attenuate(...)(EP-004) on a VAC they hold, each time producing a new narrower-looking but still large credential object. - Because the depth/size constraints (MAX_CHAIN_DEPTH=8, non-empty actions) are enforced only inside
verify_chain(per the provided authority.rs) and the create.rs diff shows only an empty-actions constructor check, nothing stops a holder from generating and storing/transmitting arbitrarily many attenuated credentials or chains that exceed practical depth before any verifier ever rejects them. - Attacker floods a credential-issuance or storage endpoint with such over-generated attenuated credentials, consuming storage/bandwidth resources, and only at the eventual verify_chain call (if ever reached) are they rejected with
TooDeep. - This produces an asymmetric cost: cheap for attacker to generate, but each stored/transmitted artifact and eventual (possibly never-triggered) verification consumes disproportionate downstream resources.
🔎 Threat Clue: Derived from COMP-002 via EP-004
- Data Flows: DF-credential-attenuation
Preconditions: attenuate() is reachable by a holder without a pre-check against MAX_CHAIN_DEPTH or without validating resulting chain size before storage/transmission, No storage-layer or transport-layer size/depth limit exists ahead of verify_chain, src/create.rs full content not available for definitive confirmation — inferred from truncated diff showing only an empty-actions check for the new VAC/VDC constructors
Existing Controls: verify_chain enforces MAX_CHAIN_DEPTH=8 as a hard backstop at the trust-decision point • Constructor rejects empty actions lists (per diff notes) preventing one particular meaningless-grant case
Recommended Mitigations: Add depth/size validation directly inside DTGCredential::attenuate (or a pre-attenuation check against the accumulating chain) so oversized chains are rejected at creation time, not only at verification time • Enforce storage/transport-layer size caps on credential chain artifacts ahead of any verify_chain call • Add tests in tests/authority_chain.rs asserting attenuate() itself refuses to produce a chain that would exceed MAX_CHAIN_DEPTH
🟡 STRIDE-9: Type-Confusion Bypass via Non-Exhaustive DTGCredentialType Matching in Downstream Callers
| Field | Detail |
|---|---|
| Category | Tampering, Elevation of Privilege |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.1 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-440,CWE-697 |
| CAPEC | CAPEC-698 |
| OWASP | A08:2021 - Software and Data Integrity Failures |
Description: DTGCredentialType (COMP-002) marked #[non_exhaustive] allows silent authorization bypass in downstream consumers due to wildcard match arms added before the Authority/Delegation variants existed, resulting in new VAC/VDC credentials being mishandled (e.g., treated as an unrecognized/default case) by any pre-existing caller code that pattern-matches with a catch-all _ => arm predating this 0.6.0 release
Evidence: CHANGELOG.md:~48-50
- `DTGCredentialType` is `#[non_exhaustive]`, so the two new variants are not a breaking\n change for callers matching with a wildcard arm. Callers matching exhaustively need one arm\n each.
Attack Scenario:
- Prior to 0.6.0, downstream application code pattern-matches on
DTGCredentialTypewith an exhaustive-looking match but a wildcard_ => default_behavior()arm, per the#[non_exhaustive]design noted in the CHANGELOG ('not a breaking change for callers matching with a wildcard arm'). - After upgrading the dtg-credentials dependency to 0.6.0, new credentials of type
DTGCredentialType::Authorityor::Delegationbegin flowing through the system (e.g., presented at EP-002/EP-003). - The unmodified downstream
_ =>arm silently absorbs these new variants into whatever default behavior existed before Authority/Delegation types were introduced — potentially treating a VAC as an unrecognized/generic credential type and skipping type-specific authorization logic that assumes only the older variants exist. - If that default arm is permissive (e.g., 'treat unknown type as basic, allow read') rather than fail-closed, an attacker presenting a VAC/VDC could have it processed under weaker rules than the newly-introduced Authority-specific rules intend, effectively bypassing the entire authority-chain verification requirement this PR introduces if the caller never routes Authority-typed credentials into
verify_chainat all. - This is a supply-chain / dependency-upgrade integration hazard rather than a bug in authority.rs itself, but it directly undermines the security value this PR delivers if downstream integration is not updated in lockstep.
🔎 Threat Clue: Derived from COMP-002 via EP-002, EP-003
- Data Flows: DF-credential-issuance
Preconditions: Downstream code depends on dtg-credentials and matches on DTGCredentialType with a non-exhaustive wildcard arm, That wildcard arm's default behavior is permissive rather than fail-closed, Downstream code is not updated to explicitly handle Authority/Delegation after upgrading to 0.6.0
Existing Controls: CHANGELOG explicitly documents the non-exhaustive design and calls out the compatibility implication for exhaustive matchers • authority.rs itself defensively checks matches!(link.type_(), DTGCredentialType::Authority) rather than relying on caller pre-filtering
Recommended Mitigations: Recommend (in migration docs) that all downstream matches on DTGCredentialType use fail-closed default arms (reject/deny) rather than permissive defaults • Add a deprecation-cycle warning or clippy lint suggestion for exhaustive-with-wildcard matches on this enum in consuming crates • Provide a migration guide item explicitly calling out the Authority/Delegation addition as security-relevant, not just an API compatibility note
🟡 STRIDE-10: Panic-Based Denial of Service via Violated Internal Invariant in expect() Calls
| Field | Detail |
|---|---|
| Category | Denial of Service |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.9 CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-248,CWE-617 |
| CAPEC | CAPEC-153 |
| OWASP | A04:2021 - Insecure Design |
Description: verify_chain in COMP-001 allows process-crashing denial of service due to reliance on .expect("checked above") calls that assume a prior matches!/ok_or_else check guarantees authority() returns Some, resulting in an unwrap panic and potential service crash if any code path allows a chain element to reach these lines without having actually passed the per-element loop (e.g., future refactors, feature-flag divergence, or an untested DTGCredentialType::Authority value whose credential().authority() legitimately returns None due to a construction/deserialization edge case not covered by tests/authority_chain.rs)
Evidence: src/authority.rs:~241-244, 290-292
let leaf_grant = leaf.credential().authority().expect(\"checked above\");\n...\nlet root_grant = root.credential().authority().expect(\"checked above\");
Attack Scenario:
- Attacker (or a future code change) introduces a data path where a
DTGCredentialreportstype_() == DTGCredentialType::Authority(passing thematches!check) but itscredential().authority()returnsNonein some malformed/edge-case deserialization state not covered by the initial per-element validation loop — for example, via direct construction throughDTGCommon::authority_mutmentioned in the CHANGELOG as existing specifically so 'tests can build chains attenuate would refuse.' - If any future refactor reorders the per-element loop relative to the leaf/walk sections, or if a code path calls
leaf_grant/grant/parent_grant.expect("checked above")(multiple occurrences in src/authority.rs) on an element that bypassed the initial loop'sok_or_elsecheck due to such a refactor or an alternate entry path, the.expect()panics. - In a service context (e.g., an async request handler), an unhandled panic in a thread without
catch_unwindcan crash the worker thread/task, and if replicated across concurrent requests, can degrade or crash the verifying service. - Because
DTGCommon::authority_mutis explicitly documented as existing to let tests construct otherwise-invalid states, the invariant that 'a chain passing the type check always has Some(authority())' is only as strong as the code paths that reach verify_chain, and is not enforced by the type system itself (stillOption<AuthorityGrant>, not a non-optional field).
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: DF-authority-chain-verification
Preconditions: A future code change, alternate entry point, or malformed construction path allows a DTGCredentialType::Authority-tagged credential to reach the walk/root sections without passing the initial per-element loop's authority()-is-Some check, No panic-catching/isolation boundary (e.g., catch_unwind, process-per-request isolation) around verify_chain invocation in the hosting service
Existing Controls: Current control flow does perform the ok_or_else check for every element in the initial loop before any .expect() call is reached, so under the code as currently written, the invariant holds • thiserror-derived error handling elsewhere avoids panics for all expected/normal error conditions
Recommended Mitigations: Replace .expect("checked above") calls with explicit error propagation (ok_or_else(|| AuthorityError::NotAuthority{...})?) to eliminate reliance on cross-loop invariants entirely, making the function panic-free under all inputs by construction • Add a regression test explicitly constructing (via authority_mut) a credential that is type Authority but has authority() == None, feeding it into every code path of verify_chain to confirm no panic occurs • Wrap verify_chain invocation at the service boundary with panic isolation (catch_unwind or per-request task isolation) as defense-in-depth
🟡 STRIDE-11: Audience-Binding Bypass via Absent Audience Enforcement on Non-Leaf Chain Links
| Field | Detail |
|---|---|
| Category | Spoofing |
| 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-287,CWE-863 |
| CAPEC | CAPEC-593 |
| OWASP | A01:2021 - Broken Access Control |
Description: verify_chain in COMP-001 allows unauthorized presentation of intermediate credentials due to the WrongAudience check being applied only to chain[0] (the leaf) via leaf_grant.audience, resulting in bypass of audience binding for any non-leaf link that separately sets its own audience field, since intermediate links' audience constraints are never validated against presenter
Evidence: src/authority.rs:~204-212
let leaf = &chain[0];\nlet leaf_grant = leaf.credential().authority().expect(\"checked above\");\nif let Some(audience) = &leaf_grant.audience\n && audience != presenter\n{\n return Err(AuthorityError::WrongAudience { index: 0, ... });\n}
Attack Scenario:
- A legitimate credential holder A obtains an intermediate VAC (position 1..n-1 in a future chain) that is audience-bound to a specific delegate D via that intermediate link's own
audiencefield, intended so only D may ever present a chain containing that link. - Attacker (not D) somehow obtains this intermediate link plus a compatible leaf/child link chaining from it (e.g., via credential leakage, insider threat, or a compromised storage system) and constructs a full chain with themselves as
presenter. verify_chainin src/authority.rs only checksif let Some(audience) = &leaf_grant.audience && audience != presenteragainstchain[0]— no equivalent check exists in the leaf-to-root walking loop forparent_grant.audienceor any intermediate link's own audience field.- The attacker's chain passes verify_chain despite the intermediate link being audience-bound to someone else, because only the leaf's audience binding is enforced, not every link's.
- Attacker obtains a
VerifiedAuthorityresult and is granted the scope/actions the chain confers, defeating the audience-binding protection for all non-leaf positions.
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: DF-authority-chain-verification
Preconditions: A chain contains an intermediate (non-leaf) link with its own audience field set to a value other than the current presenter, Attacker has obtained such an intermediate link plus a valid child link chaining from it, through leakage or a compromised distribution channel, Design intent (per module doc: 'audience, where set, must be the presenter') is understood to apply to every link, not only the leaf, but the code only enforces it for chain[0]
Existing Controls: Leaf audience binding is enforced, mitigating the most common single-hop leaked-credential replay case • IssuerNotParentSubject check still requires correct issuer/subject chaining regardless of audience
Recommended Mitigations: Extend the per-link loop (or the initial per-element loop) to check grant.audience against presenter for every link in the chain, not only the leaf • Add a specific regression test in tests/authority_chain.rs presenting a chain where an intermediate (non-leaf) link is audience-bound to a different party than the presenter, asserting rejection • Clarify the module doc/spec draft (PR #29) on whether audience binding is leaf-only by design or should apply chain-wide, and align code to that explicit decision
🟡 STRIDE-12: Scope String Ambiguity Enabling Hierarchical Scope Confusion in WidensScope Check
| Field | Detail |
|---|---|
| Category | Tampering, Elevation of Privilege |
| Severity | Medium |
| Likelihood | Possible |
| CVSS | 5.7 CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N |
| Residual Severity | Low |
| CWE | CWE-863,CWE-697 |
| CAPEC | CAPEC-680 |
| OWASP | A01:2021 - Broken Access Control |
Description: verify_chain in COMP-001 allows scope-narrowing bypass due to scope being an opaque String compared with exact != equality (grant.scope != parent_grant.scope) rather than a structured hierarchical scope model, resulting in potential authority misapplication if the calling system treats scope strings as hierarchical/prefix-matched (e.g., org/team implying access to org/team/sub) while verify_chain enforces only byte-for-byte equality, creating a mismatch between the verifier's narrowing guarantee and the resource server's actual authorization semantics
Evidence: src/authority.rs:~267-273
if grant.scope != parent_grant.scope {\n return Err(AuthorityError::WidensScope { index, scope: grant.scope.clone(), parent_scope: parent_grant.scope.clone() });\n}
Attack Scenario:
- Resource server / policy enforcement point treats
scopestrings hierarchically, e.g., granting access to any resource path prefixed by the scope string (a common real-world pattern for path- or namespace-based scopes). - Root VAC issued by governing party sets
scope = \"org/finance\". A legitimate attenuated child setsscope = \"org/finance\"too (unchanged, passes WidensScope's exact equality), which is expected and fine. - However, because verify_chain's WidensScope check is exact-string equality rather than hierarchy-aware, it cannot distinguish 'same scope, no widening' from 'differently-formatted but semantically identical or overlapping scope' — an attacker who can influence how scope strings are serialized (e.g., trailing slash
org/finance/vsorg/finance, or case) could potentially get a chain accepted by verify_chain (because the string happens to match through some formatting coincidence introduced upstream) that the resource server then interprets more broadly than the governing party intended, since the two systems (verifier and resource server) do not share the same scope-comparison semantics. - This is a semantic mismatch/confusion vulnerability: verify_chain provides only exact-match narrowing, but says nothing about how consuming policy engines interpret the resulting scope string, allowing scope semantics to diverge from the intended strict narrowing guarantee across system boundaries.
🔎 Threat Clue: Derived from COMP-001 via EP-001
- Data Flows: DF-authority-chain-verification
Preconditions: Downstream policy enforcement treats scope as hierarchical/prefix-matched rather than as an exact opaque token, Scope string formatting is not strictly canonicalized before being set into a credential (e.g., trailing separators, case), Attacker has some influence over scope string formatting during credential issuance or attenuation (e.g., via a shared library bug or user-supplied scope names)
Existing Controls: Exact-string WidensScope check does correctly prevent all literal scope changes across a link boundary • Design intent (per module doc) is explicit that scope must not widen, which the exact-match implementation correctly enforces at the string level
Recommended Mitigations: Canonicalize scope strings (e.g., strip trailing separators, enforce case, define a formal grammar) at credential construction time so exact-match semantics align with any hierarchical interpretation used downstream • Document explicitly that scope is treated as an opaque, exact-match token by this verifier and that any hierarchical interpretation must be implemented consistently by the resource server using the same canonicalization • Add integration tests spanning both the credential library and a representative policy-enforcement consumer to catch scope-semantics drift
🍝 PASTA Threat Model
Application Purpose
dtg-credentials is a Rust library implementing Decentralized Trust Graph (DTG) verifiable credentials, and this PR adds Verifiable Authority Credentials (VAC) and Verifiable Delegation Credentials (VDC) with a chain-verification function so that decentralized systems can safely delegate and attenuate scoped authority without any party being able to self-grant privileges.
Inherent Risks
- The library explicitly defers signature verification to a separate, unshown data-integrity suite, creating an integration-order dependency that consumers may get wrong.
- Trust decisions rely on caller-supplied strings (governing_party, presenter, timestamps) whose authenticity and normalization are outside this module's control.
- The module is explicitly tied to unapproved, moving draft specifications (trustoverip/dtgwg-cred-spec PR #29 and ci: check for an already-published version before authenticating #19), so the security model itself may change before stabilization.
- Full source (src/lib.rs, complete src/create.rs, tests/authority_chain.rs) was not available for review, limiting confidence in whether upstream callers correctly invoke verify_chain and signature verification together.
Objectives
Risk: Treat any bypass of the seven documented chain-narrowing invariants as a critical risk equivalent to full authority forgery.
Business: Enable decentralized, cryptographically-grounded delegation of authority between organizations, agents, and sub-agents without a central authorization server.
Security: Guarantee that no chain of attenuations can ever grant more authority than the governing party's root credential conferred.; Ensure verification never depends on network resolution of parent credentials (bearer-side resolution only).
Financial: Avoid costly security incidents (privilege escalation, credential forgery) that would damage trust in the DTG credential ecosystem and its adopters.
Compliance: Align eventual behavior with the trustoverip/dtgwg-cred-spec draft (PR #29, #19) once ratified, without introducing security regressions during the draft period.
Functional: Provide constructors for VAC/VDC credentials and a chain-attenuation mechanism (attenuate) plus a verifier (verify_chain) that enforces narrowing invariants.
Operational: Keep chain verification performant and bounded (MAX_CHAIN_DEPTH) so it can run on every credential presentation without becoming an availability bottleneck.
Business Impact Analysis (2)
BIA-1: Authority Chain Verification Service (Critical)
The end-to-end process by which a credential holder presents an authority chain and a relying party calls verify_chain (plus separate signature verification) to decide whether to grant a requested scope/action.
MTD: 00 days 04:00 hours | RTO: 00 days 01:00 hours | RPO: 00 days 00:15 hours
- Stakeholders: Credential Holders / Governing Parties / Relying Parties / Security/Compliance Team
- Dependencies: chrono crate for timestamp handling / serde/serde_json for credential deserialization / thiserror for error typing / Unshown signature/data-integrity verification suite / Unshown src/lib.rs and src/create.rs full implementation
- Disruptions: Signature verification step is skipped or misordered by an integrating application / Chain depth or actions-list size is used to exhaust verifier CPU / Caller supplies a manipulated or stale
attimestamp / Non-exhaustive enum matching in downstream code silently mishandles new credential types - Impacts: Complete authority forgery allowing unauthorized access to any scope/action (financial and reputational) / Denial of service against the verification path degrading availability of every relying party using it / Regulatory/compliance exposure if the credential system is used for access to regulated resources
BIA-2: Credential Attenuation Issuance (High)
The process by which a credential holder derives a narrower VAC from one they hold via DTGCredential::attenuate, without contacting the original issuer.
MTD: 01 days 00:00 hours | RTO: 00 days 04:00 hours | RPO: 00 days 01:00 hours
- Stakeholders: Credential Holders / Agent/Sub-Agent Delegates / Relying Parties
- Dependencies: src/create.rs constructor logic / AuthorityGrant structure
- Disruptions: Attenuate produces oversized or maximal-depth chains that are only rejected later at verify_chain / Attenuate lacks the same rigor as verify_chain for enforcing narrowing at creation time
- Impacts: Resource waste from over-generated invalid chains / Delayed detection of misuse until the verification boundary
Technical Scope
Roles (3): RO-1 Governing Party (Root Issuer) · RO-2 Credential Holder / Delegate · RO-3 Relying Party / Verifier Operator
Actors (3): AC-1 Human Delegate / Agent User · AC-2 Relying Party Service · AC-3 Governing Party Issuance System
Entry Points (4): EP-001 verify_chain Function Call · EP-002 new_vac Constructor Call · EP-003 new_vdc Constructor Call · EP-004 attenuate Method Call
Threat Actors (3): TA-1 Malicious Credential Holder · TA-2 External Network Attacker · TA-3 Insider / Compromised Integrator
Infrastructure (1): IF-1 Rust Library Crate (dtg-credentials)
Trust Boundaries (3): TB-1 Untrusted Credential Presentation Boundary · TB-2 Library-Internal Trust Boundary · TB-3 Governing Party Root-of-Trust Boundary
External Entities (2): EE-1 Credential Holder / Presenter · EE-2 Governing Party
System Components (4): SC-1 Authority Chain Verifier (authority.rs) · SC-2 Credential Constructor/Attenuator (create.rs) · SC-3 Credential Data Model (lib.rs, not fully shown) · SC-4 Integrating Relying-Party Application
Resources And Assets (3): RA-1 Authority Chain (Vec) · RA-2 VerifiedAuthority Result · RA-3 AuthorityGrant (scope, actions, parent, audience)
Technologies And Dependencies (3): TD-1 chrono · TD-2 thiserror · TD-3 serde / serde_json
Use Cases (3)
- Governing Party Issues a Root Authority Credential: The governing party for a scope uses DTGCredential::new_vac to mint a root VAC naming the scope and actions it grants, establishing the trust anchor that all attenuated chains must ultimately trace ba
- Holder Attenuates a Narrower Delegation Credential: A credential holder calls DTGCredential::attenuate on a VAC they hold to derive a narrower VAC for an agent or sub-agent, without contacting the original issuer, enabling scoped delegation such as a m
- Relying Party Verifies a Presented Authority Chain: A relying party service receives a full authority chain from a holder along with a requested scope and action, verifies each credential's signature separately, then calls authority::verify_chain to co
⚔️ Attack Scenarios (2)
SC-1: Authority Chain Verifier (authority.rs)
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. System Component"]
direction LR
SC1n@{ shape: rect, label: "SC-1: Authority Chain Verifier" }
end
subgraph SL2["2. Weaknesses"]
direction LR
CWE345@{ shape: rect, label: "CWE-345: Insufficient Verification of Data Authenticity" }
CWE294@{ shape: rect, label: "CWE-294: Authentication Bypass by Capture-replay" }
CWE178@{ shape: rect, label: "CWE-178: Improper Handling of Case Sensitivity" }
CWE287@{ shape: rect, label: "CWE-287: Improper Authentication" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAPEC115@{ shape: rect, label: "CAPEC-115: Authentication Bypass" }
CAPEC90@{ shape: rect, label: "CAPEC-90: Reflection Attack in Authentication Protocol" }
CAPEC627@{ shape: rect, label: "CAPEC-627: Counterfeit GPS Signals (Identifier Spoofing Analogy)" }
CAPEC593@{ shape: rect, label: "CAPEC-593: Session Hijacking" }
end
subgraph SL4["4. Threats"]
direction LR
T1@{ shape: rect, label: "STRIDE-1: Signature Verification Omission<br><i>Critical / Likely</i>" }
T5@{ shape: rect, label: "STRIDE-5: Clock-Trust Manipulation<br><i>High / Likely</i>" }
T2@{ shape: rect, label: "STRIDE-2: String-Based Identity Comparison Spoofing<br><i>High / Possible</i>" }
T11@{ shape: rect, label: "STRIDE-11: Audience-Binding Bypass<br><i>Medium / Possible</i>" }
end
subgraph SL5["5. Threat Actors"]
direction LR
TA1@{ shape: rect, label: "TA-1: Malicious Credential Holder<br><i>Escalate authority beyond grant</i>" }
TA3@{ shape: rect, label: "TA-3: Insider / Compromised Integrator<br><i>Exploit integration mistakes</i>" }
end
SC1n --> CWE345 --> CAPEC115 --> T1 --> TA1
SC1n --> CWE294 --> CAPEC90 --> T5 --> TA3
SC1n --> CWE178 --> CAPEC627 --> T2 --> TA1
SC1n --> CWE287 --> CAPEC593 --> T11 --> TA1
linkStyle 0 stroke:#A50000, stroke-width:2px
linkStyle 1 stroke:#A50000, stroke-width:2px
linkStyle 2 stroke:#A50000, stroke-width:2px
linkStyle 3 stroke:#A50000, stroke-width:2px
linkStyle 4 stroke:#FF0000, stroke-width:2px
linkStyle 5 stroke:#FF0000, stroke-width:2px
linkStyle 6 stroke:#FF0000, stroke-width:2px
linkStyle 7 stroke:#FF0000, stroke-width:2px
linkStyle 8 stroke:#FF0000, stroke-width:2px
linkStyle 9 stroke:#FF0000, stroke-width:2px
linkStyle 10 stroke:#FFA500, stroke-width:2px
linkStyle 11 stroke:#FFA500, stroke-width:2px
linkStyle 12 stroke:#FFA500, stroke-width:2px
linkStyle 13 stroke:#FFA500, stroke-width:2px
linkStyle 14 stroke:#FFA500, stroke-width:2px
linkStyle 15 stroke:#FFA500, stroke-width:2px
SC-2: Credential Constructor/Attenuator (create.rs)
---
config:
layout: dagre
look: classic
theme: dark
---
flowchart LR
subgraph SL1["1. System Component"]
direction LR
SC2n@{ shape: rect, label: "SC-2: Credential Constructor/Attenuator" }
end
subgraph SL2["2. Weaknesses"]
direction LR
CWE770@{ shape: rect, label: "CWE-770: Allocation of Resources Without Limits" }
CWE440@{ shape: rect, label: "CWE-440: Expected Behavior Violation" }
end
subgraph SL3["3. Attack Patterns"]
direction LR
CAPEC125@{ shape: rect, label: "CAPEC-125: Flooding" }
CAPEC698@{ shape: rect, label: "CAPEC-698: Install Malicious Extension (Type Confusion Analogy)" }
end
subgraph SL4["4. Threats"]
direction LR
T8@{ shape: rect, label: "STRIDE-8: Unbounded Attenuation<br><i>Medium / Possible</i>" }
T9@{ shape: rect, label: "STRIDE-9: Type-Confusion Bypass<br><i>Medium / Possible</i>" }
end
subgraph SL5["5. Threat Actors"]
direction LR
TA2@{ shape: rect, label: "TA-2: External Network Attacker<br><i>Disrupt availability</i>" }
TA3b@{ shape: rect, label: "TA-3: Insider / Compromised Integrator<br><i>Exploit integration mistakes</i>" }
end
SC2n --> CWE770 --> CAPEC125 --> T8 --> TA2
SC2n --> CWE440 --> CAPEC698 --> T9 --> TA3b
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
📊 Risk Summary
Total Threats: 12
By Severity: Low: 1 · High: 2 · Medium: 8 · Critical: 1
By Category: Spoofing: 3 · Elevation of Privilege: 5 · Denial of Service: 3 · Tampering: 5 · Repudiation: 1 · Information Disclosure: 1
🎯 Attack Surface
Kill Chain 1: The most severe chain begins at EP-001 (verify_chain) where an attacker exploits the documented but easily-missed integration gap (STRIDE-1) that signatures are never checked inside authority.rs itself; combined with STRIDE-2's lack of identifier normalization on the RootNotGoverning check and STRIDE-5's reliance on a caller-supplied at timestamp, an attacker who controls the surrounding integration (or exploits a sloppy one) can present a fully self-issued, unsigned, or expired chain that nonetheless returns Ok(VerifiedAuthority), achieving complete authority forgery against any relying party that has not layered independent signature verification and trusted clock sourcing in front of this library call. Kill Chain 2: A secondary chain combines STRIDE-11 (audience binding enforced only on the leaf) with STRIDE-4 (no global duplicate-id detection) — an attacker who obtains a leaked intermediate credential bound to a different audience can graft it into a fresh chain terminating in their own presenter identity, and because non-adjacent duplicate ids are never checked, a poorly-designed credential cache in the relying party's SC-4 integration layer could compound the confusion by re-resolving a stale or substituted credential object post-verification, silently widening the effective authority granted. Kill Chain 3: A resource-exhaustion chain starts with STRIDE-8 (attenuate producing oversized/near-maximal-depth chains with no early rejection) feeding into STRIDE-3 (the O(depth × actions²) nested scan in the leaf-to-root walk) and STRIDE-10 (panic-prone .expect() calls that could be reached via edge-case constructions using the test-only authority_mut accessor if it is ever exposed beyond tests) — together enabling a low-cost attacker to degrade or crash the relying party's verification service through crafted, maximal-size, and possibly malformed credential chains submitted at volume against EP-001. Kill Chain 4: A supply-chain/deployment-timing chain links STRIDE-9 (non-exhaustive enum silently absorbed by pre-existing wildcard match arms in downstream code) with the overall PR's introduction of Authority/Delegation types — during the window between a dependency version bump and a downstream code update, VAC/VDC credentials may flow through legacy code paths that never invoke verify_chain at all, meaning the entire security mechanism this PR introduces can be bypassed simply by the surrounding ecosystem's lag in adopting it, independent of any bug in authority.rs itself.
🛡️ Risk Mitigation Strategy
Priority 1 (Immediate): The single highest-leverage gap is that authority.rs's core guarantee (no chain can widen authority) is only half of the security model — signature verification is explicitly out of scope and left to callers (STRIDE-1), and the trust anchor comparison (STRIDE-2) and temporal trust (STRIDE-5) both rely on caller-supplied, unauthenticated inputs. Mitigation must prioritize converting these from documentation-only contracts into type-system-enforced contracts: a VerifiedCredential type-state that can only be constructed post-signature-check, removal of the caller-supplied at parameter in favor of an internally-sourced trusted clock (or explicit, tested guidance plus replay protection at the call site), and canonicalized identifier comparison for governing_party/issuer/subject/audience fields. Priority 2 (Short-Term): Close the audience-binding gap for non-leaf links (STRIDE-11) and add duplicate-id detection across the whole chain (STRIDE-4), since both are localized, low-effort code changes within authority.rs's existing loop structure and materially reduce the blast radius of any leaked intermediate credential. Priority 3 (Medium-Term): Harden availability by enforcing size/length limits on actions lists at construction time (addressing STRIDE-3 and STRIDE-8 together), replacing .expect("checked above") calls with explicit error propagation to make the function panic-free by construction regardless of future refactors (STRIDE-10), and adding structured audit-logging hooks so every AuthorityError outcome can be correlated to a presenter identity for anomaly detection (STRIDE-6). Priority 4 (Long-Term): Address ecosystem-level risks that outlive this specific PR — publish explicit migration guidance warning downstream integrators that permissive wildcard matches on the now-non-exhaustive DTGCredentialType enum can silently bypass the new authority model (STRIDE-9), sanitize AuthorityError Display output before it ever reaches an external
Generated by Agentic Sec — Threat Model & Affect Analysis Agent
📊 Summary & findings
| ✅ Confirmed | |
|---|---|
| 2 | 3 |
Confirmed (2)
- 🟡 Validity-window checks trust a fully caller-supplied timestamp, enabling temporal bypass if
atis attacker-influenced (triaged HIGH→MEDIUM) - 🟡 Non-canonicalized string equality on identity fields (issuer/subject/audience) risks spoofing if identifiers are DIDs/URIs with encoding variance
Must-Review-By-Human (3)
- 🟠 Hardcoded Secrets: Generic Api Key (2 occurrences in 1 unique secrets)
- ⚪ UNCONFIRMED — Hardcoded Secret: Generic Api Key in lib.rs:923
- ⚪ UNCONFIRMED — Hardcoded Secret: Generic Api Key in lib.rs:955
Track 0 of the data-rooms delivery plan. Unblocks the demo and the
rooms/*work, anddepends on nothing — this is our library, and
DTGCredentialTypeis already#[non_exhaustive]with anew_v*constructor per type, so this is the existing patternrather than a fork.
Both types track drafts —
trustoverip/dtgwg-cred-spec#29 (VAC) and
#19 (VDC) — and say so in their
API docs, so a consumer knows the shape may move before those are approved.
authority::verify_chainis the part that mattersEverything else here is a struct and a signature. Anyone can mint a well-formed VAC
naming any scope and any actions, and it will verify perfectly as a credential. What makes
it worthless is that its chain does not reach the party governing the scope — so a verifier
that checks only the credential it was handed accepts a self-issued grant of arbitrary
authority.
Seven rules, each closing one way of getting more than was granted:
scopeaudience, where set, must be the presenterResolution is bearer-side by construction.
verify_chaintakes the whole chain as aslice and never dereferences
parent. Resolving over the network would make verificationdepend on availability, turn every
idinto a request the verifier can be induced to makeagainst an address the holder chooses, and signal credential use to whoever hosts the
identifier.
idvalues are identifiers, not locators.What
attenuatebuysThe case the credential exists for: a member holds
read/write/curatefor a month;their agent runs on
read, for four hours, bound to the agent's own DID. Derived by theholder with no round trip to the issuer, and refused at issue time if it would widen — while
the verifier's check remains authoritative, since nothing stops a different implementation
emitting the JSON by hand.
Tests are mostly attacks
14 in
tests/authority_chain.rs, and the interesting ones are the refusals: a self-issuedgrant, an added action, a grafted parent, an audience-bound credential presented by someone
else, an expired link beneath a live parent, a chain past the depth ceiling, and an empty
actionslist — which is refused both by the constructor and at the deserializationboundary, since a guard only on the constructor is trivially bypassed by
serde_json.cargo test73 passing ·cargo clippy --all-targetsclean ·cargo fmtapplied.Version
0.5.0→0.6.0.DTGCredentialTypeis#[non_exhaustive], so callers matching with awildcard arm are unaffected; callers matching exhaustively need one arm each.