Skip to content

Commit 924524d

Browse files
ronaldtsetebako-ci
andauthored
ci: macOS sign+notarize leg (spec 31 §5/§8.4 — the ruby factory's proven pattern) (#13)
Ports tebako-runtime-ruby's Apple OS-plane leg verbatim-logic: - ci/macos-sign-notarize-runtime.sh — the proven script unchanged in logic (diff vs the ruby factory's: license header + comment lines only): Developer ID Application by cert SHA-1, hardened runtime, the entitlements pair embedded, notarytool --wait, codesign --check-notarization, quarantine-mark so the boot smoke runs under Gatekeeper's download path. Gate: APPLE_SIGNING_ENABLED != true → loud notice, exit 0 (unsigned stays first-class, spec 00 invariant 7); armed + a missing APPLE_* secret → fast named failure. - ci/runtime.entitlements — the fixed pair (disable-library-validation for dlopen'd in-image .so extensions; allow-jit for the jit_llvm legs), comments adapted from ruby's (.bundle/YJIT) to python's. - _build-platform.yml — the step sits before the symbol-provenance check and the boot smoke so every downstream gate and the publish .sha256 sidecar anchors the SIGNED bytes (sign-then-hash is mandatory); exe path uses this factory's naming (needs.compute.outputs.tebako_version / matrix.python / matrix.host_id). The org APPLE_* secrets now cover this repo (6/6, selected-repositories plane); the APPLE_SIGNING_ENABLED repo variable stays UNSET here — the leg lands gated off, the arm flip is the owner's call after a green structural run (nothing publishes from this repo until the TODO.python chain proves, and signing rehearses per-leg without publishing). Windows: no legs to sign — the windows row is descoped (TODO.python/05); the spec 34 §8.2 pattern ports with the row. Co-authored-by: tebako-ci <tebako@ribose.com>
1 parent 0a93b31 commit 924524d

3 files changed

Lines changed: 174 additions & 0 deletions

File tree

‎.github/workflows/_build-platform.yml‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,34 @@ jobs:
256256
git config --global core.longpaths true
257257
tools/build_runtime --python "${{ matrix.python }}"
258258
259+
# Spec 31 §5/§8.4: sign (Developer ID Application, hardened
260+
# runtime, the ci/runtime.entitlements pair) + notarize the macOS
261+
# runtime exe IN-LEG, before the symbol-provenance check and the
262+
# boot smoke — every downstream gate, and the publish stage's
263+
# .sha256 sidecar, anchors the exact SIGNED bytes (sign-then-hash
264+
# is mandatory). Gated on the repo variable APPLE_SIGNING_ENABLED
265+
# (unsigned stays first-class without it — spec 00 invariant 7;
266+
# armed + a missing secret is a fast named failure). The exe is
267+
# left quarantine-marked: the boot smoke — the jit_llvm legs
268+
# exercise CPython's JIT — then runs under Gatekeeper's exact
269+
# download path, proving the allow-jit/disable-library-validation
270+
# pair on the signed exe.
271+
- name: Sign + notarize the runtime exe (macos, spec 31)
272+
if: matrix.os == 'macos'
273+
shell: bash
274+
env:
275+
APPLE_SIGNING_ENABLED: ${{ vars.APPLE_SIGNING_ENABLED }}
276+
APPLE_DEVELOPER_ID_P12: ${{ secrets.APPLE_DEVELOPER_ID_P12 }}
277+
APPLE_DEVELOPER_ID_P12_PASSWORD: ${{ secrets.APPLE_DEVELOPER_ID_P12_PASSWORD }}
278+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
279+
APPLE_ASC_KEY_P8: ${{ secrets.APPLE_ASC_KEY_P8 }}
280+
APPLE_ASC_KEY_ID: ${{ secrets.APPLE_ASC_KEY_ID }}
281+
APPLE_ASC_ISSUER_ID: ${{ secrets.APPLE_ASC_ISSUER_ID }}
282+
run: |
283+
set -euo pipefail
284+
runtime="tebako-runtime-${{ needs.compute.outputs.tebako_version }}-${{ matrix.python }}-${{ matrix.host_id }}"
285+
bash ci/macos-sign-notarize-runtime.sh "runtime-packages/$runtime"
286+
259287
# --- the gates (a red gate skips the upload, and the release
260288
# job's completeness check then fails the run loudly) -------------
261289
- name: Check symbol provenance (container legs)

‎ci/macos-sign-notarize-runtime.sh‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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")"

‎ci/runtime.entitlements‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<!--
4+
Spec 31 §3/§5: the macOS runtime signing profile (fixed pair, pinned
5+
here until spec 31 §2's manifest derivation lands).
6+
7+
disable-library-validation — EVERY runtime slice: a hardened-runtime
8+
process by default refuses unsigned dylibs, and the driver
9+
materializes in-image native extensions (.so) to the host for
10+
dlopen; they carry no Developer ID signature of their own (§1.4).
11+
allow-jit — CPython JIT builds (the matrix's jit_llvm legs) map
12+
executable memory at runtime (MAP_JIT on arm64). Harmless on a
13+
python built without a JIT: the entitlement is a grant, never a
14+
requirement.
15+
16+
Keep the two <true/> values on their own lines: the embedding check
17+
in ci/macos-sign-notarize-runtime.sh counts them (macOS 15 emits the
18+
embedded plist single-line, so it greps for <true/>, not key names).
19+
-->
20+
<plist version="1.0">
21+
<dict>
22+
<key>com.apple.security.cs.disable-library-validation</key>
23+
<true/>
24+
<key>com.apple.security.cs.allow-jit</key>
25+
<true/>
26+
</dict>
27+
</plist>

0 commit comments

Comments
 (0)