Skip to content

droren/smimesign

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

271 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

smimesign

smimesign is an S/MIME / X.509 signing tool for Git commits and tags.

This fork is maintained at droren/smimesign and adds Linux support beyond the original github/smimesign project, including PKCS#11 smart-card signing.

The current implementation supports:

  • Windows certificate store signing
  • macOS keychain signing
  • Linux PKCS#12 signing
  • Linux PKCS#11 smart-card signing
  • Git X.509 signing with gpg.format=x509
  • extraction of embedded signing certificates from Git commits

What To Use This For

Use smimesign when your organization issues X.509 certificates for user identity and you want Git commits and tags signed with those certificates instead of OpenPGP keys.

Typical setups:

  • Windows workstation with a user certificate in CurrentUser\My
  • Linux workstation with a YubiKey or CAC exposed through PKCS#11
  • Linux or macOS workstation using a PKCS#12 file

Repository Layout

  • main.go and command_*.go: main CLI
  • certstore/: OS-specific certificate store access
  • ietf-cms/: CMS / PKCS#7 signing and verification
  • cmd/git-x509-cert/: helper for extracting and displaying commit signing certs
  • Makefile: build and test targets

Installation

Windows

Build the binary:

git clone https://github.com/droren/smimesign.git
cd smimesign
go build -o smimesign.exe .

Put smimesign.exe somewhere on PATH, for example:

New-Item -ItemType Directory -Force $HOME\bin | Out-Null
Copy-Item .\smimesign.exe $HOME\bin\smimesign.exe
$env:Path = "$HOME\bin;$env:Path"

The Windows installer also ships offline documentation in the installed docs\ directory, including:

  • README.md
  • smimesign-help.txt
  • git-x509-cert-help.txt

smimesign on Windows reads identities from the Windows certificate store. The normal expectation is that the signing certificate is present in the current user's personal store:

  • Store: Current User
  • Logical store: Personal / My

To list what smimesign can currently use:

smimesign.exe --list-keys

If multiple certificates match the same Git identity, the current implementation prefers the most signing-oriented certificate automatically. If ambiguity still remains, set a persistent certificate fingerprint with SMIMESIGN_CERT_ID.

Example:

$env:SMIMESIGN_CERT_ID = "0x0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"

To persist it for future PowerShell sessions:

setx SMIMESIGN_CERT_ID 0x0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF

Linux

Build the binary:

git clone https://github.com/droren/smimesign.git
cd smimesign
go build -o smimesign .

Put it on PATH:

install -m 0755 ./smimesign ~/.local/bin/smimesign
export PATH="$HOME/.local/bin:$PATH"

Linux packages should also install offline documentation under /usr/share/doc/smimesign/, including the README, CLI help text, and the PKCS#11 wrapper example.

Linux supports two primary identity sources.

Linux With PKCS#12

export SMIMESIGN_P12=/path/to/user.p12
export SMIMESIGN_P12_PASSWORD='your-password'
smimesign --list-keys

Linux With PKCS#11 Smart Cards

Set the PKCS#11 module path:

export SMIMESIGN_PKCS11_MODULE=/usr/lib64/pkcs11/opensc-pkcs11.so
smimesign --list-smartcard-keys

If your token requires a PIN, you can either:

  • export SMIMESIGN_PKCS11_PIN for the current shell, or
  • use a wrapper script that prompts on /dev/tty and then execs smimesign
  • expect some tokens to require the PIN even for --list-smartcard-keys

An example wrapper is provided in examples/smimesign-pkcs11-wrapper.sh.

Minimal wrapper:

#!/usr/bin/env bash
set -euo pipefail
if [[ -z "${SMIMESIGN_PKCS11_PIN:-}" ]]; then
  printf 'YubiKey PIN: ' > /dev/tty
  IFS= read -r -s SMIMESIGN_PKCS11_PIN < /dev/tty
  printf '\n' > /dev/tty
  export SMIMESIGN_PKCS11_PIN
fi
exec /path/to/smimesign "$@"

When multiple certificates match the same identity, smimesign prefers a signing-capable certificate automatically. To pin one explicitly, set SMIMESIGN_CERT_ID:

export SMIMESIGN_CERT_ID=0x0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF

smimesign now prints and prefers SHA-256 certificate fingerprints by default. Exact legacy SHA-1 fingerprints are still accepted for backwards compatibility, and longer suffix matching is still supported for manual disambiguation.

Linux With Forwarded PKCS#11 Access

Some environments do not expose the smart card directly inside the shell or container where Git is running. In that case, smimesign itself still works fine, but it needs help from a wrapper that provides:

  • the forwarded PKCS#11 module path
  • any forwarding environment such as P11_KIT_SERVER_ADDRESS
  • the certificate fingerprint to prefer for signing
  • a /dev/tty PIN prompt when Git invokes signing non-interactively

Typical setup:

export P11_KIT_SERVER_ADDRESS=unix:path=/run/user/1000/p11-kit/pkcs11
export REMOTE_PKCS11_MODULE=/usr/lib64/pkcs11/p11-kit-client.so
pkcs11-tool --module "$REMOTE_PKCS11_MODULE" -L

If that shows the token, install the wrapper and point Git to it:

install -m 0755 examples/smimesign-pkcs11-wrapper.sh ~/.local/bin/smimesign
install -m 0755 ./smimesign ~/.local/bin/smimesign-real
git config --global gpg.x509.program "$HOME/.local/bin/smimesign"
git config --global gpg.format x509
git config --global commit.gpgsign true
git config --global tag.gpgSign true
git config --global log.showSignature true

Then customize the wrapper defaults:

  • set REAL_SMIMESIGN to the real binary path if different
  • set PKCS11_MODULE_DEFAULT or REMOTE_PKCS11_MODULE
  • set CERT_ID_DEFAULT or SMIMESIGN_CERT_ID to the signing certificate

Validation flow:

smimesign --list-smartcard-keys
printf "forwarded pkcs11 test\n" > payload.txt
smimesign --sign -u alex.example@example.invalid -b payload.txt > payload.sig
smimesign --verify payload.sig payload.txt
git commit -S -m "x509 signing test"

macOS

macOS uses the system keychain. Build and install like any other Go binary:

git clone https://github.com/droren/smimesign.git
cd smimesign
go build -o smimesign .

List available identities:

./smimesign --list-keys

Configure Git To Use X.509 Signing By Default

For Git 2.19 and newer, configure smimesign as the global X.509 signing program:

Windows

git config --global gpg.x509.program smimesign.exe
git config --global gpg.format x509
git config --global commit.gpgsign true
git config --global tag.gpgSign true
git config --global log.showSignature true
git config --global user.name "Alex Example"
git config --global user.email alex.example@example.invalid
git config --global user.signingkey alex.example@example.invalid

Linux and macOS

git config --global gpg.x509.program /absolute/path/to/smimesign
git config --global gpg.format x509
git config --global commit.gpgsign true
git config --global tag.gpgSign true
git config --global log.showSignature true
git config --global user.name "Alex Example"
git config --global user.email alex.example@example.invalid
git config --global user.signingkey alex.example@example.invalid

After configuration, validate with:

git commit -S -m "x509 signing test"
git log --show-signature -1

Choosing The Right Signing Certificate

When more than one certificate matches the same Git user identity, the current implementation does this:

  1. If SMIMESIGN_CERT_ID is set, that fingerprint wins.
  2. Otherwise, smimesign prefers the most signing-oriented certificate.
  3. If the choice is still ambiguous, signing fails with guidance to set SMIMESIGN_CERT_ID.

In practice, this means a certificate with KU=contentCommitment is preferred over a client-authentication certificate with EKU=clientAuth.

During verification, the defaults are intentionally stricter:

  • local signing identities are not treated as trust roots unless explicitly enabled with --trust-local-certs or SMIMESIGN_TRUST_LOCAL_CERTS=1
  • certificate usage is checked against a signing-oriented policy: emailProtection, codeSigning, Microsoft commercial code signing, and Microsoft document signing are accepted; plain client-auth certificates are not
  • if your environment needs broader compatibility than that policy, relax it with --allow-any-eku or SMIMESIGN_ALLOW_ANY_EKU=1
  • revocation checking can be enabled with --revocation-check=ocsp or SMIMESIGN_REVOCATION_CHECK=ocsp
  • if you want revocation problems to be warnings rather than hard failures, use --revocation-check=ocsp-soft or SMIMESIGN_REVOCATION_CHECK=ocsp-soft

For Git history inspection, ocsp-soft is often the better operational default. In many organizations, certificates are revoked when an employee leaves the company. That revocation is still useful information, but it should not make existing repository history unreadable or unusable. Otherwise, an ordinary shared repository would eventually become impossible to verify without rewriting history or splitting the project into new repositories every time a developer leaves. In that model:

  • the signature still proves who signed the commit at the time
  • revocation becomes an important warning about current trust state
  • historical code remains accessible and reviewable

Recommended practice:

  • set user.signingkey to the Git email address present in the certificate
  • set SMIMESIGN_CERT_ID if your environment has multiple matching certs

Common Commands

List identities from the OS store or PKCS#12:

smimesign --list-keys

List smart-card identities on Linux:

smimesign --list-smartcard-keys

Create and verify a detached signature:

smimesign --sign -u user@example.com -b file.txt > file.txt.sig
smimesign --verify file.txt.sig file.txt

Extract certificates from an existing CMS signature:

smimesign --dump-certs file.txt.sig > certs.pem

Validating The Certificate Used For A Git Commit

There are two supported ways to inspect the signing certificate embedded in a Git commit.

Option 1: Use The Cross-Platform Helper Tool

Build it once:

make build-tools

This creates build/tools/git-x509-cert.

Show the signer certificate from HEAD in human-readable form:

build/tools/git-x509-cert

Show a specific commit:

build/tools/git-x509-cert 6f274166f5db657127bfd29a07a49e46db03500d

Show all embedded certificates:

build/tools/git-x509-cert --all HEAD

Export the signer certificate as PEM:

build/tools/git-x509-cert --pem HEAD > signer.pem

The helper uses:

  • certutil -dump on Windows
  • openssl x509 -text -noout on Linux
  • openssl x509 -text -noout on macOS

If those tools are unavailable, it falls back to a built-in certificate summary plus PEM output.

This helper extracts and displays the certificate embedded in the commit signature. It does not by itself establish that the commit is trusted. Pair it with git log --show-signature or git verify-commit when you need both certificate inspection and signature verification.

Option 2: Extract PEM And Inspect It Yourself

Export the signing certificate from a commit:

build/tools/git-x509-cert --pem <commit-sha> > signer.pem

View it on Linux or macOS:

openssl x509 -in signer.pem -text -noout

View it on Windows:

certutil -dump .\signer.pem

You can also inspect the commit signature at a higher level with:

git log --show-signature -1 <commit-sha>

Trust Warnings

If the cryptographic signature is valid but the issuing CA is not trusted on the local machine, smimesign --verify reports:

  • a good signature
  • a trust warning
  • exit status 0

This is intentional. It distinguishes:

  • "the signature bytes are valid"
  • "this workstation trusts the issuing CA"

To preserve older local-only workflows, you can opt back into trusting local store identities as verification anchors:

export SMIMESIGN_TRUST_LOCAL_CERTS=1

Install Your CA To Resolve Unknown Authority Warnings

Windows

Import the issuing CA into the appropriate Windows trust store, typically Trusted Root Certification Authorities or Intermediate Certification Authorities depending on what you are installing.

Example:

certutil -addstore Root company-root-ca.cer
certutil -addstore CA company-issuing-ca.cer

Linux

RHEL / Fedora / CentOS:

sudo cp company-ca.pem /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust

Debian / Ubuntu:

sudo cp company-ca.pem /usr/local/share/ca-certificates/company-ca.crt
sudo update-ca-certificates

macOS

sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain company-root-ca.cer

After trust is installed, rerun:

git log --show-signature -1

Troubleshooting

Windows: "multiple identities match"

Current behavior:

  • smimesign tries to prefer the most signing-oriented certificate
  • if still ambiguous, it tells you to set SMIMESIGN_CERT_ID

Recommended fix:

smimesign.exe --list-keys
$env:SMIMESIGN_CERT_ID = "0xYOUR_FINGERPRINT"
git commit -S -m "retry"

Windows: Git cannot find smimesign.exe

Check:

Get-Command smimesign.exe
git config --global --get gpg.x509.program

Linux: No smart-card identities found

Check the PKCS#11 module and token visibility:

pkcs11-tool --module "$SMIMESIGN_PKCS11_MODULE" -L
smimesign --list-smartcard-keys

If the token is visible with pkcs11-tool but not in smimesign, verify the module path and any PKCS#11 forwarding environment required by your setup.

Linux: PIN entry problems

If Git signs non-interactively, use a wrapper script that prompts on /dev/tty and exports SMIMESIGN_PKCS11_PIN only for that process.

Verify shows "certificate signed by unknown authority"

The signature is valid, but your system does not trust the issuing CA yet. Install the relevant CA certificate into the system trust store and rerun the verification command.

Verify fails revocation checking

If you enable --revocation-check=ocsp, smimesign requires the signing certificate to expose a reachable OCSP responder and to return a good status. In restricted or air-gapped environments, leave revocation checking at none, or use ocsp-soft if you want revocation failures to be surfaced as warnings without making git log --show-signature fail.

This distinction matters for long-lived repositories. A revoked certificate may mean "do not trust this identity for new signing activity" without meaning "historical commits signed by this person must disappear from normal use." For that reason, ocsp-soft is the recommended mode for many repository browsing and review workflows, while strict ocsp remains available for fail-closed policies.

Timestamping over HTTP is rejected

By default --timestamp-authority must use https://. If you must use a plain HTTP TSA in a controlled environment, set:

export SMIMESIGN_ALLOW_HTTP_TSA=1

Git commit succeeds but git log --show-signature still looks wrong

Check:

git config --global --get gpg.format
git config --global --get gpg.x509.program
git config --global --get log.showSignature

Expected:

  • gpg.format = x509
  • gpg.x509.program = /absolute/path/to/smimesign or smimesign.exe
  • log.showSignature = true

If you depend on a wrapper for PKCS#11 forwarding or PIN prompting, use the absolute wrapper path rather than a bare command name. That avoids accidental fallback to a different smimesign binary found earlier on PATH.

Building

Build the main binary:

go build .

Build platform targets:

make build-linux
make build-windows
make build-darwin
make build-tools
make build-all

Run the security audit flow:

make audit-tools
make audit

make build-windows and make build-darwin require cgo cross-toolchains. The Makefile now checks explicitly for those tools and fails early with a clear message. Override the compiler commands if your environment uses different tool names:

make build-windows WINDOWS_CC_AMD64=x86_64-w64-mingw32-gcc
make build-darwin DARWIN_CC_AMD64=o64-clang DARWIN_CC_ARM64=oa64-clang

Tests

Run the minimal safe subset:

make test-min

Run the full suite:

make test-all

On macOS or other constrained environments, you may want:

export GODEBUG=x509usefallbackroots=1

Contributing

PKI environments vary widely. Contributions that improve compatibility with enterprise Windows, Linux PKCS#11, smart cards, and certificate-chain handling are welcome. See CONTRIBUTING.md.

Releases

Packages

Used by

Contributors

Languages