fenleon 21eefe3846 ClearADI: live-capture corrections — ProvisionedData mapping, PTM layout, cpim outer message
One real OpenBubbles provisioning run under gdb (5 attempts, method + dumps in
registration/dev/clearadi/captures/WIRE-NOTES.md) pins what the previous
session left TODO(capture):

- ProvisionedData = {metadata Vec (400 B = decrypt out[4:404], len u32 BE at
  out[0]), client_secret = RNG draw 2, mid = RNG draw 3, flavor u8}; the finish
  gate is the decrypt output's out[408:440] == draw1 echo (not out[0:32])
- PTM = [u32 BE 4][seed 16][u32 BE payloadLen][payload][tail]; the decrypt
  chain seed rides in the PTM itself (finish 17f2557 rcx = ptm[4:20], live)
- cpim = a 276-byte OUTER message [u32be(5)][iv 16][u32be(160)][encout 160]
  [X 32][u32be(52)][u32be(4)][Y 52] over the 160-byte plaintext
  [len][encsec(rng2)][len][rng1][blob=mid][arg9=-2 BE][unix-seconds BE]
  [u32be(1)][12 B]; arg9 = -2 live-confirmed
- the encrypt IV is 16 plain RNG bytes (the 'IV from Spim trailing fields'
  reading was the thread-RNG pool, not the Spim)
- the spim carries 99 trailing bytes (the <=23-B cap was a probe artifact) and
  trailing[16:76] relays verbatim as the wire tail's Y group
- spim_map_signature args are (rdi=in, rsi=out) and neither its output nor the
  SHA-256(encout) digest appears on the wire — the 'two 256-bit tail fields'
  reading is falsified
- decryptPTM's math model round-trips the live PTM byte-exact (gate echo found
  at out[408])

Remaining TODO(capture): the X 32-byte group's source, the 12-byte plaintext
tail, the spim trailing's first 16 bytes. Kotlin ClearAdiFraming /
ClearAnisetteProvider restructured to the live shapes; README updated.

194 tests green (:registration:test).
2026-10-07 17:03:53 +02:00

Open Madrid

An open implementation of Apple's madrid protocol (iMessage) in Kotlin. Covers the property-list codecs, the APNs "courier" push client, the IDS identity service, the encryption envelope, the registration/activation chain, and the ADI anisette chain (device provisioning and per-request OTP headers) computed fully locally in Kotlin.

Unlike open-absinthe, which is a mock placeholder with no real code, everything here is readable and Apache-2.0 licensed.

Status

Validated:

  • The parsers handle real Apple bytes. The actual 405-entry IDS bag and 65-entry APNs bag, captured from a registered device, are committed as test fixtures and decoded with exact-value assertions.
  • The registration chain ran live against Apple (2026-10-02→04, spec rev 26): Albert activation, the packed APNs courier, the full GSA sign-in (SRP, trusted-device 2FA, PET delivery, postdata), then IDS authenticate and register — both green, mailto: handle active. The earlier 409 refusals turned out to be Apple's validation-stamp replay guard (one mint = one device record), documented in the spec.
  • Two live unauthenticated probes against Apple succeeded: both bag endpoints return plists this stack parses, and certificate pinning validates Apple's real TLS chains. A drift check ships with the repo (:engine:runProbe).
  • The courier's auth model is settled: the packed path needs no client certificate; the legacy path answers the handshake with a TLS 1.3 post-handshake certificate_required alert.
  • The ADI anisette crypto is reimplemented natively, no emulation or helper binary: the white-box OTP ciphers, the GSA provisioning exchange, and the spim/cpim/ptm wire framing are transcribed clean-room from analysis of a compiled ADI implementation and verified byte-exact against oracle vectors, then live-confirmed against a genuine client's provisioning run under an instrumented debugger (194 tests green, spec rev 28). No anisette server, no Unicorn, no Mac relay.

Not validated yet:

  • No real message send or receive over the courier; renewal across sessions is untested.
  • The crypto envelopes have round-trip tests but no known-answer vectors from a live device.
  • The ADI provisioning wire shapes were confirmed from an instrumented genuine client (spec rev 28): the provisioned-state mapping (metadata/client-secret/mid/flavor), the PTM layout with its embedded decrypt seed, the 276-byte cpim outer message, and the gate echo position are all live-pinned. Three byte groups remain TODO(capture): the 32-byte cpim tail group, the 12-byte plaintext tail, and the spim trailing's first 16 bytes.
  • A few wire details are capture-bound guesses, tracked in the spec's open-questions checklist.
  • PQ3 is unsupported.

Why another project?

Almost every route to iMessage is closed in some way. Open Madrid is permissively licensed, dependency-light, and fully readable.

Project License / catch
rustpush SSPL
pypush SSPL
imessagego AGPL
BlueBubbles Needs a Mac running 24/7
OpenBubbles Depends on closed-source open-absinthe

Protocol facts live in the spec: docs/imessage-protocol-spec.md.

Contributing

This is a clean-room. Contributors never read the source of rustpush, pypush, imessagego, or their forks. Two roles, kept apart:

  1. Readers study the existing projects. Their only output is the spec.
  2. Writers work only from the spec, Apple's documentation, and recordings of their own device's traffic. Don't open other projects' source or paste their identifiers into commits or issues.

This keeps the code fully open and acceptable to F-Droid and other distribution channels.

Apple material

No Apple-derived code or binary ships here, including the IMDAppleServices framework. The bundled identity in registration/src/main/resources/imessage/fairplay/ is a synthetic placeholder, generated and self-signed, for tests only. Live activation requires Apple's real FairPlay identity, which each user stages themselves.

One deliberate exception in spirit, documented for auditability: the ClearADI white-box lookup tables (the constants the ADI OTP cipher needs) are constant data reconstructed by analyzing a compiled ADI implementation, and they ship as generated Kotlin source (ClearAdiTablesData.kt) decoded at startup — no Apple code, binary, or resource file is committed. The extraction method and every derived fact are documented in the spec.

Building

./gradlew build             # all modules + tests
./gradlew :engine:runProbe  # live drift check

License

Apache-2.0.

Support my work on Ko-fi or GitHub Sponsors.

Languages
Kotlin 100%