Bump to 4.5.1: sign and verify both XCFramework layers - #245
Merged
Conversation
4.5.0 shipped provider artifacts whose outer .xcframework was signed but whose
inner .framework bundles were not. An App Store IPA built against it reported
`signed = true` -- the 4.5.0 fix working, with receipt cdhashes matching the
published artifacts byte for byte -- but `isSecureTimestamp = false` for
Python-ios, _ssl, _hashlib and dart_bridge. Identical 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 scan demonstrably accepts, signs all ten of its slices with an Apple
Distribution identity and a secure timestamp and signs the outer bundle last. An
unsigned inner framework was the only structural difference left. python-build
20260730 and dart_bridge 1.7.1 now sign both layers.
Consumer verification follows: spv_verify_provider checks each slice's inner
framework as well as the outer bundle, so an unsigned slice fails during
packaging instead of surfacing in a submission. Two lookup bugs found while
building negative controls for it are fixed here too:
* Slice frameworks are located by glob rather than by the xcframework's name.
stage_spm.sh stages Python.xcframework as Python-<platform>.xcframework, and
a name-keyed lookup silently finds nothing there -- turning "this slice is
unsigned" into "there was nothing to check".
* Signature presence is probed with `codesign -dv` rather than by looking for a
_CodeSignature directory. A versioned macOS bundle keeps it under
Versions/<name>/ and that name is not always "A": CPython uses Versions/3.14,
so a correctly signed macOS framework was reported as unsigned.
Re-pins python-build to 20260730 and dart_bridge to 1.7.1. No Python version
moved (3.12.13 / 3.13.14 / 3.14.6); nothing about Android, Linux or Windows
changed. 20260730 also fixes the macOS Python.framework layout -- Headers and
Modules were real directories at the framework root, which codesign rejects for a
versioned bundle -- so they are now symlinks into Versions/Current.
Verified end to end against the signed artifacts in require mode with the real
team id: 57 xcframeworks and 115 slice frameworks verified at extraction, digests
unchanged through sync and both SwiftPM stagings, and the staged _ssl.xcframework
inside the Swift package reporting Apple Distribution on BOTH layers with its
original signing timestamp. Stripping an inner signature fails the run.
The 4.5.1 entry implied that signing both layers was the fix for
`isSecureTimestamp = false`. It is not, and shipping that claim unamended would
be wrong.
With both layers signed, the App Store receipts still read `signed = true` /
`isSecureTimestamp = false`. Three findings pin this down:
* The receipt's cdhash matches the OUTER xcframework's CDHash exactly
(dart_bridge 582388d2..., _ssl df6220fd...), not the inner framework's. Xcode
reads the outer signature -- the one that carries the TSA timestamp -- and
reports false anyway.
* Decoding the CMS in _CodeSignature/CodeSignature shows a genuine RFC 3161
id-smime-aa-timeStampToken (1.2.840.113549.1.9.16.2.14) unsigned attribute,
not merely a self-asserted signingTime.
* That CMS attribute set is structurally identical to the accepted OpenSSL
artifact's, down to the Apple certificate extensions -- so there is no
observable difference left between a signature Apple accepts and ours.
Every receipt in the archive reports false, and all record
signatureType = AppleDeveloperProgram; the accepted artifact is signed by an
Apple Distribution identity too and would carry the same type. The reasonable
reading is that ITMS-91065 tests `signed`, which is now true, and that
isSecureTimestamp is not reachable by signing.
Signing both layers is kept regardless: it matches what an accepted artifact
does, and it is correct practice for a distributed binary framework. No code
changes -- changelog only.
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.
Follow-up to #244. Pairs with python-build#38 / #39 and dart-bridge#14 / #16, all merged and released.
Result up front
signedis nowtruefor all four artifacts named in ITMS-91065, verified in an App Store IPA.isSecureTimestampis stillfalse— and the evidence says that is not something signing can change. Details below; the changelog records this honestly rather than implying a fix.What 4.5.0 got half-right
4.5.0 stopped destroying the publisher signature and made the receipts read
signed = true, with receipt cdhashes matching the published artifacts byte for byte. ButisSecureTimestampstayed false, identically in a development archive and an App Store export.The control — krzyzanowskim/OpenSSL 3.6.3000, accepted by Apple's scan — signs all ten of its slices and the outer bundle. Ours signed only the outer. That was the only structural difference, so python-build 20260730 and dart_bridge 1.7.1 now sign both layers.
It did not flip isSecureTimestamp
With both layers signed, the receipts still read
signed = true/isSecureTimestamp = false. Three findings pin down why that is not a defect we can fix:cdhashesmatches the published outer xcframework exactly (dart_bridge582388d2…,_ssldf6220fd…), not the inner framework's. Xcode reads the signature that does carry a TSA token, and reports false anyway._CodeSignature/CodeSignatureshows anid-smime-aa-timeStampToken(1.2.840.113549.1.9.16.2.14) unsigned attribute, not merely a self-assertedsigningTime.Two further data points: all 96 receipts in the archive report false, including third-party ones, so there is no positive control in an iOS archive; and nothing in the iOS pipeline produces a secure timestamp at all — the app itself and all 118 embedded frameworks carry
Signed Time=, notTimestamp=. Per Apple's own documentation,Signed Timemeans no secure timestamp, and that page's scope is notarization, i.e. macOS Developer ID.The reasonable reading is that
ITMS-91065: Missing signaturetestssigned, which is now true, and thatisSecureTimestampis not reachable for a Developer Program identity on iOS. Signing both layers is kept regardless: it matches what an accepted artifact does, and it is correct practice for a distributed binary framework.This PR
Consumer verification covers both layers.
spv_verify_providerchecks each slice's inner framework as well as the outer bundle, so an unsigned slice fails during packaging instead of surfacing in a submission.Two lookup bugs, both found by building negative controls rather than by reading the code:
stage_spm.shstagesPython.xcframeworkasPython-<platform>.xcframework; a name-keyed lookup silently finds nothing there, turning "this slice is unsigned" into "there was nothing to check". A check defeated by renaming a directory isn't a check.codesign -dv, not by looking for_CodeSignature. A versioned macOS bundle keeps it underVersions/<name>/, and that name isn't alwaysA— CPython usesVersions/3.14. A correctly signed macOS framework was being reported as unsigned.Snapshot re-pin
python-build 20260730, dart_bridge 1.7.1. No Python version moved (3.12.13 / 3.13.14 / 3.14.6, Pyodide unchanged); nothing about Android, Linux or Windows changed. 20260730 also fixes the macOS
Python.frameworklayout —HeadersandModuleswere real directories at the framework root, which codesign rejects for a versioned bundle, and are now symlinks intoVersions/Current.Version tables regenerated with
gen_version_tables --release-date 20260730; the no-arg re-run is a no-op.Verified end to end
SERIOUS_PYTHON_VERIFY_PROVIDER_SIGNATURES=requirewith the real team id, against the real signed artifacts:Staged
_ssl.xcframeworkinside the Swift package, both layers reportingApple Distribution: Appveyor Systems Inc. (GXXRQJK434)with the originalTimestamp=Jul 30, 2026 at 2:57:48 PM— python-build's signing job, so both signatures reached the package untouched. Stripping an inner_CodeSignaturefails withprovider signature does not verify: invalid resource directory.Next
TestFlight. ITMS-91065 arrives as an automated validation email minutes after upload, not after review, so it is fast feedback and now the only way to learn anything further.