diff --git a/docs/imessage-protocol-spec.md b/docs/imessage-protocol-spec.md index a2326f0..bcbba15 100644 --- a/docs/imessage-protocol-spec.md +++ b/docs/imessage-protocol-spec.md @@ -1,10 +1,10 @@ # iMessage protocol spec (clean-room) -**Status:** draft — revision 24. Rev 24 (2026-09-28, first live probes of this implementation — two unauthenticated bag GETs + a no-client-cert courier TLS handshake, runner `:engine:runProbe`; nothing else touched): (1) C18 SETTLED — the courier demands the Albert client certificate via a fatal TLS 1.3 `certificate_required` alert after a successful handshake, on both ports (§3.1); (2) the bags are TYPED CONFIG DICTIONARIES, not URL maps — the live IDS bag carries ints/bools/floats/string-lists alongside 400+ URLs and a `bag-expiry-timestamp` (~17-day observed TTL; 403 entries live vs 405 on 2026-09-10, two new push-debounce keys in, four A/B-test keys out); the APNs bag is 65 stable entries incl. BAA-authentication flags (`APNSEnableBAAAuthenticationForDevices` etc. — C17/C20-relevant); (3) AppleTrust's embedded-root pinning validates the live chains of `init.ess.apple.com` and `courier.push.apple.com` (no root served — chain anchored at the embedded root); (4) rev-23's container-fact follow-ups: sort-version 0 and the ref-width rule are now capture/probe-consistent. Rev 23 (2026-09-27, writer-pass follow-up, [CF] public material + host-decoder empirical cross-check only — no restricted source): three §4 container-format gaps found by the first writer session are closed — integer signedness (1/2/4-byte forms unsigned; the 8-byte form is two's-complement signed; non-negative values take the minimal width, negative values always the 8-byte form), the object-reference width rule (minimal width whose 2^(8w) ≥ object count, so every reference 0..count−1 is representable), and the sort-version byte (written as 0). Rev 22 (2026-09-12, offline byte-audit of the saved live step-2 request — no network call, nothing committed): the CLIENT-SIDE ENCODING class is closed. The exact 3898 bytes the live runner sent (§7 item 24) parse as an XML plist with the single `session-info-request` `` key, and its 2729-byte payload walks the invented TLV v1 to the last byte with ZERO slack, 17 distinct tags, no duplicates and no trailing bytes — our bytes are exactly what the code intends, so no writer bug hides there (the framing *intent* stays invented and unverified). By-product fact: the step-1 `cert` data field is NOT a bare DER cert — it is Apple's own container, `01 02` followed by two `(4-byte BE length, DER cert)` pairs (1046 and 1329 bytes; 2+4+1046+4+1329 = 2385 exactly), carried through verbatim by this implementation (§1.1) and cross-checking against the archived genuine step-1 response size. Apple's length convention there is 4-BYTE big-endian where our TLV v1 uses 2 — the only Apple framing ground truth in hand, and it weakens TLV v1 further. Rev 21 (2026-09-11, step-2 refusal re-read + offline cross-checks — no new live calls): one correction and five facts. CORRECTION — the rev-20 "BODY-INDEPENDENT" conclusion is withdrawn as unproven: a 20-byte non-plist body and our 3898-byte reconstruction returning the same generic `status` 5003 establishes only that 5003 is this endpoint's generic request-level failure; it cannot separate "the body is never parsed" from "both bodies are parsed and both rejected identically", because the reconstruction is itself unverified — the `session-info-request` blob is minted by the closed-source library (§1.1 [OA]). Blob FRAMING is therefore live again beside the envelope. FACTS — (1) 5003 is named in the IDS status space: `IDSStatusBadRequest` [IG:imessage/direct/ids/types/idserrors.gen.go], a table auto-generated from web FaceTime's JavaScript, so it is the IDS family's space and not proven for this WebObjects service; that same table shows what a body-level rejection in this family looks like (`ParseFailure` 1000, `InvalidField` 1001, `DecodeFailureBase64`/`Hex` 1004/1005, `MissingRequiredKey` 5008) and the endpoint returned none of them. (2) `reference/rustpush` IS the OpenBubbles fork (`origin` = github.com/OpenBubbles/rustpush, i.e. the genuine client the capture plan names); its tip re-fetched 2026-09-11 equals the staged snapshot, and its step-2 call is still `REQWEST.post(bag_url).body(xml_plist)` with no content-type and one default header `Accept-Language: en-US,en;q=0.9` — a current model client's envelope is body-only, which makes the header half the weakest suspect and the blob the strongest. (3) The route is confirmed against a GENUINE device: the archived 2026-09-10 capture's `id-initialize-validation` bag value is byte-identical to the URL the live run posted to. (4) That genuine bag enables attestation on this endpoint family — `absinthe-baa-option-enabled`/`-ios13` and `absinthe-v3-enabled` all true, `absinthe-baa-cert-ttl-minutes` 144000 — the one mechanism that could still put the envelope back in front. (5) Both SED traces are now recorded as a grammar: `SED:3:VEN-PROD:::::<64-hex>:EM`; between the two runs every uuid and the epoch differ (the epochs 28 s apart, matching the calls' spacing) and the 64-hex differs, so it is per-request, not a digest of any recorded input (checked against the body, the empty string and the step-2 URL). Next lever unchanged: the genuine-client capture — valued now for the BLOB it reveals, not the header set. Rev 20 (2026-09-11, live step-2 control experiment): the step-2 refusal is BODY-INDEPENDENT — a 20-byte body that is not a plist at all and the full reconstructed 3898-byte request are answered identically (HTTP 200, the same 498-byte `status` 5003 plus a nine-token SED trace; only its per-request epoch and uuids differ between the two runs). The invented TLV framing, the wrapper encoding, and the step-2 content-type are therefore ALL retired as causes at this endpoint: nothing in the body is examined before the refusal. The rev-18 "byte-identical" reading is corrected — the trace carries a live per-request epoch (two runs 28 s apart) and three per-request uuids, so the responses were shape-identical, and the rev-19 inference chain from that reading is superseded by this direct experiment. Surviving suspect class: the request ENVELOPE (the header set a genuine client carries, or an identity the endpoint resolves before parsing); the live step-2 URL is now recorded (§1.1, §7 item 24). Rev 19 (2026-09-11, step-2 self-check round): the step-2 request reconstruction was audited against the extracted client's own request construction (§7 item 24) — the XML-plist wrapper is confirmed, while our `text/x-xml-plist` default turned out to be an unsupported inference: the recorded client sets NO content-type on the step-2 POST (§1.1), so all three content-types of the rev-18 probe matrix were non-faithful (the constant is now absent in the code); the rev-18 "invariant under every flipped variable" claim is corrected to a *body-content* independence (the five attempts did NOT send identical bodies — fresh key material each time), which retires the step-2 crypto variables and leaves the invented TLV framing as the prime suspect; and an archived genuine-device capture shows the validation endpoint family reached with the full anisette/BAA header set while step 1 succeeds header-less because that URL is a CDN static object (§1.1) — next levers: one faithful (no content-type) step-2 POST beside a deliberate control POST, then a genuine-client capture. Rev 18 (2026-09-11, first live step-1 attempt + probe matrix): the §1.1 validation exchange ran against the live Apple endpoints for the first time — bag + step-1 cert GET OK (pinned TLS, HTTP 200); FIVE step-2 POST variants (default C60 set, X25519, HMAC, three content-types) all answered HTTP 200 with the same 498-byte error plist (`status` 5003, SED trace message) and no `session-info` (§1.1, §7 item 24) — the refusal is invariant under every flipped variable, so the C60 crypto hypotheses remain open and the next lever is a genuine-client wire capture; the SED status table is a new TODO(capture). Rev 17 (2026-09-11, FairPlay material metadata extraction): the provenance half of the FairPlay identity question is closed (§1.2, §7 item 21) — all known embedded copies across the reference repos are ONE 1024-bit RSA identity (leaf cert expired 2024, yet accepted), descending from a 2023 iCloud-bypass community release; rustpush's "ten pairs" are ten copies; no own-Mac extraction path is documented anywhere (open research item); BYO-identity is recorded as the sane architecture. Rev 16 (2026-09-11, activation implementation round): the three remaining GSA header literals are pinned (§1.5 — browser UA, non-akd `X-MMe-Client-Info`, the full-wrapper `X-Apple-I-CFU-State` plist, the bracketed sign-in client-info); the validation-data exchange is implemented — steps 1–2 concrete, step 3 as an explicit contract reconstruction with six named hypotheses (key agreement, KDF, signature, session-info interpretation, blob framing, step-2 content-type) collected as new **C60** (§7 item 24) and arbitrable only by one live activation; the Mac-Hardware-Info export parser and the C59 constants are wired into code. Rev 15 (2026-09-11, second activation extraction round — C59 + C58 closed): the GSA-login residue is recorded (§1.5 closure paragraph: RFC 5054 2048-bit SRP group with g=2, the full 2FA browser header set with literal values, per-flavor cpd values, akd UA/`X-MMe-Client-Info` forms, `spd` `t`-map inner keys + top-level `GsIdmsToken` + `fn`/`ln`, the `gsas.apple.com` postdata/teardown host split, the sign-in UA/timezone forms); C58 is closed — the Mac-Hardware-Info export-code format is fully recorded in §1.1 (`OABS` magic + flag byte + protobuf field map; plain-base64 or OpenSSL-`Salted__` AES-256-CBC delivery; sharing mechanics server-side; upstream repo unlicensed — facts only). Citation legend gains [MHG]. Rev 14 (2026-09-11, activation extraction round — facts-only over the anisette/GSA crates and the Mac client, read-only agents, main-session merge): C6's value-format and provisioning halves are closed (§1.1 — per-request anisette value forms, the ADI provisioning flow with `spim`/`cpim`/`ptm`/`tk`, the 60-byte Apple-issued `mid`, the 16-byte local `keychain_identifier`, machine-state persistence); new §1.5 records the full GSA login chain end-to-end (SRP-6a 2048-bit with the GSA empty-username identity-hash quirk, `s2k`/`s2k_fo`, `spd` decryption, both 2FA variants + the Circle PAKE, PET semantics, delegate logins yielding the IDS `authToken`/`profileId`, the `X-Mme-Nas-Qualify` sign-in POST); the five obfuscation labels are mapped to their fields (§1.1); C56 step-3 and the OTP derivation are CONFIRMED closed at source (upstream ships stubs — contracts only). Rev 13 (2026-09-11, implementer round — rev 12's closed fact sets wired into code by four sequential firewall-clean implementer components; no new protocol facts): courier command builders + connect/auth + the engine's courier frame handler are concrete (§3.3, C45); lookups ride the APNs tunnel with the C47/C48 wire names, plain-string `uris`, and C55 top-level-status routing (§2.1); register/authenticate bodies and the Albert activation wire half are concrete (C53/C54/C8, §1.2/§1.4); envelope fixes landed (HKDF salt `"LastPawn-MessageKeys"` with empty info, pair HMAC keying, recorded pair-ec field numbers, dtl array-of-dicts regression-tested, §4). Nothing closed was re-opened. C57 (§7 item 21) gains six writer-pass gaps: filter multi-topic encoding, connect field-0x10 position, connect-response 5/6 shapes, tunnel 96/97 field ids + payload framing/encoding + status table, phone-flow authentication-data value encodings, Albert request/response value types. Rev 12 (2026-09-10, facts-only extraction round over both source repos plus the OpenAbsinthe-Stub mock): closed C45 (courier field-id tables + length-prefix scope, §3.2–3.3), C47/C48 (lookup headers/keys, §2.1), C49/C51/C52 and C50's protobuf half (§4), C53 (register wire names, §1.4), C54 (authenticate exchange, §1.2), C8's wire half (Albert activation, §1.2), and C56's steps 1–2 with the GSA login + full anisette set (§1.1). Corrected: the HKDF "info string" is the salt (§4.3); `dtl` is an array of dicts; payload command 160 is not a frame command (§3.3, §5.1). Narrowed: C55 — both clients read lookup error statuses from the top-level `status` only (§2.1, §7 item 19). New first-class architecture fact: IDS lookups ride the APNs tunnel, never plain HTTPS (§2.1). New **C57** (§7 item 21) collects extraction disagreements and closed-source residue. Legend gains [OA]. Rev 11 (2026-09-10, OpenBubbles-flow research — C5/C6 model corrected): the Mac's role in activation is **identifier export only** — the shipped OpenBubbles helper (`Mac-Hardware-Info` 1.2.0, notarized) reads serial/board-ID/model via public IORegistry/sysctl APIs on any macOS ≥ High Sierra *including 26.x*, and never calls an Apple private framework (§1.1, §6.1). The prior "minting requires a Mac on macOS ≤ 14.3" conclusion bound only framework-calling helpers (beeper's archived `mac-registration-provider`; rustpush running *on* a Mac); the remaining C5/C6 work is client-side software — the generation procedure, recorded as new **C56** (§7 item 20). Legend gains [OB-MHI]. Rev 10 (2026-09-10, Phase 2 lookup-orchestration component): added C55 (§2.1) — the lookup response's error-status locations: whether 5206 ("response too large"), 6005, and the send-side 5040/5041 session-token codes appear as a top-level response status, per-handle result statuses, or both; the lookup flow reads per-handle result statuses only, the one numeric-status location the fact set records. Rev 9 (2026-09-10, Phase 2 component 5): registered the registration wire-name gaps the component's writer pass hit — C53 (§1.4: register request/response key names beyond the recorded set, and the `x-auth-user-id-` value form) and C54 (§1.2: the authenticate request/response pair — body wire keys, CSR encoding, CN encoding, self-signature algorithm, response keys) — plus §7 item 18. The activation code takes every unrecorded name as caller-supplied configuration and keeps every unrecorded response field in a raw map. Rev 8 (2026-09-10, Phase 2 component 4): [CAP-IDSTLS] (§1.2) fixed the IDS TLS anchor set — `Apple Root CA - G3` and `Apple Root CA`, roots not served, system store rejects the unanchored chain — and [CAP-BAGCT] (§1.2, §7 item 16) closed C47's encoding half: both bag responses and the embedded bag payload are XML plists on the wire, content-type labels notwithstanding. The component-4 writer pass added TODO(capture) C49–C52 and §7 item 17 (unrecorded plist/body value types, the §4.2 HMAC key + ECDSA wire encoding, §4.3 protobuf field numbers beyond validator=99 and the HKDF salt). Rev 7 (2026-09-10, Phase 2 component 3): recorded the IDS lookup/bag wire details the phase-1 fact set never captured — endpoint plist encodings and the unrecorded lookup header/body/response key names (C47/C48, §1.2, §2.1, §7 item 16). The writer phase guesses none of them: they are configuration in the code until a capture closes them. Rev 6 (2026-09-10, Phase 2 component 2): added [CAP-COURIERTLS] chain ground truth to §3.1 and the C45 framing-gaps TODO (§3.2, §7 item 15). Rev 5 (2026-09-10, Phase-2 start): added the publicly documented binary-plist container format to §4.1 ([CF]) so the clean-room codec can be written from the spec alone. Revision 2 merged three phase-1 extractions plus first capture ground truth (own mitmproxy captures of a registered iPhone 14,4 running iOS 26.1, 2026-09-10 — still the only post-PQ3 evidence here). Revision 3 (2026-09-10) is a second targeted extraction round over both source repos on the three highest-value gaps (lifetimes C41/C10, envelope selection C25, bag/registration C1/C3); it corrects two rev-2 misreadings (the HTTP signature construction is agreed, not disagreed, §1.4; the pair-ec validator final byte is agreed 0x0c, not "0xc0-ish", §4.3) and re-reads the ~45-day certificate figure as a client-side cap (§6.1). Revision 4 (2026-09-10) is the phase-1 review-gate pass: removed one leaked source identifier (`registered_at`, rewritten as prose), paraphrased the only reproduced source fragment, added citation-form legend entries and the protocol-constant provenance notes; no copied expression, code, or structural transcription found. +**Status:** draft — revision 24. The full revision history is at the bottom ([Revision history](#revision-history)); the sections below state the facts as of rev 24. **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. -**Citation legend:** [APS-iMessage] Apple Platform Security, iMessage security overview · [APS-CKV] Apple Platform Security, Contact Key Verification · [APS-Send] Apple Platform Security, How iMessage sends and receives messages securely · [PQ3] Apple Security Research, iMessage with PQ3 · [JJ] jjtech.dev, iMessage explained · [BM] Beeper blog, How Beeper Mini Works · [BA] Beeper blog, How Beeper Android Works · [OB-FAQ]/[OB-Renewal]/[OB-Hosted]/[OB-PNR]/[OB-QS]/[OB-MHI] OpenBubbles docs · [CF] Apple's publicly documented binary property-list container format (Core Foundation; public Apple material — not a restricted source) · [CAP-REG]/[CAP-COURIER] own mitmproxy captures of a registered iPhone 14,4, iOS 26.1 (2026-09-10; `reference/imessage-captures/`, gitignored) — ground truth, post-PQ3 · [CAP-IDSTLS]/[CAP-BAGCT] direct unauthenticated probes from this machine (2026-09-10, `reference/imessage-captures/2026-09-10/`: openssl s_client TLS captures; byte-exact bag bodies with response headers) · [RP:file] rustpush source (SSPL, facts-only) · [APA:file] apple-private-apis source (MPL 2.0, facts-only) · [IG:file] imessagego source (AGPL, facts-only) · [OA] OpenAbsinthe-Stub public API surface (official mock of a closed-source implementation; call sites in rustpush) — facts-only · [MHG:file] the Mac-Hardware-Info helper's public upstream repo (github.com/OpenBubbles/Mac-Hardware-Info; unlicensed upstream — facts-only, no code reuse). Bare `[RP]`/`[IG]` = fact from that repo with the source file not recorded in the extraction notes. `[RP-issue-21]` = rustpush GitHub issue #21. +**Citation legend:** [APS-iMessage] Apple Platform Security, iMessage security overview · [APS-CKV] Apple Platform Security, Contact Key Verification · [APS-Send] Apple Platform Security, How iMessage sends and receives messages securely · [PQ3] Apple Security Research, iMessage with PQ3 · [JJ] jjtech.dev, iMessage explained · [BM] Beeper blog, How Beeper Mini Works · [BA] Beeper blog, How Beeper Android Works · [OB-FAQ]/[OB-Renewal]/[OB-Hosted]/[OB-PNR]/[OB-QS]/[OB-MHI] OpenBubbles docs · [CF] Apple's publicly documented binary property-list container format (Core Foundation; public Apple material — not a restricted source) · [CAP-REG]/[CAP-COURIER] own mitmproxy captures of a registered iPhone 14,4, iOS 26.1 (2026-09-10; captures on file, not published) — ground truth, post-PQ3 · [CAP-IDSTLS]/[CAP-BAGCT] direct unauthenticated probes from this machine (2026-09-10: openssl s_client TLS captures; byte-exact bag bodies with response headers) · [RP:file] rustpush source (SSPL, facts-only) · [APA:file] apple-private-apis source (MPL 2.0, facts-only) · [IG:file] imessagego source (AGPL, facts-only) · [OA] OpenAbsinthe-Stub public API surface (official mock of a closed-source implementation; call sites in rustpush) — facts-only · [MHG:file] the Mac-Hardware-Info helper's public upstream repo (github.com/OpenBubbles/Mac-Hardware-Info; unlicensed upstream — facts-only, no code reuse). Bare `[RP]`/`[IG]` = fact from that repo with the source file not recorded in the extraction notes. `[RP-issue-21]` = rustpush GitHub issue #21. **Provenance notes:** "NGM" (next-generation-message) is research terminology, not a wire string. Two §4.3 strings — the HKDF salt `"LastPawn-MessageKeys"` (rev-12 correction: it is the HKDF salt, with empty info — not an info string, as earlier recorded) and the fixed ASCII prekey-signature label — are interop-required protocol constants that are not directly wire-observable; both independent sources agree on them, and capture arbitration is trivial (a wrong string fails decryption or signature validation). APNs-bag constant names (§3, §7 C18/C20) and Apple private-framework symbol names (e.g. `NACKeyEstablishment`) are Apple-published/observable data, not source expression. @@ -29,7 +29,7 @@ - Bag shape and fetching (second-extraction facts): the live bag is fetched over GET from `https://init.ess.apple.com/WebObjects/VCInit.woa/wa/getBag?ix=3`; the response body is a plist dictionary with a single `bag` key whose value is embedded plist data — those bytes are themselves a plist parsed into a flat bag-key → URL dictionary [RP:util.rs:57-81]. The fetch is cached for the process lifetime keyed by bag URL; there is no TTL or refresh, and a missing key inside the parsed bag is its own error [RP:util.rs:59-81]. The APNs bag is fetched over plain HTTP from `http://init-p01st.push.apple.com/bag` and resolves integer `APNSCourierHostcount` and string `APNSCourierHostname` [RP:util.rs:56], [RP:aps.rs:1223-1224]. - The second implementation never fetches a bag at runtime: all endpoint URLs are compile-time pinned, and the bag fetch URL is only mentioned as the manual refresh source. Its pinned URLs: DS-ID authentication at `https://profile.ess.apple.com/WebObjects/VCProfileService.woa/wa/authenticateDS`, phone-number authentication at `https://identity.ess.apple.com/WebObjects/TDIdentityService.woa/wa/authenticatePhoneNumber`, dependent registrations at `.../TDIdentityService.woa/wa/getDependentRegistrations`, handles at `.../VCProfileService.woa/wa/idsGetHandles`, lookup at `https://query.ess.apple.com/WebObjects/QueryService.woa/wa/query` (used only inside the APNs HTTP tunnel, never plain HTTPS), registration at `https://identity.ess.apple.com/WebObjects/TDIdentityService.woa/wa/register` [IG:ids/bag.go:19-28], [IG:ids/urls.go:19-23], [IG:lookup.go:519-527]. It has no support at all for the alias-provisioning, spam-report, validation-certificate, validation-initialization, or multi-user phone-auth endpoints [IG:ids/bag.go, repo-wide]. -- IDS HTTPS requests in both implementations use a custom TLS trust pool containing only embedded Apple root certificates, not the system store [RP:util.rs:208-242], [IG:util/appletls/util.go]. — 2026-09-10 [CAP-BAG]: the live bag was fetched directly with an unauthenticated `GET https://init.ess.apple.com/WebObjects/VCInit.woa/wa/getBag?ix=3` — no auth, no anisette; the response is a plist with `signature`, `certs`, and `bag` keys, the inner bag a flat dictionary of **405 entries** (full response archived as `reference/imessage-captures/ids-bag-2026-09-10.xml`). This closes the live-bag-contents half of C1 without an instrumented client. Notably the endpoint presented a TLS chain the local system store rejected (consistent with the sources' Apple-root-only pinning). Every id-* URL that either source pins (`id-register`, `id-query`, `id-authenticate-ds-id`, `id-authenticate-phone-number`, `id-authenticate-multiple-users`, `id-get-dependent-registrations`, `id-get-handles`, `id-validation-cert`, `id-initialize-validation`, …) matches the live bag — no drift. The bag also carries the scalar `id-query-refresh-queued-query-interval: 28800` (8 h, queued-refresh interval). `TODO(capture):` C47 — the concrete plist encoding of both bag responses, and of the embedded `bag` payload those responses carry, is not stated in this fact set (the archives are named `.xml`, and that is the whole of the evidence here); neither is the response content type. — 2026-09-10 [CAP-BAGCT]: the encoding half of C47 is settled. `GET https://init.ess.apple.com/WebObjects/VCPermsService.woa/wa/getBag?ix=3` (the `VCPermsService.woa` app also serves the bag — [CAP-BAG] had used `VCInit.woa`) returned `Content-Type: text/xml`, 94,859 bytes, an XML plist, top-level keys `signature`/`certs`/`bag`; the embedded `bag` value is base64 of an XML plist (decodes to `…`). The APNs bag (`GET http://init-p01st.push.apple.com/bag`) returned `Content-Type: application/x-apple-plist` but the body is still an XML plist, 10,675 bytes, byte-identical across two same-day fetches. Verdict: both bag responses and the embedded bag payload are XML plists in every observed case — no binary plist on the bag wire; the content-type labels are unreliable (`text/xml` vs `application/x-apple-plist` with XML bodies) and parsing goes by content, not label. Also [CAP-IDSTLS]: `openssl s_client` with bare-hostname SNI to `init.ess.apple.com:443` and `query.ess.apple.com:443` — init serves leaf `CN=init.ess.apple.com` ← `Apple Server Authentication CA 2` ← `Apple Root CA - G3`; query serves leaf `CN=query.ess.apple.com` ← `Apple Server Authentication CA` (the older CA) ← `Apple Root CA`; neither root is served, and the unanchored chain is rejected by the system store (OpenSSL verify code 20; curl error 60 with the system bundle) — matching the Apple-root-only pool of the sources. Anchor set to embed: `Apple Root CA - G3` and `Apple Root CA` — Apple roots, distinct from the publicly-trusted Comodo/AAA root the courier chain needs ([CAP-COURIERTLS], §3.1); pin roots only, never the leaf. Both root DERs are staged in the worktree as `server/src/main/resources/imessage/AppleRootCA-G3.cer` and `AppleRootCA.cer` (G3 from Apple's live CA page; the G1 `Apple Root CA` is no longer published by Apple — sourced from the public CT log, crt.sh id 1160190, SHA-256 `B0:B1:73:0E:CB:C7:FF:45:05:14:2C:49:F1:29:5E:6E:DA:6B:CA:ED:7E:2C:68:C5:BE:91:B5:A1:10:01:F0:24`; G3 SHA-256 `63:34:3A:BF:B8:9A:6A:03:EB:B5:7E:9B:3F:5F:A7:BE:7C:4F:5C:75:6F:30:17:B3:A8:C4:88:C3:65:3E:91:79`) and each captured intermediate openssl-verifies against its root. +- IDS HTTPS requests in both implementations use a custom TLS trust pool containing only embedded Apple root certificates, not the system store [RP:util.rs:208-242], [IG:util/appletls/util.go]. — 2026-09-10 [CAP-BAG]: the live bag was fetched directly with an unauthenticated `GET https://init.ess.apple.com/WebObjects/VCInit.woa/wa/getBag?ix=3` — no auth, no anisette; the response is a plist with `signature`, `certs`, and `bag` keys, the inner bag a flat dictionary of **405 entries** (full response archived on file as `ids-bag-2026-09-10.xml`). This closes the live-bag-contents half of C1 without an instrumented client. Notably the endpoint presented a TLS chain the local system store rejected (consistent with the sources' Apple-root-only pinning). Every id-* URL that either source pins (`id-register`, `id-query`, `id-authenticate-ds-id`, `id-authenticate-phone-number`, `id-authenticate-multiple-users`, `id-get-dependent-registrations`, `id-get-handles`, `id-validation-cert`, `id-initialize-validation`, …) matches the live bag — no drift. The bag also carries the scalar `id-query-refresh-queued-query-interval: 28800` (8 h, queued-refresh interval). `TODO(capture):` C47 — the concrete plist encoding of both bag responses, and of the embedded `bag` payload those responses carry, is not stated in this fact set (the archives are named `.xml`, and that is the whole of the evidence here); neither is the response content type. — 2026-09-10 [CAP-BAGCT]: the encoding half of C47 is settled. `GET https://init.ess.apple.com/WebObjects/VCPermsService.woa/wa/getBag?ix=3` (the `VCPermsService.woa` app also serves the bag — [CAP-BAG] had used `VCInit.woa`) returned `Content-Type: text/xml`, 94,859 bytes, an XML plist, top-level keys `signature`/`certs`/`bag`; the embedded `bag` value is base64 of an XML plist (decodes to `…`). The APNs bag (`GET http://init-p01st.push.apple.com/bag`) returned `Content-Type: application/x-apple-plist` but the body is still an XML plist, 10,675 bytes, byte-identical across two same-day fetches. Verdict: both bag responses and the embedded bag payload are XML plists in every observed case — no binary plist on the bag wire; the content-type labels are unreliable (`text/xml` vs `application/x-apple-plist` with XML bodies) and parsing goes by content, not label. Also [CAP-IDSTLS]: `openssl s_client` with bare-hostname SNI to `init.ess.apple.com:443` and `query.ess.apple.com:443` — init serves leaf `CN=init.ess.apple.com` ← `Apple Server Authentication CA 2` ← `Apple Root CA - G3`; query serves leaf `CN=query.ess.apple.com` ← `Apple Server Authentication CA` (the older CA) ← `Apple Root CA`; neither root is served, and the unanchored chain is rejected by the system store (OpenSSL verify code 20; curl error 60 with the system bundle) — matching the Apple-root-only pool of the sources. Anchor set to embed: `Apple Root CA - G3` and `Apple Root CA` — Apple roots, distinct from the publicly-trusted Comodo/AAA root the courier chain needs ([CAP-COURIERTLS], §3.1); pin roots only, never the leaf. Both root DERs are committed as `ids/src/main/resources/imessage/AppleRootCA-G3.cer` and `AppleRootCA.cer` (G3 from Apple's live CA page; the G1 `Apple Root CA` is no longer published by Apple — sourced from the public CT log, crt.sh id 1160190, SHA-256 `B0:B1:73:0E:CB:C7:FF:45:05:14:2C:49:F1:29:5E:6E:DA:6B:CA:ED:7E:2C:68:C5:BE:91:B5:A1:10:01:F0:24`; G3 SHA-256 `63:34:3A:BF:B8:9A:6A:03:EB:B5:7E:9B:3F:5F:A7:BE:7C:4F:5C:75:6F:30:17:B3:A8:C4:88:C3:65:3E:91:79`) and each captured intermediate openssl-verifies against its root. - The Albert activation request is a form post containing an activation-info plist (activation randomness, activation state, build version, a DER certificate request, device class "MacOS", product type, product version, serial number, and a unique device ID) plus a FairPlay certificate chain and a FairPlay signature over the activation-info bytes [RP:activation.rs]. The certificate request is generated over a fresh 1024-bit RSA key with a "Client Push Certificate" common name [RP:activation.rs]; a second implementation agrees on the 1024-bit RSA push key (2048 also works) and notes a generator-version field [IG:pushcert]. The response embeds a plist whose activation record carries the device certificate (plus chain) [RP:activation.rs], [IG:pushcert]. `TODO(capture):` C8 — the exact activation-info field set and FairPlay certificate choice a genuine current client sends (one source reports signing with one of ten bundled FairPlay keys [RP:activation.rs]). — 2026-09-10: the wire half of C8 is closed by facts-only extraction (both source repos). The request is a POST (form-urlencoded) with a single form key `activation-info` whose value is an XML plist carrying `ActivationInfoComplete` (true), `ActivationInfoXML` (a nested XML plist), `FairPlayCertChain`, and `FairPlaySignature`; no timestamp is sent in the request [RP:activation.rs], [IG:activationinfo.go]. Nested activation-info keys: `ActivationRandomness` (uppercase UUIDv4), `ActivationState` ("Unactivated"), `BuildVersion`, `DeviceCertRequest` (PEM CSR), `DeviceClass`, `ProductType`, `ProductVersion`, `SerialNumber`, `UniqueDeviceID`. The CSR is 1024-bit RSA, CN "Client Push Certificate", OU "iPhone", O "Apple Inc.", signed SHA-1WithRSA. FairPlay signing covers the serialized ActivationInfo plist (SHA-1, RSA PKCS1v15) and yields the (chain, signature) pair placed into the activation body; both sources embed FairPlay identities (Go: two embedded files; Rust: ten certificate/key pairs, one chosen at random per activation — provenance unrecorded, → C57) [IG:fairplay.go], [RP:activation.rs]. The response's outer XML `` block is plist-parsed: `device-activation` → `activation-record` → `DeviceCertificate` (PEM), plus `ack-received` and `show-settings` — only the device certificate returns; the private key is client-generated and never sent, and no device token appears in the response [IG:pushcert], [RP:activation.rs]. Courier link: the DeviceCertificate (with the client private key) becomes connect field 12 (§3.3), and the connect nonce is signed with that key — Go: a 20-byte random nonce with the first byte zeroed; Rust: 0x00 ‖ big-endian unix-milliseconds ‖ 8 random bytes; signature = 0x01 0x01 ‖ RSA-SHA1(nonce) (both) [IG:connection.go], [RP:aps.rs] (nonce-shape tension with §3.3's recorded agreement — C57). - Account login requires an authentication token from Apple ID username and password. With 2FA there are two retrofitted options: legacy (2FA code appended to the password) and GrandSlam, where "anisette data" proves device identity so no repeated 2FA entry is needed; the result is a Password Equivalent Token (PET) usable in place of password+2FA [JJ]. GrandSlam login uses an SRP-6a variant (3072-bit group) [APA:client], [IG:appleid]; the Apple ID login side uses `idmsa.apple.com` endpoints with session, scnt, and client-info headers [IG:appleid]. The iCloud setup login (`setup.icloud.com`) posts a delegates map and yields per-delegate service data; the IDS delegate yields an auth token and a profile id [RP:auth.rs]. - The short-lived authentication token must be exchanged for a longer-lived authentication certificate; this certificate allows IDS registration but is not sufficient for key lookups [JJ]. @@ -248,7 +248,7 @@ - Client-side scheduling policies (second-extraction refinement): the first source reads `next-hbi` as "seconds from registration time until the client should re-register" and schedules the next re-registration at the minimum, across all users, of (registration time + `next-hbi`); when `next-hbi` is absent it falls back to seconds remaining until the identity certificate's `notAfter` minus a 300-second (5-minute) safety margin, and rechecks the wall clock every 60 seconds [RP:ids/user.rs:79-98,1171-1180], [RP:identity_manager.rs:491-525]. Failed registrations retry with exponential backoff — minimum 5 minutes, maximum 1 day, indefinitely — and a triggered refresh is a no-op if one succeeded within the last 15 seconds, with callers waiting at most 30 seconds for the running attempt [RP:identity_manager.rs:434-443], [RP:util.rs:916-917,1116-1133]. The second source recomputes its re-register ticker after every successful registration: it starts from the earliest `notAfter` among all identity certs but caps the interval at 45 days, clamps to a maximum of 1 day and a minimum of 30 minutes, then subtracts a 2-minute buffer [IG:connector.go:178-224]. It also forces re-registration whenever 45 days have elapsed since the last successful registration [IG:connector.go:301]. Its per-cert safety buffer is tiered by approximate certificate lifetime (registered-time → `notAfter`): under 8 hours → re-register when less than 3 minutes remain (fresh phone-number certs are described as short-lived); under 1 day → 30 minutes; under 7 days → 4 hours; otherwise 24 hours [IG:connector.go:317-334]. When its validation-data provider is unreachable it pauses by resetting the ticker to 1 hour [IG:connector.go:238-243]. - Forced immediate re-registration (second-extraction refinement): a push on the private IDS topic with command 32 (re-register) or 66 (handles changed — re-fetch handles, re-register if they differ) triggers it in the first source; command 34 (devices changed) only invalidates the cached identities for own handles [RP:identity_manager.rs:758-797]. A lookup returning 6005 triggers a re-register and retry, at most once per hour with a two-strike guard, in the second source [IG:lookup.go:447-471]; the first treats a 6005 from registration itself as permanent ("do not retry") pending re-login [RP:identity_manager.rs:369-380]. An APNs command 130 received from self also forces re-registration in the second source [IG:connector.go:824-841]. Disagreement: the second source's receive path explicitly ignores the private IDS topic entirely, so server-pushed commands 32/66/34 do nothing there [IG:connector.go:810-813]. A stored client-data hash differing from what the client would now send triggers immediate re-registration (first source) [RP:identity_manager.rs:400-412]. - Second-extraction correction on the "~45-day certificate" folklore: 45 days appears in the sources only as the second source's client-side cap on re-registration cadence and on time-since-last-registration; neither source records an observed server-issued certificate validity, and the first source assumes no certificate lifetime at all (the certificate's own `notAfter` is authoritative) [IG:connector.go:181,208,301], [RP:ids/user.rs:79-88]. Treat 45 days as a client policy constant, not a measured Apple certificate TTL. `TODO(capture):` C41 — real `next-hbi` values and whether Apple sends it at all, real identity-certificate `notBefore`/`notAfter` per handle type (phone vs Apple ID), real `session-token-expires-seconds`/`-refresh-seconds` values, and which renewal trigger fires in practice. An instrumented client closes this with a debug log of: the raw register response plist (top-level keys, per-user `next-hbi`, cert DER), cert validity tagged by handle type, the computed next-renewal deadline and the trigger that fired, and the lookup identity dicts' two token fields. -- **Mac-renewal arbitration (2026-09-10 research; Phase 0 go/no-go input):** renewal never needs the Mac again — every re-registration submits freshly generated validation data, produced on-device by the client's own NAC key-establishment session keyed with the cached Mac hardware identifiers [RP:ids/user.rs], [RP:macos.rs]. OpenBubbles documents Mac-based registration as renewing indefinitely with no connection to the Mac, and never requiring the Mac online after setup [OB-Renewal], [OB-FAQ]; corten-matrix likewise generates validation data locally from the hardware key extracted from the Mac once (the extraction produces an already-complete key, so no Mac round-trip is needed later). Contrast: in the iPhone-relay configuration each renewal is an HTTP call to the relay, so the iPhone must be live at every renewal [RP:relay.rs]. Client-side schedule confirmed: earliest of (registration time + `next-hbi`) or the identity-cert `notAfter` − 5 min; failure retries back off 5 min–24 h; re-registration also fires reactively on APS re-register commands and identity errors. IDS status 6005 is a server-side logout needing an Apple-ID relogin (not a Mac/validation-data problem). No public source states the actual day count — treat the ~45-day max-certificate figure as approximate. **Standing risk:** macOS 26 protected `NACKeyEstablishment` with PAC (rustpush issue #21) — an Apple-side protocol change can break local generation; watch rustpush/corten releases. — 2026-09-10: the standing risk **fired** — on macOS 26.6.3 (confirmed live in this workspace) `NACKeyEstablishment` is PAC-protected and SIGABRTs when called [RP-issue-21], and beeper's `mac-registration-provider` is archived (Apr 2026) with support only up to macOS 14.3. Minting validation data currently requires a Mac on macOS ≤ 14.3; the issue is open with no upstream fix or comments. rustpush's own fallback note stands: any genuine Mac's validation data works — the identifiers only need to be valid, not the capturing client's own hardware. — 2026-09-10 correction (OpenBubbles-flow research): the ≤ 14.3 conclusion above is **narrowed** — it binds only helpers that call the framework themselves (beeper's `mac-registration-provider`; rustpush running *on* a Mac, which is what issue #21 breaks). OpenBubbles' shipped flow never calls `NACKeyEstablishment` on the Mac at all: the helper exports identifiers only (public APIs, any macOS ≥ High Sierra including 26.x), and the client performs the generation in software (§1.1, C56). A macOS 26 Mac therefore suffices as the identifier source; the remaining C5/C6 work is client-side software (the generation procedure + Apple-Account login), not hardware access. +- **Mac-renewal arbitration (2026-09-10 research; Phase 0 go/no-go input):** renewal never needs the Mac again — every re-registration submits freshly generated validation data, produced on-device by the client's own NAC key-establishment session keyed with the cached Mac hardware identifiers [RP:ids/user.rs], [RP:macos.rs]. OpenBubbles documents Mac-based registration as renewing indefinitely with no connection to the Mac, and never requiring the Mac online after setup [OB-Renewal], [OB-FAQ]; corten-matrix likewise generates validation data locally from the hardware key extracted from the Mac once (the extraction produces an already-complete key, so no Mac round-trip is needed later). Contrast: in the iPhone-relay configuration each renewal is an HTTP call to the relay, so the iPhone must be live at every renewal [RP:relay.rs]. Client-side schedule confirmed: earliest of (registration time + `next-hbi`) or the identity-cert `notAfter` − 5 min; failure retries back off 5 min–24 h; re-registration also fires reactively on APS re-register commands and identity errors. IDS status 6005 is a server-side logout needing an Apple-ID relogin (not a Mac/validation-data problem). No public source states the actual day count — treat the ~45-day max-certificate figure as approximate. **Standing risk:** macOS 26 protected `NACKeyEstablishment` with PAC (rustpush issue #21) — an Apple-side protocol change can break local generation; watch rustpush/corten releases. — 2026-09-10: the standing risk **fired** — on macOS 26.6.3 (confirmed live on a test machine) `NACKeyEstablishment` is PAC-protected and SIGABRTs when called [RP-issue-21], and beeper's `mac-registration-provider` is archived (Apr 2026) with support only up to macOS 14.3. Minting validation data currently requires a Mac on macOS ≤ 14.3; the issue is open with no upstream fix or comments. rustpush's own fallback note stands: any genuine Mac's validation data works — the identifiers only need to be valid, not the capturing client's own hardware. — 2026-09-10 correction (OpenBubbles-flow research): the ≤ 14.3 conclusion above is **narrowed** — it binds only helpers that call the framework themselves (beeper's `mac-registration-provider`; rustpush running *on* a Mac, which is what issue #21 breaks). OpenBubbles' shipped flow never calls `NACKeyEstablishment` on the Mac at all: the helper exports identifiers only (public APIs, any macOS ≥ High Sierra including 26.x), and the client performs the generation in software (§1.1, C56). A macOS 26 Mac therefore suffices as the identifier source; the remaining C5/C6 work is client-side software (the generation procedure + Apple-Account login), not hardware access. - A status of "refresh credentials" in the register response marks that auth pair for refresh [IG:register]; error 6005 forces a full re-login [RP:identity_manager.rs] (see §2.3 for the lookup-side reading). ### 6.2 Expiry and failure modes @@ -267,7 +267,7 @@ Ordered by value. IDs match the inline `TODO(capture):` markers above. 1. **C25 — pair vs pair-ec vs PQ3 in current traffic.** The single most load-bearing question: do today's Apple clients still send "pair"/"pair-ec" envelopes, or has PQ3 replaced them entirely? Determines whether §4.2–4.3 are sufficient for receive compatibility. Second-extraction sharpening: Apple stated in 2024 that PQ3 "will fully replace the existing protocol within all supported conversations", but neither source repo has any post-2024 evidence, and one repo's fan-out code notes Apple senders shipping the same destination both with and without EC keys in one send (mixed schemes observed pre-PQ3) [IG:connector.go]. Payloads are end-to-end encrypted, so network captures can never settle this — the only path is an instrumented client. Observe: per-recipient `E` values (top-level and per-`dtl` entry) on outgoing and incoming messages; payload-less messages' `E` presence; whether current lookup results still carry the legacy/EC key sets beside the PQ3-registered Kyber-1024 + P-256 keys; the incoming validator final byte; incoming inner counters and gossip fields. -2. **C17 + C18 + C19 — APNs transport stack.** Courier hostname/count from the APNs bag (range 1–30 vs 1–50); whether the TLS handshake presents the Albert client certificate (mutual TLS) or auth happens only in the connect message; negotiated ALPN (`apns-security-v3` vs packed variants) and therefore legacy vs packed frame format. — 2026-09-10: `courier2.push.apple.com` observed live, 5223↔443 both used; vanilla-TLS interception rejected (`bad extension`) — Apple-private TLS negotiation confirmed (see §3.1). 2026-09-10 pcap mining: live session is TLS 1.3 (server picks 0x0304 / TLS_AES_256_GCM_SHA384) with Apple-private version 0xfafa, group 0x9a9a, extensions 0x5A5A/0x1A1A, and the X25519+ML-KEM-768 hybrid group offered; client offers both ALPNs, server selects none → legacy frame format assumed. C18 is unobservable by passive capture under TLS 1.3 — needs client-side evidence. — 2026-09-10 [CAP-APNSBAG]: the APNs bag was fetched directly (`GET http://init-p01st.push.apple.com/bag`, unauthenticated plain HTTP; archived `reference/imessage-captures/apns-bag-2026-09-10.xml`): `APNSCourierHostname: courier.push.apple.com`, `APNSCourierHostcount: 50` — the 1–50 range is confirmed (`courier2` is one host within it); `APNSAllowTLS1_3: 1` is consistent with the observed TLS 1.3 session. C17's hostname/count half is closed; C18 still needs client-side evidence. [CAP-COURIER] +2. **C17 + C18 + C19 — APNs transport stack.** Courier hostname/count from the APNs bag (range 1–30 vs 1–50); whether the TLS handshake presents the Albert client certificate (mutual TLS) or auth happens only in the connect message; negotiated ALPN (`apns-security-v3` vs packed variants) and therefore legacy vs packed frame format. — 2026-09-10: `courier2.push.apple.com` observed live, 5223↔443 both used; vanilla-TLS interception rejected (`bad extension`) — Apple-private TLS negotiation confirmed (see §3.1). 2026-09-10 pcap mining: live session is TLS 1.3 (server picks 0x0304 / TLS_AES_256_GCM_SHA384) with Apple-private version 0xfafa, group 0x9a9a, extensions 0x5A5A/0x1A1A, and the X25519+ML-KEM-768 hybrid group offered; client offers both ALPNs, server selects none → legacy frame format assumed. C18 is unobservable by passive capture under TLS 1.3 — needs client-side evidence. — 2026-09-10 [CAP-APNSBAG]: the APNs bag was fetched directly (`GET http://init-p01st.push.apple.com/bag`, unauthenticated plain HTTP; archived on file as `apns-bag-2026-09-10.xml`): `APNSCourierHostname: courier.push.apple.com`, `APNSCourierHostcount: 50` — the 1–50 range is confirmed (`courier2` is one host within it); `APNSAllowTLS1_3: 1` is consistent with the observed TLS 1.3 session. C17's hostname/count half is closed; C18 still needs client-side evidence. [CAP-COURIER] 3. **C1 + C3 — registration request shape.** Second-extraction state: the bag fetch URL, nested-plist shape (`bag` key → embedded plist), one-per-process caching, and both clients' full endpoint sets are now known (§1.2) — rustpush resolves 11 bag keys at runtime with no fallbacks [RP]; the other pins everything and never fetches. What remains needs an instrumented client (pinned hosts bypass every proxy tier tried): the **live** bag contents (all `id-*` → URL mappings as served today), the current sub-service list and Messenger capability flags (17 vs 1), the capability-boolean set the server expects, and whether the server accepts both gzip-XML and plain-XML register bodies. — 2026-09-10: the bag contents are now observed directly — both bags fetched unauthenticated, no drift in the pinned URLs ([CAP-BAG]/[CAP-APNSBAG], §1.2). What remains C1/C3-bound to an instrumented client: the current sub-service list and Messenger capability flags (17 vs 1), the capability-boolean set the server expects, and whether the server accepts both gzip-XML and plain-XML register bodies — these live in the register request/response pair, which does require authentication (and Apple throttles abusive registrations, so no blind probing). C2 (protocol-version: "1640" vs "1660") and C4 (signature construction — structure now source-agreed, §1.4) also resolve from the same instrumented-client capture. 4. **C32 + C33 + C34 — semantic wire forms.** Tapback action codes (2006/2007/4000/1000/2 extensions) and target-string formats ("bp:" vs raw GUID vs "p:"-variants); typing start/stop encoding (empty payload vs body-field stop); whether command 102 carries a body (timestamp + read-up-to UUID) or none. 5. **C41 + C10 — lifetimes.** Real `next-hbi` values (and whether Apple sends it at all — one source parses it, the other doesn't read it), real identity-certificate lifetimes per handle type (the "~45 days" figure is now known to be a client-side cap, not an observed cert TTL — §6.1), real `session-token-expires-seconds`/`-refresh-seconds` values (one source consults them, the other ignores them), the effective renewal cadence, and which renewal trigger fires in practice (timer, private-IDS push 32/66, 6005, handle change). All reachable only via an instrumented client's debug log — the exact logging list is in §6.1. @@ -323,6 +323,33 @@ Ordered by value. IDs match the inline `TODO(capture):` markers above. All six live as parameters/strategies in the code; capture or live-attempt results collapse them. - 2026-09-11 (first live step-1 attempt, rev 18): the DEFAULT hypothesis set (P-256 ECDH, HKDF-SHA256 empty salt/info, ECDSA-P256-SHA256, TLV v1, RAW_PUBLIC_POINT, `text/x-xml-plist`) was rejected at STEP 2 — the bag fetch and step-1 cert GET both succeeded (HTTP 200, TLS pinned to the embedded Apple roots, cert parsed), and the step-2 POST returned HTTP 200 with a plist dict of exactly two keys: `status` (integer **5003**) and `message` (a server trace string of the form `SED:3:VEN-PROD:::::<64-hex>`; exact values not recorded), with NO `session-info` key. The exchange died before key establishment, so this data point does NOT yet arbitrate the crypto hypotheses — it shows the step-2 request itself was refused. The meaning of SED status 5003 is unrecorded: `TODO(capture)` the SED error-status table (candidate refusal causes: the body shape under a wrong C60 hypothesis, the C56 content-type, or a required-but-unrecorded step-2 header). - 2026-09-11 (probe matrix, same session): FIVE step-2 attempts returned the byte-identical 498-byte error shape (`status` 5003): default set; KA=X25519; sig=HMAC-SHA256; content-type `application/x-www-form-urlencoded`; content-type `application/octet-stream`. The refusal is INVARIANT under every flipped variable — the server rejects something common to all attempts. Eliminated as causes: the key-agreement scheme, the signature scheme, the step-2 content-type. Remaining suspects, ranked: (1) the invented TLV v1 framing itself (tags/lengths wrong → body unparsable → generic error), (2) the single-key plist wrapper's encoding (XML `` vs base64-string value form), (3) a required-but-unrecorded step-2 header (protocol drift since the 2024-era recordings, e.g. an anisette set now enforced), (4) the exported Mac identifiers being stale/invalid server-side. Next lever is a wire capture of a genuine client performing the exchange, not more blind flips. - - 2026-09-11 (rev 19, step-2 self-check round): the reconstruction was audited offline against the extracted client's own request construction and the archived genuine captures; the audit MOVES the ranking rather than adding a hypothesis. (a) Wrapper — CLOSED as a suspect: the extracted client serializes the `session-info-request` body with the plist XML writer (the same ``-base64 XML form this implementation emits), so suspect 2 is retired. (b) Content-type — CORRECTED, not just probed: the extracted client sets NO content-type on the step-2 POST (only `Accept-Language` rides the client's default headers), so `text/x-xml-plist` was an unsupported inference carried in from the §1.5 GSA posts and the probe matrix's three content-types were all non-faithful; the code's constant is now absent (§1.1). (c) Invariance — CORRECTED: the five probe attempts did NOT send identical bodies (each mints fresh key-agreement key material, the X25519 and P-256 variants differ in that field's width, and the HMAC variant drops the signing-key field), so "byte-identical response" shows the refusal is independent of the body's CONTENT — which retires the key-agreement and signature *values* as variables at this step (step 2 precedes key establishment entirely) and leaves the body's FRAMING (the invented TLV v1 layout) and something outside the body as the surviving explanations. (d) Headers — WEAKENED as a suspect but not retired: the archived 2026-09-10 capture of a genuine device shows the validation endpoint family reached with the full anisette + BAA header set (`X-Apple-I-MD`, `X-Apple-I-MD-M`, `X-Apple-I-MD-RINFO`, `X-Apple-I-Client-Time`, `X-Apple-I-TimeZone`, `X-Apple-I-Locale`, `x-apple-i-device-type`, the `x-apple-baa-*` set) under an `invitation-registration` user-agent, while this implementation posts body-only; but that captured request is the CDN-served static step-1 cert, which is exactly why it succeeds header-less here, so the capture neither requires nor acquits a header set at step 2. No archived capture contains the step-2 POST (every `.mitm` and every plist in `reference/imessage-captures/` was searched for the request's key). Next levers, in order: (1) live — one faithful step-2 POST (no content-type) plus ONE deliberate control POST whose body is not the session-info-request plist, to learn whether the endpoint examines the body at all (control identical to the 5003 shape ⇒ the refusal precedes the body, so framing is not the cause; control different ⇒ the body is parsed, so framing is the cause); (2) a genuine-client wire capture of the exchange, still the only way to READ the real framing — the Mac-side client performs it in software from exported identifiers, so the capture host is whatever machine runs a client that completes the exchange (the identifier-export helper alone never performs it). + - 2026-09-11 (rev 19, step-2 self-check round): the reconstruction was audited offline against the extracted client's own request construction and the archived genuine captures; the audit MOVES the ranking rather than adding a hypothesis. (a) Wrapper — CLOSED as a suspect: the extracted client serializes the `session-info-request` body with the plist XML writer (the same ``-base64 XML form this implementation emits), so suspect 2 is retired. (b) Content-type — CORRECTED, not just probed: the extracted client sets NO content-type on the step-2 POST (only `Accept-Language` rides the client's default headers), so `text/x-xml-plist` was an unsupported inference carried in from the §1.5 GSA posts and the probe matrix's three content-types were all non-faithful; the code's constant is now absent (§1.1). (c) Invariance — CORRECTED: the five probe attempts did NOT send identical bodies (each mints fresh key-agreement key material, the X25519 and P-256 variants differ in that field's width, and the HMAC variant drops the signing-key field), so "byte-identical response" shows the refusal is independent of the body's CONTENT — which retires the key-agreement and signature *values* as variables at this step (step 2 precedes key establishment entirely) and leaves the body's FRAMING (the invented TLV v1 layout) and something outside the body as the surviving explanations. (d) Headers — WEAKENED as a suspect but not retired: the archived 2026-09-10 capture of a genuine device shows the validation endpoint family reached with the full anisette + BAA header set (`X-Apple-I-MD`, `X-Apple-I-MD-M`, `X-Apple-I-MD-RINFO`, `X-Apple-I-Client-Time`, `X-Apple-I-TimeZone`, `X-Apple-I-Locale`, `x-apple-i-device-type`, the `x-apple-baa-*` set) under an `invitation-registration` user-agent, while this implementation posts body-only; but that captured request is the CDN-served static step-1 cert, which is exactly why it succeeds header-less here, so the capture neither requires nor acquits a header set at step 2. No archived capture contains the step-2 POST (every capture on file was searched for the request's key). Next levers, in order: (1) live — one faithful step-2 POST (no content-type) plus ONE deliberate control POST whose body is not the session-info-request plist, to learn whether the endpoint examines the body at all (control identical to the 5003 shape ⇒ the refusal precedes the body, so framing is not the cause; control different ⇒ the body is parsed, so framing is the cause); (2) a genuine-client wire capture of the exchange, still the only way to READ the real framing — the Mac-side client performs it in software from exported identifiers, so the capture host is whatever machine runs a client that completes the exchange (the identifier-export helper alone never performs it). - 2026-09-11 (rev 20, live control experiment): the BODY half of this item is CLOSED. A deliberate control — 20 bytes that are not a plist at all, POSTed to the same live step-2 URL with the same (absent) content-type — was answered exactly like the reconstruction: HTTP 200, 498 bytes, `status` 5003, same nine token classes. The live step-2 URL the bag serves is `https://identity.ess.apple.com/WebObjects/TDIdentityService.woa/wa/initializeValidation`. Consequences: (1) the invented TLV framing, the wrapper encoding, and the step-2 content-type are all retired as causes — the endpoint refuses WITHOUT examining the body, so no further body flip can arbitrate anything; (2) the rev-18 "byte-identical under five variants" reading is corrected: the trace carries a live per-request epoch (the two rev-20 runs' epochs sit 28 s apart, matching the gap between them) and three per-request uuids, so those responses were shape-identical, not byte-identical — and the rev-19 inference built on that reading is superseded by this experiment; (3) the surviving suspect class is the request ENVELOPE: the header set a genuine client carries (the anisette + BAA set, §1.1/C6) or an identity/attestation the endpoint resolves before parsing; (4) next levers, ranked: a one-call envelope probe with a plausible client header set (a UA, `X-Apple-I-Client-Time`/`TimeZone`/`Locale`, `X-Mme-Device-Id` — none of which needs the OTP derivation that is still missing), then the genuine-client wire capture, which is now the only way to see the real envelope. Instrumentation note (dev-only, uncommitted): the live runner dumps the exact request bytes mode-0600 beside the blob and classifies the server trace token-by-token (shape plus boolean match against our own digests/identifiers), so every attempt is post-mortem-analyzable without spending another call. - - 2026-09-11 (rev 21, offline re-read + cross-checks — this SUPERSEDES the rev-20 conclusion that the body half is closed): (a) the rev-20 experiment cannot prove the endpoint ignores the body. Both requests it compared were invalid — the 20-byte control is not a plist at all, and our reconstruction's `session-info-request` is the invented TLV v1 blob, emitted in place of the closed-source library's real output — so "one generic 5003 for both" is equally consistent with "the server parses each, rejects each, and reports a single generic code". Retiring the TLV framing is therefore withdrawn: framing is live again, and it is now the leading suspect, because the reference client's envelope is body-only (below). Content-type and the step-2 crypto *values* stay retired (they were flipped and changed nothing). (b) 5003 = `IDSStatusBadRequest` in the IDS status space [IG:imessage/direct/ids/types/idserrors.gen.go]; that family has dedicated body-level codes (`ParseFailure` 1000, `InvalidField` 1001, `DecodeFailureBase64`/`Hex` 1004/1005, `MissingRequiredKey` 5008) and none came back — weak evidence for a pre-body rejection, equally explicable by this endpoint simply not using the fine-grained codes; not decisive either way. (c) The model client is current and still body-only: `reference/rustpush` IS the OpenBubbles fork (`origin` github.com/OpenBubbles/rustpush), re-fetched 2026-09-11 with a tip identical to the staged snapshot; its step-2 call remains `REQWEST.post(bag_url).body(xml_plist)`, no content-type, `Accept-Language: en-US,en;q=0.9` the sole default header (`src/macos.rs`, `src/util.rs`). If that client completes the exchange today, headers are not the cause — and the two envelopes then differ only at the HTTP layer (client stack, protocol version, header casing). (d) Route confirmed against a genuine device: the archived 2026-09-10 capture's `id-initialize-validation` bag value is byte-identical to the URL the live run posted to. (e) The genuine bag enables `absinthe` attestation on this endpoint family (`absinthe-baa-option-enabled`, `-ios13`, `absinthe-v3-enabled` all true) — the one mechanism that could still demand an envelope this project cannot currently produce. (f) SED trace grammar from the two recorded responses: `SED:3:VEN-PROD:::::<64-hex>:EM`; every uuid and the epoch differ between the runs (the epochs 28 s apart, matching the calls) and the 64-hex differs too and matches no digest of the recorded body, the empty string, or the step-2 URL — per-request state, not a hash of our input. (g) Ranked next levers: the genuine-client capture, now valued primarily for the real `session-info-request` BLOB (plus, in the same exchange, the real step-3 validation blob) rather than for the header set; then, only if the capture shows a richer envelope, the envelope probe. The capture needs a client that *performs* the exchange (an OpenBubbles app or a rustpush-based bridge — the macOS identifier-export helper never does); one is reported available. + - 2026-09-11 (rev 21, offline re-read + cross-checks — this SUPERSEDES the rev-20 conclusion that the body half is closed): (a) the rev-20 experiment cannot prove the endpoint ignores the body. Both requests it compared were invalid — the 20-byte control is not a plist at all, and our reconstruction's `session-info-request` is the invented TLV v1 blob, emitted in place of the closed-source library's real output — so "one generic 5003 for both" is equally consistent with "the server parses each, rejects each, and reports a single generic code". Retiring the TLV framing is therefore withdrawn: framing is live again, and it is now the leading suspect, because the reference client's envelope is body-only (below). Content-type and the step-2 crypto *values* stay retired (they were flipped and changed nothing). (b) 5003 = `IDSStatusBadRequest` in the IDS status space [IG:imessage/direct/ids/types/idserrors.gen.go]; that family has dedicated body-level codes (`ParseFailure` 1000, `InvalidField` 1001, `DecodeFailureBase64`/`Hex` 1004/1005, `MissingRequiredKey` 5008) and none came back — weak evidence for a pre-body rejection, equally explicable by this endpoint simply not using the fine-grained codes; not decisive either way. (c) The model client is current and still body-only: the rustpush reference IS the OpenBubbles fork (`origin` github.com/OpenBubbles/rustpush), re-fetched 2026-09-11, unchanged from the snapshot on file; its step-2 call remains `REQWEST.post(bag_url).body(xml_plist)`, no content-type, `Accept-Language: en-US,en;q=0.9` the sole default header (`src/macos.rs`, `src/util.rs`). If that client completes the exchange today, headers are not the cause — and the two envelopes then differ only at the HTTP layer (client stack, protocol version, header casing). (d) Route confirmed against a genuine device: the archived 2026-09-10 capture's `id-initialize-validation` bag value is byte-identical to the URL the live run posted to. (e) The genuine bag enables `absinthe` attestation on this endpoint family (`absinthe-baa-option-enabled`, `-ios13`, `absinthe-v3-enabled` all true) — the one mechanism that could still demand an envelope this project cannot currently produce. (f) SED trace grammar from the two recorded responses: `SED:3:VEN-PROD:::::<64-hex>:EM`; every uuid and the epoch differ between the runs (the epochs 28 s apart, matching the calls) and the 64-hex differs too and matches no digest of the recorded body, the empty string, or the step-2 URL — per-request state, not a hash of our input. (g) Ranked next levers: the genuine-client capture, now valued primarily for the real `session-info-request` BLOB (plus, in the same exchange, the real step-3 validation blob) rather than for the header set; then, only if the capture shows a richer envelope, the envelope probe. The capture needs a client that *performs* the exchange (an OpenBubbles app or a rustpush-based bridge — the macOS identifier-export helper never does); one is reported available. + + +## Revision history + +- Rev 24 (2026-09-28, first live probes of this implementation — two unauthenticated bag GETs + a no-client-cert courier TLS handshake, runner `:engine:runProbe`; nothing else touched): (1) C18 SETTLED — the courier demands the Albert client certificate via a fatal TLS 1.3 `certificate_required` alert after a successful handshake, on both ports (§3.1); (2) the bags are TYPED CONFIG DICTIONARIES, not URL maps — the live IDS bag carries ints/bools/floats/string-lists alongside 400+ URLs and a `bag-expiry-timestamp` (~17-day observed TTL; 403 entries live vs 405 on 2026-09-10, two new push-debounce keys in, four A/B-test keys out); the APNs bag is 65 stable entries incl. BAA-authentication flags (`APNSEnableBAAAuthenticationForDevices` etc. — C17/C20-relevant); (3) AppleTrust's embedded-root pinning validates the live chains of `init.ess.apple.com` and `courier.push.apple.com` (no root served — chain anchored at the embedded root); (4) rev-23's container-fact follow-ups: sort-version 0 and the ref-width rule are now capture/probe-consistent. +- Rev 23 (2026-09-27, writer-pass follow-up, [CF] public material + host-decoder empirical cross-check only — no restricted source): three §4 container-format gaps found by the first writer session are closed — integer signedness (1/2/4-byte forms unsigned; the 8-byte form is two's-complement signed; non-negative values take the minimal width, negative values always the 8-byte form), the object-reference width rule (minimal width whose 2^(8w) ≥ object count, so every reference 0..count−1 is representable), and the sort-version byte (written as 0). +- Rev 22 (2026-09-12, offline byte-audit of the saved live step-2 request — no network call, nothing committed): the CLIENT-SIDE ENCODING class is closed. The exact 3898 bytes the live runner sent (§7 item 24) parse as an XML plist with the single `session-info-request` `` key, and its 2729-byte payload walks the invented TLV v1 to the last byte with ZERO slack, 17 distinct tags, no duplicates and no trailing bytes — our bytes are exactly what the code intends, so no writer bug hides there (the framing *intent* stays invented and unverified). By-product fact: the step-1 `cert` data field is NOT a bare DER cert — it is Apple's own container, `01 02` followed by two `(4-byte BE length, DER cert)` pairs (1046 and 1329 bytes; 2+4+1046+4+1329 = 2385 exactly), carried through verbatim by this implementation (§1.1) and cross-checking against the archived genuine step-1 response size. Apple's length convention there is 4-BYTE big-endian where our TLV v1 uses 2 — the only Apple framing ground truth in hand, and it weakens TLV v1 further. +- Rev 21 (2026-09-11, step-2 refusal re-read + offline cross-checks — no new live calls): one correction and five facts. CORRECTION — the rev-20 "BODY-INDEPENDENT" conclusion is withdrawn as unproven: a 20-byte non-plist body and our 3898-byte reconstruction returning the same generic `status` 5003 establishes only that 5003 is this endpoint's generic request-level failure; it cannot separate "the body is never parsed" from "both bodies are parsed and both rejected identically", because the reconstruction is itself unverified — the `session-info-request` blob is minted by the closed-source library (§1.1 [OA]). Blob FRAMING is therefore live again beside the envelope. FACTS — (1) 5003 is named in the IDS status space: `IDSStatusBadRequest` [IG:imessage/direct/ids/types/idserrors.gen.go], a table auto-generated from web FaceTime's JavaScript, so it is the IDS family's space and not proven for this WebObjects service; that same table shows what a body-level rejection in this family looks like (`ParseFailure` 1000, `InvalidField` 1001, `DecodeFailureBase64`/`Hex` 1004/1005, `MissingRequiredKey` 5008) and the endpoint returned none of them. (2) the rustpush reference IS the OpenBubbles fork (github.com/OpenBubbles/rustpush), re-fetched 2026-09-11 and unchanged from the snapshot on file, and its step-2 call is still `REQWEST.post(bag_url).body(xml_plist)` with no content-type and one default header `Accept-Language: en-US,en;q=0.9` — a current model client's envelope is body-only, which makes the header half the weakest suspect and the blob the strongest. (3) The route is confirmed against a GENUINE device: the archived 2026-09-10 capture's `id-initialize-validation` bag value is byte-identical to the URL the live run posted to. (4) That genuine bag enables attestation on this endpoint family — `absinthe-baa-option-enabled`/`-ios13` and `absinthe-v3-enabled` all true, `absinthe-baa-cert-ttl-minutes` 144000 — the one mechanism that could still put the envelope back in front. (5) Both SED traces are now recorded as a grammar: `SED:3:VEN-PROD:::::<64-hex>:EM`; between the two runs every uuid and the epoch differ (the epochs 28 s apart, matching the calls' spacing) and the 64-hex differs, so it is per-request, not a digest of any recorded input (checked against the body, the empty string and the step-2 URL). Next lever unchanged: the genuine-client capture — valued now for the BLOB it reveals, not the header set. +- Rev 20 (2026-09-11, live step-2 control experiment): the step-2 refusal is BODY-INDEPENDENT — a 20-byte body that is not a plist at all and the full reconstructed 3898-byte request are answered identically (HTTP 200, the same 498-byte `status` 5003 plus a nine-token SED trace; only its per-request epoch and uuids differ between the two runs). The invented TLV framing, the wrapper encoding, and the step-2 content-type are therefore ALL retired as causes at this endpoint: nothing in the body is examined before the refusal. The rev-18 "byte-identical" reading is corrected — the trace carries a live per-request epoch (two runs 28 s apart) and three per-request uuids, so the responses were shape-identical, and the rev-19 inference chain from that reading is superseded by this direct experiment. Surviving suspect class: the request ENVELOPE (the header set a genuine client carries, or an identity the endpoint resolves before parsing); the live step-2 URL is now recorded (§1.1, §7 item 24). +- Rev 19 (2026-09-11, step-2 self-check round): the step-2 request reconstruction was audited against the extracted client's own request construction (§7 item 24) — the XML-plist wrapper is confirmed, while our `text/x-xml-plist` default turned out to be an unsupported inference: the recorded client sets NO content-type on the step-2 POST (§1.1), so all three content-types of the rev-18 probe matrix were non-faithful (the constant is now absent in the code); the rev-18 "invariant under every flipped variable" claim is corrected to a *body-content* independence (the five attempts did NOT send identical bodies — fresh key material each time), which retires the step-2 crypto variables and leaves the invented TLV framing as the prime suspect; and an archived genuine-device capture shows the validation endpoint family reached with the full anisette/BAA header set while step 1 succeeds header-less because that URL is a CDN static object (§1.1) — next levers: one faithful (no content-type) step-2 POST beside a deliberate control POST, then a genuine-client capture. +- Rev 18 (2026-09-11, first live step-1 attempt + probe matrix): the §1.1 validation exchange ran against the live Apple endpoints for the first time — bag + step-1 cert GET OK (pinned TLS, HTTP 200); FIVE step-2 POST variants (default C60 set, X25519, HMAC, three content-types) all answered HTTP 200 with the same 498-byte error plist (`status` 5003, SED trace message) and no `session-info` (§1.1, §7 item 24) — the refusal is invariant under every flipped variable, so the C60 crypto hypotheses remain open and the next lever is a genuine-client wire capture; the SED status table is a new TODO(capture). +- Rev 17 (2026-09-11, FairPlay material metadata extraction): the provenance half of the FairPlay identity question is closed (§1.2, §7 item 21) — all known embedded copies across the reference repos are ONE 1024-bit RSA identity (leaf cert expired 2024, yet accepted), descending from a 2023 iCloud-bypass community release; rustpush's "ten pairs" are ten copies; no own-Mac extraction path is documented anywhere (open research item); BYO-identity is recorded as the sane architecture. +- Rev 16 (2026-09-11, activation implementation round): the three remaining GSA header literals are pinned (§1.5 — browser UA, non-akd `X-MMe-Client-Info`, the full-wrapper `X-Apple-I-CFU-State` plist, the bracketed sign-in client-info); the validation-data exchange is implemented — steps 1–2 concrete, step 3 as an explicit contract reconstruction with six named hypotheses (key agreement, KDF, signature, session-info interpretation, blob framing, step-2 content-type) collected as new **C60** (§7 item 24) and arbitrable only by one live activation; the Mac-Hardware-Info export parser and the C59 constants are wired into code. +- Rev 15 (2026-09-11, second activation extraction round — C59 + C58 closed): the GSA-login residue is recorded (§1.5 closure paragraph: RFC 5054 2048-bit SRP group with g=2, the full 2FA browser header set with literal values, per-flavor cpd values, akd UA/`X-MMe-Client-Info` forms, `spd` `t`-map inner keys + top-level `GsIdmsToken` + `fn`/`ln`, the `gsas.apple.com` postdata/teardown host split, the sign-in UA/timezone forms); C58 is closed — the Mac-Hardware-Info export-code format is fully recorded in §1.1 (`OABS` magic + flag byte + protobuf field map; plain-base64 or OpenSSL-`Salted__` AES-256-CBC delivery; sharing mechanics server-side; upstream repo unlicensed — facts only). Citation legend gains [MHG]. +- Rev 14 (2026-09-11, activation extraction round — facts-only over the anisette/GSA crates and the Mac client, facts-only extraction): C6's value-format and provisioning halves are closed (§1.1 — per-request anisette value forms, the ADI provisioning flow with `spim`/`cpim`/`ptm`/`tk`, the 60-byte Apple-issued `mid`, the 16-byte local `keychain_identifier`, machine-state persistence); new §1.5 records the full GSA login chain end-to-end (SRP-6a 2048-bit with the GSA empty-username identity-hash quirk, `s2k`/`s2k_fo`, `spd` decryption, both 2FA variants + the Circle PAKE, PET semantics, delegate logins yielding the IDS `authToken`/`profileId`, the `X-Mme-Nas-Qualify` sign-in POST); the five obfuscation labels are mapped to their fields (§1.1); C56 step-3 and the OTP derivation are CONFIRMED closed at source (upstream ships stubs — contracts only). +- Rev 13 (2026-09-11, implementer round — rev 12's closed fact sets wired into code; no new protocol facts): courier command builders + connect/auth + the engine's courier frame handler are concrete (§3.3, C45); lookups ride the APNs tunnel with the C47/C48 wire names, plain-string `uris`, and C55 top-level-status routing (§2.1); register/authenticate bodies and the Albert activation wire half are concrete (C53/C54/C8, §1.2/§1.4); envelope fixes landed (HKDF salt `"LastPawn-MessageKeys"` with empty info, pair HMAC keying, recorded pair-ec field numbers, dtl array-of-dicts regression-tested, §4). Nothing closed was re-opened. C57 (§7 item 21) gains six writer-pass gaps: filter multi-topic encoding, connect field-0x10 position, connect-response 5/6 shapes, tunnel 96/97 field ids + payload framing/encoding + status table, phone-flow authentication-data value encodings, Albert request/response value types. +- Rev 12 (2026-09-10, facts-only extraction round over both source repos plus the OpenAbsinthe-Stub mock): closed C45 (courier field-id tables + length-prefix scope, §3.2–3.3), C47/C48 (lookup headers/keys, §2.1), C49/C51/C52 and C50's protobuf half (§4), C53 (register wire names, §1.4), C54 (authenticate exchange, §1.2), C8's wire half (Albert activation, §1.2), and C56's steps 1–2 with the GSA login + full anisette set (§1.1). Corrected: the HKDF "info string" is the salt (§4.3); `dtl` is an array of dicts; payload command 160 is not a frame command (§3.3, §5.1). Narrowed: C55 — both clients read lookup error statuses from the top-level `status` only (§2.1, §7 item 19). New first-class architecture fact: IDS lookups ride the APNs tunnel, never plain HTTPS (§2.1). New **C57** (§7 item 21) collects extraction disagreements and closed-source residue. Legend gains [OA]. +- Rev 11 (2026-09-10, OpenBubbles-flow research — C5/C6 model corrected): the Mac's role in activation is **identifier export only** — the shipped OpenBubbles helper (`Mac-Hardware-Info` 1.2.0, notarized) reads serial/board-ID/model via public IORegistry/sysctl APIs on any macOS ≥ High Sierra *including 26.x*, and never calls an Apple private framework (§1.1, §6.1). The prior "minting requires a Mac on macOS ≤ 14.3" conclusion bound only framework-calling helpers (beeper's archived `mac-registration-provider`; rustpush running *on* a Mac); the remaining C5/C6 work is client-side software — the generation procedure, recorded as new **C56** (§7 item 20). Legend gains [OB-MHI]. +- Rev 10 (2026-09-10, Phase 2 lookup-orchestration component): added C55 (§2.1) — the lookup response's error-status locations: whether 5206 ("response too large"), 6005, and the send-side 5040/5041 session-token codes appear as a top-level response status, per-handle result statuses, or both; the lookup flow reads per-handle result statuses only, the one numeric-status location the fact set records. +- Rev 9 (2026-09-10, Phase 2 component 5): registered the registration wire-name gaps the component's writer pass hit — C53 (§1.4: register request/response key names beyond the recorded set, and the `x-auth-user-id-` value form) and C54 (§1.2: the authenticate request/response pair — body wire keys, CSR encoding, CN encoding, self-signature algorithm, response keys) — plus §7 item 18. The activation code takes every unrecorded name as caller-supplied configuration and keeps every unrecorded response field in a raw map. +- Rev 8 (2026-09-10, Phase 2 component 4): [CAP-IDSTLS] (§1.2) fixed the IDS TLS anchor set — `Apple Root CA - G3` and `Apple Root CA`, roots not served, system store rejects the unanchored chain — and [CAP-BAGCT] (§1.2, §7 item 16) closed C47's encoding half: both bag responses and the embedded bag payload are XML plists on the wire, content-type labels notwithstanding. The component-4 writer pass added TODO(capture) C49–C52 and §7 item 17 (unrecorded plist/body value types, the §4.2 HMAC key + ECDSA wire encoding, §4.3 protobuf field numbers beyond validator=99 and the HKDF salt). +- Rev 7 (2026-09-10, Phase 2 component 3): recorded the IDS lookup/bag wire details the phase-1 fact set never captured — endpoint plist encodings and the unrecorded lookup header/body/response key names (C47/C48, §1.2, §2.1, §7 item 16). The writer phase guesses none of them: they are configuration in the code until a capture closes them. +- Rev 6 (2026-09-10, Phase 2 component 2): added [CAP-COURIERTLS] chain ground truth to §3.1 and the C45 framing-gaps TODO (§3.2, §7 item 15). +- Rev 5 (2026-09-10, Phase-2 start): added the publicly documented binary-plist container format to §4.1 ([CF]) so the clean-room codec can be written from the spec alone. +- Revision 2 merged three phase-1 extractions plus first capture ground truth (own mitmproxy captures of a registered iPhone 14,4 running iOS 26.1, 2026-09-10 — still the only post-PQ3 evidence here). +- Revision 3 (2026-09-10) is a second targeted extraction round over both source repos on the three highest-value gaps (lifetimes C41/C10, envelope selection C25, bag/registration C1/C3); it corrects two rev-2 misreadings (the HTTP signature construction is agreed, not disagreed, §1.4; the pair-ec validator final byte is agreed 0x0c, not "0xc0-ish", §4.3) and re-reads the ~45-day certificate figure as a client-side cap (§6.1). +- Revision 4 (2026-09-10) is the phase-1 review-gate pass: removed one leaked source identifier (`registered_at`, rewritten as prose), paraphrased the only reproduced source fragment, added citation-form legend entries and the protocol-constant provenance notes; no copied expression, code, or structural transcription found. \ No newline at end of file