Documentation
¶
Overview ¶
Package setup signing primitives.
This file defines the cryptographic foundation for Phase 2 of the remote-update-checksum-verification spec: a TrustSet of vetted public keys, a minimum-strength policy that runs at TrustSet construction time, and a verifier for a detached ASCII-armored signature over the checksums manifest.
Trust sets are immutable once constructed. Verification iterates the set and returns nil on the first key that validates the signature; the failure path returns ErrSignatureInvalid without naming the keys tried.
Example ¶
Example shows the consumer (verify) side: load a trust set from the publisher's armored public key, then verify a detached signature over a release manifest. This is the path a project like afmpeg uses to authenticate a dependency's signed release assets.
package main
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"fmt"
"time"
"gitlab.com/phpboyscout/signing/openpgpkey"
"gitlab.com/phpboyscout/signing/verify"
)
func main() {
// In real use the public key is embedded in your binary (or fetched via
// WKD); here we mint one alongside a signature to keep the example
// self-contained.
priv, _ := rsa.GenerateKey(rand.Reader, 3072)
now := time.Unix(0, 0)
pub, _ := openpgpkey.ArmoredPublicKey(priv, "Release", "release@example.test", now)
manifest := []byte("sha256 ffmpeg.wasm 0xc0ffee\n")
sig, _ := openpgpkey.DetachSign(priv, pub, bytes.NewReader(manifest), now)
// Consumer side: trust the publisher's key, verify the manifest.
trust, err := verify.LoadTrustSet(pub)
if err != nil {
fmt.Println("load trust set:", err)
return
}
if err := trust.VerifyManifestSignature(manifest, sig); err != nil {
fmt.Println("verify:", err)
return
}
fmt.Println("verified")
// A tampered manifest is rejected.
tampered := append([]byte("evil "), manifest...)
fmt.Println("tampered accepted:", trust.VerifyManifestSignature(tampered, sig) == nil)
}
Output: verified tampered accepted: false
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // MaxSignatureSize caps the bytes read from a detached signature // download. GPG detached signatures are typically 400-800 bytes; // 8 KiB is 10x headroom. MaxSignatureSize int64 = 8 << 10 // MaxWKDResponseSize caps the bytes read from a WKD public-key fetch. MaxWKDResponseSize int64 = 64 << 10 )
Size bounds on signing-path untrusted inputs. Exported as variables so tool authors with exceptional release layouts can adjust them; the defaults are generous but protect against an unbounded download.
var ( // DefaultRequireSignature is the compile-time default for signature // enforcement. Tool authors should set this to true in main() once // the first signed release is available and clients have received an // embedded key in a prior release (see the N+1 / N+2 / N+3 rollout // in docs/development/phase2-signing-prep.md). DefaultRequireSignature = false // DefaultKeySource is the compile-time default for the key-source // mode. Accepted values: "embedded", "external", "both" (default). DefaultKeySource = "both" // DefaultRequireExternalCrosscheck controls whether a failure to // reach the external key resolver (WKD) aborts the update. Set to // true in locked-down environments where silent fallback to // embedded-only is unacceptable. DefaultRequireExternalCrosscheck = false // DefaultExternalKeyEmail is the email used to derive the WKD URL. // Tool authors should set this in main() to their release email. DefaultExternalKeyEmail = "" )
Compile-time defaults for signature enforcement. Tool authors override these in main() to change behaviour for their binary; downstream config and env vars override per-invocation.
var ( // ErrSignatureInvalid is returned when no key in the trust set // validates the detached signature over the checksums manifest. The // same error is used for malformed signatures: an unparseable // signature is not distinguishable from a forged one for the // caller's purpose. ErrSignatureInvalid = errors.New("signature verification failed") // ErrSignatureMissing is returned when require_signature is true and // no signature asset was found in the release. ErrSignatureMissing = errors.New("signature asset not found in release") // ErrWeakKey is returned when a public key (embedded or fetched) // fails the minimum-strength policy at LoadTrustSet time. ErrWeakKey = errors.New("public key fails minimum-strength policy") // ErrSignatureTooLarge is returned when the signature download // exceeds MaxSignatureSize. ErrSignatureTooLarge = errors.New("signature download exceeds maximum size") // ErrKeyResolverMismatch is returned by CompositeResolver when the // fingerprint sets returned by child resolvers do not match. ErrKeyResolverMismatch = errors.New("key resolvers returned mismatched trust sets") // fetch its keys (network failure, DNS, TLS) and RequireAll or // RequireExternalCrosscheck is true. ErrKeyResolverUnavailable = errors.New("key resolver unavailable") // ErrWKDResponseTooLarge is returned when a WKD response exceeds // MaxWKDResponseSize. ErrWKDResponseTooLarge = errors.New("WKD response exceeds maximum size") )
Sentinel errors raised by the signing primitives. Callers should match on these with errors.Is rather than string comparison.
Functions ¶
func WKDURLs ¶
WKDURLs derives the advanced and direct WKD URLs and the canonical advanced host ("openpgpkey.<domain>") for an email address, per draft-koch-openpgp-webkey-service. The advanced URL is the preferred fetch target; resolvers fall back to the direct URL on 404. Returns an error for malformed input (missing '@', empty local or domain).
Types ¶
type CompositeResolver ¶
type CompositeResolver struct {
Resolvers []KeyResolver
RequireAll bool
// Logger receives fail-open warnings (RequireAll=false). Optional; nil
// disables them. Any *slog.Logger works.
Logger *slog.Logger
}
CompositeResolver wraps an ordered list of KeyResolvers and requires them to agree on the set of key fingerprints. It is the production default for tools that publish both an embedded key and an external (WKD) key: trust is only granted when both sources match, so an attacker must compromise both within the same window.
RequireAll controls the failure mode when a child resolver returns a non-mismatch error (network failure, weak key, malformed data):
- true — any child failure aborts with ErrKeyResolverUnavailable wrapping the first child's error. The fail-closed posture.
- false — child failures are logged at Warn and the composite returns the successful children's trust set, as long as at least one resolver succeeded. The fail-open posture, suitable for tools that tolerate WKD outages.
Fingerprint disagreement among successful resolvers always aborts with ErrKeyResolverMismatch, regardless of RequireAll: a mismatch means at least one trust anchor has been tampered with.
func (*CompositeResolver) Name ¶
func (c *CompositeResolver) Name() string
Name returns "composite[<child1>,<child2>,...]" for diagnostics.
func (*CompositeResolver) Resolve ¶
func (c *CompositeResolver) Resolve(ctx context.Context) (*TrustSet, error)
Resolve runs each child resolver concurrently with the supplied context and applies the RequireAll / fingerprint-agreement policy. Returns ErrKeyResolverUnavailable when no usable trust set survives, or ErrKeyResolverMismatch when multiple resolvers succeed with divergent fingerprint sets.
type KeyResolver ¶
type KeyResolver interface {
// Name returns a short identifier used in logs and diagnostics
// (e.g. "embedded", "wkd:openpgpkey.example.com", "composite").
Name() string
// Resolve returns the trust set for the current update attempt.
// Callers are responsible for caching where appropriate; Resolve
// may perform I/O on every call.
Resolve(ctx context.Context) (*TrustSet, error)
}
KeyResolver returns the TrustSet used to verify release signatures. Implementations may read embedded data, fetch over the network, or combine multiple sources with cross-checks. The interface separates "where the trust anchor comes from" from "how a signature is verified against it", so SelfUpdater can be wired with whichever resolver chain a tool needs without changing verification logic.
func BuildKeyResolver ¶
func BuildKeyResolver(cfg KeyResolverConfig, embeddedKeys ...[]byte) (KeyResolver, error)
BuildKeyResolver maps a KeyResolverConfig and a set of embedded public keys onto a concrete KeyResolver:
- "embedded" → EmbeddedResolver(embeddedKeys). Errors without keys.
- "external" → WKDResolver(email). Errors without an email.
- "both" → CompositeResolver{Embedded, WKD} when both an email and keys are available; degrades to whichever single source is configured; errors when neither is.
Embedded keys are validated against the minimum-strength policy here (via the error-returning constructor) so a malformed or weak key is reported as an error rather than panicking mid-update.
Tool authors who want full control can call this directly and pass the result via WithKeyResolver; the convenience path is to supply raw keys via WithEmbeddedKeys and let SelfUpdater call this from NewUpdater.
func NewEmbeddedResolver ¶
func NewEmbeddedResolver(armoredKeys ...[]byte) KeyResolver
NewEmbeddedResolver returns a KeyResolver backed by the supplied ASCII-armored public keys. The keys are parsed and validated against the minimum-strength policy at construction time; if any key is weak or any input fails to parse, the constructor panics. This matches the spec's "binary refuses to start" invariant for a weak or malformed embedded key: such a problem must surface at startup, not at the first update attempt.
Tool authors typically call this indirectly via an internal trustkeys helper that embeds the project's public keys with //go:embed and constructs the resolver at package init.
func NewWKDResolver ¶
func NewWKDResolver(cfg WKDResolverConfig) (KeyResolver, error)
NewWKDResolver returns a KeyResolver that fetches a public key from the Web Key Directory derived from the supplied email. The HTTPClient is mandatory: callers should supply a client that enforces TLS 1.2+, certificate validation, a request timeout, and an HTTPS-downgrade redirect policy (BuildKeyResolver's default sets a timeout; harden further by injecting your own).
Resolve tries the advanced URL first and falls back to the direct URL only on HTTP 404 — any other failure (network, non-200, TLS, oversize, weak key) is returned to the caller and does not fall through to the direct URL.
type KeyResolverConfig ¶
type KeyResolverConfig struct {
// KeySource is "embedded", "external", or "both". Empty defaults
// to "both".
KeySource string
// ExternalKeyEmail is the release email whose WKD directory holds
// the external key. Required for "external"; enables the WKD leg of
// "both" when set.
ExternalKeyEmail string
// RequireExternalCrosscheck maps to CompositeResolver.RequireAll:
// when true, a WKD fetch failure aborts the update instead of
// falling back to the embedded key.
RequireExternalCrosscheck bool
// HTTPClient is the client used for WKD fetches. When nil, a default
// stdlib client (30s timeout) is used; inject a hardened client for
// custom TLS / proxy / instrumentation.
HTTPClient *http.Client
// Logger receives CompositeResolver fail-open warnings. Optional (nil
// disables them). Any *slog.Logger works; gtb injects one built from its
// own logger via slog.New(<handler>).
Logger *slog.Logger
}
KeyResolverConfig parameterises BuildKeyResolver. It is deliberately decoupled from Viper so tool authors and tests can construct it directly; SelfUpdater fills it from the resolved update.* config keys.
type TrustSet ¶
type TrustSet struct {
// contains filtered or unexported fields
}
TrustSet is the collection of public keys that can validate update signatures. It is constructed by a KeyResolver per update attempt and is immutable once constructed: LoadTrustSet validates the strength policy at construction time so weak keys never enter the trust set even transiently.
func LoadTrustSet ¶
LoadTrustSet parses one or more ASCII-armored public-key blobs and enforces the minimum-strength policy: Ed25519 keys are accepted, RSA keys are accepted at 3072 bits or stronger, everything else is rejected with ErrWeakKey. Any weak key in the input aborts the load, so weak keys cannot enter the trust set even transiently.
func (*TrustSet) Fingerprints ¶
Fingerprints returns the 40-char uppercase hex fingerprints of every key in the trust set, sorted ascending so two TrustSets can be compared for equality by their fingerprint slices.
func (*TrustSet) VerifyManifestSignature ¶
VerifyManifestSignature verifies an ASCII-armored detached signature against the checksums manifest using any key in the trust set.
Returns nil on the first successful verification. Returns ErrSignatureInvalid (via errors.Is) for an empty, malformed, or non-validating signature; the underlying parser error is wrapped for diagnostics but the sentinel is the contract the caller matches.
The error returned for a failed signature deliberately does not name the keys tried, so a caller that logs only the sentinel does not leak which key in the set rejected the signature.
func (*TrustSet) VerifyManifestSignatureSigner ¶
VerifyManifestSignatureSigner is the fingerprint-returning form of TrustSet.VerifyManifestSignature. On success it returns the 40-char uppercase hex fingerprint of the trust-set key that validated the signature, so callers can record which key was used for the audit trail.
The error contract is identical to VerifyManifestSignature: it returns ErrSignatureInvalid (via errors.Is) for an empty, malformed, or non-validating signature, and the returned fingerprint is empty on any error.
type WKDResolverConfig ¶
WKDResolverConfig parameterises NewWKDResolver. Email and HTTPClient are required; URLOverride is for tests pointing at a local TLS server (the canonical scheme+host is replaced, the WKD path+query from Email are preserved).