Apr 14, 2023 Tags: cryptography, devblog, programming, rust
This is a short tool announcement post for pgpkeydump
, a CLI tool I’ve
written as part of a yak stack of tasks related to bulk auditing of PGP
signatures and their associated keys1.
TL;DR: You can use pgpkeydump
to get a JSON representation of a PGP key,
including relevant components like the key’s algorithm, algorithm parameters,
expiry, and so forth:
1
2
3
4
cargo install pgpkeydump
pgpkeydump \
<(curl https://keys.openpgp.org/vks/v1/by-keyid/85AE00C504833B3C)
produces (with some light editing for readability):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
"armor_headers": [
"46C3 9716 A8F0 7E98 384E 28F7 85AE 00C5 0483 3B3C",
"William Woodruff (yossarian.net) <william@yossarian.net"
],
"fingerprint": "46C39716A8F07E98384E28F785AE00C504833B3C",
"keyid": "85AE00C504833B3C",
"userids": [
"William Woodruff (yossarian.net) <william@yossarian.net>"
],
"primary_key": {
"algorithm": "RSA",
"parameters": {
"algorithm": "RSA",
"e": {
"bitness": 17,
"value": "010001"
},
"n": {
"bitness": 4096,
"value": "very-long-n"
}
},
"fingerprint": "46C39716A8F07E98384E28F785AE00C504833B3C",
"keyid": "85AE00C504833B3C",
"expiration": "2025-05-23T16:56:46+00:00",
"purposes": {
"authentication": true,
"certification": true,
"signing": true,
"storage_encryption": false,
"transport_encryption": false
}
},
"subkeys": [],
"revocation_keys": [],
"alive": true
}
Despite2 being around for over 30 years, there is no good way to get a machine-readable dump of a PGP key (or, more generally, any sequence of PGP packets) using tools in the mainstream PGP ecosystem. This is particularly annoying when you want to do something extremely reasonable, like see how strong the average signing key for a particular task (such as package or code signing) is.
If you stay within the world of gpg
and adjacent tools, your best options
are:
pgpdump
, which provides a textual dump of each packet in a sequence of
PGP packets. This dump is intended for humans and is not easy to
re-parse without lots of text munging; pgpdump
itself is also
lightly maintained and not regularly updated by Linux distributions;
Ubuntu hasn’t updated their copy of it in over 4 years.
gpg --with-colons
, which GPG’s manual describes as “easily machine
parsed.” This is a lie.
sq packet dump
, which is built into Sequoia PGP’s sq
CLI. While more
modern and resilient than pgpdump
, it’s similarly geared towards humans
and doesn’t provide a machine-readable output format.
There is no good technical reason why I should’t be able to get a basic JSON
representation of a PGP key message (i.e., a primary key packet, subkey
packets, and a few other related packets). This is why pgpkeydump
now
exists.
Under the hood, pgpkeydump
is just Sequoia’s Rust APIs, wrapped into
serde
Serialize
-able structures. Everything it does is something that
Sequoia could do on its own (either in APIs or via the sq
CLI), but it
was easier for me to make a standalone tool than to go through an upstreaming
process for a single frame in my yak stack.
For example, this is all that pgpkeydump
does to turn a Sequoia
ValidKeyAmalgamation
(i.e. a key that’s been bound to a time and policy)
into a serializable structure:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#[derive(Serialize)]
struct DumpableKey {
algorithm: String,
parameters: DumpableKeyParams,
fingerprint: String,
keyid: String,
expiration: Option<String>,
purposes: KeyPurposes,
}
impl From<ValidKeyAmalgamation<'_, PublicParts, PrimaryRole, ()>> for DumpableKey {
fn from(key: ValidKeyAmalgamation<'_, PublicParts, PrimaryRole, ()>) -> Self {
let expiration = key
.key_expiration_time()
.map(|t| DateTime::<Utc>::from(t).to_rfc3339());
let purposes = KeyPurposes {
authentication: key.for_authentication(),
certification: key.for_certification(),
signing: key.for_signing(),
storage_encryption: key.for_storage_encryption(),
transport_encryption: key.for_transport_encryption(),
};
let key = key.key();
Self {
algorithm: key.pk_algo().to_string(),
parameters: key.mpis().into(),
fingerprint: key.fingerprint().to_hex(),
keyid: key.keyid().to_hex(),
expiration,
purposes,
}
}
}
(There’s a bit more to it in terms of further nested structures and From
implementations, but you get the idea.)