Spec §13 is the normative scope of protection. This document maps each threat to where the protection lives in the code and which test proves it, so a security reviewer starts from the design rather than rediscovering it. Paths are relative to the repository root; check ids are spec §5.2/§5.3. Controls that live in AWS rather than in code (accounts, keys, evidence bucket, network, secrets, alerting) are stated in infra-; this document links to its sections wherever an infrastructure control is named.
Protected in v0 (§13.1)#
| ID | Threat | Delivered by (spec) | Implementation | Proven by |
|---|---|---|---|---|
| TM-01 | Forged Grant | P2, P3 | packages/core/src/diddoc.ts finds the kid in the issuer's DID document snapshot (Multikey, Ed25519, ≤ 5 keys, controller = issuer); packages/core/src/evaluate.ts verifies the Grant JWS with that key only | vectors deny-issuer-key-not-found, deny-grant-signature-invalid, deny-grant-signature-invalid-wrong-principal-key |
| TM-02 | Modified Grant | P3 | JWS signature over the exact received bytes (packages/core/src/jws.ts, strict base64url, 64-byte signature) | deny-grant-signature-invalid; test/grant.test.ts malformed set |
| TM-03 | Stolen Grant used without the Agent key | P9, P10 | Proof signature verified against the key its kid/iss names (proof.ts), then iss compared with the Grant's sub (evaluate.ts) | deny-proof-signature-invalid, deny-proof-agent-mismatch |
| TM-04 | Proof replay at the same RP | P13, T5 | iat within the RP's window (evaluate.ts); atomic SET replay:{rp}:{jti} NX PX 2×window executed only after P1–P14 pass (apps/api/src/service/verify.ts → adapters/redis/replay.ts); store unavailable fails closed | deny-proof-expired, deny-proof-replayed, deny-service-unavailable-replay; apps/api/test/api.test.ts "replay" |
| TM-05 | Proof forwarded to another RP | P12 | aud equals the DID the API key resolved to (evaluate.ts; RP identity from apps/api/src/http.ts) | deny-proof-audience-mismatch |
| TM-06 | Action tampering between Agent and Delegus | P14 | action_hash equals sha256 of JCS(action) as the RP submitted it (jcs.ts, hash.ts); the RP builds the action, never the Agent | deny-proof-action-mismatch; JCS set |
| TM-07 | Expired or not-yet-valid authority | P4, P5 | now from the service clock, recorded as evaluated_at; offline verifiers use the signed value (erratum 2, receipt.ts reverifyReceipt) | deny-grant-not-yet-valid, deny-grant-expired; test/receipt.test.ts "now = evaluated_at" |
| TM-08 | Revoked authority | P6 + §7 guarantee | Delegus-signed Bitstring Status List verified in status.ts; /revoke commits the re-signed list to Postgres (compare-and-set) then INCR status_ver, success only after both; verify nodes reload whenever the counter is missing or differs from the cached value (apps/api/src/service/status-lists.ts) | deny-authority-revoked; apps/api/test/api.test.ts "demo flow … on another node too" incl. Redis failover; deny-service-unavailable-status |
| TM-09 | Issuer key compromise after notification | T3 | key state in the registry, read on every verify (issuerLookup); POST /keys/{kid}/compromise (Delegus's own keys: infra-security § Signing keys) | deny-issuer-key-compromised; api test "trust layer" |
| TM-10 | Stale identity evidence unrecorded | receipt pins did_document_hash, resolved_at | snapshot bytes stored by hash at resolution and refresh ≤ 5 min (apps/api/src/service/principals.ts); hash and time in every receipt | api test "onboarding … DID snapshot"; offline re-verification tests |
| TM-11 | Unknown or unverified issuer | T1, T2 | registry lookup; domain verification by DNS TXT or well-known, re-run every 24 h; unregistered → ISSUER_UNKNOWN, registered without a snapshot → SERVICE_UNAVAILABLE | deny-issuer-unknown, deny-issuer-unverified; api test "hosted principal whose DID document cannot be fetched" |
| TM-12 | Status-list substitution or forgery | P6 Delegus-signed; receipt pins credential_hash | status.ts requires kid in the injected Delegus status keys, exact header/type/context, id = the Grant's URL, issuer = the service DID | test/evaluate.test.ts, vectors with revoked lists; substituted artifacts detected in test/receipt.test.ts |
| TM-13 | Grant used at an excluded RP | P7 | audience, if present, must contain the RP DID | deny-audience-mismatch |
| TM-14 | Evidence altered after the fact | content-addressed store | keys sha256/<hex>, write-once PUT, Object Lock (infra-security § Evidence store); GET /evidence/{hash}; offline verifier compares every pinned hash (receipt.ts) | test/receipt.test.ts "substituted artifacts are detected by hash"; api test "offline re-verification from GET /evidence alone" |
Fail-closed properties that cut across the table: unknown header parameters, claims, members and constraint values deny at P1/P8 (grant.ts, proof.ts); non-canonical htu denies (htu.ts); duplicate JSON keys and unsafe integers are rejected by the strict parser (json.ts); every unavailable or throwing service seam yields SERVICE_ (evaluate.ts safely()); the decision is the first false in the fixed §5.4 order and T5 never runs before P14.
Explicitly not protected (§13.2) and where the boundary is enforced#
| ID | Not protected | Where the boundary shows |
|---|---|---|
| NP-01 | Malicious Principal authorizing malicious activity | Delegus attests authority only; no content inspection anywhere in core |
| NP-02 | Compromised active Principal key before notification | bounded by validUntil ≤ validFrom + 90 d (P1) and ≤ 5 keys (P2) |
| NP-03 | Compromised Agent key | bounded by Grant scope (P16–P20) and expiry; SDK signer is pluggable for KMS/HSM |
| NP-04 | Business fraud unrelated to authority | out of scope; outcomes are recorded, never adjudicated |
| NP-05 | Semantic ambiguity of actions | closed vocabulary (capability.ts); unknown constraint keys deny (P17); the RP owns the mapping |
| NP-06 | False Outcome Attestations | append-only, reporter-tagged (apps/api/src/service/outcomes.ts); no adjudication |
| NP-07 | Lookalike domains | domain control ≠ organizational identity; no KYB in v0 |
| NP-08 | RP misreporting the action | the receipt records the RP's own submission (request_hash) |
| NP-09 | Compromise of Delegus signing keys | KMS custody (infra-security § Signing keys); keys retired, never removed; two-roll rotation drilled on dev, documented in apps/api/README.md |
| NP-10 | DNS or web PKI compromise | shared with TLS; out of scope |
Service-layer surface (not in §13; for the reviewer)#
| Surface | Control | Code | Test |
|---|---|---|---|
| API keys | 256-bit random, stored as sha256, shown once; rotation and revocation; revoked keys dead immediately | apps/api/src/ids.ts, http.ts, /api-keys routes in server.ts | api test "api keys" |
| Admin token | required outside local/test; onboarding routes only; injected from Secrets Manager (infra-security § Secrets and deployments) | config.ts, http.ts authAdmin | api test "registry admin" |
| Rate limits | per relying party on /verify, per key on other authenticated routes, per IP on public routes and on failed authentication; sandbox 30/min per IP; WAF in front of the ALB (infra-security § Network and edge) | ratelimit.ts, http.ts, server.ts, service/demo.ts | api test "rate limits"; sandbox test |
| Request bodies | strict JSON, 64 KB limit, exact envelope for /verify | http.ts, service/verify.ts | api test "request envelope validation" |
| Outbound fetches (SSRF) | only at onboarding and maintenance, never on /verify; https only, 10 s timeout, 64 KB cap; URLs derived from did:web rules only, DID domain must not be a Delegus domain for hosted Principals | adapters/network.ts, didweb.ts, service/principals.ts | units test "did:web resolution rules"; api test "hosted principal" |
| Host separation | status host serves lists only; DID-domain host serves DID documents only | server.ts | api test "the DID domain host serves DID documents only" |
| Sandbox | DELEGUS_DEMO=1 only, refused in prod; ephemeral identities shared via a write-once setting; CORS for configured origins only | service/demo.ts, config.ts | sandbox tests |
| Logging | one structured line per request; never keys, Grants, Proofs or receipt bodies; request ids echoed; metric filters and alarms on the log group (infra-security § Logging and alerting) | log.ts, server.ts | api test "every response carries x-request-id" |
| Audit | append-only, no update or delete path exists | service/audit.ts, migration 005 | api test "audit log" |
| Postgres / Redis | TLS with CA verification (RDS bundle), fail-fast Redis with no offline queue; unavailability → 503 or SERVICE_UNAVAILABLE; private subnets only (infra-security § Network and edge) | adapters/pg/registry.ts (pgPoolConfig), adapters/redis/replay.ts | units test "pg: an explicit CA survives…"; api test "service unavailable at each seam" |
Suggested pen-test order#
/verifywith hostile Grants, Proofs and Actions (the vectors inpackages/are the baseline; try what they do not).core/ test/ vectors/ cases - Replay and revocation under concurrency across two API tasks.
- Onboarding: API key handling, admin token, did:web resolution and domain verification (SSRF, redirects, oversized documents).
- Public endpoints: evidence, status lists, DID documents (enumeration, cache behavior, rate limits).
- The sandbox on dev (abuse, CORS, exhaustion).
- The SDK: JWS handling, binding check, key files.
Out of scope for this list and covered separately: the AWS account controls in infra- (account structure, SCPs, key custody, bucket policy, WAF rule groups, alarms) and the marketing site.