Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 42 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: <Mac14,2> <macOS;15.7.5;24G624> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>' \
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.

---

Expand Down
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,15 @@ public AdiDeviceIdentity(String uniqueDeviceIdentifier, String adiIdentifier,
* <p><b>Why this stopped being the only one.</b> It was a constant, so every install of this
* app anywhere presented Apple the same serial while presenting a <i>different</i> 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.
*
* <p>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.
* <p><b>It does not explain the 503s, and this file used to claim it did.</b> 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";

Expand Down
5 changes: 3 additions & 2 deletions app/src/main/python/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/src/test/python/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 9 additions & 8 deletions python/exporter/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""


Expand Down
2 changes: 1 addition & 1 deletion python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions python/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading