Skip to content

Verify checksums and signatures

A self-updating tool downloads a binary and runs it. Verifying what it fetched is the difference between an update mechanism and a remote code execution mechanism.

This module supplies the retrieval half, getting the manifest and signature bytes from the forge. Verifying them is the consumer's job.

Two routes to the same bytes

Most releases ship a checksums.txt as an ordinary asset, so the default route is to find it by name in the asset list. Nothing special is required.

Some sources cannot do that. The direct provider composes a checksum URL from a template that may point outside any asset listing. For those, a provider opts into ChecksumProvider:

var cp forge.ChecksumProvider
if forge.As(provider, &cp) {
    manifest, err := cp.DownloadChecksumManifest(ctx, rel, forge.DefaultMaxChecksumsSize)
    switch {
    case err == nil:
        return manifest, nil
    case errors.Is(err, forge.ErrNotSupported):
        // opted out for this configuration — fall through
    default:
        return nil, err
    }
}

// default route: locate checksums.txt among the release assets

Use forge.As rather than a bare type assertion. A decorator that forwards only the four required Provider methods strips the optional interfaces, and since absence is a graceful fallback, verification would quietly downgrade instead of failing.

SignatureProvider mirrors it exactly for a detached signature.

ErrNotSupported is not a failure

Treat "does not implement the interface" and "returned ErrNotSupported" identically: fall back. Only a different error is fatal. Getting this wrong turns a working provider into a broken one for every release where the capability happens to be unconfigured.

Bound the read, always

Both methods take a maxBytes cap. Pass one:

forge.DefaultMaxChecksumsSize  // 1 MiB
forge.DefaultMaxSignatureSize  // 1 MiB

These are defaults rather than mandates. Raise them if your tool genuinely ships larger artefacts. The provider enforces whatever you pass, because it is the only code that sees the response before it is buffered; a cap applied afterwards has already let the bytes into memory.

A manifest for a typical multi-OS release is around 1 KiB, so the default is roughly a thousandfold headroom while still stopping a hostile server streaming indefinitely.

What this does not cover

forge supplies the retrieval half only. Comparing a hash, verifying a signature against a key you already trust, and deciding what to do when either fails are all yours, because a provider returning a manifest has checked nothing. See what forge does not do.

Checksums also prove nothing about who wrote the manifest: an attacker who can replace an asset can usually replace checksums.txt beside it. Closing that gap needs the detached signature.