From ca057376ee7fa60b3d96bb909a3842f7fb0c793c Mon Sep 17 00:00:00 2001 From: fenleon Date: Sat, 10 Oct 2026 08:06:02 +0200 Subject: [PATCH] C56-a request-blob field map ~85% pinned: OAEP(pearKey16+hmacKey20) + HMAC-SHA1 tail verified (rev 34) --- README.md | 2 +- docs/imessage-protocol-spec.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1b36c69..204aad9 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ 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 chain is reimplemented natively, no emulation or helper binary: the white-box OTP ciphers, the GSA provisioning exchange, the full spim/cpim/ptm wire framing, and the validation-stamp ("NAC") mint — pear white-box cipher, blob assembly, and the 16-byte OTS signature (`NacSign`) — are transcribed clean-room from analysis of a compiled ADI implementation, verified byte-exact against oracle vectors and live captures (three genuine provisioning runs plus a validation-stamp capture under an instrumented debugger). Every byte group of the exchange and of the 517-byte stamp is identified and reproduced; the full stamp computes byte-exact in Kotlin from its inputs (the earlier python reference model is retired to dev capture notes; the Kotlin port is pinned by synthetic-vector tests against it). 208 tests green, spec rev 31. A minimal public facade (`NacMint`) exposes the mint for consumers; the remaining gap to a live stamp is five wire-input mappings from the validation exchange, each named in the spec (§1.1 C56). 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, the full spim/cpim/ptm wire framing, and the validation-stamp ("NAC") mint — pear white-box cipher, blob assembly, and the 16-byte OTS signature (`NacSign`) — are transcribed clean-room from analysis of a compiled ADI implementation, verified byte-exact against oracle vectors and live captures (three genuine provisioning runs plus validation-stamp captures under an instrumented debugger). Every byte group of the exchange and of the 517-byte stamp is identified and reproduced; the full stamp computes byte-exact in Kotlin from its inputs (the earlier python reference model is retired to dev capture notes; the Kotlin port is pinned by synthetic-vector tests against it). 209 tests green, spec rev 34. A minimal public facade (`NacMint`) exposes the mint for consumers. Of the five wire-input mappings from the validation exchange (§1.1 C56), four are live-pinned (pear key, session blob, RNG pads, cert tail); the last gate is the step-2 request blob's field map (C56-a) — ~85% pinned (RSA-OAEP of the pear key + an HMAC key, an HMAC-SHA1 tail, one 32-B pear-AES field and one 20-B slot remain), with the two unpinned sub-fields named in the spec. No anisette server, no Unicorn, no Mac relay. Not validated yet: diff --git a/docs/imessage-protocol-spec.md b/docs/imessage-protocol-spec.md index ed1a6cf..1439a0f 100644 --- a/docs/imessage-protocol-spec.md +++ b/docs/imessage-protocol-spec.md @@ -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 pure. `gen = unix_seconds / 30`; dispatch by OTP flavor byte: 1 (IOS) → `gen_otp` (0x17fba60), 0 (Mac) → `gen_otp_ios` (0x1801d90) — the binary's naming is inverted relative to the flavor (a distinct numbering from the §1.1 session flavor `Mac`=1/`IOS`=3). Cipher inputs: `one` = the machine's `client_info` (required ≥ 0x190 B IOS / ≥ 0xd0 B Mac, length-checked) and `two` = the 32-byte `mid`; output 16 bytes. The wire `X-Apple-I-MD` frame is 28 B: `u32be(5)` ‖ `u32be(16)` ‖ sig16 ‖ `u32be(flavor)`. The 2FA code is `SHA-1(sig16 ‖ secret60) mod 1_000_000` over the machine's full 60-byte secret slice (`machine[0x38..0x74]`; two standard SHA-1 blocks — an earlier report's "48-byte" reading is wrong, 0x18013a2/0x18013bb/0x18013ef). Provisioning framing, asm-pinned: `spim` = `field1` u32 BE @0 ‖ payload-length u32 BE @4 ‖ payload @8 (payload must exceed 59 B, 0x17efb33; more than 23 trailing bytes rejected with error 0x41); `cpim` = 9 fields (u32be(32) ‖ `encsec(rng2)` 32 B ‖ u32be(32) ‖ rng1 32 B ‖ 60 B blob = caller-extra ‖ rng3 pad ‖ u64 BE of arg9 = −2 ‖ u32 BE of the byte-swapped LE dword at rng3[8..12] ‖ two 256-bit tail fields fed by `encrypt(session, rng16, data)` + `spim_map_signature` — the tail's exact wire assembly is `TODO(capture)`); `ptm` is lenient (u32 BE then the whole tail — every candidate shape accepted, error 0); finish requires `tk` exactly 16 B (error 0x8, 0x17f1f11), key16 = `encrypt(session, NULL iv, tk)` (0x17f1eac), decrypt block count = payload.size/16 (0x17f2548), yielding the 0x78-byte provisioned struct (+0x00 16 B, +0x10 u64, +0x18 u64, +0x20 16 B, +0x30..0x70 64 B metadata, +0x70 u32, +0x74 u8 flavor — which bytes are `client_secret` vs `mid` vs `metadata` is `TODO(capture)`). Kotlin: `registration` module `ClearAdiOtp`/`ClearAdiProvision`/`ClearAdiFraming`/`ClearAnisetteProvider`, 192 tests green incl. byte-exact oracle vectors. — 2026-10-07 (rev 28, live capture: one real OpenBubbles provisioning run under gdb, `registration/dev/clearadi/captures/WIRE-NOTES.md`; the framer is corrected): `spim` trailing is NOT capped at 23 B — the live spim carries 99 trailing bytes and parses fine (the earlier 0x41 pin was a probe-harness artifact); the trailing is server material the client relays. `cpim` = an OUTER 276-B message `[u32be(5)][iv 16][u32be(160)][encout 160][X 32][u32be(52)][u32be(4)][Y 52]` whose 160-B encrypt plaintext = `u32be(32) ‖ encsec(rng2) ‖ u32be(32) ‖ rng1 ‖ 60 B blob (caller-extra ‖ rng3 pad) ‖ u64 BE arg9 = −2 (live-confirmed) ‖ u32 BE unix-seconds ‖ u32be(1) ‖ 12 B (source TODO)`; the iv is 16 plain RNG bytes — the earlier 'IV assembled from the Spim's trailing fields' reading was actually the thread-RNG pool (words at r14+0x10, cursor at r14+0x110). `spim_map_signature` (0x17fe2a0) args are (rdi = in, rsi = out): the live call signs a constant 16-B block `e955a4f0…` → `f877c30e…`, and NEITHER that signature nor the SHA-256(encout) digest appears anywhere in the captured wire — the §6 'two 256-bit tail fields' reading is falsified. The `[u32be(52)][u32be(4)][Y 52]` group is the spim's trailing[16:76] relayed verbatim; X (32 B) = `TODO(capture)`. `ptm` = `[u32 BE 4][seed 16][u32 BE payloadLen][payload][tail]` (live 503 B: seed = ptm[4:20] feeds decryptPTM as its chain seed via rcx; block count = payloadLen/16 = 28); key16 = `encrypt(session, NULL iv, tk)` confirmed byte-exact against the live call. ProvisionedData = the 0x78-B Rust struct `{metadata: Vec (400 B live = decrypt out[4:404], length u32 BE at out[0]), client_secret = rng2 (32 B), mid = rng3 (60 B), flavor u8 (0x01 on a live Mac run — distinct from the OTP flavor numbering)}`; the finish gate is the decrypt output's **out[408:440] == rng1 echo** (NOT out[0:32]); the OTP-path report's ProvisionedMachine note (mid[32]@+0x18, secret[60]@+0x38) had the two swapped. Kotlin `ClearAdiFraming`/`ClearAnisetteProvider` restructured to the live shapes; 194 tests green. Remaining `TODO(capture)`: the X32 source, the 12-B plaintext tail, trailing[0:16]'s role. — 2026-10-07 (rev 29, continuation capture — two further live provisioning runs, `gdb_prov8/9.gdb`, byte-exact on both): the three remaining cpim TODOs are CLOSED. X = SHA-256(encout ‖ spim_map_signature(trailing[0:16]) ‖ a fixed 24-byte filler constant `3795020c05f77009cbfeaa78a5bcb3175bad60b9deaaaa8d` (.so .rodata) — the digest is written to the wire byte-exact as the `[X 32]` group, so the rev-28 "sig16 and the digest appear nowhere on the wire" note resolves: both are on the wire transitively (sig16 as the hash input, the digest as X). The signature's INPUT is NOT a constant block — the "constant e955a4f0…" seen in the morning runs was that session's trailing[0:16]; the trailing head is server material that differs between provisioning sessions, and the signature model still reproduces the live output exactly. The 12-B plaintext tail = 12 fresh RNG bytes (low bytes of 12 thread-RNG pool words drawn immediately before the IV's 16 — the same construction as the IV, so the framer draws 12). Kotlin: the pinned production seal (`CpimShaTailSeal` — SHA-256 via the platform, sig via the ported `spimMapSignature`) is the provider default; 195 tests green. — 2026-10-08 (rev 30, clean-room RE of the NAC mint — the validation blob "C5" — over the same unstripped Linux `.so`; gdb oracle quadruples first, then python model, then Kotlin; method + findings in `registration/dev/clearadi/captures/NAC-NOTES.md`): the mint is local and pure, and every byte of the 517-B validation stamp is now derived EXCEPT one residue (below). The stamp = `0x02 ‖ rand16 (RNG) ‖ sig16 ‖ u32be(0x1e0) ‖ blob480`; blob480 = `tag 0x05 ‖ u32be(1) ‖ u32be(0x180) ‖ scramble_body(session250, rng_pad130) ‖ u64be(79) ‖ cert79`, where cert79 is the first 79 B of the ctx cert chain and session250 the 250-B session slice — E2E the full stamp computes byte-exact from (cert79, session250, rng_pad130, body576, rand16). The crypto layers, all byte-exact vs three independent live runs: **pear_aes** — a white-box AES variant (`create_rounds` = AES-128-style expansion with pluggable sbox/rcon; `init_key` = S1-scramble → schedule; `establish_key` = schedule + T-transform of blocks 1–9 only, blocks 0/10 raw; `encrypt_cbc` = whitening + **9 rounds** + final `SE[st9[p]]^rk11` with p=[12,10,4,3,15,5,2,8,14,1,9,7,0,13,11,6]; `decrypt_cbc` = backward rounds with the key xor fused into the table index, final `perm(SDI[st9^rk1])^chain^rk0`); **domergestep2** — the white-box core of `scramble_body` (KP key schedule over 80 B, 4 blocks of the `0x51·m` constant with state[0] xored on EVERY block, final `TC[x]+k`, out = state ⊕ in1; 21/21 live captures); **key_establishment** — the EstablishKeyResponse wire is `tag ‖ key16@0x01 ‖ u32be bodyLen@0x11 ‖ body@0x15 (0x250) ‖ trailing (85 B, unused by the crypto)`. **SignState::sign** (the 16-B OTS signature over the blob): structure fully decoded — scatter → polynomial chain → flag table → u32 permutations → divisor table → key map → keygen → matrix/phases B–F → 16 position rounds → tail (fold chains + threshold cipher + staging + final formula); stages 1–9 are hand-transcribed and byte-exact vs ground truth, the tail's threshold cipher (AES-like, 9 rounds × 30 generator runs) is structurally decoded with 270 checkpointed rounds, and the signature the model produces is byte-exact vs the live stamp. CLOSED (same day, follow-up): the capture-free pure-code transcription of the tail cipher and the pre-init region — the full 517-B stamp now computes byte-exact from (cert79, session250, rng_pad130, body576, rand16) in a pure-Python model (no emulator, no captures; `sig_hand.py verify` 20/20) with the complete constant inventory recorded for the Kotlin port (see WORKLOG session 20). Kotlin: `registration` `PearTablesData`/`PearTables` (generated base64 table image, ClearAdiTablesData pattern), `PearAes`, `NacMerge`, `NacValidation` (`mint` + `keyEstablishment`; sig16 a parameter — no placeholder signature), 206 tests green (synthetic vectors, no Apple bytes). — 2026-10-08 (rev 31, Kotlin port of the sign model — the sig16 residue above is CLOSED): `SignState::sign` is transcribed to `registration` `NacSign` (full pipeline: scatter → u32 flag table → perms → divisor/key map → keygen → matrix/phases B–E → 16 position rounds, then the 30-run tail — RS32/rec0/r70ab init, run-start feedback, soup57/soup80, the 9-round threshold cipher, staging from the generator soup, final formula, chain1 fold), with its .rodata tables embedded as a generated base64 image (`NacSignTablesData`/`NacSignTables`, PearTablesData pattern; the 17-loop byte tables are in-range slices of the T2_BIG span). `NacValidation.mint(cert79, session250, rngPad130, rand16, body576)` now computes the full 517-B stamp including sig16 internally (the sig16-parameter assembly step remains as `mintWithSig`); Kotlin sign == python model on LCG-seeded synthetic vectors (incl. zero-rand16 and zero-blob edges), full-mint e2e byte-exact, 208 tests green. — 2026-10-08 (rev 32, three instrumented live runs of the same OpenBubbles app under gdb — the mint's INPUT mappings (C56-b/c/d/e + cert79) are now wire-pinned, and a live-input divergence in the rev-30/31 sign model is discovered; dumps + method in `registration/dev/clearadi/captures/NAC-NOTES.md` session-21 continuation): the five mint inputs map from the exchange as follows — **C56-b pearKey16** = 16 fresh client RNG bytes created inside `ValidationCtx::new` (rand-crate `[u8;16]` sample; wrapped to the server inside the step-2 request blob, which is why it never appears in the session-info response); **C56-c session250** = the 250-B `HardwareConfig::to_bin` output — the client's hardware-identity serialization (model identifier, serial, platform-UUID + second UUID, board-id `Mac-…`, OS build, MLB — ASCII, len-prefixed), held at ctx+0x20 and NOT a slice of any server response; **C56-d rngPad130** = 130 fresh thread-RNG bytes filling the 0x180-B scramble buffer after `u32be(len) ‖ data` (pad content is irrelevant filler, drawn fresh per mint); **C56-e body576** = the SDAT-mapped decrypt of the session-info response's `body@0x15` (0x250 B) region `[2:578]` — i.e. ctx[0x40:0x280]; and **cert79 is NOT the step-1 certificate** (the rev-30 reading is falsified): it is the first 0x4f bytes of the response's 85-B trailing Vec (ctx+0x00), server material relayed into the stamp — wire-verified ×3 (stamp[438:517] == trailing[0:79]). The step-2 request blob itself is now wire-captured: 338 B = constant header `01 08 4b2c91481d9b7da0` + 328 B per-session material (RSA-OAEP under the step-1 cert's public key + digests, built inside `ValidationCtx::new`, held at ctx+0x2c0) — its exact field map and the HardwareConfig serialization grammar remain `TODO(capture)` (C56-a/C56-c). **Live-input divergence (new residue):** with ALL FOUR `sign::sign` args byte-verified against the captured call (rsi=body576, rdx=rand16, rcx=blob480), the rev-30/31 pure model produces a WRONG sig16 on two independent fresh runs (e.g. `7a9555e5…`/`b2c72389…` vs live `0160f19c…`) while still reproducing the session-20 capture byte-exact — the empirical tail-cipher/initial-carry fit contains an input-dependent path that the session-20 data never exercised; a live mint is blocked on closing this, and the rev-30/31 "byte-exact" claims hold only for the session-20 capture and the synthetic vectors. Kotlin: `NacValidation.keyEstablishmentFull` (payload + trailing), `NacMint` facade updated with the pinned mappings; 209 registration tests green. — 2026-10-09 (rev 33, the sign-model fresh-input divergence is CLOSED — the live-mint blocker from rev 32 is gone; method + dumps in `registration/dev/clearadi/captures/NAC-NOTES.md` session-23 continuation): the divergence was two more session-20-frozen constants inside `SignState::sign`, both found by rebuilding a fresh-input round-state checkpoint harness (`gdb_c56d.gdb`, 270 round dumps in `captures/c56d/`) and diffing stage-by-stage against a unicorn execution of the same input (the emulator reproduces the live sig16 byte-exact, so the fit — not the emulator — was wrong). (1) **staging i==10 ecx2**: the staging maze's i==10 branch multiplies in `perm2u[st[0x6fa2]]` (rsp+0x28, loaded at 0x17a36c0 from state+0x6fa2); the fitted `perm2u[0x40]` was the session-20 snapshot (s20 st[0x6fa2]==0x40 — a no-op there, silently wrong elsewhere). (2) **the P region (state+0x6898, 7×256) is INPUT-DEPENDENT** — the "runtime constant, byte-identical across base/mod/body-flip" reading (rev 30) is falsified: the SSE builder at 0x17a1381-0x17a20b5 parameterizes five of the seven 256-byte tables on five body-derived bytes (`st[0x288a]^st[0x287a]`, `st[0x287f]^st[0x267f]`, `st[0x2683]^st[0x2753]`, `st[0x287d]^st[0x288d]`, `st[0x274b]^st[0x288b]`); tables 2/5 are the only true constants. The builder's instruction stream is serialized and re-executed lane-wise (generated `NacPTable.kt`, 1833 ints + 46 const vectors), verified byte-exact vs the emulated builder on the fresh input, two random states and the session-20 capture. Also pinned while re-diffing: the region-0x6f98 sel bytes, 0x6fd8 selectors (base-7 digits of `body[0xe0+i]^body[0xf0+i]`), 0x6fe8/0x6fec masks/flags, the scatter N and the map-key cursor are all body-derived (already fixed in-session in the python model; now ported), and the cipher's round "RC constants" are the state's own 0x25d8 words. Verification: the pure model + Kotlin `NacSign` reproduce the FRESH live sig16 `307d7151…` and the FULL fresh 517-B stamp byte-exact (`captures/c56d/fresh_stamp.bin`), the session-20 stamp still byte-exact, `sig_hand.py verify` 20/20; Kotlin: `NacSign` re-ported (region6f98/masks/sel/N/cursor/scatter-mutated/RC now computed; the four former state-embedded constants dropped from `NacSignTablesData`), 209 registration tests green (vectors regenerated), imessage `:server:testDebugUnitTest` green. Remaining before a live attempt: C56-a (request-blob field map) + C56-c (HardwareConfig grammar) — both `TODO(capture)`, C58 OABS parser. +- 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 pure. `gen = unix_seconds / 30`; dispatch by OTP flavor byte: 1 (IOS) → `gen_otp` (0x17fba60), 0 (Mac) → `gen_otp_ios` (0x1801d90) — the binary's naming is inverted relative to the flavor (a distinct numbering from the §1.1 session flavor `Mac`=1/`IOS`=3). Cipher inputs: `one` = the machine's `client_info` (required ≥ 0x190 B IOS / ≥ 0xd0 B Mac, length-checked) and `two` = the 32-byte `mid`; output 16 bytes. The wire `X-Apple-I-MD` frame is 28 B: `u32be(5)` ‖ `u32be(16)` ‖ sig16 ‖ `u32be(flavor)`. The 2FA code is `SHA-1(sig16 ‖ secret60) mod 1_000_000` over the machine's full 60-byte secret slice (`machine[0x38..0x74]`; two standard SHA-1 blocks — an earlier report's "48-byte" reading is wrong, 0x18013a2/0x18013bb/0x18013ef). Provisioning framing, asm-pinned: `spim` = `field1` u32 BE @0 ‖ payload-length u32 BE @4 ‖ payload @8 (payload must exceed 59 B, 0x17efb33; more than 23 trailing bytes rejected with error 0x41); `cpim` = 9 fields (u32be(32) ‖ `encsec(rng2)` 32 B ‖ u32be(32) ‖ rng1 32 B ‖ 60 B blob = caller-extra ‖ rng3 pad ‖ u64 BE of arg9 = −2 ‖ u32 BE of the byte-swapped LE dword at rng3[8..12] ‖ two 256-bit tail fields fed by `encrypt(session, rng16, data)` + `spim_map_signature` — the tail's exact wire assembly is `TODO(capture)`); `ptm` is lenient (u32 BE then the whole tail — every candidate shape accepted, error 0); finish requires `tk` exactly 16 B (error 0x8, 0x17f1f11), key16 = `encrypt(session, NULL iv, tk)` (0x17f1eac), decrypt block count = payload.size/16 (0x17f2548), yielding the 0x78-byte provisioned struct (+0x00 16 B, +0x10 u64, +0x18 u64, +0x20 16 B, +0x30..0x70 64 B metadata, +0x70 u32, +0x74 u8 flavor — which bytes are `client_secret` vs `mid` vs `metadata` is `TODO(capture)`). Kotlin: `registration` module `ClearAdiOtp`/`ClearAdiProvision`/`ClearAdiFraming`/`ClearAnisetteProvider`, 192 tests green incl. byte-exact oracle vectors. — 2026-10-07 (rev 28, live capture: one real OpenBubbles provisioning run under gdb, `registration/dev/clearadi/captures/WIRE-NOTES.md`; the framer is corrected): `spim` trailing is NOT capped at 23 B — the live spim carries 99 trailing bytes and parses fine (the earlier 0x41 pin was a probe-harness artifact); the trailing is server material the client relays. `cpim` = an OUTER 276-B message `[u32be(5)][iv 16][u32be(160)][encout 160][X 32][u32be(52)][u32be(4)][Y 52]` whose 160-B encrypt plaintext = `u32be(32) ‖ encsec(rng2) ‖ u32be(32) ‖ rng1 ‖ 60 B blob (caller-extra ‖ rng3 pad) ‖ u64 BE arg9 = −2 (live-confirmed) ‖ u32 BE unix-seconds ‖ u32be(1) ‖ 12 B (source TODO)`; the iv is 16 plain RNG bytes — the earlier 'IV assembled from the Spim's trailing fields' reading was actually the thread-RNG pool (words at r14+0x10, cursor at r14+0x110). `spim_map_signature` (0x17fe2a0) args are (rdi = in, rsi = out): the live call signs a constant 16-B block `e955a4f0…` → `f877c30e…`, and NEITHER that signature nor the SHA-256(encout) digest appears anywhere in the captured wire — the §6 'two 256-bit tail fields' reading is falsified. The `[u32be(52)][u32be(4)][Y 52]` group is the spim's trailing[16:76] relayed verbatim; X (32 B) = `TODO(capture)`. `ptm` = `[u32 BE 4][seed 16][u32 BE payloadLen][payload][tail]` (live 503 B: seed = ptm[4:20] feeds decryptPTM as its chain seed via rcx; block count = payloadLen/16 = 28); key16 = `encrypt(session, NULL iv, tk)` confirmed byte-exact against the live call. ProvisionedData = the 0x78-B Rust struct `{metadata: Vec (400 B live = decrypt out[4:404], length u32 BE at out[0]), client_secret = rng2 (32 B), mid = rng3 (60 B), flavor u8 (0x01 on a live Mac run — distinct from the OTP flavor numbering)}`; the finish gate is the decrypt output's **out[408:440] == rng1 echo** (NOT out[0:32]); the OTP-path report's ProvisionedMachine note (mid[32]@+0x18, secret[60]@+0x38) had the two swapped. Kotlin `ClearAdiFraming`/`ClearAnisetteProvider` restructured to the live shapes; 194 tests green. Remaining `TODO(capture)`: the X32 source, the 12-B plaintext tail, trailing[0:16]'s role. — 2026-10-07 (rev 29, continuation capture — two further live provisioning runs, `gdb_prov8/9.gdb`, byte-exact on both): the three remaining cpim TODOs are CLOSED. X = SHA-256(encout ‖ spim_map_signature(trailing[0:16]) ‖ a fixed 24-byte filler constant `3795020c05f77009cbfeaa78a5bcb3175bad60b9deaaaa8d` (.so .rodata) — the digest is written to the wire byte-exact as the `[X 32]` group, so the rev-28 "sig16 and the digest appear nowhere on the wire" note resolves: both are on the wire transitively (sig16 as the hash input, the digest as X). The signature's INPUT is NOT a constant block — the "constant e955a4f0…" seen in the morning runs was that session's trailing[0:16]; the trailing head is server material that differs between provisioning sessions, and the signature model still reproduces the live output exactly. The 12-B plaintext tail = 12 fresh RNG bytes (low bytes of 12 thread-RNG pool words drawn immediately before the IV's 16 — the same construction as the IV, so the framer draws 12). Kotlin: the pinned production seal (`CpimShaTailSeal` — SHA-256 via the platform, sig via the ported `spimMapSignature`) is the provider default; 195 tests green. — 2026-10-08 (rev 30, clean-room RE of the NAC mint — the validation blob "C5" — over the same unstripped Linux `.so`; gdb oracle quadruples first, then python model, then Kotlin; method + findings in `registration/dev/clearadi/captures/NAC-NOTES.md`): the mint is local and pure, and every byte of the 517-B validation stamp is now derived EXCEPT one residue (below). The stamp = `0x02 ‖ rand16 (RNG) ‖ sig16 ‖ u32be(0x1e0) ‖ blob480`; blob480 = `tag 0x05 ‖ u32be(1) ‖ u32be(0x180) ‖ scramble_body(session250, rng_pad130) ‖ u64be(79) ‖ cert79`, where cert79 is the first 79 B of the ctx cert chain and session250 the 250-B session slice — E2E the full stamp computes byte-exact from (cert79, session250, rng_pad130, body576, rand16). The crypto layers, all byte-exact vs three independent live runs: **pear_aes** — a white-box AES variant (`create_rounds` = AES-128-style expansion with pluggable sbox/rcon; `init_key` = S1-scramble → schedule; `establish_key` = schedule + T-transform of blocks 1–9 only, blocks 0/10 raw; `encrypt_cbc` = whitening + **9 rounds** + final `SE[st9[p]]^rk11` with p=[12,10,4,3,15,5,2,8,14,1,9,7,0,13,11,6]; `decrypt_cbc` = backward rounds with the key xor fused into the table index, final `perm(SDI[st9^rk1])^chain^rk0`); **domergestep2** — the white-box core of `scramble_body` (KP key schedule over 80 B, 4 blocks of the `0x51·m` constant with state[0] xored on EVERY block, final `TC[x]+k`, out = state ⊕ in1; 21/21 live captures); **key_establishment** — the EstablishKeyResponse wire is `tag ‖ key16@0x01 ‖ u32be bodyLen@0x11 ‖ body@0x15 (0x250) ‖ trailing (85 B, unused by the crypto)`. **SignState::sign** (the 16-B OTS signature over the blob): structure fully decoded — scatter → polynomial chain → flag table → u32 permutations → divisor table → key map → keygen → matrix/phases B–F → 16 position rounds → tail (fold chains + threshold cipher + staging + final formula); stages 1–9 are hand-transcribed and byte-exact vs ground truth, the tail's threshold cipher (AES-like, 9 rounds × 30 generator runs) is structurally decoded with 270 checkpointed rounds, and the signature the model produces is byte-exact vs the live stamp. CLOSED (same day, follow-up): the capture-free pure-code transcription of the tail cipher and the pre-init region — the full 517-B stamp now computes byte-exact from (cert79, session250, rng_pad130, body576, rand16) in a pure-Python model (no emulator, no captures; `sig_hand.py verify` 20/20) with the complete constant inventory recorded for the Kotlin port (see WORKLOG session 20). Kotlin: `registration` `PearTablesData`/`PearTables` (generated base64 table image, ClearAdiTablesData pattern), `PearAes`, `NacMerge`, `NacValidation` (`mint` + `keyEstablishment`; sig16 a parameter — no placeholder signature), 206 tests green (synthetic vectors, no Apple bytes). — 2026-10-08 (rev 31, Kotlin port of the sign model — the sig16 residue above is CLOSED): `SignState::sign` is transcribed to `registration` `NacSign` (full pipeline: scatter → u32 flag table → perms → divisor/key map → keygen → matrix/phases B–E → 16 position rounds, then the 30-run tail — RS32/rec0/r70ab init, run-start feedback, soup57/soup80, the 9-round threshold cipher, staging from the generator soup, final formula, chain1 fold), with its .rodata tables embedded as a generated base64 image (`NacSignTablesData`/`NacSignTables`, PearTablesData pattern; the 17-loop byte tables are in-range slices of the T2_BIG span). `NacValidation.mint(cert79, session250, rngPad130, rand16, body576)` now computes the full 517-B stamp including sig16 internally (the sig16-parameter assembly step remains as `mintWithSig`); Kotlin sign == python model on LCG-seeded synthetic vectors (incl. zero-rand16 and zero-blob edges), full-mint e2e byte-exact, 208 tests green. — 2026-10-08 (rev 32, three instrumented live runs of the same OpenBubbles app under gdb — the mint's INPUT mappings (C56-b/c/d/e + cert79) are now wire-pinned, and a live-input divergence in the rev-30/31 sign model is discovered; dumps + method in `registration/dev/clearadi/captures/NAC-NOTES.md` session-21 continuation): the five mint inputs map from the exchange as follows — **C56-b pearKey16** = 16 fresh client RNG bytes created inside `ValidationCtx::new` (rand-crate `[u8;16]` sample; wrapped to the server inside the step-2 request blob, which is why it never appears in the session-info response); **C56-c session250** = the 250-B `HardwareConfig::to_bin` output — the client's hardware-identity serialization (model identifier, serial, platform-UUID + second UUID, board-id `Mac-…`, OS build, MLB — ASCII, len-prefixed), held at ctx+0x20 and NOT a slice of any server response; **C56-d rngPad130** = 130 fresh thread-RNG bytes filling the 0x180-B scramble buffer after `u32be(len) ‖ data` (pad content is irrelevant filler, drawn fresh per mint); **C56-e body576** = the SDAT-mapped decrypt of the session-info response's `body@0x15` (0x250 B) region `[2:578]` — i.e. ctx[0x40:0x280]; and **cert79 is NOT the step-1 certificate** (the rev-30 reading is falsified): it is the first 0x4f bytes of the response's 85-B trailing Vec (ctx+0x00), server material relayed into the stamp — wire-verified ×3 (stamp[438:517] == trailing[0:79]). The step-2 request blob itself is now wire-captured: 338 B = constant header `01 08 4b2c91481d9b7da0` + 328 B per-session material (RSA-OAEP under the step-1 cert's public key + digests, built inside `ValidationCtx::new`, held at ctx+0x2c0) — its exact field map and the HardwareConfig serialization grammar remain `TODO(capture)` (C56-a/C56-c). **Live-input divergence (new residue):** with ALL FOUR `sign::sign` args byte-verified against the captured call (rsi=body576, rdx=rand16, rcx=blob480), the rev-30/31 pure model produces a WRONG sig16 on two independent fresh runs (e.g. `7a9555e5…`/`b2c72389…` vs live `0160f19c…`) while still reproducing the session-20 capture byte-exact — the empirical tail-cipher/initial-carry fit contains an input-dependent path that the session-20 data never exercised; a live mint is blocked on closing this, and the rev-30/31 "byte-exact" claims hold only for the session-20 capture and the synthetic vectors. Kotlin: `NacValidation.keyEstablishmentFull` (payload + trailing), `NacMint` facade updated with the pinned mappings; 209 registration tests green. — 2026-10-09 (rev 33, the sign-model fresh-input divergence is CLOSED — the live-mint blocker from rev 32 is gone; method + dumps in `registration/dev/clearadi/captures/NAC-NOTES.md` session-23 continuation): the divergence was two more session-20-frozen constants inside `SignState::sign`, both found by rebuilding a fresh-input round-state checkpoint harness (`gdb_c56d.gdb`, 270 round dumps in `captures/c56d/`) and diffing stage-by-stage against a unicorn execution of the same input (the emulator reproduces the live sig16 byte-exact, so the fit — not the emulator — was wrong). (1) **staging i==10 ecx2**: the staging maze's i==10 branch multiplies in `perm2u[st[0x6fa2]]` (rsp+0x28, loaded at 0x17a36c0 from state+0x6fa2); the fitted `perm2u[0x40]` was the session-20 snapshot (s20 st[0x6fa2]==0x40 — a no-op there, silently wrong elsewhere). (2) **the P region (state+0x6898, 7×256) is INPUT-DEPENDENT** — the "runtime constant, byte-identical across base/mod/body-flip" reading (rev 30) is falsified: the SSE builder at 0x17a1381-0x17a20b5 parameterizes five of the seven 256-byte tables on five body-derived bytes (`st[0x288a]^st[0x287a]`, `st[0x287f]^st[0x267f]`, `st[0x2683]^st[0x2753]`, `st[0x287d]^st[0x288d]`, `st[0x274b]^st[0x288b]`); tables 2/5 are the only true constants. The builder's instruction stream is serialized and re-executed lane-wise (generated `NacPTable.kt`, 1833 ints + 46 const vectors), verified byte-exact vs the emulated builder on the fresh input, two random states and the session-20 capture. Also pinned while re-diffing: the region-0x6f98 sel bytes, 0x6fd8 selectors (base-7 digits of `body[0xe0+i]^body[0xf0+i]`), 0x6fe8/0x6fec masks/flags, the scatter N and the map-key cursor are all body-derived (already fixed in-session in the python model; now ported), and the cipher's round "RC constants" are the state's own 0x25d8 words. Verification: the pure model + Kotlin `NacSign` reproduce the FRESH live sig16 `307d7151…` and the FULL fresh 517-B stamp byte-exact (`captures/c56d/fresh_stamp.bin`), the session-20 stamp still byte-exact, `sig_hand.py verify` 20/20; Kotlin: `NacSign` re-ported (region6f98/masks/sel/N/cursor/scatter-mutated/RC now computed; the four former state-embedded constants dropped from `NacSignTablesData`), 209 registration tests green (vectors regenerated), imessage `:server:testDebugUnitTest` green. Remaining before a live attempt: C56-a (request-blob field map) + C56-c (HardwareConfig grammar) — both `TODO(capture)`, C58 OABS parser. — 2026-10-09 (rev 34, C56-a request-blob field map ~85% pinned — four further instrumented runs, `gdb_c56e..h.gdb`, dumps in `captures/c56e..h/`, write-up in NAC-NOTES.md session-24 continuation): the 338-B step-2 request blob = constant header `01 08 4b2c91481d9b7da0` ‖ [10:266] = 256-B **RSA-2048-OAEP-SHA1** ciphertext under the step-1 cert's public key (no sha2 crate linked in the helper binary; a single `Oaep::new` monomorphization; output 0x100 checked in the disasm), plaintext = **pearKey16 ‖ hmacKey20** (36 B — the encrypt's r9=0x24; hmacKey byte-verified at plaintext offset 16 on two runs) ‖ [266:286] = 20 B, UNPINNED (ends with `u32be(32)`, a length marker for the next field; assembly source = struct+0x450 ← [rsp+0x260] + r15d) ‖ [286:318] = 32 B = the serialize-time content of the `encrypt_cbc` output Vec (the serializer reads the Vec the encrypt_cbc sret was moved into; offline pear-AES reproduction from (plaintext32, iv, rk176) is open — the plaintext carries a 0x72-fill pattern suggesting a reused/padded scratch struct) ‖ [318:338] = **HMAC-SHA1(hmacKey20, the 32-B field)** — VERIFIED byte-exact: the two digest finalizes are the inner/outer of a standard HMAC-SHA1 (inner = sha1(key⊕ipad ‖ msg), outer = sha1(key⊕opad ‖ inner); the 20-B key = u1[0:20]^0x36 == u3[0:20]^0x5c). Role model: the server decrypts the OAEP → (pear16, hmacKey), derives the PAESKey from pear16 (`init_key` input confirmed = pear16), decrypts the 32-B field, and verifies the HMAC — a possession-of-pear16 proof bound to the blob. Also pinned: the blob Vec at ctx+0x2c0 is already moved-out by key_establishment time — capture it at the serializer return (0x1794998, sret {tag=6, cap, ptr@[0xc8], len@[0xd0]=0x152}) instead. Remaining for C56-a closure: the 20-B slot's source and the 32-B field's derivation. ### 1.2 Activation endpoints and certificates @@ -331,6 +331,7 @@ Ordered by value. IDs match the inline `TODO(capture):` markers above. ## Revision history +- Rev 34 (2026-10-09, C56-a request-blob field map ~85% pinned — four instrumented runs): the 338-B request blob decomposes as header ‖ RSA-2048-OAEP-SHA1(cert pubkey, pearKey16 ‖ hmacKey20) ‖ 20 B (unpinned, ends u32be(32)) ‖ 32 B pear-AES field (derivation open) ‖ HMAC-SHA1(hmacKey20, the 32-B field) — the HMAC chain verified byte-exact. Live mint still gated on the two unpinned C56-a sub-fields; C58 parser + §1.2/§1.4 activation chain wired in imessage (`register()`), tool UI inputs (export-code paste, courier push token) pending. - Rev 33 (2026-10-09, the sign-model fresh-input divergence CLOSED — the last functional live-mint blocker on the sign path): two session-20-frozen constants inside `SignState::sign` are falsified and replaced by their input-dependent forms — the staging maze's i==10 `ecx2` index (`perm2u[st[0x6fa2]]`, was frozen `perm2u[0x40]`) and the ENTIRE P region state+0x6898 (7×256; five of seven tables are parameterized on five body-derived byte XOR pairs via the SSE builder at 0x17a1381-0x17a20b5 — the "byte-identical across base/mod/body-flip" claim held only because those captures shared a body). The builder stream is serialized into generated Kotlin (`NacPTable`) and re-executed lane-wise, byte-exact vs the emulated builder on fresh + random + s20 states. `NacSign` re-ported with all body-derived regions computed (region6f98/0x6fd8/0x6fe8/0x6fec/N/cursor/RC); fresh live sig16 + full 517-B stamp byte-exact, session-20 still byte-exact; 209 registration tests green, imessage `:server:testDebugUnitTest` green. Live mint still gated on C56-a + C56-c (capture) and C58 (OABS parser). - Rev 32 (2026-10-08, three instrumented live runs — the mint's input mappings C56-b/c/d/e + cert79 are wire-pinned; a live-input divergence in the rev-30/31 sign model is discovered and recorded as a new residue): pearKey16/rngPad130/rand16 = fresh client RNG draws; session250 = the HardwareConfig::to_bin hardware-identity blob; body576 = the established payload's [2:578]; cert79 = the session-info response's trailing[0:79] (NOT the step-1 cert — falsified); step-2 request blob wire-captured (338 B, constant header + OAEP + digests; field map open). Kotlin `keyEstablishmentFull`; 209 tests green. Live mint blocked on: C56-a field map, C56-c grammar, and the sign-model fresh-input divergence. - Rev 31 (2026-10-08, Kotlin port of the sign model — the rev-30 sig16 residue is CLOSED; method in `registration/dev/clearadi/captures/NAC-NOTES.md`, constant inventory in `STAGE7-10-REPORT.md` session 6): `SignState::sign` is transcribed to Kotlin `registration` `NacSign` (full pipeline — scatter → u32 flag table → perms → divisor/key map → keygen → matrix/phases B–E → 16 position rounds, then the 30-run tail: RS32/rec0/r70ab init, run-start feedback, soup57/soup80, the 9-round threshold cipher, staging from the generator soup, final formula, chain1 fold), byte-exact vs the pure-Python model on LCG-seeded synthetic vectors incl. edge inputs, with the .rodata tables embedded as a generated base64 image (`NacSignTablesData`/`NacSignTables`, PearTablesData pattern; the 17-loop byte tables are in-range slices of the T2_BIG span, no duplicate embedding). `NacValidation.mint(cert79, session250, rngPad130, rand16, body576)` now computes the full 517-B stamp including sig16 internally (assembly-with-sig kept as `mintWithSig`); full-mint e2e test byte-exact vs the python-generated vector. 208 tests green (synthetic vectors only, no Apple bytes).