Skip to content

Verify a certificate against GnuPG

Before publishing a certificate, check it with the implementation your correspondents will actually use. A certificate that a Go parser accepts and gpg rejects is a certificate nobody can send you anything with.

This is the same sequence the package's own interoperability tests run, so a failure here is a real problem rather than a local quirk.

Use a throwaway keyring

Never import an untested certificate into your real keyring — a bad import is tedious to unpick.

export GNUPGHOME="$(mktemp -d)" && chmod 700 "$GNUPGHOME"

Everything below is scoped to that directory. When you are finished:

gpgconf --kill all && rm -rf "$GNUPGHOME"

Killing the agent matters: it holds the directory open, and removing the home underneath a running agent leaves a confusing mess.

1. Import it

gpg --import certificate.pgp

This is a stronger check than it looks. gpg validates self-signatures at import and silently drops anything that does not verify — so a certificate that imports with its subkey intact is one whose binding signature gpg checked and accepted.

2. Confirm the subkey survived

gpg --list-keys --with-colons | grep '^sub'

The twelfth colon-separated field is the capability set. It must contain e for encryption. If there is no sub line at all, the binding signature was rejected and nothing can be encrypted to this certificate.

3. Check the signatures explicitly

gpg --check-sigs --with-colons | grep '^sig'

The second field is ! for a signature that verified and - for one that did not. Expect at least two ! — the self-certification and the subkey binding — and no -.

4. Confirm your algorithm preferences took effect

This is the check most people skip, and the one that silently costs you security. The preferences live in the certification signature, so they can be read straight off the certificate:

gpg --list-packets certificate.pgp | grep -E 'pref-sym|pref-hash|features'

Expect:

hashed subpkt 11 len 3 (pref-sym-algos: 9 8 7)
hashed subpkt 21 len 3 (pref-hash-algos: 10 9 8)
hashed subpkt 30 len 1 (features: 01)

9 8 7 is AES-256, AES-192, AES-128 — strongest first. features: 01 is the request for integrity-protected encryption. If the pref-sym-algos line is missing, senders fall back to AES-128 and every message you receive will be weaker than your key allows, with no error to tell you.

Not --list-packets on the message. The chosen cipher is inside the encrypted session key, so gpg cannot print it without the private key — which is the whole point of this design. Running it against a message shows No secret key and no algorithm. To see what a sender actually chose, decrypt the message: SessionKey returns the algorithm identifier alongside the key.

5. Round-trip a real message

echo 'hello' | gpg --trust-model always --encrypt \
  --recipient you@example.com --output test.pgp

Then open test.pgp with your key service — the decrypt with AWS KMS guide has the code. Recovering the plaintext is the only end-to-end proof, because the KDF binds the certificate's fingerprint: assembly and decryption cannot be checked independently. See the fingerprint binding.

If gpg disagrees with Go

Believe gpg. It is what the other end of the exchange runs, and a certificate only has to satisfy the implementations that will actually read it.

Two independent Go implementations agreeing proves less than it appears to when one of them was written by reading the other — which is exactly why these tests exist alongside the differential ones.