20 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:
- Keys sorted lexicographically using Unicode code points
- No whitespace between structural characters
- No trailing commas
- Numbers without unnecessary decimal points or exponents
- 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
9.1 stella prove
Generate a replay proof for an image verdict (RPL-015 through RPL-019):
# Generate proof using local bundle (offline mode)
stella prove --image sha256:abc123 --bundle /path/to/bundle --output compact
# Generate proof at specific point in time
stella prove --image sha256:abc123 --at 2026-01-05T10:00:00Z
# Generate proof using explicit snapshot ID
stella prove --image sha256:abc123 --snapshot snap-001
# Output in JSON format
stella prove --image sha256:abc123 --bundle /path/to/bundle --output json
# Full table output with all fields
stella prove --image sha256:abc123 --bundle /path/to/bundle --output full
Options:
-i, --image <digest>- Image digest (sha256:...) - required-a, --at <timestamp>- Point-in-time for snapshot lookup (ISO 8601)-s, --snapshot <id>- Explicit snapshot ID-b, --bundle <path>- Local bundle path (offline mode)-o, --output <format>- Output format: compact, json, full (default: compact)-v, --verbose- Enable verbose output
Exit Codes:
| Code | Name | Description |
|---|---|---|
| 0 | Success | Replay successful, verdict matches expected |
| 1 | InvalidInput | Invalid image digest or options |
| 2 | SnapshotNotFound | No snapshot found for image/timestamp |
| 3 | BundleNotFound | Bundle not found in CAS |
| 4 | ReplayFailed | Verdict replay failed |
| 5 | VerdictMismatch | Replayed verdict differs from expected |
| 6 | ServiceUnavailable | Timeline or bundle service unavailable |
| 7 | FileNotFound | Local bundle path not found |
| 8 | InvalidBundle | Bundle manifest invalid |
| 99 | SystemError | Unexpected error |
| 130 | Cancelled | Operation cancelled |
9.2 stella verify
# 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
9.3 stella replay
# 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:
- Schema migration: Run
migrate-replay-schema.sql - Re-sign existing proofs: Use
stella replay migrate --signto add DSSE envelopes - Verify migration: Run
stella replay verify --allto check integrity - Update consumers: Point to new
/api/v1/replayendpoints
13. Security Considerations
- Key Management: Signing keys managed by Authority service with rotation support
- Tenant Isolation: Proofs scoped to tenants; cross-tenant access prohibited
- Integrity: All digests use SHA-256; Merkle proofs enable partial verification
- Immutability: Proofs cannot be modified once signed
- Audit: All verification attempts logged with correlation IDs
- Air-gap: Proofs and bundles can be exported for offline verification
14. References
- DSSE Specification
- RFC 8785 - JSON Canonicalization
- in-toto Attestation Framework
- SLSA Provenance
- Platform Architecture
- Facet Sealing Architecture
Last updated: 2026-01-05