#!/usr/bin/env bash # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # # Pre-release verification for MAR artifacts. # # Run this AFTER `scripts/mar_sign.sh -s` and BEFORE publishing a release. # It confirms that every MAR we are about to ship is present, non-empty, # signed with the cert whose public half lives at build/signing/public_key.der, # and that the accompanying update.xml manifest reflects the signed file's # current sha512 / size. Exits non-zero on the first verification failure so # CI halts before we overwrite a good release with a broken one. set -euo pipefail CERT_PATH_DIR="build/signing" PUBLIC_KEY_DER="$CERT_PATH_DIR/public_key.der" VERIFY_NSS_DIR="$CERT_PATH_DIR/nss_verify" LINUX_EXTRACT_DIR="$CERT_PATH_DIR/extracted_linux" LINUX_ARCHIVE="zen.linux-x86_64.tar.xz/zen.linux-x86_64.tar.xz" FAILURES=0 fail() { echo " [FAIL] $*" >&2 FAILURES=$((FAILURES + 1)) } ok() { echo " [ OK ] $*" } cleanup_verify_db() { rm -rf "$VERIFY_NSS_DIR" rm -rf "$LINUX_EXTRACT_DIR" } trap cleanup_verify_db EXIT if [ -z "${SIGNMAR:-}" ]; then echo "Error: SIGNMAR environment variable is not set." >&2 exit 1 fi if [ ! -f "$SIGNMAR" ]; then echo "Error: signmar not found at $SIGNMAR." >&2 exit 1 fi chmod +x "$SIGNMAR" if [ ! -f "$PUBLIC_KEY_DER" ]; then echo "Error: $PUBLIC_KEY_DER not found. Run 'mar_sign.sh -g' first." >&2 exit 1 fi EXPECTED_MAR_CHANNEL="${RELEASE_BRANCH:-}" if [ -z "$EXPECTED_MAR_CHANNEL" ]; then echo "Error: RELEASE_BRANCH environment variable is not set (expected 'release' or 'twilight')." >&2 exit 1 fi # Build a throwaway NSS database that trusts only the signing cert, so # signmar -v can verify signatures without the private key being present. setup_verify_db() { rm -rf "$VERIFY_NSS_DIR" mkdir -p "$VERIFY_NSS_DIR" local pass="$VERIFY_NSS_DIR/password.txt" : > "$pass" certutil -N -d "$VERIFY_NSS_DIR" -f "$pass" certutil -A -d "$VERIFY_NSS_DIR" -n "mar_sig" -t "CT,C,C" -i "$PUBLIC_KEY_DER" } # Each entry: "||" declare -a pairs=( "linux.mar/linux.mar|linux_update_manifest_x86_64|linux-x86_64" "linux-aarch64.mar/linux-aarch64.mar|linux_update_manifest_aarch64|linux-aarch64" "macos.mar/macos.mar|macos_update_manifest|macos" ) if [ -d ".github/workflows/object/windows-x64-signed-x86_64" ]; then pairs+=( ".github/workflows/object/windows-x64-signed-x86_64/windows.mar|.github/workflows/object/windows-x64-signed-x86_64/update_manifest|windows-x86_64" ".github/workflows/object/windows-x64-signed-arm64/windows-arm64.mar|.github/workflows/object/windows-x64-signed-arm64/update_manifest|windows-arm64" ) else pairs+=( "windows.mar/windows.mar|windows_update_manifest_x86_64|windows-x86_64" "windows-arm64.mar/windows-arm64.mar|windows_update_manifest_arm64|windows-arm64" ) fi hash_file() { if command -v sha512sum >/dev/null 2>&1; then sha512sum "$1" | awk '{print $1}' else shasum -a 512 "$1" | awk '{print $1}' fi } size_file() { wc -c < "$1" | tr -d ' ' } verify_signature() { local mar="$1" if "$SIGNMAR" -d "$VERIFY_NSS_DIR" -n "mar_sig" -v "$mar" >/dev/null 2>&1; then ok "Signature valid: $mar" else fail "Signature INVALID (or missing) for $mar" fi } # Cache signmar -T output per MAR so we only pay for it once per file. mar_info() { local mar="$1" "$SIGNMAR" -T "$mar" 2>&1 } verify_signature_count() { local mar="$1" local info count info=$(mar_info "$mar") # signmar -T prints one "Signature block found with 1 signature" line per signature block. count=$(echo "$info" | grep -cE '^[[:space:]]*Signature block found with [0-9]+ signature' || true) if [ "$count" != "1" ]; then fail "$mar has $count signatures, expected exactly 1" else ok "$mar has exactly 1 signature" fi } verify_mar_channel() { local mar="$1" local info channel info=$(mar_info "$mar") # Accept either "MAR channel name:" or "MAR channel ID:" — the label # has drifted between Mozilla releases. channel=$(echo "$info" \ | grep -iE 'MAR channel (name|id)[[:space:]]*:' \ | head -1 \ | sed -E 's/.*MAR channel (name|id)[[:space:]]*:[[:space:]]*//I' \ | tr -d '[:space:]') if [ -z "$channel" ]; then fail "$mar: could not read MAR channel from product info block" return fi if [ "$channel" != "$EXPECTED_MAR_CHANNEL" ]; then fail "$mar: MAR channel is '$channel', expected '$EXPECTED_MAR_CHANNEL' (RELEASE_BRANCH)" else ok "$mar: MAR channel = $channel" fi } verify_update_settings() { echo "" echo "Checking update-settings.ini in $LINUX_ARCHIVE..." if [ ! -f "$LINUX_ARCHIVE" ]; then fail "Linux build archive not found at $LINUX_ARCHIVE" return fi rm -rf "$LINUX_EXTRACT_DIR" mkdir -p "$LINUX_EXTRACT_DIR" if ! tar -xf "$LINUX_ARCHIVE" -C "$LINUX_EXTRACT_DIR"; then fail "Failed to extract $LINUX_ARCHIVE" return fi local ini ini=$(find "$LINUX_EXTRACT_DIR" -type f -name "update-settings.ini" | head -1) if [ -z "$ini" ]; then fail "update-settings.ini not found inside $LINUX_ARCHIVE" return fi local accepted accepted=$(grep -oP '^ACCEPTED_MAR_CHANNEL_IDS[[:space:]]*=[[:space:]]*\K.*' "$ini" \ | head -1 | tr -d '\r') if [ -z "$accepted" ]; then fail "ACCEPTED_MAR_CHANNEL_IDS not set in $ini" return fi # ACCEPTED_MAR_CHANNEL_IDS is a comma-separated list; membership is what # the updater enforces, so we check membership rather than strict equality. local found=0 entry IFS=',' read -ra entries <<< "$accepted" for entry in "${entries[@]}"; do entry=$(echo "$entry" | tr -d '[:space:]') if [ "$entry" = "$EXPECTED_MAR_CHANNEL" ]; then found=1 break fi done if [ "$found" = "1" ]; then ok "update-settings.ini accepts MAR channel '$EXPECTED_MAR_CHANNEL' (ACCEPTED_MAR_CHANNEL_IDS=$accepted)" else fail "update-settings.ini ACCEPTED_MAR_CHANNEL_IDS='$accepted' does not include '$EXPECTED_MAR_CHANNEL'" fi } verify_manifest() { local mar="$1" manifest_dir="$2" label="$3" if [ ! -d "$manifest_dir" ]; then fail "$label: manifest directory $manifest_dir not found" return fi local xmls xmls=$(find "$manifest_dir" -type f -name "update.xml") if [ -z "$xmls" ]; then fail "$label: no update.xml files found under $manifest_dir" return fi local actual_hash actual_size actual_hash=$(hash_file "$mar") actual_size=$(size_file "$mar") while IFS= read -r xml; do local xhash xsize xurl xhash=$(grep -oP 'hashValue="\K[^"]+' "$xml" | head -1 || true) xsize=$(grep -oP 'size="\K[^"]+' "$xml" | head -1 || true) xurl=$(grep -oP 'URL="\K[^"]+' "$xml" | head -1 || true) if [ -z "$xhash" ] || [ -z "$xsize" ]; then fail "$label: $xml is missing hashValue or size" continue fi if [ -z "$xurl" ]; then fail "$label: $xml is missing URL attribute" continue fi if ! grep -q 'hashFunction="sha512"' "$xml"; then fail "$label: $xml hashFunction is not sha512" continue fi if [ "$xhash" != "$actual_hash" ]; then fail "$label: hashValue mismatch in $xml" echo " manifest: $xhash" >&2 echo " actual: $actual_hash" >&2 continue fi if [ "$xsize" != "$actual_size" ]; then fail "$label: size mismatch in $xml (manifest=$xsize, actual=$actual_size)" continue fi ok "$label: $(basename "$xml") matches $mar (size=$actual_size)" done <<< "$xmls" } setup_verify_db verify_update_settings for entry in "${pairs[@]}"; do IFS='|' read -r mar manifest label <<< "$entry" echo "" echo "Verifying $label: $mar" if [ ! -f "$mar" ]; then fail "$label: MAR file $mar not found" continue fi if [ ! -s "$mar" ]; then fail "$label: MAR file $mar is empty" continue fi verify_signature "$mar" verify_signature_count "$mar" verify_mar_channel "$mar" verify_manifest "$mar" "$manifest" "$label" done echo "" if [ "$FAILURES" -gt 0 ]; then echo "Pre-release verification FAILED with $FAILURES issue(s)." >&2 exit 1 fi echo "Pre-release verification passed."