Verify downloads with cURL and b2sum
Download into a temporary file, compare its BLAKE2b checksum with a trusted publisher’s value, and
create the destination only when the bytes match. The Bash script below combines cURL and b2sum
without replacing an existing file, then demonstrates the workflow with an actual release checksum.
Get the expected checksum from the publisher
Running b2sum on a download tells you its current checksum. It does not tell you what the checksum
should be. Obtain the expected value for the exact release, architecture, and filename from a
publisher you trust, such as its official HTTPS release page or a signature-verified checksum
manifest. A checksum copied from the same untrusted mirror as the download adds no trustworthy
reference.
GNU b2sum defaults
to BLAKE2b-512: 64 digest bytes written as 128 hexadecimal characters. This script requires that
format. BLAKE2s, a shorter BLAKE2b digest, and SHA-256 are not interchangeable with it. If the
publisher supplies only SHA-256, use a SHA-256 verification workflow instead of inventing a BLAKE2b
reference by hashing the downloaded file yourself.
Check the Linux prerequisites
Use a Linux machine with GNU Bash, cURL with HTTPS support, and GNU Coreutils already installed.
Coreutils provides b2sum, mktemp, ln, and rm. This example was tested with Bash 5.3.15,
cURL 8.22.0, and Coreutils 9.11; it is not a macOS or PowerShell recipe. Check your tools:
bash --version && curl --version && b2sum --version
Run in a directory you control, on a filesystem that supports hard links. The destination is a
filename in that directory, not a path into another directory. Spaces and leading hyphens are
allowed; slashes, line breaks, . and .. are rejected. The file is saved with owner-only read/write
permissions, and an existing file, directory, or symlink is left alone.
Save the complete verification script
Save this as a new file named verify-download.sh. Run it with bash; do not paste the script into
your interactive shell, because its error exits are intended to terminate the saved program.
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
umask 077
if (( $# != 3 )); then
printf 'Usage: bash verify-download.sh HTTPS_URL BLAKE2B_512_HEX FILENAME\n' >&2
exit 2
fi
url=$1
expected=${2,,}
filename=$3
if [[ ! $expected =~ ^[0-9a-f]{128}$ ]]; then
printf 'Expected checksum must contain exactly 128 hexadecimal characters.\n' >&2
exit 2
fi
if [[ $url != https://* ]]; then
printf 'The download URL must use HTTPS.\n' >&2
exit 2
fi
if [[ -z $filename || $filename == */* || $filename == . || $filename == .. ||
$filename == *$'\n'* || $filename == *$'\r'* ]]; then
printf 'Use a filename without slashes or line breaks.\n' >&2
exit 2
fi
destination="./$filename"
if [[ -e $destination || -L $destination ]]; then
printf 'Destination already exists: %s\n' "$destination" >&2
exit 1
fi
temporary=$(mktemp -d ./.b2-download.XXXXXXXX)
trap 'rm -rf -- "$temporary"' EXIT
if ! curl -q -fsSL --globoff --proto '=https' --proto-redir '=https' \
--max-redirs 5 --connect-timeout 10 --max-time 300 \
--output "$temporary/payload" --url "$url"; then
printf 'Download failed; no file saved.\n' >&2
exit 1
fi
if ! printf '%s %s\n' "$expected" "$temporary/payload" |
b2sum --check --strict --status -; then
printf 'Checksum mismatch or unreadable download; no file saved.\n' >&2
exit 1
fi
if ! ln -T -- "$temporary/payload" "$destination"; then
printf 'Destination exists or cannot be created; no file saved.\n' >&2
exit 1
fi
printf 'Verified and saved: %s\n' "$destination"
The checksum input uses GNU’s format: the digest, two spaces, then the filename. The script builds
that record for its own temporary payload rather than letting a downloaded manifest choose local
paths. --strict rejects malformed checksum records, while the argument check enforces the
512-bit digest length before any transfer.
The final step uses a hard link,
not mv: GNU ln -T without --force fails if the destination exists, including if it appears
after the initial check. Keeping the temporary file in the destination directory avoids a
cross-filesystem link. The exit trap removes the private temporary directory on normal success and
failure. Abrupt termination or power loss can leave it behind; this is not a crash-recovery system.
Download a release with a published BLAKE2b checksum
Arch Linux’s download page links its BLAKE2b manifest. For a reproducible example, use the September 1, 2026 bootstrap tarball, not a moving “latest” URL. This is an approximately 121 MiB archive download, not an instruction to install, extract, or execute that release.
Read the publisher’s versioned manifest over HTTPS:
curl -q -fsSL --proto '=https' --proto-redir '=https' --max-time 30 \
--url 'https://archive.archlinux.org/iso/2026.09.01/b2sums.txt'
Find the line ending in archlinux-bootstrap-2026.09.01-x86_64.tar.zst. Copy its first field, not the
ISO’s checksum or a checksum you generated locally. The invocation below uses that published
128-character value and saves the matching tarball:
(
url='https://archive.archlinux.org/iso/2026.09.01/archlinux-bootstrap-2026.09.01-x86_64.tar.zst'
expected='3893c310ec7d52fd5dee7eec51bc9d611200a5636985c8b627871d1ee85d83821284bcb9a6843afa011a6607ee9ce3202f7e7f5f168f29a07541c0836eeaefd0'
bash verify-download.sh "$url" "$expected" archlinux-bootstrap-2026.09.01-x86_64.tar.zst
)
On success, the saved program prints:
Verified and saved: ./archlinux-bootstrap-2026.09.01-x86_64.tar.zst
Only then is the destination created. An empty file is also valid if its expected checksum matches; the script verifies bytes, not whether they form a useful archive.
Interpret failures before using the file
- Checksum mismatch: no destination is created, and the temporary download is removed. Recheck the release, architecture, and trusted expected value. Do not “fix” the mismatch by using the download’s own checksum as the expected value.
- HTTP, TLS, or incomplete-transfer error: cURL fails, the script returns nonzero, and any partial payload is removed. The transfer has a five-minute deadline; adjust it deliberately for larger downloads or slower connections.
- Existing destination: the script returns nonzero and preserves its bytes. Choose another filename rather than deleting an earlier download without checking it.
- Publication failure: an unsupported hard-link operation or a destination created during the transfer prevents success. Verification alone does not mean the file was saved.
The cURL options make transfer failures visible: -f treats
most HTTP error responses as failures, -sS hides the progress meter but retains diagnostics, and -L
follows redirects. Both the starting URL and redirects are restricted to HTTPS, with at most five
redirects. -q is first so a local curl configuration cannot silently change this command’s options.
HTTPS-only redirects can still change hosts; the trusted checksum remains the byte-level reference.
Understand what a matching checksum proves
A match establishes agreement with the expected digest, not that the file is harmless or that a particular person published it. In this example, the trust basis is Arch Linux’s HTTPS publisher site. If an attacker controls both the artifact and that checksum source, they can replace both. For publisher authentication beyond that trust basis, follow the publisher’s signature-verification instructions and establish the signing key’s identity separately.
The script also assumes other processes cannot maliciously modify your working directory or the saved file. It is a local download check, not a sandbox or a guarantee that the bytes remain unchanged after verification. In a media cataloging workflow, keep the expected checksum with its source and release information so a later comparison has a reference you can explain.
