ipa-resign
v0.0.3
Published
Resign iOS `.ipa` files with a **free Apple ID** so they install on a specific iPhone or iPad. Same trick AltStore / SideStore / Sideloadly use, but as a Node.js library you can drive from any tool and the shared, transport-agnostic **protocol core** behi
Readme
ipa-resign
Resign iOS .ipa files with a free Apple ID so they install on a
specific iPhone or iPad. Same trick AltStore / SideStore / Sideloadly use, but
as a Node.js library you can drive from any tool and the shared, transport-agnostic
protocol core behind the browser app in the sibling
apple-resign-web repo.
Runs on macOS, Linux, Windows and web.
What this is
If you download an .ipa (an EAS internal-distribution build, a TestFlight
export, anything ad-hoc-signed), iOS will reject the install on your phone
unless the IPA's provisioning profile lists your device's UDID. The fix is
to re-sign the IPA with a development cert + profile that does include
your device.
Apple gives every Apple ID a free 7-day development certificate for exactly this purpose. They just don't make it easy to use without Xcode. This library implements the protocol Xcode uses internally so any tool can resign IPAs without shelling out to Xcode — and without even running on a Mac.
Status
macOS, Linux, and Windows; free Apple ID; main .app only. See
CLAUDE.md for architectural detail.
| Capability | Status |
| ----------------------------------------- | --------------------------------------------------------- |
| Sign in with Apple ID + password | ✅ |
| Two-factor auth (trusted device + SMS) | ✅ |
| Mint / revoke development certificates | ✅ |
| Register a device for signing | ✅ |
| Create / delete an App ID | ✅ |
| Download a provisioning profile | ✅ |
| Resign main .app (zsign, inside-out) | ✅ |
| Repack into an installable .ipa | ✅ |
| macOS / Linux / Windows | ✅ x64 + arm64 |
| App extensions (.appex, WatchKit) | ❌ Free-account limit. --strip-extensions removes them. |
How it stays cross-platform
The same code runs on all three desktop OSes — no macOS-only tooling:
| Job | How |
| --------------------------- | ------------------------------------------------------------------------- |
| Code signing | zsign — C++ binary in bin/ |
| Unzip / repack IPA | pure-Node yauzl + yazl (preserves modes + symlinks) |
| Decode .mobileprovision | pure-Node PKCS#7 plist extraction |
| Secret storage | 0600 JSON file at ~/.orbit/apple-resign/secrets.json |
| Anisette (OTP) headers | Swift helper on macOS; Rust helper + Apple Music for Android libs elsewhere |
Architecture — one protocol core, two consumers
The Apple GrandSlam + developer-portal protocol lives here once and is shared by two
front-ends: this Node CLI/library, and the browser app in the sibling apple-resign-web
repo (which also esbuild-bundles into a native iOS WKWebView). A protocol fix lands in one
place and both consumers get it.
The protocol is transport-agnostic — it depends only on a small injected Platform port
(HTTP + anisette), never on fetch/node:/fs directly. Crypto goes through node-forge
(sync + isomorphic). Each consumer wires its own adapter:
| | Node CLI (this repo) | Browser app (apple-resign-web) |
| -------- | -------------------------------------------- | -------------------------------------------- |
| HTTP | appleFetch + system-ca (Apple private roots) | fetch via the /api/apple-proxy CORS shim |
| Crypto | node-forge | node-forge |
| Anisette | native helper binary (bin/anisette*) | ipa-resign/anisette-browser (in-browser WASM) |
| Signer | zsign binary (src/sign/codesign.ts) | ipa-resign/sign-browser (zsign-wasm) |
| Storage | 0600 JSON secret file + state.json | localStorage |
Subpath exports
ipa-resign— the Node library/CLI surface (the Usage section below).ipa-resign/core— the pure, transport-agnostic protocol (gsa, srp, devPortal, csr, errors, cert roots, entitlements) + thePlatformport. ESM, no Node builtins; the browser app imports this and supplies a browser adapter.ipa-resign/anisette-browser— the in-browser anisette generator (device attestation via WASM). Browser-only; consumed by the web app + its native WKWebView bundle.ipa-resign/sign-browser— browser-side IPA re-signing via zsign compiled to WASM (thezsign-wasmpackage; its glue is served same-origin by the host app). Browser-only; the web counterpart to the Nodezsignbinary path.ipa-resign/assets/anisette— the vendored anisette-js- Unicorn WASM the browser generator fetches at runtime (source of truth; the web app syncs
these into its
public/).
- Unicorn WASM the browser generator fetches at runtime (source of truth; the web app syncs
these into its
Internally the Node protocol modules (src/auth/gsa.ts, src/devPortal.ts, src/auth/srp.ts,
src/sign/csr.ts) are thin adapters that wrap core with the Node Platform (src/platform.ts)
and re-wrap bytes as Buffer for the CLI. Full design:
TRANSPORT-AGNOSTIC-PLAN.md and
ANISETTE-MIGRATION-PLAN.md.
Install
yarn install
yarn build # dist/: bytecode CLI bundle (index.jsc) + the ipa-resign/core and
# ipa-resign/anisette-browser subpath builds + bundled .d.tsyarn build is platform-agnostic — it just bundles TypeScript (via esbuild). It runs
three steps: the bytecode CLI build, then build:core and build:anisette-browser (the
two browser-consumable subpaths, which you can also run on their own). The native helper
binaries are built separately because they require platform-specific toolchains:
# Build the macOS anisette helper (only works on macOS — needs swiftc, i.e. Xcode CLT).
# Produces bin/anisette as a universal arm64 + x86_64 Mach-O.
yarn build:helper:macos
# Cross-compile the Rust anisette helper for Windows + Linux from any host.
# Produces bin/anisette-{linux,win}-{x64,arm64}{.exe}. Requires zig + cargo-zigbuild
# for the Linux targets and cross + Docker for the Windows targets. See
# anisette/orbit-anisette-rs/README.md for setup.
yarn build:helper:rustEnd users installing the package via npm don't run any of the helper scripts —
the published package ships with the per-platform binaries already built
(bin/anisette* for OTP headers, bin/zsign* for code signing).
src/auth/anisette.ts and src/sign/codesign.ts pick the right binary at
runtime by inspecting process.platform + process.arch.
On Linux and Windows, the anisette helper also needs Apple Music for
Android's libstoreservicescore.so. The library fetches Apple's universal
Apple Music APK on first resign and extracts the native libraries into your
user data dir automatically — no manual setup. macOS links Apple's
AOSKit.framework natively and skips this.
Usage
import {
signInAsync,
submitTwoFactorCodeAsync,
resignIpaAsync,
AppleTwoFactorRequiredError,
} from "ipa-resign";
// First-time sign-in. Apple pushes a 6-digit code to a trusted device or SMS.
try {
await signInAsync({ appleId: "[email protected]", password: "…" });
} catch (err) {
if (err instanceof AppleTwoFactorRequiredError) {
const code = await promptUserFor2FACode(err.details);
await submitTwoFactorCodeAsync({
appleId: "[email protected]",
password: "…",
code,
});
} else {
throw err;
}
}
// The session is now persisted in the local secret file; future calls
// (resignIpaAsync, etc.) work without re-auth until the token expires.
const result = await resignIpaAsync({
ipaPath: "/path/to/build.ipa",
deviceUdid: "00008130-XXXXXXXXXXXXXXXX",
deviceName: "Gabriel's iPhone",
appleId: "[email protected]",
outputIpaPath: "/path/to/resigned.ipa",
stripExtensions: true, // see "Caveats"
onProgress: (step, detail) => console.log(step, detail),
});
console.log(result.resignedIpaPath); // → /path/to/resigned.ipa
console.log(result.bundleId); // → the (possibly rewritten) bundle id
console.log(result.profileExpiresAt); // → Date, 7 days outInstall on the device using your favorite usbmuxd / installation_proxy
client (libimobiledevice's ideviceinstaller, xcrun devicectl, Orbit's
built-in usbmux stack, …). The library only resigns — it does not install.
Other exported helpers
import {
signOutAsync, // wipe the persisted session for an Apple ID
listAppIdsForAccountAsync, // list App IDs on the primary team
deleteAppIdForAccountAsync, // free a slot against the 10-per-7-days cap
} from "ipa-resign";Each pipeline step is also exported for power use — see src/devPortal.ts,
src/sign/*, and src/auth/*.
How the free-Apple-ID flow works
- Sign in (Grand Slam Authentication). SRP-6a (Apple-flavored) + anisette over
gsa.apple.com/grandslam/GsService2. On success, Apple returns an encrypted SPD blob that decrypts to{ adsid, GsIdmsToken, sk, c, … }. - Two-factor auth. Trusted-device path validates via
GET /grandslam/GsService2/validate; SMS path usesPOST /auth/verify/phone/securitycode. After validate, the next sign-in completes without challenge. - Mint per-service token. Another
/grandslam/GsService2POST witho: apptokensreturns an AES-256-GCMetpayload (with a"XYZ"magic prefix). Decrypt withskto get the token forcom.apple.gs.xcode.auth. This token isX-Apple-GS-Tokenfor all dev-portal calls. - Mint development cert. Generate a 2048-bit RSA keypair, PKCS#10 CSR, POST it to
services/QH65B2/ios/submitDevelopmentCSR.action. Apple returns acertificateId(cert bytes come from a separateservices/v1/certificatesquery). Store the cert + key in the local secret file; zsign reads the PEMs directly at sign time. - Register device.
addDevice.actionwith the iPhone UDID. Idempotent. - Create / reuse App ID. Bundle ids you don't own (
host.exp.Exponent,com.facebook.*) are reserved globally; rewrite with a.orbit<8hex>suffix derived from(teamId, originalBundleId)and remember the mapping in~/.orbit/apple-resign/state.json. - Download provisioning profile.
downloadTeamProvisioningProfile.actionreturns a.mobileprovisionlisting your cert + your device UDID + the App ID. - Resign. Unzip (pure-Node) → strip non-allowlisted entitlements → hand the cert PEM, key PEM, profile, and entitlements to
zsign, which recurses intoFrameworks/,PlugIns/, etc. and signs each Mach-O inside-out. - Repack. Zip
Payload/back into a.ipa(pure-Node, preserving Unix modes + symlinks). Done.
The full HTTP exchange is in src/auth/gsa.ts and src/devPortal.ts. The
signing recipe is in src/sign/codesign.ts.
Caveats
- No app extensions. Apple's free-account cap of 10 App IDs per 7-day window means apps with
.appex/ WatchKit targets aren't viable to resign with the same Apple ID more than once or twice a week.stripExtensions: trueremovesPlugIns/*.appexandWatch/*before signing — the main app loses push, share extensions, widgets, etc., but installs and runs. - Bundle id gets rewritten. Reserved bundle ids (
host.exp.Exponent,com.example.something-style ids someone else already registered) get a.orbit<8hex>suffix. The original is instate.json::appIdMapand reused on subsequent resigns. Apps that read their ownBundle.main.bundleIdentifierfor analytics / deep links will see the new id. - Entitlements are stripped. Anything outside the free-account allowlist (
application-identifier,keychain-access-groups,com.apple.developer.team-identifier,get-task-allow,com.apple.security.application-groups) is removed. Apps that require push / iCloud / HealthKit / etc. will install but crash on launch — this is a fundamental free-account limitation. Seesrc/sign/entitlements.ts. - 7-day expiry. The provisioning profile dies after 7 days. Re-run
resignIpaAsync— it reuses the cached cert and App ID, just downloads a fresh profile and re-signs (under 10 seconds for cached state). - First-install trust prompt. iOS requires the user to tap "Trust" once per Apple ID in Settings → General → VPN & Device Management. Xcode bypasses this via the
misagentservice on the device; this library doesn't talk to misagent yet, so the prompt is unavoidable today. - Apple Music APK download (Linux / Windows). Anisette on non-macOS hosts requires Apple Music for Android's
libstoreservicescore.so, fetched from Apple's CDN on first resign. The host needs outbound network access toapps.mzstatic.comonce; the libraries are cached in the user data dir thereafter.
Errors
All errors extend a binary-compatible InternalError class (same shape as
Expo's common-types). Discriminate via error.code:
| Code | When |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| APPLE_AUTH_REQUIRED | No persisted session for this Apple ID. Run signInAsync(). |
| APPLE_BAD_CREDENTIALS | Apple rejected the password or 2FA code. |
| APPLE_TWO_FACTOR_REQUIRED | error.details has { authMode: 'trustedDevice' \| 'sms', … }. Apple pushed a code; call submitTwoFactorCodeAsync(). |
| APPLE_RESIGN_UNSUPPORTED_IPA | error.details.reason is extensions / watchapp / multiple-apps. Set stripExtensions: true to remove the offender. |
| APPLE_RESIGN_QUOTA_EXCEEDED | Hit the 10-App-IDs-per-7-days cap. Wait or delete older entries with deleteAppIdForAccountAsync() / via developer.apple.com. |
| APPLE_RESIGN_FAILED | Catch-all for anything else (network errors, zsign failures, …). Run with DEBUG=expo:apple-resign* for protocol traces. |
Local storage
- 0600 JSON file at
~/.orbit/apple-resign/secrets.json(no OS keyring — the Keychain was rejected because eachpkgrebuild changes the binary's code-directory hash and macOS re-prompts; keys are prefixedexpo.orbit.apple-resign.*) — GSA refresh token, cert private key (PEM), cert (PEM). ~/.orbit/apple-resign/state.json— non-secret:teamId,appleId,appIdMap(original bundle id → assigned bundle id),certIds(per teamId),lastProfile. Safe to delete; will be regenerated on next resign.<user data dir>/orbit/apple-resign/anisette/ssc(Linux / Windows only) — extracted Apple Music for Android SSC libraries, plus anapk-metadata.jsonrecording the APK'setag/ version so we can detect when Apple ships a new one. At most once every 24h a resign re-checks Apple's APK headers and re-extracts if it changed (best-effort — a failed check never blocks the resign). Safe to delete; re-downloaded on next resign. Override the location withORBIT_ANISETTE_LIB(which also disables the auto-update check, since you manage that directory); force an immediate check withORBIT_ANISETTE_FORCE_UPDATE_CHECK=1.
Credits
- SideStore/apple-private-apis — the most modern, actively-maintained reference for Apple's auth protocol. The SRP-6a port is verified against their vendored
rustcrypto-srpfork, and the Rust anisette helper builds on theiromnisette. - rileytestut/AltSign — the original. Especially
ALTAppleAPI+Authentication.mfor the apptokens / AES-GCM dance. - Dadoum/Provision — anisette generator + the
provisionprotocol details. - zhlynn/zsign — the cross-platform code signer used to re-sign the app bundle.
