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:
- SBOM generation — Scanner export emits CycloneDX 1.7 (
application/vnd.cyclonedx+json; version=1.7) and SPDX (default SPDX 2.3, with SPDX 3.0.1 available via content negotiation / explicit?format=spdx3). SBOM ingest (SbomService upload) accepts CycloneDX 1.4–1.6 and SPDX 2.3/3.0. (Source:src/Scanner/StellaOps.Scanner.WebService/Endpoints/ExportEndpoints.cs,src/SbomService/StellaOps.SbomService/Services/SbomUploadService.cs.) - VEX statements (OpenVEX, plus CSAF and CycloneDX VEX via VexLens normalizers)
- Policy verdicts
- Delta computations
- DSSE attestations and Sigstore bundles
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:
blockingDriversandwarningDriversare sorted bytype, thencveId, thenpurl, thenseverityappliedExceptionsare sorted lexicographically- All string comparisons use Ordinal (case-sensitive, lexicographic)
- Canonical JSON follows RFC 8785 (JCS) with keys sorted alphabetically
- The
_canonVersionfield ensures hash stability across algorithm evolution
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:
InputStampsdoes not carry atoolchainVersionfield — toolchain/version information lives in the manifest’s separatetoolchainblock (see below). At replay time, the runtime input set is resolved by theInputManifestrecord inStellaOps.Replay.Core(src/Replay/__Libraries/StellaOps.Replay.Core/InputManifestResolver.cs), which carriesFeedSnapshotHash,PolicyManifestHash,SourceCodeHash,BaseImageDigest,VexDocumentHashes,ToolchainVersion,RandomSeed, andTimestampOverride.
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):
- Object keys sorted with Ordinal comparison (case-sensitive, recursive)
- No whitespace or formatting variations
- Minimal escaping — the writer uses
JavaScriptEncoder.UnsafeRelaxedJsonEscaping, so non-ASCII characters are emitted as raw UTF-8 (NOT\uXXXX-escaped) - Strings and property names are NFC-normalized (Unicode
FormC) before emission - Consistent number formatting (e.g. negative-zero normalized to
0) - 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/v1with a{verdictId, status, gate, inputs, evidence}body. No such predicate type exists insrc/Attestor. The implemented verdict predicate types aredelta-verdict.stella/v1(Smart-Diff delta verdict) andverdict.stella/v1(used by the Attestor bundling/offline verifier paths). Other implemented predicate types includeunknowns-budget.stella/v1,stella.ops/sbom-delta@v1,https://stellaops.io/attestation/budget-check/v1, andhttps://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
- 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 offlineexposesimportandstatussubcommands (src/Cli/StellaOps.Cli/Commands/OfflineCommandGroup.cs); there is nooffline extract.- Deterministic replay is
stella scan replay(src/Cli/StellaOps.Cli/Commands/CommandFactory.cs), notstella scan --image ... --feeds <dir>. Its required--feeds/--policyarguments are content hashes.- There is no top-level
stella verifycommand and noSTELLAOPS_DETERMINISTIC_SEED/STELLAOPS_CLOCK_FIXEDenvironment 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: VerificationPolicyYAML (withrequiredPredicateTypes,trustedIssuers,maxAge: 90d,requireRekorEntry,unknownBudget) does not match any implemented schema. The real fields arepredicateTypes,signerRequirements.{requireRekor, trustedIssuers, trustedKeyFingerprints, algorithms, minimumSignatures}, andvalidityWindow.maxAttestationAge(seconds). The air-gapped importer uses a separateOfflineVerificationPolicyshape (keys,tlog,attestations.required[].type,constraints.certs.allowed_issuers) — seesrc/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/v1predicate (UnknownsBudgetPredicate) exists for delta-oriented unknowns budget attestations.
Schema Versions
Schema locations below are verified against the repository tree:
| Format | Version | Schema Location |
|---|---|---|
| CycloneDX | 1.7 | docs/modules/sbom-service/schemas/cyclonedx-bom-1.7.schema.json (also mirrored at docs/schemas/cyclonedx-bom-1.7.schema.json) |
| SPDX | 3.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) |
| OpenVEX | — | No 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 Bundle | 0.3 | Model-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 Manifest | 1.0 | No 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 Policy | v1 | src/Attestor/StellaOps.Attestor.Types/schemas/verification-policy.v1.schema.json (docs/contracts/verification-policy.md) |
CI Integration
Note: the
schema-validationanddeterminism-gateworkflows 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 viastella scan replaywith pinned input hashes (see Replay Procedure), not via a barestella 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
Related Documentation
- Testing Strategy
- Determinism Verification
- DSSE Attestation Guide
- Verification Policy Contract
- Offline Operation
- Repro Bundle Profile
- Proof Chain Specification
Changelog
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2025-12-24 | Initial specification based on product advisory gap analysis |
| 1.1 | 2026-05-30 | Doc↔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. |
