Skip to content

Publish keys via WKD

The Web Key Directory (WKD) lets consumers fetch your signing public key over HTTPS from your domain, keyed by the signing email address. It is the publisher side of the external trust anchor that Configure trust consumes — together they give the embedded-key + WKD cross-check that the trust model relies on.

This guide writes a static WKD tree you can serve from any web host.

Generate the tree

openpgpkey.WriteWKDTree writes a complete, RFC-compliant directory layout for one or more email→keys bindings:

import "gitlab.com/phpboyscout/signing/openpgpkey"

paths, err := openpgpkey.WriteWKDTree(
    "public",                // output directory
    "example.com",           // the domain the keys are served from
    openpgpkey.Options{
        // Method defaults to MethodAdvanced (dedicated-subdomain layout).
        SubmissionAddress: "[email protected]", // optional
    },
    openpgpkey.Entry{
        Email: "[email protected]",
        Keys:  [][]byte{armoredPub}, // armored or binary; auto-detected
    },
)
if err != nil {
    return err
}
// `paths` lists every file written.

Entry.Keys accepts the armoured public key you minted with openpgpkey.ArmoredPublicKey (see Sign with the local backend or Sign with AWS KMS). Multiple Entry values sharing an Email merge into one published file.

The resulting layout

For the advanced method (the default) and domain example.com:

public/.well-known/openpgpkey/example.com/
├── policy                 (zero-byte, RFC-required)
├── submission-address     (only if Options.SubmissionAddress set)
└── hu/<z-base-32-hash>    (one per unique email — binary OpenPGP packets)

The direct method (Options{Method: openpgpkey.MethodDirect}) drops the <domain>/ level — use it when serving from the domain itself rather than a dedicated openpgpkey. subdomain.

To find the hashed filename for an address (e.g. to verify hosting):

hash, _ := openpgpkey.WKDHash("[email protected]") // the hu/<hash> local part

Serve it

Publish the .well-known/openpgpkey/… tree under HTTPS so that, for the advanced method, this resolves:

https://openpgpkey.example.com/.well-known/openpgpkey/example.com/hu/<hash>

WKD mandates HTTPS; the consumer's fetch enforces TLS (inject a hardened *http.Client on the verify side if you need stricter transport — see Configure trust).

Note on the WKD hash. The hu/<hash> local-part hash is SHA-1 by WKD wire-format mandate (draft-koch §3.1) — it is an addressing scheme, not a security control. Authenticity comes from the OpenPGP signature verification, not the filename.

How consumers use it

A consumer points verify.KeyResolverConfig.ExternalKeyEmail (or a WKDResolver) at [email protected]; the resolver fetches the hu/ file, and a CompositeResolver cross-checks it against the embedded key. See Configure trust.

See also