Install GnuPG (OpenPGP)

#/usr/bin/env zsh

brew install gnupg

Or if you're on a linux machine without gpg:

#!/usr/bin/env zsh 

sudo apt-get install gnupg

Generate a PGP Key

#!/usr/bin/env zsh

gpg --full-generate-key

Prompt 1: Key Type

Select the Default Option i.e. (9) ECC (sign and encrypt)

➜  ~ gpg --full-generate-key
gpg (GnuPG) 2.4.4; Copyright (C) 2024 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Please select what kind of key you want:
   (1) RSA and RSA
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
   (9) ECC (sign and encrypt) *default*
  (10) ECC (sign only)
  (14) Existing key from card
Your selection?

Prompt 2: Elliptic curve

Select the Default Option i.e. (1) Curve 25519

Please select which elliptic curve you want:
   (1) Curve 25519 *default*
   (4) NIST P-384
   (6) Brainpool P-256
Your selection?

Prompt 3: Key Expiry

Pick a suitable expiry period.

Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years

Remaining Prompts

Fill in your user ID (name/email) and choose a strong passphrase for the key.

Listing Keys

#!/usr/bin/env zsh

gpg --list-secret-keys --keyid-format=long

GPG Agent Configuration

The GPG Agent should be up and running automatically. Check the GPG Agent Socket path using the following command:

#!/usr/bin/env zsh
# Sample output: $HOME/.gnupg/S.gpg-agent
gpgconf --list-dirs agent-socket

However, make sure to set GPG_TTY=$(tty) in your shell configuration as per the GPG documentation:

You should always add the following lines to your .bashrc or whatever initialization file is used for all shell invocations:
GPG_TTY=$(tty)
export GPG_TTY

In addition, you may want to optionally override the default cache TTLs:

#!/usr/bin/env zsh
mkdir -p "$HOME/.gnupg"
chmod 700 "$HOME/.gnupg"
# "$HOME/.gnupg/gpg-agent.conf"
# Note: See GPG Agent Configuration Options: https://www.gnupg.org/documentation/manuals/gnupg/Agent-Options.html
# --default-cache-ttl default is 600 seconds (10 minutes) | Override below =>  86400  = 1 Day
# --max-cache-ttl default is 7200 seconds (2 hours)       | Override below =>  604800 = 7 days
#
{
  echo "default-cache-ttl 86400"
  echo "max-cache-ttl 604800"
} >> "$HOME/.gnupg/gpg-agent.conf"
# Restart the agent:
gpgconf --kill gpg-agent

Configuring Git

#!/usr/bin/env zsh

# Configure git to use a particular GPG key
git config --global user.signingkey <key-id>

# Configure git to sign all tags + commits by default
git config --global commit.gpgsign true
git config --global tag.gpgSign true

Verifying a Commit

#!/usr/bin/env zsh

git log --show-signature

Sample output:

commit 78c95a95965d7d9ef816ed192396ba875b0962dc (HEAD -> main)
gpg: Signature made Wed Dec  3 01:10:46 2025 EST
gpg:                using EDDSA key AB244C5A61307221FAC18D84535C84F6751CC5C4
gpg: Good signature from "Muneeb Khawaja <[email protected]>" [ultimate]
Author: Muneeb Khawaja <[email protected]>

Extras

GPG Keys Uploaded to GitHub:

Any gpg public keys uploaded to GitHub will be available via the following URL https://github.com/<username>.gpg e.g. https://github.com/mtkhawaja.gpg

Upload GPG Pub Keys on Public Key Servers:

Digitally Signing Artifacts

#!/usr/bin/env zsh

# Different encodings for the same data:
# .asc ==> ascii armor format (by convention)
# .sig ==> binary  encoding (by convention)
gpg --armor --detach-sig "<doc-to-sign>"
# Or shortened
gpg -ab "<doc-to-sign>"

Using GPG Keys to Validate Artifact Authenticity

Sample Scenario: Validating a JAR

Suppose we're looking at the springboot-starter-web release artifacts.

There are a bunch of artifacts available for download. But let's just look at the JAR and related PGP signature:

#!/usr/bin/env zsh
# Download JAR
wget "https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-starter-web/4.0.0/spring-boot-starter-web-4.0.0.jar"
# Download PGP Signature in ASCII format
wget "https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-starter-web/4.0.0/spring-boot-starter-web-4.0.0.jar.asc"
# MD5 Checksum for JAR
wget "https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-starter-web/4.0.0/spring-boot-starter-web-4.0.0.jar.md5"
# SHA1 Checksum for JAR
wget "https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-starter-web/4.0.0/spring-boot-starter-web-4.0.0.jar.sha1"

After downloading the artifacts, we should have the following files:

Artifact Purpose Description
spring-boot-starter-web-4.0.0.jar Consumption The compiled Java library you’ll use as a dependency.
spring-boot-starter-web-4.0.0.jar.md5 Integrity Only MD5 checksum of the JAR File
spring-boot-starter-web-4.0.0.jar.asc Integrity and Authenticity PGP/GPG detached signature for the JAR. Used to verify authenticity and integrity using the developer’s public signing key.
spring-boot-starter-web-4.0.0.jar.sha1 Integrity Only SHA-1 checksum of the JAR File

Note: We don't verify the JAR integrity using the checksums in this article but you can verify these with shasum --check / md5sum --check

Importing Spring's PGP Key

From the spring website

Spring JARs released on Maven Central are signed with the following key.

Key ID: [email protected]

Fingerprint: 48B0 86A7 D843 CFA2 58E8 3286 928F BF39 003C 0425

Key size: RSA 4096

Date: 2023-01-16

You can import this key using a public key server:

$ gpg --keyserver keyserver.ubuntu.com --recv 48B086A7D843CFA258E83286928FBF39003C0425

You can also verify locally a manually downloaded key with:

$ gpg --import --import-options show-only spring.gpg

Note: When possible, you should avoid getting both the public key and the artifacts you’re validating from the exact same place, because a single compromised origin could silently serve you both a malicious key and malicious artifacts that all “verify” cleanly. Instead, treat key verification as an out-of-band process:

  1. Get the fingerprint (or the key itself) via one or more independent channels e.g., official documentation, the official project website, multiple key servers etc.
  2. Compare that fingerprint to the key you actually imported.

We'll go ahead and import the PGP key in from the public ubuntu key servers:

#!/usr/bin/env zsh

gpg --keyserver keyserver.ubuntu.com --recv 48B086A7D843CFA258E83286928FBF39003C0425

Validating JAR PGP Signature

And now we can validate the signature as follows:

#!/usr/bin/env zsh

gpg --verify spring-boot-starter-web-4.0.0.jar.asc spring-boot-starter-web-4.0.0.jar

If the signature is valid, we'll see the following output:

gpg: Signature made Thu Nov 20 12:41:49 2025 EST
gpg:                using RSA key 48B086A7D843CFA258E83286928FBF39003C0425
gpg: Good signature from "Spring Builds (JAR Signing) <[email protected]>" [unknown]
gpg:                 aka "Spring Builds (JAR Signing) <[email protected]>" [unknown]
gpg:                 aka "Spring Builds (JAR Signing) <[email protected]>" [unknown]
gpg:                 aka "Spring Builds (JAR Signing) <[email protected]>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: 48B0 86A7 D843 CFA2 58E8  3286 928F BF39 003C 0425

Trusting the Key (Optional)

Notice, however, that we're getting an ominous message in the output:

gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.

The warning above does not mean the signature is invalid or that the file is malicious. It just means GPG can’t vouch for the identity beyond 'this key signed this file.'

Trust and Validity are two different concepts in GPG:

  • Validity is GnuPG’s judgment about whether a key really belongs to the identity it claims (i.e., whether the key–user ID binding is authentic).
  • Trust (ownertrust) expresses how much you trust the key’s owner to correctly verify and sign other people’s keys. Highly trusted keys can influence GPG’s decision to mark additional keys as valid in the web-of-trust model.

We can trust the key by starting an interactive session to edit the key:

#!/usr/bin/env zsh

# This will start an interactive session to edit the key
gpg --edit-key "48B086A7D843CFA258E83286928FBF39003C0425"
# The next command will start a trust dialog
trust

After this, we should see the following prompt asking us our trust level. Refer to the gpg documentation for more details:

Please decide how far you trust this user to correctly verify other users' keys
(by looking at passports, checking fingerprints from different sources, etc.)

  1 = I don't know or won't say                                                                      
  2 = I do NOT trust
  3 = I trust marginally
  4 = I trust fully
  5 = I trust ultimately
  m = back to the main menu

Let's go with option 3, which, as per the documentation, implies:

The owner understands the implications of key signing and properly validates keys before signing them.

Now to certify the key as valid (who it belongs to), we need to sign the key with our own private key:

#!/usr/bin/env zsh
# The sign command will start a new dialog
# ⚠️ Only sign third-party vendor keys if you have verified the owner's identity using strong out-of-band channels. 
lsign

After typing sign we'll be asked: Really sign all user IDs? (y/N) and if we type y we'll see a list of the user IDs / keys we signed.

Then type in save to save our changes and exit the interactive session. If we run the gpg verification again, we should see the following output:

gpg: Signature made Thu Nov 20 12:41:49 2025 EST
gpg:                using RSA key 48B086A7D843CFA258E83286928FBF39003C0425
gpg: Good signature from "Spring Builds (JAR Signing) <[email protected]>" [full]
gpg:                 aka "Spring Builds (JAR Signing) <[email protected]>" [full]
gpg:                 aka "Spring Builds (JAR Signing) <[email protected]>" [full]
gpg:                 aka "Spring Builds (JAR Signing) <[email protected]>" [full]

Tampering with the Artifacts

If we tamper with the jar, e.g., let's say we append random chars at the end:

#!/usr/bin/env zsh
echo "random chars" >> "spring-boot-starter-web-4.0.0.jar"

And then run the verification again, we should see the following output:

gpg: Signature made Thu Nov 20 12:41:49 2025 EST
gpg:                using RSA key 48B086A7D843CFA258E83286928FBF39003C0425
gpg: BAD signature from "Spring Builds (JAR Signing) <[email protected]>" [full]

Backing up and Importing PGP Keys

Exporting GPG Pub Key

#!/usr/bin/env zsh

gpg --armor --export "<key id>"

Exporting GPG Private Key

#!/usr/bin/env zsh 

gpg  --armor --export-secret-key <key-id>

Exporting Revocation Certificate

#!/usr/bin/env zsh

gpg --output revoke.asc --gen-revoke "<key-id>"

Importing a PGP Key Pair from Backup

#!/usr/bin/env zsh

gpg --import <path/to/private/key/file>

# Or from STDIN

## Via Curl

curl <key-url> | gpg --import

## Clipboard (OSX)

pbpaste | gpg --import 

OSX Specific Configuration

pinentry-mac is a macOS-native passphrase prompt used by GPG. GnuPG depends on a secure pinentry program for collecting passwords and PINs, and the standard terminal-based versions often fail or behave inconsistently in macOS GUI environments. This tool provides a secure, reliable dialog for those prompts.

#!/usr/bin/env zsh

# Install and configure Pinentry  
# macOS must install `pinentry-mac`:

brew install pinentry-mac

echo "pinentry-program /opt/homebrew/bin/pinentry-mac" >> "$HOME/.gnupg/gpg-agent.conf"

# Restart the agent:
gpgconf --kill gpg-agent

References

Configuring GPG Keys for Commit Signing

A short guide to setting up GnuPG for Git signing and verifying downloaded artifacts. Covers creating ECC keys, configuring Git, checking Spring signatures, macOS pinentry setup, and safe key backup.