mirror of
https://github.com/fenleon/open-madrid.git
synced 2026-10-22 16:12:35 +00:00
ClearADI cpim tail closed: X = SHA-256(encout || sig(trailing[0:16]) || const) — wire fully pinned
Two further live provisioning runs (gdb_prov8/9) close the last three TODO(capture) items of the cpim: - X (the 32-byte tail group) = SHA-256(encout || spim_map_signature( trailing[0:16]) || fixed 24-byte filler), written to the wire byte-exact; verified against both runs. - The signature input is the spim trailing's first 16 bytes (server material, varies per session) — the earlier 'constant block' reading was a stale session's value; the signature model matches the live output exactly. - The 12-byte plaintext tail is 12 fresh RNG bytes (same thread-RNG pool construction as the IV). - trailing[0:16] and the sig/digest 'nowhere on the wire' mystery resolved: both ride transitively through X. Kotlin: CpimShaTailSeal (the pinned production seal) wired as the provider default; doc comments updated; one synthetic-vector test. Spec rev 29. 195 tests green.
This commit is contained in:
1 parent
21eefe3846
commit
77b9819de7
5 files changed
+66
-12
No files matched your search
@@ -12,13 +12,13 @@ Validated:
|
||||
- The registration chain ran live against Apple (2026-10-02→04, spec rev 26): Albert activation, the packed APNs courier, the full GSA sign-in (SRP, trusted-device 2FA, PET delivery, postdata), then IDS authenticate and register — both green, `mailto:` handle active. The earlier 409 refusals turned out to be Apple's validation-stamp replay guard (one mint = one device record), documented in the spec.
|
||||
- Two live unauthenticated probes against Apple succeeded: both bag endpoints return plists this stack parses, and certificate pinning validates Apple's real TLS chains. A drift check ships with the repo (`:engine:runProbe`).
|
||||
- The courier's auth model is settled: the packed path needs no client certificate; the legacy path answers the handshake with a TLS 1.3 post-handshake `certificate_required` alert.
|
||||
- The ADI anisette crypto is reimplemented natively, no emulation or helper binary: the white-box OTP ciphers, the GSA provisioning exchange, and the spim/cpim/ptm wire framing are transcribed clean-room from analysis of a compiled ADI implementation and verified byte-exact against oracle vectors, then live-confirmed against a genuine client's provisioning run under an instrumented debugger (194 tests green, spec rev 28). No anisette server, no Unicorn, no Mac relay.
|
||||
- The ADI anisette chain is reimplemented natively, no emulation or helper binary: the white-box OTP ciphers, the GSA provisioning exchange, and the full spim/cpim/ptm wire framing are transcribed clean-room from analysis of a compiled ADI implementation, verified byte-exact against oracle vectors, then live-pinned against three genuine provisioning runs under an instrumented debugger. Every byte group of the exchange is now identified, including the cpim tail's SHA-256 seal over the encrypt output and the spim trailing's signature (195 tests green, spec rev 29). No anisette server, no Unicorn, no Mac relay.
|
||||
|
||||
Not validated yet:
|
||||
|
||||
- No real message send or receive over the courier; renewal across sessions is untested.
|
||||
- The crypto envelopes have round-trip tests but no known-answer vectors from a live device.
|
||||
- The ADI provisioning wire shapes were confirmed from an instrumented genuine client (spec rev 28): the provisioned-state mapping (metadata/client-secret/mid/flavor), the PTM layout with its embedded decrypt seed, the 276-byte cpim outer message, and the gate echo position are all live-pinned. Three byte groups remain `TODO(capture)`: the 32-byte cpim tail group, the 12-byte plaintext tail, and the spim trailing's first 16 bytes.
|
||||
- The validation-blob ("NAC") mint — the per-device hardware signature Apple's registration accepts — is the one ADI-adjacent piece not yet reimplemented; the analysis plan and method are in the spec and the workspace worklog.
|
||||
- A few wire details are capture-bound guesses, tracked in the spec's open-questions checklist.
|
||||
- PQ3 is unsupported.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# iMessage protocol spec (clean-room)
|
||||
|
||||
**Status:** draft — revision 28. The full revision history is at the bottom ([Revision history](#revision-history)); the sections below state the facts as of rev 28.
|
||||
**Status:** draft — revision 29. The full revision history is at the bottom ([Revision history](#revision-history)); the sections below state the facts as of rev 29.
|
||||
|
||||
**Firewall rule:** this document contains facts only — no code, no function/type names, no copied expression from any reference implementation. Wire-format field names are quoted as on-the-wire protocol data (observable in captures, capture-arbitrated), never as source expression. Facts derived from SSPL/AGPL sources are used facts-only; nothing from them is reproduced. Where the source-derived fact sets agree, the fact is stated once with both citations; where they disagree, the disagreement is stated and tagged `TODO(capture):` — running mitmproxy captures arbitrate. All `TODO(capture)` items are collected in §7.
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
- Current shipped flow (OpenBubbles, 2026): the Mac-side helper (`Mac-Hardware-Info` 1.2.0, notarized) only *reads* the machine identifiers — serial, board ID, model, etc. — via public IORegistry/sysctl APIs (docs: "High Sierra and above", no upper macOS version bound) and exports them as a shareable code (≤ 20 users per code, optional one-time-use/48-hour mode); it never invokes an Apple private framework [OB-MHI], [OB-FAQ]. The client then performs the three-step exchange above itself, in software, keyed with those identifiers and paired with an Apple-Account login — the [JJ] emulation path with an official identifier-export helper in front of it. `TODO(capture):` **C56** — the client-side generation procedure behind the three-step exchange: what the client computes with the `id-initialize-validation` session info (the key-establishment math and blob assembly), how it interacts with the Apple-Account/GSA login and the anisette header set (C6) at activation time, and which pieces are fixed protocol constants vs per-account/per-session state. — 2026-09-10: steps 1–2 of C56 are recorded (bullet above); step 3 — key establishment + signing — is closed source: the public OpenAbsinthe-Stub repo is an official mock whose methods are stubs (its README states the real repository is closed source), so the spec records only the API contract: context creation consumes the certificate chain + hardware configuration and outputs the session-info-request blob; key establishment consumes the server response; signing produces the final blob with no further inputs (all state retained in the context) [RP:macos.rs], [RP:activation.rs], [RP:auth.rs], [OA]. Remaining: wire-verification of the blob against one live activation; the enc-variant derivation rides C57. — 2026-09-11 (rev 14 extraction): the five obfuscation labels are now MAPPED to their fields (previously only listed together): serial → `Gq3489ugfi`, platform UUID → `Fyp98tpgj`, root-disk UUID → `kbjfrfpoJU`, ROM → `oycqAZloTNDm`, MLB → `abKPld1EcMni` [OA:nac.rs]; the derivation producing each `_enc` byte variant is still closed source. The hardware-configuration field set is recorded in full: product name (Apple product-type form, e.g. `Macmini9,1`), MAC address (exactly 6 raw bytes), platform serial, platform UUID, root-disk UUID, board ID, OS build number, ROM (raw bytes), MLB, plus the five `_enc` raw-byte variants [OA:nac.rs]. Step 3's closure is confirmed at the source level: the crate holding the key-establishment math ships as a closed-source stub whose mock methods panic; only the API contract is observable [OA:README.md], [OA:nac.rs].
|
||||
- The helper's export-code format (2026-09-11, rev 15 — **C58 closed** by facts-only extraction from the helper's public upstream repo, which is unlicensed — facts only): the payload is a 4-byte ASCII magic `OABS`, a one-byte flag (1 = "prevent sharing" mode, 0 otherwise), then a raw protobuf message (no length prefix; protobuf starts at offset 5). Protobuf field map: outer — 1 nested identifier message, 2 macOS product version (sysctl `kern.osproductversion`), 3 protocol version (hardcoded `1640`), 4 device id (the IOPlatformUUID again), 5 a hardcoded iCloudHelper user-agent string, 6 a hardcoded AOSKit version string; nested identifier message — 1 product name (e.g. `MacBookPro18,3`), 2 MAC address (6 raw bytes), 3 platform serial, 4 platform UUID, 5 root-disk UUID, 6 board ID (Apple Silicon: literal prefix `Mac-` + lowercase hex of the device-tree board-id bytes), 7 OS build number (sysctl `kern.osversion`), 8/9/10/12/14 the five `_enc` obfuscated blobs read from the `IOPower:/` registry under Apple's key names (`Gq3489ugfi` serial, `Fyp98tpgj` platform UUID, `kbjfrfpoJU` root-disk UUID, `oycqAZloTNDm` ROM, `abKPld1EcMni` MLB — matching §1.1's label mapping), 11 ROM (Intel: the device-tree options ROM value; Apple Silicon: last 6 bytes of SHA-256 of `/chosen` `unique-chip-id`), 13 MLB string. Two delivery modes: plain = standard base64 of the payload (clipboard text, or the raw payload bytes as a QR code at medium error correction); restricted = AES-256-CBC/PKCS7 over the payload, ciphertext prefixed with ASCII `Salted__` + 8-byte random salt (legacy OpenSSL `enc` container), key/IV derived by the legacy EVP_BytesToKey KDF (repeated MD5 of previous-hash ‖ password ‖ salt to 48 bytes: first 32 = key, next 16 = IV) keyed by a human activation code (`MB` + 4×4 chars from the alphabet `ABCDEFGHJKLMNPQRSTUVWXYZ123456789`), then base64. The restricted mode additionally uploads the blob (JSON `data` + `id` = lowercase hex SHA-256 of the activation code) to the vendor's server (`hw.openbubbles.app/code`) — the ≤20-user/one-time/48-hour mechanics live SERVER-side; nothing is embedded in the payload (no checksum, signature, compression, or expiry). No public parser exists (the client apps are closed-source); the construction above is the parse specification [MHG:ViewController.swift], [MHG:HwIdentifiers.swift], [MHG:mac_hw_info.pb.swift].
|
||||
- The validation blob is submitted as the `validation-data` field of the IDS register body and, base64, in the `X-Mme-Nas-Qualify` header of the iCloud account login [RP:ids/user.rs], [RP:auth.rs]. `TODO(capture):` C5 — exact byte shape of the blob a genuine Mac produces, and the current exchange over the two validation bag URLs. — 2026-10-04 (rev 26, live): **REPLAY GUARD — one mint = one device record.** Apple keys a validation blob's consumption server-side: re-submitting an already-consumed blob at the sign-in/register pair answers HTTP 409, which is a REPLAY refusal, NOT a blob rejection — a fresh blob under the same device identity succeeds where the reused one failed, and a captured blob of external provenance succeeds outright (session 17: a captured stamp replaced the local mint and the 409 vanished). Client consequences: mint or fetch a fresh blob per registration attempt (the 15-minute validity above makes stale reuse doubly fatal), and mint the sign-in's blob AT SIGN-IN TIME — never a login-start blob carried across the 2FA wait. A caller-injected blob from any source (local mint, relay Mac, staged capture) is a legitimate stamp; the injection points are the register body's `validation-data` and the sign-in's `X-Mme-Nas-Qualify` header.
|
||||
- Hardware-related anisette headers seen in activation flows: `X-Apple-I-SRL-NO` (serial), `X-Apple-I-MLB` (logic-board serial), `X-Apple-I-ROM` (hex ROM, lowercase) [RP:relay.rs], [APA:anisette]. `TODO(capture):` C6 — the full anisette header set a current client actually sends, including which optional hardware headers appear. — 2026-09-10: the header-set half of C6 is closed by facts-only extraction: per-request anisette headers `X-Apple-I-Client-Time`, `X-Apple-I-TimeZone`, `X-Apple-Locale`, `X-Apple-I-MD-RINFO`, `X-Mme-Device-Id`, `X-Apple-I-MD` (base64 OTP), `X-Apple-I-MD-M` (base64 machine ID), plus per-platform hardware headers — macOS: `X-Apple-I-MLB`, `X-Apple-I-ROM` (lowercase hex), `X-Apple-I-SRL-NO` [APA:omnisette], [APA:icloud-auth], [RP:relay.rs]. They are produced by an ADI provisioning exchange with gsa.apple.com (start/finish provisioning returning machine tokens), then a local OTP [APA:omnisette]. GSA context (cross-referenced with C6): the Albert activation itself needs NO Apple-Account login — only the FairPlay-signed activation-info plist (§1.2); the GSA login (full Apple-ID password, SRP over gsa.apple.com/grandslam/GsService2, s2k/s2k_fo PBKDF2, 2FA-capable) feeds the delegate logins that yield the IDS `auth-token`/`profile-id` and the PET token [RP:auth.rs], [APA:icloud-auth]. The extraction confirms the validation blob rides base64 in the `X-Mme-Nas-Qualify` header of the iCloud sign-in POST (recorded above) [RP:auth.rs]. — 2026-09-11 (rev 14 extraction round over the anisette crates): the value-format and provisioning halves of C6 are closed. Per-request value forms: `X-Apple-I-Client-Time` = UTC now truncated to whole seconds, RFC 3339/ISO-8601 with the offset suffix replaced by `Z` (provisioning requests send the raw offset form without the Z — a recorded inconsistency between code paths); `X-Apple-I-TimeZone` = literal `UTC` (not the device zone); `X-Apple-Locale` = `en_US` hardcoded (a companion `X-Apple-I-Locale: en_US` variant exists on the browser-style header set); `X-Apple-I-MD-RINFO` = verbatim passthrough of the string returned in the finish-provisioning response; `X-Mme-Device-Id` = lowercase hyphenated UUID string of 16 locally random bytes (`keychain_identifier`); `X-Apple-I-MD-M` = base64 (standard, padded) of the 60-byte `mid`; `X-Apple-I-MD-LU` = lowercase hex (no dashes) SHA-256 over the same 16 bytes (real macOS instead uses AuthKit's local-user UUID) — present in some header sets, omitted in others (disagreement — C57). Header sets are cached 60 s; a fresh OTP is generated per cache miss. The ADI provisioning flow: `GET https://gsa.apple.com/grandslam/GsService2/lookup` → XML plist whose `urls` keys `midStartProvisioning`/`midFinishProvisioning` give the endpoint URLs (never hardcoded); POST to the start URL with an XML plist body of two empty top-level dicts `Header` and `Request` → response `Response` → `spim` (base64); a local session is built from `spim` (+ empty hostuuid, DSID `-2`, flavor `Mac`=1 / `IOS`=3, flavor chosen by whether the client-info string contains `iPhone OS`) yielding `cpim` (base64); POST to the finish URL with `Request` = {`cpim`} → response `Response` → `ptm`, `tk` (both base64) and `X-Apple-I-MD-RINFO`; finishing the session with the decoded `tk`/`ptm` yields the machine state: `client_secret` (32 bytes), `mid` (60 bytes, Apple-issued at provisioning — reused for every OTP until re-provisioning, which happens only on a not-provisioned failure, e.g. ADI error -45061), `metadata` (variable blob). Provisioning HTTP specifics: `Content-Type: application/x-www-form-urlencoded` despite the XML-plist body (intentional), title-cased header names forced, HTTP/1.1, Apple root CA pinned (embedded DER), and a fixed companion header set on all three requests: akd `User-Agent`, `X-Apple-Baa-E: -10000`, `X-Apple-Baa-Avail: 2`, `X-Apple-Baa-UE: AKAuthenticationError:-7066|com.apple.devicecheck.error.baa:-10000`, `X-Apple-Host-Baa-E: -7066`, `X-Apple-Client-App-Name: akd`, `Accept: */*`, `Accept-Language: en-US,en;q=0.9`, plus `X-Apple-I-MD-LU`, `X-Mme-Device-Id`, `X-Mme-Client-Info`, `X-Apple-I-Client-Time` and any caller-supplied hardware headers. State persists as an XML plist with `keychain_identifier` (data) + the provisioned fields (`client_secret`, `mid`, `metadata`, `rinfo`, `flavor`) [APA:omnisette]. CLOSED-SOURCE RESIDUE: the OTP (`X-Apple-I-MD`) and trusted-device 2FA-code derivations live inside a closed-source crate — its public repo is a stub with `todo!()` methods; recorded is only the call contract: inputs (32-byte `client_secret`, 60-byte `mid`, `metadata`, flavor) → raw OTP bytes (base64 into the header) / 32-bit 2FA code [OA-stub], [APA:omnisette]. — rides C57/C56 residue; the seam stays injectable in the code. — 2026-10-07 (rev 27, clean-room RE of the OpenBubbles ADI library — unstripped Linux `.so`, oracle-verified; the derivation is no longer closed residue): the OTP is fully local and purLine truncated
|
||||
- Hardware-related anisette headers seen in activation flows: `X-Apple-I-SRL-NO` (serial), `X-Apple-I-MLB` (logic-board serial), `X-Apple-I-ROM` (hex ROM, lowercase) [RP:relay.rs], [APA:anisette]. `TODO(capture):` C6 — the full anisette header set a current client actually sends, including which optional hardware headers appear. — 2026-09-10: the header-set half of C6 is closed by facts-only extraction: per-request anisette headers `X-Apple-I-Client-Time`, `X-Apple-I-TimeZone`, `X-Apple-Locale`, `X-Apple-I-MD-RINFO`, `X-Mme-Device-Id`, `X-Apple-I-MD` (base64 OTP), `X-Apple-I-MD-M` (base64 machine ID), plus per-platform hardware headers — macOS: `X-Apple-I-MLB`, `X-Apple-I-ROM` (lowercase hex), `X-Apple-I-SRL-NO` [APA:omnisette], [APA:icloud-auth], [RP:relay.rs]. They are produced by an ADI provisioning exchange with gsa.apple.com (start/finish provisioning returning machine tokens), then a local OTP [APA:omnisette]. GSA context (cross-referenced with C6): the Albert activation itself needs NO Apple-Account login — only the FairPlay-signed activation-info plist (§1.2); the GSA login (full Apple-ID password, SRP over gsa.apple.com/grandslam/GsService2, s2k/s2k_fo PBKDF2, 2FA-capable) feeds the delegate logins that yield the IDS `auth-token`/`profile-id` and the PET token [RP:auth.rs], [APA:icloud-auth]. The extraction confirms the validation blob rides base64 in the `X-Mme-Nas-Qualify` header of the iCloud sign-in POST (recorded above) [RP:auth.rs]. — 2026-09-11 (rev 14 extraction round over the anisette crates): the value-format and provisioning halves of C6 are closed. Per-request value forms: `X-Apple-I-Client-Time` = UTC now truncated to whole seconds, RFC 3339/ISO-8601 with the offset suffix replaced by `Z` (provisioning requests send the raw offset form without the Z — a recorded inconsistency between code paths); `X-Apple-I-TimeZone` = literal `UTC` (not the device zone); `X-Apple-Locale` = `en_US` hardcoded (a companion `X-Apple-I-Locale: en_US` variant exists on the browser-style header set); `X-Apple-I-MD-RINFO` = verbatim passthrough of the string returned in the finish-provisioning response; `X-Mme-Device-Id` = lowercase hyphenated UUID string of 16 locally random bytes (`keychain_identifier`); `X-Apple-I-MD-M` = base64 (standard, padded) of the 60-byte `mid`; `X-Apple-I-MD-LU` = lowercase hex (no dashes) SHA-256 over the same 16 bytes (real macOS instead uses AuthKit's local-user UUID) — present in some header sets, omitted in others (disagreement — C57). Header sets are cached 60 s; a fresh OTP is generated per cache miss. The ADI provisioning flow: `GET https://gsa.apple.com/grandslam/GsService2/lookup` → XML plist whose `urls` keys `midStartProvisioning`/`midFinishProvisioning` give the endpoint URLs (never hardcoded); POST to the start URL with an XML plist body of two empty top-level dicts `Header` and `Request` → response `Response` → `spim` (base64); a local session is built from `spim` (+ empty hostuuid, DSID `-2`, flavor `Mac`=1 / `IOS`=3, flavor chosen by whether the client-info string contains `iPhone OS`) yielding `cpim` (base64); POST to the finish URL with `Request` = {`cpim`} → response `Response` → `ptm`, `tk` (both base64) and `X-Apple-I-MD-RINFO`; finishing the session with the decoded `tk`/`ptm` yields the machine state: `client_secret` (32 bytes), `mid` (60 bytes, Apple-issued at provisioning — reused for every OTP until re-provisioning, which happens only on a not-provisioned failure, e.g. ADI error -45061), `metadata` (variable blob). Provisioning HTTP specifics: `Content-Type: application/x-www-form-urlencoded` despite the XML-plist body (intentional), title-cased header names forced, HTTP/1.1, Apple root CA pinned (embedded DER), and a fixed companion header set on all three requests: akd `User-Agent`, `X-Apple-Baa-E: -10000`, `X-Apple-Baa-Avail: 2`, `X-Apple-Baa-UE: AKAuthenticationError:-7066|com.apple.devicecheck.error.baa:-10000`, `X-Apple-Host-Baa-E: -7066`, `X-Apple-Client-App-Name: akd`, `Accept: */*`, `Accept-Language: en-US,en;q=0.9`, plus `X-Apple-I-MD-LU`, `X-Mme-Device-Id`, `X-Mme-Client-Info`, `X-Apple-I-Client-Time` and any caller-supplied hardware headers. State persists as an XML plist with `keychain_identifier` (data) + the provisioned fields (`client_secret`, `mid`, `metadata`, `rinfo`, `flavor`) [APA:omnisette]. CLOSED-SOURCE RESIDUE: the OTP (`X-Apple-I-MD`) and trusted-device 2FA-code derivations live inside a closed-source crate — its public repo is a stub with `todo!()` methods; recorded is only the call contract: inputs (32-byte `client_secret`, 60-byte `mid`, `metadata`, flavor) → raw OTP bytes (base64 into the header) / 32-bit 2FA code [OA-stub], [APA:omnisette]. — rides C57/C56 residue; the seam stays injectable in the code. — 2026-10-07 (rev 27, clean-room RE of the OpenBubbles ADI library — unstripped Linux `.so`, oracle-verified; the derivation is no longer closed residue): the OTP is fully local and purLine truncated
|
||||
|
||||
### 1.2 Activation endpoints and certificates
|
||||
|
||||
@@ -331,6 +331,7 @@ Ordered by value. IDs match the inline `TODO(capture):` markers above.
|
||||
|
||||
## Revision history
|
||||
|
||||
- Rev 29 (2026-10-07, continuation capture — two further live OpenBubbles provisioning runs under gdb, `gdb_prov8/9.gdb`): the cpim's X group is fully pinned — X = SHA-256(encout ‖ spim_map_signature(trailing[0:16]) ‖ a fixed 24-byte .rodata filler constant, digest byte-exact on the wire — closing the X32, 12-B-plaintext-tail (12 fresh RNG bytes, same pool construction as the IV) and trailing[0:16] (the signature input) TODOs; the "constant sig block" reading is corrected (it was a stale session's trailing head) and "sig16/digest not on the wire" is resolved (both ride transitively through X). Kotlin `CpimShaTailSeal` wired as the production default; 195 tests green.
|
||||
- Rev 28 (2026-10-07, live capture — one real OpenBubbles provisioning run under gdb over the unstripped Linux `.so`; dumps + method in `registration/dev/clearadi/captures/WIRE-NOTES.md`): the §1.1 anisette framer is corrected on every point the previous session left `TODO(capture)`. The cpim is an OUTER 276-B message `[u32be(5)][iv 16][u32be(160)][encout 160][X 32][u32be(52)][u32be(4)][Y 52]` over a 160-B plaintext; the PTM is `[u32 BE 4][seed 16][u32 BE payloadLen][payload]` with the seed riding into decryptPTM as its chain seed; ProvisionedData = `{metadata Vec (400 B), client_secret = rng2 (32 B), mid = rng3 (60 B), flavor u8}` with the finish gate = the decrypt output's out[408:440] == rng1 echo; the spim carries 99 trailing bytes (the ≤23-B cap was a probe artifact) and trailing[16:76] is relayed verbatim as the wire tail's `Y` group. Superseded readings recorded dead: the 'IV from Spim trailing fields' (it is the thread-RNG pool), the 'two 256-bit sig/sha tail fields' (neither the spim_map_signature output nor the SHA-256 digest appears on the wire), and the OTP-path ProvisionedMachine mid/secret ordering. Remaining `TODO(capture)`: the X32 source, the 12-B plaintext tail, trailing[0:16]. Kotlin `ClearAdiFraming`/`ClearAnisetteProvider` restructured to the live shapes; 194 tests green.
|
||||
- Rev 26 (2026-10-02→04, live registration of this protocol family against real Apple endpoints — Albert activation, APNs courier (packed ALPN), the full GSA chain through 2FA, and IDS authenticate + register all green; user-gated with a real Apple ID, `mailto:` handle ACTIVE): six live-verified fact groups beyond the extraction record. (1) **The 409's root cause is the validation-stamp REPLAY guard (§1.1)** — one mint = one device record: a consumed blob re-submitted answers HTTP 409 `ICLOUD_UNSUPPORTED_DEVICE`, a fresh (or captured-elsewhere) blob succeeds; the earlier readings (request shape, blob provenance) are dead. §1.2 authenticate + §1.4 register completed green after the stamp route was fixed, with the `mailto:` handle active (§1.3/§1.4). (2) **The courier's packed format is live (§3.1/§3.2, C19 CLOSED)** — the server selects `apns-pack-v1:4096:4096` and speaks the tag-based packed codec (full wire shape recorded from the wire); its chain anchors at the `Apple Server Authentication CA` intermediate, no TLS client certificate is demanded on that path, and the post-connect set-state + filter sequence is mandatory (unfiltered connections are dropped within seconds, invalidating the connect token). (3) **Apple's live GSA sign-in response is kebab-case** — delegate `service-data`/`status-message` (camelCase recordings become the fallback), and the sign-in body's client id rides `client-id` (§1.5). (4) **Anisette freshness is per-run and per-call** — fresh per SRP run incl. the post-2FA re-run (stale → `ec` −22421), fresh per 2FA call (frozen set → HTTP 434); the trigger/validate GETs carry the plist content type, and failed verifies name the real failure in the body plist's `Status.ec`/`em` (§1.5). (5) **The GSA `postdata` liveness event is recorded in full (§1.5)** — services `icloud`/`imessage`/`facetime`, `X-Apple-I-UrlSwitch-Info`/`X-Apple-HB-Token` binding, `ptkn` (−800012 with a dead push token — the courier connection must stay open across the registration), and the order sign-in FIRST, postdata AFTER (correcting the earlier reversed note). (6) **Albert activation refinements (§1.2)** — `DeviceCertRequest` is `<data>`(base64(PEM)), DER rejected; the accepted `FairPlayCertChain` is leaf + FairPlay CA + Apple Root CA concatenated; the response's `<Protocol>` block may arrive wrapped in a larger `<Document>` (parse the span); refusals state their reason in wrapped-UI page text. Code side: the packed courier codec + ALPN negotiation, `GsaPostdata`, the kebab-case parsing, the per-run/per-call anisette providers, the `<Protocol>`-span extraction, and the three-cert FairPlay chain all landed; the validation stamp stays caller-injected with the replay guard documented.
|
||||
- Rev 25 (2026-09-29, first LIVE GSA login of this protocol family — SRP init/complete, both 2FA steps, and the PET-bearing re-login against `gsa.apple.com` with a real Apple ID, user-gated; the delegate sign-in POST was attempted and refused): the §1.5 chain is live-confirmed end-to-end up to the delegate sign-in, with five behavioral facts beyond the extraction record. (1) `au` (`trustedDeviceSecondaryAuth` observed) rides INSIDE the complete response's `Status` dict — reading it at the `Response` level silently turns a 2FA login into a "logged in, no PET" dead end (§1.5 2FA). (2) The PET's second live-confirmed delivery channel: the post-2FA re-login's decrypted `spd` token map `t` (21 service tokens observed incl. `com.apple.gs.idms.pet`), while a no-2FA login's `spd` carries only `com.apple.gs.appleid.auth` and no PET — an earlier implementation inference that a no-2FA login suffices for the PET is dead; 2FA is mandatory for the PET on this account class (§1.5 PET). (3) The trusted-device trigger creates the code on the devices and must precede the prompt; triggering twice before one validate invalidates the outstanding code and re-prompts (§1.5 2FA). (4) Anisette is one-time per request: headers minted at flow start are spent by the GSA hops, and the delegate sign-in with a reused set is refused — mint fresh per POST, matching the reference client's per-request provider (§1.5 delegate logins). Post-login ADI behavior confirmed live: after an account login the machine re-provisions for the personal DSID (GetLoginCode → not-provisioned → full exchange succeeds), which is the personal-`X-Apple-I-MD-M` path, not a defect. (5) The delegate sign-in answered HTTP 409 `ICLOUD_UNSUPPORTED_DEVICE` with the recorded error-plist shape (§1.5 delegate logins) — a device-identity-bound refusal, the current open blocker. Velocity note: a persisted-password-hash re-login was rejected once with -22406 ("enter the correct password") after four full logins in 30 minutes, with the identical hash derivation succeeding twice earlier in the same window — treat as server-side velocity throttling, not a derivation defect.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package dev.fenn.imessage.registration
|
||||
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* Wire framing of the ClearADI provisioning exchange — the deku reads/writes around the
|
||||
* already-ported ciphers ([ClearAdiProvision]). Ground truth: `ProvisioningSession::new`
|
||||
@@ -26,7 +28,8 @@ class SpimFormatException(message: String) : Exception(message)
|
||||
* (session 19f) shows a real Spim carrying **99 trailing bytes** that parses fine — the
|
||||
* earlier "≤ 23 leftover" pin (error 0x41) was an artifact of the probe harness, not the
|
||||
* reader. The trailing bytes are server material the client relays verbatim into the Cpim
|
||||
* wire tail (see [Cpim]) and their leading words feed the encrypt IV selection pool.
|
||||
* wire tail (see [Cpim]) and their first 16 bytes are the `spim_map_signature` input
|
||||
* whose output feeds the tail's X field (session-19f continuation pin).
|
||||
*/
|
||||
data class Spim(
|
||||
val field1: Int,
|
||||
@@ -75,10 +78,12 @@ fun interface ClearAdiRng {
|
||||
* The Cpim wire tail — the 92 bytes after `[u32 BE 5][iv][u32 BE len][encout]` in the outer
|
||||
* message (live capture, session 19f: `cap_wiravec.bin`). Shape: `[X 32][u32 BE 52][u32 BE 4]
|
||||
* [Y 52]`, where the `[52][4][Y52]` group == the Spim's trailing[16:76] relayed verbatim
|
||||
* (server material) and **X (32 B) is not yet source-pinned** — TODO(live-capture) with a
|
||||
* second wire sample. The earlier §6 reading (two 256-bit sig/sha fields) is falsified: the
|
||||
* `spim_map_signature` output and the SHA-256 digest appear nowhere in the captured wire.
|
||||
* Tests seal deterministically; production wires in once X is named.
|
||||
* (server material) and X (32 B) is live-pinned too (session 19f continuation, two runs
|
||||
* byte-exact): X = SHA-256(encout ‖ spimMapSignature(trailing[0:16]) ‖ CONST24) — the
|
||||
* `spim_map_signature` over the trailing's first 16 bytes feeds the digest, so neither the
|
||||
* sig16 nor the digest of encout alone appears in the wire (the §6 "two 256-bit sig/sha
|
||||
* fields" reading stays falsified). [CpimShaTailSeal] is the production seal; tests may
|
||||
* seal deterministically via their own [CpimTailSeal].
|
||||
*/
|
||||
fun interface CpimTailSeal {
|
||||
/**
|
||||
@@ -89,6 +94,35 @@ fun interface CpimTailSeal {
|
||||
fun seal(spimTrailing: ByteArray, encryptedPayload: ByteArray, iv: ByteArray): ByteArray
|
||||
}
|
||||
|
||||
/**
|
||||
* The live-pinned production tail seal: `X = SHA-256(encout ‖ sig ‖ CONST24)` with
|
||||
* sig = [ClearAdiProvision.spimMapSignature] over the Spim trailing's first 16 bytes
|
||||
* (17f0921: the sig input is the trailing head, not a constant — the run-3/8/10
|
||||
* "constant block" reading was a stale stack slot), then the digest rides the wire
|
||||
* byte-exact (17f0d0d–17f0d63 shuffle = u32 BE pack, 17f1223 wire write) and the
|
||||
* 60-byte relayed group follows (17f1223 wire write order: X then trailing[16:76]).
|
||||
* The 24-B constant is at .so vaddr 0x24cf4e (appended into the hash stream at
|
||||
* 17f0a32/17f0ad8 — a fixed protocol filler, not key material).
|
||||
*/
|
||||
object CpimShaTailSeal : CpimTailSeal {
|
||||
private val CONST24 = byteArrayOf(
|
||||
0x37, 0x95.toByte(), 0x02, 0x0c, 0x05, 0xf7.toByte(), 0x70, 0x09,
|
||||
0xcb.toByte(), 0xfe.toByte(), 0xaa.toByte(), 0x78, 0xa5.toByte(), 0xbc.toByte(), 0xb3.toByte(), 0x17,
|
||||
0x5b, 0xad.toByte(), 0x60, 0xb9.toByte(), 0xde.toByte(), 0xaa.toByte(), 0xaa.toByte(), 0x8d.toByte(),
|
||||
)
|
||||
|
||||
override fun seal(spimTrailing: ByteArray, encryptedPayload: ByteArray, iv: ByteArray): ByteArray {
|
||||
require(spimTrailing.size >= 76) {
|
||||
"spim trailing of ${spimTrailing.size} B cannot supply the 60-byte relayed group"
|
||||
}
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
md.update(encryptedPayload)
|
||||
md.update(ClearAdiProvision.spimMapSignature(spimTrailing.copyOfRange(0, 16)))
|
||||
md.update(CONST24)
|
||||
return md.digest() + spimTrailing.copyOfRange(16, 76)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Cpim wire message (`request.cpim`, base64) — live-pinned from the session-19f capture
|
||||
* (`cap_wiravec.bin`, 276 B; the earlier §6 208-B reading is superseded):
|
||||
@@ -102,8 +136,9 @@ fun interface CpimTailSeal {
|
||||
* - blob = the caller's ≤ 60-byte `extra` padded with draw3 (draw3 = the machine `mid`)
|
||||
* - gsaArg = −2 (omnisette reference), wire `FF FF FF FF FF FF FF FE` — CONFIRMED live
|
||||
* - ts = unix seconds (live: 0x6ac64ac3 ≈ provisioning wall clock)
|
||||
* - the trailing `u32 BE 1` + 12 bytes are live-pinned in position; the 12 bytes' source is
|
||||
* TODO(live-capture) (modeled as RNG here)
|
||||
* - the trailing `u32 BE 1` + 12 bytes are live-pinned in position; the 12 bytes are fresh
|
||||
* RNG draws (session-19f continuation: low bytes of 12 consecutive thread-RNG pool words,
|
||||
* drawn right before the IV's 16)
|
||||
* - iv = 16 RNG bytes (17f0782–17f0888 draw them from the thread RNG pool — the earlier
|
||||
* "IV assembled from the Spim's trailing fields" reading was the RNG pool, not the Spim)
|
||||
*/
|
||||
|
||||
+1
-1
@@ -171,7 +171,7 @@ class ClearAnisetteProvider(
|
||||
private val stateStore: ClearAdiStateStore,
|
||||
private val loginInfo: ClearAdiLoginInfo,
|
||||
private val rng: ClearAdiRng,
|
||||
private val cpimTailSeal: CpimTailSeal,
|
||||
private val cpimTailSeal: CpimTailSeal = CpimShaTailSeal,
|
||||
/** 16 random bytes identifying this client (reference `keychain_identifier`). */
|
||||
private val keychainIdentifier: ByteArray = rng.draw(16),
|
||||
private val clock: () -> Long = { System.currentTimeMillis() },
|
||||
|
||||
+18
@@ -70,6 +70,24 @@ class ClearAnisetteProviderTest {
|
||||
ByteArray(32) + spimTrailing.copyOfRange(16, 76)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cpimShaTailSealMatchesPinnedFormula() {
|
||||
// session-19f continuation: X = SHA-256(encout ‖ spimMapSignature(trailing[0:16]) ‖ CONST24),
|
||||
// tail = X ‖ trailing[16:76] — verified byte-exact against two live runs (cap8/cap9).
|
||||
val trailing = ByteArray(99) { ((it * 7 + 3) and 0xff).toByte() }
|
||||
val encout = ByteArray(32) { ((it * 11 + 5) and 0xff).toByte() }
|
||||
val tail = CpimShaTailSeal.seal(trailing, encout, ByteArray(16))
|
||||
assertEquals(92, tail.size)
|
||||
assertContentEquals(
|
||||
hex(
|
||||
"342f120b8ed971980b7a4641822e648a4da000004a7e3116ee65a33a8999c2ab" +
|
||||
"737a81888f969da4abb2b9c0c7ced5dce3eaf1f8ff060d141b222930373e454c535a61686f767d" +
|
||||
"848b9299a0a7aeb5bcc3cad1d8dfe6edf4fb020910",
|
||||
),
|
||||
tail,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cpimBuildMatchesLiveWireLayout() {
|
||||
val extra = ByteArray(10) { 0xAB.toByte() }
|
||||
|
||||
Reference in new issue
Block a user