# Policy Verdict Attestations > **Status:** Implementation in Progress (SPRINT_3000_0100_0001) > **Predicate URI:** `https://stellaops.dev/predicates/policy-verdict@v1` > **Schema:** [`docs/schemas/stellaops-policy-verdict.v1.schema.json`](../schemas/stellaops-policy-verdict.v1.schema.json) --- ## Overview **Verdict Attestations** provide cryptographically-bound proof that a policy evaluation verdict (passed/warned/blocked/quieted) was issued for a specific finding at a specific time with specific evidence. Every policy run produces signed verdict attestations wrapped in DSSE envelopes, enabling: - **Trust & Integrity:** Cryptographic proof verdicts haven't been tampered with - **Audit & Compliance:** Standalone artifacts for incident review, regulatory compliance - **Automation:** Downstream tools can verify verdict authenticity before acting - **Transparency:** Optional anchoring in Rekor transparency log --- ## Architecture ### Flow ``` Policy Engine ↓ (evaluates finding) PolicyExplainTrace ↓ (mapped to) VerdictPredicate (canonical JSON) ↓ (sent to) Attestor Service ↓ (signs with DSSE) Verdict Attestation (signed envelope) ↓ (stored in) Evidence Locker (PostgreSQL + object store) ↓ (optionally anchored in) Rekor Transparency Log ``` ### Components | Component | Responsibility | Location | |-----------|---------------|----------| | **PolicyExplainTrace** | Policy evaluation result | `StellaOps.Scheduler.Models` | | **VerdictPredicateBuilder** | Maps trace → predicate | `StellaOps.Policy.Engine.Attestation` | | **VerdictAttestationService** | Sends attestation requests | `StellaOps.Policy.Engine.Attestation` | | **VerdictAttestationHandler** | Receives, validates, signs | `StellaOps.Attestor` | | **Evidence Locker** | Stores and indexes verdicts | `StellaOps.EvidenceLocker` | --- ## Predicate Schema See [`stellaops-policy-verdict.v1.schema.json`](../schemas/stellaops-policy-verdict.v1.schema.json) for the complete JSON schema. ### Example Predicate ```json { "_type": "https://stellaops.dev/predicates/policy-verdict@v1", "tenantId": "tenant-alpha", "policyId": "P-7", "policyVersion": 4, "runId": "run:P-7:20251223T140500Z:1b2c3d4e", "findingId": "finding:sbom:S-42/pkg:npm/lodash@4.17.21", "evaluatedAt": "2025-12-23T14:06:01+00:00", "verdict": { "status": "blocked", "severity": "critical", "score": 19.5, "rationale": "CVE-2025-12345 exploitable via lodash.template with confirmed reachability" }, "ruleChain": [ { "ruleId": "rule-allow-known", "action": "allow", "decision": "skipped" }, { "ruleId": "rule-block-critical", "action": "block", "decision": "matched", "score": 19.5 } ], "evidence": [ { "type": "advisory", "reference": "CVE-2025-12345", "source": "nvd", "status": "affected", "digest": "sha256:abc123...", "weight": 1.0 }, { "type": "vex", "reference": "vex:ghsa-2025-0001", "source": "vendor", "status": "not_affected", "digest": "sha256:def456...", "weight": 0.5, "metadata": {} } ], "vexImpacts": [ { "statementId": "vex:ghsa-2025-0001", "provider": "vendor", "status": "not_affected", "accepted": false, "justification": "VEX statement contradicts confirmed reachability analysis" } ], "reachability": { "status": "confirmed", "paths": [ { "entrypoint": "GET /api/users", "sink": "lodash.template", "confidence": "high", "digest": "sha256:path123..." } ] }, "metadata": { "componentPurl": "pkg:npm/lodash@4.17.21", "sbomId": "sbom:S-42", "traceId": "01HE0BJX5S4T9YCN6ZT0", "determinismHash": "sha256:..." } } ``` ### DSSE Envelope The predicate is wrapped in a DSSE envelope: ```json { "payload": "", "payloadType": "application/vnd.stellaops.policy-verdict+json", "signatures": [ { "keyid": "sha256:keypair123...", "sig": "" } ] } ``` --- ## API Reference ### Create Verdict Attestation (Internal) **Note:** This is an internal API called by the Policy Engine, not exposed externally. ```http POST /internal/api/v1/attestations/verdict Content-Type: application/json ``` **Request:** ```json { "predicateType": "https://stellaops.dev/predicates/policy-verdict@v1", "predicate": "{...}", "subject": [ { "name": "finding:sbom:S-42/pkg:npm/lodash@4.17.21", "digest": { "sha256": "abc123..." } } ] } ``` **Response:** ```json { "verdictId": "verdict:run:P-7:20251223T140500Z:finding-42", "attestationUri": "/api/v1/verdicts/verdict:run:P-7:20251223T140500Z:finding-42", "rekorLogIndex": 12345678 } ``` ### Retrieve Verdict Attestation ```http GET /api/v1/verdicts/{verdictId} ``` **Response:** ```json { "verdictId": "verdict:run:P-7:20251223T140500Z:finding-42", "tenantId": "tenant-alpha", "policyRunId": "run:P-7:20251223T140500Z:1b2c3d4e", "findingId": "finding:sbom:S-42/pkg:npm/lodash@4.17.21", "verdictStatus": "blocked", "evaluatedAt": "2025-12-23T14:06:01+00:00", "envelope": { "payload": "", "payloadType": "application/vnd.stellaops.policy-verdict+json", "signatures": [...] }, "rekorLogIndex": 12345678, "createdAt": "2025-12-23T14:06:05+00:00" } ``` ### List Verdicts for Policy Run ```http GET /api/v1/runs/{runId}/verdicts?status=blocked&limit=50 ``` **Query Parameters:** - `status`: Filter by verdict status (passed/warned/blocked/quieted) - `severity`: Filter by severity (critical/high/medium/low) - `limit`: Maximum results (default 50, max 200) - `offset`: Pagination offset **Response:** ```json { "verdicts": [ { "verdictId": "verdict:run:P-7:20251223T140500Z:finding-42", "findingId": "finding:sbom:S-42/pkg:npm/lodash@4.17.21", "verdictStatus": "blocked", "severity": "critical", "evaluatedAt": "2025-12-23T14:06:01+00:00" } ], "pagination": { "total": 234, "limit": 50, "offset": 0 } } ``` ### Verify Verdict Signature ```http POST /api/v1/verdicts/{verdictId}/verify ``` **Response:** ```json { "verdictId": "verdict:run:P-7:20251223T140500Z:finding-42", "signatureValid": true, "verifiedAt": "2025-12-23T15:00:00+00:00", "verifications": [ { "keyId": "sha256:keypair123...", "algorithm": "ed25519", "valid": true } ], "rekorVerification": { "logIndex": 12345678, "inclusionProofValid": true, "verifiedAt": "2025-12-23T15:00:01+00:00" } } ``` --- ## CLI Usage ### Retrieve Verdict ```bash stella verdict get verdict:run:P-7:20251223T140500Z:finding-42 # Output: # Verdict: blocked (critical, score 19.5) # Finding: finding:sbom:S-42/pkg:npm/lodash@4.17.21 # Evaluated: 2025-12-23T14:06:01+00:00 # Signature: ✓ Verified (ed25519) # Rekor: ✓ Anchored (log index 12345678) ``` ### Verify Verdict Signature (Offline) ```bash stella verdict verify verdict-12345.json --public-key ./pubkey.pem # Output: # ✓ Signature valid # ✓ Predicate schema valid # ✓ Determinism hash matches ``` ### List Verdicts for Run ```bash stella verdict list --run run:P-7:20251223T140500Z:1b2c3d4e --status blocked # Output: # 234 verdicts found (showing 50) # # verdict:...:finding-42 | blocked | critical | CVE-2025-12345 | lodash@4.17.21 # verdict:...:finding-78 | blocked | high | CVE-2025-99999 | express@4.18.0 # ... ``` ### Download Verdict Envelope ```bash stella verdict download verdict:run:P-7:20251223T140500Z:finding-42 --output ./verdict.json ``` --- ## Implementation Guide ### Policy Engine Integration The Policy Engine's `PolicyRunExecutionService` calls `VerdictAttestationService` after evaluating each finding: ```csharp public class PolicyRunExecutionService { private readonly VerdictAttestationService _verdictAttestationService; public async Task ExecuteAsync(PolicyRunRequest request) { // ... policy evaluation logic foreach (var trace in explainTraces) { // Emit verdict attestation if (_options.VerdictAttestationsEnabled) { var verdictId = await _verdictAttestationService.AttestVerdictAsync(trace); _logger.LogInformation("Verdict attestation created: {VerdictId}", verdictId); } } // ... continue } } ``` ### Attestor Handler The Attestor receives attestation requests via internal API: ```csharp public class VerdictAttestationHandler { public async Task HandleAsync(AttestationRequest request) { // 1. Validate predicate schema await _schemaValidator.ValidateAsync(request.Predicate, request.PredicateType); // 2. Create DSSE envelope var envelope = await _dsseService.SignAsync(request); // 3. Store in Evidence Locker var verdictId = await _evidenceLocker.StoreVerdictAsync(envelope); // 4. Optional: Anchor in Rekor long? rekorLogIndex = null; if (_options.RekorEnabled) { rekorLogIndex = await _rekorClient.UploadAsync(envelope); } return new AttestationResult { VerdictId = verdictId, RekorLogIndex = rekorLogIndex }; } } ``` ### Evidence Locker Storage Verdicts are stored in PostgreSQL with full-text index and object store reference: ```sql INSERT INTO verdict_attestations ( verdict_id, tenant_id, run_id, policy_id, policy_version, finding_id, verdict_status, evaluated_at, envelope, predicate_digest, rekor_log_index ) VALUES ( 'verdict:run:P-7:20251223T140500Z:finding-42', 'tenant-alpha', 'run:P-7:20251223T140500Z:1b2c3d4e', 'P-7', 4, 'finding:sbom:S-42/pkg:npm/lodash@4.17.21', 'blocked', '2025-12-23T14:06:01+00:00', '{"payload": "...", "signatures": [...]}', 'sha256:abc123...', 12345678 ); ``` --- ## Determinism Verdict attestations are **deterministic** when evaluated with identical inputs: 1. **Input Normalization:** Policy inputs (SBOMs, advisories, VEX, environment) are canonicalized 2. **Canonical JSON:** Predicates serialize with lexicographic key ordering 3. **Stable Sorting:** Evidence, rule chains, and arrays are sorted deterministically 4. **Hash Computation:** `determinismHash` = SHA256(sorted digests of all evidence) ### Determinism Validation ```bash # Compute determinism hash for policy run stella policy determinism-hash run:P-7:20251223T140500Z:1b2c3d4e # Compare with original run's hash # If hashes match → deterministic # If hashes differ → identify divergence source ``` --- ## Offline Support Verdict attestations support **air-gapped deployments**: - **Signature Verification:** Uses bundled public keys, no network required - **Rekor Optional:** Transparency log anchoring can be disabled - **Bundle Export:** Verdicts included in evidence packs for offline transfer --- ## Performance Considerations ### Volume Large policy runs can produce **millions of verdicts**: - **Batch Signing:** Group verdicts into batches, sign batch manifest - **Async Processing:** Attestation pipeline runs asynchronously from policy evaluation - **Compression:** Verdict envelopes use compact JSON and gzip compression ### Storage - **Hot Storage:** Recent verdicts (< 30 days) in PostgreSQL - **Cold Storage:** Older verdicts archived to object store - **Indexing:** Indexes on `run_id`, `finding_id`, `tenant_id`, `evaluated_at` --- ## Security ### Signing Keys - **Key Management:** Keys stored in KMS or CryptoPro (GOST support) - **Key Rotation:** Verdicts include `keyId`, support multi-signature - **Offline Keys:** Support for offline signing ceremonies (air-gapped) ### Access Control - **RBAC:** `policy:verdict:read` scope required for API access - **Tenant Isolation:** Verdicts scoped by `tenantId`, cross-tenant queries blocked - **Audit Trail:** All verdict retrievals logged with actor, timestamp --- ## Troubleshooting ### Verdict Attestation Failed **Symptom:** Policy run completes but no verdicts created **Causes:** 1. Attestor service unavailable → Check health endpoint 2. Schema validation failure → Check predicate structure 3. Signing key unavailable → Verify KMS connectivity **Resolution:** ```bash # Check attestor health curl http://attestor:8080/health # Verify predicate schema stella schema validate policy-verdict.json --schema stellaops-policy-verdict.v1.schema.json # Retry attestation stella policy rerun run:P-7:20251223T140500Z:1b2c3d4e --attestations-only ``` ### Signature Verification Failed **Symptom:** `POST /api/v1/verdicts/{id}/verify` returns `signatureValid: false` **Causes:** 1. Public key mismatch → Verify correct key for `keyId` 2. Payload tampering → Compare digest with original 3. Clock skew → Check timestamp validity window **Resolution:** ```bash # Verify with correct public key stella verdict verify verdict.json --public-key ./correct-pubkey.pem # Check payload digest stella digest compute verdict.json ``` --- ## References - [DSSE Specification](https://github.com/secure-systems-lab/dsse) - [Rekor Transparency Log](https://docs.sigstore.dev/rekor/overview/) - [Policy Engine Architecture](../modules/policy/architecture.md) - [Attestor Architecture](../modules/attestor/architecture.md) - [Evidence Locker Architecture](../modules/evidence-locker/architecture.md)