Consolidated threat model for the wire format, the open-source reference
verifier, the MCP server, and the published Layer 4 adapters. Each threat
below lists the mitigation and a test path that exercises it; the
companion verifier scripts/verify-trust-artifacts.mjs
fails CI if any referenced test path does not exist or if any threat row
lacks a test link.
In scope:
- Wire format (
typ: interaction-record+jwt, Wire 0.2;peac-receipt/0.1, Wire 0.1; archivalpeac.receipt/0.9). - Signature verification (Ed25519, JWS Compact Serialization, JCS).
- Issuer-config and JWKS resolution.
- Reference verifier (
apps/api). - MCP server (
@peac/mcp-server) stdio and Streamable HTTP transports. - Layer 4 commerce mapper boundary
(
@peac/adapter-coreassertExplicitFinality). - Cross-language parity (
packages/schemaandsdks/go).
Out of scope:
- Managed Hosted Verify (operated separately under its own threat model).
- Customer-side key custody (bring-your-own-key; see Key custody and tenancy).
- Transport-level confidentiality beyond what TLS and RFC 9421 provide.
- Application-level business logic layered on top of PEAC records.
| Boundary | Assumption |
|---|---|
| Record issuer ↔ record verifier | Untrusted over the wire; trust is established by the issuer's JWKS binding |
| Reference verifier ↔ upstream JWKS | Untrusted network; SSRF-safe fetch + private-range block |
Caller ↔ @peac/protocol public API |
Trusted process boundary; caller supplies keys and fixtures |
| MCP client ↔ MCP server | Session-isolated per client; no shared mutable state between clients |
| Layer 4 mapper ↔ upstream artifact | Upstream state is never synthesized into finality; see commerce-finality guard |
Each row links to a test file that exercises the mitigation. CI enforces that every link resolves to a tracked path.
| ID | Threat | Mitigation | Test coverage |
|---|---|---|---|
| T-WIRE-01 | Forged signature | Ed25519 verify (RFC 8032); JWS Compact Serialization (RFC 7515) | packages/crypto/tests/jws.test.ts |
| T-WIRE-02 | JOSE header abuse (jwk/x5c/x5u/jku) |
Embedded-key forms rejected on verify | packages/crypto/__tests__/jws-wire-02.test.ts |
| T-WIRE-03 | Critical-extension abuse (crit / b64:false / zip) |
Disallowed at the JWS layer | packages/crypto/__tests__/jws.property.test.ts |
| T-WIRE-04 | Wire-version confusion (0.1 ↔ 0.2 cross-claim) | typ header drives dispatch; coherence enforced at verify |
packages/protocol/__tests__/strictness.property.test.ts |
| T-WIRE-05 | Receipt-ref tampering | receipt_ref = sha256(receipt_jws) verified at extraction |
packages/schema/__tests__/carrier.test.ts |
| T-WIRE-06 | Replay attack | jti uniqueness; iat / nbf / exp time bounds; verifier-side replay cache |
packages/protocol/__tests__/verify-local-order.test.ts |
| T-WIRE-07 | Canonical-JSON divergence (JCS RFC 8785) | Golden vectors; property tests; cross-language parity | packages/crypto/tests/jcs.test.ts, packages/net/node/tests/jcs-property.test.ts |
| T-WIRE-08 | Algorithm confusion / downgrade | Ed25519 only; no alg negotiation | packages/crypto/tests/golden-vectors.test.ts |
| T-WIRE-09 | Policy-binding forgery (policy claim not bound to issuance) | JCS-based policy digest + three-state policy-binding result | packages/protocol/__tests__/policy-binding.test.ts |
| ID | Threat | Mitigation | Test coverage |
|---|---|---|---|
| T-RES-01 | SSRF via issuer-config / JWKS fetch | HTTPS only; private, loopback, and reserved-range block; IDNA / bracket / IPv4-in-IPv6 expansion blocked | packages/net/node/tests/safe-fetch.test.ts, packages/net/node/tests/ssrf-expansion.test.ts |
| T-RES-02 | Slowloris / unbounded fetch time | Explicit timeout cap on every network-bearing path | packages/jwks-cache/tests/resolver.test.ts |
| T-RES-03 | JWKS cache poisoning / kid substitution | Bounded LRU keyed per issuer; kid retention with reuse detection |
packages/jwks-cache/tests/security.test.ts, packages/jwks-cache/tests/cache.test.ts |
| T-RES-04 | Redirect-chain attack (cross-origin redirect to private range) | Redirect policy bounded; redirects to blocked ranges rejected | packages/net/node/tests/safe-fetch.test.ts |
| T-RES-05 | receipt_url fetch amplification |
Semaphore, per-tenant quota, fetch timeout, SSRF-safe | packages/net/node/tests/receipt-url-middleware.test.ts |
| T-RES-06 | Reference-verifier discovery ambient fetch | Discovery constrained to verified resolver paths; no ambient pointer fetch | packages/protocol/tests/pointer-fetch.test.ts, packages/protocol/tests/jwks-resolver.test.ts |
| ID | Threat | Mitigation | Test coverage |
|---|---|---|---|
| T-VRFY-01 | Oversized record DoS | Receipt, per-extension-group, total-extension, nesting-depth, and member-count caps enforced | packages/schema/__tests__/constraints.test.ts, packages/schema/__tests__/byte-budget-enforcement.test.ts |
| T-VRFY-02 | Kernel-constraint bypass (pre-signing path) | validateKernelConstraints() enforced in issue() before signing and on verify |
packages/protocol/tests/issue-constraints.test.ts, packages/protocol/tests/verify-constraints.test.ts |
| T-VRFY-03 | Issuance path calls network (no-network invariant) | Issuance MUST NOT perform any network I/O | tests/security/no-fetch-audit.test.ts |
| T-VRFY-04 | Wire 0.2 issuance byte-identity drift | Property test over canonical JCS + JWS construction | packages/protocol/__tests__/issue-wire-02.test.ts |
v0.13.2 adds an internal-only shadow-mode diagnostic foundation in apps/api for cross-implementation pointer-fetch parity observation. The foundation is governed by PEAC_INTERNAL_SHADOW_RESOLVER and PEAC_INTERNAL_SHADOW_BUFFER_SIZE (see docs/STABILITY-CONTRACT.md "Internal-only flags"). Diagnostic prose lives in docs/diagnostics/SHADOW-MISMATCHES.md.
| ID | Threat | Mitigation | Test coverage |
|---|---|---|---|
| T-SHDW-01 | Raw URL path / query / fragment leakage into mismatch sink | Normalization layer surfaces only public response metadata; redaction-discipline assertions on every recorded entry | apps/api/tests/parity-public-root-smoke.test.ts |
| T-SHDW-02 | Bearer / cookie / private-key material in sink entries | Bounded summary fields; classifier emits class enum + numeric counters only; per-field length caps | apps/api/tests/parity-public-root-smoke.test.ts |
| T-SHDW-03 | Mismatch sink memory exhaustion | Fixed-capacity ring buffer; default 1024 entries; env-clamped to [64, 16384]; in-memory only (no file writes, no network egress) |
apps/api/tests/shadow-mismatch-sink.test.ts |
| T-SHDW-04 | Per-entry size unbounded | ~512-byte JSON-stringified cap per entry with progressive degradation (clamp -> drop excerpts -> placeholder code -> minimal entry) |
apps/api/tests/shadow-mismatch-sink.test.ts, apps/api/tests/parity-public-root-smoke.test.ts |
| T-SHDW-05 | Shadow path executes when flag unset | Lazy module import gated by PEAC_INTERNAL_SHADOW_RESOLVER === '1'; default OFF; the workspace-private resolver composition layer is never loaded otherwise |
apps/api/tests/shadow-resolver.boundary.test.ts |
| T-SHDW-06 | Cross-implementation digest disagreement (silent drift) | Pure-function parity verdict tags parity_digest_mismatch; treated as stop-the-line if observed in production |
apps/api/tests/shadow-classify.test.ts, apps/api/tests/shadow-execute.test.ts |
| ID | Threat | Mitigation | Test coverage |
|---|---|---|---|
| T-MCP-01 | Cross-client state leak (Streamable HTTP) | Session-isolated McpServer + transport per client |
packages/mcp-server/tests/http/session-manager.test.ts, packages/mcp-server/tests/http/http-transport.test.ts |
| T-MCP-02 | Static-policy bypass (runtime policy mutation) | Static policy loaded at startup; immutable at runtime | packages/mcp-server/tests/infra/policy.test.ts |
| T-MCP-03 | Path-traversal via handler input | Path-safety validation on every file-bearing handler input | packages/mcp-server/tests/infra/path-safety.test.ts |
| T-MCP-04 | Stdout framing bypass (stdio transport) | Line-buffered stdout fence | packages/mcp-server/tests/security/stdout-fence.test.ts |
| T-MCP-05 | Evidence-carrier size overflow in _meta |
Transport-binding size caps; 64 KB embed for MCP / A2A / UCP | packages/mcp-server/tests/integration/e2e-smoke.test.ts |
| ID | Threat | Mitigation | Test coverage |
|---|---|---|---|
| T-CMRC-01 | Mapper synthesizes finality from non-payment artifacts | assertExplicitFinality boundary guard; stable code commerce.finality_synthesis_blocked |
packages/adapters/core/tests/finality.test.ts, packages/adapters/core/tests/finality-fixtures.test.ts |
| ID | Threat | Mitigation | Test coverage |
|---|---|---|---|
| T-PARITY-01 | TS / Go wire-output divergence | Shared JCS golden vectors; claim parity property tests | tests/parity/core-claims.test.ts, specs/conformance/fixtures/go-interaction-record/jcs-golden-vectors.json |
These mitigations are in effect but have scheduled follow-ups for a future release. The stability contract and security operations describe the current behavior; the scheduled work is planning scope, not a current gap in shipped behavior.
- Mutation-testing baseline (Stryker for TypeScript;
go-mutestingfor Go). - Error-code emission audit (classify unused codes in
specs/kernel/errors.json). - Verifier-policy extraction from
@peac/kernelinto a verifier-owned surface. - Resource-limit invariant table with per-row implementation, test, and baseline references.
The CLI command-execution and observational lifecycle record surfaces have shipped. Their security contract is enforced by the rules below.
| ID | Threat | Mitigation | Test coverage |
|---|---|---|---|
| T-CLI-01 | Raw command, stream, or argument secret capture | Hash/redact defaults for argv/stdin/stdout/stderr; explicit raw-capture acknowledgement; bounded byte ceilings; secret-scan guard on raw samples |
packages/cli/tests/secret-scan.test.ts, packages/cli/tests/capture.test.ts |
| T-CLI-02 | Shell ambiguity or hidden shell expansion | shell: false default; --shell-mode required to acknowledge a shell binary; command after -- spawned as supplied |
packages/cli/tests/observe-command.test.ts, packages/cli/tests/record-command.test.ts |
| T-CLI-03 | Environment-variable leakage | Deny-by-default env capture; explicit --env-allow allowlist; hashed values by default; raw env requires double opt-in |
packages/cli/tests/observe-command.test.ts, packages/cli/tests/capture.test.ts |
| T-LIFE-01 | Lifecycle record overclaims decision truth | Caller-reported event model; opaque-reference grammar; no inline scoring/finality synthesis; closed enum of forbidden top-level keys | packages/schema/__tests__/extensions/lifecycle-observation.test.ts, packages/schema/__tests__/extensions/lifecycle-observation-shape.test.ts |
| T-LIFE-02 | PEAC mistaken for evaluator/approver/orchestrator | Profile boundary: PEAC issues records; upstream systems evaluate, approve, schedule, transition, or orchestrate | docs/specs/LIFECYCLE-OBSERVATION-PROFILE.md §8 (orchestrator boundary, normative) |
Security defaults summary:
- No raw secret capture by default.
argv,stdin,stdout,stderrdefault to hash/redact modes; raw capture requires--capture-mode rawAND--unsafe-allow-raw-capture(double opt-in). - Environment-variable allowlist plus value hashing. No blanket environment dump. Only explicitly-listed variables enter the record, and even those are hashed by default. Raw env requires
--env-mode rawAND--unsafe-allow-raw-env. - Explicit shell mode. Shell-binary detected without
--shell-modeis a hard fail. Hidden shell-expansion ambiguity is rejected. - Bounded byte ceilings on command capture.
argvbytes, stream bytes, and environment-variable reference counts all carry documented upper bounds. Exceeding a bound truncates or hashes; it never silently drops. - Lifecycle records are observational-only. Approval, evaluation, experiment, and workflow records describe what another system attested. They never imply PEAC made the decision, scored the runtime, enforced the policy, or determined payment finality.