altstore couldn't sign in. apple was sending back an html page.
a writeup of the september 2026 altstore sign-in breakage: how a "data is not in the correct format" error turned out to be an html 503 page fed into a plist parser, how i got a wrong answer from my own retries, and how the fix ended up as a 39-character string swap inside signed binaries on macos and windows.
this was a community effort in altstoreio/AltStore#1699. the upstream fixes that shipped were written by other people, credited below. my part was the diagnosis, the binary patches for people who couldn't wait for a release, and a second, unrelated failure (-80009) that the first workaround introduced.
tl;dr
- altserver couldn't sign in:
NSCocoaErrorDomain 3840, "encountered unknown tag html on line 1". - capturing the grandslam traffic showed the apple id login worked. the third request,
apptokens, got a 503 html page, and altsign parsed it as a plist. - the trigger was a stale, mojave-era
User-Agenthard-coded in altsign, made worse by retries reusing one pooled connection. - the same string lived in the macos framework (two copies, one per cpu slice) and in
AltServer.exe(utf-16le, one copy). swapping it for a same-length string fixed sign-in without a rebuild. on macos the app then has to be re-signed. - the upstream fixes were merged in altsign between sep 8 and 10, and altserver 1.7.6 fixed it for everyone.
the symptom
NSCocoaErrorDomain 3840
Debug Description: Encountered unknown tag html on line 1
AltServer could not sign in with your Apple ID. The data is not in the correct format.
it reads like a credentials problem. people were creating new apple ids, switching vpns, changing regions. none of it helped, because the message wasn't about their account at all.
1. look at the actual traffic
i injected a dylib into stock altserver to log its NSURLSession calls, and captured the grandslam exchange against POST https://gsa.apple.com/grandslam/GsService2:
Request.o=init -> 200 text/x-xml-plist
Request.o=complete -> 200 text/x-xml-plist <-- srp auth succeeds
Request.o=apptokens -> 503 text/html <-- "503 Service Temporarily Unavailable"
so apple accepted the password and the anisette data. only the third call was refused, and it came back as an html error page. altsign has no handling for a non-plist body, so the html goes straight into PropertyListSerialization, which fails on <html> at line 1. that's the whole error message.
that also explained why nothing people tried mattered. i reproduced the same result on my home isp, an iphone hotspot and a us vpn exit, with six different anisette servers and two client identities. other reports in the thread added a brand-new apple id and windows with anisette from icloud for windows. same 503 every time.
2. my first conclusion was wrong
i added automatic retries: six attempts over about seven seconds, all 503. i posted that the refusal was deterministic.
it wasn't. every retry reused the same NSURLSession, so every attempt went back over the same pooled connection, pinned to the same failing apple backend. six retries on one connection are one sample, not six. @Calvin-Zikakis had described exactly that bug in AltSign#49. i posted a correction in the thread, because "apple has closed this off" was the wrong thing for people to walk away with.
3. the cause: a user-agent from 2019
@FrakesH identified the trigger: altsign sends
akd/1.0 CFNetwork/978.0.7 Darwin/18.7.0
a macos mojave user-agent, hard-coded. apple's servers started refusing it on apptokens.
to check that before touching any binary, i applied both fixes at runtime to stock altserver 1.7.2 (build 90) through the injected dylib. it rewrote User-Agent on *.apple.com requests and ran every request on a fresh ephemeral URLSession. sign-in and installation worked end to end. on macos 27 i also needed @kimziro's anisette fix for the AOSKit -45070 / empty machineID stage.
4. patching the binaries
a dylib isn't something most people can use. the replacement string is the same length as the old one, 39 characters, so a straight byte swap works without moving anything:
akd/1.0 CFNetwork/978.0.7 Darwin/18.7.0
AuthKit/1 (com.apple.dt.Xcode/3594.4.1)
macos
the string isn't in AltServer itself. it's in the altsign framework, in ascii, twice: AltSign-Dynamic is a universal binary and each architecture slice has its own copy.
AltServer.app/Contents/Frameworks/AltSign-Dynamic.framework/Versions/A/AltSign-Dynamic
0x62920 -> x86_64 slice
0x10E210 -> arm64 slice
both sit next to X-MMe-Client-Info. the same layout holds in 1.6.2 and 1.7.2. i downloaded a stock 1.6.2 from apple's cdn to verify it.
the catch is code signing. the stock app has a developer id signature with hardened runtime (flags=0x10000(runtime)). editing the framework invalidates it:
AltServer.app: invalid signature (code or signature have been modified)
so the patch has to end with an ad-hoc re-sign:
APP="/Applications/AltServer.app"
F="$APP/Contents/Frameworks/AltSign-Dynamic.framework/Versions/A/AltSign-Dynamic"
cp -R "$APP" ~/Desktop/AltServer-backup.app
perl -pi -e 's/akd\/1\.0 CFNetwork\/978\.0\.7 Darwin\/18\.7\.0/AuthKit\/1 (com\.apple\.dt\.Xcode\/3594\.4\.1)/g' "$F"
codesign --force --deep --sign - "$APP"
codesign --verify --deep --strict "$APP" && echo OK
@angel-knights confirmed it on 1.6.2 under macos 10.15, a combination i couldn't test.
windows
i'd said windows users had no path because altserver for windows isn't open source. that was wrong: a string swap doesn't need source. i don't have a windows machine, so i took the official installer apart instead (altinstaller.zip → AltInstaller.msi → embedded cab) and read AltServer.exe 1.7.4.0 directly:
- the stale user-agent is stored as utf-16le at offset
0x14D6A8, exactly one occurrence - it sits next to
/grandslam/GsService2andviewDeveloper.action, the same endpoints from the macos capture - the exe has no authenticode signature (empty certificate directory), so patching invalidates nothing
- after the swap it's still a valid pe32, with zero copies of the old string and one of the new
i posted a powershell script that backs up the file, refuses to run unless it finds exactly one occurrence, and writes the new bytes in place. @akgang-rgb and @BorbushArtem confirmed it on windows. @BorbushArtem's version was better than mine: it checks both program files paths, asserts the length match before writing, and searches through an iso-8859-1 round-trip instead of my byte loop. i pointed people to theirs.
the altstore app on the phone carries its own copy of altsign, so patching altserver fixes sign-in on the computer only. @spaceace90 found the equivalent swap inside the ipa.
5. the release that didn't fix it
altserver 1.7.5 shipped on sep 8. i pulled it from the official sparkle feed and checked:
AltServer.app/Contents/MacOS/AltServer
0x111070 -> x86_64 slice akd/1.0 CFNetwork/978.0.7 Darwin/18.7.0
0x27E6D0 -> arm64 slice akd/1.0 CFNetwork/978.0.7 Darwin/18.7.0
same stale string. altsign was now statically linked, so the patch target had moved from the framework to the main executable. and since sparkle replaces the whole app, updating silently undid anyone's patch. AltSign#52, the connection-reuse fix, had been merged seven hours after 1.7.5 was cut, so it wasn't in any release yet.
6. a second bug, caused by the workaround
some people who used a rebuilt altserver started getting -80009 MID is invalid instead. that's a different failure: apple doesn't accept the machine identity.
i probed the three aoskit calls directly on macos 27:
retrieveOTPHeadersForDSID: -> {} (broken)
machineSerialNumber -> <real serial> (still works)
machineUDID -> <real udid> (still works)
only one of them was broken. the macos 27 anisette fix bundled in that build hooks only retrieveOTPHeadersForDSID: and provisions a synthetic identity, which on my machine had serialNumber: "0" and a random deviceID. altserver then sent that synthetic machineID next to the host mac's real serial and udid. the request described two different machines, and apple rejected it.
@oddharsh had already found that gap in anisette-fix#4, which hooks the serial and udid too so the whole set describes one identity. the measurement showed why it was needed: hooking one call out of three leaves the identity half real and half synthetic.
how it ended
- on sep 11, two people reported the patch had stopped working the same day. apple had most likely started filtering the spoofed xcode user-agent that the thread converged on. @MeemeeLab reported that any
Xcodein the client info seemed to trigger the 503. - upstream, altsign merged #52 (@sshane), #51 and #54 (@Calvin-Zikakis) between sep 8 and 10, and altserver 1.7.6 was reported working.
- the manual patch was a stopgap for a few days, not the fix. the official release was.
mistakes i made in public
- "deterministic" from six retries on one pooled connection. corrected in the thread.
- "no path on windows". a length-preserving string swap doesn't need source. corrected, then shipped the script.
- linking
-22410to the connection bug. the official error list documents-22410as "failed to write to disk", and i had mixed a symptom from altstore on ios with one from altserver on the pc. i said so when @kswartz26 pointed it out.
takeaways
- an error message can describe the parser, not the problem. "not in the correct format" meant a server error page was parsed as data. the first thing to capture is what actually came over the wire.
- retries on the same connection are not independent samples. if the connection is pinned to a bad backend, you measure that backend six times.
- length-preserving patches are cheap, re-signing is the real work. on macos, a patched binary with a broken signature fails before your change ever runs.
- check releases, don't assume. 1.7.5 looked like the fix and wasn't. two string offsets proved it in a minute.
- other people's testing is part of the result. the windows patch and the 10.15 path were confirmed by users on hardware i don't have.
links
- altstoreio/AltStore#1699, the main thread
- AltSign#51, #52 and #54, the merged upstream fixes
- kimziro/altserver-macos27-anisette-fix and #4