Attest Command Reference

The attest command verifies signatures and provenance on artefacts you consume. It reads what cdx --sign writes — a DSSE envelope and cosign-compatible detached sidecars — and anything else in those formats.

Run it with no flags:

vulnetix attest verify sbom.cdx.json
[OK] sbom.cdx.json is signed, and the signature is valid.

Signed by
  https://github.com/acme/repo/.github/workflows/release.yml@refs/heads/main
  authenticated by https://token.actions.githubusercontent.com
  certificate issued by Sigstore public-good
  this is the repository you are in

This check is looser than it could be
  any identity this CA will issue to would also pass, not just this one
    vulnetix attest verify sbom.cdx.json --identity https://github.com/acme/repo/.github/workflows/release.yml@refs/heads/main

  any OIDC provider this CA accepts would also pass
    vulnetix attest verify sbom.cdx.json --issuer github

What it checks, and what it does not

Two rules pull against each other, and both matter.

A verifier must not report success for a check it did not run — a green tick over a skipped chain validation converts an unknown into a false assurance. But a verifier that answers every question with “you did not tell me what to trust” is not being careful either; it is making you do its job.

So: do the work by default, and be exact about what was done.

CheckRuns by defaultNotes
SignaturealwaysThe signature covers the DSSE pre-authentication encoding, not the bare payload
Certificate chainyesAgainst the built-in Sigstore public-good root
Certificate validityalwaysAn expired keyless certificate passes, with the caveat stated — see below
Identitywhen you pin oneThe identity is always read and reported regardless
Issuerwhen you pin one
Transparency lognoNeeds the log’s public key and an online query — see below

Anything not established becomes a suggestion carrying a complete, pre-filled command. Never a fragment with an ellipsis: a suggestion you have to complete is one you can only use if you already knew the answer.

Expired certificates

A Fulcio certificate lives about ten minutes. Failing every signature older than that would make the verifier useless — the signature was valid when it was made. So an expired certificate passes, and the detail says what that does and does not mean: only a transparency-log entry proves the signature predates the expiry.

The transparency log

Rekor inclusion is the one thing genuinely not doable from the material at hand: the sidecars carry no log entry, so proving inclusion means querying the log by artefact hash and verifying its signed tree head — a network dependency and a second trust root.

Rather than a skipped check dressed up as a gap in your run, it is a suggestion naming the tool that does it, fully populated:

cosign verify-blob --certificate-identity https://github.com/acme/repo/... \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --bundle sbom.cdx.json.intoto.jsonl sbom.cdx.json

Trust anchor resolution

--trusted-root is an override for a private Sigstore deployment, not a prerequisite. The anchor resolves most-specific first:

SourceScope
--trusted-rootThis run
SIGSTORE_ROOT_FILEThis shell or CI job
.vulnetix/trusted-root.pemThis repository
Built inThe Sigstore public-good instance

A team running a private Sigstore commits its root once and nobody types the flag again:

cp fulcio-root.pem .vulnetix/trusted-root.pem
git add .vulnetix/trusted-root.pem

The output names whichever link answered, so “trusted” is never ambiguous:

  certificate issued by .vulnetix/trusted-root.pem

A chain that does not anchor to the resolved root fails, and the failure names the flag that would fix it:

certificate-chain: does not chain to Sigstore public-good
  (x509: certificate signed by unknown authority).
  If this was signed by a private Sigstore instance, pass --trusted-root
  with its root certificate.

The built-in root is pinned rather than fetched over TUF. A test fails once it expires, so a stale pin is loud rather than an unexplained chain error.


Pinning the signer

A valid signature means somebody Sigstore trusts made it — which, for GitHub Actions, is any workflow in any repository. Pinning the identity is what turns that into an answer.

# Exact — the safe default. Paste back what the command printed.
vulnetix attest verify sbom.cdx.json \
  --identity https://github.com/acme/repo/.github/workflows/release.yml@refs/heads/main

# Pattern — for deliberately accepting a set
vulnetix attest verify sbom.cdx.json --identity-regex '^https://github\.com/acme/.*'

--identity is an exact comparison; --identity-regex is the pattern form. This is the split cosign uses, and the safe side is the default for a reason: every keyless subject is a URL, so under regex semantics every . is a wildcard and https://github.com/acme/repo would also match https://githubXcom/acme/repo. The identity the command prints pastes back verbatim.

Issuer shortcuts

vulnetix attest verify sbom.cdx.json --issuer github
ShortcutIssuer
githubhttps://token.actions.githubusercontent.com
gitlabhttps://gitlab.com
googlehttps://accounts.google.com
microsofthttps://login.microsoftonline.com
buildkitehttps://agent.buildkite.com
codefreshhttps://oidc.codefresh.io

A full URL is still accepted. Nobody remembers token.actions.githubusercontent.com, and a pin nobody can spell is a pin that does not get applied.

–strict

One flag instead of learning three. It requires the signer to be pinned, and fails naming the exact flags to add — values already filled in from the certificate it just read:

$ vulnetix attest verify sbom.cdx.json --strict

strict: --strict requires the signer to be pinned; add:
  --identity https://github.com/acme/repo/.github/workflows/release.yml@refs/heads/main
  --issuer https://token.actions.githubusercontent.com

Is it my repository?

Zero configuration. The signer’s repository is compared against the local git remote, and the output says which it is:

  this is the repository you are in
  a different repository from the one you are in

“Signed by github.com/acme/repo” is not an answer until you know which of those it is. It is a note, not a check — a third-party artefact legitimately comes from a third party, and failing on that by default would break the case verification matters most for.


Flags

vulnetix attest verify <artifact> [flags]
FlagDefaultDescription
--identity-Require this exact signer
--identity-regex-Require the signer to match this pattern
--issuer-Require this OIDC issuer — a URL or a shortcut name
--strictfalseRequire the signer to be pinned; fails naming the flags to add
--trusted-rootresolvedRoot certificate of a private Sigstore deployment
--require-Fail when a named check did not run, e.g. transparency-log (repeatable)
--envelope<artifact>.intoto.jsonlDSSE envelope
--signature<artifact>.sigDetached signature
--certificate<artifact>.pemSigning certificate
-o, --outputprettypretty, json
--verbosefalseShow every check, including the ones that passed

--identity and --identity-regex are mutually exclusive.

Sidecars are discovered beside the artefact using the suffixes cdx --sign writes and stock cosign expects, so there is no second convention to learn.

Provenance

When the payload is an in-toto statement, its predicate is read and reported — SLSA v0.2 and v1 both. It is labelled claimed, not verified, because it is what the payload asserts about itself.

The report names which fields are present rather than a SLSA level. A level is a property of the build platform and its controls, which no consumer can determine by reading a document that build produced.


In CI

- name: Verify the SBOM came from our own release workflow
  run: |
    vulnetix attest verify sbom.cdx.json \
      --identity "https://github.com/${{ github.repository }}/.github/workflows/release.yml@refs/heads/main" \
      --issuer github

Or, verifying a document at the point it is consumed:

vulnetix bom import vendor-sbom.cdx.json --verify-attestation --strict

A failed check aborts the import — see bom import.

Exit codes

CodeMeaning
0Every check that ran passed
1A check failed, or a --required check did not run
2Usage error

See also