Configuration fields¶
Every exported configuration struct in the module, field by field. There is no config file and no environment-variable reading anywhere in this module: each struct is populated by the calling program.
verify.KeyResolverConfig — where trust comes from¶
Passed to verify.BuildKeyResolver together with zero or more armoured
embedded keys.
| Field | Type | Default when zero | Notes |
|---|---|---|---|
KeySource |
string |
"both" |
Accepts "embedded", "external", "both". Lower-cased and trimmed before matching, so " Both " works. Anything else is an error. |
ExternalKeyEmail |
string |
none | The release address whose WKD directory holds the external key. Required for "external". A whitespace-only value counts as unset. |
RequireExternalCrosscheck |
bool |
false |
Becomes CompositeResolver.RequireAll. true makes a child-resolver error fatal. |
HTTPClient |
*http.Client |
&http.Client{Timeout: 30 * time.Second} |
Used for WKD fetches. The default sets a timeout and nothing else — no redirect policy, no TLS floor. |
Logger |
*slog.Logger |
none — warnings are discarded | Only ever written to by CompositeResolver when RequireAll is false. Nothing else in the module logs. |
What BuildKeyResolver returns for each combination¶
KeySource alone does not determine the result. What you get also depends on
whether you supplied embedded keys and an email:
KeySource |
Embedded keys | ExternalKeyEmail |
Result |
|---|---|---|---|
"embedded" |
yes | any | Embedded resolver |
"embedded" |
no | any | Error: key_source=embedded requires embedded keys (WithEmbeddedKeys) |
"external" |
any | set | WKD resolver |
"external" |
any | unset | Error: key_source=external requires update.external_key_email |
"both" |
yes | set | CompositeResolver over both — the cross-check runs |
"both" |
yes | unset | Embedded resolver only. No cross-check, no error, no warning. |
"both" |
no | set | WKD resolver only. No cross-check, no error, no warning. |
"both" |
no | unset | Error: key_source=both requires embedded keys, an external key email, or both |
| anything else | any | any | Error: unknown key_source %q (want embedded, external, or both) |
The two bold rows are the trap. "both" is a request, not a guarantee: with
only one anchor configured it degrades to that single anchor silently, and the
resulting resolver behaves exactly like a single-source one. Setting
RequireExternalCrosscheck: true does not rescue it, because that flag is
only read when a CompositeResolver is actually constructed.
To confirm the cross-check is really running, check the resolver's name before
you rely on it. A composite reports composite[embedded,wkd:openpgpkey.<domain>];
a degraded one reports just embedded or wkd:openpgpkey.<domain>.
resolver, err := verify.BuildKeyResolver(cfg, embeddedKey)
if err != nil {
return err
}
if !strings.HasPrefix(resolver.Name(), "composite[") {
return fmt.Errorf("expected a cross-checked resolver, got %q", resolver.Name())
}
Embedded keys are parsed and strength-checked inside BuildKeyResolver, so a
weak or malformed embedded key surfaces there as an error rather than later.
verify.WKDResolverConfig — a single WKD anchor¶
Passed to verify.NewWKDResolver when you compose resolvers by hand.
| Field | Type | Required | Notes |
|---|---|---|---|
Email |
string |
yes | Error wkd: email is required when empty. |
HTTPClient |
*http.Client |
yes | Error wkd: HTTPClient is required when nil. Unlike BuildKeyResolver, this constructor does not substitute a default. |
URLOverride |
string |
no | Test hook. Replaces the scheme and host of both derived URLs while keeping the WKD path and query. |
Email must contain an @ that is neither the first nor the last character.
The domain is lower-cased and then validated: it must be non-empty, contain no
/ or \, contain no .., have no leading or trailing dot, and consist only
of letters, digits, - and .. Anything else is rejected before a request is
made, so a hostile address cannot reshape the request target.
The local part is not lower-cased for the ?l= query parameter — it is
passed through url.QueryEscape as given — but it is lower-cased before
hashing into the hu/ filename, as the WKD specification requires.
verify.CompositeResolver — cross-checking several anchors¶
A struct with exported fields rather than a constructor, so build it directly.
| Field | Type | Default when zero | Notes |
|---|---|---|---|
Resolvers |
[]KeyResolver |
none | Empty gives composite: no resolvers configured at Resolve time. Children run concurrently. |
RequireAll |
bool |
false |
true: any child error aborts with ErrKeyResolverUnavailable. false: child errors are logged and skipped, provided at least one child succeeded. |
Logger |
*slog.Logger |
none | Receives one Warn per failed child, and only when RequireAll is false. |
Fingerprint agreement between the children that succeeded is checked
regardless of RequireAll, and a disagreement always aborts with
ErrKeyResolverMismatch. Agreement is exact and total — the fingerprint sets
must be identical. There is no quorum or m-of-n mode.
With one successful child there is nothing to compare, so no cross-check runs. That is how a fail-open composite behaves during a WKD outage.
openpgpkey.Options — writing a WKD tree¶
Passed to openpgpkey.WriteWKDTree.
| Field | Type | Default when zero | Notes |
|---|---|---|---|
Method |
Method |
MethodAdvanced |
MethodAdvanced ("advanced") serves from openpgpkey.<domain> and includes an extra <domain>/ path segment. MethodDirect ("direct") serves from the domain itself. Any other value is an error. |
SubmissionAddress |
string |
not written | When set it must parse as an RFC 5322 address, or WriteWKDTree fails. |
The domain argument is validated with the same rules as WKDResolverConfig.Email's
domain, which is what keeps a pathological value from escaping
outDir/.well-known/openpgpkey.
openpgpkey.Entry — one address and its keys¶
| Field | Type | Required | Notes |
|---|---|---|---|
Email |
string |
yes | Must parse with net/mail. Only the local part is hashed into the hu/ filename. |
Keys |
[][]byte |
yes | Each element may be ASCII-armoured or already binary; the format is detected per key. An Entry with no keys is an error. |
WriteWKDTree needs at least one Entry. Several entries sharing an Email
merge into one published file, so passing the same address twice is equivalent
to passing one entry holding all the keys.
minisign.Options — comments and timestamp¶
Passed to minisign.Sign and minisign.SignDigest.
| Field | Type | Default when zero | Notes |
|---|---|---|---|
UntrustedComment |
string |
minisign public key <KEYID> |
Not covered by any signature. Never make a trust decision on it. |
TrustedComment |
string |
built from Filename, Time and Project |
Covered by the global signature. Setting it makes Filename, Project and Time irrelevant. |
Filename |
string |
empty base name | Only filepath.Base of it is used. Ignored when TrustedComment is set. |
Project |
string |
omitted | When set, appends project:<Project> and key:<KEYID> to the default trusted comment. Ignored when TrustedComment is set. |
Time |
time.Time |
time.Now() |
Zero means the signature embeds the current time and is not reproducible. Pin it from the release tag or SOURCE_DATE_EPOCH for byte-identical re-signing. |
Both comments are rejected if they contain a carriage return or newline
(ErrInvalidComment) or exceed their length limit (ErrCommentTooLong). The
limits are in Limits, defaults and tunables.
Function arguments worth spelling out¶
A few arguments are not obvious from their names.
openpgpkey.ArmoredPublicKey(signer, name, email, creationTime) bakes
creationTime into both the key packet and its self-signature. Two calls with
the same signer and a different creationTime produce different fingerprints,
so keep it stable: a rotation is a new key, not a new timestamp on the old one.
openpgpkey.DetachSign(signer, publicKey, data, sigCreationTime) takes the
armoured public key you previously minted, and rebuilds the OpenPGP entity from
it so the fingerprint in the signature matches what verifiers expect. The
signer's RSA public half is compared against the one inside publicKey, and a
mismatch is refused. sigCreationTime is the signature's own creation-time
subpacket, independent of the key's; it is truncated to whole seconds because
that is all an OpenPGP v4 signature packet stores.
minisign.SignDigest(signer, digest, opts) requires digest to be exactly
BLAKE2b-512 output (64 bytes). Any other length is refused rather than signed.
Related¶
- Errors — the sentinel each failure above maps to.
- Configure trust — applying these fields.
- The trust model — why the cross-check exists.