From bcb016b2a82fbde2365fda4e4cd76f039b6dd864 Mon Sep 17 00:00:00 2001 From: fenleon Date: Mon, 12 Oct 2026 23:36:32 +0200 Subject: [PATCH] JitPack publishing (maven-publish on all modules), docs/USAGE.md, CI + weekly drift probe --- .github/workflows/ci.yml | 17 ++++++++ .github/workflows/drift-probe.yml | 18 ++++++++ README.md | 4 ++ build.gradle.kts | 23 +++++++++++ docs/USAGE.md | 68 +++++++++++++++++++++++++++++++ engine/build.gradle.kts | 13 ++++++ 6 files changed, 143 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/drift-probe.yml create mode 100644 docs/USAGE.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d6705f1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,17 @@ +name: CI + +on: + push: + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + - uses: gradle/actions/setup-gradle@v4 + - run: ./gradlew build --no-daemon diff --git a/.github/workflows/drift-probe.yml b/.github/workflows/drift-probe.yml new file mode 100644 index 0000000..c05077f --- /dev/null +++ b/.github/workflows/drift-probe.yml @@ -0,0 +1,18 @@ +name: drift probe + +on: + schedule: + - cron: '23 5 * * 1' + workflow_dispatch: + +jobs: + probe: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + - uses: gradle/actions/setup-gradle@v4 + - run: ./gradlew :engine:runProbe --no-daemon diff --git a/README.md b/README.md index cfd72db..90821b1 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,10 @@ No Apple-derived code or binary ships here, including the IMDAppleServices frame 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](https://jitpack.io/#fenleon/open-madrid) — module coordinates and code snippets for the mint, anisette, and the engine are in [docs/USAGE.md](docs/USAGE.md). + ## Building ```bash diff --git a/build.gradle.kts b/build.gradle.kts index 4a993c8..306ae6f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,27 @@ +import org.gradle.api.publish.maven.MavenPublication + plugins { id("com.android.application") version "8.12.3" apply false id("org.jetbrains.kotlin.android") version "2.3.20" apply false } + +// JitPack publishing: a tag build runs publishToMavenLocal on every module. +// Consumers take com.github.fenleon.open-madrid::. +subprojects { + apply(plugin = "maven-publish") + group = "com.github.fenleon" + version = "0.1.0" + afterEvaluate { + // :engine (Android library) publishes itself in its own build file — + // the AGP release component is not visible from the root callback. + if (components.findByName("java") != null) { + extensions.configure { + publications { + create("maven") { + from(components["java"]) + } + } + } + } + } +} diff --git a/docs/USAGE.md b/docs/USAGE.md new file mode 100644 index 0000000..5d484e4 --- /dev/null +++ b/docs/USAGE.md @@ -0,0 +1,68 @@ +# Using Open Madrid + +Open Madrid is a set of plain JVM/Android Kotlin modules. All Apple endpoints are resolved from Apple's own bag files at runtime; nothing is hardcoded beyond the roots the TLS pinning needs. + +## Getting it + +No Maven repository yet — the fastest route is [JitPack](https://jitpack.io/#fenleon/open-madrid): it builds tagged commits on demand. Pick a tag, then take the modules you need: + +```kotlin +repositories { maven("https://jitpack.io") } + +dependencies { + implementation("com.github.fenleon.open-madrid:registration:v0.1.0") + implementation("com.github.fenleon.open-madrid:engine:v0.1.0") +} +``` + +Modules: `codec`, `courier`, `crypto`, `ids`, `registration` (plain JVM), `engine` (Android library, the composition root). Vendoring the source works too — every module builds standalone. + +## The validation-stamp mint (`registration`) + +The NAC stamp is what an Apple validation endpoint wants as proof of hardware identity. Inputs come from the three-step §1.1 exchange (spec §1.1): + +```kotlin +// step 2: POST a NacRequestBlob, get session-info back +val pearKey16 = SecureRandom().generateSeed(16) +val blob = NacRequestBlob.build(certChain.last(), pearKey16, SecureRandom()) +val sessionInfo = certClient.initializeValidation(blob) + +// key establishment + the stamp +val established = NacMint.keyEstablishmentFull(sessionInfo, pearKey16) +val stamp = NacMint.mint( + cert79 = established.trailing.copyOfRange(0, 79), + session250 = HardwareConfiguration.from(identifiers).toBin(), + rngPad = ByteArray(0x17c - 250).also(rng::nextBytes), + rand16 = ByteArray(16).also(rng::nextBytes), + body576 = established.payload.copyOfRange(2, 578), +) +``` + +`identifiers` is a Mac hardware identity — the C58 "Mac-Hardware-Info" export code from a Mac carries all of it (see `MacHardwareInfoExport`). + +## Anisette headers (`registration`) + +```kotlin +val provider = ClearAnisetteProvider(http, stateStore, loginInfo, rng) +val provisioned = provider.ensureProvisioned() // live GSA machine provisioning +val headers = provider.anisetteValues(provisioned, System.currentTimeMillis()) +``` + +`http` is any `IdsHttp`. **Important:** Apple's validation endpoint (`identity.ess.apple.com`) rejects every JVM TLS stack by handshake fingerprint — it only accepts rustls-shaped clients. Use a small rust transport (reqwest + rustls, two functions, exposed over UniFFI — see the `ids-bridge` in this org's imessage tool) for the two §1.1 calls. The GSA endpoints accept plain JVM TLS. + +## Everything wired together (`engine`) + +`Engine` composes the bag fetcher, IDS store, key pairs, HTTP and courier connections over one config: + +```kotlin +val engine = Engine(Engine.Config(workingDir = File(context.filesDir, "imessage"))) +engine.http // IdsHttp over the platform stack (pinned Apple roots) +engine.bagFetcher // IDS + APNs bags +engine.courier() // the APNs courier connection +``` + +Registration flows live in the `registration` module: `GsaLoginChain` (Apple-ID sign-in, SRP + 2FA), `GsaCpd`, `GsaDelegateLogin`, `AlbertActivation`, `IdsActivation` — the imessage tool composes them; see its `ImessageRegistration` for a worked end-to-end wiring. + +## Support status + +See the README and the spec's open-questions checklist. Message send/receive over the courier is not implemented yet. diff --git a/engine/build.gradle.kts b/engine/build.gradle.kts index 4902558..d8fc462 100644 --- a/engine/build.gradle.kts +++ b/engine/build.gradle.kts @@ -64,3 +64,16 @@ tasks.register("runProbe") { workingDir = rootDir args = (findProperty("probeArgs") as String?)?.split(" ")?.filter { it.isNotBlank() } ?: emptyList() } + +// JitPack publishing — :engine is an Android library; the release component +// only exists in this module's own evaluation scope. +apply(plugin = "maven-publish") +afterEvaluate { + extensions.configure { + publications { + create("maven") { + from(components["release"]) + } + } + } +}