Skip to content

Bump to 4.5.1: sign and verify both XCFramework layers - #245

Merged
FeodorFitsner merged 2 commits into
mainfrom
bump-4.5.1
Jul 31, 2026
Merged

Bump to 4.5.1: sign and verify both XCFramework layers#245
FeodorFitsner merged 2 commits into
mainfrom
bump-4.5.1

Conversation

@FeodorFitsner

@FeodorFitsner FeodorFitsner commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #244. Pairs with python-build#38 / #39 and dart-bridge#14 / #16, all merged and released.

Result up front

signed is now true for all four artifacts named in ITMS-91065, verified in an App Store IPA. isSecureTimestamp is still false — 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. But isSecureTimestamp stayed 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:

  • The receipt records the OUTER signature. Its cdhashes matches the published outer xcframework exactly (dart_bridge 582388d2…, _ssl df6220fd…), not the inner framework's. Xcode reads the signature that does carry a TSA token, and reports false anyway.
  • Our timestamp is a genuine RFC 3161 token. Decoding _CodeSignature/CodeSignature shows an id-smime-aa-timeStampToken (1.2.840.113549.1.9.16.2.14) unsigned attribute, not merely a self-asserted signingTime.
  • Our CMS is structurally identical to the accepted artifact's, down to the Apple certificate extensions. There is no observable difference left between a signature Apple accepts and ours.

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=, not Timestamp=. Per Apple's own documentation, Signed Time means no secure timestamp, and that page's scope is notarization, i.e. macOS Developer ID.

The reasonable reading is that ITMS-91065: Missing signature tests signed, which is now true, and that isSecureTimestamp is 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_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, both found by building negative controls rather than by reading the code:

  • Slice frameworks are located by glob, not by the xcframework's name. stage_spm.sh stages Python.xcframework as Python-<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.
  • Signature presence is probed with codesign -dv, not by looking for _CodeSignature. A versioned macOS bundle keeps it under Versions/<name>/, and that name isn't always A — CPython uses Versions/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.framework layout — Headers and Modules were real directories at the framework root, which codesign rejects for a versioned bundle, and are now symlinks into Versions/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=require with the real team id, against the real signed artifacts:

prepare_ios     → provider signatures verified on 57 xcframework(s) and 115 slice framework(s)
sync            → provider artifacts unchanged (xcframeworks, python-xcframeworks, site-xcframeworks subset)
stage_spm ios   → unchanged; verified on 2 xcframework(s) and 5 slice framework(s)
prepare_macos   → verified on 2 xcframework(s) and 4 slice framework(s)
stage_spm macos → unchanged

Staged _ssl.xcframework inside the Swift package, both layers reporting Apple Distribution: Appveyor Systems Inc. (GXXRQJK434) with the original Timestamp=Jul 30, 2026 at 2:57:48 PM — python-build's signing job, so both signatures reached the package untouched. Stripping an inner _CodeSignature fails with provider 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.

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.
@FeodorFitsner
FeodorFitsner merged commit 9896a51 into main Jul 31, 2026
43 of 58 checks passed
@FeodorFitsner
FeodorFitsner deleted the bump-4.5.1 branch July 31, 2026 00:06
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