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 signaturesexpired_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 behindgpg_commit_delegate_to_signature)
Closes #255279 (closed)
References
- GPGME Ruby signature API: https://rubydoc.info/gems/gpgme/GPGME/Signature
git log --format='%G?'status codes:man git-log(search for%G?)
Screenshots or screen recordings
Screenshots to be added.
| Before | After |
|---|---|
![]() |
![]() |
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> save2. 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 pushVerify 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> save5. Re-upload the key with revocation info
- Go to
http://gdk.local:3000/-/user_settings/gpg_keysand delete the existing key. - Export and re-upload:
gpg --armor --export <YOUR_EMAIL>- 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 pushWait 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)

