What this module does not do¶
A list of the things signing deliberately does not do, does not support, or
does less of than the name suggests. Every entry is a real limit in the code,
not a gap waiting to be filled — where something is genuinely planned, it says
so.
If you are asking "can I…" and the answer here is no, that is the answer. The absence of a feature is rarely written down, which makes it the hardest thing to confirm from documentation.
It does not generate keys¶
There is no GenerateKey anywhere in the module. Every entry point takes a
crypto.Signer you already hold. Generating an RSA or Ed25519 key pair, or
provisioning one inside a KMS, happens before this module is involved — with
openssl, your cloud provider's tooling, or a CLI built on top of this.
openpgpkey mints an OpenPGP public key from a signer you supply, which is a
different thing: it wraps an existing key in the OpenPGP packet structure, User
ID and self-signature that a verifier requires.
It does not ship a command-line tool¶
The module builds no binaries. Where the documentation mentions gtb sign or
gtb keys mint, that is
go-tool-base — a separate
project that wraps these libraries in a CLI. Installing this module gives you
packages to import, nothing to run.
It does not read configuration or environment variables¶
Every setting is a plain Go struct field, populated by the calling program. There is no config-file loader, no env-var prefix, no flag parsing. That is deliberate: keeping a flag library out of the core is what lets a verify-only consumer import this with almost no dependency graph.
The four verify.Default* variables look like an exception and are not:
nothing in this module reads them. They are compile-time knobs for a downstream
self-updater to consult. See
Limits, defaults and tunables.
It does not download releases or check asset hashes¶
verify verifies a detached signature over manifest bytes you already have. It
does not fetch the manifest, fetch the signature, parse the manifest, or hash
any asset. Comparing each downloaded asset against its manifest line is the
caller's job, and skipping it means the signature has proved nothing about the
assets themselves.
The only network call anywhere in the module is the WKD key fetch.
verify.ErrSignatureMissing, verify.ErrSignatureTooLarge and
verify.MaxSignatureSize exist for a downstream release-fetcher and are never
raised or read here.
Key types it refuses¶
| Path | Accepts | Refuses |
|---|---|---|
OpenPGP minting and signing (openpgpkey) |
RSA only | Ed25519 and everything else, with ErrUnsupportedKeyType |
minisign (minisign) |
Ed25519 only | RSA and everything else, with its own ErrUnsupportedKeyType |
Trust sets (verify) |
Ed25519, and RSA at 3072 bits or more | DSA, ElGamal, ECDH, ECDSA, X25519, X448, Ed448, RSA-encrypt-only, RSA under 3072 bits, and any algorithm a future go-crypto adds |
The local backend |
RSA (PKCS#1 or PKCS#8) and Ed25519 (PKCS#8) | ECDSA and anything else, with local.ErrUnsupportedKeyType |
Two consequences worth spelling out. One key cannot serve both signing
paths — publishing OpenPGP manifests and minisign artefacts means holding two
keys. And verify accepts Ed25519 keys into a trust set even though
openpgpkey cannot mint one: an Ed25519 OpenPGP key produced elsewhere
verifies here perfectly well.
ECDSA is refused everywhere on purpose. Nothing in this estate verifies ECDSA, and accepting a key type no consumer can check would be a trap rather than a feature.
The local backend will not read an encrypted PEM¶
local.ErrEncryptedPEMUnsupported. The standard library exposes no clean
PKCS#8 decryption path, so the backend refuses rather than half-supporting it.
Decrypt out of band first, encrypt the file at the filesystem layer (LUKS,
FileVault, age), or use a KMS backend where the key never lands on disk at
all. Encrypted-PKCS#8 support would be purely additive and is slated for a
later version if a real consumer asks.
The backend also reads whatever path it is handed, following symlinks, with no canonicalisation. That is appropriate for an operator-supplied path and worth knowing if the path could originate anywhere less trusted.
key_source: "both" does not guarantee a cross-check¶
This is the sharpest edge in the module.
Asking for "both" requests embedded and WKD anchors with a fingerprint
cross-check between them. But BuildKeyResolver degrades to whichever single
source is actually configured. Supply embedded keys and no
ExternalKeyEmail, and you get a plain embedded resolver: no cross-check, no
error, no warning. RequireExternalCrosscheck: true does not rescue it, because
that flag is only consulted when a composite resolver is genuinely built.
The resulting configuration is not wrong — a single anchor is a legitimate
choice — but it is not the guarantee the name implies. Check
resolver.Name() for a composite[…] prefix if you need to know which you
have. The full matrix is in
Configuration fields.
Cross-checking has no quorum mode¶
CompositeResolver requires the fingerprint sets from every successful child
to be identical. There is no m-of-n, no majority vote, no "two of these
three must agree". Any divergence aborts with ErrKeyResolverMismatch.
And with only one successful child there is nothing to compare, so no cross-check runs at all. That is exactly what a fail-open composite does during a WKD outage: it degrades to a single anchor and logs a warning, if you gave it a logger.
WKD support stops well short of the whole specification¶
- Only the two URL layouts are tried: advanced first, then direct, and the direct URL only when the advanced one returns HTTP 404. Any other failure is returned immediately.
- There is no keyserver, HKP, DANE or
gpg --recv-keysfallback. WKD is the only external anchor implemented. - Nothing is cached.
Resolveperforms its I/O on every call, so cache above the resolver if that matters. - Key submission is not implemented.
WriteWKDTreecan write asubmission-addressfile advertising where submissions go, but nothing here sends or services one. - The default HTTP client sets a 30-second timeout and nothing else. It applies
no redirect policy, so a redirect from the WKD host to an
http://URL will be followed — the HTTPS check covers the initial URL only. Inject your own client to close that, along with any TLS floor or certificate pinning you want.
Revocation and rotation are not managed¶
On the OpenPGP path, go-crypto does enforce what the key material itself
says: a revoked primary key, a revoked identity, a revoked or expired signing
subkey, an expired key or an expired signature all fail verification, judged
against the current clock. All of them surface as ErrSignatureInvalid.
What the module does not provide is any policy around that. There is no
revocation-certificate distribution, no key-rotation workflow, no way to
distrust a key short of rebuilding with a different embedded set, and no
trust-set mutation — a TrustSet is immutable once constructed. Deciding when
to rotate, and getting that decision to your users, is yours.
On the minisign path there is no expiry or revocation at all: the format cannot express either.
It does not bound the data it signs¶
openpgpkey.DetachSign reads its entire io.Reader into memory. There is no
size cap and no streaming path. For a checksums manifest of a few kilobytes
that is fine; for input you did not produce, wrap the reader in an
io.LimitReader yourself.
By contrast minisign.Sign does stream, so artefact size is not a concern
there.
Registration is one-way and panics on error¶
signing.Register panics on a nil backend, an empty name, or a duplicate name,
rather than returning an error. There is no Unregister; ResetForTesting
exists for tests and must not be called in production. Blank-importing a
backend and registering a configured instance of the same backend panics on
the duplicate.
verify.NewEmbeddedResolver likewise panics on a malformed or weak key,
because a bad embedded key must stop a binary at startup rather than at the
first update attempt. BuildKeyResolver is the error-returning route to the
same thing.
It does not protect against these at all¶
- Confidentiality. This is integrity and authenticity. Manifests, signatures and public keys are not secret.
- A compromised build pipeline. An attacker who controls the build that embeds your keys can embed theirs. The WKD cross-check exists precisely because that assumption can fail — see Threat model.
- A compromised verifying host. Nothing a signature check can do survives an attacker who already owns the machine running it.
Related¶
- Threat model — the security boundary in full.
- Configuration fields — what each setting really does.
- Limits, defaults and tunables — the numbers behind these limits.
- Errors — the sentinel each refusal returns.