4.0 KiB
Open Madrid
An open implementation of Apple's madrid protocol (iMessage) in Kotlin: the binary plist codecs, the APNs "courier" push client, the IDS identity service, the encryption envelopes, the registration/activation chain, and the ADI anisette chain (device provisioning, OTP headers, the validation-stamp mint) — all computed locally, in the open. Apache-2.0. open-absinthe, the reference everyone else depends on, is a mock placeholder with no real code; this is the real thing.
Status
Verified against Apple:
- Registration — Albert activation, full GSA sign-in (SRP, trusted-device 2FA, PET delivery), IDS authenticate and register all ran live (2026-10-02→04); the registered handle stayed active.
- Bags and TLS — both bag endpoints return plists these parsers handle; certificate pinning validates Apple's real chains. A drift check ships with the repo (
:engine:runProbe). - ADI/anisette — the white-box ciphers, the GSA provisioning exchange, the spim/cpim/ptm wire framing, and the validation-stamp ("NAC") mint are reimplemented in Kotlin, clean-room, byte-exact against live captures. The full 517-byte stamp computes from its inputs;
NacMintis the public facade. No anisette server, no Unicorn, no Mac relay. - The validation endpoint's
5003 EMerror is solved (spec rev 38): it fingerprints the TLS handshake and only accepts rustls-shaped clients. Proven by posting fresh, never-seen blobs minted by the reference client itself — accepted over rustls, rejected byte-for-byte identically over OpenSSL, BoringSSL, and the JVM stacks. Kotlin code and framing were never wrong. JVM consumers need a small rust transport (reqwest + rustls, two functions) for the two validation calls; the private imessage tool in this org carries the reference implementation.
Not yet:
- No message send or receive over the courier; renewal across sessions untested.
- Crypto envelopes have round-trip tests but no known-answer vectors from a live device.
- A few wire details are capture-bound guesses, tracked in the spec's open-questions checklist.
- PQ3 unsupported.
Why another project?
Almost every route to iMessage is closed in some way:
| Project | License / catch |
|---|---|
| rustpush | SSPL |
| pypush | SSPL |
| imessagego | AGPL |
| BlueBubbles | Needs a Mac running 24/7 |
| OpenBubbles | Depends on closed-source open-absinthe |
The protocol facts behind the code are written up 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:
- Readers study the existing projects. Their only output is the spec.
- Writers work only from the spec, Apple's documentation, and recordings of their own device's traffic.
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 each user to stage Apple's real FairPlay identity themselves.
One documented exception: the ClearADI and pear lookup tables (constant data the ADI ciphers need) were reconstructed by analyzing a compiled ADI implementation and ship as generated Kotlin source, decoded at startup. No Apple code, binary, or resource file is committed. The extraction method is in the spec.
Using it
Tagged releases build on JitPack — module coordinates and code snippets for the mint, anisette, and the engine are in docs/USAGE.md.
Building
./gradlew build # all modules + tests
./gradlew :engine:runProbe # live drift check
License
Support my work on Ko-fi or GitHub Sponsors.