Files
git.stella-ops.org/docs/product-advisories/01-Dec-2025 - Turning SBOM Data Into Verifiable Proofs.md
2025-12-01 17:50:11 +02:00

18 KiB
Raw Blame History

Heres a tight, practical blueprint to turn your SBOM→VEX links into an auditable “proof spine”—using signed DSSE statements and a perdependency trust anchor—so every VEX verdict can be traced, verified, and replayed.

What this gives you

  • A chain of evidence from each SBOM entry → analysis → VEX verdict.
  • Tamperevident DSSEsigned records (offlinefriendly).
  • Deterministic replay: same inputs → same verdicts (great for audits/regulators).

Core objects (canonical IDs)

  • ArtifactID: digest of package/container (e.g., sha256:…).
  • SBOMEntryID: stable ID for a component in an SBOM (sbomDigest:package@version[:purl]).
  • EvidenceID: hash of raw evidence (scanner JSON, reachability, exploit intel).
  • ReasoningID: hash of normalized reasoning (rules/lattice inputs used).
  • VEXVerdictID: hash of the final VEX statement body.
  • ProofBundleID: merkle root of {SBOMEntryID, EvidenceID[], ReasoningID, VEXVerdictID}.
  • TrustAnchorID: perdependency anchor (public key + policy) used to validate the above.

Signed DSSE envelopes youll produce

  1. Evidence Statement (per evidence item)
  • subject: SBOMEntryID
  • predicateType: evidence.stella/v1
  • predicate: source, tool version, timestamps, EvidenceID
  • Signers: scanner/ingestor key
  1. Reasoning Statement
  • subject: SBOMEntryID
  • predicateType: reasoning.stella/v1 (your lattice/policy inputs + ReasoningID)
  • Signers: “Policy/Lattice Engine” key (Authority)
  1. VEX Verdict Statement
  • subject: SBOMEntryID
  • predicateType: CycloneDX or CSAF VEX body + VEXVerdictID
  • Signers: VEXer key (or vendor key if you have it)
  1. Proof Spine Statement (the spine itself)
  • subject: SBOMEntryID
  • predicateType: proofspine.stella/v1
  • predicate: EvidenceID[], ReasoningID, VEXVerdictID, ProofBundleID
  • Signers: Authority key

Trust model (perdependency anchor)

  • TrustAnchor (per package/purl): { TrustAnchorID, allowed signers (KMS refs, PKs), accepted predicateTypes, policy version, revocation list }.
  • Store anchors in Authority and pin them in your graph by SBOMEntryID→TrustAnchorID.
  • Optional: PQC mode (Dilithium/Falcon) for longterm archives.

Verification pipeline (deterministic)

  1. Resolve SBOMEntryID → TrustAnchorID.
  2. Verify every DSSE envelopes signature against the anchors allowed keys.
  3. Recompute EvidenceID/ReasoningID/VEXVerdictID from raw content; compare hashes.
  4. Recompute ProofBundleID (merkle root) and compare to the spine.
  5. Emit a Receipt: {ProofBundleID, verification log, tool digests}. Cache it.

Storage layout (Postgres + blob store)

  • sbom_entries(entry_id PK, bom_digest, purl, version, artifact_digest, trust_anchor_id)
  • dsse_envelopes(env_id PK, entry_id, predicate_type, signer_keyid, body_hash, envelope_blob_ref, signed_at)
  • spines(entry_id PK, bundle_id, evidence_ids[], reasoning_id, vex_id, anchor_id, created_at)
  • trust_anchors(anchor_id PK, purl_pattern, allowed_keyids[], policy_ref, revoked_keys[])
  • Blobs (immutable): raw evidence, normalized reasoning JSON, VEX JSON, DSSE bytes.

API surface (clean and small)

  • POST /proofs/:entry/spine → submit or update spine (idempotent by ProofBundleID)
  • GET /proofs/:entry/receipt → full verification receipt (JSON)
  • GET /proofs/:entry/vex → the verified VEX body
  • GET /anchors/:anchor → fetch trust anchor (for offline kits)

Normalization rules (so hashes are stable)

  • Canonical JSON (UTF8, sorted keys, no insignificant whitespace).
  • Strip volatile fields (timestamps that arent part of the semantic claim).
  • Version your schemas: evidence.stella/v1, reasoning.stella/v1, etc.

Signing keys & rotation

  • Keep keys in your Authority module (KMS/HSM; offline export for airgap).
  • Publish key material via an attestation feed (or Rekormirror) for thirdparty audit.
  • Rotate by adding new allowed_keyids in the TrustAnchor; never mutate old envelopes.

CI/CD hooks

  • On SBOM ingest → create/refresh SBOMEntry rows + attach TrustAnchor.
  • On scan completion → produce Evidence Statements (DSSE) immediately.
  • On policy evaluation → produce Reasoning + VEX, then assemble Spine.
  • Gate releases on GET /proofs/:entry/receipt == PASS.

UX (auditorfriendly)

  • Proof timeline per entry: SBOM → Evidence tiles → Reasoning → VEX → Receipt.
  • Oneclick “Recompute & Compare” to show deterministic replay passes.
  • Red/amber flags when a signature no longer matches a TrustAnchor or a key is revoked.

Minimal dev checklist

  • Implement canonicalizers (Evidence, Reasoning, VEX).
  • Implement DSSE sign/verify (ECDSA + optional PQC).
  • TrustAnchor registry + resolver by purl pattern.
  • Merkle bundling to get ProofBundleID.
  • Receipt generator + verifier.
  • Postgres schema + blob GC (contentaddressed).
  • CI gates + API endpoints above.
  • Auditor UI: timeline + diff + receipts download.

If you want, I can drop in a readytouse JSON schema set (evidence.stella/v1, reasoning.stella/v1, proofspine.stella/v1) and sample DSSE envelopes wired to your .NET 10 stack. Heres a focused Stella Ops Developer Guidelines doc, specifically for the pipeline that turns SBOM data into verifiable proofs (your SBOM → Evidence → Reasoning → VEX → Proof Spine).

Feel free to paste this into your internal handbook and tweak names to match your repos/services.


Stella Ops Developer Guidelines

Turning SBOM Data Into Verifiable Proofs


1. Mental Model: What Youre Actually Building

For every component in an SBOM, Stella must be able to answer, “Why should anyone trust our VEX verdict for this dependency, today and ten years from now?”

We do that with a pipeline:

  1. SBOM Ingest Raw SBOM → validated → normalized → SBOMEntryID.

  2. Evidence Collection Scans, feeds, configs, reachability, etc. → canonical evidence blobs → EvidenceID → DSSE-signed.

  3. Reasoning / Policy Policy + evidence → deterministic reasoning → ReasoningID → DSSE-signed.

  4. VEX Verdict VEX statement (CycloneDX / CSAF) → canonicalized → VEXVerdictID → DSSE-signed.

  5. Proof Spine {SBOMEntryID, EvidenceIDs[], ReasoningID, VEXVerdictID} → merkle bundle → ProofBundleID → DSSE-signed.

  6. Verification & Receipts Re-run verification → Receipt that proves everything above is intact and anchored to trusted keys.

Everything you do in this area should keep this spine intact and verifiable.


2. NonNegotiable Invariants

These are the rules you dont break without an explicit, company-level decision:

  1. Immutability of Signed Facts

    • DSSE envelopes (evidence, reasoning, VEX, spines) are appendonly.
    • You never edit or delete content inside a previously signed envelope.
    • Corrections are made by superseding (new statement pointing at the old one).
  2. Determinism

    • Same {SBOMEntryID, Evidence set, policyVersion} ⇒ same {ReasoningID, VEXVerdictID, ProofBundleID}.
    • No non-deterministic inputs (e.g., “current time”, random IDs) in anything that affects IDs or verdicts.
  3. Traceability

    • Every VEX verdict must be traceable back to:

      • The precise SBOM entry
      • Concrete evidence blobs
      • A specific policy & reasoning snapshot
      • A trust anchor defining allowed signers
  4. Least Trust / Least Privilege

    • Services only know the keys and data they need.
    • Trust is always explicit: through TrustAnchors and signature verification, never “because its in our DB”.
  5. Backwards Compatibility

    • New code must continue to verify old proofs.
    • New policies must not rewrite history; they produce new spines, leaving old ones intact.

3. SBOM Ingestion Guidelines

Goal: Turn arbitrary SBOMs into stable, addressable SBOMEntryIDs and safe internal models.

3.1 Inputs & Formats

  • Support at least:

    • CycloneDX (JSON)
    • SPDX (JSON / Tag-Value)
  • For each ingested SBOM, store:

    • Raw SBOM bytes (immutable, content-addressed)
    • A normalized internal representation (your own model)

3.2 IDs

  • Generate:

    • sbomDigest = hash(raw SBOM, canonical form)
    • SBOMEntryID = sbomDigest + purl + version (or equivalent stable tuple)
  • SBOMEntryID must:

    • Not depend on ingestion time or database IDs.
    • Be reproducible from SBOM + deterministic normalization.

3.3 Validation & Errors

  • Validate:

    • Syntax (JSON, schema)
    • Core semantics (package identifiers, digests, versions)
  • If invalid:

    • Reject the SBOM but record a small DSSE “failure attestation” explaining:

      • Why it failed
      • Which file
      • Which system version
    • This still gives you a proof trail for “we tried and it failed”.


4. Evidence Collection Guidelines

Goal: Capture all inputs that influence the verdict in a canonical, signed form.

Typical evidence types:

  • SCA / vuln scanner results
  • CVE feeds & advisory data
  • Reachability / call graph analysis
  • Runtime context (where this component is used)
  • Manual assessments (e.g., security engineer verdicts)

4.1 Evidence Canonicalization

For every evidence item:

  • Normalize to a schema like evidence.stella/v1 with fields such as:

    • source (scanner name, feed)
    • sourceVersion (tool version, DB version)
    • collectionTime
    • sbomEntryId
    • vulnerabilityId (if applicable)
    • rawFinding (or pointer to it)
  • Canonical JSON rules:

    • Sorted keys
    • UTF8, no extraneous whitespace
    • No volatile fields beyond whats semantically needed (e.g., you might include collectionTime, but then know it affects the hash and treat that consciously).

Then:

  • Compute EvidenceID = hash(canonicalEvidenceJson).

  • Wrap in DSSE:

    • subject: SBOMEntryID
    • predicateType: evidence.stella/v1
    • predicate: canonical evidence + EvidenceID.
  • Sign with evidence-ingestor key (per environment).

4.2 Ops Rules

  • Idempotency: Re-running the same scan with same inputs should produce the same evidence object and EvidenceID.
  • Tool changes: When tool version or configuration changes, thats a new evidence statement with a new EvidenceID. Do not overwrite old evidence.
  • Partial failure: If a scan fails, produce a minimal failure evidence record (with error details) instead of “nothing”.

5. Reasoning & Policy Engine Guidelines

Goal: Turn evidence into a defensible, replayable reasoning step with a clear policy version.

5.1 Reasoning Object

Define a canonical reasoning schema, e.g. reasoning.stella/v1:

  • sbomEntryId
  • evidenceIds[] (sorted)
  • policyVersion
  • inputs: normalized form of all policy inputs (severity thresholds, lattice rules, etc.)
  • intermediateFindings: optional but useful — e.g., “reachable vulns = …”

Then:

  • Canonicalize JSON and compute ReasoningID = hash(canonicalReasoning).

  • Wrap in DSSE:

    • subject: SBOMEntryID
    • predicateType: reasoning.stella/v1
    • predicate: canonical reasoning + ReasoningID.
  • Sign with Policy/Authority key.

5.2 Determinism

  • Reasoning functions must be pure:

    • Inputs: SBOMEntryID, evidence set, policy version, configuration.
    • No hidden calls to external APIs at decision time (fetch feeds earlier and record them as evidence).
  • If you need “current time” in policy:

    • Treat it as explicit input and record it inside reasoning under inputs.currentEvaluationTime.

5.3 Policy Evolution

  • When policy changes:

    • Bump policyVersion.
    • New evaluations produce new ReasoningID and new VEX/spines.
    • Dont retroactively apply new policy to old reasoning objects; generate new ones alongside.

6. VEX Verdict Guidelines

Goal: Generate VEX statements that are strongly tied to SBOM entries and your reasoning.

6.1 Shape

  • Target standard formats:

    • CycloneDX VEX
    • or CSAF
  • Required linkages:

    • Component reference = SBOMEntryID or a resolvable component identifier from your SBOM normalize layer.
    • Vulnerability IDs (CVE, GHSA, internal IDs).
    • Status (not_affected, affected, fixed, etc.).
    • Justification & impact.

6.2 Canonicalization & Signing

  • Define a canonical VEX body schema (subset of the standard + internal metadata):

    • sbomEntryId
    • vulnerabilityId
    • status
    • justification
    • policyVersion
    • reasoningId
  • Canonicalize JSON → VEXVerdictID = hash(canonicalVexBody).

  • DSSE-envelope:

    • subject: SBOMEntryID
    • predicateType: e.g. cdx-vex.stella/v1
    • predicate: canonical VEX + VEXVerdictID.
  • Sign with VEXer key or vendor key (depending on trust anchor).

6.3 External VEX

  • When importing vendor VEX:

    • Verify signature against vendors TrustAnchor.

    • Canonicalize to your internal schema but preserve:

      • Original document
      • Original signature material
    • Record “source = vendor” vs “source = stella” so auditors see origin.


7. Proof Spine Guidelines

Goal: Build a compact, tamper-evident “bundle” that ties everything together.

7.1 Structure

For each SBOMEntryID, gather:

  • EvidenceIDs[] (sorted lexicographically).
  • ReasoningID.
  • VEXVerdictID.

Compute:

  • Merkle tree root (or deterministic hash) over:

    • sbomEntryId
    • sorted EvidenceIDs[]
    • ReasoningID
    • VEXVerdictID
  • Result is ProofBundleID.

Create a DSSE “spine”:

  • subject: SBOMEntryID

  • predicateType: proofspine.stella/v1

  • predicate:

    • evidenceIds[]
    • reasoningId
    • vexVerdictId
    • policyVersion
    • proofBundleId
  • Sign with Authority key.

7.2 Ops Rules

  • Spine generation is idempotent:

    • Same inputs → same ProofBundleID.
  • Never mutate existing spines; new policy or new evidence ⇒ new spine.

  • Keep a clear API contract:

    • GET /proofs/:entry returns all spines, each labeled with policyVersion and timestamps.

8. Storage & Schema Guidelines

Goal: Keep proofs queryable forever without breaking verification.

8.1 Tables (conceptual)

  • sbom_entries: entry_id, bom_digest, purl, version, artifact_digest, trust_anchor_id.
  • dsse_envelopes: env_id, entry_id, predicate_type, signer_keyid, body_hash, envelope_blob_ref, signed_at.
  • spines: entry_id, proof_bundle_id, policy_version, evidence_ids[], reasoning_id, vex_verdict_id, anchor_id, created_at.
  • trust_anchors: anchor_id, purl_pattern, allowed_keyids[], policy_ref, revoked_keys[].

8.2 Schema Changes

Always follow:

  1. Expand

    • Add new columns/tables.
    • Make new code tolerant of old data.
  2. Backfill

    • Idempotent jobs that fill in new IDs/fields without touching old DSSE payloads.
  3. Contract

    • Only after all code uses the new model.
    • Never drop the raw DSSE or raw SBOM blobs.

9. Verification & Receipts

Goal: Make it trivial (for you, customers, and regulators) to recheck everything.

9.1 Verification Flow

Given SBOMEntryID or ProofBundleID:

  1. Fetch spine and trust anchor.

  2. Verify:

    • Spine DSSE signature against TrustAnchors allowed keys.
    • VEX, reasoning, and evidence DSSE signatures.
  3. Recompute:

    • EvidenceIDs from stored canonical evidence.
    • ReasoningID from reasoning.
    • VEXVerdictID from VEX body.
    • ProofBundleID from the above.
  4. Compare to stored IDs.

Emit a Receipt:

  • proofBundleId
  • verifiedAt
  • verifierVersion
  • anchorId
  • result (pass/fail, with reasons)

9.2 Offline Kit

  • Provide a minimal CLI (stella verify) that:

    • Accepts a bundle export (SBOM + DSSE envelopes + anchors).
    • Verifies everything without network access.

Developers must ensure:

  • Export format is documented and stable.
  • All fields required for verification are included.

10. Security & Key Management (for Devs)

  • Keys live in KMS/HSM, not env vars or config files.

  • Separate keysets:

    • dev, staging, prod
    • Authority vs VEXer vs Evidence Ingestor.
  • TrustAnchors:

    • Edit via Authority service only.

    • Every change:

      • Requires code-reviewed change.
      • Writes an audit log entry.

Never:

  • Log private keys.
  • Log full DSSE envelopes in plaintext logs (log IDs and hashes instead).

11. Observability & OnCall Expectations

11.1 Metrics

For the SBOM→Proof pipeline, expose:

  • sboms_ingested_total
  • sbom_ingest_errors_total{reason}
  • evidence_statements_created_total
  • reasoning_statements_created_total
  • vex_statements_created_total
  • proof_spines_created_total
  • proof_verifications_total{result} (pass/fail reason)
  • Latency histograms per stage (_duration_seconds)

11.2 Logging

Include in structured logs wherever relevant:

  • sbomEntryId
  • proofBundleId
  • anchorId
  • policyVersion
  • requestId / traceId

11.3 Runbooks

You should maintain runbooks for at least:

  • “Pipeline is stalled” (backlog of SBOMs, evidence, or spines).
  • “Verification failures increased”.
  • “Trust anchor or key issues” (rotation, revocation, misconfiguration).
  • “Backfill gone wrong” (how to safely stop, resume, and audit).

12. Dev Workflow & PR Checklist (SBOM→Proof Changes Only)

When your change touches SBOM ingestion, evidence, reasoning, VEX, or proof spines, check:

  • IDs (SBOMEntryID, EvidenceID, ReasoningID, VEXVerdictID, ProofBundleID) remain deterministic and fully specified.

  • No mutation of existing DSSE envelopes or historical proof data.

  • Schema changes follow expand → backfill → contract.

  • New/updated TrustAnchors reviewed by Authority owner.

  • Unit tests cover:

    • Canonicalization for any new/changed predicate.
    • ID computation.
  • Integration test covers:

    • SBOM → Evidence → Reasoning → VEX → Spine → Verification → Receipt.
  • Observability updated:

    • New paths emit logs & metrics.
  • Rollback plan documented (especially for migrations & policy changes).


If you tell me which microservices/repos map to these stages (e.g. stella-sbom-ingest, stella-proof-authority, stella-vexer), I can turn this into a more concrete, servicebyservice checklist with example API contracts and class/interface sketches.