Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,17 @@ release (per-job artifacts only).
### Apple XCFramework signing

Every `.xcframework` in the Darwin tarballs is provider-signed with the Flet
publishing team's Apple Distribution identity and a secure timestamp.
publishing team's Apple Distribution identity and a secure timestamp — **both the
inner `.framework` bundles in each slice and the outer xcframework**, in that
order.

Signing only the outer bundle is not enough: an IPA built against such an
artifact reports `signed = true` but `isSecureTimestamp = false`. Every slice of
an XCFramework Apple's App Store scan demonstrably accepts
([krzyzanowskim/OpenSSL](https://github.com/krzyzanowskim/OpenSSL)) carries its
own signature, with the outer bundle signed last. The order matters — the outer
seal hashes the bundle contents, so signing an inner framework afterwards
invalidates it.

This matters because Xcode records the state of each `.xcframework` an app links
against **as its publisher shipped it**, and writes the result into the IPA as
Expand Down
155 changes: 120 additions & 35 deletions darwin/xcframework_signing.sh
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,13 @@ xcf_find() {
# depends on the consuming application.
xcf_signing_identifier() {
local xcf=$1
local name plist ident
name=$(basename "$xcf" .xcframework)
local fw plist ident

local slice
for slice in "$xcf"/*/; do
[ -d "$slice$name.framework" ] || continue
while IFS= read -r fw; do
[ -n "$fw" ] || continue
# Flat (iOS) layout, then versioned (macOS). Versions/Current is a
# symlink to the real version directory, so skip it.
for plist in "$slice$name.framework/Info.plist" \
"$slice$name.framework"/Versions/*/Resources/Info.plist; do
for plist in "$fw/Info.plist" "$fw"/Versions/*/Resources/Info.plist; do
[ -f "$plist" ] || continue
case "$plist" in */Versions/Current/*) continue ;; esac
ident=$(plutil -extract CFBundleIdentifier raw -o - "$plist" 2>/dev/null) || continue
Expand All @@ -142,11 +139,51 @@ xcf_signing_identifier() {
return 0
fi
done
done
done <<EOF
$(xcf_slice_frameworks "$xcf")
EOF
return 1
}

# Sign one completed outer XCFramework.
# Emit the per-slice .framework bundles inside an xcframework.
#
# Found by globbing each slice directory rather than by assuming the framework is
# named after the xcframework. The two normally match, but a consumer may stage a
# copy under a different name -- serious_python renames Python.xcframework to
# Python-<platform>.xcframework -- and a name-keyed lookup silently finds nothing
# there, which would let an unsigned slice pass as "no slices to check".
xcf_slice_frameworks() {
local xcf=$1
local slice fw
for slice in "$xcf"/*/; do
[ -d "$slice" ] || continue
for fw in "$slice"*.framework; do
[ -d "$fw" ] && printf '%s\n' "$fw"
done
done
return 0
}

# Sign one completed outer XCFramework — inner frameworks first, outer last.
#
# WHY BOTH LAYERS
# Signing only the outer bundle is not enough. Compare against a third-party
# XCFramework Apple's App Store scan demonstrably accepts
# (github.com/krzyzanowskim/OpenSSL): every one of its ten slices carries its
# own Apple Distribution signature with a secure timestamp, and the outer
# bundle is signed afterwards. Our artifacts previously had unsigned inner
# frameworks, and their IPA receipts came back `signed = true` but
# `isSecureTimestamp = false` — the one structural difference between the two.
#
# Order matters and is not optional: the outer seal hashes the bundle contents,
# so the inner signatures have to exist before it is computed. Signing an inner
# framework afterwards would invalidate the outer seal, which is the same
# mistake this whole effort exists to stop making.
#
# `xcodebuild -create-xcframework` preserves inner `_CodeSignature`
# directories, so a producer may equally sign the .framework bundles before
# assembling the xcframework; doing it here keeps one code path for both the
# build-time and the sign-the-finished-archive callers.
xcf_sign_one() {
local xcf=$1
[ -d "$xcf" ] || { xcf_err "not a directory: $xcf"; return 1; }
Expand All @@ -158,69 +195,117 @@ xcf_sign_one() {
return 1
fi

local args=(--force --timestamp -i "$ident" --sign "$XCFRAMEWORK_CODESIGN_IDENTITY")
local args=(--force --timestamp --sign "$XCFRAMEWORK_CODESIGN_IDENTITY")
[ -n "${XCFRAMEWORK_SIGNING_KEYCHAIN:-}" ] && args+=(--keychain "$XCFRAMEWORK_SIGNING_KEYCHAIN")

xcf_log "signing $xcf as $ident"
# Deliberately no --deep: it re-signs nested code with the outer options and
# is documented by Apple as inappropriate for producing a distributable
# signature. Deliberately no --timestamp=none: the receipt's
# isSecureTimestamp is exactly what we are here to make true.
codesign "${args[@]}" "$xcf" || { xcf_err "codesign failed for $xcf"; return 1; }
# Deliberately no --deep anywhere below: it re-signs nested code with the
# outer bundle's options and is documented by Apple as inappropriate for
# producing a distributable signature. Signing each slice explicitly is the
# supported way to get the same coverage. Deliberately no --timestamp=none:
# the receipt's isSecureTimestamp is exactly what we are here to make true.
local fw count=0
while IFS= read -r fw; do
[ -n "$fw" ] || continue
# No -i for the inner bundles: a .framework has a real CFBundleIdentifier
# in its own Info.plist, which codesign picks up.
codesign "${args[@]}" "$fw" || { xcf_err "codesign failed for $fw"; return 1; }
count=$((count + 1))
done <<EOF
$(xcf_slice_frameworks "$xcf")
EOF

if [ "$count" -eq 0 ]; then
xcf_err "$xcf: no slice frameworks found to sign"
return 1
fi

xcf_log "signing $xcf as $ident ($count slice framework(s) + outer)"
codesign "${args[@]}" -i "$ident" "$xcf" || { xcf_err "codesign failed for $xcf"; return 1; }
}

# Verify one outer XCFramework carries a real provider signature.
xcf_verify_one() {
local xcf=$1
# Assert one signed bundle (inner framework or outer xcframework) carries a real
# provider signature: verifiable, not ad-hoc, securely timestamped, right
# authority, right team.
xcf_assert_signature() {
local target=$1
local expect_team=${XCFRAMEWORK_EXPECTED_TEAM_ID:-}
local expect_authority=${XCFRAMEWORK_EXPECTED_AUTHORITY:-Apple Distribution}

if [ ! -f "$xcf/_CodeSignature/CodeResources" ]; then
xcf_err "$xcf: no outer _CodeSignature/CodeResources — the XCFramework is unsigned"
return 1
fi

if ! codesign --verify --strict --verbose=4 "$xcf" 2>&1; then
xcf_err "$xcf: codesign --verify --strict failed"
if ! codesign --verify --strict --verbose=4 "$target" 2>&1; then
xcf_err "$target: codesign --verify --strict failed"
return 1
fi

local info
if ! info=$(codesign -dvvv "$xcf" 2>&1); then
xcf_err "$xcf: codesign -dvvv failed: $info"
if ! info=$(codesign -dvvv "$target" 2>&1); then
xcf_err "$target: codesign -dvvv failed: $info"
return 1
fi
printf '%s\n' "$info"

# An ad-hoc signature satisfies --verify but carries no identity at all, so
# it would sail past the checks below if they were the only ones.
if printf '%s\n' "$info" | grep -q '^Signature=adhoc'; then
xcf_err "$xcf: ad-hoc signature; a release artifact needs a real identity"
xcf_err "$target: ad-hoc signature; a release artifact needs a real identity"
return 1
fi

# `Timestamp=` is the secure (Apple TSA) timestamp. A signature made without
# --timestamp reports `Signed Time=` instead, which is self-asserted and is
# what makes an IPA receipt report isSecureTimestamp = false.
if ! printf '%s\n' "$info" | grep -q '^Timestamp='; then
xcf_err "$xcf: no secure timestamp (signed without --timestamp?)"
xcf_err "$target: no secure timestamp (signed without --timestamp?)"
return 1
fi

if ! printf '%s\n' "$info" | grep '^Authority=' | grep -qF "$expect_authority"; then
xcf_err "$xcf: no '$expect_authority' authority in the signature chain"
xcf_err "$target: no '$expect_authority' authority in the signature chain"
return 1
fi

if [ -n "$expect_team" ]; then
local actual_team
actual_team=$(printf '%s\n' "$info" | sed -n 's/^TeamIdentifier=//p' | head -1)
if [ "$actual_team" != "$expect_team" ]; then
xcf_err "$xcf: TeamIdentifier '$actual_team' != expected '$expect_team'"
xcf_err "$target: TeamIdentifier '$actual_team' != expected '$expect_team'"
return 1
fi
fi
}

# Verify one XCFramework carries real provider signatures on BOTH layers.
xcf_verify_one() {
local xcf=$1

if [ ! -f "$xcf/_CodeSignature/CodeResources" ]; then
xcf_err "$xcf: no outer _CodeSignature/CodeResources — the XCFramework is unsigned"
return 1
fi

# Every slice's framework must be signed in its own right. An unsigned inner
# bundle is what produced `isSecureTimestamp = false` receipts even though the
# outer seal was valid — see xcf_sign_one.
local fw count=0
while IFS= read -r fw; do
[ -n "$fw" ] || continue
if [ ! -d "$fw/_CodeSignature" ] && [ ! -d "$fw/Versions/A/_CodeSignature" ]; then
xcf_err "$fw: inner framework is unsigned"
return 1
fi
xcf_assert_signature "$fw" >/dev/null || return 1
count=$((count + 1))
done <<EOF
$(xcf_slice_frameworks "$xcf")
EOF

if [ "$count" -eq 0 ]; then
xcf_err "$xcf: no slice frameworks found to verify"
return 1
fi

xcf_assert_signature "$xcf" || return 1

local info
info=$(codesign -dvvv "$xcf" 2>&1) || info=""
printf '%s\n' "$info"

# The outer seal must name the same provider-owned identifier as the inner
# framework. A mismatch means the bundle was re-signed by something that did
# not pass -i, and fell back to the file name.
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"default_python_version": "3.14",
"dart_bridge_version": "1.7.0",
"dart_bridge_version": "1.7.1",
"pythons": {
"3.12": {
"full_version": "3.12.13",
Expand Down
Loading