Predictable, passphrase-based PGP key generator
passphrase2pgp generates, in OpenPGP format, an EdDSA signing key and Curve25519 encryption subkey entirely from a passphrase, essentially allowing you to store a backup of your PGP keys in your brain. At any time you can re-run the tool and re-enter the passphrase to reproduce the original keys.
The keys are derived from the passphrase and User ID (as salt) using Argon2id (memory=1GB and time=8) and RFC 8032. It's aggressive enough to protect from offline brute force attacks passphrases short enough to be memorable by humans.
See also: Long Key ID Collider
Installation
$ go install nullprogram.com/x/passphrase2pgp@latest
Remember to add $(go env GOPATH)/bin
(or $GOBIN
) to your $PATH
.
Usage
Quick start: Provide a user ID (-u
) and pipe the output into GnuPG.
$ passphrase2pgp -u "Real Name <[email protected]>" | gpg --import
Either --uid
(-u
) or --load
(-l
) is required.
-
The
--uid
(-u
) option supplies the user ID string for the key to be generated. If--uid
is missing, theREALNAME
andEMAIL
environmental variables are used to construct a user ID, but only if both are present. -
The
--load
(-l
) option loads a previously generated key for use in other operations (signature creation, ASCII-armored public key, etc.).
There are three commands:
-
Key generation (
--key
,-K
) [default]: Writes a key to standard output. This is a secret key by default, but--public
(-p
) restricts it to a public key. -
Detached signatures (
--sign
,-S
): Signs one or more input files. Unless--load
is used, also generates a key, but that key is not output. If no files are given, signs standard input to standard output. Otherwise for each argumentfile
createsfile.sig
with a detached signature. If armor is enabled (--armor
,-a
), the file is namedfile.asc
. -
Cleartext signature (
--clearsign
,-T
): Cleartext signs standard input to standard output, or from a file to standard output. The usual cleartext signature caveats apply.
Use --help
(-h
) for a full option listing:
Usage:
passphrase2pgp <-u id|-l key> [-hv] [-c id] [-i pwfile] [--pinentry[=cmd]]
-K [-anps] [-e[n]] [-f pgp|ssh|x509|signify] [-r n] [-t secs] [-x[spec]]
-S [-a] [-r n] [files...]
-T [-r n] >doc-signed.txt <doc.txt
Commands:
-K, --key output a key (default)
-S, --sign output detached signatures
-T, --clearsign output a cleartext signature
Options:
-a, --armor encode output in ASCII armor
-c, --check KEYID require last Key ID bytes to match
-e, --protect[=ASKS] protect private key with S2K
-f, --format FORMAT select key format [pgp]
-h, --help print this help message
-i, --input FILE read passphrase from file
-l, --load FILE load key from file instead of generating
-n, --now use current time as creation date
--pinentry[=CMD] use pinentry to read the passphrase
-p, --public only output the public key
-r, --repeat N number of repeated passphrase prompts
-s, --subkey also output an encryption subkey
-t, --time SECONDS key creation date (unix epoch seconds)
-u, --uid USERID user ID for the key
-v, --verbose print additional information
--version print version information
-x, --expires[=SPEC] set key expiration [2y]
Per the OpenPGP specification, the Key ID is a hash over both the key
and its creation date. Therefore using a different date with the same
passphrase/ID will result in a different Key ID, despite the underlying
key being the same. For this reason, passphrase2pgp uses Unix epoch 0
(January 1, 1970) as the default creation date. You can override this
with --time
(-t
) or --now
(-n
), but, to regenerate the same key
in the future, you will need to use --time
to reenter the exact time.
If 1970 is a problem, then choose another memorable date.
The --check
(-c
) causes passphrase2pgp to abort if the final bytes
of the Key ID do not match the hexadecimal argument. If this option is
not provided, the KEYID
environment variable is used if available. In
either case, --repeat
(-r
) is set to zero unless it was explicitly
provided. The additional passphrase check is unnecessary if they Key ID
is being checked.
The --protect
option uses OpenPGP's S2K feature to encrypt the private
key in the exported format. Rather than prompt for an S2K passphrase,
passphrase2pgp will reuse your derivation passphrase as the protection
passphrase. However, keep in mind that the S2K algorithm is much
weaker than the algorithm used to derive the asymmetric key, Argon2id.
Given an optional numeric argument, --protect
will prompt that many
times (like --repeat
) for a separate S2K passphrase.
By default keys are not given an expiration date and do not expire. To
retire a key, you would need to use another OpenPGP implementation to
import your key and generate a revocation certificate. Alternatively,
the --expires
(-x
) option sets an expiration date, defaulting to two
years from now. As an optional argument, it accepts a time specification
similar to GnuPG: days (d), weeks (w), months (m), and years (y). For
example, --expires=10y
or -x10y
sets the expiration date to 10 years
from now. Without a suffix, the value is interpreted as a specific unix
epoch timestamp.
Unfortunately there's a bug in the way GnuPG processes key expiration
dates that affect passphrase2pgp. Keys with a zero creation date are
incorrectly considered never to expire despite an explicit
expiration date. This means if you use passphrase2pgp's default creation
date, the --expires
(-x
) may appear not to work, and GnuPG will
incorrectly verify signatures from your expired keys. Further, GnuPG
generally doesn't compute expiration dates correctly. OpenPGP
allows expiration dates beyond the year 2106, and, unlike GnuPG,
passphrase2pgp will allow you construct such keys, but GnuPG will use an
incorrect (earlier) date.
Examples
Generate a private key and send it to GnuPG (no protection passphrase):
$ passphrase2pgp --uid "..." | gnupg --import
Or, with --protect
, reuse the derivation passphrase as the protection
passphrase so that the key is encrypted on the GnuPG keyring using your
derivation passphrase:
$ passphrase2pgp --protect --uid "..." | gnupg --import
Or to prompt (once) for a different passphrase to use as the protection passphrase:
$ passphrase2pgp --protect=1 --uid "..." | gnupg --import
Create an armored public key for publishing and sharing:
$ passphrase2pgp --uid "..." --armor --public > Real-Name.asc
Since passing --uid
every time you need it is tedious, that argument
can be supplied implicitly via two environment variables, REALNAME
and
EMAIL
. The remaining examples assume these variables are set.
$ export REALNAME="Real Name"
$ export EMAIL="[email protected]"
$ passphrase2pgp -ap > Real-Name.asc
Create detached signatures (-S
) for some files:
$ passphrase2pgp -S document.txt avatar.jpg
This will create document.txt.sig
and avatar.jpg.sig
. The other end
would use GnuPG to verify the signatures like so:
$ gpg --import Real-Name.asc
$ gpg --verify document.txt.sig
$ gpg --verify avatar.jpg.sig
Normally each command must derive keys from scratch from the passphrase,
requiring the user to re-enter it for each command and wait. To avoid
this, save the secret key to a file in OpenPGP format and then load
(--load
) it for other commands. This will save an unprotected version:
$ passphrase2pgp > secret.pgp
Then you can sign files without re-entering your passphrase:
$ passphrase2pgp -S --load secret.pgp document.txt avatar.jpg
If you used an S2K protection passphrase (--protect
), passphrase2pgp
will prompt for it when loading such keys.
More signatures, but ASCII-armored:
$ passphrase2pgp -S -lsecret.pgp --armor document.txt avatar.jpg
$ gpg --verify document.txt.asc
$ gpg --verify avatar.jpg.asc
Create a cleartext-signed (-T
) text document:
$ passphrase2pgp -T >signed-doc.txt <doc.txt
Intended Workflow
There are two usage patterns: "lite" and "full".
When a "lite" user sets up a new computer, they run passphrase2pgp just once and send the key straight into GnuPG. After this they use GnuPG for everything OpenPGP-related. This command installs the secret key in GnuPG with a separate, more convenient, protection passphrase:
$ passphrase2pgp -u '...' -e1 | gpg --import
This user will not need to backup their keyring since they can always regenerate their key in the future. They're also free to destroy their keyring at any moment, such as before their computer is accessed by untrusted people (border agents, etc.).
A "full" user will use passphrase2pgp directly for signatures and will
never store the secret key permanently. They derive the key on demand
only when needed. To make this convenient, this users sets REALNAME
,
EMAIL
, and KEYID
in their .profile
. This means they never have to
supply --uid
, and there's no passphrase confirmation prompt.
For example, supposed John Doe is a "full" user setting up for the first
time with the passphrase "boa trusted stew critics dispute asked naming
gyms". First he sets his user ID in his .profile
:
export REALNAME="John Doe"
export EMAIL="[email protected]"
Then he generates his public key and gets the fingerprint:
$ passphrase2pgp --verbose --public >John-Doe.asc
User ID: John Doe <[email protected]>
passphrase:
passphrase (repeat):
Key ID: C8A22A0535AF18BC83D7AE21406CC07F8DABE73B
He publishes John-Doe.asc
and adds the fingerprint to his .profile
:
export KEYID=C8A22A0535AF18BC83D7AE21406CC07F8DABE73B
This is the actual key for that user ID and passphrase, so you can try each of these commands yourself. Later if he, say, needs to clearsign a message:
$ echo The swallow flies at midnight >message.txt
$ passphrase2pgp -T <message.txt
passphrase:
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
The swallow flies at midnight
-----BEGIN PGP SIGNATURE-----
wnUEARYIACcFAl1UG1gJEEBswH+Nq+c7FiEEyKIqBTWvGLyD164hQGzAf42r5zsA
ADWqAP9KfoQm02q+AXE5brS9lNZ8LVjFs6CefMA4C/83Da7E4wD/QnYNyFmpmTOm
B6w1UvDnxyD0ksjmyj6NDiRs25b20gk=
=0gf5
-----END PGP SIGNATURE-----
Again, this was all done without ever storing the secret key in the file system, even in protected form.
GnuPG Trust
Trust is stored external to keys, so imported keys are always initially
untrusted. You will likely want to mark your newly-imported primary key
as trusted. Or use the --trusted-key
option in gpg.conf
.
$ gpg --edit-key "Real Name"
gpg> trust
Similarly, to allow gpgv to verify your signatures, append your public key to its trusted keyring:
$ passphrase2pgp -p >> ~/.gnupg/trustedkeys.kbx
Signing Git tags and commits
It's even possible to use passphrase2pgp directly to sign your Git tags
and commits. Just set gpg.program
to passphrase2pgp:
$ git config --global gpg.program passphrase2pgp
When passphrase2pgp detects that it's been invoked via Git, it presents
a GnuPG-like interface to Git. When asked to verify tags and commits
(git verify-tag
, git verify-commit
), it delegates to the program
named gpg
.
OpenSSH format
Despite the name, passphrase2pgp can output a key in OpenSSH format,
selected by --format
(-f
). Passphrase protection (bcrypt) is
supported. When using this format, the --armor
(-a
), --now
(-n
),
--subkey
(-s
), and --time
(-t
) options are ignored since they do
not apply. The user ID becomes the key comment and is still used as the
salt.
$ passphrase2pgp --format ssh | (umask 077; tee id_ed25519)
This will be exactly the same key pair as when generating an OpenPGP
key. It's just written out in a different format. The public key will be
harmlessly appended to the private key, but it could also be regenerated
with ssh-keygen
:
$ ssh-keygen -y -f id_ed25519 > id_ed25519.pub
With the --public
(-p
) option, only the public key will be output.
You may want to add a protection key to the generated key, which, again,
can be done with ssh-keygen
:
$ ssh-keygen -p -f id_ed25519
Generally you really should have a unique SSH key per host, and this sort of long-term key is both unnecessary and undesired. If you loose access to that computer โ theft, retirement, etc. โ then you can remove just that host's key as an authorized key without affecting other hosts. In general, SSH keys need not and should not be backed up, including in your brain.
However, there is at least one case where a long-term, important SSH key could be useful. Suppose you have a vital, remote system with password authentication disabled. If you loose access to all of the authorized keys, you can no longer remotely log into that system. Correcting this problem may require traveling to the computer's location or using some inconvenient means to regain access.
Instead, you could use passphrase2pgp to generate an emergency SSH key and install it as an authorized key on the remote host. But don't actually store the private key anywhere and don't normally use this key! When you're in a pinch, use passphrase2pgp to regenerate the emergency key, recover your access, then immediately destroy the emergency key.
Setting up the emergency key ahead of time:
$ passphrase2pgp -u emergency -f ssh -p > ~/.ssh/emergency.pub
$ ssh-copy-id -f -i ~/.ssh/emergency.pub important.example.com
Later, when in dire straits, generate the private key, and use it to install a non-emergency key as a new authorized key:
$ passphrase2pgp -u emergency -f ssh | ssh-add -
$ ssh-copy-id -i ~/.ssh/id_ed25519 important.example.com
Other formats
The --format
option also supports:
x509
: self-signed TLS certificates (--protect
unsupported)signify
: OpenBSDsignify(1)
keys, including--protect
Justification
Isn't generating a key from a passphrase foolish? If you can reproduce your key from a passphrase, so can any one else!
In 2019, the fastest available implementation of Argon2id running on the best available cloud hardware takes just over 6 seconds with passphrase2pgp's default parameters. That's 6 seconds of a dedicated single CPU core and 1GB of RAM for a single guess. This means that at the current cloud computing rates it costs around US$50 to make 2^20 (~1 million) passphrase guesses.
A randomly-generated password of length 8 composed of the 95 printable ASCII characters has ~52.6 bits of entropy. Therefore it would cost around US$ 158 billion to for just a 50% chance of cracking that passphrase. If your passphrase is generated by a random process, and it's at least this long, it is not the weak point in this system.
Regarding the encryption subkey
Since OpenPGP encryption is neither good nor useful anymore, I considered not generating an encryption subkey. The "privacy" portion of OpenPGP has become the least important part. However, the upcoming update to OpenPGP, rfc4880bis, adds AEAD encryption, and this could make encryption interesting again.
OpenPGP digital signatures still have some limited use, mostly due to the lack of adoption of the alternatives. The OpenPGP specification is too flexible and is loaded with legacy cruft. Further, GnuPG is honestly not a great OpenPGP implementation, and I do not have high confidence in it.
Roadmap
- AEAD (tag 20) encryption and limited decryption
References
- RFC 4880: OpenPGP Message Format
- RFC 6637: Elliptic Curve Cryptography (ECC) in OpenPGP (incomplete / inaccurate)
- RFC 7748: Elliptic Curves for Security
- RFC 8032: Edwards-Curve Digital Signature Algorithm (EdDSA)
- RFC Draft: EdDSA for OpenPGP
- RFC Draft: OpenPGP Message Format (incomplete / inaccurate)