Skip to content

Provider-sign every published Apple XCFramework; dart_bridge 1.7.0 - #37

Merged
FeodorFitsner merged 2 commits into
mainfrom
sign-apple-xcframeworks
Jul 29, 2026
Merged

Provider-sign every published Apple XCFramework; dart_bridge 1.7.0#37
FeodorFitsner merged 2 commits into
mainfrom
sign-apple-xcframeworks

Conversation

@FeodorFitsner

Copy link
Copy Markdown
Contributor

Why

Xcode records the state of each .xcframework an app links against as its publisher shipped it, and writes the result into the IPA as Signatures/<name>.xcframework-<platform>.signature. Everything we publish was unsigned, so those receipts read signed = false / isSecureTimestamp = false — which is what Apple's App Store scan reports as ITMS-91065: Missing signature (flet-dev/flet#6724).

The app's own signature does not fill this in. Xcode re-signs the embedded copy with the submitting team's identity; the SDK-origin receipt is a separate record. See Verifying the origin of your XCFrameworks.

This is the second of three repos. dart-bridge#13 shipped as 1.7.0 and is verified signed; serious_python follows, to preserve these signatures through staging.

Stable provider-owned identifiers — darwin/xcframework_identifiers.sh

A framework's CFBundleIdentifier becomes its code-signing identifier, and it is baked in here long before anyone knows which app will embed it. Two properties matter:

  • It must not depend on the consuming app. serious_python used to rewrite it downstream, which edits an Info.plist inside a signed bundle and destroys the very signature the receipt reports on. That rewrite is being removed in the third PR; this side makes it unnecessary.
  • It must be unique per framework. _ssl and _hashlib sharing an identifier is the kind of collision that only shows up as a confusing validation error.

So: dev.flet.python.runtime for the runtime, dev.flet.python.<module> for each stdlib extension, replacing the shared org.python.*. Leading hyphens are kept (_ssldev.flet.python.-ssl), which is CPython's own convention and keeps _ssl distinct from a hypothetical ssl — the previous code stripped them.

Validation asserts every identifier is syntactically valid, unique across the set, identical across device and simulator slices, and provider-prefixed.

Signing — darwin/xcframework_signing.sh, darwin/sign_darwin_archives.sh

Signing has to be the last thing that happens to a bundle, and it needs a release certificate. Operating on the finished archives satisfies both: by the time an archive exists, every mutation — install names, plists, privacy manifests, header overlays, pruning, stripping, bytecode compilation — is done, and the certificate never has to be present in the build matrix that runs on every push and PR.

Per archive: extract → validate identifiers → sign every outer xcframework, verifying each immediately → re-pack → extract the re-packed archive into a fresh directory and verify again. The re-pack preserves the original member-path shape (./install/… vs install/…), because consumers extract with --wildcards patterns that match one and not the other.

The outer seal carries an explicit -i, read off the xcframework's own inner framework — an .xcframework root Info.plist is an XFWK manifest with no CFBundleIdentifier, so codesign would otherwise fall back to the bundle's file name.

Verification asserts: an outer _CodeSignature/CodeResources; codesign --verify --strict; not ad-hoc; a real Timestamp= rather than the self-asserted Signed Time= a --timestamp-less signature carries; the expected authority and TeamIdentifier; and that the seal's identifier matches the inner framework's. No --deep, no --timestamp=none.

package-ios-for-dart.sh additionally asserts the _ssl / _hashlib privacy manifests survived into both slices — a silent miss there surfaces as an App Store rejection, not a build error.

CI

sign-darwin-artifacts is a release-only job in the protected release-signing environment, gated on a workflow_dispatch with a release_date from main. It imports the p12 into a temporary keychain without -A (only codesign and security may reach the key), derives exactly one identity fingerprint and fails on any other count, and deletes the keychain in an always() step.

The unsigned build artifact is renamed darwin-unsigned-* rather than python-darwin-*, specifically so that no python-* download pattern in publish-release can reach it. publish-release now downloads Android/Linux/Windows by explicit pattern plus the python-darwin-signed bundle, is gated on main like the signing job, and fails if the expected Darwin tarballs are absent.

The mobile-forge Python.xcframework gets its identifier immediately after build_ios.py finishes — that script strips and rewrites inside the bundle, so that is the first moment the artifact is final.

Builds without credentials still work and produce unsigned artifacts. REQUIRE_XCFRAMEWORK_SIGNATURE=1, set on the release path, turns every missing credential, missing timestamp, wrong team, or empty archive into a hard failure.

dart_bridge 1.7.0

manifest.json moves to 1.7.0, the first dart_bridge release whose xcframework is provider-signed. Both halves have to move together or the IPA still reports one of them as unsigned. Version bump rather than a re-release, because consumer caches are version-keyed and hold the unsigned 1.6.1 zip.

Verified locally

Against a real python-ios-dart-3.14.6.tar.gz:

  • 56 xcframeworks validated — unique, slice-consistent, provider-prefixed identifiers
  • re-packed archive has a byte-identical 2122-entry member list
  • codesign --verify --strict survives a tar round trip, including the versioned macOS Python.framework with its Versions/Current symlinks
  • verifier correctly rejects unsigned and ad-hoc bundles
  • identifier derivation resolves under both the flat iOS and versioned macOS layouts

Before merging

release-signing needs to exist in this repo with APPLE_DISTRIBUTION_CERT_P12_BASE64 + APPLE_DISTRIBUTION_CERT_P12_PASSWORD secrets and an XCFRAMEWORK_EXPECTED_TEAM_ID variable (GXXRQJK434). Unlike dart-bridge, the deployment protection here should be a branch rule for main — releases are dispatched, not tagged.

A workflow_dispatch without a release_date exercises the full matrix and never touches the certificate, so that is the safe rehearsal for the identifier and validation changes before any secret is configured.

Xcode records the state of each .xcframework an app links against AS ITS
PUBLISHER SHIPPED IT and writes that into the IPA as
Signatures/<name>.xcframework-<platform>.signature. Everything we publish was
unsigned, so those receipts read `signed = false` / `isSecureTimestamp = false` --
which is what Apple's App Store scan reports as ITMS-91065: Missing signature
(flet-dev/flet#6724). The app's own signature does not fill this in: Xcode
re-signs the embedded copy with the submitting team's identity, and the
SDK-origin receipt is a separate record.

Stable provider-owned identifiers (darwin/xcframework_identifiers.sh)

A framework's CFBundleIdentifier becomes its code-signing identifier and is baked
in here, long before anyone knows which app will embed it. It must therefore not
depend on the consuming app -- serious_python used to rewrite it downstream,
which edits an Info.plist inside a signed bundle and destroys the very signature
the receipt reports on. The Python runtime is now dev.flet.python.runtime and each
stdlib extension dev.flet.python.<module>, replacing the shared org.python.*.
Leading hyphens are kept (`_ssl` -> `dev.flet.python.-ssl`), matching CPython's own
convention, so `_ssl` cannot collide with a hypothetical `ssl`. Validation asserts
every identifier is syntactically valid, unique across the set, identical across
device and simulator slices, and provider-prefixed.

Signing (darwin/xcframework_signing.sh, darwin/sign_darwin_archives.sh)

Signing must be the last thing that happens to a bundle and it needs a release
certificate. Doing it on the finished archives satisfies both: by the time an
archive exists, every mutation -- install names, plists, privacy manifests, header
overlays, pruning, stripping, bytecode compilation -- is done, and the certificate
never has to be present in the build matrix that runs on every push and PR.

Per archive: extract, validate identifiers, sign every outer xcframework and
verify each immediately, re-pack, then extract the re-packed archive into a fresh
directory and verify again. The re-pack preserves the original member-path shape
("./install/..." vs "install/...") because consumers extract with --wildcards
patterns that match one and not the other.

The outer seal is stamped with an explicit `-i`, read off the xcframework's own
inner framework: an .xcframework root Info.plist is an XFWK manifest with no
CFBundleIdentifier, so codesign would otherwise fall back to the bundle's file
name. Verification asserts an outer _CodeSignature/CodeResources, `codesign
--verify --strict`, a non-ad-hoc signature, a real Timestamp= (not the
self-asserted Signed Time=), the expected authority and TeamIdentifier, and that
the seal's identifier matches the inner framework's. No --deep, no
--timestamp=none.

package-ios-for-dart.sh additionally asserts the _ssl and _hashlib privacy
manifests survived into both slices -- a silent miss there surfaces as an App
Store rejection, not a build error.

CI

sign-darwin-artifacts is a release-only job in the protected `release-signing`
environment, gated on a workflow_dispatch with a release_date from main. It
imports the p12 into a temporary keychain without -A (only codesign and security
may reach the key), derives exactly one identity fingerprint and fails on any
other count, and deletes the keychain in an always() step.

The unsigned build artifact is renamed darwin-unsigned-* rather than
python-darwin-*, specifically so no `python-*` download pattern in publish-release
can reach it. publish-release now downloads Android/Linux/Windows by explicit
pattern plus the python-darwin-signed bundle, is gated on main like the signing
job, and fails if the expected Darwin tarballs are absent.

The mobile-forge Python.xcframework gets its identifier immediately after
build_ios.py finishes -- that script strips and rewrites inside the bundle, so it
is the first moment the artifact is final.

Local builds without credentials still work and produce unsigned artifacts.
REQUIRE_XCFRAMEWORK_SIGNATURE=1, set on the release path, turns every missing
credential, missing timestamp, wrong team, or empty archive into a hard failure.
1.7.0 is the first dart_bridge release whose dart_bridge.xcframework carries a
provider signature with a secure timestamp, sealed under dev.flet.dartbridge --
the counterpart of what this branch does for the Python XCFrameworks. Both halves
have to move together, or the IPA still reports one of them as unsigned.

Version-keyed consumer caches hold the unsigned 1.6.1 zip, so this must be a
version bump rather than a re-release. Compiled binaries are unchanged from 1.6.1.
if: >-
github.event_name == 'workflow_dispatch'
&& inputs.release_date != ''
&& github.ref == 'refs/heads/main'
@FeodorFitsner
FeodorFitsner merged commit 7d064ec into main Jul 29, 2026
17 of 34 checks passed
@FeodorFitsner
FeodorFitsner deleted the sign-apple-xcframeworks branch July 29, 2026 18:56
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.

2 participants