From 345ce66fb6bcb126a00ee8ce63e27790e5727f45 Mon Sep 17 00:00:00 2001 From: "Shane B." Date: Sun, 13 Sep 2026 16:35:04 +0200 Subject: [PATCH] Stop sending the Xcode client identifier Apple now refuses Every sign-in has been failing with HTTP 503 from Grand Slam since late August. Apple's edge refuses any POST to `gsa.apple.com/grandslam/GsService2` whose `X-MMe-Client-Info` names `com.apple.dt.Xcode`, before a credential is examined - a 190-byte HTML page from `Server: Apple` instead of a GSA plist. Naming `com.apple.akd`, the daemon that really makes this call on macOS, is answered normally. Fixed in the fork at `b6f544b` and repinned here, in all four places rule 14 lists. Verified end to end against a real Apple ID: a sign-in that had failed for days now completes, and the library's own request shape returns HTTP 200 with akd against 503 with Xcode, one variable changed. **Not our diagnosis.** AltStore found it (altstoreio/AltStore#1790, shipped in AltServer 1.7.6); the same block took out SideStore, Macless Haystack, OpenBubbles and every Anisette server. Upstream fix offered as malmeloo/FindMy.py#271, and Dadoum/anisette-v3-server#60 is the same one-token change. Four files claimed the shared constant serial was the leading suspect for #168, #176 and #181. It was not, and the correction is in each of them rather than only in the rule: serials were eliminated on the way to the real cause - a drawn one, the old constant, and upstream's bare `0` all refused identically - and a QEMU macOS VM signs in with a fabricated `C02...` serial that is not unique across installs. The per-install serial stays on its own merits; it explains none of the 503s. Rule 18 records the two things that cost the afternoon: the answer was already published by a neighbouring project two days earlier, and the Xcode identifier was tested and wrongly cleared early because the patch went to the Anisette header builder while `_gsa_request` composes that header at a separate call site. --- AGENTS.md | 47 +++++++++++++++++-- app/build.gradle.kts | 2 +- .../anisette/AdiDeviceIdentity.java | 14 +++--- app/src/main/python/identity.py | 5 +- app/src/test/python/requirements.txt | 2 +- python/exporter/identity.py | 17 +++---- python/pyproject.toml | 2 +- python/uv.lock | 4 +- 8 files changed, 67 insertions(+), 26 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 44a57da7..db987de3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -258,11 +258,15 @@ Four things follow: leaves out the pairs a person comparing two screens would confuse. **Do not put it back to a constant.** It was one, and that meant a single serial arriving at Apple from thousands of installs, against thousands of different machine identities and Apple IDs, from every continent - at once — a shape no real hardware produces, and the leading suspect for the Grand Slam 503s in - [#168](https://github.com/parawanderer/OpenTagViewer/issues/168), - [#176](https://github.com/parawanderer/OpenTagViewer/issues/176) and - [#181](https://github.com/parawanderer/OpenTagViewer/issues/181), one of whom cleared their - device identity to no effect — which regenerates the ids and not the serial. + at once — a shape no real hardware produces, and worth not sending on that basis alone. + + **It had nothing to do with the Grand Slam 503s, and this rule claimed it did.** That guess was + written into four files before anyone tested it. The actual cause was Apple's edge refusing any + request whose `X-MMe-Client-Info` names `com.apple.dt.Xcode` — see rule 18. Serials were + eliminated on the way there: a drawn one, the old constant, and upstream FindMy.py's bare `0` + were all refused identically, and a QEMU macOS VM signs in happily with a fabricated `C02…` + serial that is **not even unique across installs**. Apple does not appear to check the serial at + all. - **An install that already has one keeps it**, including the installs that predate this and have no stored serial at all: those keep `0PENTAGVIEWR` / `0PENTAGXPORT`. Neither of those can be drawn — both contain a letter the alphabet excludes — so a serial with an `I` or an `O` in it @@ -489,6 +493,39 @@ constraint it does not understand, and the argument is the payload. **A person reading a pull request review is a different job.** State the finding, the file and line, and the fix. The same goes for issue replies and anything else sent to a contributor or a reporter. +### 18. When everything breaks at once, it is probably not yours + +On 2026-09-13 every sign-in failed with HTTP 503 from Grand Slam. An afternoon went into +eliminating the serial, the device ids, the ADI provisioning, three networks, two Apple IDs, two +machines and finally unmodified upstream FindMy.py — before anyone looked sideways. + +**The answer had been published two days earlier**, by AltStore, in +[altstoreio/AltStore#1790](https://github.com/altstoreio/AltStore/pull/1790): Apple's edge refuses +any POST to `gsa.apple.com/grandslam/GsService2` whose `X-MMe-Client-Info` names +`com.apple.dt.Xcode`, before a credential is examined. It answers a 190-byte HTML page from +`Server: Apple` rather than a GSA plist, which arrives as 503 and reads like an outage. Naming +`com.apple.akd` — the daemon that really makes this call on macOS — is answered normally. + +```bash +curl -so /dev/null -w '%{http_code}\n' -X POST --data-binary t \ + -H 'X-MMe-Client-Info: ' \ + https://gsa.apple.com/grandslam/GsService2 # 503 + + ... (com.apple.akd/1.0)> ... # 401, i.e. it arrived +``` + +**So check the neighbours first.** This project shares an authentication path with AltStore, +SideStore, Macless Haystack, OpenBubbles and every Anisette server, because they all copied it +from the same place. When something that worked yesterday fails for everybody, half an hour +reading those trackers is worth more than a day of bisecting this repository. The reverse holds +too: a breakage only *our* users see is ours, and the neighbours will be quiet. + +**And instrument the call site, not one that looks like it.** The Xcode identifier was tested and +cleared early — by patching the Anisette header builder, while `_gsa_request` sets +`X-MMe-Client-Info` from `self._anisette.client` at a separate call site. The patched header never +reached the wire, the request still failed, and that false negative sent the investigation away +from the right answer for hours. A test that changes a value next to the one being sent proves +nothing; print what actually goes out, or assert on the source line that composes it. --- diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d035c3ed..e8864292 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -407,7 +407,7 @@ chaquopy { // wheel for desktop platforms and a pure-Python `py3-none-any` one as well. // There is no Android wheel, so pip falls back to the pure-Python build - which // is correct but markedly slower. The messages here are small enough not to care. - install("git+https://github.com/parawanderer/FindMy.py@3c2b4926252193e9cd265b39fa52252adcbaad4e") + install("git+https://github.com/parawanderer/FindMy.py@b6f544bc673b66adeac9e8315f8ef499c4956036") install("NSKeyedUnArchiver==1.5") diff --git a/app/src/main/java/dev/wander/android/opentagviewer/anisette/AdiDeviceIdentity.java b/app/src/main/java/dev/wander/android/opentagviewer/anisette/AdiDeviceIdentity.java index 2c06350b..5e668fc8 100644 --- a/app/src/main/java/dev/wander/android/opentagviewer/anisette/AdiDeviceIdentity.java +++ b/app/src/main/java/dev/wander/android/opentagviewer/anisette/AdiDeviceIdentity.java @@ -109,13 +109,15 @@ public AdiDeviceIdentity(String uniqueDeviceIdentifier, String adiIdentifier, *

Why this stopped being the only one. It was a constant, so every install of this * app anywhere presented Apple the same serial while presenting a different machine * identity: one serial against thousands of device ids and thousands of Apple IDs, from every - * continent, at once. Real hardware does not look like that. The 503s from Grand Slam that - * some accounts never recover from - issues #168, #176 and #181 - are consistent with that - * fingerprint being refused, and one reporter cleared their device identity to no effect, - * which is what would happen if the serial were the part being matched on. + * continent, at once. Real hardware does not look like that, and a fingerprint nothing real + * produces is worth not sending whether or not anything is matching on it. * - *

That is a hypothesis and is written down as one. It has not been confirmed against - * Apple, and the cheap way to confirm it is exactly this change. + *

It does not explain the 503s, and this file used to claim it did. Issues #168, + * #176 and #181 were Apple's edge refusing any request whose {@code X-MMe-Client-Info} names + * {@code com.apple.dt.Xcode} - see AGENTS.md rule 18. Serials were eliminated on the way + * there: a drawn one, the old constant, and upstream FindMy.py's bare {@code 0} were all + * refused identically, and a QEMU macOS VM signs in with a fabricated {@code C02...} serial + * that is not even unique across installs. Apple does not appear to look at it at all. */ public static final String LEGACY_SERIAL = "0PENTAGVIEWR"; diff --git a/app/src/main/python/identity.py b/app/src/main/python/identity.py index c73763ef..5cbeadac 100644 --- a/app/src/main/python/identity.py +++ b/app/src/main/python/identity.py @@ -55,8 +55,9 @@ pinned equal by `IdentityBridgeTest`; a serial drawn per install cannot be pinned that way, and a second copy of it is exactly what rule 11 is about. `appSerial` asks. See `AdiDeviceIdentity.LEGACY_SERIAL` for why a constant was wrong: one serial against thousands of -machine identities and Apple IDs is not a shape real hardware produces, and it is the leading -suspect for the 503s in issues #168, #176 and #181. +machine identities and Apple IDs is not a shape real hardware produces. It is **not** the cause of +the 503s in #168, #176 and #181 - that was Apple refusing the Xcode client identifier, AGENTS.md +rule 18. **An install that has one keeps it**, which is why this value still goes out at all: an install from before the change has been presenting it, and drawing it a new serial now would register a diff --git a/app/src/test/python/requirements.txt b/app/src/test/python/requirements.txt index 84952e1d..f43b0747 100644 --- a/app/src/test/python/requirements.txt +++ b/app/src/test/python/requirements.txt @@ -9,7 +9,7 @@ # `FindMy==0.9.8` here for as long as the app built the fork, so every bridge test # ran against a library the app does not ship - which is not a small difference: # the fork's Anisette providers take `serial=` and PyPI's do not. -git+https://github.com/parawanderer/FindMy.py@3c2b4926252193e9cd265b39fa52252adcbaad4e +git+https://github.com/parawanderer/FindMy.py@b6f544bc673b66adeac9e8315f8ef499c4956036 NSKeyedUnArchiver==1.5 PyYAML==6.0.2 diff --git a/python/exporter/identity.py b/python/exporter/identity.py index f8664b23..33ab2a4f 100644 --- a/python/exporter/identity.py +++ b/python/exporter/identity.py @@ -64,14 +64,15 @@ **Why this stopped being the only one.** It was a constant, so every install of this program, everywhere, presented Apple the same serial while presenting a *different* machine identity: one serial against thousands of device IDs and thousands of Apple IDs, from every continent, at once. -Real hardware does not look like that. A 503 from Grand Slam that some accounts never recover from -- issues #168, #176 and #181 - is consistent with that fingerprint being refused, and one reporter -cleared their device identity to no effect, which is what would happen if the serial were the part -being matched on. - -That is a hypothesis and is written down as one. It has not been confirmed against Apple, and the -cheap way to confirm it is exactly this change: an affected user deleting their identity file now -draws a different serial instead of the same one. +Real hardware does not look like that, and a fingerprint nothing real produces is worth not +sending whether or not anything is matching on it. + +**It has nothing to do with the 503s, and this docstring used to say it probably did.** Issues +#168, #176 and #181 were Apple's edge refusing any request naming `com.apple.dt.Xcode` - see +AGENTS.md rule 18. Serials were eliminated on the way there: a drawn one, this constant, and +upstream FindMy.py's bare `0` were all refused identically, and a QEMU macOS VM signs in with a +fabricated `C02...` serial that is not even unique across installs. Apple does not appear to look +at it at all. """ diff --git a/python/pyproject.toml b/python/pyproject.toml index a80d929b..7ac98da4 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -69,7 +69,7 @@ constraint-dependencies = [ ] [tool.uv.sources] -FindMy = { git = "https://github.com/parawanderer/FindMy.py", rev = "3c2b4926252193e9cd265b39fa52252adcbaad4e" } +FindMy = { git = "https://github.com/parawanderer/FindMy.py", rev = "b6f544bc673b66adeac9e8315f8ef499c4956036" } [dependency-groups] # Only the release build installs this, with `uv sync --no-default-groups --group build`. It is diff --git a/python/uv.lock b/python/uv.lock index c03d1477..0a543aa1 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -659,7 +659,7 @@ wheels = [ [[package]] name = "findmy" version = "0.10.1" -source = { git = "https://github.com/parawanderer/FindMy.py?rev=3c2b4926252193e9cd265b39fa52252adcbaad4e#3c2b4926252193e9cd265b39fa52252adcbaad4e" } +source = { git = "https://github.com/parawanderer/FindMy.py?rev=b6f544bc673b66adeac9e8315f8ef499c4956036#b6f544bc673b66adeac9e8315f8ef499c4956036" } dependencies = [ { name = "aiohttp" }, { name = "anisette" }, @@ -1032,7 +1032,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "findmy", git = "https://github.com/parawanderer/FindMy.py?rev=3c2b4926252193e9cd265b39fa52252adcbaad4e" }, + { name = "findmy", git = "https://github.com/parawanderer/FindMy.py?rev=b6f544bc673b66adeac9e8315f8ef499c4956036" }, { name = "pycryptodome", specifier = "==3.22.0" }, { name = "pyyaml", specifier = "==6.0.2" }, { name = "pyzipper", specifier = "==0.4.0" },