Safeguard
Supply Chain Security•

CycloneDX Attestations (CDXA) vs in-toto, SLSA and Sigstore Bundles

CDXA records claims and evidence against a standard; in-toto and SLSA describe how an artifact was built; a Sigstore bundle proves who signed it.

Safeguard Security Team
40 min read

CycloneDX Attestations (CDXA) record claims, evidence and conformance scores against a named standard, such as "this release meets NIST SSDF PS.3.2". An in-toto Statement binds any typed claim to an artifact digest, SLSA provenance is the predicate that says how the build ran, and a Sigstore bundle carries the signature plus everything needed to verify it.

They sit at different layers, and a well documented release usually carries several. Below, each format is taken apart field by field, then one release is walked through end to end.

Key takeaways

  • CDXA is a compliance document, not a signature format. It lives in the declarations and definitions blocks of a CycloneDX BOM (added in CycloneDX 1.6, carried forward in 1.7) and maps requirements to claims, evidence, counter evidence and scored conformance.
  • An in-toto Statement is an envelope for any claim about an artifact. It has three working fields: subject (name plus digest), predicateType (a URI) and predicate (the claim body). DSSE signs it.
  • SLSA provenance is one predicate type. Its URI is https://slsa.dev/provenance/v1, and it records the build type, external and internal parameters, resolved dependencies, the builder identity and run metadata. SLSA 1.2 kept that URI.
  • A Sigstore bundle is the verification package. It holds the signed content (a message signature or a DSSE envelope) plus the certificate or key hint, transparency log entries with inclusion proofs, and optional RFC 3161 timestamps.
  • The artifact digest is the join key. An SBOM, a provenance attestation and a CDXA describe the same release only when they all point at the same digest, or at each other through a BOM-Link.
  • The formats nest. An SBOM can be an in-toto predicate, wrapped in DSSE, inside a Sigstore bundle, which a CDXA cites as evidence.

The four formats at a glance

Each format answers a different question, and none of them replaces an SBOM.

FormatThe question it answersUnit it describesWho usually writes itSigned how
CycloneDX SBOMWhat is inside this software?Components, services, dependenciesBuild tooling, SBOM generatorsOptional JSF signature in the BOM, or external (DSSE, Sigstore)
CycloneDX Attestation (CDXA)Does this thing meet requirement X, how sure are we, and what evidence shows it?Requirements, claims, evidence, assessorsProduct security, compliance, auditorsJSF signatures on claims, evidence, attestations and the affirmation
in-toto StatementWhat is claimed about the artifact with this exact digest?Subject digests plus one typed predicateAny tool in the pipelineDSSE envelope (recommended by the in-toto spec)
SLSA provenanceHow, where and from what inputs was this artifact built?One build runThe build platformAs an in-toto Statement in DSSE
Sigstore bundleWho signed this, when, and can I check it later without trusting a live service?One signature or one DSSE envelopeThe signer (cosign or a Sigstore client)Short lived Fulcio certificate or a supplied key, logged in Rekor

CDXA tells an auditor which requirements you claim to meet and points at the other three as proof. For the broader framing, see SBOMs vs attestations: what each proves and how they fit and signature vs provenance vs SBOM attestation.

Version timeline

Dates come from each publisher's own page or repository; undated rows say so.

DateFormat and versionWhat changed for this topicSource
February 2022NIST SP 800-218 (SSDF) 1.1 finalPS.2.1 (integrity verification information) and PS.3.2 (provenance data, for example in an SBOM)NIST CSRC
9 April 2024CycloneDX 1.6First schema with the declarations block (CDXA) and definitions.standardsCycloneDX specification releases
10 May 2024DSSE envelope 1.0.2Current DSSE envelope document; JSON is the only recommended encodingDSSE repository
June 2024ECMA-424, 1st editionCycloneDX 1.6 published as an Ecma standardEcma International
7 November 2024CycloneDX 1.6.1Patch release of 1.6CycloneDX specification releases
14 January 2025Sigstore bundle doc 0.3.2Documents the v0.3 bundle (application/vnd.dev.sigstore.bundle.v0.3+json)docs.sigstore.dev
21 October 2025CycloneDX 1.7declarations carried forward; only description wording changed in that blockCycloneDX specification releases
Current editionECMA-424 (current)The Ecma page now states that the standard defines CycloneDX 1.7, and lists ISO/IEC DIS 27055Ecma International
Not dated on the pageSLSA 1.2Adds the Source track; Build provenance keeps predicate type https://slsa.dev/provenance/v1; the 1.1 page now shows "Retired"slsa.dev
Not dated on the pagein-toto Statement v1_type is https://in-toto.io/Statement/v1in-toto attestation repository
17 September 2026CycloneDX 1.7.2Latest tagged patch release at the time of writingCycloneDX specification releases

Two practical notes: a CDXA must declare specVersion 1.6 or later, because earlier schemas have no declarations property; and the SLSA 1.1 provenance page is now marked retired, while the predicate type string your verifiers check is unchanged.

What CDXA is

The CycloneDX project describes CDXA as "machine-readable statements of claims, evidence, and testimony" for showing compliance with standards. In practice it is a set of objects inside an ordinary CycloneDX document. There is no separate CDXA file format and no separate media type. A BOM that carries only declarations is still a CycloneDX BOM with bomFormat and specVersion, which are the only two top level fields the 1.7 schema requires.

So any CycloneDX 1.6 or 1.7 validator can validate a CDXA, and any tool that ignores unknown blocks will silently ignore your attestations.

Where CDXA lives in a CycloneDX document

Two top level blocks carry the whole model:

  • definitions.standards holds the standards you are attesting against. Each standard has a name, version, description, owner and a list of requirements. Each requirement has its own bom-ref, an identifier that should match the standard's own numbering (for example PS.3.2), a title, the requirement text, optional descriptions, optional OWASP OpenCRE identifiers in openCre, and an optional parent to build a hierarchy.
  • declarations holds everything else: assessors, attestations, claims, evidence, targets, an affirmation and an overall signature.

Everything inside is wired together with bom-ref values. A claim points at a target by bom-ref. An attestation map points at a requirement and at claims by bom-ref. A claim points at evidence by bom-ref. This makes the document a small graph, and it means one mistyped reference breaks the chain without failing JSON schema validation, because the schema checks the shape of a reference, not whether it resolves.

Targets

declarations.targets lists what claims are made against. It has three arrays: organizations, components and services. A component target uses the normal CycloneDX component object, so it can carry a purl, hashes and a version. A claim's target field is then a bom-ref to one of these, or to the BOM's own metadata component.

The schema text says a target can represent "a specific system, application, API, module, team, person, process, business unit, company". That breadth is intentional. CDXA is not limited to artifacts. You can attest that a team follows a code review process just as you can attest that a binary was built on a hardened runner.

Claims, evidence and counter evidence

A claim is the heart of CDXA. Its fields in the 1.6 and 1.7 schemas are:

Claim fieldWhat it holds
bom-refThe claim's identifier
targetbom-ref of the thing the claim is about
predicateThe assertion itself, as text ("the release has SLSA build provenance")
mitigationStrategiesbom-ref values of evidence describing mitigations
reasoningWhy the evidence supports the claim
evidencebom-ref values of supporting evidence
counterEvidencebom-ref values of evidence that cuts against the claim
externalReferencesLinks to related material, including other BOMs
signatureOptional enveloped JSF signature on this claim

Note the word predicate. In CDXA it is a human readable sentence. In in-toto it is a typed JSON object identified by a URI. Same word, different job.

An evidence object has a bom-ref, an optional propertyName from the CycloneDX property taxonomy, a description, a data array, created and expires timestamps, an author, a reviewer and its own signature. Each data entry has a name and contents, where contents is either an inline attachment (with content type and optional encoding) or a url. Data entries can also carry a classification, sensitiveData notes and governance information, which is useful when the evidence is an internal report you cannot publish.

Counter evidence is the feature that sets CDXA apart from most attestation formats. Evidence objects are the same shape either way; what makes one "counter" is that a claim lists it under counterEvidence. The CycloneDX attestation page calls out non conformance, mitigations and counter evidence as supported use cases. A provenance attestation can only say what happened. A CDXA can say what happened, what did not, and what you did about it.

Assessors and the attestation map

An assessor has a bom-ref, an organization and a thirdParty boolean. The schema says false means a self assessor. That one flag is the difference between "we say so" and "an outside party says so", and buyers should read it first.

An attestation has a summary, an assessor reference, a map and a signature. Each map entry ties one requirement to a list of claims and counterClaims, and then scores the result twice:

  • conformance.score, a number from 0 to 1, with a rationale and optional mitigationStrategies.
  • confidence.score, also 0 to 1, with a rationale.

Splitting the two is honest: you can be fully conformant on paper with low confidence because nobody checked.

Affirmation and signatures

declarations.affirmation is a short statement plus a list of signatories. The schema requires each signatory to have either a signature, or both an externalReference and an organization. That second option lets you point at an electronic signature held in another system, which is how many legal sign offs work.

Every signature slot in CDXA is an enveloped signature in JSON Signature Format (JSF). That is a different mechanism from DSSE. JSF signs the JSON object it sits in, which depends on canonical JSON. DSSE signs opaque bytes with a type, which avoids canonicalization. You can use both: JSF inside the CDXA per claim, and DSSE plus a Sigstore bundle around the whole document, as the worked example does.

A minimal, schema valid CDXA

This document validates against the CycloneDX 1.7 JSON schema (bom-1.7.schema.json, with its referenced spdx, jsf-0.82 and cryptography-defs schemas). It also validates against 1.6 if you change specVersion to "1.6". It attests two NIST SSDF tasks for one release archive, records a counter claim about reproducibility, and links the claim about the SBOM to the SBOM itself through a BOM-Link.

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.7",
  "serialNumber": "urn:uuid:5c4f3a1e-8d2b-4e6f-9a7c-1b2d3e4f5a6b",
  "version": 1,
  "metadata": {
    "timestamp": "2026-10-23T09:00:00Z",
    "component": {
      "type": "application",
      "bom-ref": "app-2.4.0",
      "name": "example-api",
      "version": "2.4.0"
    }
  },
  "declarations": {
    "assessors": [
      {
        "bom-ref": "assessor-self",
        "thirdParty": false,
        "organization": { "name": "Example Corp Product Security" }
      }
    ],
    "attestations": [
      {
        "summary": "Release 2.4.0 build and inventory requirements",
        "assessor": "assessor-self",
        "map": [
          {
            "requirement": "req-ps-3-2",
            "claims": ["claim-sbom-published"],
            "conformance": { "score": 1, "rationale": "Signed SBOM attestation published with the release" },
            "confidence": { "score": 0.9, "rationale": "Verified with cosign in CI; not independently audited" }
          },
          {
            "requirement": "req-ps-2-1",
            "claims": ["claim-provenance"],
            "counterClaims": ["claim-reproducible"],
            "conformance": { "score": 0.75, "rationale": "Provenance signed; rebuild not yet reproducible" },
            "confidence": { "score": 0.8, "rationale": "Provenance verified, builder identity pinned" }
          }
        ]
      }
    ],
    "claims": [
      {
        "bom-ref": "claim-sbom-published",
        "target": "app-2.4.0-release",
        "predicate": "A CycloneDX SBOM for this release is published and signed",
        "evidence": ["ev-sbom-attestation"],
        "reasoning": "The SBOM attestation subject digest matches the release archive digest",
        "externalReferences": [
          { "type": "bom", "url": "urn:cdx:9f1e2d3c-4b5a-4c6d-8e7f-0a1b2c3d4e5f/1" }
        ]
      },
      {
        "bom-ref": "claim-provenance",
        "target": "app-2.4.0-release",
        "predicate": "The release archive has SLSA build provenance from the CI builder",
        "evidence": ["ev-provenance"],
        "reasoning": "Sigstore bundle verifies against the expected workflow identity"
      },
      {
        "bom-ref": "claim-reproducible",
        "target": "app-2.4.0-release",
        "predicate": "An independent rebuild produces a bit for bit identical archive",
        "counterEvidence": ["ev-rebuild-diff"],
        "reasoning": "A rebuild differed in embedded timestamps"
      }
    ],
    "evidence": [
      {
        "bom-ref": "ev-sbom-attestation",
        "description": "cosign verify-blob-attestation output for the CycloneDX predicate",
        "data": [
          { "name": "verify-log", "contents": { "url": "https://example.com/releases/2.4.0/verify-sbom.txt" } }
        ],
        "created": "2026-10-23T08:55:00Z"
      },
      {
        "bom-ref": "ev-provenance",
        "description": "Sigstore bundle holding the SLSA provenance attestation",
        "data": [
          { "name": "provenance-bundle", "contents": { "url": "https://example.com/releases/2.4.0/provenance.sigstore.json" } }
        ],
        "created": "2026-10-23T08:50:00Z",
        "expires": "2027-10-23T08:50:00Z"
      },
      {
        "bom-ref": "ev-rebuild-diff",
        "description": "Diff between the released image and an independent rebuild",
        "data": [
          { "name": "rebuild-diff", "contents": { "attachment": { "contentType": "text/plain", "content": "layer 3: mtime differs in 214 files" } } }
        ],
        "created": "2026-10-23T08:58:00Z"
      }
    ],
    "targets": {
      "components": [
        { "type": "file", "bom-ref": "app-2.4.0-release", "name": "example-api-2.4.0.tar.gz", "version": "2.4.0" }
      ]
    },
    "affirmation": {
      "statement": "I affirm these declarations are accurate to the best of my knowledge.",
      "signatories": [
        {
          "name": "Jane Doe",
          "role": "Head of Product Security",
          "organization": { "name": "Example Corp" },
          "externalReference": { "type": "electronic-signature", "url": "https://example.com/releases/2.4.0/affirmation-signature" }
        }
      ]
    }
  },
  "definitions": {
    "standards": [
      {
        "bom-ref": "std-ssdf",
        "name": "NIST SSDF",
        "version": "1.1",
        "owner": "NIST",
        "requirements": [
          { "bom-ref": "req-ps-2-1", "identifier": "PS.2.1", "title": "Make software integrity verification information available to software acquirers" },
          { "bom-ref": "req-ps-3-2", "identifier": "PS.3.2", "title": "Collect, safeguard, maintain, and share provenance data for all components of each software release" }
        ]
      }
    ]
  }
}

Three claims target one release archive, one backed only by counter evidence; the attestation maps each SSDF requirement to claims with self assessed scores. The URLs are placeholders.

What an in-toto Statement contains

The in-toto Attestation Framework splits an attestation into layers. The Statement is the middle one: it binds a claim to artifacts and names the claim's type. The Envelope is the outer layer that handles serialization and signatures. The Predicate is the inner claim.

subject, predicateType and predicate

A v1 Statement has four fields:

FieldRequiredMeaning
_typeYesAlways https://in-toto.io/Statement/v1 for this version
subjectYesArray of ResourceDescriptors; each MUST have a digest
predicateTypeYesURI naming the predicate schema
predicateNoThe claim body; unset is treated as set but empty

The spec is blunt about matching: subject artifacts are matched "purely by digest, regardless of content type". The name is a hint, not an identity. If a verifier matches on name alone, it is not verifying anything.

How DSSE wraps it

The in-toto envelope spec recommends DSSE v1.0. A DSSE JSON envelope has three fields: payload (the base64 encoded Statement), payloadType and signatures, an array of objects with an optional keyid and a required sig. For in-toto, payloadType must be application/vnd.in-toto+json or a predicate specific variant named after the predicate's spec file, and the payload must be a base64 encoded Statement.

What actually gets signed is not the raw payload. DSSE defines a Pre Authentication Encoding:

PAE(type, body) = "DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(body) + SP + body

Because the type and the lengths are inside the signed bytes, an attacker cannot take a valid signature over one payload type and replay it as another. Because the body is treated as opaque bytes, nobody has to agree on JSON canonicalization. Those two properties are why the in-toto envelope spec asks envelopes to authenticate the payload type and to avoid depending on canonicalization. There is more on this in what is a DSSE envelope and why attestations use it.

Predicate types you will meet

The in-toto attestation repository lists vetted predicate types. The ones that matter next to a BOM are:

PredicateType URI (as used by tools)What it carries
CycloneDXhttps://cyclonedx.org/bomA whole CycloneDX BOM as the predicate
SPDX 2 and SPDX 3https://spdx.dev/Document for SPDX 2 in cosign and in-toto-golangA whole SPDX document
SLSA Provenancehttps://slsa.dev/provenance/v1How the artifact was built
SLSA Verification Summary (VSA)Defined by SLSAA verifier's decision that an artifact meets a SLSA level
SCAI Reporthttps://in-toto.io/attestation/scai/v0.3Evidence based assertions about attributes of an artifact or its supply chain
Vulns, Test Result, Release, Runtime Traces, LinkSee the repositoryScan results, test outcomes, release membership, runtime traces, in-toto 0.9 links

The CycloneDX and SPDX URIs are the constants in the cosign and in-toto-golang source. See also which attestation predicate type do you need and the broader in-toto attestation formats review.

What SLSA provenance v1 records about a build

SLSA provenance is "verifiable information about software artifacts describing where, when, and how something was produced", in the spec's words. Its model is simple: a build platform, identified by builder.id, ran a parameterized build template, identified by buildType, and produced the artifacts in subject.

The fields

The predicate has two halves.

buildDefinition describes what was asked for:

  • buildType: a URI for the build template. It is often specific to the build platform.
  • externalParameters: the build's external interface, such as the repository, ref and workflow path. The spec says these are untrusted, MUST be included, and MUST be verified downstream.
  • internalParameters: values set by the platform. They are trusted because the platform is trusted, and they are optional.
  • resolvedDependencies: everything fetched during the build, as ResourceDescriptors. A git URI parameter usually resolves to a specific commit recorded here.

runDetails describes what actually ran:

  • builder.id: the identity of the build platform, which the spec defines as the transitive closure of everything trusted to run the build and record the provenance honestly.
  • builder.builderDependencies and builder.version.
  • metadata.invocationId, metadata.startedOn and metadata.finishedOn.
  • byproducts: other outputs, such as logs, as ResourceDescriptors.

The 1.0 change history explains the renames you will still see in older tools: parameters became externalParameters, environment became internalParameters, materials became resolvedDependencies, and buildInvocationId, buildStartedOn and buildFinishedOn were shortened. completeness and reproducible were removed and are now implied by builder.id. If a parser looks for materials, it is reading v0.2.

What a provenance Statement looks like

Here is the shape for the release archive used in this post. It is an illustrative in-toto Statement v1 with a SLSA provenance predicate; the digests and IDs are examples.

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    {
      "name": "example-api-2.4.0.tar.gz",
      "digest": { "sha256": "ca5e6e6f43aaae4aed693d1c5e6b28e83b3617f79ea88e64a6008142086f17f2" }
    }
  ],
  "predicateType": "https://slsa.dev/provenance/v1",
  "predicate": {
    "buildDefinition": {
      "buildType": "https://example.com/build-types/release-workflow/v1",
      "externalParameters": {
        "repository": "https://git.example.com/example/api",
        "ref": "refs/tags/v2.4.0",
        "workflow": ".ci/release.yml"
      },
      "resolvedDependencies": [
        {
          "uri": "git+https://git.example.com/example/api@refs/tags/v2.4.0",
          "digest": { "gitCommit": "3f9a1c2b7d4e5f60718293a4b5c6d7e8f9012345" }
        }
      ]
    },
    "runDetails": {
      "builder": { "id": "https://ci.example.com/hosted-runners/v3" },
      "metadata": {
        "invocationId": "https://ci.example.com/example/api/runs/8812",
        "startedOn": "2026-10-23T08:41:07Z",
        "finishedOn": "2026-10-23T08:46:52Z"
      }
    }
  }
}

Provenance says how a build ran, not that its inputs were safe or that it met any standard beyond what builder.id implies. Our SLSA v1 provenance guide covers levels and builder trust.

What a Sigstore bundle contains and why it matters offline

The Sigstore documentation defines a bundle as "everything required to verify a signature on an artifact". It has two parts: verification material and content. The current documented version is v0.3, with media type application/vnd.dev.sigstore.bundle.v0.3+json.

Verification material

  • Key material. Either a single X.509 leaf certificate (the recommended form on the public instance, usually a short lived Fulcio certificate that carries the signer's identity) or a public key hint for a key delivered out of band.
  • Transparency log entries. Each entry has a logIndex, logId, kindVersion (kind hashedrekord for plain signatures or dsse for attestations), integratedTime, an inclusionPromise holding a signed entry timestamp, an inclusionProof (tree index, root hash, tree size, the hash path and a signed checkpoint) and the canonicalizedBody. The spec does not require log entries, but strongly encourages them for anything public.
  • Timestamps. Zero or more RFC 3161 signed timestamps.

The doc spells out why timestamps matter. Fulcio certificates are short lived, and verification often happens after the certificate has expired, so a verifier needs proof the signature was made while the certificate was valid. A bundle must therefore include at least one signed entry timestamp from a transparency log or an RFC 3161 timestamp.

Content

The content is exactly one of:

  • messageSignature: a signature over an artifact digest, with an informational messageDigest.
  • dsseEnvelope: a DSSE envelope whose payloadType is application/vnd.in-toto+json and whose payload is an in-toto Statement.

One detail trips people up. The Sigstore bundle allows only a single signature in its DSSE envelope, even though DSSE allows many. The in-toto envelope spec notes that this makes the Sigstore bundle not ITE-5 compliant for multi signer cases. If you need two parties to sign one Statement, plan for two bundles or a different envelope.

Why the bundle matters for offline verification

Before bundles, verifying a keyless signature meant asking the transparency log for the entry and the inclusion proof at verification time. The bundle carries the inclusion proof, the signed checkpoint and the signed entry timestamp with the signature. A verifier that also has the Sigstore trusted root (the CA and log public keys, which cosign accepts as a file through --trusted-root) can check the certificate chain, the signature, the identity and the log inclusion from local files.

That is why the CDXA in this post cites a bundle file as evidence rather than a log URL. More in can you verify Sigstore signatures offline.

Field by field comparison

This table lines up the fields that do the same job across formats. A dash means the format has no equivalent.

JobCycloneDX SBOM (1.7)CDXA (CycloneDX 1.6, 1.7)in-toto Statement v1SLSA provenance v1Sigstore bundle v0.3
Identify the artifactmetadata.component with hashes, purltargets.components and claim targetsubject[].digestInherits subjectDigest inside the signed content
Say what kind of document this isbomFormat, specVersionSame as SBOM_type, predicateTypepredicateTypemediaType
The claim itselfInventory of componentsclaims[].predicate (text)predicate (typed object)buildDefinition, runDetailsNone, it carries others' claims
Supporting proofevidence on components (identity, occurrences)evidence[] with data, counterEvidenceOther attestationsresolvedDependencies, byproductsLog inclusion proof, timestamps
Who made the claimmetadata.authors, metadata.toolsassessors[] with thirdPartySigner key or identitybuilder.idCertificate identity or key hint
Requirements or policyNonedefinitions.standards[].requirements[]NonebuildType implies the templateNone
ScoringNoneconformance.score, confidence.score (0 to 1)NoneNoneNone
SignatureOptional JSFJSF on claims, evidence, attestations, affirmationDSSE envelopeDSSE envelopeContains the signature or envelope
Timemetadata.timestampevidence.created, evidence.expiresNone in the StatementstartedOn, finishedOnintegratedTime, RFC 3161 timestamps
Link to other documentsexternalReferences, BOM-LinkexternalReferences on claims, evidence URLsResourceDescriptor uriResourceDescriptor uriNone

Only CDXA has a native requirement and score, and only the Sigstore bundle carries the material to verify a signature years later.

Which question each format answers, versus what a BOM answers

Written from the reader's side: what is the person on the other end trying to find out?

The question you need answeredReach forWhy not the others
Which components and versions are in this release?SBOMProvenance lists resolved build inputs, not the full shipped inventory
Is this release affected by a newly published vulnerability?SBOM plus VEXCDXA can reference a VEX but is not where exploitability lives
Was this artifact built by our CI, from this commit, with these parameters?SLSA provenanceAn SBOM does not record the builder or the run
Did someone with the right identity sign this, and was it logged?Sigstore bundleJSF signatures in a BOM do not carry log proofs or identity certificates
Does this SBOM really belong to this artifact?in-toto Statement with the SBOM as predicateA bare SBOM file names a component but is not cryptographically bound to the artifact bytes
Does this release meet NIST SSDF PS.3.2, and who says so?CDXAProvenance and SBOMs are evidence, not a scored claim against a requirement
Did a verifier check the provenance against a policy so I do not have to?SLSA VSAProvenance is raw input to that decision
Where are we not compliant, and what mitigates it?CDXA with counter evidence and mitigation strategiesNo other format here has a field for non conformance

CDXA next to SCAI and VSA

CDXA is not the only format that makes claims backed by evidence. Two in-toto predicates overlap with it, and it helps to know where the lines are.

SCAI (Software Supply Chain Attribute Integrity, version 0.3) captures "functional attribute and integrity information" about an artifact and the environment that built it, with evidence per attribute, bound to a digest through the in-toto subject. SLSA VSA records a named verifier's decision that an artifact met a SLSA level. Neither has a notion of an external standard, a requirement hierarchy, assessors or scores.

CDXA is standard agnostic, scores conformance and confidence separately, supports counter evidence and can target organizations and services. It is not natively bound to a digest, so put a component with hashes in targets or wrap the CDXA as an in-toto predicate. A sensible split: SCAI or VSA for machine checked facts a deploy gate reads, CDXA for the auditor facing roll up.

How to link a CDXA, a provenance attestation and an SBOM for one release

The goal is that someone holding only the release artifact can find and verify all three documents, and someone holding only the CDXA can trace every claim back to signed proof. There are four linking mechanisms, and a good release uses all of them.

1. The artifact digest

Every in-toto Statement for the release, whether it carries the SBOM or the provenance, must have the same subject digest. Put the same digest in the SBOM's metadata.component.hashes and in the CDXA target component. Verifiers then check equality, not names.

2. BOM-Link between CycloneDX documents

CycloneDX defines BOM-Link URNs in the schema. A document link has the form urn:cdx: followed by the target BOM's serial number UUID, a slash and its version; an element link adds # and a bom-ref. The CDXA above uses urn:cdx:9f1e2d3c-4b5a-4c6d-8e7f-0a1b2c3d4e5f/1 in a claim's externalReferences with type bom, which points at version 1 of the SBOM whose serialNumber is urn:uuid:9f1e2d3c-4b5a-4c6d-8e7f-0a1b2c3d4e5f. The link only holds if the SBOM really has that serial number and version, so generate serial numbers deliberately and never reuse them.

3. External reference types

The CycloneDX 1.7 schema has external reference types built for this, including attestation, bom, build-meta, build-system, evidence, formulation, vulnerability-assertion, exploitability-statement, certification-report and digital-signature. Use attestation on the SBOM's metadata component to point at the provenance bundle, and bom to point back from attestations to the SBOM.

4. Evidence data URLs

In CDXA, each evidence object's data[].contents.url should point at the exact signed file: the Sigstore bundle for the provenance, the bundle for the SBOM attestation, and verification logs. Set expires on evidence that goes stale, such as a scan result.

Worked example: sign an SBOM with cosign, attach provenance, record a CDXA claim

This walkthrough uses a release archive so it works without a registry. Every command flag below appears in the official cosign, cdxgen, CycloneDX CLI or SLSA generator documentation. Run steps 1 to 4 and 6 in CI with keyless signing so the certificate identity is your workflow, not a person's laptop.

Step 1: build the artifact and record its digest

tar -czf example-api-2.4.0.tar.gz dist/
sha256sum example-api-2.4.0.tar.gz | tee example-api-2.4.0.tar.gz.sha256

Everything hangs off this digest; a rebuild means redoing every attestation.

Step 2: generate and validate the SBOM

cdxgen, from the CycloneDX organisation, defaults to CycloneDX 1.7 and accepts --spec-version for 1.6 or 1.7. Its README installs it with lifecycle scripts disabled.

npm install -g @cdxgen/cdxgen --ignore-scripts
cdxgen -r -o sbom.json --spec-version 1.7 .

# CycloneDX CLI: validate against the 1.7 schema and fail the job on errors
cyclonedx validate --input-file sbom.json --input-version v1_7 --fail-on-errors

Before moving on, edit or post process sbom.json so metadata.component names the archive and carries its SHA-256 in hashes. A minimal 1.7 SBOM with the matching serial number looks like this, and it validates against bom-1.7.schema.json:

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.7",
  "serialNumber": "urn:uuid:9f1e2d3c-4b5a-4c6d-8e7f-0a1b2c3d4e5f",
  "version": 1,
  "metadata": {
    "timestamp": "2026-10-23T08:40:00Z",
    "component": {
      "type": "file",
      "bom-ref": "app-2.4.0-release",
      "name": "example-api-2.4.0.tar.gz",
      "version": "2.4.0"
    }
  },
  "components": [
    {
      "type": "library",
      "bom-ref": "pkg:npm/express@4.21.2",
      "name": "express",
      "version": "4.21.2",
      "purl": "pkg:npm/express@4.21.2"
    }
  ],
  "dependencies": [
    { "ref": "app-2.4.0-release", "dependsOn": ["pkg:npm/express@4.21.2"] }
  ]
}

Step 3: sign the SBOM file itself

This proves the SBOM file has not changed since it was produced. It does not bind it to the archive; step 4 does that.

cosign sign-blob --bundle sbom.json.sigstore.json sbom.json

In keyless mode cosign obtains a short lived certificate from Fulcio, records the signature in Rekor and writes a v0.3 bundle. With a managed key, add --key pointing at your key or KMS URI.

Step 4: attest the SBOM to the archive

cosign attest-blob wraps a predicate in an in-toto Statement whose subject is the blob's digest, signs it with DSSE and writes a bundle. The --type cyclonedx shorthand sets the predicate type to https://cyclonedx.org/bom.

cosign attest-blob \
  --predicate sbom.json \
  --type cyclonedx \
  --bundle example-api-2.4.0.sbom.sigstore.json \
  example-api-2.4.0.tar.gz

Now the SBOM is cryptographically tied to the archive bytes.

Step 5: attach SLSA provenance from the build platform

Provenance should come from the build platform, not from the job that built the artifact. The SLSA project's generic generator for GitHub Actions runs as a separate reusable workflow and signs provenance for the hashes you pass it. This is the shape from its README:

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      hashes: ${{ steps.hash.outputs.hashes }}
    steps:
      - uses: actions/checkout@v4
      - name: Build release archive
        run: tar -czf example-api-2.4.0.tar.gz dist/
      - name: Hash artifacts
        id: hash
        run: echo "hashes=$(sha256sum example-api-2.4.0.tar.gz | base64 -w0)" >> "$GITHUB_OUTPUT"

  provenance:
    needs: [build]
    permissions:
      actions: read
      id-token: write
      contents: write
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
    with:
      base64-subjects: "${{ needs.build.outputs.hashes }}"
      upload-assets: true

The generator names its output after the subject with an .intoto.jsonl extension. Its README notes that the provenance is wrapped in a DSSE envelope and shows how to read it:

cat example-api-2.4.0.tar.gz.intoto.jsonl | jq -r '.dsseEnvelope.payload' | base64 -d | jq

If you cannot use a builder that produces its own provenance yet, you can still sign a provenance predicate you write yourself with cosign attest-blob --type slsaprovenance1. Be honest about what that proves. A consumer should treat it as a record your pipeline made about itself, not as evidence from an isolated, trusted builder.

Step 6: verify both attestations the way a consumer would

Verification must pin the identity. Without --certificate-identity (or its regexp form) and --certificate-oidc-issuer, keyless verification only proves that someone with some identity signed.

cosign verify-blob-attestation \
  --bundle example-api-2.4.0.sbom.sigstore.json \
  --type cyclonedx \
  --certificate-identity-regexp '^https://github\.com/example/api/\.github/workflows/release\.yml@refs/tags/v' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  example-api-2.4.0.tar.gz \
  > verify-sbom.txt

By default verify-blob-attestation checks that the blob's digest appears in the Statement's subject (--check-claims defaults to true). For an air gapped verifier, pass --trusted-root with a Sigstore trusted root JSON file so the certificate chain and log keys come from a local file.

Step 7: write the CDXA, validate it and attest it

Write the CDXA from the earlier section, replacing the placeholder URLs with the real locations of verify-sbom.txt, the provenance file and the rebuild diff. Then validate and attest it to the same archive.

cyclonedx validate --input-file cdxa.json --input-version v1_7 --fail-on-errors

cosign attest-blob \
  --predicate cdxa.json \
  --type cyclonedx \
  --bundle example-api-2.4.0.cdxa.sigstore.json \
  example-api-2.4.0.tar.gz

A CDXA is a CycloneDX document, so the CycloneDX predicate type fits, but consumers then cannot tell an inventory from declarations by predicateType alone. A custom predicate type URI you control avoids that. cosign attest-blob --type accepts a URI as well as the built in names.

Step 8: gate on it

A simple policy check reads the verified CDXA and fails if any requirement scores below a threshold or rests only on self assessment.

cosign verify-blob-attestation \
  --bundle example-api-2.4.0.cdxa.sigstore.json \
  --type cyclonedx \
  --certificate-identity-regexp '^https://github\.com/example/api/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  example-api-2.4.0.tar.gz || exit 1

# Read the predicate from the bundle you just verified, not from a loose local file
jq -r '.dsseEnvelope.payload' example-api-2.4.0.cdxa.sigstore.json | base64 -d \
  | jq -e '[.predicate.declarations.attestations[].map[].conformance.score] | min >= 0.75'

Run the jq check only after verification succeeds, and read the predicate out of the bundle you verified rather than a copy of cdxa.json that might differ. Keep the threshold in policy, not in a developer's head.

How NIST SSDF and EO 14028 connect

NIST SP 800-218 (SSDF 1.1, February 2022) has two tasks that map onto these formats. PS.2.1 asks producers to make integrity verification information available to acquirers, with hashes and code signing as examples. PS.3.2 asks them to collect, safeguard, maintain and share provenance data for all components of each release, with an SBOM as the example, and to give recipients a way to verify its integrity. The SSDF maps PS.3.2 to EO 14028 section 4e items vi, vii, ix and x.

In format terms, the SBOM is the component provenance, the Sigstore bundle is the integrity verification information, SLSA provenance covers the build, and CDXA states per task that you did it and where the proof is. The SSDF does not require any of these formats by name.

Where Safeguard fits

Safeguard's fit here is partly direct and partly indirect, so here it is plainly.

Direct: Safeguard's SBOM generation and analysis is generally available and produces SPDX (JSON, XML, RDF) and CycloneDX (JSON, XML), with 30+ export formats and continuous scanning on push, schedule or release. Its attestation and signing capability is generally available and covers SLSA, Sigstore keyless signing through Fulcio and Rekor, in-toto and X.509, with Kubernetes admission control and SCAL (component) and LCAL (package) attestation scores. Policies, Gates and Guardrails are generally available; gates can block, warn, notify or require approval at a checkpoint in your pipeline. The Compliance and GRC suite is generally available and lists NIST SSDF, SLSA, EO 14028 and VEX among supported frameworks.

Indirect: Safeguard does not author CycloneDX declarations (CDXA) documents today. If you want a CDXA, you write it with the CycloneDX tooling shown above and use Safeguard for the SBOMs, signatures and policy checks it can cite as evidence. The Portal, which is generally available, is the surface for publishing and sharing SBOMs with audit trails and EO 14028 verification; the Trust Center inside it is early access.

Safeguard also does not convert between attestation formats or between SBOM formats beyond its listed export formats.

Common mistakes

  1. Matching on name instead of digest. The in-toto spec matches subjects only by digest. A verifier that accepts an SBOM because the filename matches has verified nothing.
  2. Verifying keyless signatures without pinning identity. cosign verify-blob-attestation without an expected identity and issuer proves only that a valid Fulcio certificate signed it. Pin both, and prefer an exact identity or a tightly anchored regexp.
  3. Treating a CDXA as proof. A CDXA is a set of claims. Its value comes from the evidence it cites and from whether the assessor is third party. Read thirdParty and the evidence URLs before you read the scores.
  4. Dangling bom-ref values. The schema validates the shape of references, not that they resolve. Add a small script to check every target, requirement, claims, evidence and assessor reference exists in the document.
  5. Self written provenance presented as builder provenance. If the same job that builds the artifact also writes the provenance, the provenance is only as trustworthy as that job. Use a builder that generates and signs provenance in isolation.
  6. Expecting one Sigstore bundle to carry two signers. The bundle allows only one signature in its DSSE envelope. Plan separate bundles per signer.
  7. Archiving signatures without bundles. A bare signature plus a log URL needs the log to be reachable at verification time. Keep the v0.3 bundle and a copy of the trusted root with your audit evidence.
  8. Forgetting expires on evidence. A clean vulnerability scan from six months ago is not evidence today. Set expires and regenerate.

FAQ

What is CycloneDX Attestations (CDXA)?

CDXA is the part of the CycloneDX standard, from version 1.6, that records claims about conformance to a standard, the evidence and counter evidence behind each claim, who assessed it, and scored conformance and confidence. It lives in the declarations and definitions blocks of a normal CycloneDX document.

Is CDXA the same as an in-toto attestation?

No. An in-toto attestation binds a typed predicate to artifact digests and is signed with DSSE. CDXA is a CycloneDX data model for compliance claims and uses JSF for embedded signatures. You can carry a CDXA as the predicate of an in-toto Statement, which gives it digest binding and DSSE signing.

Does SLSA provenance replace an SBOM?

No. Provenance records how one build ran: builder, build type, parameters and resolved dependencies. An SBOM records what the shipped software contains, including transitive components that may never appear as a resolved build dependency. Most teams need both.

What is the predicate type for SLSA provenance v1?

https://slsa.dev/provenance/v1. The SLSA spec says to always use that string rather than the version in the page URL, and it stayed the same in SLSA 1.2.

What is inside a Sigstore bundle?

Verification material (a certificate or public key hint, transparency log entries with inclusion proofs and signed entry timestamps, and optional RFC 3161 timestamps) and content (either a message signature over a digest or a single signature DSSE envelope holding an in-toto Statement).

Can I verify a Sigstore signature offline?

Yes, when you have the bundle and a copy of the Sigstore trusted root. The bundle carries the log inclusion proof and timestamps; the trusted root supplies the CA and log keys. cosign accepts the trusted root as a file through --trusted-root.

How do I sign an SBOM with cosign?

Use cosign sign-blob --bundle sbom.json.sigstore.json sbom.json to sign the file, and cosign attest-blob --predicate sbom.json --type cyclonedx --bundle out.sigstore.json ARTIFACT to bind the SBOM to the artifact's digest. For container images, cosign attest --predicate sbom.json --type cyclonedx IMAGE stores the attestation with the image.

Which tools can validate a CDXA?

Any CycloneDX 1.6 or 1.7 JSON schema validator. The CycloneDX CLI's validate command accepts --input-version v1_6 or v1_7 and --fail-on-errors. Schema validation does not check that bom-ref links resolve, so add your own reference check.

Fix it without waiting for upstream

Attestations prove what you built and what it contains, including the vulnerable dependency still sitting in the SBOM. Someone still has to ship the fixed build, and the faster that happens, the sooner every signature and claim in this post describes software you would actually want to run.

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.

3. Gold Registry: start from components that are already fixed

registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
You want to start clean on new projectsPull hardened components and images from Gold Registry

One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

See how Safeguard would help

The demos below are real product sessions. None of them shows CDXA authoring, which Safeguard does not do; they show the parts of the release flow that produce the evidence a CDXA cites.

This first demo is a real Safeguard CLI session: it installs the CLI, opens the interactive terminal, asks it to fix a lodash CVE, and approves each command as it edits package.json, runs npm install and npm test, and prints a summary without pushing anything. The same flow is how the dependency in your SBOM gets fixed before you re-run cdxgen, re-sign and re-attest the release. It ends with /permissions, /model and /scan ..

Click through it at your own pace, or open it full screen.

This one sets up the Safeguard runner on your own machine, approves it with a device code, and then scans from the terminal. It suits teams that want scanning and SBOM work to happen on infrastructure they control, next to the build that produces the artifact being attested.

Click through it at your own pace, or open it full screen.

The last demo connects Claude to the Safeguard MCP server with OAuth and chooses which tools Claude can use. Once connected, an assistant can look up the projects and findings behind a release while you draft the evidence and reasoning fields of a CDXA by hand.

Click through it at your own pace, or open it full screen.

To try it on your own release, download Safeguard or read the CLI documentation.

Quick check

Which format would you use to state that a release meets NIST SSDF PS.3.2, with a score and evidence?

CycloneDX Attestations (CDXA). Define the standard and requirement in definitions.standards, make a claim against the release in declarations.claims, cite the signed SBOM attestation as evidence, and score conformance and confidence in an attestation map entry.

Why does a Sigstore bundle help when you verify a signature a year after it was made?

The short lived signing certificate has long expired. The bundle carries a signed entry timestamp or an RFC 3161 timestamp plus the log inclusion proof, which show the signature was made while the certificate was valid, and with a trusted root file you can check all of it locally.

Sources

Last reviewed: 23 October 2026. Check the linked specifications for newer versions before you pin tooling to them.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.