A Web Key Directory (WKD) lets a domain publish the OpenPGP key for each of its addresses at a fixed https URL, so a key for [email protected] is fetched from example.org itself. WkdClient builds that URL and fetches it; KeyDiscovery.ForAddress keeps only the certificate that carries the address you asked for. The program below looks up a real, public WKD entry and saves the key it finds.
dotnet new console -o WkdLookup && cd WkdLookup dotnet add package RecursiveAI.OneCrypto.OpenPgp --version 0.3.0-preview.2 dotnet add package RecursiveAI.OneCrypto.Online --version 0.3.0-preview.2
OneCrypto is a preview, so dotnet add package needs --version or --prerelease. RecursiveAI.OneCrypto.Online does the fetching. Put this in Program.cs and run dotnet run, or dotnet run -- name@domain for another address:
using RecursiveAI.OneCrypto.Online; using RecursiveAI.OneCrypto.OpenPgp; string address = args.Length > 0 ? args[0] : "[email protected]"; DateTimeOffset now = DateTimeOffset.UtcNow; // Ask the address's domain for its Web Key Directory entry, over https using var transport = new HttpTransport(); var wkd = new WkdClient(new Fetcher(transport)); WkdLookupResult lookup = await wkd.LookupAsync(address); foreach ((WkdMethod method, KeyFetchResult attempt) in lookup.Attempts) { string why = attempt.IsSuccess ? "" : ", " + attempt.Reason; Console.WriteLine($"{method}: {attempt.Outcome}{why}\n {attempt.Uri}"); } if (!lookup.IsSuccess) { Console.WriteLine($"No key: {lookup.Reason}"); return; } // Keep only certificates that carry this address, with every other User ID removed DiscoveredKeys found = KeyDiscovery.ForAddress(lookup.Body, address); Console.WriteLine($"{found.Certificates.Count} certificate(s), " + $"{found.Dropped.Count} dropped, " + $"{found.ThirdPartySignaturesRemoved} third-party signature(s) removed"); foreach (PgpCertificate cert in found.Certificates) { string userIds = string.Join(", ", cert.UserIds); Console.WriteLine($"{Convert.ToHexString(cert.Fingerprint)} {userIds}"); foreach (PgpKey key in cert.Keys) { // Valid: its binding signatures check out; not expired or revoked now. // That is not the same as "this key belongs to the person". PgpKeyState state = key.StateAt(now); string status = state.IsValid ? "valid" : $"not valid ({state.Reason})"; string role = key.IsPrimary ? "primary" : "subkey "; Console.WriteLine($" {role} {key.Algorithm} {status}, {state.Usage}"); } // A subkey this library can't decode is skipped and listed; the rest is kept foreach (var skipped in cert.SkippedSubkeys) Console.WriteLine($" skipped v{skipped.Version} subkey: {skipped.Reason}"); File.WriteAllBytes($"{address}.asc", cert.ExportPublic(armor: true)); }
Advanced: FetchFailed, the name openpgpkey.kernel.org did not resolve https://openpgpkey.kernel.org/.well-known/openpgpkey/kernel.org/hu/pf113mfnx1f3eb1yiwhsipa91xfc7o4x?l=torvalds Direct: Found https://kernel.org/.well-known/openpgpkey/hu/pf113mfnx1f3eb1yiwhsipa91xfc7o4x?l=torvalds 1 certificate(s), 0 dropped, 0 third-party signature(s) removed ABAF11C65A2970B130ABE3C479BE3E4300411886 Linus Torvalds <[email protected]> primary Rsa valid, Certify, Sign subkey Rsa valid, EncryptCommunications, EncryptStorage
This ran on 2 November 2026 against kernel.org’s published directory. WKD has two URL forms. The client always tries the “advanced” one on the openpgpkey. subdomain first, and falls back to the “direct” one on the domain itself only when that name doesn’t resolve, as happened here. A server that is down or answers 404 counts as an answer, as the draft requires. The path is the SHA-1 of the lower-cased local part in z-base-32. An address with no key ends like this:
Advanced: FetchFailed, the name openpgpkey.kernel.org did not resolve https://openpgpkey.kernel.org/.well-known/openpgpkey/kernel.org/hu/4kwkd9sts53os8uh3a7xka43cjhwokym?l=no-such-user Direct: NotFound, the server answered 404 https://kernel.org/.well-known/openpgpkey/hu/4kwkd9sts53os8uh3a7xka43cjhwokym?l=no-such-user No key: the server answered 404
A key with a subkey OneCrypto can’t decode still loads: the subkey is skipped and listed in SkippedSubkeys, and the primary key and the other subkeys stay usable. GnuPG’s own address carries a LibrePGP version 5 Kyber subkey:
dotnet run -- [email protected]Advanced: Found https://openpgpkey.gnupg.org/.well-known/openpgpkey/gnupg.org/hu/nq6t9teux7edsnwdksswydu4o9i5es3f?l=wk 1 certificate(s), 0 dropped, 1 third-party signature(s) removed AEA84EDCF01AD86C4701C85C63113AE866587D0A [email protected] primary EdDsaLegacy valid, Certify, Sign subkey EdDsaLegacy valid, Sign subkey Ecdh valid, EncryptCommunications, EncryptStorage skipped v5 subkey: The input is truncated: curve oid length (offset 0).
The saved [email protected] is a PgpCertificate like any other, ready for Recipients in Encrypt and decrypt a file with PGP or Certificates in Sign and verify with PGP, once you have decided to rely on it.
What a WKD lookup proves
WKD tells you which key the address’s domain publishes for it, served over https. That is all. OneCrypto checks what it can:
- every request and redirect stays on https, and the address guard refuses private addresses, because the domain comes from outside;
- the answer is not trusted to be what was asked for:
ForAddresskeeps only a certificate with a User ID for this exact address, drops every other User ID, removes third-party certifications and never accepts secret key material; validmeans the key’s own binding signatures check out and it is not expired or revoked at the time you pass.
None of that says the key belongs to the person. Whoever runs the domain’s web server decides what it serves. Before you rely on a key fetched by WKD (or from a keyserver), compare its fingerprint with the person through another channel, or authenticate it from certifications by keys you already trust: WebOfTrust and ClassicTrust do that in code, and onecrypto pgp authenticate on the command line (OpenPGP trust models).
Check it with GnuPG
GnuPG 2.4.8, with WKD as its only key source, finds the same key, and reads the file the program saved. --locate-keys imports what it finds, so GNUPGHOME points gpg at a new, empty home for this terminal session and your own keyring is left alone.
export GNUPGHOME="$(mktemp -d)" gpg --batch --locate-keys --auto-key-locate clear,wkd [email protected] gpg --show-keys [email protected]
gpg: keybox '/tmp/tmp.gOqoWDSOM7/pubring.kbx' created gpg: /tmp/tmp.gOqoWDSOM7/trustdb.gpg: trustdb created gpg: key 79BE3E4300411886: public key "Linus Torvalds <[email protected]>" imported gpg: Total number processed: 1 gpg: imported: 1 pub rsa2048 2011-09-20 [SC] ABAF11C65A2970B130ABE3C479BE3E4300411886 uid [ unknown] Linus Torvalds <[email protected]> sub rsa2048 2011-09-20 [E] pub rsa2048 2011-09-20 [SC] ABAF11C65A2970B130ABE3C479BE3E4300411886 uid Linus Torvalds <[email protected]> sub rsa2048 2011-09-20 [E]
GnuPG marks the User ID [ unknown] for the same reason: it fetched the key, and nothing it holds vouches for it yet.
Limits
- Not audited. OneCrypto (cryptography, not cryptocurrency) has not been independently audited. It is a preview: 0.3.0-preview.2, and the API can change.
- Skipped subkeys are kept, not used. A subkey that doesn’t decode, or has a key version OneCrypto doesn’t read, is left out of
Keysand listed inSkippedSubkeyswith the reason; its packets are kept, so the key writes back byte for byte. A primary key that doesn’t decode still fails withOpenPgpParseException. - Lookup only. Publishing through the Web Key Directory Update Protocol (sending the key by mail) is not implemented;
GetSubmissionAddressAsynconly reads the address. Responses are capped at 2 MiB, and nothing is cached unlessWkdOptions.UseCacheis set. WkdOptions.RequirePolicychecks the domain’s policy file first and stops when it has none;FallbackOnAnyAdvancedFailuretries the direct form after any advanced failure, not only an unresolved name.HkpClient(keyservers, legacy HKP API only) andVksClient(keys.openpgp.org) fetch the same way; pass their responses throughKeyDiscoverytoo. A key from either proves no more than one from WKD.
The WKD, HKP and VKS clients in full: Online clients and transport. Filtering: OpenPGP, Key discovery.