Policy Verdict Attestations

Status: Live and proven end-to-end on the production evaluate paths (SPRINT_20260704_005, PVA-1…3; proof captured 2026-07-16). Default-ON per PLAN_20260704 decision D1; degrades to unattested when the Attestor is unavailable or misconfigured. The Production wiring section below is authoritative and is the contract sprint 006 consumes; the older “Implementation Guide” further down describes the per-finding batch model and is illustrative, not the live path. Predicate URI: https://stellaops.dev/predicates/policy-verdict@v1 Schema: docs/modules/policy/schemas/stellaops-policy-verdict.v1.schema.json


Overview

This guide describes Stella Ops verdict attestations for engineers integrating the Policy Engine, Attestor, and Evidence Locker, and for auditors who need to verify a verdict’s provenance.

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. Production policy evaluations attach a post-decision verdict-attestation reference to their response (see Production wiring below), wrapped in DSSE envelopes on the Attestor, enabling:


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

ComponentResponsibilityLocation
PolicyExplainTracePolicy evaluation resultStellaOps.Scheduler.Models
VerdictPredicateBuilderMaps trace → predicateStellaOps.Policy.Engine.Attestation
VerdictAttestationServiceSends attestation requestsStellaOps.Policy.Engine.Attestation
VerdictAttestationCoordinatorMaps a live evaluation → trace → attestation ref; guarantees non-interferenceStellaOps.Policy.Engine.Attestation
VerdictControllerReceives, signs (server-side DSSE), storesStellaOps.Attestor (POST /internal/api/v1/attestations/verdict)
Evidence LockerStores and indexes verdictsStellaOps.EvidenceLocker

Production wiring (SPRINT_20260704_005) — the live evaluate paths and attestationRef

This is the authoritative description of how verdict attestations reach the running stack. VerdictAttestationCoordinator attaches a post-decision attestation reference to the two production evaluation responses; it is registered as a singleton in Policy.Engine/Program.cs.

Which responses carry the ref

PathEndpointResponse typeConsumed by
Pack-revision evaluate (primary)POST /api/policy/packs/{packId}/revisions/{version}/evaluatePolicyEvaluationResponse.attestationRefRelease-orchestrator’s HttpPolicyEngineGateClient; sprint 006 records the ref + basis into gate_decisions
Drift gate (secondary)POST /api/v1/policy/gate/evaluateGateEvaluateResponse.AttestationRefCLI/CI callers

The pack-revision path is the one RO’s gate client actually calls, so it is the load-bearing path for the evidence chain. For the pack path the attestation is computed once per unique (bundleDigest|subject) and cached with the response, so a cache hit returns the same attestation state.

attestationRef shape (contract)

Both responses expose the same object (nullable / additive):

{
  "status": "attested",                    // "attested" | "unattested"
  "verdictId": "verdict-<sha256>",         // Attestor-assigned id; null when unattested
  "attestationUri": "/api/v1/verdicts/verdict-<sha256>", // retrieval URI; null when unattested
  "predicateType": "https://stellaops.dev/predicates/policy-verdict@v1",
  "determinismHash": "sha256:<hex>",       // reproducible-by-reference anchor; present even when unattested
  "rekorLogIndex": null,                   // set only when anchored (OnlineRekor); null in the offline posture
  "reason": null                           // populated with the failure reason when status = "unattested"
}

Rules a consumer (sprint 006) can rely on:

Non-interference (binding)

Attestation is strictly post-decision and never changes the verdict. Every failure mode — disabled, predicate-build error, Attestor unreachable/501/503, or the service configured to throw (FailOnError) — is contained by the coordinator and degrades to unattested with a loud log. A verdict response’s decision/status is byte-for-byte identical whether or not attestation succeeds.

What the predicate pins (basis) — PVA-2

The predicate metadata records the real basis available at the evaluation data layer, and marks everything it genuinely does not have as absent (never fabricated):

Metadata keyPack pathDrift path
policypackdigestcompiled bundle digest (real)absent (drift gate has no compiled bundle)
findingshashsha256 over the matched CVE set (real)sha256 over the gate-result set (real)
totalfindingscves, denylistrulecount, subjectreleaseidreal from the evaluation evidence
baselinesnapshotid, targetsnapshotid, deltaidreal snapshot/delta refs
advisorysnapshotdigest, vexsnapshotdigest, feedsnapshotdigestabsent (finding C4 — not at this layer yet)absent

The determinismHash is computed over the full canonical decision including this basis metadata (SPRINT_20260704_005 fixed VerdictPredicateBuilder so the stamped hash equals ComputeDeterminismHash(predicate)). It is therefore:

Default-on posture and the off-switch

Per PLAN_20260704 D1 the base stack signs verdicts by default. In code this is VerdictAttestationOptions.Enabled = true with TransparencyMode = Offline (offline-first; also required so the production ValidateOnStart guard passes, since policy-engine runs with the default ASP.NET environment = Production). Operators disable it per-profile with VerdictAttestation:Enabled=false, or opt into transparency-log anchoring with TransparencyMode=OnlineRekor plus a real Rekor submission client.

Live status (verified 2026-07-16)

The base compose profile provisions the Policy Engine’s offline Attestor URL/key, the Attestor trust roster, and signed tenant-bound service identities for both internal hops. The coherent Policy Engine, Attestor, and Evidence Locker Web images were deployed and a fresh pack-revision evaluation returned status="attested". Attestor returned HTTP 201, Evidence Locker durably stored the record, and gateway retrieval returned the exact pack id/version, evaluation run id, subject, and determinism hash.

The sanitized evidence is under docs/qa/feature-checks/runs/2026-07-16-policy-verdict-attestation-live/. The separate POST /api/v1/verdicts/{id}/verify forcing function currently fails closed when verification trust roots are unavailable; do not interpret successful creation/retrieval as proof of offline signature verification until that trust-root lane is configured and proven.


Predicate Schema

See stellaops-policy-verdict.v1.schema.json for the complete JSON schema.

Example Predicate

{
  "_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:

{
  "payload": "<base64(canonicalJson(predicate))>",
  "payloadType": "application/vnd.stellaops.policy-verdict+json",
  "signatures": [
    {
      "keyid": "sha256:keypair123...",
      "sig": "<base64(signature)>"
    }
  ]
}

API Reference

Create Verdict Attestation (Internal)

Note: This is an internal API called by the Policy Engine, not exposed externally.

POST /internal/api/v1/attestations/verdict
Content-Type: application/json

Request:

{
  "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:

{
  "verdictId": "verdict:run:P-7:20251223T140500Z:finding-42",
  "attestationUri": "/api/v1/verdicts/verdict:run:P-7:20251223T140500Z:finding-42",
  "rekorLogIndex": 12345678
}

Retrieve Verdict Attestation

GET /api/v1/verdicts/{verdictId}

Response:

{
  "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": "<base64>",
    "payloadType": "application/vnd.stellaops.policy-verdict+json",
    "signatures": [...]
  },
  "rekorLogIndex": 12345678,
  "createdAt": "2025-12-23T14:06:05+00:00"
}

List Verdicts for Policy Run

GET /api/v1/runs/{runId}/verdicts?status=blocked&limit=50

Query Parameters:

Response:

{
  "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

POST /api/v1/verdicts/{verdictId}/verify

Response:

{
  "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

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)

stella verdict verify verdict-12345.json --public-key ./pubkey.pem

# Output:
# ✓ Signature valid
# ✓ Predicate schema valid
# ✓ Determinism hash matches

List Verdicts for Run

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

stella verdict download verdict:run:P-7:20251223T140500Z:finding-42 --output ./verdict.json

Implementation Guide

Note: the snippets below illustrate the per-finding batch model and predate the live wiring. The authoritative production integration is the Production wiring section above (VerdictAttestationCoordinator on the pack-revision + drift-gate evaluate responses). Treat the PolicyRunExecutionService example as illustrative, not the shipped code path.

Policy Engine Integration

The Policy Engine’s PolicyRunExecutionService calls VerdictAttestationService after evaluating each finding:

public class PolicyRunExecutionService
{
    private readonly VerdictAttestationService _verdictAttestationService;

    public async Task<PolicyRunStatus> 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:

public class VerdictAttestationHandler
{
    public async Task<AttestationResult> 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:

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(canonical verdict decision)

The canonical verdict decision includes the predicate type, tenant, policy identity and version, finding identity, verdict status and rationale, rule chain, evidence entries, VEX impacts, reachability details, EWS, budget, policy-bundle metadata, and unknown-detail metadata. It excludes run identifiers and wall-clock evaluation timestamps so replayed equivalent decisions hash identically.

Determinism Validation

# 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:

Runtime Rekor Configuration

Policy Engine treats verdict Rekor anchoring as optional, but the transparency posture must be explicit in production whenever VerdictAttestation:Enabled=true.

SettingRequired behavior
VerdictAttestation:TransparencyMode=OfflineDo not submit verdicts to Rekor; export signed evidence for offline verification
VerdictAttestation:TransparencyMode=OnlineRekorRekor anchoring is intended and must use a real Rekor client
VerdictAttestation:RekorEnabled=trueRequires TransparencyMode=OnlineRekor

VerdictAttestation:RekorEnabled=false remains the offline default. The built-in UnsupportedVerdictRekorClient is not a mock success path: accidental submission attempts return a failed result, score-gate requests that explicitly ask for Rekor anchoring fail instead of returning an unanchored verdict, and startup rejects VerdictAttestation:RekorEnabled=true while that unsupported client is the only registered implementation.

Future real Rekor support must replace the unsupported client, provide the Rekor endpoint and credentials through configuration/secrets, and add host tests proving submissions are accepted by the real client.

Policy decision signing posture

Policy decision attestations require the Signer service by default. If PolicyDecisionAttestation:UseSignerService=true and no signer client is registered, creation fails closed. If Rekor submission is requested and submission fails, attestation creation fails rather than returning an unanchored success.

PolicyDecisionAttestation:AllowLocalDevelopmentSigning=true is an explicit dev/test-only escape hatch for deterministic local hash signatures. It must not be enabled in production or protected profiles, and local signatures are not a substitute for Signer-backed DSSE verification.


Performance Considerations

Volume

Large policy runs can produce millions of verdicts:

Storage


Security

Signing Keys

Access Control


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:

# 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:

# Verify with correct public key
stella verdict verify verdict.json --public-key ./correct-pubkey.pem

# Check payload digest
stella digest compute verdict.json

References