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
Related rules
- OSS-19 Tell users how to verify a release in Open source maintenance
- OSS-18 Build every release in CI with signed provenance in Open source maintenance
- ENG-42 Make release tags and files immutable in Engineering
- DEV-19 Pin the checksum in every package manager manifest in Developer tools, SDKs and CLIs
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.