As I’ve vaguely alluded to in the past, I’ve wanted to investigate how a PGP keyserver backed by an on-chain contract might look. Now that I’ve had a few moments to think about it, here’s what I’ve come up with:
Motivation
Why might we want PGP keys to be registered on chain? PGP has been around for eons, and is well supported in many programs: from email clients, to source control, to software distribution, and more.
Unfortunately, one of the downsides of PGP is key distribution/verification. How do you retrieve the other party’s encryption key before sending the first email? How do you know whether the signing key is the correct one for the software you downloaded?
Keyservers solve some of these problems by mapping from a user id (typically an email address) to a key bundle, or certificate. Keyservers are trusted and centralized parties. A keyserver can publish any certificate for an email, or could withhold revocations, or just disappear entirely.
I’d like to come up with a way to run a decentralized and trust-minimized keyserver on top of the Ethereum blockchain to solve these issues.
A Solution
type CertId is uint256;
interface KeyRegistry {
//--------------------------------------------------------------------------
// Events
//--------------------------------------------------------------------------
event Certify(
address indexed certifier, bytes16 indexed kind, CertId indexed certId
);
event Revoke(
address indexed certifier, bytes16 indexed kind, CertId indexed certId
);
//--------------------------------------------------------------------------
// Key Management Functions
//--------------------------------------------------------------------------
/// @notice Link the given key with the sender's (certifier's) address.
function certify(
bytes16 kind,
uint64 validBefore,
bytes memory publicKey,
bytes memory location
) external returns (CertId);
/// @notice Mark the identified key as revoked.
function revoke(CertId certId) external;
//--------------------------------------------------------------------------
// Getter Functions
//--------------------------------------------------------------------------
/// @notice Retrieve the revocation token for a certification.
function idOf(address addr, bytes16 kind) external view returns (CertId);
/// @notice Retrieve the public key of a certification.
function keyOf(address addr, bytes16 kind)
external
view
returns (bytes memory publicKey);
/// @notice Retrieve the location of data associated with a certification.
function locationOf(address addr, bytes16 kind)
external
view
returns (bytes memory location);
/// @notice Retrieve the first second where the certification is invalid.
function validBeforeOf(address addr, bytes16 kind)
external
view
returns (uint64 validBefore);
//--------------------------------------------------------------------------
// Permit-style Functions
//--------------------------------------------------------------------------
/// @notice Mark the identified key as revoked.
function revoke(CertId certId, bytes calldata signature) external;
/// @notice Link the key with the address recovered from the signature.
function certify(
bytes16 kind,
uint64 validBefore,
bytes calldata publicKey,
bytes calldata location,
bytes calldata signature
) external returns (CertId);
//--------------------------------------------------------------------------
// Multi-call Functions
//--------------------------------------------------------------------------
/// @dev See https://eips.ethereum.org/EIPS/eip-6357.
function multicall(bytes[] calldata data)
external
returns (bytes[] memory);
}
Certification
A “certification” above is a statement from the owner of an Ethereum account (called the certifier) that a particular public key is a valid signer for that account. A certification SHALL be created after the successful execution of either of the certify
methods above.
The location of a certification may be changed without triggering a revocation, or more formally:
- If
certify
is called for the same certifier-kind pair with an unchanged public key and an unchanged valid before time, the previous certification MAY be revoked. - If
certify
is called for the same certifier-kind pair with a different public key, the previous certification MUST be revoked. - If
certify
is called for the same certifier-kind pair with a different valid before time, the previous certification MUST be revoked.
A certification SHALL remain valid as long as it hasn’t expired and hasn’t been revoked.
Getter functions MUST revert if no matching valid certification exists.
When a certification is created or changed, a Certify
event MUST be emitted.
If a certification is changed without being revoked, the returned certification identifier MUST remain the same.
Kind
This proposal standardizes the following kind
values:
Value | Public Key Algorithm | Location Interpretation |
---|---|---|
0x50475008132b8104000a000000000000 |
ECDSA+secp256k1 (stored in uncompressed form with the 0x04 prefix) | IPFS CID in binary encoding pointing to a series of unarmoured OpenPGP packets |
Further proposals may define other non-conflicting values.
The value 0x50475008132b8104000a000000000000
is derived from "PGP" (0x504750) || SHA256 (0x08) || ECDSA (0x13) || secp256k1 (0x2b8104000a)
, but the kind value is completely arbitrary. 0x01
would’ve been just as valid here.
Valid Before
The certification is valid while block.timestamp < validBefore
. Implementations MAY interpret the value 2^64 - 1
as meaning the certification never expires.
Public Key
The public key material to associate with the certifier’s identity. The format of the public key is determined by the kind field.
Implementations MAY assume a maximum length of 0xFFFF
octets.
Location
A location, in a format determined by the kind field, of off-chain data associated with the certification.
Implementations MAY assume a maximum length of 0xFFFF
octets.
Revocation
A certification is said to be revoked if either of the revoke
methods above are successfully executed with a matching identifier, or if a certification is replaced according to the rules above.
When a certification is revoked, a Revoke
event MUST be emitted.
Notes on Compatibility
gnupg supports ECDSA with the secp256k1 curve for signatures, which I believe is compatible with the ecrecover
precompile (in a roundabout way.)
SHA-256 (not keccak256) hashes are supported both by RFC 4880 and by the SHA256
precompile.
Notes on Permit-style Functions
The format of the signed messages will likely be EIP-712, but the specifics are to be determined.
Further Questions
- Can we standardize storing private key material on-chain, encrypted with a key derived from a mnemonic phrase?
- Should we allow, deny, or leave unspecified revoking and replacing expired keys?
Is there a more efficient way to represent packets to avoid writing a PGP parser in Solidity?Can we execute the merge (with verification) off-chain and use a zk-proof to show it was done correctly for less gas than verifying on-chain?Storing certificates on-chain will be very expensive (napkin math says at least ~400k gas.) Can we do the work of merging/verifying and only store an IPFS hash? inspired by a twitter conversation with @LefterisJP