# GnuPG

## Table of contents <!-- omit in toc -->

1. [TL;DR](#tldr)
1. [Encryption](#encryption)
1. [Decryption](#decryption)
1. [Key export](#key-export)
1. [Key import](#key-import)
1. [Key trust](#key-trust)
1. [Unattended key generation](#unattended-key-generation)
1. [Change a key's password](#change-a-keys-password)
1. [Put comments in a message or file](#put-comments-in-a-message-or-file)
1. [Use GPG keys for SSH authentication](#use-gpg-keys-for-ssh-authentication)
    1. [Create authentication-capable keys or subkeys](#create-authentication-capable-keys-or-subkeys)
    1. [Enable SSH to use GPG (sub)keys](#enable-ssh-to-use-gpg-subkeys)
    1. [Share GPG-SSH keys](#share-gpg-ssh-keys)
1. [Troubleshooting](#troubleshooting)
    1. [`gpg failed to sign the data; fatal: failed to write commit object`](#gpg-failed-to-sign-the-data-fatal-failed-to-write-commit-object)
    1. [`sign_and_send_pubkey: signing failed for … from agent: agent refused operation`](#sign_and_send_pubkey-signing-failed-for--from-agent-agent-refused-operation)
    1. [New configuration settings are ineffective](#new-configuration-settings-are-ineffective)
1. [Further readings](#further-readings)
1. [Sources](#sources)

## TL;DR

```sh
# Install on Mac OS X.
# Choose one.
brew install --cask 'gpg-suite-no-mail'
brew install 'gnupg' 'pinentry-mac'

# Launch components.
gpgconf --launch
gpgconf --launch 'gpg-agent'

# Reload components.
gpgconf -R
gpgconf --reload 'gpg-agent'

# Kill components.
gpgconf -K 'all'
gpgconf --kill 'gpg-agent'

# FIXME
gpg-connect-agent updatestartuptty /bye
gpg-connect-agent reloadagent '/bye'


# List existing keys.
gpg -k
gpg --list-keys --keyid-format 'short'
gpg -K --with-subkey-fingerprint
gpg --list-secret-keys --with-keygrip --keyid-format '0xlong'

# Generate new keys.
gpg --gen-key
gpg --generate-key
gpg --full-generate-key
gpg --expert --full-generate-key

# Generate new key in an unattended way.
# The non-interactive (--batch) option requires a settings file.
gpg --generate-key --batch 'setting.txt'
gpg --generate-key --batch <<-EOF
…
EOF

# Import keys from files.
gpg --import 'keys.asc'

# Export keys to files.
gpg -a --export > 'all.public-keys.asc'
gpg --armor --export -o 'given.public-key.asc' 'key_fingerprint'
gpg -a --export-secret-keys --output 'all.private-keys.asc'
gpg -a --export-secret-subkeys 'subkey_fingerprint'! > 'given.private-subkey.asc'

# Delete keys from the keyring.
# The non-interactive (--batch) option requires the key fingerprint.
gpg --delete-secret-key 'recipient'
gpg --delete-key 'recipient'
gpg --delete-keys --batch 'key_fingerprint'

# Get keys' fingerprint information.
gpg --fingerprint
gpg --fingerprint 'recipient'

# Change keys' expiration date.
# Use '0', 'never' or 'none' as expiration period to disable expiration.
# Use '*' as subkey fingerprint to set the expiration date of all non-revoked
# subkeys.
gpg --quick-set-expire 'key_fingerprint' '0'
gpg --quick-set-expire 'key_fingerprint' '2085-11-24'
gpg --quick-set-expire 'key_fingerprint' '20241101T203012' 'subkey_fingerprint'
gpg --quick-set-expire 'key_fingerprint' '1y' '*'

# Generate revoking certificates.
# To actually revoke the key, merge it with the certificate using '--import'.
# Use the '--edit' command to only revoke a subkey or a key signature.
gpg --gen-revoke
gpg --generate-revocation -ao 'revoke.cert' 'fingerprint'

# Change keys' passphrase.
# Use '--dry-run' to just check the current password is correct.
gpg --passwd 'fingerprint'
gpg --change-passphrase --dry-run 'recipient'


# Encrypt files *a*symmetrically.
gpg -o 'file.out.gpg' -r 'recipient' -e 'file.in'
gpg -o 'file.out.gpg' -u 'sender' -r 'recipient' --encrypt 'file.in'
gpg --encrypt-files --batch -r 'recipient' 'file.in.1' 'file.in.N'
gpg -e --multifile --batch -r 'recipient' --yes 'file.in.1' 'file.in.N'

# Encrypt files *symmetrically*.
# Simply encrypts data with a passphrase.
gpg -c 'input.file'
gpg --symmetric --s2k-cipher-algo 'AES256' --s2k-digest-algo 'SHA512' \
  --s2k-count '65536' 'input.file'

# Encrypt files for multiple recipients.
gpg -e … -r 'recipient_1' -r 'key_fingerprint' -r 'recipient_N'

# Decrypt files.
gpg -d 'file.gpg'
gpg -o 'file.out' --decrypt 'file.in.gpg'
gpg --decrypt-files --batch 'file.in.gpg.1' 'file.in.gpg.N'
gpg -d --multifile --batch --yes 'file.in.gpg.1' 'file.in.gpg.N'

# Encrypt directories.
gpgtar -o 'dir.tar.gpg' -c 'input/dir'

# Decrypt directories.
gpgtar -d 'dir.tar.gpg'
gpgtar -o 'dir' -d 'dir.tar.gpg'


# Get the short ID of the signing key only for a user.
# Primarily usable for git's signingKey configuration.
gpg --list-keys --keyid-format 'short' 'recipient' \
| grep --extended-regexp '^pub[[:blank:]]+[[:alnum:]]+/[[:alnum:]]+[[:blank:]].*\[[[:upper:]]*S[[:upper:]]*\]' \
| awk '{print $2}' \
| cut -d '/' -f 2


# Integrate with the SSH agent.
export SSH_AUTH_SOCK="$(gpgconf --list-dirs 'agent-ssh-socket')" && \
gpgconf --launch 'gpg-agent'

# Export keys in OpenSSH format.
gpg --export-ssh-key 'key_identifier'
gpg --export-ssh-key 'ed25519_key' > ~'/.ssh/id_ed25519.pub'


# Integrate with Pinentry.
export GPG_TTY="$(tty)"
```

## Encryption

```sh
# Single file.
gpg --output 'file.out.gpg' --encrypt --recipient 'recipient' 'file.in'
gpg --armor --symmetric --output 'file.out.gpg' 'file.in'

# All files found.
find . -type 'f' -name 'secret.txt' \
  -exec gpg --batch --yes --encrypt-files --recipient 'recipient' {} ';'
```

## Decryption

```sh
# Single file.
gpg -d 'file.gpg'
gpg --output 'file.out' --decrypt 'file.in.gpg'

# All files found.
find . -type f -name "*.gpg" -exec gpg --decrypt-files {} +
```

The second command will create the decrypted version of all files in the same directory. Each file will have the same
name of the encrypted version, minus the `.gpg` extension.

## Key export

As the original user, export all public keys to a base64-encoded text file and create an encrypted version of that file:

```sh
# Export.
gpg --armor --export > 'all.public-keys.asc'
gpg --armor --export 'recipient' > 'recipient.public-keys.asc'

# Encryption.
gpg --output 'file.out.gpg' --encrypt --recipient 'recipient' 'file.in'
gpg --armor --symmetric --output 'file.out.gpg' 'file.in'
```

Export all encrypted private keys (which will also include corresponding public keys) to a text file and create an
encrypted version of that file:

```sh
# Export.
gpg --armor --export-secret-keys > 'all.private-keys.asc'
gpg --armor --export-secret-keys 'recipient' > 'recipient.private-keys.asc'

# Encryption.
gpg --output 'file.out.gpg' --encrypt --recipient 'recipient' 'file.in'
gpg --armor --symmetric --output 'file.out.gpg' 'file.in'
```

Optionally, also export `gpg`'s trustdb to a text file:

```sh
gpg --export-ownertrust > 'otrust.txt'
```

## Key import

As the new user execute `gpg --import` commands against the secured files, or the decrypted content of those files, and
then check for the new keys with `gpg -k` and `gpg -K`, e.g.:

```sh
gpg --output 'myprivatekeys.asc' --decrypt 'mysecretatedprivatekeys.sec.asc' && \
gpg --import 'myprivatekeys.asc'
gpg --output 'mypubkeys.asc' --decrypt 'mysecretatedpubkeys.sec.asc'
gpg --import 'mypubkeys.asc'
gpg --list-secret-keys
gpg --list-keys
```

Optionally import the trustdb file as well:

```sh
gpg --import-ownertrust 'otrust.txt'
```

## Key trust

```sh
$ gpg --edit-key 'key_fingerprint'
gpg> trust
gpg> quit
```

## Unattended key generation

> The non-interactive (--batch) option requires a settings file.

```sh
# basic key with default values
gpg --batch --generate-key <<EOF
    %echo Generating a default key
    Key-Type: default
    Subkey-Type: default
    Name-Real: Joe Tester
    Name-Comment: with stupid passphrase
    Name-Email: joe@foo.bar
    Expire-Date: 0
    Passphrase: abc
    # Do a commit here, so that we can later print "done" :-)
    %commit
    %echo done
EOF
```

## Change a key's password

```sh
$ gpg --edit-key 'key_fingerprint'
gpg> passwd
gpg> quit
```

## Put comments in a message or file

One can put comments in an armored ASCII message or key block using the `Comment` keyword for each line:

```txt
-----BEGIN PGP MESSAGE-----
Comment: …
Comment: …

hQIMAwbYc…
-----END PGP MESSAGE-----
```

OpenPGP defines all text to be in UTF-8, so a comment may be any UTF-8 string.<br/>
The whole point of armoring, however, is to provide seven-bit-clean data, so if a comment has characters that are
outside the US-ASCII range of UTF they may very well not survive transport.

## Use GPG keys for SSH authentication

> See also [How to enable SSH access using a GPG key for authentication].

This exercise will use a GPG subkey with only the authentication capability enabled to complete SSH connections.<br/>
You can create multiple subkeys as you would do for SSH key pairs.

### Create authentication-capable keys or subkeys

To create subkeys, you should already have a GPG key. If you don't, read one of the many fine tutorials available on
this topic.<br/>
Create the subkey by editing your existing key **in expert mode** to get access to the appropriate options:

```sh
$ gpg --expert --edit-key 'key_fingerprint'
gpg> addkey
Please select what kind of key you want:
   (3) DSA (sign only)
   (4) RSA (sign only)
   (5) Elgamal (encrypt only)
   (6) RSA (encrypt only)
   (7) DSA (set your own capabilities)
   (8) RSA (set your own capabilities)
  (10) ECC (sign only)
  (11) ECC (set your own capabilities)
  (12) ECC (encrypt only)
  (13) Existing key
Your selection? 8

Possible actions for a RSA key: Sign Encrypt Authenticate
Current allowed actions: Sign Encrypt

   (S) Toggle the sign capability
   (E) Toggle the encrypt capability
   (A) Toggle the authenticate capability
   (Q) Finished

Your selection? s
Your selection? e
Your selection? a

Possible actions for a RSA key: Sign Encrypt Authenticate
Current allowed actions: Authenticate

   (S) Toggle the sign capability
   (E) Toggle the encrypt capability
   (A) Toggle the authenticate capability
   (Q) Finished

Your selection? q
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (4096)
Requested keysize is 4096 bits
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0)
Key does not expire at all
Is this correct? (y/N) y
Really create? (y/N) y

sec  rsa2048/8715AF32191DB135
     created: 2019-03-21  expires: 2021-03-20  usage: SC
     trust: ultimate      validity: ultimate
ssb  rsa2048/150F16909B9AA603
     created: 2019-03-21  expires: 2021-03-20  usage: E
ssb  rsa4096/17E7403F18CB1123
     created: 2019-03-21  expires: never       usage: A
[ultimate] (1). Johnny B. Good

gpg> quit
Save changes? (y/N) y
```

### Enable SSH to use GPG (sub)keys

When using SSH, `ssh-agent` is used to manage SSH keys. When using a GPG key, `gpg-agent` is used to manage GPG
keys.<br/>
To get `gpg-agent` to handle requests from SSH, you need to enable its SSH support:

```sh
echo "enable-ssh-support" >> ~/.gnupg/gpg-agent.conf
```

You can avoid using `ssh-add` to load the keys by preemptively specifying which GPG keys to use in the
`~/.gnupg/sshcontrol` file.<br/>
Entries in this file need to be keygrips (internal identifiers that `gpg-agent` uses to refer to the keys). A keygrip
refers to both the public and private key.<br/>
Find the keygrips you need, then add them to the `~/.gnupg/sshcontrol` file:

```sh
$ gpg -K --with-keygrip
/home/jbgood/.gnupg/pubring.kbx
-----------------------------
sec   rsa4096 2017-11-13 [SC] [expires: 2025-11-12]
      7425E11D898C449FDD3D1B4A7E747A8618CB109F
      Keygrip = E045864F555B3432E6DFCA2EF7ED47403A6E399C
uid           [ultimate] Johnny B. Good <j.b.good@email.com>
uid           [ultimate] Johnny <johnny@mymail.com>
ssb   rsa4096 2018-01-03 [E]
      Keygrip = B4674D4429AE049663BE4FEF9407C6B90F7AF122
ssb   rsa4096 2018-05-04 [A]
      Keygrip = B3D630644D14A452502A84FB09E5257CF54C3E04

sec   rsa4096 2022-01-03 [SCEA] [expires: 2025-01-02]
      8B6DC0BF4D73373C2A529C65CC54F9AC6E542DE7
      Keygrip = 03CE1FCE255AC0BB747BBBF61C9B8378CF78A2FC
uid           [ unknown] Luna Varasi <luna.var@example.com>

$ echo 'B3D630644D14A452502A84FB09E5257CF54C3E04' >> ~'/.gnupg/sshcontrol'
$ echo '03CE1FCE255AC0BB747BBBF61C9B8378CF78A2FC' >> ~'/.gnupg/sshcontrol'
```

Now tell SSH how to access `gpg-agent` by setting the value of the `SSH_AUTH_SOCK` environment variable.<br/>
Alternatively, and for a more permanent solution, set the option in the `.ssh/config` file:

```sh
export SSH_AUTH_SOCK=$(gpgconf --list-dirs 'agent-ssh-socket')

# alternative
echo "\nIdentityAgent $(gpgconf --list-dirs 'agent-ssh-socket')" \
| tee -a ~'/.ssh/config'
```

Now you can launch the gpg agent:

```sh
gpgconf --launch gpg-agent
```

Check the key has been imported correctly:

```sh
$ gpg --export-ssh-key 'Johnny B. Good'
ssh-rsa AAAAB3NzaC…7SD8UQ== openpgp:0x7BB65DA2
$ ssh-add -L
ssh-rsa AAAAB3NzaC…7SD8UQ== (none)
```

### Share GPG-SSH keys

Run `ssh-add -L` to list one's public keys, then copy them over manually to the remote host.<br/>
Alternatively, use `ssh-copy-id` as one would normally do.

## Troubleshooting

### `gpg failed to sign the data; fatal: failed to write commit object`

**Context:**

- `git` is instructed to sign a commit with `gpg`.
- `git commit` fails with the following error:

  > ```txt
  > gpg failed to sign the data
  > fatal: failed to write commit object
  > ```

- Pinentry should have prompted to input the key's passphrase, but the prompt did **not** appear.

**Cause:**

The environment variable `GPG_TTY` was not set and Pinentry could not reach the terminal session to prompt for the key's
passphrase.

**Solution:**

Make sure the environment variable `GPG_TTY` is set to the current TTY:

```sh
export GPG_TTY=$(tty)
```

### `sign_and_send_pubkey: signing failed for … from agent: agent refused operation`

**Context:**

- `git pull` fails with the following error:

  > ```txt
  > sign_and_send_pubkey: signing failed for … from agent: agent refused operation
  > user@git.server.fqdn: Permission denied (publickey).
  > fatal: Could not read from remote repository.
  > ```

- The repository is configured to connect to the remote using SSH.
- `ssh` is instructed to use a GPG key for authentication.
- Said GPG key is accepted for the user on the Git server.
- Pinentry should have prompted to input the key's passphrase, but the prompt did **not** appear.

**Solution:**

- Make sure the environment variable `GPG_TTY` is set to the current TTY:

  ```sh
  export GPG_TTY=$(tty)
  ```

- Reload the GPG agent for good measure:

  ```sh
  gpg-connect-agent reloadagent '/bye'
  ```

### New configuration settings are ineffective

**Cause:**

If already running, the GPG agent is still using the old configuration.<br/>
Changing the configuration file now will have no effect.

**Solution:**

Reload the GPG agent:

```sh
gpg-connect-agent reloadagent '/bye'
```

## Further readings

- [Commonly seen problems]
- [Unattended key generation]
- [OpenPGP best practices]
- [GNU/Linux crypto series]

## Sources

All the references in the [further readings] section, plus the following:

- [Decrypt multiple openpgp files in a directory]
- [ask redhat]
- [how can i remove the passphrase from a gpg2 private key?]
- [How to enable SSH access using a GPG key for authentication]
- [gpg failed to sign the data fatal: failed to write commit object]
- [Can you manually add a comment to a PGP public key block and not break it?]
- [How to renew a (soon to be) expired GPG key]
- [Renew GPG key]
- [Archlinux's GnuPG wiki page]
- [GPG agent for SSH authentication]
- [Modify .gnupg home directories]
- [Stick with security: YubiKey, SSH, GnuPG, macOS]

<!--
  References
  -->

<!-- Upstream -->
[commonly seen problems]: https://www.gnupg.org/documentation/manuals/gnupg/Common-Problems.html
[modify .gnupg home directories]: https://www.gnupg.org/documentation/manuals/gnupg/gpgconf.html
[unattended key generation]: https://www.gnupg.org/documentation/manuals/gnupg/Unattended-GPG-key-generation.html

<!-- In-article sections -->
[further readings]: #further-readings

<!-- Others -->
[archlinux's gnupg wiki page]: https://wiki.archlinux.org/title/GnuPG
[ask redhat]: https://access.redhat.com/solutions/2115511
[can you manually add a comment to a pgp public key block and not break it?]: https://stackoverflow.com/questions/58696139/can-you-manually-add-a-comment-to-a-pgp-public-key-block-and-not-break-it#58696634
[decrypt multiple openpgp files in a directory]: https://stackoverflow.com/questions/18769290/decrypt-multiple-openpgp-files-in-a-directory/42431810#42431810
[gnu/linux crypto series]: https://blog.sanctum.geek.nz/series/gnu-linux-crypto/
[gpg agent for ssh authentication]: https://mlohr.com/gpg-agent-for-ssh-authentication-update/
[gpg failed to sign the data fatal: failed to write commit object]: https://stackoverflow.com/questions/39494631/gpg-failed-to-sign-the-data-fatal-failed-to-write-commit-object-git-2-10-0#42265848
[how can i remove the passphrase from a gpg2 private key?]: https://unix.stackexchange.com/a/550538
[how to enable ssh access using a gpg key for authentication]: https://opensource.com/article/19/4/gpg-subkeys-ssh
[how to renew a (soon to be) expired gpg key]: https://filipe.kiss.ink/renew-expired-gpg-key/
[openpgp best practices]: https://help.riseup.net/en/security/message-security/openpgp/gpg-best-practices
[renew gpg key]: https://gist.github.com/krisleech/760213ed287ea9da85521c7c9aac1df0
[stick with security: yubikey, ssh, gnupg, macos]: https://evilmartians.com/chronicles/stick-with-security-yubikey-ssh-gnupg-macos
