SBOM Determinism Guide

Verified against commit 942a18727c (2026-09-26): CLI src/Cli/StellaOps.Cli/Commands/SbomVerificationCommandGroup.cs, writers src/Evidence/__Libraries/StellaOps.Attestor.StandardPredicates/Writers/, canonicalization src/__Libraries/StellaOps.Canonical.Json/. Origin: SPRINT_20260118_025_ReleaseOrchestrator_sbom_release_association (TASK-025-005).

This document consolidates the determinism requirements for SBOMs that Stella Ops generates. SBOM generation lives in the Evidence writers and the Scanner; SbomService stores uploaded documents verbatim and does not re-serialize them (see audit-bundle-sbom-endpoint.md). Deterministic SBOMs are critical for reproducible builds, verifiable release gates, and trust chain integrity.


1. Why Determinism Matters

1.1 Reproducibility

Deterministic SBOMs ensure that scanning the same artifact multiple times produces identical output. This is essential for:

1.2 Verifiable Gates

Policy gates rely on SBOM hashes for trust verification:

Artifact Digest → SBOM Generation → Canonical Hash → DSSE Signature → Policy Evaluation

If SBOM generation is non-deterministic, the same artifact could produce different hashes, breaking:

1.3 Trust Chaining

Evidence chains require stable identifiers. A release component’s SbomDigest must match the SBOM retrieved later for verification. Non-determinism breaks this chain:

Release Finalization:     SbomDigest = sha256:abc123...
Later Verification:       sha256(regenerated-sbom) = sha256:xyz789...  ← BROKEN

2. Canonicalization Rules

Stella Ops uses its server-authoritative StellaOps.Canonical.Json.CanonJson profile for deterministic JSON serialization. It is not a full RFC 8785/JCS interoperability implementation: it normalizes property names and strings to NFC and retains the platform serializer’s number and escaping behavior. Cross-runtime clients sign server-provided canonical bytes verbatim rather than reimplementing canonicalization.

2.1 Core Stella canonical JSON rules

  1. No Whitespace: Output has no formatting, newlines, or indentation
  2. Sorted Keys: Object keys are sorted lexicographically (Unicode code point order)
  3. Stable Platform Numbers: System.Text.Json emission is retained, with negative zero normalized to zero
  4. UTF-8 Encoding: All strings encoded as UTF-8 without BOM
  5. NFC Strings: Property names and string values are normalized to Unicode NFC

2.2 Implementation

// Using StellaOps.Canonical.Json
using StellaOps.Canonical.Json;

// Canonicalize raw JSON bytes
byte[] canonical = CanonJson.CanonicalizeParsedJson(jsonBytes);

// Compute SHA-256 of canonical form
string digest = CanonJson.Sha256Hex(canonical);

2.3 SBOM-Specific Ordering

Before CanonJson runs, the CycloneDX and SPDX writers apply additional ordering for SBOM elements:

ElementOrdering Strategy
componentsSorted by bom-ref (Ordinal)
dependenciesSorted by ref (Ordinal)
hashesSorted by alg (Ordinal)
licensesSorted by license ID (Ordinal)
dependsOnSorted lexicographically

This ensures component order doesn’t affect the canonical hash.


3. Identity Field Derivation

3.1 serialNumber (CycloneDX)

Rule: Use urn:sha256:<artifact-digest> format for deterministic identification.

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.7",
  "serialNumber": "urn:sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}

Benefits:

Fallback: If artifact digest is unavailable, UUIDv5 derived from sorted components is used for backwards compatibility. This produces a warning during validation.

3.2 bom-ref

Rule: Use deterministic derivation based on purl or component identity.

bom-ref = sha256(purl || name || version)[:12]  // truncated hash

Or use the package URL directly if available:

{
  "bom-ref": "pkg:npm/lodash@4.17.21",
  "name": "lodash",
  "version": "4.17.21",
  "purl": "pkg:npm/lodash@4.17.21"
}

Anti-pattern: Random UUIDs or incrementing counters as bom-ref.

3.3 SPDX Document Namespace

Rule: Use artifact-derived namespace for SPDX documents.

DocumentNamespace: https://stella-ops.org/spdx/sha256/<artifact-digest>

4. Ephemeral Data Policy

Certain SBOM fields are inherently non-deterministic and should be handled carefully.

4.1 Prunable Fields

These fields should be omitted or normalized before hashing:

FieldTreatment
metadata.timestampUse fixed epoch or artifact build time
metadata.tools[].versionOptional: pin tool versions
File paths (absolute)Convert to relative paths
Environment variablesExclude from SBOM

4.2 Timestamp Strategy

Option 1: Fixed Epoch (Recommended)

"timestamp": "1970-01-01T00:00:00Z"

Option 2: Artifact Build Time

"timestamp": "<artifact-created-at>"

Option 3: Omit Field

// No timestamp field - allowed by CycloneDX

4.3 Tool Metadata

Tool information aids debugging but affects hashes:

"tools": [
  {
    "vendor": "Stella Ops",
    "name": "stella-scanner",
    "version": "1.0.0"  // Pin this version
  }
]

Recommendation: Pin tool versions in CI configuration to ensure reproducibility.


5. Verification Workflow

5.1 CLI Commands

Verify Canonical Form:

stella sbom verify input.json --canonical
# Exit 0: Input is canonical
# Exit 1: Input is not canonical (outputs SHA-256 of canonical form)

Canonicalize and Output:

stella sbom verify input.json --canonical --output bom.canonical.json
# Writes: bom.canonical.json (canonical SBOM)
# Writes: bom.canonical.json.sha256 (digest sidecar)

Verbose Output:

stella sbom verify input.json --canonical --verbose
# SHA-256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
# Canonical: yes
# Input size: 15234 bytes
# Canonical size: 12456 bytes

5.2 CI Gate Integration

No SBOM gate workflow ships in .gitea/workflows/. A pipeline can gate on the canonical check directly:

stella sbom generate --help            # generation options for an image or directory
stella sbom verify bom.json --canonical --output bom.canonical.json
# exit 0: canonical; exit 1: not canonical (bom.canonical.json and bom.canonical.json.sha256 are still written)
DIGEST=$(cat bom.canonical.json.sha256)

The CLI has no sbom sign or sbom diff subcommand; sign the canonical bytes with the attestation tooling of your pipeline.

5.3 Release Finalization

At release finalization, the SBOM digest is captured:

1. Lookup SBOM for artifact: ISbomLookupService.GetByDigestAsync(artifact.Digest)
   (src/ReleaseOrchestrator/__Libraries/StellaOps.ReleaseOrchestrator.Release/Models/ReleaseComponentExtensions.cs)
2. Extract canonical digest: sbom.SbomSha256
3. Store on ReleaseComponent: component.SbomDigest = sbom.SbomSha256
4. Include in release manifest hash computation

6. KPIs and Monitoring

These are recommended targets. No metric for them is emitted by the platform at this commit.

6.1 Byte-Identical Rate

Metric: Percentage of SBOM regenerations that produce identical bytes.

Target: 100% for same artifact + same scanner version

Alert: < 99.9% indicates non-determinism bug

6.2 Stable-Field Coverage

Metric: Percentage of SBOM fields that are deterministic.

Field TypeTarget
Component identifiers100%
Hashes100%
Dependencies100%
Metadata timestamps95%+ (fixed epoch)
Tool versions90%+ (pinned)

6.3 Gate False Positives

Metric: Signature verification failures due to hash mismatch.

Target: 0% for valid artifacts

Investigation: Any mismatch indicates canonicalization or regeneration issue.


7. Troubleshooting

7.1 Hash Mismatch on Regeneration

Symptom: Same artifact produces different SBOM hashes.

Causes:

  1. Timestamp drift: Check if metadata.timestamp varies
  2. Tool version change: Check scanner/tool versions
  3. Ordering instability: Check component/dependency ordering
  4. Unicode normalization: Check for composed vs decomposed characters

Debug:

# Check canonical form of each document and compare the printed SHA-256 values
stella sbom verify bom1.json --canonical --verbose
stella sbom verify bom2.json --canonical --verbose

7.2 serialNumber Warning

Symptom: Warning CDX_SERIAL_NON_DETERMINISTIC during validation.

Cause: SBOM uses urn:uuid: format instead of urn:sha256:.

Fix: Ensure ArtifactDigest is provided when generating SBOM:

var document = new SbomDocument
{
    Name = "my-app",
    ArtifactDigest = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
    // ...
};

7.3 Canonical vs Pretty-Printed

Symptom: SBOM appears valid but fails canonical verification.

Cause: SBOM was saved with indentation/formatting.

Fix:

# Convert to canonical form
stella sbom verify input.json --canonical --output output.json

# Use output.json for signing and storage

7.4 Platform-Specific Differences

Symptom: Same code produces different SBOMs on Windows vs Linux.

Causes:

  1. Line endings: CR+LF vs LF in embedded content
  2. Path separators: \ vs / in file paths
  3. Locale differences: Number formatting, date formatting

Prevention:


References


Changelog

DateChange
2026-01-19Initial creation (TASK-025-005)
2026-09-26Re-verified against src/: writer path moved to src/Evidence/; removed the nonexistent sbom sign/sbom diff subcommands and the sbom-gate.yaml workflow; KPIs marked as targets.