Skip to content

Configure trust

Choose how a TrustSet is sourced: from keys embedded in your binary, from a key published over WKD, or from both — with the two cross-checked so they must agree. The high-level entry point is verify.BuildKeyResolver; for full control you can compose a CompositeResolver directly.

For the reasoning behind cross-checking independent sources, read The trust model.

Sources at a glance

KeySource Anchors used Network?
"embedded" keys baked into the binary no
"external" a key fetched via WKD for an email yes
"both" (default) embedded and WKD, must agree yes

Build a resolver from config

BuildKeyResolver takes a KeyResolverConfig plus any embedded armoured keys, and returns a KeyResolver. Call Resolve to obtain a verified TrustSet:

resolver, err := verify.BuildKeyResolver(verify.KeyResolverConfig{
    KeySource:                 "both", // "embedded" | "external" | "both"
    ExternalKeyEmail:          "[email protected]",
    RequireExternalCrosscheck: true,
}, embeddedKey)
if err != nil {
    return err
}

trust, err := resolver.Resolve(ctx)
if err != nil {
    return err // see the error cases below
}
// use trust.VerifyManifestSignature(...) as usual

KeySource defaults to "both" when empty. ExternalKeyEmail is required for "external", and enables the WKD leg of "both" when set.

Fail-closed vs fail-open

RequireExternalCrosscheck maps to CompositeResolver.RequireAll and controls how a child resolver error (such as a WKD fetch failure) is handled:

  • true (fail-closed) — any child error aborts with verify.ErrKeyResolverUnavailable. Use this when the external cross-check is mandatory: a WKD outage must stop the release, not silently downgrade trust to the embedded key alone.
  • false (fail-open) — child errors are logged at Warn (if you injected a *slog.Logger) and the surviving trust set is returned, as long as at least one resolver succeeded. Use this when WKD is a best-effort reinforcement of an already-trusted embedded key.

A fingerprint disagreement between sources is different: it always aborts with verify.ErrKeyResolverMismatch, regardless of RequireExternalCrosscheck. Sources that disagree are never reconciled — that is the whole point of the cross-check.

trust, err := resolver.Resolve(ctx)
switch {
case errors.Is(err, verify.ErrKeyResolverMismatch):
    // embedded and WKD keys disagree — treat as a possible compromise
    return err
case errors.Is(err, verify.ErrKeyResolverUnavailable):
    // no usable source (fail-closed, or every source failed)
    return err
case err != nil:
    return err
}

Inject an HTTP client and logger

WKD fetches use the *http.Client you supply; nil falls back to a stdlib client with a 30-second timeout. The *slog.Logger receives fail-open warnings; nil disables logging entirely.

resolver, err := verify.BuildKeyResolver(verify.KeyResolverConfig{
    KeySource:                 "both",
    ExternalKeyEmail:          "[email protected]",
    RequireExternalCrosscheck: false, // fail-open: warnings go to Logger
    HTTPClient:                hardenedClient, // custom TLS / proxy / instrumentation
    Logger:                    slog.New(handler),
}, embeddedKey)

Compose resolvers directly

For arrangements BuildKeyResolver does not cover — multiple WKD emails, a bespoke KeyResolver, or precise control over which resolvers must agree — build a CompositeResolver yourself. Any type implementing the KeyResolver interface (Name() string, Resolve(ctx) (*TrustSet, error)) can be a child.

embedded := verify.NewEmbeddedResolver(embeddedKey)

wkd, err := verify.NewWKDResolver(verify.WKDResolverConfig{
    Email:      "[email protected]",
    HTTPClient: hardenedClient, // nil → stdlib 30s client
})
if err != nil {
    return err
}

composite := &verify.CompositeResolver{
    Resolvers:  []verify.KeyResolver{embedded, wkd},
    RequireAll: true, // fail-closed on a child error
    Logger:     slog.New(handler),
}

trust, err := composite.Resolve(ctx)

The same semantics apply: agreement is mandatory (mismatch always aborts), and RequireAll governs only the response to a child error. To inspect the WKD URLs a resolver will query, use verify.WKDURLs(email).

See also