Skip to content

Latest commit

 

History

History
124 lines (99 loc) · 6.51 KB

File metadata and controls

124 lines (99 loc) · 6.51 KB

Bundles and CMS

Gitsign stores its signatures as Cryptographic Message Syntax (CMS/PKCS7) objects in the git gpgsig header — the same format git's gpg.x509.program interface expects. The wider Sigstore ecosystem, and the sigstore-go libraries gitsign builds on, instead use the Sigstore bundle format.

To use the shared sigstore-go signing and verification code without changing the on-disk signature format, gitsign converts between the two. This document explains how the two formats relate and how the conversion works. The compatibility layer lives in internal/sigstore/compat.

Note: This conversion is enabled by default. See Enabling the sigstore-go path below for how to fall back to the legacy path.

The key insight: the signed artifact is the SignedAttrs

A CMS signature is not computed directly over the git object. It is computed over the CMS signed attributes (SignedAttrs): a small structure containing the content type, the message digest (sha256 of the commit/tag body), and the signing time. The SignerInfo signature is ECDSA(sha256(DER(SignedAttrs))), and the Rekor HashedRekord is also keyed on sha256(DER(SignedAttrs)).

This means the artifact in sigstore-go terms — the thing the bundle's signature and transparency log entry are over — is the marshaled SignedAttrs, not the commit body. Everything else follows from that.

Field mapping

CMS (SignerInfo / SignedData) Sigstore bundle
SignerInfo.signature messageSignature.signature
sha256(DER(SignedAttrs)) messageSignature.messageDigest (SHA2_256)
leaf certificate (the only cert in the bag) verificationMaterial.certificate (v0.3, single leaf)
Rekor entry in unsigned attr OID 1.3.6.1.4.1.57264.3.1 verificationMaterial.tlogEntries[0]
RFC3161 token in unsigned attr (1.2.840.113549.1.9.16.2.14) verificationMaterial.timestampVerificationData.rfc3161Timestamps
marshaled SignedAttrs the verification artifact (verify.WithArtifact)

Gitsign emits bundles using media type application/vnd.dev.sigstore.bundle.v0.3+json, which carries a single leaf certificate (intermediates and roots come from the trusted root, not the signature). This matches gitsign's CMS, which only ever embeds the leaf cert.

Direction 1: CMS → bundle (verification)

To verify a signature with sigstore-go, gitsign parses the stored CMS and projects each signer onto a bundle:

ParseSignaturePEM(sig)            -> cms.SignedData
SignerInfoToBundle(sd, signer)    -> { Bundle, Artifact }
  • Artifact is the marshaled SignedAttrs; callers MUST pass it as the verification artifact, since the bundle's messageSignature only carries its digest.
  • The Rekor entry stored in the CMS clears its canonicalized body on storage, so it is recomputed from the signed message + signature + certificate when building the bundle's tlogEntries.

A CMS signature may contain multiple signers; each becomes its own bundle (a bundle holds exactly one messageSignature), and SignedDataToBundle returns one per signer.

sigstore-go's verifier checks the signature, certificate chain, transparency log inclusion, and identity. The one thing it cannot do from the bundle alone is confirm the SignedAttrs actually describe this git object; gitsign enforces that separately by comparing sha256(git object) against the SignedAttrs message-digest attribute (the "content binding" check). See verification.md for the higher-level verification flow.

Direction 2: bundle → CMS (signing)

Signing runs the inverse. sigstore-go produces the signature and Rekor entry as a bundle, which is converted into a CMS object for storage:

BuildSignedAttributes(body)               -> (SignedAttrs, marshaled-for-signing)
sign.Bundle(PlainData{marshaled}, ...)    -> bundle (signature + cert + tlog)
BundleToSignedData(body, SignedAttrs, b)  -> cms.SignedData  (stored as the signature)
  • The signing key and Fulcio certificate come from gitsign's existing identity (fulcio.Identity), adapted to sigstore-go's sign.Keypair and sign.CertificateProvider (see compat.NewKeypair / compat.NewCertificateProvider). The OIDC + Fulcio flow and the credential cache are unchanged — sigstore-go drives signing and the Rekor upload, not credential acquisition.
  • The CMS SignerInfo is assembled around the externally-computed signature by the fork helpers SignedAttributes and AddSignerInfoWithSignature in internal/fork/ietf-cms. The result is byte-for-byte equivalent to what the fork's own signer would produce for the same attributes, signature, and certificate.
  • RFC3161 timestamps are applied to the assembled CMS (which stores the RFC3161 TimeStampToken), rather than via sigstore-go (whose timestamp client returns the full TimeStampResp).

Because the signature is over the SignedAttrs — which include the signing time — the attributes are built once and reused for both signing and assembly.

Enabling the sigstore-go path

The sigstore-go path is used by default for both signing and verification, in both online and offline Rekor modes. To fall back to the legacy CMS + Rekor path, disable it:

git config gitsign.enableSigstoreGo false
# or: GITSIGN_ENABLE_SIGSTORE_GO=false

The two Rekor modes map onto sigstore-go differently. In offline mode the Rekor entry is embedded in the signature (the standard bundle shape). In online mode the entry is keyed on the commit SHA and uploaded to Rekor without being embedded — sigstore-go signs and uploads it, and verification discovers it via Rekor's online search API before verifying the assembled commit-SHA bundle.

The on-disk CMS signature format is identical whether or not the option is set; only the implementation of how signing and verification are performed changes.