Sigstore bundles: keyless signing and provenance

Zero-dependency verifier for a Sigstore bundle, the exact artifact npm publish --provenance produces and the npm registry serves at its attestations API. A bundle is a keyless (Fulcio) signature with a Rekor transparency-log inclusion proof, over one of two content arms: a DSSE-wrapped in-toto attestation, which cosign attest and npm provenance produce, or a message signature over an artifact's own bytes, which cosign sign-blob produces. verifyBundle composes five fail-closed legs against caller-supplied trust material (the Fulcio CA roots + Rekor log keys, never trusted from the bundle): the signature under the Fulcio leaf key, over the DSSE PAE preimage or over the artifact opts.artifact supplies; the Fulcio certificate chain, validated as of the Rekor log time (the cert is ephemeral, ~10 minutes); the Rekor inclusion proof folded to a Rekor-signed tree root; the log entry binding to this exact signature and certificate; and the artifact binding, which is the in-toto subject digest the caller confirms for a DSSE bundle and the entry's own authenticated hash for a message signature. Verify-only and offline: every input is in the bundle or a caller argument. Reuses the shipped X.509 parser, RFC 5280 path validator, RFC 9162 Merkle verifier, and native crypto engine; the net-new codecs are the DSSE PAE byte-builder and a fail-closed JSON bundle reader.

pki.sigstore.pae

since 0.2.3 stable
pki.sigstore.pae(payloadType, payloadBytes) -> Buffer

The DSSE Pre-Authentication Encoding: "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body, where LEN is the ASCII-decimal byte length (no leading zeros) and type is the UTF-8 payloadType. This is the exact preimage a DSSE signature covers; LEN is over the decoded body byte length, never the base64 length. Any deviation is a signature-verify bypass.

Example

var b = pki.sigstore.pae("application/vnd.in-toto+json", Buffer.from("{}"));
b.slice(0, 6).toString(); // "DSSEv1"

References

  • spec DSSE

pki.sigstore.parseBundle

since 0.2.3 stable
pki.sigstore.parseBundle(input) -> bundle

Decode + structurally validate a Sigstore bundle (a JSON object, string, or Buffer) fail-closed: a non-object, malformed JSON, an oversize input, an unknown mediaType, or a missing required member throws a typed sigstore/bad-bundle / sigstore/bad-bundle-version. Returns the validated bundle (structure only, no cryptographic verification).

An object is copied into plain data and read back before any rule runs on it, and the copy is what comes back, so a caller reads what was checked. A field reached through an accessor is refused rather than called, and so is a value JSON does not carry: a bundle is data, and a field that computes its value can remove another the walk has already counted. pki.sigstore.verifyBundle takes the same copy, so the two verbs cannot answer differently about one bundle.

Example

// requires: `bundle` -- a Sigstore bundle as cosign or npm provenance emits it
// (the JSON object, a JSON string, or its raw bytes)
var b = pki.sigstore.parseBundle(bundle);
b.mediaType; // "application/vnd.dev.sigstore.bundle.v0.3+json"

References

  • spec Sigstore bundle v0.3

pki.sigstore.verifyBundle

since 0.2.3 stable
pki.sigstore.verifyBundle(bundle, opts) -> Promise<result>

Verify a Sigstore bundle (an npm --provenance artifact) offline against caller-supplied trust material, composing five fail-closed legs: the DSSE signature over its PAE under the Fulcio leaf key; the Fulcio chain validated as of the Rekor log time; the Rekor inclusion proof folded to a Rekor-signed root; the log entry bound to this exact signature; and the in-toto SLSA statement. Any leg failing throws a typed sigstore/* error. On success returns { valid: true, verified: true, payload, statement, subjects, predicateType, predicate, identity, identityChecked, predicateTypeChecked, sctChecked, validScts, integratedTime, logIndex, logId } (valid is the canonical toolkit-wide verdict alias of verified, predicateTypeChecked says whether a pinned opts.predicateType was checked, and logIndex / logId identify the attested Rekor log entry). payload is the raw verified envelope bytes (never a re-serialization), and the caller confirms a subjects[].digest matches the published artifact.

Fulcio logs every certificate it issues to a certificate-transparency log and embeds the log's receipt in the certificate (RFC 6962 sec. 3.2). Supplying opts.ctLogs checks it: the receipt is verified over the certificate as it stood before the receipt was added, under the key of the log that issued it, and at least one receipt must verify against a pinned log (sec. 3.3). It is what says the signing certificate was public when it was issued rather than handed out quietly. The option is opt-in, so a caller that pins no log is unaffected and sctChecked reports false; supplying an empty array is refused rather than read as a policy that checks nothing. validScts counts the receipts that verified.

A log's validFor window is read at the instant the receipt was signed, since that is when the key was the log's, not at the time the artifact was later logged. A receipt dated after the instant being validated at is not counted (RFC 6962 sec. 5.2); with no opts.time that instant is the end of the Rekor entry's own second, which allows for the entry recording whole seconds while a receipt carries milliseconds, and accepts nothing beyond it.

verified: true says the artifact was signed and logged; it says nothing about who. Fulcio issues a certificate to anyone who completes an OIDC flow, so who signed is decided only by opts.identity, and identityChecked reports which of its fields were compared ({ san, issuer, sourceRepositoryURI }, each a boolean). An identity naming none of them is refused, since it would accept every signer while reading as a policy; so is an unrecognized field name, which would otherwise pin nothing under a spelling the operator believes constrains the signer.

A bundle carrying a message_signature is verified against the artifact itself, which opts.artifact supplies as bytes and which this hashes. It is required for that arm: the messageDigest the bundle carries is covered by no signature, and the arm's own definition says a client must not use it to verify the signature, so there is no shape here in which a caller hands over a digest instead. The artifact is held to the hash the Rekor entry records, which the inclusion proof covers, and then the signature is checked over its bytes. artifactDigest reports the digest this computed and digestAlgorithm the algorithm the entry named; messageDigestChecked says whether the bundle's own digest was there to agree with. opts.predicateType is refused for that arm, and opts.artifact for a DSSE one, rather than being read and ignored.

The verdict has one shape for both arms. contentType names the arm, and the fields the other arm has nothing to report are present and null.

Options

fulcioRoots:   Array,      // the Fulcio CA anchors: a DER Buffer or { der, validFor } each
rekorKeys:     Array,      // [{ keyId, spki, validFor? }] the Rekor log public keys
ctLogs:        Array,      // optional [{ keyId, spki, validFor? }] certificate-transparency logs; when given, the certificate's embedded receipt is checked
identity:      object,     // optional policy: { san, issuer, sourceRepositoryURI }; at least one required when present
predicateType: string,     // optional: require this in-toto predicateType (e.g. the SLSA URI); dsse_envelope bundles only
artifact:      BufferSource, // the bytes a message_signature covers; required for that arm, refused for a dsse_envelope
time:          Date,       // optional check-date override (default: the Rekor integratedTime)

Example

async function example() {
  // requires: `bundle` from cosign / npm provenance, and `sigstoreTrust` built from
  // the public-good trusted_root.json (the Fulcio + Rekor material it pins)
  var out = await pki.sigstore.verifyBundle(bundle, sigstoreTrust);
  out.verified;            // true
  out.subjects[0].digest;  // { sha512: "..." } -- confirm against your tarball
}
example();

References

  • spec DSSE
  • spec Sigstore bundle v0.3
  • spec RFC 9162
  • spec SLSA provenance v1