Conversation
Apple's auth endpoint now requires X-Apple-ActionSignature on every request. The signer runs Apple's obfuscated CommerceKit/CoreFP entry points under Unicorn emulation entirely in the browser, preserving the zero-trust invariant: the signer sees only the deviceIdentifier and public Apple assets, while the password stays client-side as opaque body bytes it signs in place. Engine: Unicorn 2.x only ships JIT TCG backends, which cannot execute under WebAssembly. This restores the QEMU 5.0.1 TCI interpreter into the tree (patched build chain in frontend/scripts/unicorn-wasm-patch), forces 64-bit virtual TCI registers on wasm32 (the 32-bit path is incomplete upstream), generates uniform-signature helper trampolines, adapts glib-compat comparators, and replaces the timeout thread with a deadline checked inside the interpreter loop. Browser: Mach-O 64 loader with dyld_info rebase/bind/weak/lazy fixups, libc/CF/IOKit guest shims, and the SAP machine orchestration running in a Web Worker. Verified against the native ipatool runtime bit-for- bit: identical Initialize context (0x400000000200) and identical Sign gating error (-42085 without the Apple key exchange). Backend: extracts the four public Apple binaries from OSXUpd10.9.pkg (xar range download -> bzip2 -> cpio, digest-pinned), strips fat binaries to the x86_64 slice (37.7 MB -> 22.5 MB, ~14 MB gzipped on the wire), and serves them via /api/sap-assets. Docker: release images prebake the stripped assets (fresh VPS serves them with zero network use); build stages run on BUILDPLATFORM with per-platform runtime dependency install. linux/386 is not published by official node images and lacks napi binaries - amd64/arm64 only. Auth flow: bag advertises sign-sap-setup* keys -> assets fetched and cached (Cache API) -> signer created per authenticate() call -> each attempt's exact body bytes signed and attached. Bags without the SAP keys gracefully degrade to the legacy flow.
…cator The upstream draft PR (Lakr233#88) solved the same problem with an npm unicorn build; three of its UX decisions are worth taking regardless of engine: - Signer singleton bound to the deviceIdentifier. Rebuilding per authenticate() call copied the 22.5 MB asset bundle into a fresh worker on every attempt (2FA retries included); the singleton reuses the worker and SAP session across attempts and rebuilds only when the account or bag endpoints change. - Background warmup (useSapWarmup): once an account exists, fetch the bag and prepare the signer so sign-in usually finds it ready. The one-time asset download dominates preparation; later runs are served from the Cache API. - Inline progress (SapStatus) wired to a zustand store, with strings in all five locales. Also documents the upstream finding that the live bag hands out the legacy MZFinance authenticate endpoint and all advertised endpoints are covered by sign-sap-request.
…nature # Conflicts: # frontend/src/components/Account/AddAccountForm.tsx
wisp-js depends on bufferutil as a hard dependency; it ships musl prebuilds for x64 but not arm64, so the per-platform runtime install fell back to node-gyp without a toolchain present. Add python3/make/g++ as a layer-local virtual package removed after npm ci.
There was a problem hiding this comment.
Happy to see this PR. And I wonder, is this .wasm file committed to the PR by accident? Also the frontend/src/apple/sap/vendor/unicorn.mjs file is a compressed mis, looks like a compiled output instead of a source code?
…ages Preserve upstream browser-side signing, allow SAP certificate and exchange hosts through Wisp, and test exact signed request bytes. Repair the Docker workflow, gate publication on tests and container checks, and publish amd64/arm64 images for this fork.
|
Confirmed this working end-to-end on a self-hosted Docker instance built from The wisp hostname whitelist in backend/src/services/wsProxy.ts does not cover opening new TCP stream to s.mzstatic.com:443 The bag (note: it only advertises these keys when fetched with a Configurator sign-sap-setup-cert = https://s.mzstatic.com/sap/setupCert.plist Adding /^s.mzstatic.com$/, to hostname_whitelist completes the flow: the tunnel then goes Verified afterwards: real-account sign-in succeeds, and search + download work |
Companion proposal to #88 — same problem, same zero-trust approach, different engine. Also credits below, since three of its UX ideas are already folded into this branch.
Summary
Apple's auth endpoints now require
X-Apple-ActionSignature: base64(Sign(bodyBytes))(403 with empty body in ~6 ms otherwise, per the analysis in #88). This PR runs Apple's obfuscated SAP entry points under Unicorn 2.1.4 TCI emulation compiled to WebAssembly, entirely in the browser, preserving the zero-trust invariant: the signer's inputs are only the per-accountdeviceIdentifierand public Apple assets — the password never leaves the client except as the signed body inside the wisp tunnel.Engine: why not
@alexaltea/unicorn-jsThat package (used by #88) is the same Unicorn 2.1.4 built to wasm, but it runs the 32-bit TCI register path and calls helpers across the JS boundary. Both are expensive:
The build chain (
frontend/scripts/unicorn-wasm-patch/) clones unicorn 2.1.4 and applies a 112 KB patch that:TODO()stubs, and i64 guests otherwise split every operation across register pairsFidelity verified against native ipatool on the same assets: relocated guest images are byte-identical,
Initializereturns the identical context (0x400000000200),Signwithout the Apple key exchange fails with the identical-42085, and a 200-instruction trace aligns 1:1.Assets: 37.7 MB → 22.5 MB → ~14 MB on the wire
CoreFP ships as an i386+x86_64 universal and the emulated guest is x86_64 only, so the backend strips fat binaries to their x86_64 slice after verifying Apple's original digests (a second, deterministic pin set covers the stripped bytes served to browsers). The route gzips responses. Release images prebake the stripped assets in a build stage (layer-cached, digest-pinned), so a fresh VPS serves them with zero network use and zero extraction — verified: first asset request on a fresh container returns in 0.8 s.
UX adopted from #88
authenticate()copied the 22.5 MB bundle into a fresh worker every attempt (2FA retries included)useSapWarmup); with the fast engine this mostly covers the one-time 14 MB downloadSapStatus+ zustand store) in all five localesNotes
@node-rs/crc32napi binaries). Platforms:linux/amd64,linux/arm64. No 386 — official node images dropped it and the napi dependency has no linux-ia32 buildMZFinanceendpoint finding from Sign-in is returning 403: Apple now requires a SAP signature on authenticate #88 (all three advertised endpoints sit insign-sap-request, so signing applies regardless)Test plan
frontend84/84 vitest (machImage: fat slicing, bind/rebase opcode semantics, addends, BSS-tail fixups)backend49/49 vitest; extraction pipeline run end-to-end twice from the live CDN (all four digests match)tests/sap/machine-live.mjs: full chain against real assets from the running container — Initialize matches native bit-for-bit, Sign gating identicalMZFinance.BadLogin.Configurator_message, and this machine matches ipatool bit-for-bit)Supersedes neither — if the engine approach is welcome, the assets/UX layers here are directly reusable either way.