Skip to content

Stamp Xcode build-provenance keys into generated framework Info.plists - #36

Merged
FeodorFitsner merged 1 commit into
mainfrom
fix/framework-info-plist-provenance
Jul 26, 2026
Merged

Stamp Xcode build-provenance keys into generated framework Info.plists#36
FeodorFitsner merged 1 commit into
mainfrom
fix/framework-info-plist-provenance

Conversation

@FeodorFitsner

Copy link
Copy Markdown
Contributor

What

create_plist() in darwin/xcframework_utils.sh wrote ten hand-authored keys and not a single DT* key. To Apple's static analysis, the frameworks we build from lib-dynload .sos don't look like anything Xcode produced.

It now emits the same key set Xcode stamps into a real framework, resolved once per run from the SDK actually in use (sw_vers -buildVersion, xcrun --sdk iphoneos --show-sdk-version / --show-sdk-build-version, xcodebuild -version):

BuildMachineOSBuild   DTPlatformName      DTSDKName      UIDeviceFamily
DTCompiler            DTPlatformVersion   DTXcode        UIRequiredDeviceCapabilities (device only)
DTPlatformBuild       DTSDKBuild          DTXcodeBuild

The generated key set is now identical to krzyzanowskim/OpenSSL's OpenSSL.framework — verified by diffing both plists.

Also fixes a latent bug: the simulator slice previously claimed CFBundleSupportedPlatforms=iPhoneOS. create_plist() takes the platform as a fourth argument (default iphoneos) and now labels each slice correctly — iPhoneSimulator / DTPlatformName=iphonesimulator / DTSDKName=iphonesimulator26.5, and no arm64 device capability on a fat arm64+x86_64 binary.

Why

flet-dev/flet#6724ITMS-91065: Missing signature on _ssl.framework and _hashlib.framework for a first App Store submission.

These missing keys are the most substantive difference we could find between our frameworks and third-party OpenSSL xcframeworks Apple accepts. The other candidates were ruled out by experiment:

  • Source signature — cannot matter. Re-signing krzyzanowskim's framework the way exportArchive does (codesign -f --preserve-metadata=identifier,flags,runtime,launch-constraints,library-constraints) destroys the certificate chain, team identifier and secure timestamp; only the identifier string survives, and that's derived from CFBundleIdentifier for an unsigned framework anyway.
  • Privacy manifest content — cannot be the differentiator. The accepted third-party package ships an empty stub with NSPrivacyAccessedAPITypes as <array/>, and our frameworks already carry a manifest (Apple emits no ITMS-91061 alongside the rejection).

Whether this is what Apple's scan reads is unverified. It's the cheapest remaining hypothesis and needs one App Store submission of a new app to test — the check only fires for new apps or updates that newly add a listed SDK, which is why existing Flet apps are unaffected.

Verification

$ bash -n darwin/xcframework_utils.sh                          # syntax OK
$ plutil -lint out-device.plist out-sim.plist                  # both OK
$ diff <(keys out-device.plist) <(keys OpenSSL.framework/Info.plist)
  IDENTICAL key set

Device slice output:

BuildMachineOSBuild => 25F71      DTPlatformBuild => 23F81a    DTXcode      => 2660
DTCompiler => …clang.1_0          DTSDKName => iphoneos26.5    DTXcodeBuild => 17F113
CFBundleSupportedPlatforms => [iPhoneOS]                       UIDeviceFamily => [1, 2]

Needs a python-build release and a snapshot re-pin in flet-dev/serious-python before it reaches an app build.

create_plist() emitted ten hand-written keys and not a single DT* key, so the
frameworks built from lib-dynload .so's did not look to Apple's tooling like
anything Xcode had produced. Now emits the same key set Xcode stamps into a
real framework: BuildMachineOSBuild, DTCompiler, DTPlatformBuild,
DTPlatformName, DTPlatformVersion, DTSDKBuild, DTSDKName, DTXcode,
DTXcodeBuild, UIDeviceFamily and (device slice only) UIRequiredDeviceCapabilities.

Values are resolved once per run from the SDK actually in use — sw_vers,
xcrun --show-sdk-version/--show-sdk-build-version, and xcodebuild -version.

Also fixes the simulator slice, which previously claimed
CFBundleSupportedPlatforms=iPhoneOS. create_plist() now takes the platform as
its fourth argument and labels each slice correctly.

Context: flet-dev/flet#6724 (ITMS-91065 on _ssl/_hashlib). The missing DT*
keys are the most substantive difference we could find between these
frameworks and third-party OpenSSL xcframeworks the App Store accepts. Whether
that is what Apple's scan reads is unverified — a source signature was ruled
out experimentally (codesign -f at export destroys everything but the
identifier) and the privacy manifest was ruled out (the accepted third-party
package ships an empty stub).
@FeodorFitsner
FeodorFitsner merged commit 00085b0 into main Jul 26, 2026
19 of 32 checks passed
@FeodorFitsner
FeodorFitsner deleted the fix/framework-info-plist-provenance branch July 26, 2026 21:34
FeodorFitsner added a commit to flet-dev/serious-python that referenced this pull request Jul 27, 2026
…ts (#242)

* Bump to 4.4.2: Xcode build-provenance keys in iOS framework Info.plists

Re-pins the bundled python-build snapshot to 20260727, which stamps the DT*
build-provenance keys Xcode writes into a real framework's Info.plist into
every framework generated from a lib-dynload .so, and labels the simulator
slice as iPhoneSimulator instead of iPhoneOS (flet-dev/python-build#36).

No versions moved: Python 3.12.13 / 3.13.14 / 3.14.6, Pyodide, and
dart_bridge 1.6.1 are all unchanged from 20260726.

This is the next hypothesis under test for the ITMS-91065 rejection in
flet-dev/flet#6724, not a confirmed fix — see the serious_python_darwin 4.4.2
entry for what was ruled out experimentally and what remains open.

* iOS: namespace framework bundle identifiers under the host app

Every Python C-extension ships as its own embedded framework, and each carried
a fixed `org.python.<module>` CFBundleIdentifier -- org.python.ssl,
org.python.hashlib, ... -- byte-identical in every app ever built with
serious_python.

A framework's bundle identifier also becomes its CODE SIGNING identifier, and
that is the one field that survives the `codesign -f` Xcode applies at embed
and again at exportArchive (everything else -- certificate chain, team
identifier, secure timestamp -- is destroyed). A globally-shared identifier on
a framework Apple fingerprints as a listed third-party SDK is the leading
explanation for the ITMS-91065 rejection in flet-dev/flet#6724.

Setting SERIOUS_PYTHON_BUNDLE_ID rewrites them to `<app id>.<module>` with
underscores mapped to hyphens, matching CPython's own iOS support
(Platforms/Apple/testbed/Python.xcframework/build/utils.sh). The leading hyphen
for underscore-prefixed modules (`_ssl` -> `<app>.-ssl`) is deliberate: it is
what keeps `_ssl` distinct from `ssl`, and it is the form CPython ships.

The rewrite runs before reconcile_framework_install_names, whose ad-hoc re-sign
reseals the modified plists; the stdlib xcframeworks are unsigned at that point
so editing them invalidates nothing. Module names come from arbitrary wheels,
so anything outside CFBundleIdentifier's [A-Za-z0-9.-] is mapped to a hyphen,
and an invalid SERIOUS_PYTHON_BUNDLE_ID leaves the defaults in place with a
warning rather than emitting a plist that fails late at export.

flet passes the value from flet-dev/flet#6731.

* iOS: namespace Python.framework's bundle id too

Python.xcframework is staged straight out of dist/xcframeworks by stage_spm.sh
and vendored from there by the podspec, so it never passes through
site-xcframeworks and kept a shared `org.python.python` -- identical in every
app, the same class of identifier the previous commit fixed for the extension
frameworks.

Verified inert at runtime before renaming: `org.python.python` appears in no
shipped binary, and neither Python.framework nor App.framework contains a
CFBundleGetBundleWithIdentifier / bundleWithIdentifier lookup. serious_python
sets PYTHONHOME explicitly and resolves extensions through the .fwork/.origin
path mechanism, not bundle identifiers.

rewrite_framework_bundle_ids takes an optional skip list so dart_bridge keeps
`dev.flet.dartbridge` -- a vendor-owned reverse-DNS identifier is the shape
we're aiming for, not the problem we're fixing.

Confirmed against a real build (flet playground abc1.xcarchive): 93 of 98
frameworks were already namespaced, no duplicate identifiers; this covers
Python.framework, leaving only Flutter's own three (App, Flutter, objective_c)
and dart_bridge.

* examples: pin jni to 1.0.0 (1.0.1 breaks aarch64 Linux builds)

jni 1.0.1, published 2026-07-27T01:10Z, added global_jni_env.c, which passes a
`va_list` where a `void *` is expected. On x86_64 va_list is an array type that
decays to a pointer so it compiles; on aarch64 it is a struct, so it is a hard
error and `flutter build linux` fails with 19 of them. Tracked upstream at
dart-lang/native#3498.

This surfaced on the release PR rather than as a scheduled failure because the
examples depend on the serious_python packages by path: bumping their version
invalidates the committed pubspec.lock, pub re-resolves the whole graph, and
picks up the newest transitive versions. Any PR touching package versions after
2026-07-27T01:10Z would have hit it. jni itself arrives via
path_provider -> path_provider_android -> jni_flutter -> jni.

jni_flutter 1.0.1 declares `jni: ^1.0.0`, so 1.0.0 satisfies it -- the override
narrows within the allowed range rather than overriding a constraint. Applied to
all three examples; only bridge_example runs in CI, but the other two break
identically for anyone building them on an ARM64 Linux host.

The flask_example / run_example locks also pick up the current path-dependency
version (4.0.0 -> 4.4.1); they were simply stale.
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