# Proof of Exposure (PoE) Predicate Specification _Last updated: 2025-12-23. Owner: Attestor Guild._ This document specifies the **PoE predicate type** for DSSE attestations, canonical JSON serialization rules, and verification algorithms. PoE artifacts provide compact, offline-verifiable evidence of vulnerability reachability at the function level. --- ## 1. Overview ### 1.1 Purpose Define a standardized, deterministic format for Proof of Exposure artifacts that: - Proves specific call paths from entry points to vulnerable sinks - Can be verified offline in air-gapped environments - Supports DSSE signing and Rekor transparency logging - Integrates with SBOM, VEX, and policy evaluation ### 1.2 Predicate Type ``` stellaops.dev/predicates/proof-of-exposure@v1 ``` **URI:** `https://stellaops.dev/predicates/proof-of-exposure/v1/schema.json` **Version:** v1 (initial release 2025-12-23) ### 1.3 Scope This spec covers: - PoE JSON schema - Canonical serialization rules - DSSE envelope format - CAS storage layout - Verification algorithm - OCI attachment strategy --- ## 2. PoE JSON Schema ### 2.1 Top-Level Structure ```json { "@type": "https://stellaops.dev/predicates/proof-of-exposure@v1", "schema": "stellaops.dev/poe@v1", "subject": { "buildId": "gnu-build-id:5f0c7c3c4d5e6f7a8b9c0d1e2f3a4b5c", "componentRef": "pkg:maven/log4j@2.14.1", "vulnId": "CVE-2021-44228", "imageDigest": "sha256:abc123def456..." }, "subgraph": { "nodes": [...], "edges": [...], "entryRefs": [...], "sinkRefs": [...] }, "metadata": { "generatedAt": "2025-12-23T10:00:00Z", "analyzer": {...}, "policy": {...}, "reproSteps": [...] }, "evidence": { "graphHash": "blake3:a1b2c3d4e5f6...", "sbomRef": "cas://scanner-artifacts/sbom.cdx.json", "vexClaimUri": "cas://vex/claims/sha256:xyz789..." } } ``` ### 2.2 Subject Block Identifies what this PoE is about: ```json { "buildId": "string", // ELF Build-ID, PE PDB GUID, or image digest "componentRef": "string", // PURL or SBOM component reference "vulnId": "string", // CVE-YYYY-NNNNN "imageDigest": "string?" // Optional: OCI image digest } ``` **Fields:** - `buildId` (required): Deterministic build identifier (see Section 3.1) - `componentRef` (required): PURL package URL (pkg:maven/..., pkg:npm/..., etc.) - `vulnId` (required): CVE identifier in standard format - `imageDigest` (optional): Container image digest if PoE applies to specific image ### 2.3 Subgraph Block The minimal call graph showing reachability: ```json { "nodes": [ { "id": "sym:java:R3JlZXRpbmc...", "moduleHash": "sha256:abc123...", "symbol": "com.example.GreetingService.greet(String)", "addr": "0x401000", "file": "GreetingService.java", "line": 42 }, ... ], "edges": [ { "from": "sym:java:caller...", "to": "sym:java:callee...", "guards": ["feature:dark-mode"], "confidence": 0.92 }, ... ], "entryRefs": [ "sym:java:main...", "sym:java:UserController.handleRequest..." ], "sinkRefs": [ "sym:java:log4j.Logger.error..." ] } ``` **Node Schema:** ```typescript interface Node { id: string; // symbol_id or code_id (from function-level-evidence.md) moduleHash: string; // SHA-256 of module/library symbol: string; // Human-readable symbol (e.g., "main()", "Foo.bar()") addr: string; // Hex address (e.g., "0x401000") file?: string; // Source file path (if available) line?: number; // Source line number (if available) } ``` **Edge Schema:** ```typescript interface Edge { from: string; // Caller node ID (symbol_id or code_id) to: string; // Callee node ID guards?: string[]; // Guard predicates (e.g., ["feature:dark-mode", "platform:linux"]) confidence: number; // Confidence score [0.0, 1.0] } ``` **Entry/Sink Refs:** - Arrays of node IDs (symbol_id or code_id) - Entry nodes: Where execution begins (HTTP handlers, CLI commands, etc.) - Sink nodes: Vulnerable functions identified by CVE ### 2.4 Metadata Block Provenance and reproduction information: ```json { "generatedAt": "2025-12-23T10:00:00Z", "analyzer": { "name": "stellaops-scanner", "version": "1.2.0", "toolchainDigest": "sha256:def456..." }, "policy": { "policyId": "prod-release-v42", "policyDigest": "sha256:abc123...", "evaluatedAt": "2025-12-23T09:58:00Z" }, "reproSteps": [ "1. Build container image from Dockerfile (commit: abc123)", "2. Run scanner with config: etc/scanner.yaml", "3. Extract reachability graph with maxDepth=10", "4. Resolve CVE-2021-44228 to symbol: org.apache.logging.log4j.core.lookup.JndiLookup.lookup" ] } ``` **Analyzer Schema:** ```typescript interface Analyzer { name: string; // Analyzer identifier (e.g., "stellaops-scanner") version: string; // Semantic version (e.g., "1.2.0") toolchainDigest: string; // SHA-256 hash of analyzer binary/container } ``` **Policy Schema:** ```typescript interface Policy { policyId: string; // Policy version identifier policyDigest: string; // SHA-256 hash of policy document evaluatedAt: string; // ISO-8601 UTC timestamp } ``` **Repro Steps:** - Array of human-readable strings - Minimal steps to reproduce the PoE - Includes: build commands, scanner config, graph extraction params ### 2.5 Evidence Block Links to related artifacts: ```json { "graphHash": "blake3:a1b2c3d4e5f6...", "sbomRef": "cas://scanner-artifacts/sbom.cdx.json", "vexClaimUri": "cas://vex/claims/sha256:xyz789...", "runtimeFactsUri": "cas://reachability/runtime/sha256:abc123..." } ``` **Fields:** - `graphHash` (required): BLAKE3 hash of parent richgraph-v1 - `sbomRef` (optional): CAS URI of SBOM artifact - `vexClaimUri` (optional): CAS URI of VEX claim if exists - `runtimeFactsUri` (optional): CAS URI of runtime observation facts --- ## 3. Canonical Serialization Rules ### 3.1 Determinism Requirements For reproducible hashes, PoE JSON must be serialized deterministically: 1. **Key Ordering**: All object keys sorted lexicographically 2. **Array Ordering**: Arrays sorted by deterministic field (specified per array type) 3. **Timestamp Format**: ISO-8601 UTC with millisecond precision (`YYYY-MM-DDTHH:mm:ss.fffZ`) 4. **Number Format**: Decimal notation (no scientific notation) 5. **String Escaping**: Minimal escaping (use `\"` for quotes, `\n` for newlines, no Unicode escaping) 6. **Whitespace**: Prettified with 2-space indentation (not minified) 7. **No Null Fields**: Omit fields with `null` values ### 3.2 Array Sorting Rules | Array | Sort Key | Example | |-------|----------|---------| | `nodes` | `id` (lexicographic) | `sym:java:Aa...` before `sym:java:Zz...` | | `edges` | `from`, then `to` | `(A→B)` before `(A→C)` | | `entryRefs` | Lexicographic | `sym:java:main...` before `sym:java:process...` | | `sinkRefs` | Lexicographic | Same as `entryRefs` | | `guards` | Lexicographic | `feature:dark-mode` before `platform:linux` | | `reproSteps` | Numeric order (1, 2, 3, ...) | Preserve original order | ### 3.3 C# Serialization Example ```csharp using System.Text.Json; using System.Text.Json.Serialization; var options = new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; // Custom converter to sort object keys options.Converters.Add(new SortedKeysJsonConverter()); // Custom converter to sort arrays deterministically options.Converters.Add(new DeterministicArraySortConverter()); var json = JsonSerializer.Serialize(poe, options); var bytes = Encoding.UTF8.GetBytes(json); // Compute BLAKE3-256 hash var hash = Blake3.Hash(bytes); var poeHash = $"blake3:{Convert.ToHexString(hash).ToLowerInvariant()}"; ``` ### 3.4 Golden Example **File:** `tests/Attestor/Fixtures/log4j-cve-2021-44228.poe.json` ```json { "@type": "https://stellaops.dev/predicates/proof-of-exposure@v1", "evidence": { "graphHash": "blake3:a1b2c3d4e5f6789012345678901234567890123456789012345678901234", "sbomRef": "cas://scanner-artifacts/sbom.cdx.json" }, "metadata": { "analyzer": { "name": "stellaops-scanner", "toolchainDigest": "sha256:def456789012345678901234567890123456789012345678901234567890", "version": "1.2.0" }, "generatedAt": "2025-12-23T10:00:00.000Z", "policy": { "evaluatedAt": "2025-12-23T09:58:00.000Z", "policyDigest": "sha256:abc123456789012345678901234567890123456789012345678901234567", "policyId": "prod-release-v42" }, "reproSteps": [ "1. Build container image from Dockerfile (commit: abc123)", "2. Run scanner with config: etc/scanner.yaml", "3. Extract reachability graph with maxDepth=10" ] }, "schema": "stellaops.dev/poe@v1", "subject": { "buildId": "gnu-build-id:5f0c7c3c4d5e6f7a8b9c0d1e2f3a4b5c", "componentRef": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1", "vulnId": "CVE-2021-44228" }, "subgraph": { "edges": [ { "confidence": 0.95, "from": "sym:java:R3JlZXRpbmdTZXJ2aWNl", "to": "sym:java:bG9nNGo" } ], "entryRefs": [ "sym:java:R3JlZXRpbmdTZXJ2aWNl" ], "nodes": [ { "addr": "0x401000", "file": "GreetingService.java", "id": "sym:java:R3JlZXRpbmdTZXJ2aWNl", "line": 42, "moduleHash": "sha256:abc123456789012345678901234567890123456789012345678901234567", "symbol": "com.example.GreetingService.greet(String)" }, { "addr": "0x402000", "file": "JndiLookup.java", "id": "sym:java:bG9nNGo", "line": 128, "moduleHash": "sha256:def456789012345678901234567890123456789012345678901234567890", "symbol": "org.apache.logging.log4j.core.lookup.JndiLookup.lookup(LogEvent, String)" } ], "sinkRefs": [ "sym:java:bG9nNGo" ] } } ``` **Hash:** `blake3:7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b` --- ## 4. DSSE Envelope Format ### 4.1 Envelope Structure ```json { "payload": "", "payloadType": "application/vnd.stellaops.poe+json", "signatures": [ { "keyid": "scanner-signing-2025", "sig": "" } ] } ``` **Fields:** - `payload`: Base64-encoded canonical PoE JSON (from Section 3) - `payloadType`: MIME type `application/vnd.stellaops.poe+json` - `signatures`: Array of DSSE signatures (usually single signature) ### 4.2 Signature Algorithm **Supported Algorithms:** | Algorithm | Use Case | Key Size | |-----------|----------|----------| | ECDSA P-256 | Standard (online) | 256-bit | | ECDSA P-384 | High-security (regulated) | 384-bit | | Ed25519 | Performance (offline) | 256-bit | | RSA-PSS 3072 | Legacy compatibility | 3072-bit | | GOST R 34.10-2012 | Russian FIPS (sovereign) | 256-bit | | SM2 | Chinese FIPS (sovereign) | 256-bit | **Default:** ECDSA P-256 (balances security and performance) ### 4.3 Signing Workflow ```csharp // 1. Canonicalize PoE JSON var canonicalJson = CanonicalizeJson(poe); var payload = Convert.ToBase64String(Encoding.UTF8.GetBytes(canonicalJson)); // 2. Create DSSE pre-authentication encoding (PAE) var pae = DsseHelper.CreatePae( payloadType: "application/vnd.stellaops.poe+json", payload: Encoding.UTF8.GetBytes(canonicalJson) ); // 3. Sign PAE with private key var signature = _signer.Sign(pae, keyId: "scanner-signing-2025"); // 4. Build DSSE envelope var envelope = new DsseEnvelope { Payload = payload, PayloadType = "application/vnd.stellaops.poe+json", Signatures = new[] { new DsseSignature { KeyId = "scanner-signing-2025", Sig = Convert.ToBase64String(signature) } } }; // 5. Serialize envelope to JSON var envelopeJson = JsonSerializer.Serialize(envelope, _options); ``` --- ## 5. CAS Storage Layout ### 5.1 Directory Structure ``` cas://reachability/poe/ {poe_hash}/ poe.json # Canonical PoE body poe.json.dsse # DSSE envelope poe.json.rekor # Rekor inclusion proof (optional) poe.json.meta # Metadata (created_at, image_digest, etc.) ``` **Hash Algorithm:** BLAKE3-256 (as defined in Section 3.3) **Example Path:** ``` cas://reachability/poe/blake3:7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d/poe.json ``` ### 5.2 Indexing Strategy **Primary Index:** `poe_hash` (BLAKE3 of canonical JSON) **Secondary Indexes:** | Index | Key | Use Case | |-------|-----|----------| | By Image | `image_digest → [poe_hash, ...]` | List all PoEs for container image | | By CVE | `vuln_id → [poe_hash, ...]` | List all PoEs for specific CVE | | By Component | `component_ref → [poe_hash, ...]` | List all PoEs for package | | By Build | `build_id → [poe_hash, ...]` | List all PoEs for specific build | **Implementation:** PostgreSQL JSONB columns or Redis sorted sets ### 5.3 Metadata File **File:** `poe.json.meta` ```json { "poeHash": "blake3:7a8b9c0d1e2f...", "createdAt": "2025-12-23T10:00:00Z", "imageDigest": "sha256:abc123...", "vulnId": "CVE-2021-44228", "componentRef": "pkg:maven/log4j@2.14.1", "buildId": "gnu-build-id:5f0c7c3c...", "size": 4567, // Bytes "rekorLogIndex": 12345678 } ``` --- ## 6. OCI Attachment Strategy ### 6.1 Attachment Model **Options:** 1. **Per-PoE Attachment**: One OCI ref per PoE artifact 2. **Batched Attachment**: Single OCI ref with multiple PoEs in manifest **Decision:** Per-PoE attachment (granular auditing, selective fetch) ### 6.2 OCI Reference Format ``` {registry}/{repository}:{tag}@sha256:{image_digest} └─> attestations/ └─> poe-{short_poe_hash} ``` **Example:** ``` docker.io/myorg/myapp:v1.2.3@sha256:abc123... └─> attestations/ └─> poe-7a8b9c0d ``` ### 6.3 Attachment Manifest **OCI Artifact Manifest** (per PoE): ```json { "schemaVersion": 2, "mediaType": "application/vnd.oci.artifact.manifest.v1+json", "artifactType": "application/vnd.stellaops.poe", "blobs": [ { "mediaType": "application/vnd.stellaops.poe+json", "digest": "sha256:def456...", "size": 4567, "annotations": { "org.opencontainers.image.title": "poe.json" } }, { "mediaType": "application/vnd.dsse.envelope.v1+json", "digest": "sha256:ghi789...", "size": 2345, "annotations": { "org.opencontainers.image.title": "poe.json.dsse" } } ], "subject": { "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "sha256:abc123...", "size": 7890 }, "annotations": { "stellaops.poe.hash": "blake3:7a8b9c0d...", "stellaops.poe.vulnId": "CVE-2021-44228", "stellaops.poe.componentRef": "pkg:maven/log4j@2.14.1" } } ``` --- ## 7. Verification Algorithm ### 7.1 Offline Verification Steps **Input:** PoE hash or file path **Steps:** 1. **Load PoE Artifact** - Fetch `poe.json` from CAS or local file - Fetch `poe.json.dsse` (DSSE envelope) 2. **Verify DSSE Signature** - Decode DSSE envelope - Extract payload (base64 → canonical JSON) - Verify signature against trusted public keys - Check key validity (not expired, not revoked) 3. **Verify Content Integrity** - Compute BLAKE3-256 hash of canonical JSON - Compare with expected `poe_hash` 4. **(Optional) Verify Rekor Inclusion** - Fetch `poe.json.rekor` (inclusion proof) - Verify proof against Rekor transparency log - Check timestamp is within acceptable window 5. **(Optional) Verify Policy Binding** - Extract `metadata.policy.policyDigest` from PoE - Compare with expected policy digest (from CLI arg or config) 6. **(Optional) Verify OCI Attachment** - Fetch OCI image manifest - Verify PoE is attached to expected image digest 7. **Display Verification Results** - Status: VERIFIED | FAILED - Details: signature validity, hash match, Rekor inclusion, etc. ### 7.2 Verification Pseudocode ```python def verify_poe(poe_hash, options): # Step 1: Load artifacts poe_json = load_from_cas(f"cas://reachability/poe/{poe_hash}/poe.json") dsse_envelope = load_from_cas(f"cas://reachability/poe/{poe_hash}/poe.json.dsse") # Step 2: Verify DSSE signature payload = base64_decode(dsse_envelope["payload"]) signature = base64_decode(dsse_envelope["signatures"][0]["sig"]) key_id = dsse_envelope["signatures"][0]["keyid"] public_key = load_trusted_key(key_id) pae = create_dsse_pae("application/vnd.stellaops.poe+json", payload) if not verify_signature(pae, signature, public_key): return {"status": "FAILED", "reason": "Invalid DSSE signature"} # Step 3: Verify content hash computed_hash = blake3_hash(payload) if computed_hash != poe_hash: return {"status": "FAILED", "reason": "Hash mismatch"} # Step 4: (Optional) Verify Rekor if options.check_rekor: rekor_proof = load_from_cas(f"cas://reachability/poe/{poe_hash}/poe.json.rekor") if not verify_rekor_inclusion(rekor_proof, dsse_envelope): return {"status": "FAILED", "reason": "Rekor inclusion verification failed"} # Step 5: (Optional) Verify policy binding if options.policy_digest: poe_data = json_parse(payload) if poe_data["metadata"]["policy"]["policyDigest"] != options.policy_digest: return {"status": "FAILED", "reason": "Policy digest mismatch"} return {"status": "VERIFIED", "poe": poe_data} ``` ### 7.3 CLI Verification Command ```bash stella poe verify --poe blake3:7a8b9c0d... --offline --check-rekor --check-policy sha256:abc123... # Output: PoE Verification Report ======================= PoE Hash: blake3:7a8b9c0d1e2f... Vulnerability: CVE-2021-44228 Component: pkg:maven/log4j@2.14.1 ✓ DSSE signature valid (key: scanner-signing-2025) ✓ Content hash verified ✓ Rekor inclusion verified (log index: 12345678) ✓ Policy digest matches Subgraph Summary: Nodes: 8 Edges: 12 Paths: 3 (shortest: 4 hops) Status: VERIFIED ``` --- ## 8. Schema Evolution ### 8.1 Versioning Strategy **Current Version:** v1 **Future Versions:** v2, v3, etc. (increment on breaking changes) **Breaking Changes:** - Add/remove required fields - Change field types - Change serialization rules - Change hash algorithm **Non-Breaking Changes:** - Add optional fields - Add new annotations - Improve documentation ### 8.2 Compatibility Matrix | PoE Version | Scanner Version | Verifier Version | Compatible? | |-------------|-----------------|------------------|-------------| | v1 | 1.x.x | 1.x.x | ✓ Yes | | v1 | 1.x.x | 2.x.x | ✓ Yes (forward compat) | | v2 | 2.x.x | 1.x.x | ✗ No (needs v2 verifier) | ### 8.3 Migration Guide (v1 → v2) **TBD when v2 is defined** --- ## 9. Security Considerations ### 9.1 Threat Model | Threat | Mitigation | |--------|------------| | **Signature Forgery** | Use strong key sizes (ECDSA P-256+), hardware key storage (HSM) | | **Hash Collision** | BLAKE3-256 provides 128-bit security against collisions | | **Replay Attack** | Include timestamp in PoE, verify timestamp is recent | | **Key Compromise** | Key rotation every 90 days, monitor Rekor for unexpected entries | | **CAS Tampering** | All artifacts signed with DSSE, verify signatures on fetch | ### 9.2 Key Management **Signing Keys:** - Store in HSM (Hardware Security Module) or KMS (Key Management Service) - Rotate every 90 days - Require multi-party approval for key generation (ceremony) **Verification Keys:** - Distribute via TUF (The Update Framework) or equivalent - Include in offline verification bundles - Pin key IDs in policy configuration ### 9.3 Rekor Considerations **Public Rekor:** - All PoE DSSE envelopes submitted to Rekor by default - Provides immutable timestamp and transparency **Private Rekor Mirror:** - For air-gapped or sovereign environments - Same verification workflow, different Rekor endpoint **Opt-Out:** - Disable Rekor submission in dev/test (set `rekor.enabled: false`) - Still generate DSSE, just don't submit to transparency log --- ## 10. Cross-References - **Sprint:** `docs/implplan/SPRINT_3500_0001_0001_proof_of_exposure_mvp.md` - **Advisory:** `docs/product-advisories/23-Dec-2026 - Binary Mapping as Attestable Proof.md` - **Subgraph Extraction:** `src/Scanner/__Libraries/StellaOps.Scanner.Reachability/SUBGRAPH_EXTRACTION.md` - **Function-Level Evidence:** `docs/reachability/function-level-evidence.md` - **Hybrid Attestation:** `docs/reachability/hybrid-attestation.md` - **DSSE Spec:** https://github.com/secure-systems-lab/dsse --- _Last updated: 2025-12-23. See Sprint 3500.0001.0001 for implementation plan._