Provider-sign every published Apple XCFramework; dart_bridge 1.7.0 - #37
Merged
Conversation
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' |
This was referenced Jul 29, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Xcode records the state of each
.xcframeworkan app links against as its publisher shipped it, and writes the result into the IPA asSignatures/<name>.xcframework-<platform>.signature. Everything we publish was unsigned, so those receipts readsigned = false/isSecureTimestamp = false— which is what Apple's App Store scan reports asITMS-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_pythonfollows, to preserve these signatures through staging.Stable provider-owned identifiers —
darwin/xcframework_identifiers.shA framework's
CFBundleIdentifierbecomes its code-signing identifier, and it is baked in here long before anyone knows which app will embed it. Two properties matter:serious_pythonused to rewrite it downstream, which edits anInfo.plistinside 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._ssland_hashlibsharing an identifier is the kind of collision that only shows up as a confusing validation error.So:
dev.flet.python.runtimefor the runtime,dev.flet.python.<module>for each stdlib extension, replacing the sharedorg.python.*. Leading hyphens are kept (_ssl→dev.flet.python.-ssl), which is CPython's own convention and keeps_ssldistinct from a hypotheticalssl— 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.shSigning 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/…vsinstall/…), because consumers extract with--wildcardspatterns that match one and not the other.The outer seal carries an explicit
-i, read off the xcframework's own inner framework — an.xcframeworkrootInfo.plistis anXFWKmanifest with noCFBundleIdentifier, 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 realTimestamp=rather than the self-assertedSigned Time=a--timestamp-less signature carries; the expected authority andTeamIdentifier; and that the seal's identifier matches the inner framework's. No--deep, no--timestamp=none.package-ios-for-dart.shadditionally asserts the_ssl/_hashlibprivacy manifests survived into both slices — a silent miss there surfaces as an App Store rejection, not a build error.CI
sign-darwin-artifactsis a release-only job in the protectedrelease-signingenvironment, gated on aworkflow_dispatchwith arelease_datefrommain. It imports the p12 into a temporary keychain without-A(onlycodesignandsecuritymay reach the key), derives exactly one identity fingerprint and fails on any other count, and deletes the keychain in analways()step.The unsigned build artifact is renamed
darwin-unsigned-*rather thanpython-darwin-*, specifically so that nopython-*download pattern inpublish-releasecan reach it.publish-releasenow downloads Android/Linux/Windows by explicit pattern plus thepython-darwin-signedbundle, is gated onmainlike the signing job, and fails if the expected Darwin tarballs are absent.The mobile-forge
Python.xcframeworkgets its identifier immediately afterbuild_ios.pyfinishes — 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.jsonmoves 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:codesign --verify --strictsurvives a tar round trip, including the versioned macOSPython.frameworkwith itsVersions/CurrentsymlinksBefore merging
release-signingneeds to exist in this repo withAPPLE_DISTRIBUTION_CERT_P12_BASE64+APPLE_DISTRIBUTION_CERT_P12_PASSWORDsecrets and anXCFRAMEWORK_EXPECTED_TEAM_IDvariable (GXXRQJK434). Unlike dart-bridge, the deployment protection here should be a branch rule formain— releases are dispatched, not tagged.A
workflow_dispatchwithout arelease_dateexercises 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.