Skip to content

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-keys fallback. WKD is the only external anchor implemented.
  • Nothing is cached. Resolve performs its I/O on every call, so cache above the resolver if that matters.
  • Key submission is not implemented. WriteWKDTree can write a submission-address file 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.