Differentiate GPG signature status for revoked and expired keys

What does this MR do and why?

Fixes incorrect GPG signature verification status when a signing (sub)key has been revoked or has expired.

Previously, GPG signatures made with revoked or expired keys were mapped to the generic unverified status. This is misleading because the signature itself is cryptographically valid — only the key status has changed after signing. GPGME provides specific error codes (GPG_ERR_CERT_REVOKED and GPG_ERR_KEY_EXPIRED) that distinguish these cases from a truly bad signature, but GitLab was not using them.

This MR replaces the single GPGME::Signature#valid? check with a cascade that also inspects #revoked_key? and #expired_key? to return the appropriate verification status:

  • revoked_key — already existed as an enum value (used for SSH signatures) but was never set for GPG signatures
  • expired_key — new enum value added by this MR

Both code paths are updated:

  • Gitlab::Gpg::Commit#verification_status (legacy path)
  • Gitlab::Gpg::Signature#calculate_verification_status (feature-flagged path behind gpg_commit_delegate_to_signature)

Closes #255279 (closed)

References

Screenshots or screen recordings

Screenshots to be added.

Before After
GPG_Unverified image

How to set up and validate locally

Prerequisites

  • A working GDK installation
  • GnuPG installed on your local machine (not inside the GDK container)
  • A test project in the GDK (e.g. root/gpg-test)

1. Create a GPG key with a signing subkey

On your local machine:

# Generate a primary key
gpg --full-generate-key
# Choose: (1) RSA and RSA, 3072 bits, no expiration

# Add a signing subkey
gpg --edit-key <YOUR_EMAIL>
# gpg> addkey -> (4) RSA (sign only), 3072 bits, no expiration
# gpg> save

2. Upload the public key to GitLab

gpg --armor --export <YOUR_EMAIL>

Copy the output and paste it at http://gdk.local:3000/-/user_settings/gpg_keys.

Make sure the email on the GPG key matches a verified email on your GitLab user account.

3. Create a signed commit

git clone http://gdk.local:3000/root/gpg-test.git
cd gpg-test
git config --local user.email "<YOUR_EMAIL>"
git config --local user.name "<YOUR_NAME>"
git config --local user.signingkey <KEY_ID>

echo "test" >> test.txt
git add test.txt
git commit -S -m "Signed test commit"
git push

Verify in the GDK UI that the commit shows a green "Verified" badge.

4. Revoke the signing subkey

gpg --edit-key <YOUR_EMAIL>
# gpg> list          (note the index of the [S] subkey)
# gpg> key <N>       (select the signing subkey)
# gpg> revkey        (choose reason, e.g. "No reason specified")
# gpg> save

5. Re-upload the key with revocation info

  1. Go to http://gdk.local:3000/-/user_settings/gpg_keys and delete the existing key.
  2. Export and re-upload:
gpg --armor --export <YOUR_EMAIL>
  1. Paste the output and click Add key.

6. Verify the fix

Open the commit from step 3 in the GDK UI and click the signature badge.

  • Before this MR: Generic tooltip: "This commit was signed with an unverified signature."
  • After this MR: Specific tooltip: "This commit was signed with a key that was revoked."

You can also confirm via Rails console:

sig = CommitSignatures::GpgSignature.find_by(commit_sha: '<COMMIT_SHA>')
sig.verification_status
# Expected: "revoked_key" (was "unverified" before)

7. Test expired key (optional)

gpg --edit-key <YOUR_EMAIL>
# gpg> addkey -> (4) RSA (sign only), 3072 bits, expires in 1 minute
# gpg> save

echo "test2" >> test.txt
git add test.txt
git commit -S -m "Signed with expiring key"
git push

Wait for the key to expire, then delete and re-upload the key. The commit should show "This commit was signed with a key that has expired."

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

  • Tests added (RSpec + Jest) for revoked and expired key scenarios
  • All existing tests pass (134 RSpec, 91 Jest)
  • Rubocop, Prettier, ESLint — no offenses
  • Changelog trailer included (Changelog: fixed)
  • No database migration required (enum stored as integer)
Edited by Norman Debald

Merge request reports

Loading
Loading