Files
git.stella-ops.org/docs/modules/replay/replay-proof-schema.md

18 KiB

Replay Proof Schema

Ownership: Replay Guild, Scanner Guild, Attestor Guild Audience: Service owners, platform engineers, auditors, compliance teams Related: Platform Architecture, Replay Architecture, Facet Sealing, DSSE Specification

This document defines the schema for Replay Proofs - compact, cryptographically verifiable artifacts that attest to deterministic policy evaluation outcomes.


1. Overview

A Replay Proof is a DSSE-signed artifact that proves a policy evaluation produced a specific verdict given a specific set of inputs. Replay proofs enable:

  • Audit trails: Compact proof that a verdict was computed correctly
  • Determinism verification: Re-running with same inputs produces identical output
  • Time-travel debugging: Understand why a past decision was made
  • Compliance evidence: Cryptographic proof for regulatory requirements

2. Replay Bundle Structure

A complete replay bundle consists of three artifacts stored in CAS:

cas://replay/<run-id>/
    manifest.json           # DSSE-signed manifest (this document's focus)
    inputbundle.tar.zst     # Compressed input artifacts
    outputbundle.tar.zst    # Compressed output artifacts

2.1 Directory Layout

<run-id>/
    manifest.json
    inputbundle.tar.zst
        feeds/
            nvd/<date>.json
            osv/<date>.json
            ghsa/<date>.json
        policy/
            bundle.tar
            version.json
        sboms/
            <sbom-id>.spdx.json
            <sbom-id>.cdx.json
        vex/
            <vex-id>.openvex.json
        config/
            lattice.json
            feature-flags.json
        seeds/
            random-seeds.json
            clock-offsets.json
    outputbundle.tar.zst
        verdicts/
            <verdict-id>.json
        findings/
            <finding-id>.json
        merkle/
            verdict-tree.json
            finding-tree.json
        logs/
            replay.log
            trace.json

3. Core Schema Definitions

3.1 ReplayProof

The primary proof artifact - a compact summary suitable for verification:

public sealed record ReplayProof
{
    // Identity
    public required Guid ProofId { get; init; }
    public required Guid RunId { get; init; }
    public required string Subject { get; init; }           // Image digest or SBOM ID

    // Input digest
    public required KnowledgeSnapshotDigest InputDigest { get; init; }

    // Output digest
    public required VerdictDigest OutputDigest { get; init; }

    // Execution metadata
    public required ExecutionMetadata Execution { get; init; }

    // CAS references
    public required BundleReferences Bundles { get; init; }

    // Signature
    public required DateTimeOffset SignedAt { get; init; }
    public required string SignedBy { get; init; }
}

3.2 KnowledgeSnapshotDigest

Cryptographic digest of all inputs:

public sealed record KnowledgeSnapshotDigest
{
    // Component digests
    public required string SbomsDigest { get; init; }           // SHA-256 of sorted SBOM hashes
    public required string VexDigest { get; init; }             // SHA-256 of sorted VEX hashes
    public required string FeedsDigest { get; init; }           // SHA-256 of feed version manifest
    public required string PolicyDigest { get; init; }          // SHA-256 of policy bundle
    public required string LatticeDigest { get; init; }         // SHA-256 of lattice config
    public required string SeedsDigest { get; init; }           // SHA-256 of random seeds

    // Combined root
    public required string RootDigest { get; init; }            // SHA-256 of all component digests

    // Counts for quick comparison
    public required int SbomCount { get; init; }
    public required int VexCount { get; init; }
    public required int FeedCount { get; init; }
}

3.3 VerdictDigest

Cryptographic digest of all outputs:

public sealed record VerdictDigest
{
    public required string VerdictMerkleRoot { get; init; }     // Merkle root of verdicts
    public required string FindingMerkleRoot { get; init; }     // Merkle root of findings
    public required int VerdictCount { get; init; }
    public required int FindingCount { get; init; }
    public required VerdictSummary Summary { get; init; }
}

public sealed record VerdictSummary
{
    public required int Critical { get; init; }
    public required int High { get; init; }
    public required int Medium { get; init; }
    public required int Low { get; init; }
    public required int Informational { get; init; }
    public required int Suppressed { get; init; }
    public required int Total { get; init; }
}

3.4 ExecutionMetadata

Execution environment and timing:

public sealed record ExecutionMetadata
{
    // Timing
    public required DateTimeOffset StartedAt { get; init; }
    public required DateTimeOffset CompletedAt { get; init; }
    public required long DurationMs { get; init; }

    // Engine version
    public required EngineVersion Engine { get; init; }

    // Environment
    public required string HostId { get; init; }
    public required string RuntimeVersion { get; init; }        // e.g., ".NET 10.0.0"
    public required string Platform { get; init; }              // e.g., "linux-x64"

    // Determinism markers
    public required bool DeterministicMode { get; init; }
    public required string ClockMode { get; init; }             // "frozen", "simulated", "real"
    public required string RandomMode { get; init; }            // "seeded", "recorded", "real"
}

public sealed record EngineVersion
{
    public required string Name { get; init; }                  // e.g., "PolicyEngine"
    public required string Version { get; init; }               // e.g., "2.1.0"
    public required string SourceDigest { get; init; }          // SHA-256 of engine source/binary
}

3.5 BundleReferences

CAS URIs to full bundles:

public sealed record BundleReferences
{
    public required string ManifestUri { get; init; }           // cas://replay/<run-id>/manifest.json
    public required string InputBundleUri { get; init; }        // cas://replay/<run-id>/inputbundle.tar.zst
    public required string OutputBundleUri { get; init; }       // cas://replay/<run-id>/outputbundle.tar.zst
    public required string ManifestDigest { get; init; }        // SHA-256 of manifest.json
    public required string InputBundleDigest { get; init; }     // SHA-256 of inputbundle.tar.zst
    public required string OutputBundleDigest { get; init; }    // SHA-256 of outputbundle.tar.zst
    public required long InputBundleSize { get; init; }
    public required long OutputBundleSize { get; init; }
}

4. DSSE Envelope

Replay proofs are wrapped in DSSE envelopes for cryptographic binding:

{
  "payloadType": "application/vnd.stellaops.replay-proof.v1+json",
  "payload": "<base64url-encoded canonical JSON>",
  "signatures": [
    {
      "keyid": "sha256:abc123...",
      "sig": "<base64url-encoded signature>"
    }
  ]
}

4.1 Payload Type URI

  • v1: application/vnd.stellaops.replay-proof.v1+json
  • in-toto compatible: https://stellaops.io/ReplayProof/v1

4.2 Canonical JSON Encoding

Payloads MUST be encoded using RFC 8785 canonical JSON:

  1. Keys sorted lexicographically using Unicode code points
  2. No whitespace between structural characters
  3. No trailing commas
  4. Numbers without unnecessary decimal points or exponents
  5. Strings with minimal escaping (only required characters)

5. Full Manifest Schema

The manifest.json file contains the complete proof plus additional metadata:

{
  "_type": "https://stellaops.io/ReplayManifest/v1",
  "proofId": "550e8400-e29b-41d4-a716-446655440000",
  "runId": "660e8400-e29b-41d4-a716-446655440001",
  "subject": "sha256:abc123def456...",
  "tenant": "acme-corp",
  "inputDigest": {
    "sbomsDigest": "sha256:111...",
    "vexDigest": "sha256:222...",
    "feedsDigest": "sha256:333...",
    "policyDigest": "sha256:444...",
    "latticeDigest": "sha256:555...",
    "seedsDigest": "sha256:666...",
    "rootDigest": "sha256:aaa...",
    "sbomCount": 1,
    "vexCount": 5,
    "feedCount": 3
  },
  "outputDigest": {
    "verdictMerkleRoot": "sha256:bbb...",
    "findingMerkleRoot": "sha256:ccc...",
    "verdictCount": 42,
    "findingCount": 156,
    "summary": {
      "critical": 2,
      "high": 8,
      "medium": 25,
      "low": 12,
      "informational": 3,
      "suppressed": 106,
      "total": 156
    }
  },
  "execution": {
    "startedAt": "2026-01-05T10:00:00.000Z",
    "completedAt": "2026-01-05T10:00:05.123Z",
    "durationMs": 5123,
    "engine": {
      "name": "PolicyEngine",
      "version": "2.1.0",
      "sourceDigest": "sha256:engine123..."
    },
    "hostId": "scanner-worker-01",
    "runtimeVersion": ".NET 10.0.0",
    "platform": "linux-x64",
    "deterministicMode": true,
    "clockMode": "frozen",
    "randomMode": "seeded"
  },
  "bundles": {
    "manifestUri": "cas://replay/660e8400.../manifest.json",
    "inputBundleUri": "cas://replay/660e8400.../inputbundle.tar.zst",
    "outputBundleUri": "cas://replay/660e8400.../outputbundle.tar.zst",
    "manifestDigest": "sha256:manifest...",
    "inputBundleDigest": "sha256:input...",
    "outputBundleDigest": "sha256:output...",
    "inputBundleSize": 10485760,
    "outputBundleSize": 2097152
  },
  "signedAt": "2026-01-05T10:00:06.000Z",
  "signedBy": "scanner-worker-01"
}

6. Verification Protocol

6.1 Quick Verification (Proof Only)

Verify the DSSE signature and check digest consistency:

public async Task<VerificationResult> VerifyProofAsync(
    ReplayProof proof,
    DsseEnvelope envelope,
    CancellationToken ct)
{
    // 1. Verify DSSE signature
    var sigValid = await _dsseVerifier.VerifyAsync(envelope, ct);
    if (!sigValid)
        return VerificationResult.Failed("DSSE signature invalid");

    // 2. Verify input digest consistency
    var inputRoot = ComputeInputRoot(
        proof.InputDigest.SbomsDigest,
        proof.InputDigest.VexDigest,
        proof.InputDigest.FeedsDigest,
        proof.InputDigest.PolicyDigest,
        proof.InputDigest.LatticeDigest,
        proof.InputDigest.SeedsDigest);

    if (inputRoot != proof.InputDigest.RootDigest)
        return VerificationResult.Failed("Input root digest mismatch");

    return VerificationResult.Passed();
}

6.2 Full Verification (With Replay)

Download bundles and re-execute to verify determinism:

public async Task<VerificationResult> VerifyWithReplayAsync(
    ReplayProof proof,
    CancellationToken ct)
{
    // 1. Quick verification first
    var quickResult = await VerifyProofAsync(proof, envelope, ct);
    if (!quickResult.Passed)
        return quickResult;

    // 2. Download bundles from CAS
    var inputBundle = await _cas.DownloadAsync(proof.Bundles.InputBundleUri, ct);
    var outputBundle = await _cas.DownloadAsync(proof.Bundles.OutputBundleUri, ct);

    // 3. Verify bundle digests
    if (ComputeDigest(inputBundle) != proof.Bundles.InputBundleDigest)
        return VerificationResult.Failed("Input bundle digest mismatch");
    if (ComputeDigest(outputBundle) != proof.Bundles.OutputBundleDigest)
        return VerificationResult.Failed("Output bundle digest mismatch");

    // 4. Extract and verify individual input digests
    var inputs = await ExtractInputsAsync(inputBundle, ct);
    var computedInputDigest = ComputeKnowledgeDigest(inputs);
    if (computedInputDigest.RootDigest != proof.InputDigest.RootDigest)
        return VerificationResult.Failed("Computed input digest mismatch");

    // 5. Re-execute policy evaluation
    var replayResult = await _replayEngine.ExecuteAsync(inputs, ct);

    // 6. Compare output digests
    var computedOutputDigest = ComputeVerdictDigest(replayResult);
    if (computedOutputDigest.VerdictMerkleRoot != proof.OutputDigest.VerdictMerkleRoot)
        return VerificationResult.Failed("Verdict Merkle root mismatch - non-deterministic!");

    if (computedOutputDigest.FindingMerkleRoot != proof.OutputDigest.FindingMerkleRoot)
        return VerificationResult.Failed("Finding Merkle root mismatch - non-deterministic!");

    return VerificationResult.Passed();
}

7. Digest Computation

7.1 Input Root Digest

public string ComputeInputRoot(
    string sbomsDigest,
    string vexDigest,
    string feedsDigest,
    string policyDigest,
    string latticeDigest,
    string seedsDigest)
{
    // Concatenate in fixed order with separators
    var combined = string.Join("|",
        sbomsDigest,
        vexDigest,
        feedsDigest,
        policyDigest,
        latticeDigest,
        seedsDigest);

    return ComputeSha256(combined);
}

7.2 SBOM Collection Digest

public string ComputeSbomsDigest(IEnumerable<SbomRef> sboms)
{
    // Sort by ID for determinism
    var sorted = sboms.OrderBy(s => s.SbomId, StringComparer.Ordinal);

    // Concatenate hashes
    var combined = string.Join("|", sorted.Select(s => s.ContentHash));

    return ComputeSha256(combined);
}

7.3 Verdict Merkle Root

public string ComputeVerdictMerkleRoot(IEnumerable<Verdict> verdicts)
{
    // Sort by verdict ID for determinism
    var sorted = verdicts.OrderBy(v => v.VerdictId, StringComparer.Ordinal);

    // Compute leaf hashes
    var leaves = sorted.Select(v => ComputeVerdictLeafHash(v)).ToArray();

    // Build Merkle tree
    return MerkleTreeBuilder.ComputeRoot(leaves);
}

private string ComputeVerdictLeafHash(Verdict verdict)
{
    var canonical = CanonicalJsonSerializer.Serialize(verdict);
    return ComputeSha256(canonical);
}

8. Database Schema

-- Replay proof storage
CREATE TABLE replay_proofs (
    proof_id            UUID PRIMARY KEY,
    run_id              UUID NOT NULL,
    tenant              TEXT NOT NULL,
    subject             TEXT NOT NULL,
    input_root_digest   TEXT NOT NULL,
    output_verdict_root TEXT NOT NULL,
    output_finding_root TEXT NOT NULL,
    execution_json      JSONB NOT NULL,
    bundles_json        JSONB NOT NULL,
    dsse_envelope       JSONB NOT NULL,
    signed_at           TIMESTAMPTZ NOT NULL,
    signed_by           TEXT NOT NULL,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uq_replay_run UNIQUE (run_id)
);

CREATE INDEX ix_replay_proofs_tenant ON replay_proofs (tenant, created_at DESC);
CREATE INDEX ix_replay_proofs_subject ON replay_proofs (subject);
CREATE INDEX ix_replay_proofs_input ON replay_proofs (input_root_digest);

-- Replay verification log
CREATE TABLE replay_verifications (
    verification_id     UUID PRIMARY KEY,
    proof_id            UUID NOT NULL REFERENCES replay_proofs(proof_id),
    tenant              TEXT NOT NULL,
    verification_type   TEXT NOT NULL,     -- 'quick', 'full'
    passed              BOOLEAN NOT NULL,
    failure_reason      TEXT,
    duration_ms         BIGINT NOT NULL,
    verified_at         TIMESTAMPTZ NOT NULL,
    verified_by         TEXT NOT NULL,

    CONSTRAINT fk_proof FOREIGN KEY (proof_id) REFERENCES replay_proofs(proof_id)
);

CREATE INDEX ix_replay_verifications_proof ON replay_verifications (proof_id);

9. CLI Integration

# Verify a replay proof (quick - signature only)
stella verify --proof proof.json

# Verify with full replay
stella verify --proof proof.json --replay

# Verify from CAS URI
stella verify --bundle cas://replay/660e8400.../manifest.json

# Export proof for audit
stella replay export --run-id 660e8400-... --output proof.json

# List proofs for an image
stella replay list --subject sha256:abc123...

# Diff two replay results
stella replay diff --run-id-a 660e8400... --run-id-b 770e8400...

10. API Endpoints

# Get proof by run ID
GET /api/v1/replay/{runId}/proof
Response: ReplayProof (JSON)

# Verify proof
POST /api/v1/replay/{runId}/verify
Request: { "type": "quick" | "full" }
Response: VerificationResult

# List proofs for subject
GET /api/v1/replay/proofs?subject={digest}&tenant={tenant}
Response: ReplayProofSummary[]

# Download bundle
GET /api/v1/replay/{runId}/bundles/{type}
Response: Binary stream (tar.zst)

# Compare two runs
GET /api/v1/replay/diff?runIdA={id}&runIdB={id}
Response: ReplayDiffResult

11. Error Codes

Code Description
REPLAY_001 Proof not found
REPLAY_002 DSSE signature verification failed
REPLAY_003 Input digest mismatch
REPLAY_004 Output digest mismatch (non-deterministic)
REPLAY_005 Bundle not found in CAS
REPLAY_006 Bundle digest mismatch
REPLAY_007 Engine version mismatch
REPLAY_008 Replay execution failed
REPLAY_009 Insufficient permissions
REPLAY_010 Bundle format invalid

12. Migration from v0

If upgrading from pre-v1 replay bundles:

  1. Schema migration: Run migrate-replay-schema.sql
  2. Re-sign existing proofs: Use stella replay migrate --sign to add DSSE envelopes
  3. Verify migration: Run stella replay verify --all to check integrity
  4. Update consumers: Point to new /api/v1/replay endpoints

13. Security Considerations

  1. Key Management: Signing keys managed by Authority service with rotation support
  2. Tenant Isolation: Proofs scoped to tenants; cross-tenant access prohibited
  3. Integrity: All digests use SHA-256; Merkle proofs enable partial verification
  4. Immutability: Proofs cannot be modified once signed
  5. Audit: All verification attempts logged with correlation IDs
  6. Air-gap: Proofs and bundles can be exported for offline verification

14. References


Last updated: 2026-01-05