Sign Git Commits

You can sign Git commits using GPG and Signum Linux Agent.

When you sign a commit, Git calls GPG to create a signature over the commit data. The signature is embedded in the commit object itself. Anyone with your public key can verify that the commit came from you and was not tampered with. The private key never leaves the Signum HSM at any point.

What is a Virtual GPG Key?

GPG uses its own key format (OpenPGP), while Signum stores keys as X.509 certificates inside an HSM. A virtual GPG key is the bridge between them:

  • The public key and metadata (name, email, expiry) are stored locally in ~/.gnupg

  • The private key entry is just a stub file that says "this key lives on smart card with serial X"

  • When GPG needs to sign something, it follows that stub → calls the smart card daemon → calls the PKCS#11 library → reaches Signum → the HSM performs the signing operation

You can tell the difference in gpg --list-secret-keys:

sec   rsa3072  ← private key stored on disk (no HSM)
sec>  rsa2048  ← private key is in the HSM — the > means "stub only"

Prerequisites


Step 1 - Install Dependencies

  1. Install the packages:

Bash
sudo apt update
sudo apt install -y \
  gnupg2 \
  pinentry-curses \
  opensc \
  build-essential \
  bzip2 \
  wget \
  devscripts \
  libgpg-error-dev \
  libgcrypt20-dev \
  libassuan-dev \
  libpkcs11-helper1-dev \
  pkg-config
  1. Build gnupg-pkcs11-scd 0.11.0 from source with a one-line patch in cmd_keyinfo:

The version of gnupg-pkcs11-scd available in Debian apt repositories is 0.10.0 and has a known bug. Version 0.11.0 partially fixes this but still requires a one-line patch in cmd_keyinfo.

cd /tmp
wget https://github.com/alonbl/gnupg-pkcs11-scd/releases/download/gnupg-pkcs11-scd-0.11.0/gnupg-pkcs11-scd-0.11.0.tar.bz2
tar -xjf gnupg-pkcs11-scd-0.11.0.tar.bz2
cd gnupg-pkcs11-scd-0.11.0
# Apply the patch
python3 << 'EOF'
with open('gnupg-pkcs11-scd/command.c', 'r') as f:
    content = f.read()
old = '\t\tif (error != GPG_ERR_NO_ERROR) {\n\t\t\tgoto cleanup;\n\t\t}\n\t}\n\n\terror = found ? GPG_ERR_NO_ERROR : GPG_ERR_NOT_FOUND;'
new = '\t\tif (error == GPG_ERR_WRONG_PUBKEY_ALGO) {\n\t\t\terror = GPG_ERR_NO_ERROR;\n\t\t}\n\t\tif (error != GPG_ERR_NO_ERROR) {\n\t\t\tgoto cleanup;\n\t\t}\n\t}\n\n\terror = found ? GPG_ERR_NO_ERROR : GPG_ERR_NOT_FOUND;'
if old in content:
    content = content.replace(old, new)
    with open('gnupg-pkcs11-scd/command.c', 'w') as f:
        f.write(content)
    print("Patch applied!")
else:
    print("Pattern not found!")
EOF
# Build and install
./configure && make && sudo make install
# Verify
/usr/local/bin/gnupg-pkcs11-scd --version

Expected output:

gnupg-pkcs11-scd 0.11.0

Step 2 - Verify Signum Agent and PKCS#11 Library

  1. Verify the Agent is authenticated and certificates are visible:

signum-util test
signum-util listcertificates
  1. Verify the PKCS#11 library is present and keys are accessible:

ls -la /usr/lib/libsignumpkcs11.so
pkcs11-tool --module /usr/lib/libsignumpkcs11.so --list-objects

Step 3 - Create GPG Configuration Files

  1. Create the GPG configuration files:

mkdir -p ~/.gnupg
chmod 700 ~/.gnupg
cat > ~/.gnupg/gpg-agent.conf << 'EOF'
verbose
debug-all
log-file /tmp/gpg-agent.log
scdaemon-program /usr/local/bin/gnupg-pkcs11-scd
pinentry-program /usr/bin/pinentry-curses
allow-loopback-pinentry
EOF
cat > ~/.gnupg/gnupg-pkcs11-scd.conf << 'EOF'
providers signum
provider-signum-library /usr/lib/libsignumpkcs11.so
EOF
chmod 600 ~/.gnupg/gpg-agent.conf
chmod 600 ~/.gnupg/gnupg-pkcs11-scd.conf
  1. Set GPG_TTY and start the smart card daemon:

GPG_TTY must always be set before signing.

export GPG_TTY=$(tty)
gpgconf --kill all
gpg --card-status

Expected result is the card detected with Application type: OpenPGP.

  1. Get the KEY-FRIENDLY hash for your certificate from Signum:

gpg-connect-agent << 'EOF'
SCD LEARN --force
SCD GETATTR KEYPAIRINFO
EOF

Note the KEY-FRIENDLY hash for the certificate you want to use:

S KEY-FRIENDLY 2588F2F67BBC82E872B15287621964636FB074D9 /CN=SignumCertificate on Signum for Linux

Save this hash for the following steps.


Step 4 - Map the Signing Key

Update gnupg-pkcs11-scd.conf with the KEY-FRIENDLY hash:

  1. Replace <KEY-FRIENDLY-HASH> with your actual hash value:

cat > ~/.gnupg/gnupg-pkcs11-scd.conf << 'EOF'
providers signum
provider-signum-library /usr/lib/libsignumpkcs11.so
openpgp-sign <KEY-FRIENDLY-HASH>
EOF
  1. Restart the daemon to apply the change:

gpgconf --kill all
gpg --card-status
  1. Confirm the Signature key field is now populated. It should show the keygrip instead of [none]:

Signature key ....: 2588 F2F6 7BBC 82E8 72B1  5287 6219 6463 6FB0 74D9

Step 5 - Create a Virtual GPG Key with Your GitLab/GitHub Email

The GPG key UID email must match your Git hosting account email for the "Verified" badge to appear on commits.

  1. Generate the virtual GPG key:

gpg --expert --full-generate-key
  1. When prompted, provide the following values:

Prompt

Value

Key type

13 (Existing key)

Keygrip

<KEY-FRIENDLY-HASH>

Capabilities

Q (keep defaults)

Expiry

0y

Real name

Your full name

Email address

Your GitLab or GitHub email

Confirm

O

PIN prompt

your Signum password

Expected output:

pub   rsa2048 2026-05-21 [SCE]
      BB07CE2D9778F6CED7D0F05A46C163CE62A3B77B
uid                      Your Name <your@email.com>
  1. Verify the key is linked to the Signum Card:

gpg --list-secret-keys --with-keygrip
  1. Confirm sec> (with >) that the private key is in the Signum HSM, not on disk:

sec>  rsa2048 2026-05-21 [SCE]
      BB07CE2D9778F6CED7D0F05A46C163CE62A3B77B
      Keygrip = 2588F2F67BBC82E872B15287621964636FB074D9
      Card serial no. = 3131 DC6624E9
uid           [ultimate] Your Name <your@email.com>
  1. Set this key as the GPG default. Replace <GPG_KEY_FINGERPRINT> with the full fingerprint
    from the output above:

echo "default-key <GPG_KEY_FINGERPRINT>" >> ~/.gnupg/gpg.conf
  1. Make GPG_TTY permanent across sessions:

echo 'export GPG_TTY=$(tty)' >> ~/.bashrc
source ~/.bashrc

Step 6 - Configure Git

  1. Configure Git to use your identity and the Signum GPG key:

git config --global user.name "<YOUR_FULL_NAME>"
git config --global user.email "<YOUR_GITLAB_OR_GITHUB_EMAIL>"
git config --global user.signingkey <GPG_KEY_FINGERPRINT>
git config --global commit.gpgsign true
git config --global gpg.program gpg2
  1. Verify the configuration:

git config --global --list | grep -E "user|gpg|sign"

Expected output:

user.name=Your full name
user.email=<your email>
user.signingkey=<GPG_KEY_FINGERPRINT>
commit.gpgsign=true
gpg.program=gpg2

Step 7 - Export Your Public Key

Export your public key to share with GitLab or GitHub so it can verify your
signed commits.

  1. Run the following command to export the public key:

gpg --armor --export <YOUR_GITLAB_OR_GITHUB_EMAIL>
  1. Copy the full output including the header and footer lines:

 -----BEGIN PGP PUBLIC KEY BLOCK-----
 ...
 -----END PGP PUBLIC KEY BLOCK-----

Step 8 - Upload Public Key to GitLab or GitHub

GitLab:

  1. Go to avatar (top right) → Edit profile → GPG Keys

  2. Paste the full public key block.

  3. Click Add key.

GitHub:

  1. Go to Settings → SSH and GPG keys → New GPG key

  2. Paste the full public key block.

  3. Click Add GPG key.


Step 9 - Make a Signed Commit

  1. Make sure GPG_TTY is set and the smart card daemon is running:

export GPG_TTY=$(tty)
gpgconf --kill all
gpg --card-status

If you get Card error, run gpgconf --kill all && gpg --card-status and try again.

  1. Make a signed commit (signing is automatic if commit.gpgsign=true is set):

git commit -m "your commit message"

To test the functionality without making changes, use the flag --allow-empty to create a commit:

git commit --allow-empty -m "your commit message"
  1. Verify the signature locally:

git log --show-signature -1

Expected output:

gpg: Signature made <DATE>
gpg:                using RSA key <GPG_KEY_FINGERPRINT>
gpg: Good signature from "<YOUR_FULL_NAME> <<YOUR_GITLAB_OR_GITHUB_EMAIL>>" [ultimate]
commit <COMMIT_HASH>
Author: <YOUR_FULL_NAME> <<YOUR_GITLAB_OR_GITHUB_EMAIL>>
Date:   <DATE>
    your commit message

On GitLab or GitHub, the commit shows a green Verified badge.


Troubleshooting

gpg --card-status Returns "No card" or Hangs

  • Confirm gnupg-pkcs11-scd is the patched 0.11.0 build:

    /usr/local/bin/gnupg-pkcs11-scd --version
    
  • Check the scdaemon-program path in gpg-agent.conf points to the correct binary.

  • Kill all GPG daemons and retry:

    gpgconf --kill all
    gpg --card-status
    
  • Review the agent log for errors:

    cat /tmp/gpg-agent.log
    

KEY-FRIENDLY Hash Not Appearing in SCD GETATTR KEYPAIRINFO

  • Confirm the Signum Agent is authenticated: signum-util test

  • Confirm the PKCS#11 library is accessible:

    pkcs11-tool --module /usr/lib/libsignumpkcs11.so --list-objects
    
  • If the token has mixed RSA + ECC certificates, confirm you applied the patch to the gnupg-pkcs11-scd build, or the loop stops before reaching RSA certificates.

gpg --full-generate-key Fails at Keygrip Prompt

  • The keygrip entered must match exactly the KEY-FRIENDLY hash.

  • Make sure you ran gpg --card-status after updating gnupg-pkcs11-scd.conf
    and that the Signature key field is populated (not [none]).

sec (Without >) After Key Generation

  • This means the private key was stored locally on disk, not linked to the HSM.

  • Delete the key (gpg --delete-secret-and-public-key <FINGERPRINT>) and repeat
    Step 5, confirming you selected key type 13 (Existing key) and entered the
    correct keygrip.

Commit Shows "Unverified" on GitLab or GitHub

  • The email in the GPG key UID must exactly match the email on your GitLab or
    GitHub account.

  • Confirm the public key has been uploaded and that GitLab or GitHub
    shows it as active.

  • Check that the key has not expired: gpg --list-keys