mirror of
https://github.com/fenleon/open-madrid.git
synced 2026-10-22 16:12:35 +00:00
registration: the prefpane presents the LOGIN RUN's anisette set (session-wide reuse, wire-pinned)
The trapped-binary decode (2026-10-15) shows ONE anisette mint serves the whole session — GSA hops, 2FA validate, postdata, both prefpane POSTs — so the PET is paired with the OTP window it was issued under. The rev-25/26 'fresh set at the sign-in POST' reading is corrected (confounded with the replay-guard era). GsaLoginChain mints one cpd per login run and exposes it as lastLoginAnisette (Client-Time frozen at the mint); GsaDelegateLoginClient's prefpane branch reuses a supplied Client-Time instead of re-minting. VEN step-2 wire fact recorded in spec rev 46: zero anisette on initializeValidation, no step-2 binding. Also wire-proven live: the GSA init 'ps' field is REQUIRED (omission answers ec=-80001).
This commit is contained in:
1 parent
9da53e31f8
commit
2ff994751c
5 files changed
+43
-14
No files matched your search
@@ -334,6 +334,7 @@ Ordered by value. IDs match the inline `TODO(capture):` markers above.
|
||||
- Rev 37 (2026-10-12, live-provisioning wire facts + the step-2 EM A/B): driving the Kotlin `ClearAnisetteProvider` against Apple's live endpoints for the first time (the session-26 EM A/B, NAC-NOTES session-26 addendum) surfaced two wire facts the offline scripted-HTTP tests could not catch: (1) the GSA lookup GET **404s without `X-Mme-Client-Info`**, and accepts only the angle-bracket form `<Model> <macOS;ver;build> <com.apple.AuthKit/1 (…)>` — a bare `com.apple.AuthKit/1 (com.apple.akd/1.0)` value 404s (the app's real value: `<MacBookPro18,3> <macOS;26.6.2;25G83> <com.apple.AuthKit/1 (com.apple.MobileSMS/1262.500.151.1.2)>`; imessage's `loginInfoFor`/`DEFAULT_LOGIN_INFO` already use the angle form); (2) the start/finish provisioning POST body keys are **case-sensitive**: `{header, request}` (lowercase — what the Kotlin sent) gets a **silent HTTP 200 with an EMPTY body**, `{Header, Request}` (as §1.5/C6 records) gets the real `Response.spim`/`ptm`/`tk` — fixed in `ClearAnisetteProvider` (Kotlin now sends the capitalized keys; 217 registration tests green; first live Kotlin provisioning run succeeded: metadata 400 B / mid 60 B). The vendored anisette client's fixed companion headers (rev-14 set) were confirmed against a live 2026-10-03 capture of the app's own lookup. Step-2 gate A/B (all via the identical transport): the app's genuine 10-08 blob replay → 5003/EM; our minted blob → 5003/EM; a FRESH blob minted by the app's own unmodified `ValidationCtx::new` the same hour (replay-state eliminated) → 5003/EM; with the full anisette header set on step 2 (± hardware headers, both blobs) → 5003/EM ×4 — **the anisette-on-step-2 hypothesis is falsified**. Residue: transport fingerprinting vs a server-side gate for everyone; the app's own-flow verdict is blocked on interactive IDS re-registration (user Apple-ID login) — removing `anisette_test/state.plist` re-fires provisioning but never the mint (no `ValidationCtx::new` hit in a fully-trapped run), and the vendored anisette client no longer traverses an intercepting proxy (baked DER root, SSL_CERT_FILE ignored).
|
||||
- Rev 38 (2026-10-12 late, the §1.1 step-2 5003/EM gate CLOSED — it is a TLS-stack fingerprint, not a blob/server-gate problem): after the user's 2FA re-login re-fired the app's own IDS registration and the full mint chain was observed ACCEPTED live (`vctx_new → step2_serialize → key_establishment → vctx_sign` ×3, gdb traps), fresh genuine blobs minted per-test (gdb `ValidationCtx::new`) settled the transport matrix: reqwest 0.11 + rustls 0.21 (+ the vendored client's baked Apple root DER, extracted from the .so at 0x24afa9 — webpki- roots/Mozilla does not include this Apple private root) POSTing the identical XML-plist envelope gets a real `session-info` response; curl (OpenSSL 3) over BOTH HTTP/2 and HTTP/1.1, and curl with UA/Accept headers removed (Accept-Language only), gets 5003/EM on the same fresh blobs. HTTP version and request headers are irrelevant: the VEN endpoint (Akamai-fronted identity.ess.apple.com) fingerprints the TLS ClientHello and rejects OpenSSL-shaped handshakes with the app-layer error 5003/EM. Earlier probes (JDK HttpURLConnection, python urllib, both cert-verif-off) were rejected for the same reason. Kotlin `NacRequestBlob` and the §1.1 framing are fully vindicated — the blob and envelope were never wrong. Consequence: the imessage tool's §1.1 HTTP must present a rustls-shaped handshake — JVM/Android default stacks (conscrypt/BoringSSL, OpenSSL-shaped) are expected to be rejected (unverified on-device); Conscrypt (Android's default stack, BoringSSL) is ALSO rejected — verified with a plain `HttpsURLConnection` probe on the lightos AVD posting a fresh genuine blob (EM-REJECTED) — so the accepted shape so far is rustls specifically, and the platform TLS stacks cannot do §1.1 step 2 at all. Implementation: a rust helper (rustls) behind UniFFI in `:server` (the calls/ pattern); a fingerprint-shaped JVM stack is not viable given the BoringSSL result. Evidence + probe sources: `registration/dev/clearadi/captures/c56j/tls-ab/` (NAC-NOTES session-26 EM-gate closure).
|
||||
- Rev 35 (2026-10-12, C56-a CLOSED — direct in-process replay of ValidationCtx::new, two byte-verified runs): the request blob's full recipe — header ‖ OAEP(pubkey, pearKey16 ‖ hmacKey20) ‖ RNG16 ‖ u32be(32) ‖ field32 ‖ HMAC-SHA1(hmacKey20, field32), with field32 = mapA[encrypt_cbc(mapB[02 ‖ u32be(1) ‖ innerKey16 ‖ zeros], iv, init_key(pearKey16))] and mapA/mapB the 0x2249f9/0x224af9 byte-substitution tables (table[byte], not xor). Kotlin `NacRequestBlob`; imessage unstubbed at C56-a; live-mint gate = C56-c only.
|
||||
- Rev 46 (2026-10-15, the trapped login decoded offline — the anisette is SESSION-WIDE, one mint; the prefpane "fresh anisette" reading corrected; VEN step-2 is anisette-free): full decode of the trapped 2FA login (`captures/c56k/ob-trap-20261014-201103/`, rustls + static-OpenSSL frames, HPACK-decoded): (1) **ONE anisette set serves the ENTIRE session** — one `X-Apple-I-MD` OTP, one `X-Apple-I-MD-M`, one `X-Apple-I-Client-Time` frozen at the session's mint (never refreshed) — byte-identical (hash-compared) across GsService2 init/complete, circle, 2FA validate, postdata ×5, CloudKit/escrowproxy, and BOTH prefpane `loginDelegates` POSTs. The rev-25/26 "OTP spent per request / the sign-in POST needs a fresh set" reading is CORRECTED: those observations predate the loginDelegates endpoint fix (session 29) and are confounded with the stamp replay-guard refusals of that era. The accepted prefpane POST pairs the PET with the OTP window it was issued under — the anisette↔login-session binding. (2) **The VEN step-2 (`TDIdentityService.woa/wa/initializeValidation`, 4 occurrences) carries ZERO anisette** — 8 headers only (h2 pseudo + accept/accept-language/accept-encoding/content-length), no OTP/MID/Client-Time/UA — no step-2 binding exists and our header-less step-2 is at parity; body = single `session-info-request` key, the 338-B blob with the constant `01 08 4b 2c 91 48 1d 9b 7d a0` header (`NacRequestBlob` exact). No separate keyEstablishment request exists on the wire. (3) A fresh stamp per delegate POST (2 POSTs → 2 distinct stamps; 692 B base64 = the 517-B stamp encoded). Code: `GsaLoginChain` now mints ONE cpd per login run (the cpd dict, the POST headers, and the exposed `lastLoginAnisette` — Client-Time included — share it) and `GsaDelegateLoginClient`'s prefpane branch reuses a supplied Client-Time instead of re-minting; imessage `register()` presents the login run's set at the prefpane. The 2FA-validate and postdata paths keep their fresh-per-call mints for now (the live 434 wall of rev 26 contradicts the wire's reuse — untangle only if the prefpane fix leaves residue); registration tests green.
|
||||
- Rev 45 (2026-10-13, the vendored delegate sign-in request RE'd to FULL parity — the prefpane endpoint speaks a V1 protocol, and the 400 ICLOUD_INVALID_DEVICE_TOKEN survives both body shapes): rustpush is now PUBLIC (OpenBubbles/rustpush, 2026-10-12) and the vendored .so embeds it with symbols — `login_apple_delegates` disassembled (closure 0xc3a5d0): fresh `os_config.generate_validation_data()` per sign-in (the public byte-exact VEN chain), the account's ClearADI anisette (persisted provisioning, OTP per call), the recorded header family (UA com.apple.iCloudHelper/282, X-Mme-Client-Info, X-Mme-Nas-Qualify, X-Apple-ADSID), Basic `username:PET`, optional `Cookie: termsAccepted=true` (terms flow only) — **and the body is the V1 kebab-case `AuthRequest {apple-id, client-id (plain v4, no uppercase), delegates, password = PET}` at `/setup/prefpane/login`** (`MacOSConfig::get_login_url` returns prefpane in the vendored build; the .so carries ZERO V2AuthRequest/protocolVersion/userInfo strings — the V1 shape public rustpush has commented out is what ships; the V2 wrapper stays recorded for `/setup/signin/v2/login`). Ladder (all on-device, same account, real C58): prefpane + V1 body WITHOUT Basic → 401 UNAUTHORIZED (the server reads Basic even on the V1 shape); prefpane + V1 + Basic + fresh PET → 400 ICLOUD_INVALID_DEVICE_TOKEN — the SAME verdict as V2 + Basic + fresh PET, so the body shape is not the variable and the request is at parity. User intel: the C58 Mac may have been REMOVED from the account — the 400 reads as Apple failing to tie the validation stamp's machine identity to a device record the account still has (the vendored client's last success 2026-10-10 predates any removal); next attempt is gated on confirming/re-verifying the Mac (or a fresh C58 from a currently-registered Mac). Code: `GsaDelegateLoginClient` V1 branch on PREFPANE_ENDPOINT; logs `captures/c56k/register-ladder-log-2026-10-13b.txt`.
|
||||
- Rev 44 (2026-10-13 later, the albert GENERIC_ERROR root-caused — it was OURS, not Apple's: **the public-repo port shipped a PLACEHOLDER FairPlay identity**): the staged `leaf.crt`/`identity.key` are self-signed (`CN = open-madrid-placeholder`, valid to 2036) — Apple bytes cannot ship in the public repo, and the port never wired the real pair. Apple's verdict for it is `SIGNATURE_VERIFICATION_FAILED` (not GENERIC_ERROR — that earlier page was the string-form `ActivationInfoXML`; with the rev-43 shapes the error is specific). The real identity (the one accepted live 2026-10-03) verifies fine: **host A/B with the genuine pair → device certificate on the FIRST probe, anisette/hardware headers unnecessary** (UA-only accepted too). Changes: `BundledFairPlaySigner` accepts an out-of-repo identity override (DER leaf + PKCS#1 PEM; null = committed placeholder — shape tests stay green); the imessage tool loads `/data/local/tmp/imessage-fairplay/{leaf.crt,identity.key}` (adb push, world-readable, no Apple bytes in any repo) and logs which identity is in use; the real pair lives in the gitignored captures (`captures/fairplay-identity/`). **On-device: activation ACCEPTED, courier connected, delegate sign-in reached the session-15 final wall `409 ICLOUD_UNSUPPORTED_DEVICE`** ("Cannot create another iCloud account with this device") — the recorded Nas-Qualify device-flag hypothesis (session 15) is now the only remaining §1.2 residue. Logs: `captures/c56k/register-final-log-2026-10-13.txt` + the anisette A/B (`AlbertAnisetteAbTest.kt`, also disproving the anisette-gate hypothesis).
|
||||
- Rev 43 (2026-10-13, REGISTER step-2 ACCEPTED on-device; the chain then moved to §1.2 and stalled at a NEW albert refusal): the rev-42 fix held live — the tool's step-2 POST returned a real `session-info` on the lightos AVD (validation → `initializing the validation session`, no 5080). Two follow-on fixes, then a new residue. (1) **Transport trust**: the engine SSLContext trusted ONLY the embedded Apple roots, but albert.apple.com chains to **DigiCert Global Root G2** (complete 3-cert chain on the wire) — `AppleTrust.platformTrustManager()` = system store + the Apple roots merged; the imessage engine context uses it (GSA's rev-40 pool + the public-CA endpoints in one manager). (2) **Albert request-shape regression**: the 7f960db port dropped two accepted shapes that session 15 carried — `ActivationInfoXML` must ride plist **`<data>` (bytes)**, not a string (the string form draws the `<Document>` GENERIC_ERROR page — pinned live 2026-10-02, re-confirmed 2026-10-13 by a host A/B), and the post carries **`User-Agent: ApplePushService/4.0 com.apple.iCloudHelper/282`** (rustpush `get_normal_ua("ApplePushService/4.0")`); both restored + shape tests updated. (3) **OPEN residue — albert GENERIC_ERROR**: with the restored shapes the activation is STILL refused with the generic wrapped-UI page (`ALEndPointErrorPage.genericError`, "Device activation failed. Please contact support.") for BOTH the user's real C58 identifiers and a synthetic identity (host A/B, same transport) — the shape now matches the session-15 accepted construction, so the refusal is server-side: top hypothesis is enforcement of the legacy FairPlay identity's leaf expiry (valid to 2024-10-11; activations stopped being observable after 2026-10-03), alternative is a new serial/record check. Next session: characterize against a CURRENT genuine activation (the vendored client's re-activation under gdb is user-gated) or a fresh FairPlay identity. Registration log excerpts archived (gitignored) in `captures/c56k/`; the A/B probe source archived beside it (`AlbertAbTest.kt`).
|
||||
|
||||
@@ -127,6 +127,10 @@ class GsaCpd(
|
||||
anisette[AnisetteHeaderProvider.DEVICE_ID]?.let { put(AnisetteHeaderProvider.DEVICE_ID, it) }
|
||||
}
|
||||
|
||||
/** The set's `X-Apple-I-Client-Time` value (this mint's frozen time) — the delegate
|
||||
* sign-in reuses it (the accepted wire carries one Client-Time session-wide). */
|
||||
fun clientTime(): String? = anisette[AnisetteHeaderProvider.CLIENT_TIME]
|
||||
|
||||
companion object {
|
||||
/** The anisette header names the §1.5 cpd carries as keys (values from the provider). */
|
||||
val ANISETTE_KEYS = listOf(
|
||||
|
||||
@@ -164,7 +164,7 @@ class GsaDelegateLoginClient(
|
||||
// endpoint's verdict is ICLOUD_INVALID_DEVICE_TOKEN — the fabricated sha256(keychain-id)
|
||||
// form is the prime suspect for the 400. GSA keeps its recorded set (rev 15).
|
||||
val anisette = if (prefpane) {
|
||||
request.anisette
|
||||
val mapped = request.anisette
|
||||
.filterKeys { !it.equals("X-Apple-I-MD-LU", true) }
|
||||
// The accepted prefpane request's exact wire casing (live capture 2026-10-14,
|
||||
// captures/c56k/mint-repro/ob-req-002.bin): real AuthKit sends mixed-case
|
||||
@@ -176,9 +176,12 @@ class GsaDelegateLoginClient(
|
||||
mapOf(
|
||||
"X-Apple-Locale" to "en_US",
|
||||
"X-Apple-I-Timezone" to "UTC",
|
||||
// fresh per request, the get_headers Z form (capture 2026-10-14)
|
||||
"X-Apple-I-Client-Time" to iso8601Z(System.currentTimeMillis()),
|
||||
)
|
||||
// Client-Time rides the supplied set when it carries one — the LOGIN RUN's
|
||||
// frozen mint (the accepted wire reuses ONE anisette set session-wide,
|
||||
// 2026-10-15 decode); fresh only when the caller minted none.
|
||||
if (mapped.keys.any { it.equals("X-Apple-I-Client-Time", true) }) mapped
|
||||
else mapped + ("X-Apple-I-Client-Time" to iso8601Z(System.currentTimeMillis()))
|
||||
} else {
|
||||
request.anisette
|
||||
}
|
||||
|
||||
@@ -207,6 +207,8 @@ class GsaLoginClient(
|
||||
"A2k" to a2k,
|
||||
"cpd" to cpd,
|
||||
"o" to OPERATION_INIT,
|
||||
// Wire-proven 2026-10-15: `ps` is REQUIRED — omitting it (the trapped-binary
|
||||
// decode's "no ps" reading) answers ec=-80001 "Protocols are invalid" live.
|
||||
"ps" to protocols,
|
||||
"u" to username,
|
||||
),
|
||||
|
||||
@@ -150,9 +150,16 @@ class GsaLoginChain(
|
||||
private val delegateClient = GsaDelegateLoginClient(http, endpoint = delegateEndpoint, logEvent = logEvent)
|
||||
private val postdataClient = GsaPostdataClient(http, logEvent = logEvent)
|
||||
|
||||
/** §1.5: the filtered anisette header set + the akd-variant headers, one per login. */
|
||||
private suspend fun gsaHeaders(): Map<String, String> =
|
||||
headerConfig?.let { cpd().headersFor() + it.toHeaders() } ?: cpd().headersFor()
|
||||
/**
|
||||
* The anisette header set of the last completed login run — the same set its GSA hops
|
||||
* presented, `X-Apple-I-Client-Time` included (frozen at the run's mint). The delegate
|
||||
* sign-in must present THIS set: the accepted wire (trapped binary, 2026-10-15) reuses
|
||||
* ONE minted set across the entire session — GSA init/complete, 2FA validate, postdata,
|
||||
* the prefpane POSTs — so the PET is paired with the OTP window it was issued under.
|
||||
* Empty until the first login run.
|
||||
*/
|
||||
var lastLoginAnisette: Map<String, String> = emptyMap()
|
||||
private set
|
||||
|
||||
/**
|
||||
* The §1.5 postdata liveness event — the account device-record registration the reference
|
||||
@@ -200,8 +207,16 @@ class GsaLoginChain(
|
||||
*/
|
||||
suspend fun loginWithPasswordHash(username: String, passwordHash: ByteArray): GsaLoginResult {
|
||||
val requestUuid = newRequestUuid()
|
||||
val cpdDict = cpd().dictFor(requestUuid)
|
||||
val gsaHeaders = gsaHeaders()
|
||||
// ONE anisette mint per login run: the cpd dict and the POST headers carry the same
|
||||
// set (the accepted wire reuses one set across the whole session, 2026-10-15), and
|
||||
// the run's set is what the delegate sign-in must present (lastLoginAnisette).
|
||||
val cpdSet = cpd()
|
||||
val anisetteHeaders = cpdSet.headersFor()
|
||||
lastLoginAnisette = cpdSet.clientTime()
|
||||
?.let { anisetteHeaders + (AnisetteHeaderProvider.CLIENT_TIME to it) }
|
||||
?: anisetteHeaders
|
||||
val cpdDict = cpdSet.dictFor(requestUuid)
|
||||
val gsaHeaders = headerConfig?.let { anisetteHeaders + it.toHeaders() } ?: anisetteHeaders
|
||||
val srp = GsaSrpClient(random)
|
||||
val a2k = srp.begin()
|
||||
val init = loginClient.init(
|
||||
@@ -342,12 +357,16 @@ class GsaLoginChain(
|
||||
* the §1.1 blob (the chunk-C exchange mints it — caller-supplied here). [timezone] defaults
|
||||
* to the recorded value (§1.5 C59); [clientInfo] defaults to the recorded bracketed AOSKit
|
||||
* form when the device config is wired ([GsaHeaderConfig.signInClientInfo]), falling back to
|
||||
* the bare recorded AOSKit inner component. [anisette] must be FRESHLY minted for this
|
||||
* call — the one-time OTP headers spent on the GSA hops are refused here (observed live;
|
||||
* the reference client mints per request). [validationData] must be a FRESH mint per
|
||||
* sign-in (minted at sign-in time, never a login-start blob carried across the 2FA wait) —
|
||||
* a consumed blob answers HTTP 409, the stamp REPLAY refusal, not a blob rejection
|
||||
* (rev 26, §1.4's replay guard).
|
||||
* the bare recorded AOSKit inner component. [anisette] is the LOGIN RUN's set
|
||||
* ([lastLoginAnisette] — the same set the GSA hops of the PET's session presented,
|
||||
* Client-Time frozen at that mint): the accepted wire (trapped binary, 2026-10-15)
|
||||
* reuses ONE anisette set across the whole session, so the prefpane POST pairs the
|
||||
* PET with the OTP window it was issued under. The earlier "freshly minted here"
|
||||
* reading (rev 25/26) predates the loginDelegates endpoint fix and is confounded
|
||||
* with the stamp replay-guard refusals of that era. [validationData] must be a FRESH
|
||||
* mint per sign-in (minted at sign-in time, never a login-start blob carried across
|
||||
* the 2FA wait) — a consumed blob answers HTTP 409, the stamp REPLAY refusal, not a
|
||||
* blob rejection (rev 26, §1.4's replay guard).
|
||||
*/
|
||||
suspend fun idsDelegateCredentials(
|
||||
username: String,
|
||||
|
||||
Reference in new issue
Block a user