17 KiB
Here’s a tight, step‑through recipe for making every VEX statement verifiably tied to build evidence—using CycloneDX (SBOM), deterministic identifiers, and attestations (in‑toto/DSSE).
1) Build time: mint stable, content‑addressed IDs
-
For every artifact (source, module, package, container layer), compute:
sha256of canonical bytes- a deterministic component ID:
pkg:<ecosystem>/<name>@<version>?sha256=<digest>(CycloneDX supportsbom-ref; use this value as thebom-ref).
-
Emit SBOM (CycloneDX 1.6) with:
metadata.component= the top artifact- each
components[].bom-ref= the deterministic ID properties[]for extras: build system run ID, git commit, tool versions.
Example (SBOM fragment):
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"serialNumber": "urn:uuid:7b4f3f64-8f0b-4a7d-9b3f-7a0a2b6cf6a9",
"version": 1,
"metadata": {
"component": {
"type": "container",
"name": "stellaops/scanner",
"version": "1.2.3",
"bom-ref": "pkg:docker/stellaops/scanner@1.2.3?sha256=7e1a...b9"
}
},
"components": [
{
"type": "library",
"name": "openssl",
"version": "3.2.1",
"purl": "pkg:apk/alpine/openssl@3.2.1-r0",
"bom-ref": "pkg:apk/alpine/openssl@3.2.1-r0?sha256=2c0f...54e",
"properties": [
{"name": "build.git", "value": "ef3d9b4"},
{"name": "build.run", "value": "gha-61241"}
]
}
]
}
2) Sign the SBOM as evidence
- Wrap the SBOM in DSSE and sign it (cosign or in‑toto).
- Record to Rekor (or your offline mirror). Store the log index/UUID.
Provenance note: keep { sbomDigest, dsseSignature, rekorLogID }.
3) Normalize vulnerability findings to the same IDs
- Your scanner should output findings where
affected.bom-refequals the component’s deterministic ID. - If using CVE/OSV, keep both the upstream ID and your local
bom-ref.
Finding (internal record):
{
"vulnId": "CVE-2024-12345",
"affected": "pkg:apk/alpine/openssl@3.2.1-r0?sha256=2c0f...54e",
"source": "grype@0.79.0",
"introducedBy": "stellaops/scanner@1.2.3",
"evidence": {"scanDigest": "sha256:aa1b..."}
}
4) Issue VEX with deterministic targets
- Create a CycloneDX VEX doc where each
vulnerabilities[].affects[].refequals the SBOMbom-ref. - Use
analysis.justificationandanalysis.state(not_affected,affected,fixed,under_investigation). - Add tight reasons (reachability, config, platform) and a link back to evidence via properties.
VEX (CycloneDX) minimal:
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"vulnerabilities": [
{
"id": "CVE-2024-12345",
"source": {"name": "NVD"},
"analysis": {
"state": "not_affected",
"justification": "vulnerable_code_not_present",
"response": ["will_not_fix"],
"detail": "Linked OpenSSL feature set excludes the vulnerable cipher."
},
"affects": [
{"ref": "pkg:apk/alpine/openssl@3.2.1-r0?sha256=2c0f...54e"}
],
"properties": [
{"name": "evidence.sbomDigest", "value": "sha256:91f2...9a"},
{"name": "evidence.rekorLogID", "value": "425c1d1e..."},
{"name": "reachability.report", "value": "sha256:reacha..."},
{"name": "policy.decision", "value": "TrustGate#R-17.2"}
]
}
]
}
5) Sign the VEX and anchor it
- Wrap the VEX in DSSE, sign, and (optionally) publish to Rekor (or your Proof‑Market mirror).
- Now you can verify: component digest ↔ SBOM bom‑ref ↔ VEX affects.ref ↔ signatures/log.
6) Verifier flow (what your UI/CLI should do)
- Load VEX → verify DSSE signature → (optional) Rekor inclusion.
- For each
affects.ref, check there exists an SBOM component with the exact same value. - Verify the SBOM signature and Rekor entry (hash of SBOM equals what VEX references in
properties.evidence.sbomDigest). - Cross‑check the running artifact/container digest matches the SBOM
metadata.component.bom-ref(or OCI manifest digest). - Render the decision with explainable evidence (links to proofs, reachability report hash, policy rule ID).
7) Attestation shapes (quick starters)
DSSE envelope (JSON) around SBOM or VEX payload:
{
"payloadType": "application/vnd.cyclonedx+json;version=1.6",
"payload": "BASE64(SBOM_OR_VEX_JSON)",
"signatures": [
{"keyid": "SHA256-PUBKEY", "sig": "BASE64(SIG)"}
]
}
in‑toto Statement for provenance → attach SBOM hash:
{
"_type": "https://in-toto.io/Statement/v1",
"predicateType": "https://slsa.dev/provenance/v1",
"subject": [{"name": "stellaops/scanner", "digest": {"sha256": "7e1a...b9"}}],
"predicate": {
"buildType": "stellaops/ci",
"materials": [{"uri": "git+https://...#ef3d9b4"}],
"metadata": {"buildInvocationID": "gha-61241"},
"externalParameters": {"sbomDigest": "sha256:91f2...9a"}
}
}
8) Practical guardrails (so it stays deterministic)
- Never generate
bom-reffrom mutable fields (like file paths). Use content digests + stable PURL. - Pin toolchains and normalize JSON (UTF‑8, sorted keys if you post‑hash).
- Store
{ toolVersions, feed snapshots, policy set hash }to replay decisions. - For containers, prefer
bom-ref = pkg:oci/<repo>@<digest>PLUS layer evidence incomponents[].
9) “Hello‑world” verification script (pseudo)
# 1) Verify SBOM sig -> get sbomDigest
cosign verify-blob --signature sbom.sig sbom.json
# 2) Verify VEX sig
cosign verify-blob --signature vex.sig vex.json
# 3) Check that every VEX affects.ref exists in SBOM
jq -r '.vulnerabilities[].affects[].ref' vex.json | while read ref; do
jq -e --arg r "$ref" '.components[] | select(.["bom-ref"]==$r)' sbom.json >/dev/null
done
# 4) Compare running image digest to SBOM metadata.component.bom-ref
Where this fits in Stella Ops (quick wiring)
- Sbomer: emits CycloneDX with deterministic
bom-refs + DSSE sig. - Scanner: normalizes findings to
bom-ref. - Vexer: produces/signed VEX; includes
propertiesback to SBOM/reachability/policy. - Authority/Verifier: one click “Prove it” view → checks DSSE, Rekor, and
refequality. - Proof Graph: edge types:
produces(SBOM),affects(VEX↔component),signedBy,recordedAt(Rekor).
If you want, I can turn this into:
- a .NET 10 helper lib for stable
bom-refgeneration, - a CLI that takes
sbom.json+vex.jsonand runs the full verification, - or fixtures (golden SBOM/VEX/DSSE triplets) for your CI. Below is a developer-oriented blueprint you can hand to engineers as “How we build a verifiable SBOM→VEX chain”.
1. Objectives and Trust Model
Goal: Any VEX statement about a component must be:
- Precisely scoped to one or more concrete artifacts.
- Cryptographically linked to the SBOM that defined those artifacts.
- Replayable: a third party can re-run verification and reach the same conclusion.
- Auditable: every step is backed by signatures and immutable logs (e.g., Rekor or internal ledger).
Questions you must be able to answer deterministically:
- “Which exact artifact does this VEX statement apply to?”
- “Show me the SBOM where this artifact is defined, and prove it was not tampered with.”
- “Prove that the VEX document I am looking at was authored and/or approved by the expected party.”
2. Canonical Identifiers: Non-Negotiable Foundation
You cannot build a verifiable chain without stable, content-addressed IDs.
2.1 Component IDs
For every component, choose a deterministic scheme:
- Base: PURL or URN, e.g.,
pkg:maven/org.apache.commons/commons-lang3@3.14.0 - Extend with content hash:
pkg:maven/org.apache.commons/commons-lang3@3.14.0?sha256=<digest> - Use this value as the CycloneDX
bom-ref.
Developer rule:
-
bom-refmust be:- Stable across SBOM regenerations for identical content.
- Independent of local, ephemeral data (paths, build numbers).
- Derived from canonical bytes (normalized archive/layer, not “whatever we saw on disk”).
2.2 Top-Level Artifact IDs
For images, archives, etc.:
- Prefer OCI-style naming:
pkg:oci/<repo>@sha256:<manifestDigest> - Set this as
metadata.component.bom-refin the SBOM.
3. SBOM Generation Guidelines
3.1 Required Properties
When emitting a CycloneDX SBOM (1.5/1.6):
-
metadata.component:name,version,bom-ref.
-
components[]:name,version,purl(if available),bom-ref.hashes[]: include at leastSHA-256.
-
properties[]:-
Build metadata:
build.gitCommitbuild.pipelineRunIdbuild.toolchain(e.g.,dotnet-10.0.100,maven-3.9.9)
-
Optional:
provenance.statementDigestscm.url
-
Minimal JSON fragment:
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"metadata": {
"component": {
"type": "container",
"name": "example/api-gateway",
"version": "1.0.5",
"bom-ref": "pkg:oci/example/api-gateway@sha256:abcd..."
}
},
"components": [
{
"type": "library",
"name": "openssl",
"version": "3.2.1",
"purl": "pkg:apk/alpine/openssl@3.2.1-r0",
"bom-ref": "pkg:apk/alpine/openssl@3.2.1-r0?sha256:1234...",
"hashes": [
{ "alg": "SHA-256", "content": "1234..." }
]
}
]
}
3.2 SBOM Normalization
Developer directions:
-
Normalize JSON before hashing/signing:
- Sorted keys, UTF-8, consistent whitespace.
-
Ensure SBOM generation is deterministic given the same:
- Inputs (image, source tree)
- Tool versions
- Settings/flags
4. Signing and Publishing the SBOM
4.1 DSSE Envelope
Wrap the raw SBOM bytes in a DSSE envelope and sign:
{
"payloadType": "application/vnd.cyclonedx+json;version=1.6",
"payload": "BASE64(SBOM_JSON)",
"signatures": [
{
"keyid": "<KID>",
"sig": "BASE64(SIGNATURE)"
}
]
}
Guidelines:
-
Use a dedicated signing identity (keypair or KMS key) for SBOMs.
-
Publish signature and payload hash to:
- Rekor or
- Your internal immutable log / ledger.
Persist:
sbomDigest = sha256(SBOM_JSON).sbomLogId(Rekor UUID or internal ledger ID).
5. Vulnerability Findings → Normalized Targets
Your scanners (or imports from external scanners) must map findings onto the same IDs used in the SBOM.
5.1 Mapping Rule
For each finding:
vulnId: CVE, GHSA, OSV ID, etc.affectedRef: exactbom-reffrom SBOM.- Optional: secondary keys (file path, package manager coordinates).
Example internal record:
{
"vulnId": "CVE-2025-0001",
"affectedRef": "pkg:apk/alpine/openssl@3.2.1-r0?sha256:1234...",
"scanner": "grype@0.79.0",
"sourceSbomDigest": "sha256:91f2...",
"foundAt": "2025-12-09T12:34:56Z"
}
Developer directions:
-
Build a component index keyed by
bom-refwhen ingesting SBOMs. -
Any finding that cannot be mapped to a known
bom-refmust be flagged:-
status = "unlinked"and either:- dropped from VEX scope, or
- fixed by improving normalization rules.
-
6. VEX Authoring Guidelines
Use CycloneDX VEX (or OpenVEX) with a strict mapping to SBOM bom-refs.
6.1 Minimal VEX Structure
{
"bomFormat": "CycloneDX",
"specVersion": "1.6",
"version": 1,
"vulnerabilities": [
{
"id": "CVE-2025-0001",
"source": { "name": "NVD" },
"analysis": {
"state": "not_affected",
"justification": "vulnerable_code_not_in_execute_path",
"response": ["will_not_fix"],
"detail": "The vulnerable function is not reachable in this configuration."
},
"affects": [
{ "ref": "pkg:apk/alpine/openssl@3.2.1-r0?sha256:1234..." }
],
"properties": [
{ "name": "evidence.sbomDigest", "value": "sha256:91f2..." },
{ "name": "evidence.sbomLogId", "value": "rekor:abcd-..." },
{ "name": "policy.decisionId", "value": "TRUST-ALG-001#rule-7" }
]
}
]
}
6.2 Required Analysis Discipline
For each (vulnId, affectedRef):
-
state∈ {not_affected,affected,fixed,under_investigation}. -
justification:vulnerable_code_not_presentvulnerable_code_not_in_execute_pathvulnerable_code_not_configuredvulnerable_code_cannot_be_controlled_by_adversary- etc.
-
detail: concrete explanation, not generic text. -
Reference back to SBOM and other proofs via
properties.
Developer rules:
- Every
affects.refmust match exactly abom-refin at least one SBOM. - VEX generator must fail if it cannot confirm this mapping.
7. Cryptographic Linking: SBOM ↔ VEX
To make the chain verifiable:
-
Compute
sbomDigest = sha256(SBOM_JSON). -
Inside each VEX vulnerability (or at top-level), include:
properties.evidence.sbomDigest = sbomDigestproperties.evidence.sbomLogIdif a transparency log is used.
-
Sign the VEX document with DSSE:
- Separate key from SBOM key, or the same with different usage metadata.
-
Optionally publish VEX DSSE to Rekor (or equivalent).
Resulting verification chain:
- Artifact digest → matches SBOM
metadata.component.bom-ref. - SBOM
bom-refs → referenced byvulnerabilities[].affects[].ref. - VEX references SBOM by hash/log ID.
- Both SBOM and VEX have valid signatures and log inclusion proofs.
8. Verifier Implementation Guidelines
You should implement a verifier library and then thin wrappers:
- CLI
- API endpoint
- UI “Prove it” button
8.1 Verification Steps (Algorithm)
Given: artifact digest, SBOM, VEX, signatures, logs.
-
Verify SBOM DSSE signature.
-
Verify VEX DSSE signature.
-
If using Rekor/log:
-
Verify SBOM and VEX entries:
- log inclusion proof
- payload hashes match local files.
-
-
Confirm that:
artifactDigestmatchesmetadata.component.bom-refor the indicated digest.
-
Build a map of
bom-reffrom SBOM. -
For each VEX
affects.ref:- Ensure it exists in SBOM components.
- Ensure
properties.evidence.sbomDigest == sbomDigest.
-
Compile per-component decisions:
For each component:
- List associated VEX records.
- Derive effective state using a policy (e.g., most recent, highest priority source).
Verifier output should be structured (not just logs), e.g.:
{
"artifact": "pkg:oci/example/api-gateway@sha256:abcd...",
"sbomVerified": true,
"vexVerified": true,
"components": [
{
"bomRef": "pkg:apk/alpine/openssl@3.2.1-r0?sha256:1234...",
"vulnerabilities": [
{
"id": "CVE-2025-0001",
"state": "not_affected",
"justification": "vulnerable_code_not_in_execute_path"
}
]
}
]
}
9. Data Model and Storage
A minimal relational / document model:
-
ArtifactsidpurldigestbomRef(top level)
-
SbomsiddigestdsseSignaturelogIdrawJson
-
SbomComponentsidsbomIdbomRef(unique per SBOM)purlhash
-
VexDocumentsiddigestdsseSignaturelogIdrawJson
-
VexEntriesidvexIdvulnIdaffectedBomRefstatejustificationevidenceSbomDigestpolicyDecisionId
Guideline: store raw JSON plus an indexed view for efficient queries.
10. Testing: Golden Chains
Developers should maintain golden fixtures where:
-
A known image or package → SBOM (JSON) → VEX (JSON) → DSSE envelopes → log entries.
-
For each fixture:
-
A test harness runs the verifier.
-
Asserts:
- All signatures valid.
- All
affects.refmap to a SBOMbom-ref. - The final summarized decision for specific
(vulnId, bomRef)pairs matches expectations.
-
Include negative tests:
- VEX referencing unknown
bom-ref→ verification error. - Mismatching
evidence.sbomDigest→ verification error. - Tampered SBOM or VEX → signature/log verification failure.
11. Operational Practices and Guardrails
Developer-facing rules of thumb:
-
Never generate
bom-reffrom mutable fields (paths, timestamps). -
Treat tool versions and feed snapshots as part of the “scan config”:
- Include hashes/versions in SBOM/VEX properties.
-
Enforce strict types in code (e.g., enums for VEX states/justifications).
-
Keep keys and signing policies separate per role:
- Build pipeline SBOM signer.
- Security team VEX signer.
-
Offer a single, stable API:
-
POST /verify:- Inputs: artifact digest (or image reference), SBOM+VEX or references.
- Outputs: structured verification report.
-
If you want, next step I can do is sketch a small reference implementation outline (e.g., .NET 10 service with DTOs and verification pipeline) that you can drop directly into your codebase.