@blamejs/exceptd-skills 0.16.10 → 0.16.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,725 @@
1
+ {
2
+ "_meta": {
3
+ "id": "vc-wallet-trust",
4
+ "version": "1.0.0",
5
+ "last_threat_review": "2026-06-02",
6
+ "threat_currency_score": 94,
7
+ "changelog": [
8
+ {
9
+ "version": "1.0.0",
10
+ "date": "2026-06-02",
11
+ "summary": "Initial seven-phase verifiable-credential / digital-wallet verifier-trust playbook. Covers the trust-verification failure modes a credential verifier exposes when it accepts SD-JWT-VC, OID4VCI/OID4VP, mdoc (ISO 18013-5), DID-identified, status-list-governed, or OpenID-Federation-anchored credentials: issuer key not pinned to a trust anchor, revocation/status not enforced, did:web resolution unpinned, presentations accepted without nonce/audience key-binding (replay), mdoc device-auth skipped, open signature-algorithm sets, unverified key attestation, federation chains not anchored, over-disclosure beyond the DCQL query, and status-list issuers outside the credential issuer trust scope. Closes the GRC loop against NIST 800-53 IA-9 / IA-5, ISO 27001 A.5.16 / A.8.5, NIS2 Art.21 identity-of-entities, and the EU Digital Identity Wallet (eIDAS 2.0) high-assurance expectations. Cross-cuts identity-sso-compromise (federation trust) and cred-stores (wallet key material)."
12
+ }
13
+ ],
14
+ "owner": "@blamejs/platform-security",
15
+ "air_gap_mode": false,
16
+ "scope": "service",
17
+ "preconditions": [
18
+ {
19
+ "id": "verifier-source-or-config-read",
20
+ "description": "Agent must be able to read the operator's credential-verifier source and/or its runtime configuration to inspect how issuer keys, revocation, DID resolution, presentation key-binding, and algorithm policy are trusted. A black-box host with neither source nor config marks the playbook visibility_gap=no_verifier_inventory.",
21
+ "check": "agent_has_filesystem_read == true OR agent_has_verifier_config == true",
22
+ "on_fail": "halt"
23
+ }
24
+ ],
25
+ "mutex": [],
26
+ "feeds_into": [
27
+ {
28
+ "playbook_id": "identity-sso-compromise",
29
+ "condition": "finding.includes_federation_trust_gap == true"
30
+ },
31
+ {
32
+ "playbook_id": "cred-stores",
33
+ "condition": "finding.includes_wallet_key_material == true"
34
+ },
35
+ {
36
+ "playbook_id": "framework",
37
+ "condition": "analyze.compliance_theater_check.verdict == 'theater'"
38
+ }
39
+ ]
40
+ },
41
+ "domain": {
42
+ "name": "Verifiable-credential / digital-wallet verifier trust",
43
+ "attack_class": "identity-abuse",
44
+ "atlas_refs": [],
45
+ "attack_refs": [
46
+ "T1556",
47
+ "T1606",
48
+ "T1550"
49
+ ],
50
+ "cve_refs": [],
51
+ "cwe_refs": [
52
+ "CWE-347",
53
+ "CWE-290",
54
+ "CWE-863",
55
+ "CWE-200",
56
+ "CWE-672"
57
+ ],
58
+ "frameworks_in_scope": [
59
+ "nist-800-53",
60
+ "iso-27001-2022",
61
+ "nis2",
62
+ "eu-cra",
63
+ "uk-caf"
64
+ ]
65
+ },
66
+ "phases": {
67
+ "govern": {
68
+ "jurisdiction_obligations": [
69
+ {
70
+ "jurisdiction": "EU",
71
+ "regulation": "eIDAS 2.0 / EUDI Wallet",
72
+ "obligation": "notify_supervisory_body",
73
+ "window_hours": 72,
74
+ "clock_starts": "detect_confirmed",
75
+ "evidence_required": [
76
+ "affected_credential_types",
77
+ "verifier_trust_config_snapshot",
78
+ "estimated_acceptance_window"
79
+ ]
80
+ },
81
+ {
82
+ "jurisdiction": "EU",
83
+ "regulation": "NIS2 Art.23",
84
+ "obligation": "notify_regulator",
85
+ "window_hours": 24,
86
+ "clock_starts": "detect_confirmed",
87
+ "evidence_required": [
88
+ "verifier_inventory",
89
+ "forged_or_replayed_credential_evidence"
90
+ ]
91
+ },
92
+ {
93
+ "jurisdiction": "EU",
94
+ "regulation": "GDPR Art.33",
95
+ "obligation": "notify_regulator",
96
+ "window_hours": 72,
97
+ "clock_starts": "detect_confirmed",
98
+ "evidence_required": [
99
+ "over_disclosed_claim_categories",
100
+ "data_subjects_estimate"
101
+ ]
102
+ }
103
+ ],
104
+ "theater_fingerprints": [
105
+ {
106
+ "pattern_id": "wallet-certified-but-verifier-unpinned",
107
+ "claim": "We accept the EUDI / mDL wallet, so our acceptance is trustworthy.",
108
+ "fast_detection_test": "Wallet certification covers the WALLET; ask for the VERIFIER trust-anchor config. If the verifier accepts any self-consistent issuer key, certification is irrelevant."
109
+ },
110
+ {
111
+ "pattern_id": "signature-valid-equals-trusted",
112
+ "claim": "The credential signature verifies, so the credential is trusted.",
113
+ "fast_detection_test": "A valid signature only proves the holder of SOME key signed it. Ask which trust anchor the issuer key was validated against; \"it verified\" without anchor-pinning is theater."
114
+ },
115
+ {
116
+ "pattern_id": "revocation-supported-not-enforced",
117
+ "claim": "Our credentials support a status list, so revocation is handled.",
118
+ "fast_detection_test": "Grep the verifier accept-path for a status fetch + bit check. Supporting a status_list claim the verifier never reads is theater."
119
+ }
120
+ ],
121
+ "framework_context": {
122
+ "gap_summary": "Org frameworks treat \"identity verified\" as a binary and assume a validated signature equals a trusted issuer. None of NIST 800-53, ISO 27001, or NIS2 prescribe issuer-trust-anchor pinning, presentation replay-binding, or revocation enforcement for the verifiable-credential / wallet acceptance path — the controls predate the wallet ecosystem.",
123
+ "lag_score": 72,
124
+ "per_framework_gaps": [
125
+ {
126
+ "framework": "nist-800-53",
127
+ "control_id": "IA-9",
128
+ "designed_for": "service identification and authentication",
129
+ "insufficient_because": "does not address verifiable-credential issuer trust-anchor pinning or presentation key-binding; written for service-to-service auth, not wallet-presented credentials."
130
+ },
131
+ {
132
+ "framework": "iso-27001-2022",
133
+ "control_id": "A.5.16",
134
+ "designed_for": "identity lifecycle management",
135
+ "insufficient_because": "covers provisioning/deprovisioning of internal identities, not the trust model for externally-issued verifiable credentials a verifier accepts."
136
+ },
137
+ {
138
+ "framework": "nis2",
139
+ "control_id": "Art.21(2)(d)",
140
+ "designed_for": "supply-chain security of entities",
141
+ "insufficient_because": "names trust of suppliers but not the cryptographic trust-anchor model for credentials those entities present."
142
+ }
143
+ ]
144
+ },
145
+ "skill_preload": [
146
+ "identity-assurance",
147
+ "api-security",
148
+ "pqc-first",
149
+ "framework-gap-analysis",
150
+ "compliance-theater",
151
+ "policy-exception-gen"
152
+ ]
153
+ },
154
+ "direct": {
155
+ "threat_context": "A verifier that accepts verifiable credentials / wallet presentations is a trust boundary: every credential it accepts grants whatever the credential asserts (age, license, employment, entitlement). The high-value abuse is not breaking crypto but exploiting MISSING trust checks — an unpinned issuer key, a skipped revocation check, a replayable presentation, an unverified device binding — to present a forged, revoked, or replayed credential that the verifier treats as authentic. eIDAS 2.0 / EUDI wallets make this a 2026 operational reality across the EU.",
156
+ "rwep_threshold": {
157
+ "escalate": 60,
158
+ "monitor": 40,
159
+ "close": 25
160
+ },
161
+ "framework_lag_declaration": "Org identity controls (NIST IA-9/IA-5, ISO A.5.16) assume service-to-service or human-credential auth and are silent on verifiable-credential issuer-trust-anchor pinning, presentation replay-binding, and revocation enforcement. Treat a clean identity-control audit as NON-EVIDENCE for verifier trust posture.",
162
+ "skill_chain": [
163
+ {
164
+ "skill": "identity-assurance",
165
+ "purpose": "map the verifier trust model to AAL/IAL/FAL and federation assurance expectations"
166
+ },
167
+ {
168
+ "skill": "pqc-first",
169
+ "purpose": "assess the credential signature-algorithm allowlist for downgrade / non-PQC exposure"
170
+ },
171
+ {
172
+ "skill": "compliance-theater",
173
+ "purpose": "separate wallet-certification / signature-valid claims from enforced verifier trust"
174
+ },
175
+ {
176
+ "skill": "framework-gap-analysis",
177
+ "purpose": "map findings to the IA-9 / A.5.16 / NIS2 gaps the controls do not cover"
178
+ }
179
+ ],
180
+ "token_budget": {
181
+ "estimated_total": 14000,
182
+ "breakdown": {
183
+ "govern": 1200,
184
+ "direct": 800,
185
+ "look": 5000,
186
+ "detect": 4000,
187
+ "analyze": 2000,
188
+ "validate": 700,
189
+ "close": 300
190
+ }
191
+ }
192
+ },
193
+ "look": {
194
+ "artifacts": [
195
+ {
196
+ "id": "vc-verifier-config",
197
+ "type": "config_file",
198
+ "source": "Repository walk for the credential-verifier configuration and call sites: grep for issuerKeyResolver / keyResolver / trustAnchors / trust_anchor / issuer_allowlist / verifyCredential / verifyPresentation across config/*.{js,ts,json,yaml} and the verifier source.",
199
+ "description": "How the verifier resolves and trusts issuer keys, and whether a trust anchor / issuer allowlist gates acceptance.",
200
+ "required": true,
201
+ "air_gap_alternative": "If only source is available (no running config), restrict to the verifier call-sites and their resolver wiring; mark runtime-config-derived trust as inventory_gap."
202
+ },
203
+ {
204
+ "id": "revocation-and-statuslist-config",
205
+ "type": "config_file",
206
+ "source": "grep for status_list / statusList / token-status-list / revocation / verifyStatus across the verifier source and config.",
207
+ "description": "Whether the verifier resolves and enforces credential revocation / status-list, and how the status-list issuer key is trusted.",
208
+ "required": true,
209
+ "air_gap_alternative": "Inspect the verifier source for a status-check call on the accept path; if status fetching is runtime-config-gated, mark inventory_gap."
210
+ },
211
+ {
212
+ "id": "did-resolution-config",
213
+ "type": "config_file",
214
+ "source": "grep for did:web / did:key / did:jwk / resolveDid / didResolver / .well-known/did.json across source and config.",
215
+ "description": "Which DID methods are accepted and whether did:web resolution is pinned (cert pin / anchor / known-document-hash).",
216
+ "required": false,
217
+ "air_gap_alternative": "Enumerate DID methods referenced in source; absence of pinning logic around did:web fetches is the inspectable signal."
218
+ },
219
+ {
220
+ "id": "oid4vp-request-config",
221
+ "type": "config_file",
222
+ "source": "grep for oid4vp / vp_token / requireKeyBinding / nonce / dcql / presentation_definition / aud across source and config.",
223
+ "description": "Whether OID4VP presentations require nonce+audience-bound key-binding and whether disclosed claims are filtered to the query.",
224
+ "required": false,
225
+ "air_gap_alternative": "Inspect the presentation-verify call options for requireKeyBinding + nonce issuance; absence is the signal."
226
+ },
227
+ {
228
+ "id": "mdoc-trust-config",
229
+ "type": "config_file",
230
+ "source": "grep for mdoc / mDL / 18013 / trustAnchorsPem / x5chain / deviceAuth / verifyDeviceAuth across source and config.",
231
+ "description": "Whether mdoc issuer certificate chains validate against configured trust anchors and whether device-auth is verified.",
232
+ "required": false,
233
+ "air_gap_alternative": "Inspect mdoc verify call-sites for a device-auth step + trustAnchors argument; omission is the signal."
234
+ },
235
+ {
236
+ "id": "federation-anchor-config",
237
+ "type": "config_file",
238
+ "source": "grep for openid-federation / trust_anchor / entity_statement / authority_hints / metadata_policy across source and config.",
239
+ "description": "Whether OpenID Federation trust chains are validated to a pinned anchor and the kid-less / authority-hint policy.",
240
+ "required": false,
241
+ "air_gap_alternative": "Inspect federation resolution call-sites for an anchor argument and chain-termination check."
242
+ },
243
+ {
244
+ "id": "algorithm-policy",
245
+ "type": "config_file",
246
+ "source": "grep for alg / algorithms / allowlist / none / HS256 / verify options across the credential/presentation verify call-sites.",
247
+ "description": "The signature-algorithm allowlist applied at verification and whether \"none\"/symmetric algorithms are refused.",
248
+ "required": true,
249
+ "air_gap_alternative": "Inspect the verify-option objects for an explicit alg allowlist; an implicit/open alg set is the signal."
250
+ }
251
+ ],
252
+ "collection_scope": {
253
+ "time_window": "current verifier configuration + source state (point-in-time posture audit)",
254
+ "asset_scope": "every service that ACCEPTS verifiable credentials or wallet presentations (SD-JWT-VC, OID4VP, mdoc, DID-identified, status-list-governed)"
255
+ },
256
+ "environment_assumptions": [
257
+ {
258
+ "assumption": "The operator runs at least one credential VERIFIER (accepts credentials), not only an issuer.",
259
+ "if_false": "If the operator only ISSUES credentials, the verifier indicators do not apply; pivot to issuer-side key-rotation + status-list-publication posture only."
260
+ },
261
+ {
262
+ "assumption": "Verifier source or runtime config is readable.",
263
+ "if_false": "Mark visibility_gap=no_verifier_inventory and report only what the accept-path source reveals."
264
+ }
265
+ ],
266
+ "fallback_if_unavailable": [
267
+ {
268
+ "artifact_id": "did-resolution-config",
269
+ "fallback_action": "If no DID methods are used (only x5c / federation issuer keys), skip the did:web indicator and note DID-resolution not-in-scope.",
270
+ "confidence_impact": "none"
271
+ },
272
+ {
273
+ "artifact_id": "mdoc-trust-config",
274
+ "fallback_action": "If no mdoc/mDL acceptance, skip the device-auth indicator.",
275
+ "confidence_impact": "none"
276
+ }
277
+ ]
278
+ },
279
+ "detect": {
280
+ "indicators": [
281
+ {
282
+ "id": "issuer-key-not-pinned-to-trust-anchor",
283
+ "type": "config_value",
284
+ "value": "A credential verifier resolves the issuer signing key via a callback (issuerKeyResolver / keyResolver / publicKey supplied per-request) with no requirement that the key chain to a pre-configured trust anchor or allowlist.",
285
+ "description": "Without trust-anchor pinning, any party that can present a self-consistent issuer key (e.g. a forged did:web document, an attacker-run OID4VCI issuer) is accepted as a valid issuer — the signature verifies but the issuer identity is unauthenticated.",
286
+ "confidence": "high",
287
+ "deterministic": false,
288
+ "attack_ref": "T1606",
289
+ "false_positive_checks_required": [
290
+ "Confirm whether the verifier passes the resolved key through a trust-anchor / issuer-allowlist check BEFORE accepting the credential — a keyResolver that returns a key for ANY issuer id is the finding; one that returns a key only for allowlisted issuer ids (or validates an x5c/OpenID-Federation chain to an anchor) is safe.",
291
+ "Distinguish a development/test config that intentionally trusts any issuer (often gated behind NODE_ENV !== production) from a production verifier path — only the production-reachable path is the finding."
292
+ ]
293
+ },
294
+ {
295
+ "id": "credential-revocation-status-not-checked",
296
+ "type": "config_value",
297
+ "value": "The verifier accepts a credential without checking the revocation status referenced by its status claim (OAuth Token Status List / status_list index), or fetches the status but ignores a revoked/suspended result.",
298
+ "description": "A revoked or suspended credential continues to validate, so a holder whose authorization was withdrawn (employee offboarded, license suspended) still passes verification.",
299
+ "confidence": "high",
300
+ "deterministic": false,
301
+ "attack_ref": "T1550",
302
+ "false_positive_checks_required": [
303
+ "Confirm the verifier actually resolves the status_list endpoint and rejects on a set status bit — a code path that reads the status claim but never compares the bit, or treats a fetch error as \"valid\", is the finding.",
304
+ "A credential type that genuinely has no revocation model (single-use, short-TTL with no status claim) is not a finding; check the credential schema declares no status mechanism rather than the verifier skipping an existing one."
305
+ ]
306
+ },
307
+ {
308
+ "id": "did-web-resolution-unpinned",
309
+ "type": "config_value",
310
+ "value": "A credential issuer identified as did:web is resolved by fetching https://<host>/.well-known/did.json with no certificate pinning, CAA check, or federation-anchor validation of <host>.",
311
+ "description": "A network/DNS adversary (or a typosquatted issuer host) can serve a did.json carrying an attacker-controlled key, and the verifier binds the credential to that forged key.",
312
+ "confidence": "medium",
313
+ "deterministic": false,
314
+ "attack_ref": "T1556",
315
+ "false_positive_checks_required": [
316
+ "did:web over a host the operator fully controls AND pins (cert pin / known-good DID-document hash / federation anchor) is safe — the finding is unpinned resolution of an externally-controlled host.",
317
+ "did:key / did:jwk are self-certifying (the key is in the identifier) and are NOT this finding; restrict to did:web (and did:webvh without witness verification)."
318
+ ]
319
+ },
320
+ {
321
+ "id": "presentation-no-nonce-audience-binding",
322
+ "type": "config_value",
323
+ "value": "An OID4VP / SD-JWT-VC presentation is accepted without requiring a Key-Binding JWT bound to the verifier-issued nonce and audience (requireKeyBinding not set / defaults off).",
324
+ "description": "A presentation captured at one verifier can be replayed to another (or replayed later to the same verifier) because nothing ties the presentation to this challenge and this audience.",
325
+ "confidence": "high",
326
+ "deterministic": false,
327
+ "attack_ref": "T1550",
328
+ "false_positive_checks_required": [
329
+ "Confirm the verifier issues a fresh nonce per request AND requires the KB-JWT to echo that exact nonce + the verifier audience — a verifier that omits the nonce, accepts any audience, or makes key-binding optional is the finding.",
330
+ "A flow that is genuinely holder-binding-exempt (e.g. an issuer-to-issuer batch import over a mutually-authenticated channel) is out of scope; only holder-presentation verifier endpoints are in scope."
331
+ ]
332
+ },
333
+ {
334
+ "id": "mdoc-device-signature-not-verified",
335
+ "type": "config_value",
336
+ "value": "An ISO 18013-5 mdoc presentation has its issuer-signed data verified (MSO digest match) but the device-auth (device signature over the session transcript) is not verified.",
337
+ "description": "A holder can replay issuer-signed mdoc data without proving possession of the device key — the credential is no longer bound to the presenting device.",
338
+ "confidence": "high",
339
+ "deterministic": false,
340
+ "attack_ref": "T1550",
341
+ "false_positive_checks_required": [
342
+ "Confirm the verifier calls device-auth verification (device-signature over the session transcript / handover) in addition to issuer-signed verification — verifying only the MSO is the finding.",
343
+ "A trust model that intentionally accepts issuer-signed-only data (e.g. a server-retrieval flow with a separately authenticated channel) is out of scope; the finding is a device-retrieval flow skipping device auth."
344
+ ]
345
+ },
346
+ {
347
+ "id": "credential-algorithm-allowlist-absent",
348
+ "type": "config_value",
349
+ "value": "The verifier does not enforce a signature-algorithm allowlist at credential / presentation verification, or the allowlist permits weak or attacker-selectable algorithms (HMAC alongside asymmetric, \"none\", non-PQC where a PQC posture is required).",
350
+ "description": "An attacker can forge a credential using an algorithm the library happens to support but the issuer never uses (algorithm-substitution), or a downgrade to a weak primitive.",
351
+ "confidence": "high",
352
+ "deterministic": false,
353
+ "attack_ref": "T1606",
354
+ "false_positive_checks_required": [
355
+ "Confirm an explicit alg allowlist is enforced AND \"none\" is always refused AND symmetric (HMAC) algs are not accepted on a path expecting asymmetric issuer signatures — absence of any of these is the finding.",
356
+ "A single-algorithm deployment that hard-codes one asymmetric alg (effectively an allowlist of one) is safe; the finding is an open or implicit alg set."
357
+ ]
358
+ },
359
+ {
360
+ "id": "key-attestation-not-verified",
361
+ "type": "config_value",
362
+ "value": "A holder-supplied key attestation (claiming the credential private key is hardware/TEE-backed) is accepted without a verifier callback that validates the attestation chain (requireKeyAttestation off / no keyAttestationVerifier).",
363
+ "description": "A holder can claim hardware key protection it does not have, defeating a policy that grants higher assurance to attested keys.",
364
+ "confidence": "medium",
365
+ "deterministic": false,
366
+ "attack_ref": "T1556",
367
+ "false_positive_checks_required": [
368
+ "Only a finding where policy actually RELIES on the attestation (e.g. grants AAL3-equivalent or higher entitlements to attested keys) — if attestation is informational and unused, downgrade.",
369
+ "Confirm the verifier validates the attestation signature chain to a known attestation root (device-vendor CA) rather than accepting the attestation JWT on its face."
370
+ ]
371
+ },
372
+ {
373
+ "id": "openid-federation-anchor-not-pinned",
374
+ "type": "config_value",
375
+ "value": "OpenID Federation trust resolution applies metadata policy / accepts entity statements without validating the chain terminates at a pinned trust anchor, or accepts kid-less JWKS / unbounded authority_hints traversal.",
376
+ "description": "A leaf or intermediate not actually subordinate to a configured anchor is treated as trusted, so federation membership (and the metadata it grants) is forgeable.",
377
+ "confidence": "medium",
378
+ "deterministic": false,
379
+ "attack_ref": "T1556",
380
+ "false_positive_checks_required": [
381
+ "Confirm the resolved trust chain is validated to terminate at an operator-configured trust anchor (not merely \"a self-consistent chain\"), and that kid-less single-key JWKS acceptance is opt-in not default.",
382
+ "A single-anchor deployment with the anchor pinned is safe; the finding is anchor-less or any-anchor chain acceptance."
383
+ ]
384
+ },
385
+ {
386
+ "id": "over-disclosure-not-filtered",
387
+ "type": "config_value",
388
+ "value": "An OID4VP verifier accepts a presentation that discloses claims beyond the DCQL / presentation-definition query, because claim filtering is applied after (not as part of) acceptance.",
389
+ "description": "The verifier ingests and may log/store personal claims it did not request (e.g. full DOB, address, government ID number when only age-over-18 was asked) — a data-minimisation and privacy-by-design failure.",
390
+ "confidence": "medium",
391
+ "deterministic": false,
392
+ "attack_ref": "T1556",
393
+ "false_positive_checks_required": [
394
+ "Confirm the verifier rejects or strips claims outside the requested set rather than accepting and processing the full disclosed claim set — accepting over-disclosed claims into the verified object is the finding.",
395
+ "A verifier that requests a broad claim set deliberately (and is authorised to) is not over-disclosure; compare the DCQL/presentation-definition to what is accepted."
396
+ ]
397
+ },
398
+ {
399
+ "id": "status-list-issuer-not-trust-scoped",
400
+ "type": "config_value",
401
+ "value": "The status-list JWT is verified with a key resolved for the status-list endpoint domain without confirming that endpoint is within the credential issuer's declared trust scope.",
402
+ "description": "A credential issuer (or an attacker who can influence the status_list URI) can point revocation at an endpoint they control and always answer \"valid\", neutralising revocation.",
403
+ "confidence": "medium",
404
+ "deterministic": false,
405
+ "attack_ref": "T1550",
406
+ "false_positive_checks_required": [
407
+ "Confirm the status-list issuer key is validated against the SAME trust anchor / issuer identity as the credential it governs, not merely against whatever key the status-list endpoint presents.",
408
+ "A deployment where issuer and status-list authority are intentionally the same pinned key is safe; the finding is an unpinned or cross-issuer status-list trust."
409
+ ]
410
+ }
411
+ ],
412
+ "false_positive_profile": [
413
+ {
414
+ "indicator_id": "issuer-key-not-pinned-to-trust-anchor",
415
+ "benign_pattern": "A test harness that injects a per-test issuer key via keyResolver.",
416
+ "distinguishing_test": "The keyResolver is reachable only when NODE_ENV/test flags are set; the production verifier path validates against a pinned issuer allowlist or federation anchor."
417
+ },
418
+ {
419
+ "indicator_id": "credential-revocation-status-not-checked",
420
+ "benign_pattern": "Short-TTL single-use credentials with no status claim at all.",
421
+ "distinguishing_test": "The credential schema declares no status mechanism — there is nothing to check, versus a status claim present but unread."
422
+ },
423
+ {
424
+ "indicator_id": "did-web-resolution-unpinned",
425
+ "benign_pattern": "did:key / did:jwk self-certifying identifiers.",
426
+ "distinguishing_test": "The identifier embeds the key (did:key/did:jwk) so no network fetch occurs; only did:web fetches an external document."
427
+ },
428
+ {
429
+ "indicator_id": "presentation-no-nonce-audience-binding",
430
+ "benign_pattern": "Issuer-to-issuer batch import over a mutually authenticated channel.",
431
+ "distinguishing_test": "The endpoint is a holder-presentation verifier (accepts wallet vp_token) rather than a server-to-server channel with its own auth."
432
+ },
433
+ {
434
+ "indicator_id": "mdoc-device-signature-not-verified",
435
+ "benign_pattern": "Server-retrieval flow with a separately authenticated transport.",
436
+ "distinguishing_test": "A device-retrieval (proximity/QR) flow that omits device-auth is the finding; server-retrieval with channel auth is not."
437
+ },
438
+ {
439
+ "indicator_id": "credential-algorithm-allowlist-absent",
440
+ "benign_pattern": "A single hard-coded asymmetric algorithm.",
441
+ "distinguishing_test": "One fixed asymmetric alg is an allowlist of one; an open/implicit alg set or accepted HMAC/\"none\" is the finding."
442
+ },
443
+ {
444
+ "indicator_id": "key-attestation-not-verified",
445
+ "benign_pattern": "Attestation present but informational and unused by policy.",
446
+ "distinguishing_test": "Policy grants higher entitlements to attested keys (finding) versus attestation logged but not relied upon (downgrade)."
447
+ },
448
+ {
449
+ "indicator_id": "openid-federation-anchor-not-pinned",
450
+ "benign_pattern": "Single-anchor deployment with the anchor pinned.",
451
+ "distinguishing_test": "The chain is validated to terminate at a configured anchor (safe) versus any self-consistent chain accepted (finding)."
452
+ },
453
+ {
454
+ "indicator_id": "over-disclosure-not-filtered",
455
+ "benign_pattern": "A verifier authorised to request a broad claim set.",
456
+ "distinguishing_test": "Compare the DCQL/presentation-definition to the accepted claims — accepting claims beyond the query is the finding."
457
+ },
458
+ {
459
+ "indicator_id": "status-list-issuer-not-trust-scoped",
460
+ "benign_pattern": "Issuer and status-list authority are the same pinned key.",
461
+ "distinguishing_test": "Status-list key validated against the credential issuer anchor (safe) versus against whatever the status endpoint presents (finding)."
462
+ }
463
+ ],
464
+ "minimum_signal": {
465
+ "detected": "At least one verifier accept-path lacks a trust check that policy relies on (issuer not anchored, revocation not enforced, presentation replayable, device-auth skipped, or open algorithm set) on a production-reachable verifier.",
466
+ "inconclusive": "A trust check is ambiguous (e.g. anchoring done in a runtime config the audit could not read) — record as visibility_gap, not a clean result.",
467
+ "not_detected": "Every accepted credential format validates the issuer key against a pinned anchor, enforces revocation, binds presentations to a fresh nonce+audience, verifies device-auth where applicable, and enforces an explicit algorithm allowlist."
468
+ }
469
+ },
470
+ "analyze": {
471
+ "rwep_inputs": [
472
+ {
473
+ "signal_id": "issuer-key-not-pinned-to-trust-anchor",
474
+ "rwep_factor": "blast_radius",
475
+ "weight": 30
476
+ },
477
+ {
478
+ "signal_id": "credential-revocation-status-not-checked",
479
+ "rwep_factor": "active_exploitation",
480
+ "weight": 20
481
+ },
482
+ {
483
+ "signal_id": "presentation-no-nonce-audience-binding",
484
+ "rwep_factor": "public_poc",
485
+ "weight": 20
486
+ }
487
+ ],
488
+ "blast_radius_model": {
489
+ "scope_question": "How many credential types and downstream entitlements does the un-trusted verifier path gate, and is it internet-reachable?",
490
+ "scoring_rubric": [
491
+ {
492
+ "condition": "Verifier is internet-facing and the un-anchored path gates a high-value entitlement (payment, age-gated access, employment/licence assertion)",
493
+ "blast_radius_score": 5,
494
+ "description": "Forged/replayed credential yields direct authorization to a high-value action"
495
+ },
496
+ {
497
+ "condition": "Verifier accepts credentials but the entitlement is low-value or human-reviewed downstream",
498
+ "blast_radius_score": 3,
499
+ "description": "Forged credential needs a second factor / human step to cause impact"
500
+ },
501
+ {
502
+ "condition": "Verifier is internal-only / behind strong network controls",
503
+ "blast_radius_score": 2,
504
+ "description": "Exploitation requires prior network access"
505
+ }
506
+ ]
507
+ },
508
+ "compliance_theater_check": {
509
+ "claim": "We accept a certified wallet / the credential signature verifies, so credential acceptance is trustworthy.",
510
+ "audit_evidence": "Wallet certification documents, a verify() call that returns success, a credential schema that declares a status_list.",
511
+ "reality_test": "Show the verifier trust-anchor / issuer-allowlist config, the revocation-check on the accept path, and the presentation nonce+audience binding. If acceptance succeeds against an unpinned issuer key, a never-read status list, or an unbound presentation, the assurance is paper.",
512
+ "theater_verdict_if_gap": "theater"
513
+ },
514
+ "framework_gap_mapping": [
515
+ {
516
+ "finding_id": "issuer-key-not-pinned-to-trust-anchor",
517
+ "framework": "nist-800-53",
518
+ "claimed_control": "IA-9 (service identification)",
519
+ "actual_gap": "IA-9 does not require credential issuer trust-anchor pinning; a verified signature from an unanchored issuer passes IA-9 as written."
520
+ },
521
+ {
522
+ "finding_id": "credential-revocation-status-not-checked",
523
+ "framework": "iso-27001-2022",
524
+ "claimed_control": "A.5.16 (identity lifecycle)",
525
+ "actual_gap": "A.5.16 governs internal identity deprovisioning, not enforcement of externally-issued credential revocation at the verifier."
526
+ }
527
+ ],
528
+ "escalation_criteria": [
529
+ {
530
+ "condition": "An internet-facing verifier accepts an unanchored issuer key OR a replayable presentation gating a high-value entitlement",
531
+ "action": "raise_severity"
532
+ },
533
+ {
534
+ "condition": "Revocation is unenforced on a credential type used for ongoing authorization",
535
+ "action": "trigger_playbook"
536
+ }
537
+ ]
538
+ },
539
+ "validate": {
540
+ "remediation_paths": [
541
+ {
542
+ "id": "pin-issuer-trust-anchors",
543
+ "description": "Replace open issuerKeyResolver acceptance with an issuer allowlist / x5c-or-federation chain validated to a configured trust anchor; reject issuer keys that do not chain to it.",
544
+ "preconditions": [
545
+ "verifier owns or can configure the set of trusted issuers"
546
+ ],
547
+ "priority": 1,
548
+ "for_signals": [
549
+ "issuer-key-not-pinned-to-trust-anchor",
550
+ "did-web-resolution-unpinned",
551
+ "openid-federation-anchor-not-pinned",
552
+ "status-list-issuer-not-trust-scoped"
553
+ ]
554
+ },
555
+ {
556
+ "id": "enforce-revocation-on-accept",
557
+ "description": "Resolve the status_list on the accept path and reject on a set status bit; treat a status fetch failure as fail-closed (not \"valid\").",
558
+ "preconditions": [
559
+ "credentials carry a status mechanism"
560
+ ],
561
+ "priority": 1,
562
+ "for_signals": [
563
+ "credential-revocation-status-not-checked"
564
+ ]
565
+ },
566
+ {
567
+ "id": "bind-presentations-to-nonce-audience",
568
+ "description": "Issue a fresh per-request nonce, require a Key-Binding JWT echoing that nonce + the verifier audience, and verify device-auth for mdoc; reject unbound presentations.",
569
+ "preconditions": [
570
+ "verifier controls the request/challenge"
571
+ ],
572
+ "priority": 1,
573
+ "for_signals": [
574
+ "presentation-no-nonce-audience-binding",
575
+ "mdoc-device-signature-not-verified"
576
+ ]
577
+ },
578
+ {
579
+ "id": "enforce-algorithm-allowlist",
580
+ "description": "Enforce an explicit signature-algorithm allowlist (asymmetric only on issuer paths; \"none\" always refused; PQC-ready per the org crypto posture) at every verify call.",
581
+ "preconditions": [],
582
+ "priority": 2,
583
+ "for_signals": [
584
+ "credential-algorithm-allowlist-absent"
585
+ ]
586
+ },
587
+ {
588
+ "id": "filter-to-requested-claims",
589
+ "description": "Strip or reject claims disclosed beyond the DCQL / presentation-definition query before processing, and validate key attestation only against a known attestation root.",
590
+ "preconditions": [],
591
+ "priority": 2,
592
+ "for_signals": [
593
+ "over-disclosure-not-filtered",
594
+ "key-attestation-not-verified"
595
+ ]
596
+ }
597
+ ],
598
+ "validation_tests": [
599
+ {
600
+ "id": "forged-issuer-rejected",
601
+ "test": "Present a credential signed by a well-formed but non-allowlisted issuer key (e.g. a fresh did:web the verifier was not configured to trust).",
602
+ "expected_result": "Verifier rejects: issuer key does not chain to a configured trust anchor.",
603
+ "test_type": "negative"
604
+ },
605
+ {
606
+ "id": "revoked-credential-rejected",
607
+ "test": "Present a structurally valid credential whose status-list bit is set to revoked.",
608
+ "expected_result": "Verifier rejects on revoked status; a status-fetch failure also rejects (fail-closed).",
609
+ "test_type": "negative"
610
+ },
611
+ {
612
+ "id": "replayed-presentation-rejected",
613
+ "test": "Capture a valid presentation and replay it with a stale nonce / to a different audience.",
614
+ "expected_result": "Verifier rejects: KB-JWT nonce/audience does not match the issued challenge.",
615
+ "test_type": "negative"
616
+ },
617
+ {
618
+ "id": "legitimate-credential-accepted",
619
+ "test": "Present a valid, unrevoked, anchored credential with a correctly bound presentation.",
620
+ "expected_result": "Verifier accepts — no regression on the legitimate path.",
621
+ "test_type": "functional"
622
+ }
623
+ ],
624
+ "residual_risk_statement": {
625
+ "risk": "A trusted issuer whose own signing key is compromised can still mint credentials the verifier will accept until the issuer is removed from the anchor set.",
626
+ "why_remains": "Trust-anchor pinning authenticates the issuer identity, not the issuer's internal key hygiene; cross-issuer compromise is out of the verifier's control.",
627
+ "acceptance_level": "ciso"
628
+ },
629
+ "evidence_requirements": [
630
+ {
631
+ "evidence_type": "config_diff",
632
+ "description": "The issuer allowlist / trust-anchor set, revocation-check wiring, presentation-binding options, and algorithm allowlist as configured at audit time.",
633
+ "retention_period": "1 year"
634
+ },
635
+ {
636
+ "evidence_type": "exploit_replay_negative",
637
+ "description": "Outcomes of the forged-issuer / revoked / replayed negative tests plus the legitimate-path functional test.",
638
+ "retention_period": "1 year"
639
+ }
640
+ ],
641
+ "regression_trigger": [
642
+ {
643
+ "condition": "A new credential format / DID method / federation is accepted",
644
+ "interval": "on change"
645
+ },
646
+ {
647
+ "condition": "Periodic re-attestation of verifier trust posture",
648
+ "interval": "quarterly"
649
+ }
650
+ ]
651
+ },
652
+ "close": {
653
+ "evidence_package": {
654
+ "bundle_format": "csaf-2.0",
655
+ "contents": [
656
+ "verifier trust-config snapshot",
657
+ "per-indicator findings + false-positive disposition",
658
+ "validation test results",
659
+ "framework gap mapping"
660
+ ],
661
+ "destination": ".exceptd/attestations/<session_id>/attestation.json"
662
+ },
663
+ "learning_loop": {
664
+ "enabled": true,
665
+ "lesson_template": {
666
+ "attack_vector": "Forged / revoked / replayed verifiable credential accepted by a verifier missing a trust check (unanchored issuer, unenforced revocation, unbound presentation, skipped device-auth, open algorithm set).",
667
+ "control_gap": "Verifier accept-path lacks issuer trust-anchor pinning / revocation enforcement / presentation replay-binding.",
668
+ "framework_gap": "NIST IA-9 + ISO A.5.16 + NIS2 Art.21 do not prescribe verifiable-credential verifier trust-anchor / revocation / replay controls.",
669
+ "new_control_requirement": "Verifier MUST validate issuer keys to a pinned anchor, enforce revocation fail-closed, and bind presentations to a fresh nonce + audience."
670
+ }
671
+ },
672
+ "notification_actions": [
673
+ {
674
+ "obligation_ref": "EU/eIDAS 2.0 / EUDI Wallet 72h",
675
+ "deadline": "72h from detect_confirmed",
676
+ "recipient": "national wallet supervisory body",
677
+ "evidence_attached": [
678
+ "attestation"
679
+ ]
680
+ },
681
+ {
682
+ "obligation_ref": "EU/NIS2 Art.23 24h",
683
+ "deadline": "24h from detect_confirmed",
684
+ "recipient": "national CSIRT / competent authority",
685
+ "evidence_attached": [
686
+ "attestation"
687
+ ]
688
+ }
689
+ ],
690
+ "exception_generation": {
691
+ "trigger_condition": "A verifier path cannot pin a trust anchor in time (e.g. an open ecosystem with no central anchor yet).",
692
+ "exception_template": {
693
+ "scope": "the specific verifier endpoint + credential type",
694
+ "duration": "90 days",
695
+ "compensating_controls": [
696
+ "issuer allowlist by issuer id",
697
+ "downstream human review of high-value entitlements",
698
+ "enhanced logging of accepted issuer identities"
699
+ ],
700
+ "risk_acceptance_owner": "ciso"
701
+ }
702
+ },
703
+ "regression_schedule": {
704
+ "next_run": "quarterly or on any new accepted credential format",
705
+ "trigger": "change to accepted issuers / DID methods / federations, or quarterly re-attestation"
706
+ }
707
+ }
708
+ },
709
+ "directives": [
710
+ {
711
+ "id": "all-credential-verifier-paths",
712
+ "title": "Inventory + trust-test every verifiable-credential / wallet acceptance path",
713
+ "applies_to": {
714
+ "always": true
715
+ }
716
+ },
717
+ {
718
+ "id": "eudi-wallet-acceptance",
719
+ "title": "Targeted sweep for EUDI / mDL wallet acceptance trust posture",
720
+ "applies_to": {
721
+ "attack_technique": "T1556"
722
+ }
723
+ }
724
+ ]
725
+ }