---
title: "Look up a PGP key by email (WKD) in C#"
url: https://recursiveai.org/onecrypto/cookbook/wkd-key-lookup/
description: "Look up a PGP (OpenPGP) public key by email address with WKD (Web Key Directory) in C#: fetch it over https from the address’s own domain, keep only the matching User ID, and compare with gpg --locate-keys. What WKD does and doesn’t prove. Runnable code with OneCrypto 0.3.0-preview.2."
version: "0.3.0-preview.2"
---

# Look up a PGP key by email (WKD) in C#

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 `someone@example.org` 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.

```shell
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:

```csharp
using RecursiveAI.OneCrypto.Online;
using RecursiveAI.OneCrypto.OpenPgp;

string address = args.Length > 0 ? args[0] : "torvalds@kernel.org";
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));
}
```

```text
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 <torvalds@kernel.org>
  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:

```text
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:

```shell
dotnet run -- wk@gnupg.org
```

```text
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  wk@gnupg.org
  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 `torvalds@kernel.org.asc` is a `PgpCertificate` like any other, ready for `Recipients` in [Encrypt and decrypt a file with PGP](https://recursiveai.org/onecrypto/cookbook/pgp-encrypt-decrypt-file/) or `Certificates` in [Sign and verify with PGP](https://recursiveai.org/onecrypto/cookbook/pgp-sign-verify/), 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: `ForAddress` keeps 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;
- `valid` means 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](https://recursiveai.org/onecrypto/docs/openpgp-trust/)).

## 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.

```shell
export GNUPGHOME="$(mktemp -d)"
gpg --batch --locate-keys --auto-key-locate clear,wkd torvalds@kernel.org
gpg --show-keys torvalds@kernel.org.asc
```

```text
gpg: keybox '/tmp/tmp.gOqoWDSOM7/pubring.kbx' created
gpg: /tmp/tmp.gOqoWDSOM7/trustdb.gpg: trustdb created
gpg: key 79BE3E4300411886: public key "Linus Torvalds <torvalds@kernel.org>" imported
gpg: Total number processed: 1
gpg:               imported: 1
pub   rsa2048 2011-09-20 [SC]
      ABAF11C65A2970B130ABE3C479BE3E4300411886
uid           [ unknown] Linus Torvalds <torvalds@kernel.org>
sub   rsa2048 2011-09-20 [E]

pub   rsa2048 2011-09-20 [SC]
      ABAF11C65A2970B130ABE3C479BE3E4300411886
uid                      Linus Torvalds <torvalds@kernel.org>
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 `Keys` and listed in `SkippedSubkeys` with the reason; its packets are kept, so the key writes back byte for byte. A primary key that doesn’t decode still fails with `OpenPgpParseException`.
- **Lookup only.** Publishing through the Web Key Directory Update Protocol (sending the key by mail) is not implemented; `GetSubmissionAddressAsync` only reads the address. Responses are capped at 2 MiB, and nothing is cached unless `WkdOptions.UseCache` is set.
- `WkdOptions.RequirePolicy` checks the domain’s policy file first and stops when it has none; `FallbackOnAnyAdvancedFailure` tries the direct form after any advanced failure, not only an unresolved name.
- `HkpClient` (keyservers, legacy HKP API only) and `VksClient` (keys.openpgp.org) fetch the same way; pass their responses through `KeyDiscovery` too. A key from either proves no more than one from WKD.

The WKD, HKP and VKS clients in full: [Online clients and transport](https://recursiveai.org/onecrypto/docs/online/#certificate-discovery-wkd-hkp-and-vks). Filtering: [OpenPGP, Key discovery](https://recursiveai.org/onecrypto/docs/openpgp/#key-discovery-and-autocrypt).
