Skip to content

Instantly share code, notes, and snippets.

@jan-warchol
Last active October 20, 2020 11:29
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jan-warchol/bd6340a8b49f0033aec5fbe3e9aa10d5 to your computer and use it in GitHub Desktop.
Save jan-warchol/bd6340a8b49f0033aec5fbe3e9aa10d5 to your computer and use it in GitHub Desktop.
GPG tutorial demonstrating typical workflow in practice

Learn GPG by doing

This tutorial will walk you through typical GPG workflow: generating, signing, trusting, renewing and backing up keys. Instead of long explanations it just shows what happens in practice on test data.

Important: older GPG versions behave significantly different and use different formats for storing data. This tutorial was tested with GPG 2.2.4; you can check your version using gpg --version.

How this works

Setting environment variable GNUPGHOME tells GPG to use a different directory for the data, allowing us to experiment safely without affecting your real keyring (stored by default in ~/.gnupg).

Usage

Running ./setup-gpg-user John will create a GPG folder named John and generate there a key with UID John, email john@example.com and passphrase I am John. To work with this keyring, run export GNUPGHOME=John.

The recommended way of going through this tutorial is to retype (or copy-paste) the commands from each script into your terminal. (You can also execute these scripts, but this will run everything at once.) Note: run, alice_does, bob_does are simple wrappers for displaying the command before its output (and setting GNUPGHOME appropriately).

In exercises simulating multiple users, I suggest opening separate terminal for each user, so that you don't have to change GNUPGHOME all the time.

#!/bin/bash
source utils.sh
# This tutorial was tested with GPG 2.2.
# Check your version; you may have to use `gpg2` command to get GPG v2
run gpg --version | highlight "gpg.*[1-9]\.[0-9]\..*"
# create test identity
./setup-gpg-user.sh Alice
export GNUPGHOME=Alice
# Show key. E=encryption, S=signing, C=certification, A=authentication
# (see https://unix.stackexchange.com/a/230859)
run gpg --list-keys alice@example.com
run gpg --list-secret-keys alice@example.com | highlight "sec|ssb"
# (You can also use key ID/fingerprint instead of email.)
# To see key details, including trust value, use --edit-key dialog
run gpg --batch --edit-key alice@example.com quit
# For some reason private key files (stored in `private-keys-v1.d`)
# are named with "keygrips", not key IDs
run ls -l $GNUPGHOME/private-keys-v1.d/
run gpg --list-keys --with-keygrip alice@example.com
# Create an ASCII-encoded ("armored") encrypted message
echo "encryption test" > encryption-test
run gpg --armor --encrypt --recipient alice@example.com encryption-test
cat encryption-test.asc; echo
# Decrypt the message (gpg will prompt for key passphrase)
# Note that the key ID mentioned is the ID of *encryption subkey*, not main key
run gpg --decrypt encryption-test.asc |& highlight "[0-9A-F]{16,}"
run gpg --list-keys --with-subkey-fingerprint alice@example.com
#!/bin/bash
# Open two terminals - one for Alice:
./setup-gpg-user.sh Alice 1w
source utils.sh
export GNUPGHOME=Alice
# ...and one for Bob:
./setup-gpg-user.sh Bob 3w
source utils.sh
export GNUPGHOME=Bob
# Bob writes a message to Alice and he wants to encrypt it
bob_does echo "Hi Alice, Bob here." > msg-to-alice
# He gets Alice's public key
alice_does \
gpg --armor --export alice@example.com > alice.pub
bob_does \
gpg --import alice.pub
# At this moment Alice's key is marked "unknown" (it's not verified yet)
bob_does \
gpg --list-keys | highlight "unknown"
# Alice's key is self-signed - but that signature doesn't prove anything
bob_does \
gpg --check-signatures alice@example.com | highlight " Alice"
# Because of that trying to encrypt with it will show a warning
# (answer "no" to the prompt)
bob_does \
gpg --armor --encrypt --recipient alice@example.com msg-to-alice
# Alice tells her key fingerprint to Bob (in person or in other secure way)
alice_does \
gpg --fingerprint alice@example.com | highlight "[0-9A-F ]{16,}"
# Bob signs her key with his own key (check if the fingerprint matches!!),
# confirming this key really belongs to Alice
bob_does \
gpg --sign-key alice@example.com
# Now Alice's key is marked as verified
bob_does \
gpg --list-keys alice@example.com | highlight "full"
# And encryption works without warning
bob_does \
gpg -a -e -r alice@example.com msg-to-alice
cat msg-to-alice.asc
# Alice can read the message
alice_does \
gpg -d msg-to-alice.asc
#!/bin/bash
source utils.sh
# Alice's key is signed by Bob
bob_does \
gpg --check-signatures alice@example.com | highlight "Bob"
# Bob exports Alice's key with his signature and gives it to Alice
bob_does \
gpg --armor --export alice@example.com > alice-signed-by-bob.pub
alice_does \
gpg --import alice-signed-by-bob.pub
# Alice has Bob's signature, but doesn't have his key to check it
alice_does \
gpg --check-signatures alice@example.com | highlight "signature not checked"
bob_does \
gpg --armor --export bob@example.com > bob.pub
alice_does \
gpg --import bob.pub
# Now Alice can see her key was signed by Bob
alice_does \
gpg --check-signatures alice@example.com | highlight "Bob"
#!/bin/bash
./setup-gpg-user.sh Eve 5w
source utils.sh
# Eve exports her public key and puts it on her website.
# Bob and Alice both import it.
eve_does gpg --armor --export eve@example.com > eve.pub
alice_does gpg --import eve.pub
bob_does gpg --import eve.pub
# Alice meets Eve in person, gets her key fingerprint and signs the key.
alice_does gpg --quick-sign-key $(cat Eve/key-id)
alice_does gpg --check-signatures eve@example.com | highlight "Alice"
# Bob cannot meet Eve in person, but he trusts Alice (select "full" or
# "marginal" trust, confirm with "save"). He asks for Eve's key signed by
# Alice; the validity of that key corresponts to Bob's trust in Alice.
bob_does gpg --edit-key alice@example.com trust
alice_does gpg --armor --export eve@example.com > eve-signed-by-alice.pub
bob_does gpg --import eve-signed-by-alice.pub
bob_does gpg --check-signatures eve@example.com |
highlight "Alice" | highlight "\[........\]"
#!/bin/bash
source utils.sh
future_gpg(){
gpg --faked-system-time $(date -d "+2 weeks" +%s) "$@"
}
# In two weeks from now, Alice's key will have expired. Since Bob relied on
# Alice's signature for validating Eve's key, that key will also be affected.
bob_does future_gpg --list-keys | highlight "expired" | highlight "unknown"
alice_does future_gpg --list-keys | highlight "expired"
# Alice can change expiration date on her key and share the update with Bob.
# (Input new expiration time and then "save"; don't update the subkey yet.)
alice_does future_gpg --edit-key alice@example.com expire
alice_does future_gpg --list-keys | highlight "ultimate" | highlight "full"
alice_does future_gpg --armor --export alice@example.com > alice-extended.pub
bob_does future_gpg --import alice-extended.pub
bob_does future_gpg --list-keys | highlight "full" | highlight "marginal"
# However, Bob still gets an error when trying to encrypt a message to Alice.
# Error message is quite misleading, but it means there is no valid key to use.
bob_does echo "Hi Alice, long time no see." > new-msg-to-alice
bob_does future_gpg -a -e -r alice@example.com new-msg-to-alice |& highlight "Unusable"
# Alice could extend encryption key, but she can also generate a new subkey.
# Rotating the subkey gives better security but is less convenient.
alice_does future_gpg --edit-key alice@example.com addkey
alice_does future_gpg --batch --edit-key alice@example.com check
alice_does future_gpg --armor --export alice@example.com > alice-new-subkey.pub
bob_does future_gpg --import alice-new-subkey.pub
bob_does future_gpg -a -e -r alice@example.com new-msg-to-alice
cat new-msg-to-alice.asc
#!/bin/bash
source utils.sh
# We can backup GPG by simply copying the files. This is the best approach in
# case of private keys, which never change. However, for pubring and trustdb
# (which contain information about validity and trust of keys you've collected)
# it's better to use export and import - this allows two-directional sync.
# NOTE: older versions of GPG use a completely different storage format!
source_dir=Bob
backup_dir=backup
target_dir=import
mkdir -pm 700 $target_dir $backup_dir
export GNUPGHOME=$source_dir
# Note: exporting and importing private keys would prompt for password, because
# they would have to be converted to OpenPGP format and reencrypted.
cp -a $source_dir/private-keys-v1.d $backup_dir/private-keys-v1.d
cp -a $source_dir/openpgp-revocs.d $backup_dir/openpgp-revocs.d
gpg --export --armor > $backup_dir/pubring.asc
gpg --export-ownertrust > $backup_dir/trust-values
export GNUPGHOME=$target_dir
cp -a $backup_dir/private-keys-v1.d $target_dir/private-keys-v1.d
cp -a $backup_dir/openpgp-revocs.d $target_dir/openpgp-revocs.d
# Note: without data from pubring restored private keys won't be listed!
run gpg --list-keys
run gpg --import $backup_dir/pubring.asc
# We need trust data to know the validity of the keys (even local private keys
# are not trusted by default)
run gpg --list-keys | highlight "unknown"
run gpg --import-ownertrust $backup_dir/trust-values
run gpg --list-keys | highlight "\[........\]"
# synchronizing GPG databases is a matter of exporting/importing pubring and
# trustdb, and occasional sync of private-keys-v1.d if we add new keys.
#!/bin/bash
# Parse arguments and construct user parameters
[ $# -lt 1 ] && echo "Usage: $0 <user name> [<key valid for>]" && exit 1
USER_NAME="$1"
NAME_SLUG="$(echo $USER_NAME | sed -E s/[^a-zA-Z0-9]+/-/g | tr A-Z a-z )"
USER_EMAIL="$NAME_SLUG@example.com"
USER_PASS="I am $USER_NAME"
KEY_EXPIRY="${2:-1w}"
# Setup a dedicated directory for testing, don't mess with ~/.gnupg
BASE_DIR="$(dirname $(readlink --canonicalize "$0"))"
GNUPGHOME="$BASE_DIR/$USER_NAME"
LOG_FILE="$GNUPGHOME/tutorial-setup.log"
mkdir -p -m 700 "$GNUPGHOME"
export GNUPGHOME
echo -e "GPG data belonging to $USER_NAME will be stored in \e[1;37m$GNUPGHOME\e[0m".
# Only run actual key generation if it wasn't done before
if [ -e "$LOG_FILE" ]; then
echo "GPG home for $USER_NAME already exists, nothing to do."; echo; exit;
fi
echo; echo "Generating new key for $USER_NAME <$USER_EMAIL>:"
# Log to a file for later reference (e.g. to get ID after script finished).
# Note: keys are short because performance > security for testing purposes
gpg --batch --generate-key --logger-file "$LOG_FILE" <<EOF
Key-Type: default
Key-Length: 1024
Subkey-Type: default
Subkey-Length: 1024
Name-Real: $USER_NAME
Name-Email: $USER_EMAIL
Passphrase: $USER_PASS
Expire-Date: $KEY_EXPIRY
EOF
cat "$LOG_FILE"
echo
KEY_ID=$(tail -1 "$LOG_FILE" | grep -Eo "[0-9A-F]{40}")
# Trigger database maintenance (could be confusing if it appears on its own)
gpg --check-trustdb; echo
# Cache some information for other scripts
echo -e "Generated key with ID \e[1;37m$KEY_ID\e[0m"
echo -e "and passphrase \e[1;37m$USER_PASS\e[0m."
echo Writing down key ID to $GNUPGHOME/key-id.
echo $KEY_ID > "$GNUPGHOME/key-id"
# helpers for displaying commands (use stderr to avoid mixing with actual output)
run() {
echo -e "\e[1;37m>>> $@ \e[0m" >&2; eval "$@"; echo "" >&2;
}
alice_does() {
echo -e "\e[1;37m> Alice runs: $@ \e[0m" >&2
eval "GNUPGHOME=Alice $@"
echo "" >&2
}
bob_does() {
echo -e "\e[1;37m> Bob runs: $@ \e[0m" >&2
eval "GNUPGHOME=Bob $@"
echo "" >&2
}
eve_does() {
echo -e "\e[1;37m> Eve runs: $@ \e[0m" >&2
eval "GNUPGHOME=Eve $@"
echo "" >&2
}
# helper for visually marking important parts of output
highlight() {
sed -E "s/($@)/\1/g"
}
# show current value of GPG home in bold yellow
export PS1="GNUPGHOME=\e[33;1m\$GNUPGHOME\e[0m $PS1"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment