GPG keys & commit signing
Uploading an OpenPGP public key lets shithub mark your signed commits and tags as Verified. Verification runs server-side against the bytes git stored in each commit object — there is no client-side trust ceremony.
1. Generate an OpenPGP key
Skip this if you already have a signing key (gpg --list-secret-keys --keyid-format=long
shows what you have).
gpg --full-generate-key
Pick ECC (sign and encrypt) and Curve 25519 when prompted —
ed25519 is the modern default and matches what gh recommends.
The key must carry at least one user ID with an email address
that you have verified on your shithub account, or the resulting
signatures will fall back to the unverified_email reason.
Encryption-only keys (e.g. a key generated for
--encryptwith no signing subkey) are accepted by shithub, but they can't verify commits. The REST response surfacescan_sign: falsehonestly when this is the case.
2. Export the public key
gpg --armor --export <KEY-ID-OR-EMAIL>
Copy the entire ASCII-armored block, including the
-----BEGIN PGP PUBLIC KEY BLOCK----- and END lines.
3. Add the key in shithub
Settings → SSH and GPG keys → "New GPG key". Paste the block, give it a label (e.g., "laptop"), save.
The page shows the primary fingerprint shithub parsed; verify it
matches gpg --fingerprint <KEY-ID> locally before relying on
the badge.
4. Tell git to sign
git config --global user.signingkey <KEY-ID>
git config --global commit.gpgsign true
git config --global tag.gpgsign true
Use the email on your shithub account as user.email — the
verification cross-check compares the signature's UID emails
against your account's verified emails.
5. Push and see the badge
After your next signed push, the commit list, the single-commit page, and the tag list all show a green Verified pill. Click it for signer + verified-at details.
What the badge states mean
| Pill | Reason | Meaning |
|---|---|---|
| Green "Verified" | valid |
Signature parsed, cryptographically checked against a registered key, signing email matches a verified email on the key. |
| Yellow "Unverified" | unknown_key |
Signature parsed but no uploaded key matches the signing subkey's fingerprint. |
| Yellow "Unverified" | unverified_email |
Signature is valid for an uploaded key, but the signing email isn't verified on that key's account. |
| Yellow "Unverified" | bad_email |
Signature is valid for an uploaded key, but the signing email isn't on the key at all. |
| Yellow "Unverified" | expired_key |
Signature is valid, but the key was expired at signing time. |
| Yellow "Unverified" | not_signing_key |
The key referenced isn't a signing key (capability bits missing). |
| Yellow "Unverified" | malformed_signature |
The signature block didn't parse. |
| Yellow "Unverified" | invalid |
Signature parsed but the cryptographic check failed. |
| no badge | unsigned |
Git stored no signature header. This is the default; we don't render anything. |
Retroactive verification
Uploading a key kicks off a background job that re-scans your existing commits across every repo and stamps the verification cache for the matches. Refresh the commit list a moment after upload — the badges appear without you doing anything.
Removing a key
Settings → SSH and GPG keys → "Delete" next to the GPG key row. The verification cache rows that resolved against the deleted key are invalidated; affected commits revert to no badge until another matching key is uploaded.
View source
| 1 | # GPG keys & commit signing |
| 2 | |
| 3 | Uploading an OpenPGP public key lets shithub mark your signed |
| 4 | commits and tags as **Verified**. Verification runs server-side |
| 5 | against the bytes git stored in each commit object — there is no |
| 6 | client-side trust ceremony. |
| 7 | |
| 8 | ## 1. Generate an OpenPGP key |
| 9 | |
| 10 | Skip this if you already have a signing key (`gpg --list-secret-keys --keyid-format=long` |
| 11 | shows what you have). |
| 12 | |
| 13 | ```sh |
| 14 | gpg --full-generate-key |
| 15 | ``` |
| 16 | |
| 17 | Pick `ECC (sign and encrypt)` and `Curve 25519` when prompted — |
| 18 | ed25519 is the modern default and matches what `gh` recommends. |
| 19 | The key must carry **at least one user ID** with an email address |
| 20 | that you have verified on your shithub account, or the resulting |
| 21 | signatures will fall back to the `unverified_email` reason. |
| 22 | |
| 23 | > Encryption-only keys (e.g. a key generated for `--encrypt` |
| 24 | > with no signing subkey) are accepted by shithub, but they can't |
| 25 | > verify commits. The REST response surfaces `can_sign: false` |
| 26 | > honestly when this is the case. |
| 27 | |
| 28 | ## 2. Export the public key |
| 29 | |
| 30 | ```sh |
| 31 | gpg --armor --export <KEY-ID-OR-EMAIL> |
| 32 | ``` |
| 33 | |
| 34 | Copy the entire ASCII-armored block, including the |
| 35 | `-----BEGIN PGP PUBLIC KEY BLOCK-----` and `END` lines. |
| 36 | |
| 37 | ## 3. Add the key in shithub |
| 38 | |
| 39 | Settings → **SSH and GPG keys** → "New GPG key". Paste the block, |
| 40 | give it a label (e.g., "laptop"), save. |
| 41 | |
| 42 | The page shows the primary fingerprint shithub parsed; verify it |
| 43 | matches `gpg --fingerprint <KEY-ID>` locally before relying on |
| 44 | the badge. |
| 45 | |
| 46 | ## 4. Tell git to sign |
| 47 | |
| 48 | ```sh |
| 49 | git config --global user.signingkey <KEY-ID> |
| 50 | git config --global commit.gpgsign true |
| 51 | git config --global tag.gpgsign true |
| 52 | ``` |
| 53 | |
| 54 | Use the email on your shithub account as `user.email` — the |
| 55 | verification cross-check compares the signature's UID emails |
| 56 | against your account's verified emails. |
| 57 | |
| 58 | ## 5. Push and see the badge |
| 59 | |
| 60 | After your next signed push, the commit list, the single-commit |
| 61 | page, and the tag list all show a green **Verified** pill. Click |
| 62 | it for signer + verified-at details. |
| 63 | |
| 64 | ## What the badge states mean |
| 65 | |
| 66 | | Pill | Reason | Meaning | |
| 67 | |------|--------|---------| |
| 68 | | Green "Verified" | `valid` | Signature parsed, cryptographically checked against a registered key, signing email matches a verified email on the key. | |
| 69 | | Yellow "Unverified" | `unknown_key` | Signature parsed but no uploaded key matches the signing subkey's fingerprint. | |
| 70 | | Yellow "Unverified" | `unverified_email` | Signature is valid for an uploaded key, but the signing email isn't verified on that key's account. | |
| 71 | | Yellow "Unverified" | `bad_email` | Signature is valid for an uploaded key, but the signing email isn't on the key at all. | |
| 72 | | Yellow "Unverified" | `expired_key` | Signature is valid, but the key was expired at signing time. | |
| 73 | | Yellow "Unverified" | `not_signing_key` | The key referenced isn't a signing key (capability bits missing). | |
| 74 | | Yellow "Unverified" | `malformed_signature` | The signature block didn't parse. | |
| 75 | | Yellow "Unverified" | `invalid` | Signature parsed but the cryptographic check failed. | |
| 76 | | _no badge_ | `unsigned` | Git stored no signature header. This is the default; we don't render anything. | |
| 77 | |
| 78 | ## Retroactive verification |
| 79 | |
| 80 | Uploading a key kicks off a background job that re-scans your |
| 81 | existing commits across every repo and stamps the verification |
| 82 | cache for the matches. Refresh the commit list a moment after |
| 83 | upload — the badges appear without you doing anything. |
| 84 | |
| 85 | ## Removing a key |
| 86 | |
| 87 | Settings → **SSH and GPG keys** → "Delete" next to the GPG key |
| 88 | row. The verification cache rows that resolved against the |
| 89 | deleted key are invalidated; affected commits revert to no |
| 90 | badge until another matching key is uploaded. |