gpg.md (20443B)
1 # gpg — the key hierarchy, and every verb 2 3 GnuPG **2.4.9** here. Pinentry is the GNOME dialog (NixOS `programs.gnupg.agent`), `GPG_TTY` is exported by core.zsh, 4 and **`gpgx`** (`gpgx-cheat`) is the no-flags wrapper for the daily lock/unlock/sign/verify. This card is the real thing underneath. 5 Terminal card: **`gpg-cheat`** — sections `model setup keygen subkeys backup import sign verify encrypt decrypt edit revoke ssh git keyservers trust inspect offline agent fix mine`. 6 `FPR` below = a full 40-hex fingerprint (`gpg -K --with-colons | awk -F: '$1=="fpr"{print $10; exit}'`). 7 8 ## model — one primary key, several subkeys 9 10 ```text 11 primary key [C] certify = the identity. Signs your own subkeys and user IDs, and other people's keys. 12 Keep it OFFLINE once the subkeys exist; it only comes out to add/revoke subkeys or UIDs. 13 subkey [S] sign = signs files, commits, mail. Rotate yearly without touching the identity. 14 subkey [E] encrypt = what others encrypt TO. Rotating it does not re-encrypt old mail; keep old ones to read it. 15 subkey [A] authenticate = SSH login (gpg-agent as the ssh agent). 16 user ID "Name <mail>" = one per address; the primary one is what people see first. 17 ``` 18 19 - Capabilities show in `gpg -K` as `[SC]`, `[E]`, `[A]`. A primary with `[SC]` signs with the identity key itself, which is fine but not rotatable. 20 - The **fingerprint** (40 hex) is the key; the "key ID" is its last 16 (long) or 8 (short, collidable — never use). 21 - `sec` = secret primary present. `sec#` = primary absent (offline), only subkeys here. `ssb` = secret subkey. 22 - Ownertrust (how much you trust a key to vouch for others) is separate from validity (is this key really theirs). `[ultimate]` is your own. 23 24 ## setup — ~/.gnupg and the agent 25 26 ```sh 27 # ~/.gnupg/gpg.conf (create it; sane modern defaults) 28 keyid-format 0xlong 29 with-fingerprint 30 with-subkey-fingerprints 31 default-key FPR 32 default-recipient-self # `gpg -e file` encrypts to you when no -r given 33 personal-cipher-preferences AES256 AES192 AES 34 personal-digest-preferences SHA512 SHA384 SHA256 35 cert-digest-algo SHA512 36 no-emit-version 37 no-comments 38 keyserver hkps://keys.openpgp.org 39 auto-key-locate local,wkd # find a key by mail address (WKD) before asking a keyserver 40 trust-model tofu+pgp # remember first-seen keys per address, warn on change 41 42 # ~/.gnupg/gpg-agent.conf 43 default-cache-ttl 3600 # seconds a passphrase stays cached after last use 44 max-cache-ttl 28800 # hard ceiling 45 # pinentry-program is set by NixOS (modules/hosts/laptop/configuration.nix: pinentryPackage = pinentry-gnome3) 46 ``` 47 48 ```sh 49 chmod 700 ~/.gnupg # gpg refuses "unsafe permissions" otherwise 50 gpg-connect-agent reloadagent /bye # after editing gpg-agent.conf 51 gpgconf --kill gpg-agent # restart it entirely (it relaunches on demand) 52 gpg --version # algorithms available 53 ``` 54 55 ## keygen — a proper key, the modern way 56 57 Certify-only primary, then three subkeys. Ed25519/Curve25519 throughout (fast, small, the 2.4 default). 58 59 ```sh 60 gpg --quick-generate-key 'Your Name <you@example.com>' ed25519 cert 2y # primary: [C] only, 2-year expiry 61 FPR=$(gpg -K --with-colons 'you@example.com' | awk -F: '$1=="fpr"{print $10; exit}') 62 gpg --quick-add-key "$FPR" ed25519 sign 1y # [S] 63 gpg --quick-add-key "$FPR" cv25519 encr 1y # [E] 64 gpg --quick-add-key "$FPR" ed25519 auth 1y # [A] (for SSH) 65 gpg -K --with-subkey-fingerprints # see the four, check capabilities and expiries 66 ``` 67 68 - Expiry is a **safety net**, not a deadline: extend it any time with the primary key (see *edit*). A key that expires with its owner gone is self-cleaning. 69 - Interactive version: `gpg --full-generate-key --expert` → `(11) ECC (set your own capabilities)` → toggle `S` off so only `Certify` remains → `Curve 25519`. Then add subkeys with `--quick-add-key` or in `--edit-key` → `addkey`. 70 - A passphrase protects the secret key *file*; the agent caches it. Use a long one; the pinentry dialog asks. 71 - The revocation certificate is written automatically to `~/.gnupg/openpgp-revocs.d/FPR.rev` — back it up with the key (see *backup*). 72 73 ## subkeys — add, rotate, drop 74 75 ```sh 76 gpg --quick-add-key FPR ed25519 sign 1y # new signing subkey (old one stays until you revoke it) 77 gpg --quick-set-expire FPR 1y '*' # extend EVERY subkey a year (`'*'` = all subkeys; a sub-fpr = that one) 78 gpg --quick-set-expire FPR 2y # extend the primary 79 gpg --edit-key FPR # interactive: `key 2` selects subkey 2 (the `*` marks it), then 80 # `expire` / `revkey` / `delkey` / `passwd` / `save` 81 gpg -K --with-colons | awk -F: '$1=="ssb"{print $12, $5}' # capability letters + key id of each subkey 82 ``` 83 84 Old encryption subkeys: **keep them** (revoked or expired, still in the ring) or you lose the ability to read what was sent to them. 85 86 ## backup — export, revocation, paper 87 88 ```sh 89 gpg --armor --export FPR > mykey.pub.asc # public key: share freely 90 gpg --armor --export-secret-keys FPR > mykey.SECRET.asc # everything: primary + subkeys (passphrase-protected) 91 gpg --armor --export-secret-subkeys FPR > mykey.subkeys.asc # subkeys only (what a laptop should carry; see *offline*) 92 cp ~/.gnupg/openpgp-revocs.d/FPR.rev mykey.rev # the revocation certificate 93 gpg --export-ownertrust > ownertrust.txt # your trust assignments 94 gpg --gen-revoke FPR > mykey.rev # make a fresh revocation cert by hand 95 nix shell nixpkgs#paperkey -c sh -c 'gpg --export-secret-keys FPR | paperkey > mykey.paper.txt' # printable secret bits 96 ``` 97 98 Where it goes: the vault (encrypted), never the dotfiles repo. The SECRET export + the .rev are the two files that matter. 99 The secret export is still encrypted with your passphrase; the passphrase is the third thing to keep. 100 101 ## import — keys, trust, restore 102 103 ```sh 104 gpg --import mykey.pub.asc # someone's public key (or your own on a new machine) 105 gpg --import mykey.SECRET.asc # restore: asks for the passphrase 106 gpg --import-ownertrust < ownertrust.txt 107 gpg --edit-key FPR trust # set ownertrust: 5 = ultimate (yours), 4 = full, 3 = marginal 108 gpg --lsign-key FPR # "I checked this key": local signature, never exported 109 gpg --sign-key FPR # exportable certification (web of trust) 110 gpg --show-keys someone.asc # look at a key file WITHOUT importing it 111 gpg --delete-keys FPR # forget a public key; --delete-secret-keys for the secret part first 112 ``` 113 114 On a fresh machine: import the SECRET file, `gpg --edit-key FPR trust` → `5`, done. Or import only the subkeys file for a laptop. 115 116 ## sign — files, text, commits 117 118 ```sh 119 gpg --detach-sign --armor file # file.asc beside it: the usual way to sign a release/file 120 gpg --detach-sign file # binary file.sig 121 gpg --clearsign message.txt # message.txt.asc: readable text with a signature block (mail, announcements) 122 gpg --sign file # file.gpg: compressed file + signature in one (recipient runs gpg -d) 123 gpg -u FPR --detach-sign file # pick the key (-u / --local-user); `FPR!` = that exact subkey 124 echo "text" | gpg --clearsign # from a pipe 125 gpg --sign --encrypt -r mail file # sign AND encrypt (see *encrypt*) 126 ``` 127 128 Signing uses the `[S]` subkey automatically (or the primary if it has S). The agent asks for the passphrase once per cache TTL. 129 130 ## verify — did this come from them, unchanged 131 132 ```sh 133 gpg --verify file.asc file # detached signature (.asc/.sig) + the file 134 gpg --verify file.sig # gpg finds `file` next to it 135 gpg --verify message.txt.asc # clearsigned text 136 gpg --decrypt file.gpg > file # inline-signed: extracts the file and prints the verification 137 gpg --verify --verbose file.asc file # which key, which subkey, when 138 ``` 139 140 Reading the result: **Good signature from "Name <mail>"** = cryptographically valid. The warning 141 `This key is not certified with a trusted signature` means you have not set ownertrust / signed their key 142 — the signature is still valid; the question is whether the key is really theirs. Compare the fingerprint 143 out-of-band once, then `gpg --lsign-key FPR` and the warning goes away. 144 `BAD signature` = the file changed or the signature is for another file. Exit code 0 only on Good. 145 146 ## encrypt — to people, to yourself, with a passphrase 147 148 ```sh 149 gpg --encrypt --recipient mail@example.com --armor file # file.asc, readable only by that key 150 gpg -e -r mail@example.com -r you@example.com file # several recipients (add yourself to read it later!) 151 gpg -e file # to yourself (default-recipient-self in gpg.conf) 152 gpg -se -r mail file # sign + encrypt: they know it is from you 153 gpg --symmetric --cipher-algo AES256 file # passphrase only, no keys (file.gpg); shares via a channel you trust 154 gpg -c --armor file # same, armored 155 gpg -o out.gpg -e -r mail file # choose the output name 156 tar cz directory | gpg -e -r mail > directory.tgz.gpg # a whole directory, through a pipe 157 gpg --hidden-recipient mail -e file # do not reveal who it is for (-R) 158 gpg --encrypt-to FPR # in gpg.conf: ALWAYS add this recipient (yourself) silently 159 ``` 160 161 - `--armor` (`-a`) makes ASCII text you can paste into mail; without it the output is binary (smaller). 162 - Encrypt uses the recipient's `[E]` subkey. "No public key" = you have not imported theirs; "unusable public key" = theirs expired. 163 - Symmetric + a strong passphrase is fine for backups and for sending to someone with no key. 164 165 ## decrypt — and what to do when it fails 166 167 ```sh 168 gpg --decrypt file.gpg > file # -d: to stdout 169 gpg -o file -d file.gpg # to a named file 170 gpg file.gpg # guesses: decrypts (or verifies) and writes `file` 171 gpg --decrypt-files *.gpg # many at once, each to its name without .gpg 172 gpg -d file.gpg | tar xz # straight into tar 173 gpg --list-packets file.gpg # WHO can decrypt this: the key IDs it was encrypted to, the algorithms 174 ``` 175 176 "decryption failed: No secret key" = it was not encrypted to any key you hold (check with `--list-packets`; 177 compare with `gpg -K`). On a laptop with subkeys only, that is fine as long as the `[E]` subkey is there. 178 179 ## edit — identities, passphrase, expiry 180 181 ```sh 182 gpg --quick-add-uid FPR 'Your Name <you@example.com>' # add a user ID (fix a name, add an address) 183 gpg --quick-set-primary-uid FPR 'Your Name <you@example.com>' 184 gpg --quick-revoke-uid FPR 'Old Name <you@example.com>' # retire a UID (keys cannot delete published UIDs; revoke them) 185 gpg --change-passphrase FPR # new passphrase for the secret key 186 gpg --quick-set-expire FPR 2y # primary expiry; `0` = never 187 gpg --quick-set-expire FPR 1y '*' # all subkeys 188 gpg --edit-key FPR # the interactive editor; `help` lists everything: 189 # uid N / key N select · adduid deluid revuid primary · addkey delkey revkey expire · passwd · trust · save / quit 190 ``` 191 192 Every change to UIDs or subkeys needs the **primary** secret key present (`sec`, not `sec#`). Then re-export the public key: others must re-import it to see the change. 193 194 ## revoke — when a key is lost or compromised 195 196 ```sh 197 gpg --import mykey.rev # the stored revocation certificate: marks YOUR key revoked locally 198 gpg --keyserver hkps://keys.openpgp.org --send-keys FPR # publish the revocation so others see it 199 gpg --edit-key FPR → key 2 → revkey → save # revoke ONE subkey (compromised laptop: revoke its subkeys, keep the primary) 200 gpg --quick-revoke-uid FPR 'uid string' # revoke an identity 201 ``` 202 203 Revoked ≠ deleted: the key stays in the ring so old signatures still verify (as "made with a revoked key") and old mail still decrypts. 204 If the laptop is lost and you kept the primary offline: revoke its subkeys, make new ones, publish. The identity survives. 205 206 ## ssh — the [A] subkey as your SSH key 207 208 ```sh 209 # modules/hosts/laptop/configuration.nix: programs.gnupg.agent.enableSSHSupport = true; # gpg-agent becomes the ssh agent (SSH_AUTH_SOCK) 210 gpg -K --with-keygrip # the keygrip of the [A] subkey 211 echo KEYGRIP >> ~/.gnupg/sshcontrol # tell the agent to serve it 212 gpg --export-ssh-key FPR # the public key in authorized_keys format 213 gpg --export-ssh-key FPR >> ~/.ssh/authorized_keys_for_that_host # paste it where you log in 214 ssh-add -L # the agent now lists it 215 ``` 216 217 Your file key `~/.ssh/id_ed25519` keeps working next to it; ssh tries agent keys first. Use one or the other per host in `~/.ssh/config` (`IdentitiesOnly yes` + `IdentityFile`). 218 219 ## git — signed commits and tags 220 221 ```sh 222 git config --global gpg.format openpgp 223 git config --global user.signingkey FPR # or a subkey fpr with `!` to force it 224 git config --global commit.gpgsign true # every commit 225 git config --global tag.gpgSign true 226 git commit -S -m "msg" # one-off when gpgsign is off 227 git tag -s v1.0 -m "release" # signed tag; git tag -v v1.0 verifies 228 git log --show-signature -3 # see who signed what 229 git verify-commit HEAD 230 gpg --armor --export FPR | wl-copy # paste into GitHub/GitLab → Settings → GPG keys, so they show "Verified" 231 ``` 232 233 jj: `jj config set --user signing.behavior own` + `signing.backend "gpg"` + `signing.key FPR` signs the commits jj makes. 234 235 ## keyservers — publishing and finding keys 236 237 ```sh 238 gpg --keyserver hkps://keys.openpgp.org --send-keys FPR # publish; keys.openpgp.org mails you to verify the address before it shows the UID 239 gpg --keyserver hkps://keys.openpgp.org --recv-keys FPR # fetch by fingerprint (the only safe way to fetch) 240 gpg --locate-keys someone@example.com # WKD: their domain serves the key (auto-key-locate in gpg.conf) 241 gpg --search-keys 'name' # interactive search; verify the fingerprint with them before trusting 242 gpg --refresh-keys # pull revocations/expiry updates for every key you hold 243 ``` 244 245 ## trust — validity versus ownertrust 246 247 - A signature is **valid** when the key is valid. A key is valid when *you* certified it (`--sign-key` / `--lsign-key`) or enough trusted people did. 248 - **Ownertrust** (`--edit-key FPR trust`, 1-5) is how much you let that person's certifications count for *other* keys. Give 5 only to yourself. 249 - `trust-model tofu+pgp`: the first key seen for an address is remembered; a different one later triggers a warning. `gpg --tofu-policy good FPR` to accept a change. 250 - `gpg --check-signatures FPR` lists who certified a key. `gpg --update-trustdb` recomputes validity after trust changes. 251 252 ## inspect — what is this thing 253 254 ```sh 255 gpg -k # public keys (--list-keys); gpg -K = secret keys 256 gpg -k --with-subkey-fingerprints --with-keygrip FPR 257 gpg --fingerprint FPR # just the fingerprint, formatted for reading aloud 258 gpg --list-packets file.gpg # structure of any OpenPGP file: recipients, algorithms, signature dates 259 gpg --show-keys key.asc # describe a key file without importing 260 gpg -k --with-colons FPR # machine-readable (pub/sub/uid/fpr records) 261 gpg --card-status # a YubiKey/OpenPGP card, if one is plugged in 262 gpg --export FPR | gpg --list-packets | grep -A2 'signature packet' # certifications on your key 263 ``` 264 265 ## offline — primary key off the laptop 266 267 The point of subkeys: the laptop carries only `[S] [E] [A]`; the primary (the identity) lives on encrypted offline storage. 268 269 ```sh 270 gpg --armor --export-secret-keys FPR > mykey.SECRET.asc # 1. full backup first (vault, encrypted USB) 271 gpg --armor --export-secret-subkeys FPR > subkeys.asc # 2. the laptop's share 272 gpg --delete-secret-keys FPR # 3. remove everything secret here 273 gpg --import subkeys.asc # 4. put the subkeys back 274 gpg -K # shows `sec#` = primary absent, `ssb` present: correct 275 ``` 276 277 To add a subkey / extend expiry / sign someone's key later: import the SECRET backup in a temporary `GNUPGHOME`, do the change, export the public key and subkeys again, wipe the temp dir: 278 ```sh 279 export GNUPGHOME=$(mktemp -d); gpg --import mykey.SECRET.asc; gpg --quick-set-expire FPR 2y '*' 280 gpg --armor --export FPR > pub.asc; gpg --armor --export-secret-subkeys FPR > subkeys.asc; rm -rf "$GNUPGHOME"; unset GNUPGHOME 281 gpg --import pub.asc subkeys.asc # back in the normal ring 282 ``` 283 A YubiKey does the same with the subkeys moved onto the card: `gpg --edit-key FPR` → `key N` → `keytocard`. 284 285 ## agent — passphrase caching, pinentry 286 287 ```sh 288 gpg-connect-agent 'KEYINFO --list' /bye # which keys are cached right now (the `1` column) 289 gpg-connect-agent reloadagent /bye # re-read gpg-agent.conf 290 gpgconf --kill gpg-agent # forget every cached passphrase now 291 gpg --pinentry-mode loopback -d file.gpg # type the passphrase in the terminal instead of the dialog (what gpgx does) 292 echo test | gpg --clearsign >/dev/null # cheap way to pre-unlock the key for a batch of operations 293 ``` 294 295 `export GPG_TTY=$(tty)` must be set for terminal pinentries (core.zsh does it). The dialog here is pinentry-gnome3. 296 297 ## fix — the usual errors 298 299 - `Inappropriate ioctl for device` → `export GPG_TTY=$(tty)` (or the session has no tty: use `--pinentry-mode loopback`). 300 - `No secret key` on decrypt → not encrypted to you: `gpg --list-packets file.gpg` shows the key IDs it was made for. 301 - `unusable public key` / `unusable secret key` → expired or revoked (sub)key: `gpg -k FPR`, then `--quick-set-expire` (yours) or ask for a fresh key (theirs). 302 - `There is no assurance this key belongs to the named user` → ownertrust: `gpg --lsign-key FPR` after checking the fingerprint, or `--trust-model always` for one command. 303 - `gpg: WARNING: unsafe permissions on homedir` → `chmod 700 ~/.gnupg; chmod 600 ~/.gnupg/*`. 304 - Commands hang → a stale lock or a stuck daemon: `gpgconf --kill all`, `rm -f ~/.gnupg/*.lock ~/.gnupg/public-keys.d/*.lock`. 305 - `signing failed: No pinentry` → agent cannot reach a pinentry: `gpgconf --kill gpg-agent` and retry from a terminal with `GPG_TTY` set. 306 - Git: `error: gpg failed to sign the data` → almost always the two above; `GIT_TRACE=1 git commit` shows gpg's own message. 307 - Key shows `[expired]` on the other side after you extended it → they need your re-exported public key (or `--refresh-keys`). 308 309 ## mine — the key this repo signs with 310 311 - The laptop's identity (name, email, fingerprint) lives in one file, 312 `modules/hosts/laptop/_identity.nix`; git.nix and gpg.nix read it. On the 313 generic host there is no key configured: make one (*create*) and set it in 314 your own git config. 315 - No revocation certificate stored yet? `gpg --gen-revoke FPR > mykey.rev`, and keep it offline. 316 - Optional upgrades that keep the identity: an expiry (`--quick-set-expire`), a `[S]` signing subkey and an `[A]` 317 auth subkey (`--quick-add-key … rsa4096 sign|auth`), the primary offline (*offline*). 318 - Pinentry: GNOME dialog (modules/hosts/laptop/configuration.nix). `gpgx` uses loopback so the passphrase is typed in the terminal instead. 319 - Secrets in the NixDaemon repo are **age**, not gpg (`nix-cheat secrets`). 320 - `gpgx-cheat` is the wrapper's card; `gpg-cheat SECTION` prints one section of this one.