openpgp-key-janitor
I've been working on a tool for maintenance of PGP keys. The repository is over at https://git.jcg.re/jcgruenhage/openpgp-key-janitor, and contains the current work-in-progress code base.
Goals
As described in a previous blogpost, I'm maintaining my key on an airgapped device. This means that for doing stuff like ariadne.id OpenPGP profiles, I need to create the proofs on an online machine, transfer stuff onto the other device and then add the notations using gpg, transfer the key back, upload it and then try to verify on keyoxide.org to see whether it all worked. This process is complicated and error prone, not user friendly and quite annoying.
My goal with openpgp-key-janitor
is that I can prepare my changes fully on an
online machine and just need to run a simple command on the airgapped device.
For this, I've created a yaml
representation of what I want the key to look
like, including expiry, subkeys, user IDs and notations. openpgp-key-janitor
just looks at the spec file, the key, and checks what changes need to be made to
get the key to match to the spec file.
As an example, a spec that matches my current key:
---
validity_period: 2y
primary:
flags: [certify]
cipher_suite: Cv25519
subs:
- flags: [authenticate]
cipher_suite: Cv25519
- flags: [sign]
cipher_suite: Cv25519
- flags: [encrypt_for_transport, encrypt_at_rest]
cipher_suite: Cv25519
user_ids:
- value: "Jan Christian Grünhage <jan.christian@gruenhage.xyz>"
notation:
- proof@metacode.biz: matrix:u/@jan.christian:gruenhage.xyz?org.keyoxide.r=!dBfQZxCoGVmSTujfiv:matrix.org&org.keyoxide.e=$dE1DfTlKCtVtxk1P4wv5keXXGIgoHC57BK1Q2kQse_I
- proof@metacode.biz: https://gist.github.com/jcgruenhage/188ca8c3fb50db507878af11f74c29b9
- proof@metacode.biz: dns:jcg.re?type=TXT
- proof@metacode.biz: https://git.jcg.re/jcgruenhage/gitea_proof
- proof@metacode.biz: dns:gruenhage.xyz?type=TXT
- proof@metacode.biz: https://gitlab.com/jcgruenhage/gitlab_proof
- proof@metacode.biz: https://chaos.social/@jcgruenhage
- value: "Jan Christian Grünhage <jcgruenhage@famedly.com>"
notation:
- proof@ariadne.id: matrix:u/@jcgruenhage:famedly.de?org.keyoxide.r=!dBfQZxCoGVmSTujfiv:matrix.org&org.keyoxide.e=$2h_cjzzmycmcpLCkJmBj8yJqSNSSrKoLYV7mljsBMJg
- value: "Jan Christian Grünhage <jcgruenhage@entropia.de>"
- value: "Jan Christian Grünhage <me@jcg.re>"
- value: "Jan Christian Grünhage <j.gruenhage@famedly.com>"
Changes I'd like to make but haven't because I was too annoyed to do so far:
- The proofs in my primary user ID are still using the old metacode.biz keys. Changing that using GnuPG is terribly annoying though, so I won't do it for now.
- My old (but still functional!) work email address is something that I wanted
to remove. It's an alias to my primary work email and I largely don't use it
anymore. Moving the notation would be nice, but I was okay with just dropping
the user ID for now. Which I thought I did. I just accidentally used
deluid
instead ofrevsig
, meaning that the UID is still very much alive. Reading the manual would've prevented this mistake, and once I was finished on my airgapped machine and continued on my online machine I realised immediately what the mistake was, but it's an easy enough mistake to make.
Current state
Right now, openpgp-key-janitor
can only do very little: It tries to read an
existing key, if that works, it writes it out again. If no key exists, it
generates a new one that matches the spec. Changes to existing keys aren't
implemented yet.
Looking at the sample spec committed in the repo:
---
validity_period: 2y
primary:
flags: [certify, sign]
cipher_suite: Cv25519
subs:
- flags: [encrypt_for_transport, encrypt_at_rest]
cipher_suite: Cv25519
validity_period: 3y
user_ids:
- value: "Alice Lovelace <alice@openpgp.example>"
Running openpgp-key-janitor
generates a few things here:
openpgp-key-janitor on main is 📦 v0.1.0 via 🦀 v1.69.0❯ ls -hl sampletotal 8.0K
-rw-rw-r-- 1 jcgruenhage jcgruenhage 252 Jun 4 11:08 spec.yml
openpgp-key-janitor on main is 📦 v0.1.0 via 🦀 v1.69.0❯ ./target/debug/openpgp-key-janitor sample
openpgp-key-janitor on main is 📦 v0.1.0 via 🦀 v1.69.0❯ ls -hl sampletotal 20K
-rw-rw-r-- 1 jcgruenhage jcgruenhage 1.3K Jun 4 11:09 public.asc
-rw-rw-r-- 1 jcgruenhage jcgruenhage 1.5K Jun 4 11:09 rev.asc
-rw-rw-r-- 1 jcgruenhage jcgruenhage 1.4K Jun 4 11:09 secret.asc
-rw-rw-r-- 1 jcgruenhage jcgruenhage 252 Jun 4 11:08 spec.yml
openpgp-key-janitor on main is 📦 v0.1.0 via 🦀 v1.69.0❯ sq inspect sample/public.ascsample/public.asc: OpenPGP Certificate.
Fingerprint: 8DD748AEABF834CCB1DDF3FCE76C163C9D484531
Public-key algo: EdDSA
Public-key size: 256 bits
Creation time: 2023-06-04 09:08:04 UTC
Expiration time: 2025-06-03 21:08:04 UTC (creation time + P730DT43200S)
Key flags: certification, signing
Subkey: 6F0A6A3BAC9A40EB19E29257A773CA5C1D7059B0
Public-key algo: ECDH
Public-key size: 256 bits
Creation time: 2023-06-04 09:08:04 UTC
Expiration time: 2026-06-04 03:08:04 UTC (creation time + P1095DT64800S)
Key flags: transport encryption, data-at-rest encryption
UserID: Alice Lovelace <alice@openpgp.example>
As you can see, a key matching the description in the spec is being generated
successfully. The three files written out are public.asc
, containing the
certificate, secret.asc
contains everything in the certificate, plus the
private bits for the keys, and rev.asc
contains everything in the certificate,
plus a revocation signature for the primary key.
When starting with secret.asc
already present, it tries to load all three
files and makes sure that the content matches. The three files are merged into a
working state, the primary key revocation is filtered out again and stored
separately. If no revocation signature is present yet, it'll be generated then.
Planned features
I want to implement at a minumum:
- Adjusting user IDs, which includes adding and removing them, as well as creating new signatures that update annotations. The usecases here are adding and removing user IDs when email addresses change, as well as updating notations for ariadne.id OpenPGP profiles for keyoxide proofs.
- Extending the exiry of keys, so that extending keys (roughly every 2 years in my case) becomes less of a hassle.
- Adding subkeys that are in the spec, but not in the key. The usecase here would be for example that you already have a key containing an encryption subkey for example, but not a signing subkey.
- Revoking subkeys that are not in the spec but are present in the key. Is there a usecase for this? I'm not entirely sure.
Additional features I've thought about but that I don't have concrete plans for yet:
- Revoking subkeys that are present in both the key and the spec. If you were handling your subkeys online and think they might have been compromised, or lost a smartcard for which you forgot to change the admin pin or something like that, you might want to replace your subkeys without fully revoking the primary key. How exactly this should be represented in the spec isn't clear to me though, which is why I don't have concrete plans for that yet.
Future
If you have opinions or suggestions on this, feel free to reach out to me or to open an issue on the repository directly. I'd be happy to receive feedback and hear about people using this.
Thanks
This work wouldn't be possible without the awesome work by the people over at
Sequoia-PGP. The Rust libraries they provide for
handling anything OpenPGP related are amazing and made the implementation of
openpgp-key-janitor
so far fairly straight forward. My implementation of
openpgp-key-janitor
resulted in a a few contributions upstream and working
with them has been really nice.