PGP public keys
Is it possible to define a codec for PGP / GPG public keys?
The most recent OpenPGP specification seems to be RFC 9580 (published: July 2024).
Sure, but you'd probably want to define a usecase to drive the addition to the table rather than just adding to the table because we can. Maybe that usecase is already covered by some other entry, or maybe there is a good case for it. If you have code or a spec that wants it then that's much more compelling.
I am interested in creating Multikey objects that represent PGP keys. This is useful on its own, because these objects can be attached to other JSON documents, but it would also be the first step towards making PGP compatible with the Data Integrity web standard.
Another possible use case: generation of a decentralized identifier (DID) from a PGP key (of did:key type).
I'm also interested in using my OpenPGP smart card as a multikey. But I also wonder if it would be sufficient to skip the OpenPGP protocol and identify the raw public key components with existing multicodecs.
In GnuPG, it seems that you can export public keys as SSH keys (which in turn can be converted to PEM/DER):
$ ssh-keygen -f <(gpg --export-ssh-key 'AA0FDB57D0C667DF804641977D0FB0D9068F977A!') -e -m pem | openssl pkey -pubin -outform der | base64
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxGbaTBcUWiWt194hO2Ei…
(Unfortunately, the ssh-keygen command doesn't seem to support Ed25519 keys as of OpenSSH_9.9p2, but you can convert Ed25519 keys to PEM with a manual coding at least.)
Also, the GnuPG agent protocol has the PKSIGN command, with which you can produce signatures for arbitrary hashes (verifiable by dependent crypto libraries).
Example usage
$ FINGERPRINT=F706F9BBB8F63CDE5F40298FDF5EB02BE5C2542E
$ KEYGRIP=EFF4BC5C63B16808DA29076D3246F53CC79EA481
$ gpg --export-ssh-key "$FINGERPRINT!" > key.pub
$ python3 - "$(gpgconf --list-dirs agent-socket)" "$KEYGRIP" hello.txt <<PYTHON
import re
import sys
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding, utils
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
from pyassuan import client, common
sock = sys.argv[1]
sigkey_grip = sys.argv[2]
in_file = sys.argv[3]
# Hash and sign the file:
hash_algo = hashes.SHA256()
hasher = hashes.Hash(hash_algo)
with open(in_file, 'rb') as f:
while (chunk := f.read(1024)):
hasher.update(chunk)
hash_bytes = hasher.finalize()
client = client.AssuanClient('')
client.connect(sock)
try:
assert client.read_response().type == 'OK'
client.make_request(common.Request('SIGKEY', sigkey_grip))
client.make_request(common.Request('SETHASH', '--hash=sha256 ' + hash_bytes.hex()))
res = client.make_request(common.Request('PKSIGN'))
assert res
sig = res[1]
assert sig is not None
# Should be replaced with a proper csexp parser in a serious application.
match = re.match(b'\\A\\(7:sig-val\\(\\d+:[^(]+(?:\\(1:r\\d+:(.+)\\))?\\(1:s\\d+:(.+)\\)\\)\\)\\Z', sig, re.DOTALL)
assert match
sig = (match[1] or b'') + match[2]
finally:
client.make_request(common.Request('BYE'))
client.disconnect()
# Verify the signature:
with open('key.pub', 'rb') as key_file:
pubkey = serialization.load_ssh_public_key(key_file.read())
if isinstance(pubkey, Ed25519PublicKey):
pubkey.verify(sig, hash_bytes)
elif isinstance(pubkey, RSAPublicKey):
algorithm = utils.Prehashed(hash_algo)
padding = padding.PKCS1v15()
pubkey.verify(sig, hash_bytes, padding=padding, algorithm=algorithm)
else:
raise Exception(f'Unsupported public key type: {pubkey.__class__.__name__}')
PYTHON
I have successfully tested it with my Ed25519 key on a Nitrokey and RSA key on an on-disk keyring.
The agent protocol's support for signing arbitrary hashes suggests that the underlying OpenPGP smart card application supports the operation too, and in fact, the spec explicitly states in 7.2.10 PSO: COMPUTE DIGITAL SIGNATURE
that:
It is possible to use the command outside of the OpenPGP environment (e. g. S-MIME), […]
So this seems to be an intended use of OpenPGP smart cards rather than an accidentally possible hack.
We already have:
- rsa-pub / rsa-priv
- ed25519-pub / ed25519-priv
- p256-pub / p384-pub / p521-pub
- secp256k1-pub / secp256k1-priv
- x25519-pub / x25519-priv
Plus we have a multikey entry. What additional entries are you anticipating necessary for your use case(s) that aren't covered here. Can you suggest some additional entries so we can discuss in more concrete terms because it's not obvious to me at a glance what additional would be required in the table.