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:

  1. sign-artifact.sh — sign an artifact with your GPG key and generate a SHA-256 checksum
  2. verify-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:

  1. Check file exists: Fail clearly if the artifact path is not a regular file
  2. GPG detached signature: Produce <artifact>.asc using gpg --detach-sign --armor
  3. SHA-256 checksum: Produce <artifact>.sha256 using shasum (macOS) or sha256sum (Linux)
  4. Print summary: Print the paths to the .asc and .sha256 files

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:

  1. Check required files exist: .asc, .sha256 — fail clearly for each missing file
  2. GPG verify: Run gpg --verify and capture success/failure
  3. Checksum verify: Run OS-appropriate checksum verify and capture success/failure
  4. Final verdict: Print RELEASE VERIFIED or RELEASE FAILED with 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.org URL)
  • 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

ErrorCauseFix
gpg: signing failed: No secret keyNo GPG key with the given key IDRun gpg --list-secret-keys to find your real key ID
gpg: WARNING: not a detached signaturePassed the wrong file order to gpg --verifyOrder must be: gpg --verify <sig-file> <data-file>
shasum: illegal option -- -Wrong shasum syntax on macOSUse shasum -a 256 --check, not sha256sum --check on macOS
sha256sum: WARNING: 1 line is improperly formattedChecksum file has wrong formatThe .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
#