Skip to content

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:

  1. Integrity — were the assets altered after publication?
  2. 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 with ErrKeyResolverUnavailable. 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