Stella Ops Reproducibility Specification

Audience: engineers and auditors who need to reproduce, verify, or attest a Stella Ops security artifact byte-for-byte.

This document defines the reproducibility guarantees, the verdict-identity computation, the canonical serialization and attestation formats, and the offline replay procedures for Stella Ops artifacts. src/ is the source of truth; where this spec was reconciled against code, the source pointers are inline and the changelog records what changed.

Overview

StellaOps provides deterministic, reproducible outputs for all security artifacts:

Core Guarantee: Given identical inputs (image digest, advisory feeds, policies, tool versions), StellaOps produces byte-for-byte identical outputs with matching content-addressed identifiers.

Verdict Identity Formula

Content-Addressed Verdict ID

All policy verdicts use content-addressed identifiers computed as:

VerdictId = "verdict:sha256:" + HexLower(SHA256(CanonicalJson(VerdictPayload)))

Where VerdictPayload is a JSON object with the following structure:

{
  "_canonVersion": "stella:canon:v1",
  "deltaId": "<content-addressed delta ID>",
  "blockingDrivers": [
    {
      "cveId": "CVE-...",
      "description": "...",
      "purl": "pkg:...",
      "severity": "Critical|High|Medium|Low",
      "type": "new-reachable-cve|..."
    }
  ],
  "warningDrivers": [...],
  "appliedExceptions": ["EXCEPTION-001", ...],
  "gateLevel": "G0|G1|G2|G3|G4"
}

Determinism guarantees:

VerdictIdGenerator Implementation

The VerdictIdGenerator class (namespace StellaOps.Policy.Deltas, source src/Policy/__Libraries/StellaOps.Policy/Deltas/VerdictIdGenerator.cs) computes deterministic verdict IDs. The driver payload is sorted by Type, then CveId, then Purl, then Severity (all Ordinal), _canonVersion is set to stella:canon:v1 (CanonVersion.Current), and the hash is hex-lowercased. Note that the gate level fed into the hash is the verdict’s RecommendedGate:

// Create a verdict with content-addressed ID
var verdict = new DeltaVerdictBuilder()
    .AddBlockingDriver(new DeltaDriver
    {
        Type = "new-reachable-cve",
        CveId = "CVE-2024-001",
        Severity = DeltaDriverSeverity.Critical,
        Description = "Critical CVE is now reachable"
    })
    .Build("delta:sha256:abc123...");

// VerdictId is deterministic:
// verdict.VerdictId == "verdict:sha256:..."

// Recompute for verification:
var generator = new VerdictIdGenerator();
var recomputed = generator.ComputeVerdictId(verdict);
Debug.Assert(recomputed == verdict.VerdictId);

Input Stamps

The InputStamps record (source src/__Tests/__Libraries/StellaOps.Testing.Determinism/Determinism/DeterminismManifest.cs) captures the provenance of all inputs as the optional inputs block of a Determinism Manifest. All fields are optional:

{
  "feedSnapshotHash": "sha256:abc123...",
  "policyManifestHash": "sha256:def456...",
  "sourceCodeHash": "sha256:789ghi...",
  "dependencyLockfileHash": "sha256:lock...",
  "baseImageDigest": "sha256:jkl012...",
  "vexDocumentHashes": ["sha256:mno345..."],
  "custom": {}
}

Note: InputStamps does not carry a toolchainVersion field — toolchain/version information lives in the manifest’s separate toolchain block (see below). At replay time, the runtime input set is resolved by the InputManifest record in StellaOps.Replay.Core (src/Replay/__Libraries/StellaOps.Replay.Core/InputManifestResolver.cs), which carries FeedSnapshotHash, PolicyManifestHash, SourceCodeHash, BaseImageDigest, VexDocumentHashes, ToolchainVersion, RandomSeed, and TimestampOverride.

Determinism Manifest

The DeterminismManifest record (schemaVersion "1.0"; source src/__Tests/__Libraries/StellaOps.Testing.Determinism/Determinism/DeterminismManifest.cs) tracks artifact reproducibility. Required: schemaVersion, artifact, canonicalHash, toolchain, generatedAt. Optional: inputs (the InputStamps block), reproducibility, verification (regeneration command / expected hash / baseline), and signatures:

{
  "schemaVersion": "1.0",
  "artifact": {
    "type": "verdict",
    "name": "scan-verdict",
    "version": "2025-12-24T12:00:00Z",
    "format": "StellaOps.DeltaVerdict@1"
  },
  "canonicalHash": {
    "algorithm": "SHA-256",
    "value": "abc123def456...",
    "encoding": "hex"
  },
  "inputs": {
    "feedSnapshotHash": "sha256:...",
    "policyManifestHash": "sha256:...",
    "baseImageDigest": "sha256:..."
  },
  "toolchain": {
    "platform": ".NET 10.0",
    "components": [
      {"name": "StellaOps.Scanner", "version": "1.0.0"},
      {"name": "StellaOps.Policy", "version": "1.0.0"}
    ]
  },
  "reproducibility": {
    "deterministicSeed": 42,
    "clockFixed": true,
    "orderingGuarantee": "stable-sort",
    "normalizationRules": ["UTF-8", "LF", "canonical-json"]
  },
  "generatedAt": "2025-12-24T12:00:00Z"
}

Canonical JSON Serialization

Content-addressed hashing uses the CanonJson canonicalizer (source src/__Libraries/StellaOps.Canonical.Json/), aligned with RFC 8785 (JSON Canonicalization Scheme):

  1. Object keys sorted with Ordinal comparison (case-sensitive, recursive)
  2. No whitespace or formatting variations
  3. Minimal escaping — the writer uses JavaScriptEncoder.UnsafeRelaxedJsonEscaping, so non-ASCII characters are emitted as raw UTF-8 (NOT \uXXXX-escaped)
  4. Strings and property names are NFC-normalized (Unicode FormC) before emission
  5. Consistent number formatting (e.g. negative-zero normalized to 0)
  6. UTF-8 encoding without BOM

DSSE Attestation Format

Envelope Structure

{
  "payloadType": "application/vnd.in-toto+json",
  "payload": "<base64url-encoded statement>",
  "signatures": [
    {
      "keyid": "sha256:...",
      "sig": "<base64url-encoded signature>"
    }
  ]
}

In-toto Statement

The in-toto statement skeleton (_type, subject[].name/digest, predicateType, predicate) is defined by InTotoStatement (src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Statements/InTotoStatement.cs). The _type is always https://in-toto.io/Statement/v1.

The delta verdict attestation uses the predicate type delta-verdict.stella/v1(constant DeltaVerdictPredicate.PredicateType in src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Predicates/DeltaVerdictPredicate.cs), and the predicate body is a DeltaVerdictPredicate:

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    {
      "name": "registry.example.com/image:tag",
      "digest": {"sha256": "abc123..."}
    }
  ],
  "predicateType": "delta-verdict.stella/v1",
  "predicate": {
    "beforeRevisionId": "...",
    "afterRevisionId": "...",
    "hasMaterialChange": true,
    "priorityScore": 7.5,
    "changes": [],
    "beforeVerdictDigest": "sha256:...",
    "afterVerdictDigest": "sha256:...",
    "comparedAt": "2025-12-24T12:00:00Z"
  }
}

Reconciliation note: earlier drafts referenced a predicate type https://stellaops.io/attestation/verdict/v1 with a {verdictId, status, gate, inputs, evidence} body. No such predicate type exists in src/Attestor. The implemented verdict predicate types are delta-verdict.stella/v1 (Smart-Diff delta verdict) and verdict.stella/v1 (used by the Attestor bundling/offline verifier paths). Other implemented predicate types include unknowns-budget.stella/v1, stella.ops/sbom-delta@v1, https://stellaops.io/attestation/budget-check/v1, and https://slsa.dev/provenance/v1.

Sigstore Bundle Format

StellaOps produces Sigstore bundles (v0.3) for offline verification. The model is SigstoreBundle (source src/Attestor/__Libraries/StellaOps.Attestor.Bundle/Models/SigstoreBundle.cs); the media-type constant is SigstoreBundleConstants.MediaTypeV03. The JSON field is mediaType (no $ prefix):

{
  "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
  "verificationMaterial": {
    "certificate": {...},
    "tlogEntries": [{
      "logIndex": "12345",
      "logId": {...},
      "inclusionProof": {...}
    }]
  },
  "dsseEnvelope": {...}
}

A legacy v0.2 media type (application/vnd.dev.sigstore.bundle+json;version=0.2) is also recognized.

Replay Procedure

Prerequisites

  1. Offline bundle containing:
    • Advisory feed snapshot
    • Policy pack
    • VEX documents
    • Tool binaries (pinned versions)

Steps

# 1. Import the offline kit (verifies DSSE + Rekor receipt by default)
stella offline import --bundle offline-kit.tar.zst

# 2. Replay the scan with explicitly pinned input hashes.
#    --artifact/--manifest/--feeds/--policy are REQUIRED and take *hashes*,
#    not directory paths. Determinism is driven by the pinned input hashes
#    (and, where applicable, the manifest's RandomSeed / TimestampOverride),
#    not by environment variables.
stella scan replay \
  --artifact sha256:abc123 \
  --manifest sha256:run-manifest... \
  --feeds sha256:feed-snapshot... \
  --policy sha256:policy-ruleset... \
  --snapshot <knowledge-snapshot-id> \
  --offline \
  --verify-inputs \
  -o ./replay/verdict.json

# 3. Verify the DSSE attestation / sigstore bundle offline
stella attest verify \
  --envelope ./replay/bundle.sigstore \
  --policy ./verification-policy.json \
  --root ./trust-root.pem \
  --transparency-checkpoint ./rekor-checkpoint.json \
  --explain

Reconciliation notes:

  • stella offline exposes import and status subcommands (src/Cli/StellaOps.Cli/Commands/OfflineCommandGroup.cs); there is no offline extract.
  • Deterministic replay is stella scan replay (src/Cli/StellaOps.Cli/Commands/CommandFactory.cs), not stella scan --image ... --feeds <dir>. Its required --feeds/--policy arguments are content hashes.
  • There is no top-level stella verify command and no STELLAOPS_DETERMINISTIC_SEED / STELLAOPS_CLOCK_FIXED environment variables in the codebase; the seed/clock-fixing inputs are carried inside the replay input manifest.
  • stella attest verify (src/Cli/StellaOps.Cli/Commands/CommandFactory.cs) takes --envelope/-e (DSSE envelope or sigstore bundle), --policy (a JSON policy file), --root (PEM trust root), --transparency-checkpoint, plus --require-timestamp / --max-skew / --explain.

Verification Policy

The verification policy consumed by stella attest verify is a JSON document (not Kubernetes-style YAML). Its canonical schema is the VerificationPolicy contract (docs/contracts/verification-policy.md; schema src/Attestor/StellaOps.Attestor.Types/schemas/verification-policy.v1.schema.json):

{
  "policyId": "audit-verification",
  "version": "1.0.0",
  "tenantScope": "*",
  "predicateTypes": [
    "delta-verdict.stella/v1",
    "https://stellaops.io/attestation/budget-check/v1"
  ],
  "signerRequirements": {
    "minimumSignatures": 1,
    "trustedKeyFingerprints": ["sha256:..."],
    "requireRekor": true,
    "algorithms": ["ES256", "EdDSA"]
  },
  "validityWindow": {
    "maxAttestationAge": 7776000
  }
}

Reconciliation note: the prior apiVersion: stellaops.io/v1 / kind: VerificationPolicy YAML (with requiredPredicateTypes, trustedIssuers, maxAge: 90d, requireRekorEntry, unknownBudget) does not match any implemented schema. The real fields are predicateTypes, signerRequirements.{requireRekor, trustedIssuers, trustedKeyFingerprints, algorithms, minimumSignatures}, and validityWindow.maxAttestationAge (seconds). The air-gapped importer uses a separate OfflineVerificationPolicy shape (keys, tlog, attestations.required[].type, constraints.certs.allowed_issuers) — see src/AirGap/StellaOps.AirGap.Importer/Policy/OfflineVerificationPolicy.cs.

Unknown Budget Attestation

Policy thresholds are attested via the BudgetCheckPredicate (predicate type https://stellaops.io/attestation/budget-check/v1; source src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Predicates/BudgetCheckPredicate.cs). The configHash provides the determinism proof. budgetConfig also carries an action (warn/fail), actualCounts also carries cumulativeUncertainty, and the predicate adds evaluatedAt and optional violations:

{
  "predicateType": "https://stellaops.io/attestation/budget-check/v1",
  "predicate": {
    "environment": "production",
    "budgetConfig": {
      "maxUnknownCount": 5,
      "maxCumulativeUncertainty": 2.0,
      "reasonLimits": {
        "Reachability": 0,
        "Identity": 2,
        "Provenance": 2
      },
      "action": "fail"
    },
    "actualCounts": {
      "total": 3,
      "cumulativeUncertainty": 1.5,
      "byReason": {"Identity": 2, "Provenance": 1}
    },
    "result": "pass",
    "configHash": "sha256:...",
    "evaluatedAt": "2025-12-24T12:00:00Z"
  }
}

A related unknowns-budget.stella/v1 predicate (UnknownsBudgetPredicate) exists for delta-oriented unknowns budget attestations.

Schema Versions

Schema locations below are verified against the repository tree:

FormatVersionSchema Location
CycloneDX1.7docs/modules/sbom-service/schemas/cyclonedx-bom-1.7.schema.json (also mirrored at docs/schemas/cyclonedx-bom-1.7.schema.json)
SPDX3.0.1 (JSON-LD)docs/modules/sbom-service/schemas/spdx-jsonld-3.0.1.schema.json (also docs/schemas/spdx-jsonld-3.0.1.schema.json)
OpenVEXNo standalone OpenVEX JSON Schema is vendored. Excititor ships vex_raw.schema.json and vex_overlay.schema.json under docs/modules/excititor/schemas/; OpenVEX/CSAF/CycloneDX VEX are handled by the VexLens normalizers.
Sigstore Bundle0.3Model-defined in code (SigstoreBundle / SigstoreBundleConstants). A related contract schema lives at docs/contracts/sigstore-services.schema.json; there is no docs/modules/attestor/schemas/sigstore-bundle-0.3.schema.json.
Determinism Manifest1.0No JSON Schema file is committed. The contract is the C# record StellaOps.Testing.Determinism.DeterminismManifest (src/__Tests/__Libraries/StellaOps.Testing.Determinism/Determinism/DeterminismManifest.cs); validation is performed by DeterminismManifestValidator (src/__Libraries/StellaOps.Replay.Core/Validation/).
Verification Policyv1src/Attestor/StellaOps.Attestor.Types/schemas/verification-policy.v1.schema.json (docs/contracts/verification-policy.md)

CI Integration

Note: the schema-validation and determinism-gate workflows currently live under .gitea/workflows-archived/ (not the active .gitea/workflows/). A live determinism harness script is .gitea/scripts/test/determinism-run.sh. The snippets below are illustrative of the gate’s intent; the verdict hash is reproduced via stella scan replay with pinned input hashes (see Replay Procedure), not via a bare stella scan --image.

Schema Validation

# .gitea/workflows-archived/schema-validation.yml
- name: Validate CycloneDX
  run: |
    sbom-utility validate \
      --input-file ${{ matrix.fixture }} \
      --schema docs/modules/sbom-service/schemas/cyclonedx-bom-1.7.schema.json

Determinism Gate

# .gitea/workflows-archived/determinism-gate.yml — illustrative
- name: Verify Verdict Hash is stable across replays
  run: |
    HASH1=$(stella scan replay --artifact "$ART" --manifest "$MAN" \
              --feeds "$FEED" --policy "$POL" -o /tmp/run1.json && jq -r '.verdictId' /tmp/run1.json)
    HASH2=$(stella scan replay --artifact "$ART" --manifest "$MAN" \
              --feeds "$FEED" --policy "$POL" -o /tmp/run2.json && jq -r '.verdictId' /tmp/run2.json)
    [ "$HASH1" = "$HASH2" ] || exit 1

Changelog

VersionDateChanges
1.02025-12-24Initial specification based on product advisory gap analysis
1.12026-05-30Doc↔code reconciliation against src/Attestor, src/Policy, src/Replay, src/SbomService, src/Cli. Corrected: verdict attestation predicate type (delta-verdict.stella/v1, not …/attestation/verdict/v1); Sigstore bundle field (mediaType, not $mediaType); canonical-JSON escaping (minimal/raw UTF-8, not Unicode-escaped); InputStamps fields (added dependencyLockfileHash, removed non-existent toolchainVersion); SBOM versions (export CycloneDX 1.7 / SPDX 2.3+3.0.1, ingest CDX 1.4–1.6 / SPDX 2.3+3.0); replay CLI (stella scan replay + stella offline import, hash-based inputs, no STELLAOPS_* env vars); verification policy (JSON VerificationPolicy, not k8s YAML); schema-location table (real paths); CI workflows (archived). Added source pointers throughout.