Thank you. This could be pinned as TL;DR.
hey @SamWilsn I just found this post and this is pretty much what I ended up building this past year.
thurin.id is a registry where an address publishes its pgp key with a clearsigned statement binding the key to the address. the whole armored key is on-chain, so a plain eth_call returns it. no logs, no indexer, no ipfs. in your reply to @blastslot you said off-chain storage makes immutability an illusion because the contract can’t see the content. that’s why the key is stored whole: the contract holds the bytes it vouches for, and after a revoke a fetch returns nothing. the contract never parses pgp. verification happens off-chain in a library and a cli.
the keyserver reads the chain on request and speaks hkp:
echo "keyserver hkps://keys.thurin.id" >> ~/.gnupg/dirmngr.conf
gpg --search-keys 08B9374FDFBEC67EFFA24E669D3D86E35361EF7B
gpg: data source: https://keys.thurin.id:443
(1) Thurin Labs <hello@thurin.id>
EDDSA key 0x9D3D86E35361EF7B, created: 2026-09-12, expires: 2028-09-11
with this, git log --show-signature, email clients, and anything else that shells out to gpg reads ethereum without knowing it. every commit and release in the thurinlabs repos verifies this way. npx @thurinlabs/thurin keyserver runs the same thing against your own node.
on ENS, I landed where you did. names expire and whoever controls a name can repoint it, so the trust path is name → address → registry. there’s an optional id.thurin text record that holds the fingerprint the name’s address claims, but it’s a discovery hint for ens viewers, not part of the chain of trust. if it disagrees with the registry, the registry wins. thurin ens check compares the two, and thurin.id shows it as a badge.
another fun thing added: keys can also carry proofs, github, dns, codeberg, farcaster, checked client-side against each platform. our company key has example proofs: Thurin.id — Prove more. Reveal less.
keys here don’t need an email either. a user id can be just a name, and whether you attest from the browser or the cli, email user ids are stripped before publishing unless you choose to keep them. a keyserver full of addresses is what got sks poisoned and spammed. so email search returns nothing on purpose, and a key is there because its owner claimed it, not because someone uploaded it.
registry 0x9302E02e2869e129aC8516fE5eFFd51EA3082c09, same address on sepolia. contract, cli, and library at https://github.com/thurinlabs. docs at https://docs.thurin.id. would love your take on it.
Thanks @SamWilsn for laying out the registry design. I think the most proportionate boundary is to keep the contract focused on recording an account’s assertion and a content-addressed reference, while leaving OpenPGP packet parsing and certificate verification to clients.
A few lifecycle details may be worth making normative before the interface is standardized:
-
Certification IDs: Define
CertIdas a unique, never-reused identifier for a certification generation. A location-only change could preserve the ID; changing the public key orvalidBeforecould supersede the old certification and create a new ID. -
Expiry and revocation: Treat expiry as time-derived (
block.timestamp >= validBefore), distinct from explicit revocation. An expired certification should not become active again; a later certification would receive a new ID. It would also help to specify whether explicit revocation is allowed after expiry. -
History and reads: Rather than having getters revert whenever no valid certification exists, consider exposing the current ID and a status-aware historical lookup. That lets clients distinguish “never registered,” “expired,” “revoked,” and “superseded,” and reconstruct the current state without guessing from partial data.
-
Events: Specify which event is emitted for each state change. For replacement, one possible convention is a revocation event for the old ID with a
Supersededreason, followed by a certification event for the new ID. A location-only update should have its own event and preserve the ID. Expiry need not emit an event; clients can derive it fromvalidBefore. -
Permit signatures: If permits are supported, the signed message should bind the chain and registry, operation, certifier, kind, relevant certification data, a per-certifier nonce, and a deadline. Updates and revocations should identify the target certification ID so stale signatures cannot affect a later generation. It may also be useful to specify support for contract accounts through ERC-1271.
One important scope distinction: an active registry entry means only that the account’s assertion has not been revoked or superseded and has not expired. It does not establish real-world identity, prove control of the corresponding PGP private key, or validate the OpenPGP certificate. Clients would still need to retrieve and verify the referenced certificate independently.
These choices are proposals rather than cryptographic necessities, but making them explicit should help contract implementers and indexers interpret updates consistently.