Skip to content

Apple: provider-sign the published xcframework (1.7.0) - #13

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

Apple: provider-sign the published xcframework (1.7.0)#13
FeodorFitsner merged 2 commits into
mainfrom
apple-provider-sign-xcframework

Conversation

@FeodorFitsner

Copy link
Copy Markdown
Contributor

Why

Xcode records the state of every .xcframework an app links against as its publisher shipped it, and writes the result into the IPA as Signatures/dart_bridge.xcframework-ios.signature. Ours was unsigned, so that receipt 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 at embed and again at exportArchive, but the SDK-origin receipt is a separate record and is unaffected by it. See Verifying the origin of your XCFrameworks.

What changed

apple/xcframework_signing.sh (new) — signing and verification helpers.

Signing happens after xcodebuild -create-xcframework and before the zip: the one point where the bundle is complete and still unmutated. The signature is then verified again after the zip is extracted into a fresh directory, so a packaging bug fails here rather than in a consumer's app. (The symlink flattening fixed in 1.6.1 would have surfaced as exactly that.)

Verification asserts:

  • an outer _CodeSignature/CodeResources
  • codesign --verify --strict
  • not an ad-hoc signature
  • a real Timestamp= — not the self-asserted Signed Time= a --timestamp-less signature carries, which is what isSecureTimestamp reflects
  • the expected authority (Apple Distribution) and TeamIdentifier
  • the outer seal's identifier matches the inner framework's

No --deep, no --timestamp=none.

Explicit signing identifier. An .xcframework's root Info.plist is an XFWK manifest with no CFBundleIdentifier, so codesign would fall back to the bundle's file name and seal it as a bare Identifier=dart_bridge. The identifier is now read off the xcframework's own inner framework (flat iOS layout or versioned macOS layout) and passed as -i, giving Identifier=dev.flet.dartbridge. No mapping table to keep in sync, and it cannot drift from the framework it wraps.

CI. The certificate is confined to a new release-only build-apple-signed job in the protected release-signing environment. 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.

build-apple keeps running unprivileged on branches and PRs, but is skipped on tags and now uploads its artifact as …-unsigned, so a release run cannot produce one. publish depends on build-apple-signed and refuses to publish if an unsigned Apple artifact turns up anyway.

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

Verified locally

Full run with the real Apple Distribution identity:

xcframework-signing: signing dist/dart_bridge.xcframework as dev.flet.dartbridge
Identifier=dev.flet.dartbridge
Authority=Apple Distribution: Appveyor Systems Inc. (GXXRQJK434)
Timestamp=Jul 29, 2026 at 11:13:17 AM
TeamIdentifier=GXXRQJK434
Sealed Resources version=2 rules=10 files=9

Identical CDHash before and after the zip round trip. Also checked that the verifier rejects unsigned and ad-hoc bundles, and that identifier derivation resolves under both the flat iOS and versioned macOS framework 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), restricted to v*.*.* tags. Without them the next tag push fails at preflight instead of publishing something unsigned — which is the intended failure mode, but worth setting up first.

Version

Bumped to 1.7.0. Compiled binaries are identical to 1.6.1; only the packaging of the published artifact changed. Do not re-release over 1.6.1 — consumer caches are version-keyed and hold the unsigned zip.

Part of the three-repo effort; python-build (signing the Python XCFrameworks) and serious_python (preserving those signatures through staging) follow.

Xcode records the state of every .xcframework an app links against AS ITS
PUBLISHER SHIPPED IT, and writes that into the IPA as
Signatures/dart_bridge.xcframework-ios.signature. Ours was unsigned, so that
receipt read `signed = false` / `isSecureTimestamp = false` -- which is what
Apple's App Store scan reports as ITMS-91065: Missing signature.

The app's own signature does not fill this in. Xcode re-signs the embedded copy
with the submitting team's identity at embed and again at exportArchive, but the
SDK-origin receipt is a separate record and is unaffected.

apple/xcframework_signing.sh holds the signing + verification helpers. Signing
happens after `xcodebuild -create-xcframework` and before the zip -- the one
point where the bundle is complete and still unmutated -- and the signature is
verified again after the zip is extracted into a fresh directory, so a packaging
bug fails here instead of in a consumer's app. (The symlink flattening fixed in
1.6.1 would have shown up as exactly that.)

Verification asserts an outer _CodeSignature/CodeResources, `codesign --verify
--strict`, a non-ad-hoc signature, a real Timestamp= (not the self-asserted
Signed Time= a --timestamp-less signature carries), the expected authority, and
the expected TeamIdentifier. No --deep, no --timestamp=none.

CI: the certificate is confined to a new release-only build-apple-signed job in
the protected `release-signing` environment. 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 if the keychain yields any other
count, and deletes the keychain in an always() step. Selecting by display name
is ambiguous when a keychain holds more than one match and codesign then picks
arbitrarily.

build-apple keeps running unprivileged on branches and PRs but is skipped on
tags and now uploads its artifact as ...-unsigned, so a release run cannot
produce one. publish depends on build-apple-signed and refuses to publish if an
unsigned Apple artifact turns up anyway.

Local and PR builds without credentials still succeed and produce an unsigned
artifact; REQUIRE_XCFRAMEWORK_SIGNATURE=1 (set on the release path) turns every
missing credential, missing timestamp, wrong team, or empty tree into a hard
failure.

dev.flet.dartbridge is unchanged -- it was already a stable, publisher-owned
identifier, which is what lets one signature cover every app that embeds it.
An .xcframework's root Info.plist is an XFWK manifest -- AvailableLibraries,
CFBundlePackageType, XCFrameworkFormatVersion -- with no CFBundleIdentifier at
all. codesign therefore fell back to the bundle's file name and the seal reported
a bare `Identifier=dart_bridge` rather than a reverse-DNS one.

xcf_signing_identifier reads the identifier off the xcframework's own inner
framework Info.plist (flat iOS layout or versioned macOS layout, skipping the
Versions/Current symlink) and xcf_sign_one passes it as `-i`. The identifier is
already stable and provider-owned in every artifact we publish, so the outer seal
and the framework it wraps agree by construction and neither depends on the
consuming application -- no mapping table to keep in sync.

Verification asserts the two still agree, which catches a re-sign that dropped
the flag. It is skipped, not failed, when the inner framework cannot be located
by name: serious_python stages Python.xcframework as Python-<platform>.xcframework
and the rename is legitimate.

Signing fails outright if no inner framework Info.plist carries a
CFBundleIdentifier, rather than silently falling back to the file name again.
@FeodorFitsner
FeodorFitsner merged commit 399bfd1 into main Jul 29, 2026
36 checks passed
@FeodorFitsner
FeodorFitsner deleted the apple-provider-sign-xcframework branch July 29, 2026 18:16
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