From f47fc49eafb39aaf20f2b2f8e6084b2a61c200f8 Mon Sep 17 00:00:00 2001 From: fenleon Date: Wed, 14 Oct 2026 17:40:40 +0200 Subject: [PATCH] registration: GSA login-shape parity per the vendored source (2026-10-14 diff) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cpd gains the hardware-identity headers (SRL-NO/MLB/ROM chained last) — the vendored get_cpd_data carries them in the login BODY; the gsservice HTTP header set does NOT (the earlier reading had this inverted). - GsaHeaderConfig.toHeaders(): drop the hardware headers, add Accept-Language (get_gsservice_headers carries it). - ProvisionedAnisette flavor doc corrected (FLAVOR_MAC=1/FLAVOR_IOS=0 — the inverted doc caused the failed vendored-state swap, GSA ec=-22421). - 'at' stays the recorded boolean false (vendored source writes Integer(0); both live-accepted). --- .../registration/ClearAnisetteProvider.kt | 5 +++- .../dev/fenn/imessage/registration/GsaCpd.kt | 26 ++++++++++++++++--- .../fenn/imessage/registration/GsaCpdTest.kt | 2 ++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/registration/src/main/kotlin/dev/fenn/imessage/registration/ClearAnisetteProvider.kt b/registration/src/main/kotlin/dev/fenn/imessage/registration/ClearAnisetteProvider.kt index 5b083ee..db13b3a 100644 --- a/registration/src/main/kotlin/dev/fenn/imessage/registration/ClearAnisetteProvider.kt +++ b/registration/src/main/kotlin/dev/fenn/imessage/registration/ClearAnisetteProvider.kt @@ -125,7 +125,10 @@ data class ProvisionedAnisette( val mid: ByteArray, val metadata: ByteArray, val rinfo: String, - /** 0 = Mac, 1 = IOS (ClearAdiOtp's flavor convention; the reference's ProvisionedFlavor). */ + /** 0 = IOS-era provisioning, 1 = Mac (ClearAdiOtp's flavor convention — FLAVOR_MAC=1, + * FLAVOR_IOS=0; the pre-2026-10-14 doc had this inverted and caused the failed + * vendored-state swap: a Mac MID written with flavor 0 dispatches the wrong OTP + * cipher → GSA ec=-22421). */ val flavor: Int, ) { fun toJson(): String = buildJsonObject { diff --git a/registration/src/main/kotlin/dev/fenn/imessage/registration/GsaCpd.kt b/registration/src/main/kotlin/dev/fenn/imessage/registration/GsaCpd.kt index de5b524..917e098 100644 --- a/registration/src/main/kotlin/dev/fenn/imessage/registration/GsaCpd.kt +++ b/registration/src/main/kotlin/dev/fenn/imessage/registration/GsaCpd.kt @@ -63,8 +63,9 @@ enum class GsaFlavor( * `X-Apple-Security-Upgrade-Context` = `com.apple.authkit.generic`, `cou` = `US`, * `loc` = `en_US`, and the booleans `X-Apple-Offer-Security-Upgrade`=true, `bootstrap`=true, * `ckgen`=true, `fcd`=true, `icdrsDisabled`=false, `icscrec`=true, `pbe`=false, - * `prkgen`=true, `webAccessEnabled`=false, `at`=0 (the spec lists `at` among the booleans - * but records its value as `0`; encoded as the plist boolean false). + * `prkgen`=true, `webAccessEnabled`=false, `at`=false (the recorded wire value is the plist + * boolean false from a genuine Mac capture; the vendored Rust source writes Integer(0) — + * both live-accepted, the recording wins). */ class GsaCpd( /** The §1.1 anisette headers (from [AnisetteHeaderProvider.headers]). */ @@ -75,6 +76,14 @@ class GsaCpd( val flavor: GsaFlavor = GsaFlavor.MESSAGES, /** Optional APNs token (`ptkn`). */ private val apnsToken: String? = null, + /** + * The Mac hardware-identity headers (§1.1) — chained into the cpd dict like the + * vendored reference's cpd construction (icloud-auth anisette.rs `get_cpd_data` chains + * `client_info.hardware_headers` LAST). This is the login-body machine anchor: the + * gsservice HTTP header set does NOT carry them (2026-10-14 source diff — the earlier + * reading had them as login HTTP headers and missing from the cpd, inverted). + */ + val hardwareHeaders: Map = emptyMap(), ) { /** The cpd dict for one login; [requestUuid] is the uppercase UUIDv4 shared by both steps. */ @@ -92,6 +101,8 @@ class GsaCpd( dict["loc"] = "en_US" dict["svct"] = flavor.akContextType dict["X-Apple-Offer-Security-Upgrade"] = true + // The recorded wire value is the boolean false (genuine Mac capture); the vendored + // Rust source writes Integer(0) — both live-accepted, keep the recording (2026-10-14). dict["at"] = false dict["bootstrap"] = true dict["ckgen"] = true @@ -102,6 +113,7 @@ class GsaCpd( dict["prkgen"] = true dict["webAccessEnabled"] = false apnsToken?.let { dict["ptkn"] = it } + hardwareHeaders.forEach { (key, value) -> dict[key] = value } return dict } @@ -182,13 +194,21 @@ data class GsaHeaderConfig( val signInClientInfo: String get() = "<$hardwareModel> <${osName};${osVersion};${osBuild}> <$AOSKIT_CLIENT_INFO>" + /** + * The GSA login POST header set — the vendored reference's `get_gsservice_headers` + * (icloud-auth anisette.rs, 2026-10-14 source diff): the akd `User-Agent`, the akd + * `X-MMe-Client-Info`, the flavor's context/bundle headers and `Accept-Language`. The + * hardware-identity headers are NOT here — the reference carries them inside the cpd + * dict only ([GsaCpd.hardwareHeaders]). + */ fun toHeaders(): Map = linkedMapOf( "X-MMe-Client-Info" to clientInfo, "User-Agent" to AKD_USER_AGENT, "X-Apple-AK-Context-Type" to flavor.akContextType, "X-Apple-Client-App-Name" to flavor.clientAppName, "X-Apple-I-Client-Bundle-Id" to flavor.clientBundleId, - ) + hardwareHeaders + "Accept-Language" to "en-US,en;q=0.9", + ) companion object { /** The recorded akd `User-Agent` literal (§1.5, C59). */ diff --git a/registration/src/test/kotlin/dev/fenn/imessage/registration/GsaCpdTest.kt b/registration/src/test/kotlin/dev/fenn/imessage/registration/GsaCpdTest.kt index 4190192..028066a 100644 --- a/registration/src/test/kotlin/dev/fenn/imessage/registration/GsaCpdTest.kt +++ b/registration/src/test/kotlin/dev/fenn/imessage/registration/GsaCpdTest.kt @@ -163,6 +163,8 @@ class GsaCpdTest { "X-Apple-AK-Context-Type" to "imessage", "X-Apple-Client-App-Name" to "Messages", "X-Apple-I-Client-Bundle-Id" to "com.apple.MobileSMS", + // The vendored reference's get_gsservice_headers carries it (2026-10-14 source diff). + "Accept-Language" to "en-US,en;q=0.9", ), headers, )