#!/usr/bin/env bash # Sign extension/ into a CRX and write the update manifest Chrome polls. # # The signing key is the extension's identity: the id Chrome derives from it is # baked into every installed copy and into the Windows policy. Sign with a # different key and you have published a different extension that no browser # will ever accept as an update — so this refuses to mint one by accident. set -euo pipefail cd "$(dirname "$0")" log() { printf '\n\033[1;34m==> %s\033[0m\n' "$1"; } fail() { printf '\n\033[1;31mERROR: %s\033[0m\n' "$1" >&2; exit 1; } [ -f .env ] || fail "no .env — copy .env.example to .env and fill it in" set -a; . ./.env; set +a : "${EXT_HOST:?set EXT_HOST in .env}" UPDATES_DIR="${UPDATES_DIR:-/opt/autofirmer-updates}" KEY="$UPDATES_DIR/key.pem" PUBLIC="$UPDATES_DIR/public" EXTENSION="$(cd .. && pwd)/extension" [ -f "$KEY" ] || fail "no signing key at $KEY. A new one would change the extension id and orphan every install. See deploy/README.md before generating one." mkdir -p "$PUBLIC" VERSION=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "$EXTENSION/manifest.json") [ -n "$VERSION" ] || fail "could not read the version out of the manifest" # The id is the first 16 bytes of the SHA-256 of the public key, each hex digit # shifted from 0-9a-f into a-p. Derived from the *manifest* here so it can be # checked against what the packer writes: if the manifest's `key` and the # signing key are not a pair, updates silently never apply, and this is the only # moment that mismatch is visible. manifest_id() { python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("key",""))' "$EXTENSION/manifest.json" \ | base64 -d 2>/dev/null | openssl dgst -sha256 -binary | xxd -p -c32 | cut -c1-32 | tr '0-9a-f' 'a-p' } log "Packing v$VERSION" # Node lives in a container so the droplet needs no toolchain of its own. The # key is mounted read-only: crx3 writes a new one when it cannot find the file, # and that is the one failure mode worth making impossible. docker run --rm \ -v "$EXTENSION:/ext:ro" \ -v "$KEY:/key.pem:ro" \ -v "$PUBLIC:/out" \ node:24-slim sh -c "cd /tmp && npm i --silent crx3@1 >/dev/null 2>&1 && \ node_modules/.bin/crx3 -p /key.pem -o /out/autofirmer.crx -x /out/updates.xml \ --crxURL https://$EXT_HOST/autofirmer.crx --appVersion '$VERSION' -- /ext" \ || fail "packing failed" EXPECTED=$(manifest_id) if [ -n "$EXPECTED" ] && ! grep -q "appid=\"$EXPECTED\"" "$PUBLIC/updates.xml"; then fail "the manifest's \`key\` and $KEY are not a pair — the update manifest advertises a different extension than the one people have installed" fi log "Published" echo " id: ${EXPECTED:-}" echo " version: $VERSION" echo " crx: https://$EXT_HOST/autofirmer.crx" echo " updates: https://$EXT_HOST/updates.xml"