|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Copyright (c) 2026 [Ribose Inc](https://www.ribose.com). |
| 3 | +# All rights reserved. |
| 4 | +# This file is a part of tamatebako |
| 5 | +# |
| 6 | +# Redistribution and use in source and binary forms, with or without |
| 7 | +# modification, are permitted provided that the following conditions |
| 8 | +# are met: |
| 9 | +# 1. Redistributions of source code must retain the above copyright |
| 10 | +# notice, this list of conditions and the following disclaimer. |
| 11 | +# 2. Redistributions in binary form must reproduce the above copyright |
| 12 | +# notice, this list of conditions and the following disclaimer in the |
| 13 | +# documentation and/or other materials provided with the distribution. |
| 14 | +# |
| 15 | +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| 16 | +# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
| 17 | +# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| 18 | +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS |
| 19 | +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| 20 | +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| 21 | +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| 22 | +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| 23 | +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| 24 | +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
| 25 | +# POSSIBILITY OF SUCH DAMAGE. |
| 26 | + |
| 27 | +# ci/macos-sign-notarize-runtime.sh — spec 31 §5/§8.4: sign the freshly |
| 28 | +# built macOS runtime exe (Developer ID Application, hardened runtime, |
| 29 | +# the ci/runtime.entitlements pair) and notarize it, in-leg, BEFORE the |
| 30 | +# symbol-provenance check and the boot smoke — every downstream gate, and |
| 31 | +# the publish stage's .sha256 sidecar, then anchors the exact SIGNED |
| 32 | +# bytes (sign-then-hash is mandatory). |
| 33 | +# |
| 34 | +# The spec 31 §5 gate: the repo variable APPLE_SIGNING_ENABLED=true |
| 35 | +# arms; anything else ships unsigned BY DESIGN (spec 00 invariant 7 — |
| 36 | +# a loud notice, exit 0). Armed + an unresolved APPLE_* secret is a |
| 37 | +# FAST named failure, never a partial release. |
| 38 | +# |
| 39 | +# The exe is left quarantine-marked on success: the leg's own boot |
| 40 | +# smoke (the jit_llvm legs exercise CPython's JIT under the signed |
| 41 | +# entitlements) then runs under Gatekeeper's exact download path. Bare |
| 42 | +# Mach-O keeps an online ticket — stapling is unsupported and spctl |
| 43 | +# rejects bare CLI tools BY DESIGN (the ruby factory's sign-probe, |
| 44 | +# tamatebako/tebako run 34319254822), so the documented verification is |
| 45 | +# codesign's notarization check. |
| 46 | +# |
| 47 | +# Required env when armed: APPLE_DEVELOPER_ID_P12 (base64), |
| 48 | +# APPLE_DEVELOPER_ID_P12_PASSWORD, APPLE_TEAM_ID, APPLE_ASC_KEY_P8, |
| 49 | +# APPLE_ASC_KEY_ID, APPLE_ASC_ISSUER_ID. Runner env: RUNNER_TEMP. |
| 50 | +# Runs from the workspace root (ci/runtime.entitlements resolves). |
| 51 | +# |
| 52 | +# Usage: ci/macos-sign-notarize-runtime.sh <runtime-exe> |
| 53 | +set -euo pipefail |
| 54 | + |
| 55 | +exe="$1" |
| 56 | +[ -f "$exe" ] || { echo "::error::no such runtime exe: $exe"; exit 64; } |
| 57 | + |
| 58 | +if [ "${APPLE_SIGNING_ENABLED:-false}" != "true" ]; then |
| 59 | + echo "::notice::spec 31: APPLE_SIGNING_ENABLED != true — this leg ships UNSIGNED (spec 00 invariant 7)" |
| 60 | + exit 0 |
| 61 | +fi |
| 62 | +missing="" |
| 63 | +for v in APPLE_DEVELOPER_ID_P12 APPLE_DEVELOPER_ID_P12_PASSWORD APPLE_TEAM_ID \ |
| 64 | + APPLE_ASC_KEY_P8 APPLE_ASC_KEY_ID APPLE_ASC_ISSUER_ID; do |
| 65 | + [ -n "${!v:-}" ] || missing="$missing $v" |
| 66 | +done |
| 67 | +if [ -n "$missing" ]; then |
| 68 | + echo "::error::APPLE_SIGNING_ENABLED=true but unset:$missing — spec 31 §5: fast failure, never a partial release" |
| 69 | + exit 1 |
| 70 | +fi |
| 71 | + |
| 72 | +step() { echo; echo "=== $*"; } |
| 73 | + |
| 74 | +step "keychain + identity (Developer ID Application)" |
| 75 | +work="$(mktemp -d "${RUNNER_TEMP:-/tmp}/tebako-sign.XXXXXX")" |
| 76 | +printf '%s' "$APPLE_DEVELOPER_ID_P12" | base64 -d > "$work/devid.p12" |
| 77 | +KC="$work/sign.keychain" |
| 78 | +security create-keychain -p sign-kc-pass "$KC" |
| 79 | +security unlock-keychain -p sign-kc-pass "$KC" |
| 80 | +# codesign resolves identities through the user search list — a freshly |
| 81 | +# created keychain is not on it (the sign-probe's attempt-2 lesson). |
| 82 | +security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"') |
| 83 | +security import "$work/devid.p12" -k "$KC" -P "$APPLE_DEVELOPER_ID_P12_PASSWORD" \ |
| 84 | + -T /usr/bin/codesign -T /usr/bin/security |
| 85 | +security set-key-partition-list -S apple-tool:,apple: -k sign-kc-pass "$KC" >/dev/null |
| 86 | +# Sign by cert SHA-1, never by name — a duplicate identity in any |
| 87 | +# search-listed keychain makes name resolution ambiguous. |
| 88 | +HASH=$(security find-identity -v -p codesigning "$KC" | awk '/Developer ID Application/ {print $2; exit}') |
| 89 | +[ -n "$HASH" ] || { echo "::error::no Developer ID Application identity in the p12"; exit 1; } |
| 90 | +echo "signing identity cert: ${HASH:0:10}…" |
| 91 | + |
| 92 | +step "codesign (hardened runtime + the spec 31 §3 pair)" |
| 93 | +codesign --force --options runtime --timestamp --keychain "$KC" \ |
| 94 | + --entitlements ci/runtime.entitlements --sign "$HASH" "$exe" |
| 95 | +codesign --verify --strict --verbose=1 "$exe" |
| 96 | +codesign -dvv "$exe" 2>&1 | grep -q "TeamIdentifier=$APPLE_TEAM_ID" \ |
| 97 | + || { echo "::error::TeamIdentifier is not $APPLE_TEAM_ID — signed by an unexpected identity"; exit 1; } |
| 98 | +# macOS 15 emits the embedded plist single-line — count <true/> values, |
| 99 | +# never key lines. |
| 100 | +codesign -d --entitlements :- "$exe" > "$work/embedded.xml" 2>/dev/null || true |
| 101 | +TRUES=$(grep -o '<true/>' "$work/embedded.xml" | wc -l | tr -d ' ') |
| 102 | +[ "$TRUES" = "2" ] || { cat "$work/embedded.xml"; echo "::error::entitlement pair not embedded (got $TRUES <true/>)"; exit 1; } |
| 103 | +echo "entitlements embedded: disable-library-validation + allow-jit" |
| 104 | + |
| 105 | +step "notarize (App Store Connect API key)" |
| 106 | +printf '%s' "$APPLE_ASC_KEY_P8" > "$work/AuthKey.p8" |
| 107 | +( cd "$(dirname "$exe")" && zip -q -j "$work/runtime.zip" "$(basename "$exe")" ) |
| 108 | +xcrun notarytool submit "$work/runtime.zip" --key "$work/AuthKey.p8" \ |
| 109 | + --key-id "$APPLE_ASC_KEY_ID" --issuer "$APPLE_ASC_ISSUER_ID" \ |
| 110 | + --wait --timeout 20m | tee "$work/notary.txt" |
| 111 | +grep -q 'status: Accepted' "$work/notary.txt" || { |
| 112 | + xcrun notarytool log "$(grep -m1 -oE '[0-9a-f-]{36}' "$work/notary.txt")" \ |
| 113 | + --key "$work/AuthKey.p8" --key-id "$APPLE_ASC_KEY_ID" --issuer "$APPLE_ASC_ISSUER_ID" 2>&1 | tail -20 |
| 114 | + echo "::error::notarytool did not Accept the submission"; exit 1; } |
| 115 | + |
| 116 | +step "verify the online ticket + quarantine-mark (the boot smoke is the exec canary)" |
| 117 | +codesign --verify --strict --check-notarization -R=notarized "$exe" |
| 118 | +xattr -w com.apple.quarantine '0081;00000000;Safari;' "$exe" |
| 119 | +echo "signed + notarized + quarantine-marked: $(basename "$exe")" |
0 commit comments