C56 input mappings pinned from three live runs (rev 32); sign-model fresh-input divergence recorded

keyEstablishmentFull (payload + trailing Established); NacMint facade
documents the wire-pinned mappings: pearKey16/rngPad130/rand16 = fresh
client RNG, session250 = HardwareConfig::to_bin blob, body576 =
established[2:578], cert79 = response trailing[0:79] (NOT the step-1
cert — falsified). C56-a field map + HardwareConfig grammar stay open.
NEW RESIDUE: the rev-30/31 sign model mis-signs fresh live inputs (all
four sign::sign args byte-verified) — live mint blocked on closing it.
209 registration tests green.
This commit is contained in:
fenleon committed 2026-10-08 21:19:13 +02:00
1 parent cfd6cd4e71
commit 075857e79e
4 files changed
+66 -16

No files matched your search

@@ -1,15 +1,28 @@
package dev.fenn.imessage.registration
/**
* Public facade over the internal NAC mint ([NacValidation], spec §1.1 rev 31 — the 517-B
* validation stamp, byte-exact vs the live captures): the consumer-visible mint surface for
* the §1.1 validation-data seam (:engine api-exposes :registration, but internal members
* stay module-private).
* Public facade over the internal NAC mint ([NacValidation], spec §1.1 — the 517-B
* validation stamp): the consumer-visible mint surface for the §1.1 validation-data seam
* (:engine api-exposes :registration, but internal members stay module-private).
*
* Pure math — no transport, no persistence. The mapping from the pinned §1.1 exchange to
* these inputs is NOT established (§1.1 C56 residue; NAC-NOTES.md closes only the math):
* callers must not fabricate cert79/session250/rngPad130/body576/pearKey16 — run the
* exchange and fail loudly at the first unestablished mapping instead.
* Pure math — no transport, no persistence.
*
* Input mappings from the live exchange (2026-10-08, three instrumented runs; §1.1):
* - pearKey16 — 16 fresh client RNG bytes (created in ValidationCtx::new; wrapped to the
* server inside the step-2 request blob).
* - rngPad130 — 130 fresh client RNG bytes (the scramble buffer's thread-RNG filler).
* - rand16 — 16 fresh client RNG bytes (the mint's own sample).
* - body576 — keyEstablishment payload[2:578] (the EstablishKeyResponse payload region).
* - cert79 — keyEstablishmentFull trailing[0:79] (server material from the response —
* NOT the step-1 certificate).
* Still NOT established (§1.1 C56-a/C56-c residue): the step-2 request blob's field
* construction and the session250 hardware-config serialization — callers must not
* fabricate them; run the exchange and fail loudly at the first unestablished mapping.
*
* KNOWN LIVE DIVERGENCE (§1.1, 2026-10-08): the sig16 pipeline (NacSign) is byte-exact
* vs the session-20 captures and the python model, but produces a WRONG sig16 for
* fresh live inputs (all four sign::sign args byte-verified; two independent runs) —
* do not ship a live mint until §1.1's sign-model residue is closed.
*/
object NacMint {
@@ -25,4 +38,16 @@ object NacMint {
/** [NacValidation.keyEstablishment] — the SDAT-mapped EstablishKeyResponse wire. */
fun keyEstablishment(sessionInfo: ByteArray, pearKey16: ByteArray): ByteArray =
NacValidation.keyEstablishment(sessionInfo, pearKey16)
/** [NacValidation.keyEstablishmentFull] — payload + the response's trailing Vec. */
fun keyEstablishmentFull(sessionInfo: ByteArray, pearKey16: ByteArray): Established =
NacValidation.keyEstablishmentFull(sessionInfo, pearKey16)
}
/** The parsed key-establishment outcome (the ctx fields the mint consumes). */
class Established(
/** The SDAT-mapped EstablishKeyResponse wire (592 B live). */
val payload: ByteArray,
/** The response's trailing Vec (85 B live) — cert79 = [0:0x4f]. */
val trailing: ByteArray,
)
@@ -39,7 +39,7 @@ internal object NacValidation {
/**
* ValidationCtx::sign: the full 517-byte validation stamp. body576 is ValidationCtx
* [0x40:0x2c0] (the sign() body input); sig16 = SignState::sign(body576, rand16, blob480)
* [0x40:0x280] (the sign() body input; wire-verified ×3 live); sig16 = SignState::sign(body576, rand16, blob480)
* with blob480 = the ValidationBody wire built here (NAC-NOTES.md; verified byte-exact,
* STAGE7-10-REPORT.md session 6).
*/
@@ -72,12 +72,14 @@ internal object NacValidation {
* ValidationCtx::key_establishment (0x1792fb0). EstablishResponse wire (698 B live,
* offsets confirmed against the captures): tag u8 | key16 @1 | u32be bodyLen @0x11 |
* body @0x15 (0x250 = 592 B live; decrypt_cbc consumes exactly bodyLen bytes) |
* trailing data Vec (copied verbatim by the binary, unused by the crypto). The iv and
* trailing data Vec (85 B live, copied verbatim into the ctx — NOT unused: its first
* 0x4f bytes are the mint's cert79, wire-verified across three live runs). The iv and
* body are mapped through SKEY (0x2247f6), decrypted with the established round keys,
* and the plaintext is mapped through SDAT (0x2246f6). Returns the SDAT-mapped
* EstablishKeyResponse wire (592 B live; its payload must parse to 0x240 bytes).
* and the plaintext is mapped through SDAT (0x2246f6). The plaintext (592 B live) is
* the EstablishKeyResponse wire: [2:2+0x240] = the payload that becomes the ctx
* body576 region ([0x40:0x280], wire-verified ×3 live); [0:2] = its header.
*/
internal fun keyEstablishment(sessionInfo: ByteArray, pearKey16: ByteArray): ByteArray {
internal fun keyEstablishmentFull(sessionInfo: ByteArray, pearKey16: ByteArray): Established {
require(pearKey16.size == 16) { "key_establishment: pear key must be 16 bytes" }
require(sessionInfo.size >= 21) { "key_establishment: truncated EstablishResponse" }
val key16 = sessionInfo.copyOfRange(1, 17)
@@ -90,9 +92,13 @@ internal object NacValidation {
val pt = PearAes.decryptCbc(ct, iv, rk)
val sdat = PearTables.sdat
for (i in pt.indices) pt[i] = sdat[pt[i].toInt() and 0xff]
return pt
val trailing = sessionInfo.copyOfRange(21 + bodyLen, sessionInfo.size)
return Established(pt, trailing)
}
internal fun keyEstablishment(sessionInfo: ByteArray, pearKey16: ByteArray): ByteArray =
keyEstablishmentFull(sessionInfo, pearKey16).payload
private fun u32be(b: ByteArray, off: Int): Int =
((b[off].toInt() and 0xff) shl 24) or ((b[off + 1].toInt() and 0xff) shl 16) or
((b[off + 2].toInt() and 0xff) shl 8) or (b[off + 3].toInt() and 0xff)
@@ -85,4 +85,22 @@ class NacValidationTest {
assertContentEquals(expected, pt)
assertEquals(592, pt.size)
}
@Test
fun keyEstablishmentFullSplitsPayloadAndTrailing() {
// the full variant (2026-10-08 live-run mapping): payload = the SDAT-mapped
// EstablishKeyResponse wire; trailing = the response's verbatim tail (85 B live)
// whose first 0x4f bytes are the mint's cert79
val pearKey = hex("aebfc2884fd1e6ffdc60273be5756ff5")
val sessionInfo = hex("021b0e89cab86b89c2911da5bbf6f475e3000002502feb2054e4786cfb482f083fb085cb87e0906991e2003bb2a45b65ecf9764d2c39bafe46cdd93600af6d1af6f27999fb275eacc6b6663b075be0bee9fc04e4018899a0e028954cd0f74a51d3c353a1b711ec431974926847dc715d71918f43d424a0c8ea663034699e704306c3c25ab7fa76e3e99a8944a6c1916204bb923e41218232ccc2d4df2658f96bad8f1e4e8e41c20a159de8485e1ec398ea86d1ef9f036d3c92d103500f9be66718c503c18d6cf724909a7e29da398a6b360b56511c034a437483612b5ee1f1fcdb582e0e9a6de81e306542beac6e96c89faa1eb43324b7051598bac4c7e2542542a0bfd6704152a187fe61623b0fdd9ec67105b22190c4b06b68c66a1ddd972ef6e23b269e128a21d9f36c0f0ca1422f4ea90bb1b7e6c5f32d524bd38beaffe99af5ad573fe7a07f3f20094adb6aa281b95e3a20fd6b7489c8d231d94287165e425de90ce4bf0a21cf523146fb0e1dac3c942eb40f75db11b3edd853975f37851adcb8883eac8952d8ad8232d17910ffcd8524167d7c14034b19e3c4a28266d45569384c5d2591333b46a542981ef4ed16e57979170276e079efecf83ec517b12ffa8cf6968d0d7ffb4c15422a6ca034d93096311bfdb724ea37155a2b8923d2bd8eb270b378f528ffd694c0a822bfe1c3016853c6474bada51f519e440f68df4589b377e36f19f839b8fa4965836d75135a5fc7dde8e083652a692cf0e2236e23044e20703d46ac3535ad2e084289c94c4e46c2220fd53e72d5ecba1baab23063544b03c01fb37160e2f2b7878ca58e55944f879c8e5a3feb6ae035cd16a4ec555c28fd73f5aa154e8aa0ce47fb96a0bc18f382bf7101638d563bfe2bd7a7d7ebc4284087ad43556ee2b6ba3973ea044d300d79d2a96dafe22e5fd1b1cfbab9cc5ccc19cd653b689b3a409882eb3441654852a593e128262806")
val full = NacValidation.keyEstablishmentFull(sessionInfo, pearKey)
assertContentEquals(NacValidation.keyEstablishment(sessionInfo, pearKey), full.payload)
assertEquals(85, full.trailing.size)
assertContentEquals(
sessionInfo.copyOfRange(sessionInfo.size - 85, sessionInfo.size),
full.trailing,
)
// body576 = payload[2:578] (the ctx [0x40:0x280] region, wire-verified ×3 live)
assertEquals(592, full.payload.size)
}
}