NixDaemon

NixOS pentest workstation as one flake — IceBreaker's successor
git clone https://git.daemon-sec.xyz/NixDaemon.git
Log | Files | Refs | README

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.