mirror of
https://github.com/zen-browser/desktop
synced 2026-04-25 17:15:00 +02:00
281 lines
8.1 KiB
Bash
Executable File
281 lines
8.1 KiB
Bash
Executable File
#!/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 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."
|