Signing commits with X.509 certificates Jump to heading

Many organisations already run a certificate authority. Engineers have smart cards or certificates issued at onboarding, revocation is handled centrally, and security teams would rather not introduce a parallel trust system of SSH keys or OpenPGP keys that nobody else manages. Git supports this third format: gpg.format = x509, which hands signing to an S/MIME-capable program such as gpgsm or smimesign. The signatures chain to your CA, and revocation follows the certificateโ€™s lifecycle. The trade-off is weaker support outside the command line. This page covers the setup and the limits, as an alternative explored in GPG vs SSH commit signing.

When to use this approach Jump to heading

  • Your organisation issues X.509 certificates to engineers and wants code signing tied to that identity system.
  • Compliance requirements name a specific CA or require certificate-based signatures.
  • Certificates live on smart cards or in an OS certificate store that already has the private key.
  • Your verification happens mostly in your own gates, where you control the trust anchors.
  • If your forgeโ€™s web interface must show commits as verified and it does not trust your CA, SSH signing is the pragmatic choice.

Step 1 โ€” Choose the signing program Jump to heading

Git does not implement X.509 signing itself; it calls an external program with GPG-compatible arguments. Two are common.

gpgsm against smimesigngpgsm is part of GnuPG, runs everywhere GnuPG does and works well with smart cards through scdaemon. smimesign uses the operating system's certificate store on macOS and Windows, which suits organisations that already push certificates there.gpgsmsmimesignships withGnuPGseparate installkey storeGnuPG keybox, smart cardOS keychain / cert storebest onLinux, smart cardsmacOS, Windowsrevocation checksCRL / OCSP via dirmngrOS chain validationboth produce the same kind of signature โ€” choose by where your certificates already live
# gpgsm (Linux, or anywhere GnuPG is installed)
gpgsm --version | head -1
# smimesign (macOS / Windows), installed from your package manager
smimesign --version

Step 2 โ€” Make the certificate and its chain available Jump to heading

With gpgsm, import the personal certificate with its private key, then the CA chain. With smimesign, the certificate must already be in the OS store, typically pushed by device management.

# gpgsm: import a PKCS#12 bundle and the CA certificates
gpgsm --import alice.p12
gpgsm --import corp-root-ca.crt corp-issuing-ca.crt
gpgsm --list-secret-keys
# Note the ID line, e.g. ID: 0x4A3B2C1D

Root CA certificates must be marked trusted before gpgsm will accept chains to them.

# Trust the corporate root by fingerprint
gpgsm --list-keys --with-colons corp-root-ca | awk -F: '/^fpr/{print $10; exit}'
echo "<ROOT_FPR> S" >> ~/.gnupg/trustlist.txt
# Verification: the personal certificate validates to a trusted root
gpgsm --list-keys --with-validation [email protected] | grep -i validity

Step 3 โ€” Configure Git to sign with X.509 Jump to heading

Set the format and program, and identify the certificate by its ID or email.

git config --global gpg.format x509
git config --global gpg.x509.program gpgsm        # or smimesign
git config --global user.signingKey 0x4A3B2C1D    # gpgsm ID, or the cert email for smimesign
git config --global commit.gpgSign true
How an X.509 commit signature is produced and trustedGit passes the commit to the configured X.509 program, which signs with the private key held in a keybox, smart card or OS store. A verifier checks the signature and walks the certificate chain to a trusted corporate root, consulting revocation data along the way.git commitgpg.format x509gpgsm / smimesignprivate keyCMS signaturein gpgsig headerChain to rootissuing โ†’ root CARevocationCRL or OCSPtrust comes from the CA, so revocation is whatever the CA says it is
# Verification: sign and check
git commit --allow-empty -m "x509 signing test"
git log -1 --show-signature
git log -1 --format='%G? %GS'

%GS reports the certificate subject for X.509 signatures, which is a distinguished name rather than an email.

Step 4 โ€” Verify in your own gates Jump to heading

Your CI or server hook needs the same trust anchors. Build an isolated gpgsm home with only the corporate root and issuing CA, then verify.

export GNUPGHOME="$RUNNER_TEMP/gnupg"; mkdir -p -m 700 "$GNUPGHOME"
gpgsm --import trust/corp-root-ca.crt trust/corp-issuing-ca.crt
echo "$(cat trust/corp-root.fpr) S" > "$GNUPGHOME/trustlist.txt"
git -c gpg.format=x509 -c gpg.x509.program=gpgsm log --format='%H %G?' "$BASE..HEAD" |
  awk '$2!="G"{print; bad=1} END{exit bad}'

The gateโ€™s range logic is the same as for any other format; see verifying a range of commits in CI, not just the tip.

Step 5 โ€” Plan for revocation and certificate renewal Jump to heading

Certificates expire, often after a year or two, and are renewed with a new serial number. Old signatures remain cryptographically valid, but whether a verifier accepts a signature made by a since-expired certificate depends on its settings โ€” and many default to rejecting signatures whose certificate is expired now, not at signing time.

A certificate's life and its signaturesA certificate is issued, used to sign commits for a year, and renewed. If it is later revoked for compromise, signatures made after the compromise date should fail, but signatures from before should still verify, which requires verifiers that consider signing time.Issuedsigning begins2025-11Renewednew serial, same subject2026-10Old cert expiresold sigs still valid2026-11Compromiserevoke from this date2027-03Verify historyneeds signing-time logicaudita verifier that checks validity only at 'now' turns every renewal into a wall of red

For gates that only verify new commits, this rarely matters: the certificate is current when the commit arrives. For historical audits it matters a great deal, so record the certificate fingerprint and validity window alongside each verification result rather than re-deriving them years later.

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Will GitHub show X.509-signed commits as verified? Jump to heading

Only if the certificate chains to a CA GitHub trusts, which in practice means certificates issued by a small set of public CAs. Commits signed by a private corporate CA appear as unverified on the web while still verifying correctly in your own gates.

Can I mix X.509 with SSH signing in one repository? Jump to heading

Yes. Each commit records its own signature type, and Git chooses the verifier from the signature, not from gpg.format. Your gate must have trust configured for every format it accepts.

Do smart cards work with this? Jump to heading

Yes, and they are a common reason to choose X.509. With gpgsm, the smart card is reached through scdaemon; with smimesign, through the OS certificate store. The private key never leaves the card.