diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3bd0e581d..6977bfe8c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -514,6 +514,13 @@ jobs: run: | bash scripts/mar_sign.sh -s + - name: Verify MARs and update manifests + env: + SIGNMAR: ${{ github.workspace }}/linux-bin-x86_64/signmar + RELEASE_BRANCH: ${{ inputs.update_branch }} + run: | + bash scripts/mar_verify.sh + - name: Copy update manifests env: RELEASE_BRANCH: ${{ inputs.update_branch }} diff --git a/.github/workflows/macos-release-build.yml b/.github/workflows/macos-release-build.yml index 2e3e7a142..66f2789f0 100644 --- a/.github/workflows/macos-release-build.yml +++ b/.github/workflows/macos-release-build.yml @@ -26,7 +26,7 @@ on: jobs: mac-build: name: Build macOS - ${{ matrix.arch }} - runs-on: ${{ (inputs.release-branch == 'release' && matrix.arch == 'aarch64') && 'blacksmith-6vcpu-macos-latest' || 'macos-26' }} + runs-on: ${{ (inputs.release-branch == 'release') && 'blacksmith-6vcpu-macos-latest' || 'macos-26' }} strategy: fail-fast: false diff --git a/scripts/mar_verify.sh b/scripts/mar_verify.sh new file mode 100755 index 000000000..67f939ecc --- /dev/null +++ b/scripts/mar_verify.sh @@ -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: "||" +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 :" 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." diff --git a/src/tools/update-packaging/common-sh.patch b/src/tools/update-packaging/common-sh.patch new file mode 100644 index 000000000..96aefa1cb --- /dev/null +++ b/src/tools/update-packaging/common-sh.patch @@ -0,0 +1,13 @@ +diff --git a/tools/update-packaging/common.sh b/tools/update-packaging/common.sh +index be2061b98cbba0d472e5f4d03cab2b058b249cb8..caa3379cd09efc2ded9686929fc658985a9dda51 100755 +--- a/tools/update-packaging/common.sh ++++ b/tools/update-packaging/common.sh +@@ -94,8 +94,6 @@ check_for_add_if_not_update() { + + if [[ "$(basename "$add_if_not_file_chk")" = "channel-prefs.js" || \ + "$add_if_not_file_chk" =~ (^|/)ChannelPrefs\.framework/ || \ +- "$(basename "$add_if_not_file_chk")" = "update-settings.ini" || \ +- "$add_if_not_file_chk" =~ (^|/)UpdateSettings\.framework/ || \ + "$(basename "$add_if_not_file_chk")" = "distribution.ini" ]]; then + ## "true" + return 0; diff --git a/src/zen/common/styles/zen-omnibox.css b/src/zen/common/styles/zen-omnibox.css index e5cb5ce19..9eede5da4 100644 --- a/src/zen/common/styles/zen-omnibox.css +++ b/src/zen/common/styles/zen-omnibox.css @@ -586,6 +586,10 @@ transform-origin: right; opacity: 0.7; + .urlbarView-row[selected] & { + opacity: 1; + } + &::after { content: ""; display: inline-block; @@ -636,7 +640,7 @@ } &[selected] { - --zen-selected-bg: color-mix(in srgb, var(--zen-primary-color) 50%, light-dark(rgba(0, 0, 0, 0.2), rgba(255, 255, 255, 0.2))); + --zen-selected-bg: color-mix(in srgb, var(--zen-primary-color) 50%, light-dark(rgba(0, 0, 0, 0.5), rgba(255, 255, 255, 0.1))); --zen-selected-color: color-mix(in srgb, var(--zen-selected-bg), black 30%); background-color: var(--zen-selected-bg) !important; @@ -654,7 +658,7 @@ color: var(--zen-selected-color) !important; } - &:where([type="switchtab"], [type="dynamic"], [type^="history"], [type^="autofill_"], [type="top_site"]) .urlbarView-favicon, + &:where([type="switchtab"], [type="dynamic"], [type^="history"], [type^="autofill_"], [type="top_site"], [type="bookmark"]) .urlbarView-favicon, & .urlbarView-shortcutContent { fill: var(--zen-selected-color) !important; background-color: rgba(255, 255, 255, 0.9) !important; diff --git a/src/zen/common/styles/zen-theme.css b/src/zen/common/styles/zen-theme.css index 27851827c..56d40d3cd 100644 --- a/src/zen/common/styles/zen-theme.css +++ b/src/zen/common/styles/zen-theme.css @@ -214,7 +214,7 @@ --urlbar-margin-inline: 1px !important; --tab-icon-overlay-stroke: light-dark(white, black) !important; - --tab-close-button-padding: 5px !important; + --tab-close-button-padding: 4px !important; --input-border-color: var(--zen-input-border-color) !important; --zen-themed-toolbar-bg-transparent: light-dark(var(--zen-branding-bg), #171717); @@ -263,7 +263,7 @@ ); --zen-sidebar-notification-shadow: 0 0 6px light-dark(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.3)); - --zen-sidebar-themed-icon-fill: color-mix(in srgb, var(--zen-primary-color) 40%, light-dark(black, white)); + --zen-sidebar-themed-icon-fill: light-dark(color-mix(in srgb, var(--zen-primary-color) 50%, black), color-mix(in srgb, var(--zen-colors-primary) 15%, #ebebeb)); --zen-native-inner-radius: var( --zen-webview-border-radius, diff --git a/src/zen/drag-and-drop/ZenDragAndDrop.js b/src/zen/drag-and-drop/ZenDragAndDrop.js index b69087884..94c6dff3f 100644 --- a/src/zen/drag-and-drop/ZenDragAndDrop.js +++ b/src/zen/drag-and-drop/ZenDragAndDrop.js @@ -119,10 +119,12 @@ init() { super.init(); this.handle_windowDragEnter = this.handle_windowDragEnter.bind(this); - gZenWorkspaces.workspaceIcons.addEventListener( - "dragover", - this.handle_spaceIconDragOver.bind(this) - ); + if (gZenWorkspaces.workspaceEnabled) { + gZenWorkspaces.workspaceIcons.addEventListener( + "dragover", + this.handle_spaceIconDragOver.bind(this) + ); + } window.addEventListener( "dragleave", this.handle_windowDragLeave.bind(this), diff --git a/src/zen/spaces/ZenSpacesSwipe.mjs b/src/zen/spaces/ZenSpacesSwipe.mjs index dca3359b7..fdffd64a5 100644 --- a/src/zen/spaces/ZenSpacesSwipe.mjs +++ b/src/zen/spaces/ZenSpacesSwipe.mjs @@ -131,15 +131,14 @@ export class ZenSpacesSwipe { event.preventDefault(); event.stopPropagation(); - const delta = event.delta * 300; const stripWidth = window.windowUtils.getBoundsWithoutFlushing( document.getElementById("navigator-toolbox") ).width + window.windowUtils.getBoundsWithoutFlushing( document.getElementById("zen-sidebar-splitter") - ).width * - 2; + ).width; + const delta = event.delta * stripWidth; let translateX = this._swipeState.lastDelta + delta; // Add a force multiplier as we are translating the strip depending on how close to the edge we are let forceMultiplier = Math.min( diff --git a/src/zen/spaces/zen-workspaces.css b/src/zen/spaces/zen-workspaces.css index 2de76a26b..f5d7cc796 100644 --- a/src/zen/spaces/zen-workspaces.css +++ b/src/zen/spaces/zen-workspaces.css @@ -75,6 +75,7 @@ &[active='true'] .zen-workspace-icon { fill: var(--zen-sidebar-themed-icon-fill); + fill-opacity: 1; } &[active='true'], @@ -216,7 +217,7 @@ height: 16px; justify-content: center; align-items: center; - fill-opacity: 0.6; + fill-opacity: 1; -moz-context-properties: fill-opacity, fill; fill: var(--zen-sidebar-themed-icon-fill); font-size: 16px;