diff --git a/README.md b/README.md index 5384b9c..496a6a3 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,13 @@ sections below describe the design, some of which is not yet connected. - **AirPlay sending** — discovery, the HTTP/plist session layer, TLV8, NTP, the mirror stream and HAP pairing are implemented. Pairing previously used a fabricated SRP group and could never succeed; it now uses the real RFC 5054 - 3072-bit group, but is unconfirmed against physical hardware - ([#27](https://github.com/Developer1010x/openplay/issues/27)). FairPlay will + 3072-bit group, and transient pair-setup is **confirmed against hardware**: + `pair_probe` completes SRP-6a against a Mac running AirTunes/950.7.1, and an + encrypted `GET /info` over the post-pairing control channel returns 200 + ([#27](https://github.com/Developer1010x/openplay/issues/27)). **Mirroring + is not confirmed** — a modern Mac gates it behind FairPlay, and the mirror + stream cannot yet write video into the encrypted post-pairing connection + anyway, so the authenticated path stops with an explicit error. FairPlay will not be implemented here, so Apple TV 2nd/3rd generation are refused up front with an explicit error — see the decision in [docs/crypto.md](docs/crypto.md). @@ -64,7 +69,7 @@ sections below describe the design, some of which is not yet connected. | Protocol | Direction | Notes | |---|---|---| -| AirPlay | Sender only, **untested against hardware** | Discovery, HTTP/plist session layer, TLV8, NTP, the mirror stream and HAP pairing are implemented. FairPlay will not be implemented, so receivers that require it (Apple TV 2nd/3rd gen) are rejected by design. Target: Apple TV, AirPlay 2 TVs, and compatible displays. See [#27](https://github.com/Developer1010x/openplay/issues/27) | +| AirPlay | Sender only — **pairing confirmed against hardware, mirroring unconfirmed** | Discovery, HTTP/plist session layer, TLV8, NTP, the mirror stream, HAP transient pairing and the encrypted control channel are implemented. Video cannot yet be sent over the encrypted post-pairing connection. FairPlay will not be implemented, so receivers that require it (Apple TV 2nd/3rd gen) are rejected by design. Target: Apple TV, AirPlay 2 TVs, and compatible displays. See [#27](https://github.com/Developer1010x/openplay/issues/27) | | Miracast / Wi-Fi Display | Sender only | Cast to Miracast adapters and smart TVs; Wi-Fi Direct P2P supported on Linux | | OpenPlay (WebRTC) | Sender and receiver, **not yet wired up** | Native protocol between two OpenPlay instances. The signaling, pipeline and discovery libraries are implemented; connecting them to the two binaries is in progress. See [#11](https://github.com/Developer1010x/openplay/issues/11) | diff --git a/crates/openplay-airplay/README.md b/crates/openplay-airplay/README.md index c6758ef..30df31d 100644 --- a/crates/openplay-airplay/README.md +++ b/crates/openplay-airplay/README.md @@ -4,11 +4,13 @@ AirPlay mirroring protocol implementation. Handles the sender-side flow from device discovery through stream setup to per-frame video delivery. > **Interoperability status.** HAP pairing used a fabricated SRP group and could -> never succeed; that is fixed (see `srp.rs`) but is **unconfirmed against physical -> Apple hardware**. `fairplay.rs` uses an invented key derivation and **has no -> callers** — Apple TV 2nd/3rd generation are refused by model string instead. -> Read [docs/crypto.md](../../docs/crypto.md) before debugging a receiver that -> rejects a connection. +> never succeed; that is fixed (see `srp.rs`) and transient pair-setup is +> **confirmed against physical Apple hardware** (issue #27). Mirroring is not: +> `mirror_stream.rs` cannot yet write into the encrypted post-pairing connection +> that `control_channel.rs` provides. `fairplay.rs` uses an invented key +> derivation and **has no callers** — Apple TV 2nd/3rd generation are refused by +> model string instead. Read [docs/crypto.md](../../docs/crypto.md) before +> debugging a receiver that rejects a connection. ## Protocol phases @@ -17,8 +19,8 @@ The AirPlay mirroring handshake proceeds in four phases, all handled by `AirPlay 1. **Feature negotiation** (`http_session.rs`) — GET `/info` to read server capabilities, POST `/stream` with SDP-like parameters. Checks `AirPlayFeatures` for mirroring support. 2. **HAP pairing** (`hap_pairing.rs`) — HomeKit Accessory Protocol pairing over TLV8-encoded HTTP POST requests to `/pair-setup` and `/pair-verify`. Two modes: - - Transient pairing (PIN `3939`) — no stored credentials. - - Full SRP-6a pairing with a 4-digit PIN, followed by Ed25519 signing and X25519 ECDH. Credentials are stored in a SQLite database via `rusqlite`. + - Transient pairing (PIN `3939`) — no stored credentials. Ends at M4 and hands the connection to the encrypted **control channel** (`control_channel.rs`), which frames every later request in ChaCha20-Poly1305. This is the only mode the session uses. + - Full SRP-6a pairing with a 4-digit PIN, followed by Ed25519 signing and X25519 ECDH. Credentials are stored in a SQLite database via `rusqlite`. Reachable from the `pair_probe` example only. 3. **FairPlay** (`fairplay.rs`) — AES-CTR content protection negotiation. @@ -29,6 +31,7 @@ The AirPlay mirroring handshake proceeds in four phases, all handled by `AirPlay | Type | File | Purpose | |---|---|---| | `AirPlaySession` | session.rs | High-level orchestration; emits `SessionEvent::Ready` / `SessionEvent::Ended` | +| `ControlChannel` | control_channel.rs | Post-M4 connection wrapper: HKDF-SHA512 keys, ChaCha20-Poly1305 frames, per-direction counter nonces | | `AirPlayFeatures` | features.rs | Bitfield parsed from `0xHEX` or `0xLO,0xHI` mDNS TXT | | `MirrorHeader` | mirror_header.rs | 128-byte fixed header: payload size (LE u32), packet type, NTP timestamp (BE u64) | | `MirrorStream` | mirror_stream.rs | Writes codec data, video frames, and heartbeats over TCP | diff --git a/crates/openplay-airplay/src/hap_pairing.rs b/crates/openplay-airplay/src/hap_pairing.rs index 6fbfcd0..3103308 100644 --- a/crates/openplay-airplay/src/hap_pairing.rs +++ b/crates/openplay-airplay/src/hap_pairing.rs @@ -7,9 +7,10 @@ //! [`crate::srp`], whose tests re-derive it from RFC 3526's formula and check //! the client against an independent implementation of the server side. //! -//! That removes the known blocker to pairing with real hardware. It has not -//! been confirmed against physical Apple hardware, so treat pairing as -//! untested-in-the-field rather than proven. +//! Transient pair-setup is confirmed against physical Apple hardware: a Mac +//! running AirTunes/950.7.1 completes SRP-6a verification (issue #27). That +//! flow ends at M4 and hands the connection to [`crate::control_channel`]; +//! there is no pair-verify in it. //! //! Note that a receiver requiring FairPlay will still fail later in the //! session, for an unrelated reason — see [`crate::fairplay`]. diff --git a/docs/architecture.md b/docs/architecture.md index f5715bd..3ea43f8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -38,7 +38,7 @@ no consumer in either binary; the WebRTC path that would use them is unwired. |---|---| | `openplay-sender` | Binary. egui UI, receiver list, `casting.rs` orchestration | | `openplay-receiver` | Binary. egui window showing a static "waiting" page | -| `openplay-airplay` | AirPlay: HAP pairing, SRP, NTP, mirror stream, TLV8. Also `fairplay.rs`, which has no callers | +| `openplay-airplay` | AirPlay: HAP pairing, SRP, encrypted control channel, NTP, mirror stream, TLV8. Also `fairplay.rs`, which has no callers | | `openplay-miracast` | Miracast/WFD: RTSP, WFD params, Wi-Fi Direct (Linux) | | `openplay-pipeline` | GStreamer pipeline construction, encoder probing | | `openplay-signaling` | WebSocket signaling client and server. Never constructed | diff --git a/docs/contributing.md b/docs/contributing.md index 8b8dea4..54b379f 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -137,12 +137,14 @@ implementation, do not invent one. Leave it unimplemented with a warning. [airplay-receiver-design.md](airplay-receiver-design.md) - macOS and Windows screen capture backends - AirPlay and Miracast receiver support -- Confirming AirPlay HAP pairing against real hardware — one `pair_probe` run, - see [#27](https://github.com/Developer1010x/openplay/issues/27) +- Confirming AirPlay mirroring end to end. Pairing itself is confirmed + ([#27](https://github.com/Developer1010x/openplay/issues/27)); what is missing + is a mirror stream that can write into the encrypted post-pairing connection, + and a receiver that mirrors without FairPlay to test it against +- Testing against real AirPlay and Miracast hardware — genuinely valuable, since + no test in this repo can substitute +- Packaging (Flatpak, Homebrew, Winget, AUR) **Not** useful: porting Apple's FairPlay key tables. That was considered and declined — see [crypto.md](crypto.md#fairplay--not-fixed). A PR adding them will be closed, so please do not spend time on it. -- Testing against real AirPlay and Miracast hardware — genuinely valuable, since - no test in this repo can substitute -- Packaging (Flatpak, Homebrew, Winget, AUR) diff --git a/docs/crypto.md b/docs/crypto.md index f2f1cd9..558fb2f 100644 --- a/docs/crypto.md +++ b/docs/crypto.md @@ -11,8 +11,9 @@ Original report: issue #8 (closed). Hardware confirmation is tracked in issue #2 | Component | Where | Status | |---|---|---| -| HAP pair-setup (SRP-6a) | `airplay/srp.rs`, `hap_pairing.rs` | **Fixed**, untested against hardware | -| HAP pair-verify | `airplay/hap_pairing.rs` | Implemented, unreachable until pairing is confirmed | +| HAP transient pair-setup (SRP-6a) | `airplay/srp.rs`, `hap_pairing.rs` | **Fixed and confirmed against hardware** — M1–M4 against AirTunes/950.7.1 | +| Encrypted control channel | `airplay/control_channel.rs` | Implemented and confirmed against hardware — an encrypted `GET /info` returns 200. The mirror stream cannot write into it yet, so mirroring is unconfirmed | +| HAP pair-verify | `airplay/hap_pairing.rs` | Implemented, no callers — the transient flow ends at M4 and keys the control channel directly; pair-verify belongs to the PIN flow, which the session path never uses | | FairPlay | `airplay/fairplay.rs` | **Will not be implemented** (decision below), and not wired in — `fp_setup` has no callers | | TLS certificates | `openplay-crypto/certs.rs` | Implemented, never constructed anywhere | | Signaling TLS config | `openplay-crypto/tls.rs` | Implemented, no callers — fingerprint pinning, which is **not** peer authentication | @@ -88,20 +89,64 @@ and continuing would derive a session key an attacker can predict. The private exponent was reduced from 2048 bits to 256. The old code called `random_bigint(256)` against a parameter named `bytes`. -### Caveat: still unconfirmed against hardware - -**This has not been confirmed against physical Apple hardware.** The known -blocker is removed; that is not the same as proven working. No test in this -repository can substitute for a real receiver. - -There is a probe for exactly this: +### Confirmed against hardware + +Transient pair-setup **has been confirmed against physical Apple hardware** +([#27](https://github.com/Developer1010x/openplay/issues/27), 2026-08-26): a +Mac running AirTunes/950.7.1 with AirPlay Receiver set to *Everyone* runs the +SRP exchange to completion and the probe reports +`SRP-6a verification successful`. + +Two facts from that run are worth more than the bare result: + +- **The receiver's public key is 384 bytes** — 3072 bits. A receiver on a + different modulus would not produce a `B` of that width, so the group is now + corroborated from the far side of the wire, not only by the self-checks in + `srp.rs`. +- **It did not work as shipped.** Reaching M4 took two fixes, both real bugs + rather than environment problems. `POST /pair-setup` needs an + `X-Apple-HKP: 4` header, or the receiver answers 400 before reading the body + (#41). And the M1 proof must hash `g`'s minimal encoding — the single byte + `0x05` — not `PAD(g)`; with padding the receiver answers HAP error `2` at M4 + (#42, recovered in #43). The neighbouring `k = H(N | PAD(g))` genuinely does + need padding, which is how the bug got written. + +Transient pairing **ends at M4**. There is no M5/M6, no long-term keys and no +pair-verify. Running M5 anyway makes the receiver close the connection right +after an otherwise successful M4 — which reads exactly like a crypto failure +and is not one. From M4 on the connection is encrypted: HKDF-SHA512 under +`Control-Salt` with the `Control-Write-Encryption-Key` / +`Control-Read-Encryption-Key` info strings, then ChaCha20-Poly1305 frames with +a 2-byte little-endian length as AEAD associated data and a per-direction +64-bit counter nonce. `control_channel.rs` implements that, and an encrypted +`GET /info` over it returns `HTTP/1.1 200 OK`. + +**A trap when re-testing.** The receiver backs off hard after a failed +pair-setup, answering HAP error `0x03` with a retry delay. Attempts closer +together than roughly two minutes return backoff rather than a real answer, and +backoff looks nothing like an authentication failure. Treat any `0x03` as "no +result", wait, and re-run. + +**What this does not establish: mirroring.** `MirrorStream` writes NAL units +to a raw `TcpStream`, but every byte after M4 must be wrapped in +control-channel frames, so `negotiate_with_auth` deliberately stops after +`POST /stream` with an explicit error rather than emit plaintext into an +encrypted connection. Making the mirror stream encryption-aware is the +remaining work. Confirming it end to end also needs a receiver that mirrors +without FairPlay: a modern Mac gates mirroring behind it (`/fp-setup` answers +400, RTSP `SETUP` 455, `/stream` 404), and FairPlay stays out of scope by the +decision below. A software receiver such as `uxplay` is probably the cheapest +way to get one. + +The probes that produced all of this: ```console -cargo run -p openplay-airplay --example pair_probe -- :7000 # transient +cargo run -p openplay-airplay --example pair_probe -- :7000 # transient, stops after M4 cargo run -p openplay-airplay --example pair_probe -- :7000 1234 # with PIN +cargo run -p openplay-airplay --example control_probe -- :7000 # M4, then the encrypted channel ``` -### What a real attempt actually produced +### What the first attempt produced Run against a MacBook Air (`Mac16,12`, AirTunes/950.7.1), `GET /info` succeeded — 1157-byte plist, features `0x38174FDE4A7FCFD5`, mirroring, video and audio all @@ -117,14 +162,14 @@ same Apple ID, and it is enforced before any crypto runs. To test the SRP path the receiver must be set to "Anyone on the same network" in **System Settings → General → AirDrop & Handoff → AirPlay Receiver**. -So the SRP question is still open. The attempt was not wasted, though: a 403 was -being reported as `Missing state TLV`, because `recv_response` never looked at -the HTTP status line and an empty body failed TLV8 decoding. That is precisely -the misleading-diagnostic problem this issue was filed about, one layer up. It -now reports the status and names the setting to change (`check_http_status` in -`hap_pairing.rs`, with four tests). +That attempt was not wasted: a 403 was being reported as `Missing state TLV`, +because `recv_response` never looked at the HTTP status line and an empty body +failed TLV8 decoding. That is precisely the misleading-diagnostic problem this +issue was filed about, one layer up. It now reports the status and names the +setting to change (`check_http_status` in `hap_pairing.rs`, with four tests). -If you get further against real hardware, please add the result to issue #27 +With the setting changed, the second attempt is the one described above. If you +get a different result against other hardware, please add it to issue #27 either way. ## FairPlay — not fixed diff --git a/docs/protocols.md b/docs/protocols.md index 89a88ad..73d4eae 100644 --- a/docs/protocols.md +++ b/docs/protocols.md @@ -9,7 +9,7 @@ audio, even where the protocol layer advertises it — see | Protocol | Discovery | Session setup | Transport | State | |---|---|---|---|---| | Miracast | mDNS + Wi-Fi Direct | RTSP M1–M7 | RTP/MPEG2-TS over UDP | Works | -| AirPlay | mDNS `_airplay._tcp` | HTTP/plist + HAP | Mirror stream (TCP) | Pairing unconfirmed; FairPlay not wired in | +| AirPlay | mDNS `_airplay._tcp` | HTTP/plist + HAP | Mirror stream (TCP) | Pairing confirmed against hardware; mirroring unconfirmed; FairPlay not wired in | | OpenPlay | mDNS `_openplay._tcp` | WebSocket signaling | WebRTC | Sender browses; everything else is libraries only | --- @@ -85,10 +85,13 @@ flow at all** — see [crypto.md](crypto.md). `POST /stream` - if that fails with **501 or 403**, falls back to `negotiate_with_auth`: `GET /info` to identify the model, then HAP **transient** pair-setup - followed by pair-verify (`hap_pairing.rs`), then `POST /stream` on the - verified connection - - wraps the resulting connection in a `MirrorStream` and starts a 2-second - heartbeat + (`hap_pairing.rs`), which ends at M4 — there is no pair-verify — then + wraps the same connection in the encrypted control channel + (`control_channel.rs`) and sends `POST /stream` through it. The path + currently **stops there with an explicit error**: `MirrorStream` writes + to a raw socket and cannot yet frame video for the encrypted connection + - on the unauthenticated path, wraps the resulting connection in a + `MirrorStream` and starts a 2-second heartbeat 4. `AirPlaySenderPipeline` captures and encodes, emitting H.264 NAL units to an appsink 5. The casting loop copies the SPS/PPS out of the first frame and sends them once @@ -121,14 +124,21 @@ frames video, codec data and heartbeats onto it. ### Pairing modes - `pair_setup_transient(addr)` — no PIN. Used when the receiver is set to - "Everyone on the Same Network". Sends flags `0x02` and uses the standard - transient PIN `3939`. **This is the only mode the session path uses.** -- `pair_setup(addr, pin)` — first-time pairing with a 4-digit PIN. Reachable via - the `pair_probe` example, not from the session flow. -- `pair_verify(...)` — subsequent connections, using stored Ed25519 keys. - -Ed25519 signing and ChaCha20-Poly1305 are used in pair-setup M5/M6; X25519 ECDH -is used separately in `pair_verify`. + "Everyone on the Same Network". Sends `X-Apple-HKP: 4`, flags `0x02` (which + the measured receiver ignores) and the standard transient PIN `3939`, and + **ends at M4**: no long-term keys, nothing persisted, no pair-verify. It hands + back the connection together with the SRP session key, because from here on + the receiver only accepts control-channel frames on it. **This is the only + mode the session path uses**, and it is confirmed against hardware + ([#27](https://github.com/Developer1010x/openplay/issues/27)). +- `pair_setup(addr, pin)` — first-time pairing with a 4-digit PIN, running + M1–M6. Reachable via the `pair_probe` example, not from the session flow. +- `pair_verify(...)` — subsequent connections, using stored Ed25519 keys. No + callers. + +Ed25519 signing and ChaCha20-Poly1305 are used in pair-setup M5/M6, which only +the PIN flow reaches; X25519 ECDH is used separately in `pair_verify`. +ChaCha20-Poly1305 also frames the control channel after transient M4. `hap_pairing.rs` provides SQLite helpers for paired devices (`init_paired_db`, `store_paired_device`, `load_paired_device`), but nothing outside the module's diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 7cfd315..debe25c 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -132,9 +132,26 @@ the device and OpenPlay does not, that is a bug worth reporting. ### Rejected during pairing Pairing previously could never succeed — the SRP group was fabricated. That is -fixed, but **has not been confirmed against physical hardware**. If you hit a -pairing failure, `RUST_LOG=openplay_airplay=debug` will show which message it -died on, and that result is worth adding to issue #27 either way. +fixed, and transient pair-setup is **confirmed against a Mac running +AirTunes/950.7.1** (issue #27). If you hit a pairing failure anyway, +`RUST_LOG=openplay_airplay=debug` will show which message it died on, and that +result is worth adding to #27 either way. + +Two failures are not what they look like: + +- **HAP error `0x03` with a retry delay.** The receiver backs off hard after a + failed pair-setup and keeps answering with backoff for roughly two minutes. + It is not an authentication failure and not a result. Wait, then re-run. +- **The connection closes right after a successful M4.** Transient pairing ends + at M4. Anything sent in plaintext after it — an M5 included — makes the + receiver drop the connection. + +### "mirror stream cannot yet send video over an encrypted connection" + +Pairing and the encrypted control channel both succeeded, and the cast stopped +because the mirror stream cannot yet frame video for the encrypted post-pairing +connection. This is the current end of the authenticated path, not a network +problem. See [crypto.md](crypto.md#confirmed-against-hardware). ### "requires FairPlay authentication which is not supported"