openpgp-key-janitor

6 minute read Published: 2023-06-04

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:

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:

Additional features I've thought about but that I don't have concrete plans for 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.