DelegusDocsv0.2

Docs/Threat model mapping

Trust

Threat model → implementation mapping

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-security.md; this document links to its sections wherever an infrastructure control is named.

Protected in v0 (§13.1)#

IDThreatDelivered by (spec)ImplementationProven by
TM-01Forged GrantP2, P3packages/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 onlyvectors deny-issuer-key-not-found, deny-grant-signature-invalid, deny-grant-signature-invalid-wrong-principal-key
TM-02Modified GrantP3JWS 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-03Stolen Grant used without the Agent keyP9, P10Proof 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-04Proof replay at the same RPP13, T5iat 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.tsadapters/redis/replay.ts); store unavailable fails closeddeny-proof-expired, deny-proof-replayed, deny-service-unavailable-replay; apps/api/test/api.test.ts "replay"
TM-05Proof forwarded to another RPP12aud equals the DID the API key resolved to (evaluate.ts; RP identity from apps/api/src/http.ts)deny-proof-audience-mismatch
TM-06Action tampering between Agent and DelegusP14action_hash equals sha256 of JCS(action) as the RP submitted it (jcs.ts, hash.ts); the RP builds the action, never the Agentdeny-proof-action-mismatch; JCS set
TM-07Expired or not-yet-valid authorityP4, P5now 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-08Revoked authorityP6 + §7 guaranteeDelegus-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-09Issuer key compromise after notificationT3key 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-10Stale identity evidence unrecordedreceipt pins did_document_hash, resolved_atsnapshot bytes stored by hash at resolution and refresh ≤ 5 min (apps/api/src/service/principals.ts); hash and time in every receiptapi test "onboarding … DID snapshot"; offline re-verification tests
TM-11Unknown or unverified issuerT1, T2registry lookup; domain verification by DNS TXT or well-known, re-run every 24 h; unregistered → ISSUER_UNKNOWN, registered without a snapshot → SERVICE_UNAVAILABLEdeny-issuer-unknown, deny-issuer-unverified; api test "hosted principal whose DID document cannot be fetched"
TM-12Status-list substitution or forgeryP6 Delegus-signed; receipt pins credential_hashstatus.ts requires kid in the injected Delegus status keys, exact header/type/context, id = the Grant's URL, issuer = the service DIDtest/evaluate.test.ts, vectors with revoked lists; substituted artifacts detected in test/receipt.test.ts
TM-13Grant used at an excluded RPP7audience, if present, must contain the RP DIDdeny-audience-mismatch
TM-14Evidence altered after the factcontent-addressed storekeys 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_UNAVAILABLE (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#

IDNot protectedWhere the boundary shows
NP-01Malicious Principal authorizing malicious activityDelegus attests authority only; no content inspection anywhere in core
NP-02Compromised active Principal key before notificationbounded by validUntil ≤ validFrom + 90 d (P1) and ≤ 5 keys (P2)
NP-03Compromised Agent keybounded by Grant scope (P16–P20) and expiry; SDK signer is pluggable for KMS/HSM
NP-04Business fraud unrelated to authorityout of scope; outcomes are recorded, never adjudicated
NP-05Semantic ambiguity of actionsclosed vocabulary (capability.ts); unknown constraint keys deny (P17); the RP owns the mapping
NP-06False Outcome Attestationsappend-only, reporter-tagged (apps/api/src/service/outcomes.ts); no adjudication
NP-07Lookalike domainsdomain control ≠ organizational identity; no KYB in v0
NP-08RP misreporting the actionthe receipt records the RP's own submission (request_hash)
NP-09Compromise of Delegus signing keysKMS custody (infra-security § Signing keys); keys retired, never removed; two-roll rotation drilled on dev, documented in apps/api/README.md
NP-10DNS or web PKI compromiseshared with TLS; out of scope

Service-layer surface (not in §13; for the reviewer)#

SurfaceControlCodeTest
API keys256-bit random, stored as sha256, shown once; rotation and revocation; revoked keys dead immediatelyapps/api/src/ids.ts, http.ts, /api-keys routes in server.tsapi test "api keys"
Admin tokenrequired outside local/test; onboarding routes only; injected from Secrets Manager (infra-security § Secrets and deployments)config.ts, http.ts authAdminapi test "registry admin"
Rate limitsper 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.tsapi test "rate limits"; sandbox test
Request bodiesstrict JSON, 64 KB limit, exact envelope for /verifyhttp.ts, service/verify.tsapi 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 Principalsadapters/network.ts, didweb.ts, service/principals.tsunits test "did:web resolution rules"; api test "hosted principal"
Host separationstatus host serves lists only; DID-domain host serves DID documents onlyserver.tsapi test "the DID domain host serves DID documents only"
SandboxDELEGUS_DEMO=1 only, refused in prod; ephemeral identities shared via a write-once setting; CORS for configured origins onlyservice/demo.ts, config.tssandbox tests
Loggingone 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.tsapi test "every response carries x-request-id"
Auditappend-only, no update or delete path existsservice/audit.ts, migration 005api test "audit log"
Postgres / RedisTLS 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.tsunits test "pg: an explicit CA survives…"; api test "service unavailable at each seam"

Suggested pen-test order#

  1. /verify with hostile Grants, Proofs and Actions (the vectors in packages/core/test/vectors/cases are the baseline; try what they do not).
  2. Replay and revocation under concurrency across two API tasks.
  3. Onboarding: API key handling, admin token, did:web resolution and domain verification (SSRF, redirects, oversized documents).
  4. Public endpoints: evidence, status lists, DID documents (enumeration, cache behavior, rate limits).
  5. The sandbox on dev (abuse, CORS, exhaustion).
  6. The SDK: JWS handling, binding check, key files.

Out of scope for this list and covered separately: the AWS account controls in infra-security.md (account structure, SCPs, key custody, bucket policy, WAF rule groups, alarms) and the marketing site.