mirror of
https://github.com/fenleon/open-madrid.git
synced 2026-10-22 16:12:35 +00:00
registration: GSA cpd/headers wire-pinned against the trapped binary (2026-10-14)
The first-ever capture of the binary's GSA init/complete/postdata/2FA requests (static OpenSSL via ssl3_write_bytes; rustls only ever saw the prefpane path): - cpd carries NO X-Apple-I-MD-LU (neither headers nor cpd) — dropped; the keychainIdentifier constructor param is gone with the derivation. - cpd 'at' is the plist INTEGER 0 (the boolean-false recording was also live-accepted; the integer form is what the binary sends). - GSA login headers: Accept */* added; casing aligned to the binary (X-Mme-Client-Info, X-Apple-Ak-Context-Type). - 2FA browser-style set: hardware-identity headers ride it as HTTP headers (unlike the login POSTs); same casing alignment. - ptkn rides every login cpd once the app holds a push token (imessage side wires the persisted courier token).
This commit is contained in:
1 parent
f47fc49eaf
commit
9da53e31f8
6 files changed
+52
-57
No files matched your search
@@ -51,8 +51,9 @@ enum class GsaFlavor(
|
||||
/**
|
||||
* The GSA `cpd` (client-provided-data) dictionary of spec §1.5 — a dict inside the request
|
||||
* body carrying the anisette values as KEYS plus the fixed-value set. Anisette values come
|
||||
* from chunk A's [AnisetteHeaderProvider]; the `X-Apple-I-MD-LU` value is derived here from
|
||||
* the keychain identifier per §1.1 (lowercase hex SHA-256 over the same 16 bytes).
|
||||
* from chunk A's [AnisetteHeaderProvider]. Wire-pinned against the trapped binary's GSA
|
||||
* init (2026-10-14): NO `X-Apple-I-MD-LU` anywhere (headers or cpd), the hardware-identity
|
||||
* headers chain LAST, and `ptkn` rides every login once the app holds a push token.
|
||||
*
|
||||
* `capp`/`cbid`/`svct` ride the recorded per-flavor values ([GsaFlavor], C59); `ptkn` (APNs
|
||||
* token) is optional.
|
||||
@@ -63,15 +64,12 @@ 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`=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).
|
||||
* `prkgen`=true, `webAccessEnabled`=false, `at`= the plist INTEGER 0 (wire-proven; the
|
||||
* earlier boolean-false reading was also live-accepted).
|
||||
*/
|
||||
class GsaCpd(
|
||||
/** The §1.1 anisette headers (from [AnisetteHeaderProvider.headers]). */
|
||||
private val anisette: Map<String, String>,
|
||||
/** The 16-byte keychain identifier — feeds the `X-Apple-I-MD-LU` derivation (§1.1). */
|
||||
private val keychainIdentifier: ByteArray,
|
||||
/** The recorded per-flavor `capp`/`cbid`/`svct` values (§1.5, C59). */
|
||||
val flavor: GsaFlavor = GsaFlavor.MESSAGES,
|
||||
/** Optional APNs token (`ptkn`). */
|
||||
@@ -90,7 +88,6 @@ class GsaCpd(
|
||||
fun dictFor(requestUuid: String): Map<String, Any?> {
|
||||
val dict = LinkedHashMap<String, Any?>()
|
||||
ANISETTE_KEYS.forEach { key -> anisette[key]?.let { dict[key] = it } }
|
||||
dict["X-Apple-I-MD-LU"] = mdLu()
|
||||
dict["X-Apple-I-Device-Configuration-Mode"] = "0"
|
||||
dict[REQUEST_UUID_KEY] = requestUuid
|
||||
dict["X-Apple-Requested-Partition"] = "0"
|
||||
@@ -101,9 +98,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
|
||||
// Wire-proven 2026-10-14 (trapped GSA init): the binary sends the plist INTEGER 0.
|
||||
dict["at"] = 0
|
||||
dict["bootstrap"] = true
|
||||
dict["ckgen"] = true
|
||||
dict["fcd"] = true
|
||||
@@ -117,23 +113,18 @@ class GsaCpd(
|
||||
return dict
|
||||
}
|
||||
|
||||
/** §1.1: lowercase hex (no dashes) SHA-256 over the 16-byte keychain identifier. */
|
||||
private fun mdLu(): String = java.security.MessageDigest.getInstance("SHA-256")
|
||||
.digest(keychainIdentifier)
|
||||
.joinToString("") { "%02x".format(it) }
|
||||
|
||||
/**
|
||||
* The §1.5 header set that rides the GSA POSTs: the base anisette headers filtered to
|
||||
* `X-Apple-I-MD-LU`, `X-Apple-I-MD-RINFO`, `X-Apple-I-MD-M`, `X-Apple-I-MD`,
|
||||
* `X-Mme-Device-Id` — the same values the cpd carries, as headers. The akd-variant
|
||||
* headers ([GsaHeaderConfig]) are appended by the caller.
|
||||
* `X-Apple-I-MD-RINFO`, `X-Apple-I-MD-M`, `X-Apple-I-MD`, `X-Mme-Device-Id` — the same
|
||||
* values the cpd carries, as headers. Wire-proven 2026-10-14: the binary's GSA POSTs
|
||||
* carry NO `X-Apple-I-MD-LU` (neither headers nor cpd). The akd-variant headers
|
||||
* ([GsaHeaderConfig]) are appended by the caller.
|
||||
*/
|
||||
fun headersFor(): Map<String, String> = buildMap {
|
||||
anisette[AnisetteHeaderProvider.MACHINE_OTP]?.let { put(AnisetteHeaderProvider.MACHINE_OTP, it) }
|
||||
anisette[AnisetteHeaderProvider.RINFO]?.let { put(AnisetteHeaderProvider.RINFO, it) }
|
||||
anisette[AnisetteHeaderProvider.MACHINE_ID]?.let { put(AnisetteHeaderProvider.MACHINE_ID, it) }
|
||||
anisette[AnisetteHeaderProvider.DEVICE_ID]?.let { put(AnisetteHeaderProvider.DEVICE_ID, it) }
|
||||
put("X-Apple-I-MD-LU", mdLu())
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -195,18 +186,19 @@ data class GsaHeaderConfig(
|
||||
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]).
|
||||
* The GSA login POST header set — wire-proven against the trapped binary's GSA init
|
||||
* (2026-10-14): the akd `X-Mme-Client-Info` (lowercase e — the binary's form),
|
||||
* akd `User-Agent`, the flavor's context/bundle headers (`X-Apple-Ak-Context-Type`,
|
||||
* lowercase k), `Accept: any` and `Accept-Language`. No hardware headers and no
|
||||
* MD-LU (both live inside the cpd / nowhere).
|
||||
*/
|
||||
fun toHeaders(): Map<String, String> = linkedMapOf(
|
||||
"X-MMe-Client-Info" to clientInfo,
|
||||
"X-Mme-Client-Info" to clientInfo,
|
||||
"User-Agent" to AKD_USER_AGENT,
|
||||
"X-Apple-AK-Context-Type" to flavor.akContextType,
|
||||
"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,
|
||||
"Accept" to "*/*",
|
||||
"Accept-Language" to "en-US,en;q=0.9",
|
||||
)
|
||||
|
||||
|
||||
@@ -96,7 +96,10 @@ class GsaTwoFactorHeaders(
|
||||
putAll(anisette)
|
||||
put("X-Apple-Client-App-Name", flavor.clientAppName)
|
||||
put("X-Apple-I-Client-Bundle-Id", flavor.clientBundleId)
|
||||
clientInfo?.let { put("X-MMe-Client-Info", it) }
|
||||
clientInfo?.let { put("X-Mme-Client-Info", it) }
|
||||
// Wire-proven 2026-10-14 (trapped /validate): the hardware-identity headers ride
|
||||
// the 2FA browser-style set as HTTP headers (unlike the GSA login POSTs).
|
||||
device?.hardwareHeaders?.forEach { (key, value) -> put(key, value) }
|
||||
put("X-Apple-I-CDP-Circle-Status", "false")
|
||||
put("X-Apple-I-ICSCREC", "true")
|
||||
put("User-Agent", userAgent)
|
||||
@@ -111,11 +114,11 @@ class GsaTwoFactorHeaders(
|
||||
put("X-Apple-I-TimeZone", "UTC")
|
||||
put("X-Apple-I-OT-Status", "false")
|
||||
put("X-Apple-I-TimeZone-Offset", "0")
|
||||
put("X-MMe-Country", "US")
|
||||
put("X-Mme-Country", "US")
|
||||
put("X-Apple-I-CDP-Status", "false")
|
||||
put("X-Apple-I-Device-Configuration-Mode", "0")
|
||||
put("Sec-Fetch-Dest", "empty")
|
||||
put("X-Apple-AK-Context-Type", flavor.akContextType)
|
||||
put("X-Apple-Ak-Context-Type", flavor.akContextType)
|
||||
put("X-Apple-I-CFU-State", CFU_STATE)
|
||||
}
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ class ActivationDryRunTest {
|
||||
osVersion = export.macOsProductVersion!!,
|
||||
osBuild = identifiers.osBuildNumber!!,
|
||||
)
|
||||
val cpd = GsaCpd(anisette = anisette, keychainIdentifier = ByteArray(16) { (it + 1).toByte() })
|
||||
val cpd = GsaCpd(anisette = anisette)
|
||||
val chain = GsaLoginChain(
|
||||
http = env,
|
||||
cpd = { cpd },
|
||||
@@ -456,10 +456,10 @@ class ActivationDryRunTest {
|
||||
)
|
||||
val chain = GsaLoginChain(
|
||||
http = env,
|
||||
cpd = { GsaCpd(anisette = anisetteHeaders(), keychainIdentifier = ByteArray(16)) },
|
||||
cpd = { GsaCpd(anisette = anisetteHeaders()) },
|
||||
twoFactorHeaders = {
|
||||
GsaTwoFactorHeaders.of(
|
||||
GsaCpd(anisette = anisetteHeaders(), keychainIdentifier = ByteArray(16)),
|
||||
GsaCpd(anisette = anisetteHeaders()),
|
||||
device = deviceConfig,
|
||||
)
|
||||
},
|
||||
|
||||
@@ -59,7 +59,6 @@ class GsaCpdTest {
|
||||
|
||||
private fun cpd(apnsToken: String? = null, flavor: GsaFlavor = GsaFlavor.MESSAGES) = GsaCpd(
|
||||
anisette = anisette,
|
||||
keychainIdentifier = keychainIdentifier,
|
||||
flavor = flavor,
|
||||
apnsToken = apnsToken,
|
||||
)
|
||||
@@ -83,10 +82,9 @@ class GsaCpdTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `md-lu is lowercase hex sha-256 of the keychain identifier`() {
|
||||
val expected = MessageDigest.getInstance("SHA-256").digest(keychainIdentifier)
|
||||
.joinToString("") { "%02x".format(it) }
|
||||
assertEquals(expected, dict()["X-Apple-I-MD-LU"])
|
||||
fun `md-lu is absent — wire-proven (the binary carries it nowhere on GSA)`() {
|
||||
assertNull(dict()["X-Apple-I-MD-LU"])
|
||||
assertNull(cpd().headersFor()["X-Apple-I-MD-LU"])
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -117,7 +115,7 @@ class GsaCpdTest {
|
||||
fun `recorded booleans are pinned`() {
|
||||
val dict = dict()
|
||||
assertEquals(true, dict["X-Apple-Offer-Security-Upgrade"])
|
||||
assertEquals(false, dict["at"])
|
||||
assertEquals(0, dict["at"])
|
||||
assertEquals(true, dict["bootstrap"])
|
||||
assertEquals(true, dict["ckgen"])
|
||||
assertEquals(true, dict["fcd"])
|
||||
@@ -135,14 +133,14 @@ class GsaCpdTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the filtered anisette header set rides the POSTs with the derived md-lu`() {
|
||||
fun `the filtered anisette header set rides the POSTs`() {
|
||||
val headers = cpd().headersFor()
|
||||
assertEquals("an-otp", headers["X-Apple-I-MD"])
|
||||
assertEquals("a-machine-id", headers["X-Apple-I-MD-M"])
|
||||
assertEquals("17106176", headers["X-Apple-I-MD-RINFO"])
|
||||
assertEquals("1234ABCD-5678-90EF-1234-ABCD567890EF", headers["X-Mme-Device-Id"])
|
||||
assertEquals(dict()["X-Apple-I-MD-LU"], headers["X-Apple-I-MD-LU"])
|
||||
// The filtered set: client-time is a cpd key only, hardware headers appear on neither.
|
||||
// The filtered set: client-time is a cpd key only; MD-LU and the hardware
|
||||
// headers appear on neither (wire-proven 2026-10-14).
|
||||
assertNull(headers["X-Apple-I-Client-Time"])
|
||||
assertNull(headers["X-Apple-I-MLB"])
|
||||
}
|
||||
@@ -158,12 +156,13 @@ class GsaCpdTest {
|
||||
).toHeaders()
|
||||
assertEquals(
|
||||
mapOf(
|
||||
"X-MMe-Client-Info" to "<iMac13,1> <macOS;13.6.4;22G513> <com.apple.AuthKit/1 (com.apple.akd/1.0)>",
|
||||
"X-Mme-Client-Info" to "<iMac13,1> <macOS;13.6.4;22G513> <com.apple.AuthKit/1 (com.apple.akd/1.0)>",
|
||||
"User-Agent" to "akd/1.0 CFNetwork/1494.0.7 Darwin/23.4.0",
|
||||
"X-Apple-AK-Context-Type" to "imessage",
|
||||
"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).
|
||||
// Wire-proven against the trapped binary's GSA init (2026-10-14).
|
||||
"Accept" to "*/*",
|
||||
"Accept-Language" to "en-US,en;q=0.9",
|
||||
),
|
||||
headers,
|
||||
@@ -175,7 +174,7 @@ class GsaCpdTest {
|
||||
osVersion = "12.5.5",
|
||||
osBuild = "16H62",
|
||||
).toHeaders()
|
||||
assertEquals("icloud", icloud["X-Apple-AK-Context-Type"])
|
||||
assertEquals("icloud", icloud["X-Apple-Ak-Context-Type"])
|
||||
assertEquals("icloud", icloud["X-Apple-Client-App-Name"])
|
||||
assertEquals("com.apple.systempreferences.AppleIDSettings", icloud["X-Apple-I-Client-Bundle-Id"])
|
||||
}
|
||||
@@ -185,7 +184,6 @@ class GsaCpdTest {
|
||||
val expected = setOf(
|
||||
"X-Apple-I-Client-Time",
|
||||
"X-Apple-I-MD",
|
||||
"X-Apple-I-MD-LU",
|
||||
"X-Apple-I-MD-M",
|
||||
"X-Apple-I-MD-RINFO",
|
||||
"X-Mme-Device-Id",
|
||||
|
||||
@@ -63,7 +63,6 @@ class GsaLoginChainTest {
|
||||
"X-Apple-I-MD-M" to "machine-id",
|
||||
"X-Mme-Device-Id" to "device-id",
|
||||
),
|
||||
keychainIdentifier = ByteArray(16) { it.toByte() },
|
||||
)
|
||||
|
||||
private fun chain(
|
||||
|
||||
@@ -70,7 +70,6 @@ class GsaTwoFactorTest {
|
||||
"X-Apple-I-MD-M" to "machine-id",
|
||||
"X-Mme-Device-Id" to "device-id",
|
||||
),
|
||||
keychainIdentifier = ByteArray(16) { it.toByte() },
|
||||
),
|
||||
device = device,
|
||||
userAgent = userAgent,
|
||||
@@ -133,6 +132,10 @@ class GsaTwoFactorTest {
|
||||
osName = "macOS",
|
||||
osVersion = "13.6.4",
|
||||
osBuild = "22G513",
|
||||
hardwareHeaders = mapOf(
|
||||
"X-Apple-I-MLB" to "MLBVALUE",
|
||||
"X-Apple-I-ROM" to "aa11bb22cc33",
|
||||
),
|
||||
)
|
||||
val headers = browserHeaders(device = device).toHeaders()
|
||||
val expected = mapOf(
|
||||
@@ -140,15 +143,15 @@ class GsaTwoFactorTest {
|
||||
"X-Apple-I-MD-RINFO" to "17106176",
|
||||
"X-Apple-I-MD-M" to "machine-id",
|
||||
"X-Mme-Device-Id" to "device-id",
|
||||
"X-Apple-I-MD-LU" to
|
||||
java.security.MessageDigest.getInstance("SHA-256")
|
||||
.digest(ByteArray(16) { it.toByte() })
|
||||
.joinToString("") { "%02x".format(it) },
|
||||
// Wire-proven 2026-10-14: the binary's /validate carries the hardware-identity
|
||||
// headers as HTTP headers and NO MD-LU.
|
||||
"X-Apple-I-MLB" to "MLBVALUE",
|
||||
"X-Apple-I-ROM" to "aa11bb22cc33",
|
||||
"X-Apple-Client-App-Name" to "Messages",
|
||||
"X-Apple-I-Client-Bundle-Id" to "com.apple.MobileSMS",
|
||||
// Spec §1.5 rev-15 closure: the device-bracket form with the fixed
|
||||
// `com.apple.akd/1.0 (com.apple.akd/1.0)` inner component.
|
||||
"X-MMe-Client-Info" to "<iMac13,1> <macOS;13.6.4;22G513> <com.apple.akd/1.0 (com.apple.akd/1.0)>",
|
||||
"X-Mme-Client-Info" to "<iMac13,1> <macOS;13.6.4;22G513> <com.apple.akd/1.0 (com.apple.akd/1.0)>",
|
||||
"X-Apple-I-CDP-Circle-Status" to "false",
|
||||
"X-Apple-I-ICSCREC" to "true",
|
||||
// Spec §1.5 rev-15 closure: the browser UA literal — NO Safari/Version token.
|
||||
@@ -165,11 +168,11 @@ class GsaTwoFactorTest {
|
||||
"X-Apple-I-TimeZone" to "UTC",
|
||||
"X-Apple-I-OT-Status" to "false",
|
||||
"X-Apple-I-TimeZone-Offset" to "0",
|
||||
"X-MMe-Country" to "US",
|
||||
"X-Mme-Country" to "US",
|
||||
"X-Apple-I-CDP-Status" to "false",
|
||||
"X-Apple-I-Device-Configuration-Mode" to "0",
|
||||
"Sec-Fetch-Dest" to "empty",
|
||||
"X-Apple-AK-Context-Type" to "imessage",
|
||||
"X-Apple-Ak-Context-Type" to "imessage",
|
||||
"X-Apple-I-CFU-State" to GsaTwoFactorHeaders.CFU_STATE,
|
||||
)
|
||||
assertEquals(expected, headers)
|
||||
@@ -178,7 +181,7 @@ class GsaTwoFactorTest {
|
||||
@Test
|
||||
fun `without device slots the non-akd client-info is omitted`() {
|
||||
val headers = browserHeaders().toHeaders()
|
||||
assertEquals(false, headers.containsKey("X-MMe-Client-Info"))
|
||||
assertEquals(false, headers.containsKey("X-Mme-Client-Info"))
|
||||
assertEquals(GsaTwoFactorHeaders.BROWSER_USER_AGENT, headers["User-Agent"])
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user