The trust model¶
This page explains why signing verifies releases the way it does. It is
background reading; for the how, see Configure trust
and Verify a release.
Two questions, separated¶
Verifying a release answers two distinct questions:
- Integrity — were the assets altered after publication?
- Authenticity — is the signer the party we actually trust?
signing separates these cleanly. A detached OpenPGP signature over a
checksums manifest answers integrity: the manifest lists a checksum per asset,
and one signature covers the whole manifest. Verifying the signature proves the
manifest is intact; comparing each asset's hash to its manifest line proves the
asset is intact. Authenticity is the harder question, and it is where the
composite trust model comes in.
Why a single key is not enough¶
If trust rested on one public key from one source, compromising that single source would be enough to forge a "valid" release. Embed the key in the binary and an attacker who controls the build pipeline can swap it. Fetch it from the network and an attacker who controls that endpoint (or its TLS) can swap it.
The model therefore draws trust from two independent anchors:
- Embedded keys — armoured public keys baked into the consuming binary at build time. Tamper-resistant once shipped, but only as trustworthy as the build that embedded them.
- WKD (Web Key Directory) — the publisher's key fetched over HTTPS from a well-known path derived from their email address. Independently controlled by whoever holds the publisher's domain and mail/web infrastructure.
These anchors fail to different adversaries. The embedded key is a build-time artefact; the WKD key is a live network resource. An attacker must subvert both within the same window to forge a release that the composite check accepts.
Cross-check: trust requires agreement¶
A CompositeResolver runs its child resolvers and compares the fingerprints
they return. Trust is granted only when the successful sources agree. If the
embedded key and the WKD key disagree, resolution aborts with
ErrKeyResolverMismatch — and this happens regardless of any other setting. A
disagreement is treated as a possible compromise, never reconciled or silently
resolved in favour of one side.
This is the core security property: the cross-check turns "compromise one source" into "compromise both sources, consistently, at once".
Fail-open vs fail-closed¶
Agreement is non-negotiable, but availability is a policy choice the consumer
makes through RequireAll (surfaced as RequireExternalCrosscheck in
KeyResolverConfig). It governs only what happens when a child resolver
errors — for example, a WKD fetch times out:
RequireAll: true(fail-closed) — any child error aborts withErrKeyResolverUnavailable. A WKD outage stops the release rather than letting trust quietly collapse back to the embedded key alone. Choose this when the external cross-check is a hard requirement.RequireAll: false(fail-open) — child errors are logged at Warn (via the optional*slog.Logger) and the surviving trust set is returned, provided at least one resolver succeeded. Choose this when WKD reinforces an already-trusted embedded key and you would rather degrade than block on a transient network failure.
The distinction is deliberate: a disagreement is a security signal and always aborts; an error is an availability event whose handling you tune to your threat tolerance.
Why RSA-3072¶
Every key entering a trust set must clear a minimum-strength policy: RSA keys
must be at least 3072 bits. The check runs at trust-set construction
(LoadTrustSet, and the resolvers that build on it), so a weak key fails fast —
during your build or startup — with ErrWeakKey, rather than silently providing
weaker-than-expected protection in the field.
3072-bit RSA is the floor for keys expected to remain trustworthy for years (broadly aligned with a 128-bit security level). Enforcing it at the boundary means the rest of the system can assume every trusted key meets the bar. WKD support is currently RSA-only, so the policy applies uniformly across embedded and external anchors.
See also¶
- Threat model — what this design does and does not protect against.
- Dependency inversion — why signing backends are injected, keeping the verifier's dependency graph small.
- Configure trust — applying these choices in code.