Skip to content

Sign inner .framework bundles too; dart_bridge 1.7.1 - #38

Merged
FeodorFitsner merged 3 commits into
mainfrom
sign-inner-frameworks
Jul 30, 2026
Merged

Sign inner .framework bundles too; dart_bridge 1.7.1#38
FeodorFitsner merged 3 commits into
mainfrom
sign-inner-frameworks

Conversation

@FeodorFitsner

@FeodorFitsner FeodorFitsner commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #37. Pairs with dart-bridge#14 — both need to land, then a new dart_bridge release and a new date-keyed release here, before serious_python re-pins.

The finding

20260729 signed each outer .xcframework but left the .framework bundles inside every slice unsigned. An App Store IPA built against it:

Python-ios.xcframework-ios.signature   signed=true  isSecureTimestamp=false
_ssl.xcframework-ios.signature         signed=true  isSecureTimestamp=false
_hashlib.xcframework-ios.signature     signed=true  isSecureTimestamp=false
dart_bridge.xcframework-ios.signature  signed=true  isSecureTimestamp=false

signed flipped as intended — that was #37 working. The receipt cdhashes match the published artifacts byte for byte (_sslddb6c974196b0e16fef3c5e8defb4805b5d344ca), and the receipt's embedded leaf certificate is our Apple Distribution: Appveyor Systems Inc. (GXXRQJK434), so Xcode was reading our signatures. But isSecureTimestamp stayed false despite the outer seals carrying genuine Apple TSA timestamps.

Checked in both a development archive and an App Store export — identical, so the export method wasn't the variable. (Also learned the Signatures/ folder only travels in App Store–bound IPAs; development and ad-hoc exports omit it, and it lives in the .xcarchive in all cases.)

The control

krzyzanowskim/OpenSSL 3.6.3000 — accepted by Apple's scan per partout-io/openssl-apple#72. All ten slices are signed, outer signed last:

inner  OpenSSL.framework    Apple Distribution: Goodnotes Limited (C88F57F4TJ)  Timestamp 4:15:36–4:15:45
outer  OpenSSL.xcframework  Apple Distribution: Goodnotes Limited (C88F57F4TJ)  Timestamp 4:15:47

Ours: codesign -dvvv on any inner framework reported not signed at all. That was the only structural difference left.

The change

xcf_sign_one signs each slice's .framework first, outer bundle last. The order is not optional — the outer seal hashes the bundle contents, so signing an inner framework afterwards would invalidate it, which is the exact failure mode this repo's signing work exists to prevent. No -i on the inner bundles: a .framework carries a real CFBundleIdentifier in its own Info.plist.

Verification refactored into xcf_assert_signature (verifiable, not ad-hoc, secure timestamp, expected authority, expected team) and applied to every slice framework as well as the outer bundle. An unsigned inner framework now fails the release.

Still no --deep — Apple documents it as inappropriate for a distributable signature; signing each slice explicitly gives the same coverage correctly.

No script outside the shared helper changes. sign_darwin_archives.sh already signs the finished archives through xcf_sign_one, and inner _CodeSignature directories survive both xcodebuild -create-xcframework and the tar round trip (verified).

dart_bridge 1.7.1

manifest.json moves to 1.7.1, the first dart_bridge release whose framework slices are signed rather than only the outer xcframework.

Both halves have to move together: an IPA that picks up a 1.7.0 dart_bridge alongside inner-signed Python XCFrameworks would still carry one receipt reporting isSecureTimestamp = false, which leaves the question unanswered rather than answered. Version bump rather than a re-release, because consumer caches are version-keyed and hold the outer-only-signed 1.7.0 zip.

Merge order: dart-bridge#14 → release dart_bridge v1.7.1 → merge this → cut the new date-keyed release here. Merging this first would pin a manifest at a release that does not exist yet.

Expect a slower signing job

This adds roughly two extra codesign invocations per xcframework — about 500 TSA round trips across the three Python versions, up from ~170. sign-darwin-artifacts will take noticeably longer than on 20260729. If it becomes a problem, the fan-out is a one-line change to pipeline per archive.

Honest caveat

This makes our artifacts structurally identical to one Apple accepts, which is the strongest evidence available. It is not proof that isSecureTimestamp will flip — the field's semantics are undocumented, and the audited archive contained no signed third-party control to compare against. The definitive test is a TestFlight upload once this, dart-bridge 1.7.1, and the serious_python re-pin have all landed.

20260729 signed each outer .xcframework but left the .framework bundles inside
every slice unsigned. An App Store IPA built against it reported `signed = true`
but `isSecureTimestamp = false` for Python-ios, _ssl, _hashlib and dart_bridge,
even though the outer seals carry genuine Apple TSA timestamps -- the receipt
cdhashes match the published artifacts byte for byte, so Xcode was reading our
signatures. The same result appeared in a development archive and an App Store
export, so the export method was not the variable.

The control that settles it: krzyzanowskim/OpenSSL 3.6.3000, an XCFramework
Apple's App Store scan demonstrably accepts, signs all ten of its slices with an
Apple Distribution identity and a secure timestamp, then signs the outer bundle
last. An unsigned inner framework was the only structural difference left.

xcf_sign_one now signs each slice's .framework first and the outer bundle last.
The order is not optional: the outer seal hashes the bundle contents, so signing
an inner framework afterwards would invalidate it. No -i for the inner bundles --
a .framework has a real CFBundleIdentifier in its own Info.plist.

Verification was refactored into xcf_assert_signature (verifiable, not ad-hoc,
secure timestamp, expected authority and team) and now covers every slice
framework as well as the outer bundle, so an unsigned inner framework fails the
release rather than shipping.

No script outside the shared helper changes: sign_darwin_archives.sh already
signs the finished archives through xcf_sign_one, and inner _CodeSignature
directories survive both `xcodebuild -create-xcframework` and the tar round trip.

This does add roughly two extra codesign invocations per xcframework -- about 500
TSA round trips across the three Python versions -- so expect
sign-darwin-artifacts to take noticeably longer than on 20260729.
1.7.1 is the first dart_bridge release whose dart_bridge.framework slices are
signed, not just the outer xcframework -- the counterpart of the change on this
branch for the Python XCFrameworks (flet-dev/dart-bridge#14).

Both halves have to move together. An IPA that picks up a 1.7.0 dart_bridge
alongside inner-signed Python XCFrameworks would still carry one receipt
reporting isSecureTimestamp = false, which leaves the ITMS-91065 question
unanswered rather than answered.

Version-keyed consumer caches hold the outer-only-signed 1.7.0 zip, so this must
be a version bump rather than a re-release. Compiled binaries are unchanged from
1.7.0 on every platform.
@FeodorFitsner FeodorFitsner changed the title Sign inner .framework bundles too, not just the outer xcframework Sign inner .framework bundles too; dart_bridge 1.7.1 Jul 30, 2026
xcf_slice_frameworks located each slice's bundle as <slice>/<xcframework
basename>.framework. Everything this repo publishes has matching names, but the
assumption fails silently rather than loudly when it does not hold: serious_python
stages Python.xcframework as Python-<platform>.xcframework, and an unsigned inner
framework in a renamed copy would be reported as "no slice frameworks found"
instead of as unsigned.

Globbing <slice>/*.framework removes the assumption. xcf_signing_identifier now
walks the same enumerator instead of repeating the name-keyed lookup.

Verification only -- signed output is unchanged. Keeps this helper byte-identical
to dart-bridge's copy (flet-dev/dart-bridge#15).
@FeodorFitsner
FeodorFitsner merged commit 3c168a5 into main Jul 30, 2026
9 of 21 checks passed
@FeodorFitsner
FeodorFitsner deleted the sign-inner-frameworks branch July 30, 2026 20:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant