Lab 02 — Signing and Verification
Phase: 04 — ASF Governance and Release Engineering
Difficulty: ⭐⭐☆☆☆ | Estimated Time: 1–2 hours
Primary Artifact: Working sign-artifact.sh and verify-release.sh
Verify: bash verify-release.sh release-lab-0.1.0.jar <your-key-id> prints RELEASE VERIFIED
Goal
Implement two shell scripts that together reproduce the signing and verification steps every Apache release manager performs before uploading artifacts to dist.apache.org:
sign-artifact.sh— sign an artifact with your GPG key and generate a SHA-256 checksumverify-release.sh— verify both the signature and checksum and print a clear pass/fail verdict
Both scripts ship with # TODO stubs. Your job is to fill them in.
Setup
1. Generate a GPG key (if you don't already have one)
# Generate a key — use your real name and email
gpg --full-generate-key
# Select: (1) RSA and RSA
# Key size: 4096
# Expiry: 0 (does not expire — Apache convention)
# Name: Your Name
# Email: your@email.com
2. Find your key ID
gpg --list-secret-keys --keyid-format LONG
Output looks like:
sec rsa4096/ABCDEF1234567890 2024-01-01 [SC]
ABC1234567890DEF...
uid [ultimate] Your Name <your@email.com>
Your key ID is the hex string after the slash (ABCDEF1234567890 in the example).
3. Build the artifact from Lab 01
cd ../lab-01-release-mechanics
git checkout v0.1.0
mvn package -DskipTests
cp target/release-lab-0.1.0.jar ../../lab-02-signing-verification/
cd ../../lab-02-signing-verification
Steps
Step 1 — Implement sign-artifact.sh
Open sign-artifact.sh. Fill in the four # TODO sections:
- Check file exists: Fail clearly if the artifact path is not a regular file
- GPG detached signature: Produce
<artifact>.ascusinggpg --detach-sign --armor - SHA-256 checksum: Produce
<artifact>.sha256usingshasum(macOS) orsha256sum(Linux) - Print summary: Print the paths to the
.ascand.sha256files
Test it:
chmod +x sign-artifact.sh
./sign-artifact.sh release-lab-0.1.0.jar
Expected output (key ID will differ):
Signed artifact : release-lab-0.1.0.jar
Signature file : release-lab-0.1.0.jar.asc
Checksum file : release-lab-0.1.0.jar.sha256
Inspect what was generated:
cat release-lab-0.1.0.jar.sha256 # should show hex hash + filename
cat release-lab-0.1.0.jar.asc # ASCII-armored GPG signature
Step 2 — Implement verify-release.sh
Open verify-release.sh. Fill in the four # TODO sections:
- Check required files exist:
.asc,.sha256— fail clearly for each missing file - GPG verify: Run
gpg --verifyand capture success/failure - Checksum verify: Run OS-appropriate checksum verify and capture success/failure
- Final verdict: Print
RELEASE VERIFIEDorRELEASE FAILEDwith a clear overall result
Test it:
chmod +x verify-release.sh
./verify-release.sh release-lab-0.1.0.jar <YOUR-KEY-ID>
Expected output:
GPG signature: VALID
Checksum: VALID
RELEASE VERIFIED: release-lab-0.1.0.jar is authentic.
Step 3 — Tamper Test
Verify that your script correctly detects tampering:
# Tamper with the JAR (flip one byte)
python3 -c "
data = open('release-lab-0.1.0.jar', 'rb').read()
tampered = bytes([data[0] ^ 0xFF]) + data[1:]
open('release-lab-0.1.0.jar', 'wb').write(tampered)
"
# Now verify — should FAIL
./verify-release.sh release-lab-0.1.0.jar <YOUR-KEY-ID>
Expected output:
GPG signature: INVALID
Checksum: INVALID
RELEASE FAILED: release-lab-0.1.0.jar could not be verified.
Restore the original JAR by rebuilding from Lab 01 before continuing.
Step 4 — Vote Email Draft (bonus, no script required)
Draft the [VOTE] email you would send if you were releasing release-lab-0.1.0.jar as an Apache project. Save it to vote-email.txt in this directory.
Your email must include:
- Subject line in the correct format
- RC artifact location (use a fake
dist.apache.orgURL) - KEYS file URL
- Git tag URL
- The three vote options with checkboxes
This vote is open for 72 hours.
Reference the HITCHHIKERS-GUIDE vote format.
Verification
# Both scripts are executable
test -x sign-artifact.sh && echo "sign-artifact.sh: OK"
test -x verify-release.sh && echo "verify-release.sh: OK"
# Sign produces expected files
./sign-artifact.sh release-lab-0.1.0.jar
test -f release-lab-0.1.0.jar.asc && echo "asc: OK"
test -f release-lab-0.1.0.jar.sha256 && echo "sha256: OK"
# Verify passes on untampered artifact
./verify-release.sh release-lab-0.1.0.jar <YOUR-KEY-ID> | grep "RELEASE VERIFIED"
Common Errors
| Error | Cause | Fix |
|---|---|---|
gpg: signing failed: No secret key | No GPG key with the given key ID | Run gpg --list-secret-keys to find your real key ID |
gpg: WARNING: not a detached signature | Passed the wrong file order to gpg --verify | Order must be: gpg --verify <sig-file> <data-file> |
shasum: illegal option -- - | Wrong shasum syntax on macOS | Use shasum -a 256 --check, not sha256sum --check on macOS |
sha256sum: WARNING: 1 line is improperly formatted | Checksum file has wrong format | The .sha256 file must have two spaces between hash and filename (default output format) |
Source Files
sign-artifact.sh
#!/usr/bin/env bash
# ==============================================================================
# sign-artifact.sh — Apache Release Signing Script
#
# Lab 02, Phase 04: ASF Governance and Release Engineering
#
# Usage:
# ./sign-artifact.sh <path-to-artifact>
#
# Produces:
# <artifact>.asc — GPG detached armored signature
# <artifact>.sha256 — SHA-256 checksum
#
# Fill in the four TODO sections below. When complete, running this script
# against any file should produce the two output files and print a summary.
#
# Reference:
# Apache Release Policy: https://www.apache.org/legal/release-policy.html
# GPG sign command: gpg --batch --yes --detach-sign --armor --output <out> <in>
# macOS checksum: shasum -a 256 <file> > <file>.sha256
# Linux checksum: sha256sum <file> > <file>.sha256
# ==============================================================================
set -euo pipefail
ARTIFACT="${1:-}"
if [[ -z "$ARTIFACT" ]]; then
echo "Usage: $0 <path-to-artifact>" >&2
exit 1
fi
SIG_FILE="${ARTIFACT}.asc"
CHECKSUM_FILE="${ARTIFACT}.sha256"
# ------------------------------------------------------------------------------
# Step 1: Verify the artifact exists
# ------------------------------------------------------------------------------
# TODO: Check that $ARTIFACT is a regular file (-f).
# If it is not, print an error message to stderr and exit with code 1.
# Example message: "Error: file not found: $ARTIFACT"
#
# Hint: Use: if [[ ! -f "$ARTIFACT" ]]; then ...
#
# ------------------------------------------------------------------------------
# Step 2: Sign with GPG (detached armored signature)
# ------------------------------------------------------------------------------
# TODO: Run the GPG sign command to produce $SIG_FILE.
#
# The command signature is:
# gpg --batch --yes --detach-sign --armor --output "$SIG_FILE" "$ARTIFACT"
#
# Flags explained:
# --batch non-interactive (no prompts)
# --yes overwrite $SIG_FILE if it already exists
# --detach-sign produce a detached signature (not embedded in the file)
# --armor ASCII-armor the output (human-readable, safe to email)
# --output <file> write the signature to this file
#
# Note: If you have multiple GPG keys, add --local-user <key-id> to pick the
# right one. For this lab any key is fine.
#
# ------------------------------------------------------------------------------
# Step 3: Generate SHA-256 checksum
# ------------------------------------------------------------------------------
# TODO: Detect the operating system and run the appropriate checksum command.
#
# macOS: shasum -a 256 "$ARTIFACT" > "$CHECKSUM_FILE"
# Linux: sha256sum "$ARTIFACT" > "$CHECKSUM_FILE"
#
# Hint for OS detection:
# OS="$(uname -s)"
# case "$OS" in
# Darwin) ... ;;
# Linux) ... ;;
# *) echo "Unsupported OS: $OS" >&2; exit 1 ;;
# esac
#
# The output file format produced by both commands is the same:
# <hex-hash> <filename> (two spaces between hash and name)
# This format is understood by --check on both macOS and Linux.
#
# ------------------------------------------------------------------------------
# Step 4: Print a summary
# ------------------------------------------------------------------------------
echo "Signed artifact : $ARTIFACT"
# TODO: Print the path to the signature file (SIG_FILE) and checksum file (CHECKSUM_FILE).
# Format them like the line above, aligned with spaces for readability.
# Example:
# Signature file : release-lab-0.1.0.jar.asc
# Checksum file : release-lab-0.1.0.jar.sha256
#
verify-release.sh
#!/usr/bin/env bash
# ==============================================================================
# verify-release.sh — Apache Release Verification Script
#
# Lab 02, Phase 04: ASF Governance and Release Engineering
#
# Usage:
# ./verify-release.sh <artifact> <signer-key-id>
#
# Example:
# ./verify-release.sh release-lab-0.1.0.jar ABCDEF1234567890
#
# This script verifies that an Apache release artifact:
# 1. Has a valid GPG detached signature from a known signer
# 2. Matches its published SHA-256 checksum
#
# Fill in the four TODO sections below. The script must:
# - Exit 0 and print "RELEASE VERIFIED: <artifact> is authentic." on success
# - Exit 1 and print "RELEASE FAILED: <artifact> could not be verified." on failure
#
# Reference:
# gpg --verify <sig-file> <data-file>
# macOS checksum verify: shasum -a 256 --check <checksum-file>
# Linux checksum verify: sha256sum --check <checksum-file>
# ==============================================================================
set -uo pipefail
# Note: we do NOT set -e here because we want to capture individual command
# exit codes rather than failing immediately on any non-zero exit.
ARTIFACT="${1:-}"
SIGNER_KEY="${2:-}"
if [[ -z "$ARTIFACT" || -z "$SIGNER_KEY" ]]; then
echo "Usage: $0 <artifact> <signer-key-id>" >&2
exit 1
fi
SIG_FILE="${ARTIFACT}.asc"
CHECKSUM_FILE="${ARTIFACT}.sha256"
# We'll track overall pass/fail in these variables.
GPG_OK=false
CHECKSUM_OK=false
# ------------------------------------------------------------------------------
# Step 1: Check that all required files exist
# ------------------------------------------------------------------------------
# TODO: Verify that each of these three files exists as a regular file:
# $ARTIFACT
# $SIG_FILE
# $CHECKSUM_FILE
#
# For each missing file, print a message to stderr like:
# "Error: missing artifact: $ARTIFACT"
# "Error: missing signature: $SIG_FILE"
# "Error: missing checksum: $CHECKSUM_FILE"
#
# If ANY file is missing, exit 1 after printing all errors.
# This way the user sees all missing files at once, not just the first one.
#
# Hint:
# MISSING=0
# [[ ! -f "$ARTIFACT" ]] && { echo "Error: ..." >&2; MISSING=1; }
# ...
# [[ $MISSING -eq 1 ]] && exit 1
#
# ------------------------------------------------------------------------------
# Step 2: Verify GPG signature
# ------------------------------------------------------------------------------
# TODO: Run: gpg --verify "$SIG_FILE" "$ARTIFACT"
#
# IMPORTANT: The argument order matters.
# Correct: gpg --verify <signature-file> <data-file>
# Wrong: gpg --verify <data-file> <signature-file> ← common mistake
#
# Capture the exit code:
# If exit code is 0 → set GPG_OK=true and print "GPG signature: VALID"
# If exit code is non-zero → print "GPG signature: INVALID"
#
# You may redirect gpg's own output to /dev/null to keep the output clean,
# or let it print (useful for debugging).
#
# Hint:
# gpg --verify "$SIG_FILE" "$ARTIFACT" 2>/dev/null
# if [[ $? -eq 0 ]]; then ...
#
# ------------------------------------------------------------------------------
# Step 3: Verify SHA-256 checksum
# ------------------------------------------------------------------------------
# TODO: Detect the OS and run the correct checksum verification command.
#
# The checksum file produced by sign-artifact.sh has format:
# <hex> <filename> (two spaces — the default output format)
#
# macOS verify: shasum -a 256 --check "$CHECKSUM_FILE"
# Linux verify: sha256sum --check "$CHECKSUM_FILE"
#
# Both commands must be run from the directory containing the artifact because
# the filename in the checksum file is a relative path.
#
# Capture the exit code the same way as Step 2:
# If exit code is 0 → set CHECKSUM_OK=true and print "Checksum: VALID"
# If exit code is non-zero → print "Checksum: INVALID"
#
# Hint for OS detection: see sign-artifact.sh Step 3.
#
# ------------------------------------------------------------------------------
# Step 4: Final verdict
# ------------------------------------------------------------------------------
# TODO: If both GPG_OK and CHECKSUM_OK are true, print:
# "RELEASE VERIFIED: $ARTIFACT is authentic."
# and exit 0.
#
# Otherwise, print:
# "RELEASE FAILED: $ARTIFACT could not be verified."
# and exit 1.
#
# Hint:
# if [[ "$GPG_OK" == "true" && "$CHECKSUM_OK" == "true" ]]; then
# ...
# fi
#