Agent Wranglers

DEV-18Required

Publish a checksum and a signature with every download

For every release file people download, publish a SHA-256 checksum file and a signature over it, both made in CI. Install scripts and update commands verify both and stop on any mismatch.

How to do it

Users install command-line tools from release pages, mirrors and curl | sh scripts. Whoever controls that path can swap the file. A checksum alone proves little, because an attacker who changes the file can change the checksum next to it. The signature ties both to your build. Sign in CI, with Sigstore keyless signing or a key held in the secrets store. The NIST Secure Software Development Framework asks producers to give users this information, in task PS.2.1. Add build provenance as Build every release in CI with signed provenance says, and publish the commands from Tell users how to verify a release. Keep files fixed after release, as Make release tags and files immutable says.

Examples

Shell example

cosign verify-blob SHA256SUMS \
  --bundle SHA256SUMS.sigstore.json \
  --certificate-identity "https://github.com/example-shop/cli/.github/workflows/release.yml@refs/tags/v2.4.0" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
sha256sum --check --ignore-missing SHA256SUMS

Python example

import hashlib, sys
from pathlib import Path

def expected_digest(sums_file: Path, name: str) -> str:
    for line in sums_file.read_text().splitlines():
        digest, _, file_name = line.partition("  ")
        if file_name == name:
            return digest
    sys.exit(f"{name} is not listed in {sums_file}. Nothing was installed.")

def verify(path: Path, sums_file: Path) -> None:
    if hashlib.sha256(path.read_bytes()).hexdigest() != expected_digest(sums_file, path.name):
        sys.exit(f"Checksum mismatch for {path.name}. Nothing was installed.")

Go example

func verify(path, sumsFile string) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return err
	}
	actual := fmt.Sprintf("%x", sha256.Sum256(data))
	sums, err := os.ReadFile(sumsFile)
	if err != nil {
		return err
	}
	for _, line := range strings.Split(string(sums), "\n") {
		digest, name, found := strings.Cut(line, "  ")
		if found && name == filepath.Base(path) && digest == actual {
			return nil
		}
	}
	return fmt.Errorf("checksum mismatch for %s, nothing was installed", filepath.Base(path))
}

How to check

The latest release lists SHA256SUMS and its signature beside every file. A test downloads each file, and both the checksum and the signature verify against the expected CI identity. The install script, given a binary with one changed byte, exits non-zero and installs nothing.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/developer-tools/distribution/checksums-and-signatures">Agent Wranglers developer tools, SDKs and CLIs playbook</a>. In code you write, add a comment next to it with the same link.