mirror of
https://github.com/zen-browser/desktop.git
synced 2026-07-16 05:40:27 +00:00
no-bug: Run update verification before releasing (gh-13353)
This commit is contained in:
280
scripts/mar_verify.sh
Executable file
280
scripts/mar_verify.sh
Executable file
@@ -0,0 +1,280 @@
|
||||
#!/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: "<mar_path>|<manifest_dir>|<platform_label>"
|
||||
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 <n>:" line per signature block.
|
||||
count=$(echo "$info" | grep -cE '^[[:space:]]*Signature[[:space:]]+[0-9]+:' || 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."
|
||||
Reference in New Issue
Block a user