Skip to content

Code Signing

Engineer/DeveloperSecurity SpecialistDevOps

🔑 Key Takeaway: Sign every commit and tag with GPG (or SSH/SMIME), enforce signature verification in CI and branch protection, rotate keys regularly, and bind developer identity to hardware-backed MFA so that every change in the repository is traceable to a verified human.

Code signing guarantees integrity (the code has not been tampered with) and authenticity (the change came from the claimed author). Without signing, an attacker who obtains push access can inject malicious code that is indistinguishable from legitimate commits. In Web3 projects, where a single unverified commit could introduce a backdoor into smart contract deployment tooling or steal signing keys, the stakes are especially high.

Practical guidance

1. Require signed commits on protected branches

Enable GitHub's Require signed commits branch protection rule on main, develop, and any release branches. This rejects any push that does not carry a verifiable GPG, SSH, or S/MIME signature.

  • In GitHub: Settings > Branches > Branch protection rules > Require signed commits.
  • Verify in CI: add git log --verify-signatures or git merge --verify-signatures as a pipeline check so that unsigned merge commits also fail.

2. Require signed pull requests

Every PR must be reviewed by another core team member before merging. Configure GitHub to require at least one approving review and enforce that the PR author's commits are signed.

  • Branch protection: require "Signed commits" and "Pull request reviews" (at least 1 approving review, dismiss stale reviews on push).
  • Consider requiring reviews from specific teams (CODEOWNERS file) for sensitive paths such as deployment scripts, contract artifacts, or CI workflow files.

3. Enforce MFA for all repository members

Require Multi-Factor Authentication for every contributor with push access.

  • Organization-level: enable "Require two-factor authentication for members" in the GitHub organization settings.
  • Encourage hardware MFA (Yubikey, Titan) over SMS or TOTP. Hardware keys resist phishing via FIDO2/WebAuthn.
  • For Yubikey GPG signing: generate the GPG subkey directly on the Yubikey's OpenPGP applet so the private key never leaves the device.

4. Generate and manage GPG keys properly

Good key management is the foundation of code signing. A poorly managed key undermines the entire trust chain.

Generating a strong GPG key

Use RSA 4096 or Ed25519 (the latter is modern, fast, and secure):

# Ed25519 (recommended for modern setups)
gpg --full-generate-key
# Choose Ed25519 when prompted, or: gpg --quick-gen-key your@email.com ed25519 sign,auth cert never
 
# RSA 4096 (legacy compatibility)
gpg --full-generate-key
# Choose RSA 4096

Using subkeys for separation of duties

Create separate subkeys for signing and encryption. This lets you keep the master key completely offline while using subkeys daily:

# Add a signing subkey
gpg --edit-key YOUR_KEYID
gpg> addkey
# Choose: RSA 4096, Sign only, expiry 1-2 years
 
# Add an encryption subkey (separate from any encryption subkey you already have)
gpg> addkey
# Choose: RSA 4096, Encrypt only, expiry 1-2 years
 
gpg> save

Your master key stays on an encrypted USB or paper backup. The subkeys go on your regular machine. If a subkey is compromised, you revoke only the subkey — the identity stays intact.

YubiKey: move subkeys to hardware

Generate GPG keys directly on the YubiKey so the private key material never touches the host system:

# Initialize the YubiKey OpenPGP applet
gpg --card-edit
gpg> admin
gpg> generate
# Choose a touch policy (require touch for signing operations)
 
# Or move existing subkeys to the YubiKey:
gpg --edit-key YOUR_KEYID
gpg> key 1        # Select the signing subkey
gpg> keytocard    # Move it to YubiKey
gpg> key          # Deselect
gpg> key 2        # Select the encryption subkey
gpg> keytocard
gpg> save

The YubiKey now holds your private keys. The host machine can use them only when the YubiKey is physically present and unlocked.

Passphrase management

Protect GPG keys with a strong passphrase (20+ characters, random). Use gpg-agent caching to avoid re-entering it constantly:

# In ~/.gnupg/gpg-agent.conf:
pinentry-program /usr/bin/pinentry-tty
default-cache-ttl 86400      # Cache for 24 hours
max-cache-ttl 604800         # Expire after 1 week

5. Rotate GPG keys regularly

Key rotation limits the damage window if a key is compromised.

  • Define a rotation schedule: every 12 months for standard keys, every 6 months for high-privilege accounts (release managers, deployers).
  • When rotating: create a new key pair, publish the new public key to GitHub and your keyserver, add a signing subkey, update keyserver records, then revoke the old key with a reason of "superseded."
  • Maintain a key rotation log: key ID, creation date, expiry date, revocation date, reason.
  • Protect GPG private keys with a strong passphrase and store the revocation certificate in a secure, offline location (encrypted USB, password manager).

5b. Backup and recover keys safely

Without proper backup, a lost key means lost identity. Without secure storage, a stolen key means forged commits.

Backup the master key:
# Export to an encrypted file
gpg --export-secret-keys --armor YOUR_KEYID | \
  gpg --symmetric --cipher-algo AES256 \
  --output master-key-backup.gpg
 
# Store on: encrypted USB (LUKS), paper (print the ASCII armor and seal in a safe),
# or a password manager as an encrypted attachment
Generate and store revocation certificates immediately:
gpg --gen-revoke YOUR_KEYID > revocation-certificate.asc
# Store this certificate alongside the backup. If you lose access to the key,
# publishing the revocation certificate invalidates the compromised key.

Recovery test: Periodically verify you can decrypt using your backup without the original key. Store the backup passphrase separately from the backup medium.

6. Publish and verify public keys

A signature is meaningless if the verifying party cannot obtain the correct public key.

  • Upload your public key to GitHub (Settings > SSH and GPG keys) and to a public keyserver (keys.openpgp.org, keys.mailvelope.com).
  • Use the same key across all platforms so that the identity is consistent.
  • In CI, pin trusted public key fingerprints in the pipeline configuration. Reject signatures from unknown keys.

6b. Document code signing procedures

Maintain clear, accessible documentation so that every team member can set up and maintain signing correctly.

  • Onboarding guide: how to generate a GPG key, configure git to sign commits, upload to GitHub, and set up a Yubikey for signing.
  • Troubleshooting: common issues (expired keys, wrong key selected, gpg-agent not running) with solutions.
  • Policy: rotation schedule, revocation procedures, acceptable signing methods (GPG, SSH, S/MIME), and enforcement mechanism.

Why is it important

Unsigned commits allow impersonation. If an attacker obtains credentials or an active session, they can push commits that appear to come from any author. Without signature verification, there is no cryptographic proof of authorship.

Real-world implications:

  • The Linux kernel community experienced a breach where an attacker attempted to inject a backdoor via a seemingly legitimate commit. Signed commits and review processes are a primary defense against this class of attack.
  • NIST SP 800-53 Rev. 5 control AU-10 (Non-Repudiation) requires that the identity of individuals who perform specific actions be determined and verified.
  • CISA's Secure Software Development Self-Attestation form requires attestors to confirm that they verify the integrity of software releases, which includes code signing.

Implementation details

Sub-topicRelated page
Branch protection & signed commitsRepository Hardening
CI pipeline enforcement of signaturesSecuring CI/CD Pipelines
Artifact signing and provenanceSandboxing & Isolation

Common pitfalls

  • Lost or expired GPG key: If you lose your private key or it expires and you cannot revoke it, GitHub cannot verify your past or future commits. Always set an expiry date, generate a revocation certificate immediately, and store it securely offline.
  • gpg-agent caching causes signing with the wrong key: When you have multiple keys, git may sign with the wrong one. Explicitly set user.signingkey per repository: git config user.signingkey <FINGERPRINT>.
  • Signing tags but not commits: Annotated tags are signed, but if the commits they point to are unsigned, an attacker could rebase onto unsigned history. Sign both commits and tags.
  • Using SSH signing without understanding trust model: GitHub supports SSH signing keys, but verification depends on the SSH allowed_signers file. If this file is not maintained, signatures verify against any key in the file. Keep allowed_signers pinned to current team members.
  • CI merges bypassing signature checks: Some CI workflows auto-merge PRs (e.g., Dependabot). Ensure that bot accounts also sign commits or that the merge commit itself is verified by the CI system.

Quick-reference cheat sheet

ActionCommand
Generate GPG keygpg --full-generate-key (choose RSA 4096 or ed25519)
List secret keysgpg --list-secret-keys --keyid-format long
Set git signing keygit config user.signingkey <FINGERPRINT>
Sign a commitgit commit -S -m "message"
Sign a taggit tag -s v1.0.0 -m "release 1.0.0"
Verify a commitgit log --verify-signatures -1
Verify a taggit tag -v v1.0.0
Export public keygpg --armor --export <FINGERPRINT> > pubkey.asc
Generate revocation certgpg --gen-revoke <FINGERPRINT> > revoke.asc
Upload to keyservergpg --keyserver keys.openpgp.org --send-keys <FINGERPRINT>

References