fix(airplay): send X-Apple-HKP so pair-setup gets past 400 - #41
Merged
Conversation
With AirPlay Receiver set to Everyone and no password, `POST /pair-setup`
returned 400 Bad Request with an empty body — before the receiver looked at
the TLV8 payload at all. `GET /info` succeeded, so this was not access
control.
Isolated by replaying the request against a Mac running AirTunes/950.7.1,
changing one thing at a time:
no Host, FLAGS=0x02 u8 (what this code sent) 400
+ Host 400
+ Host, FLAGS=0x10 u32 400
+ Host, X-Apple-HKP: 4, FLAGS=0x10 u32 200, body 409B
+ Host, X-Apple-HKP: 4, no FLAGS 200, body 409B
no Host, X-Apple-HKP: 4, FLAGS=0x10 u32 200, body 409B
`X-Apple-HKP` is the only variable that matters: present, the receiver
answers; absent, 400 every time. It selects the pairing flow, so without it
the endpoint rejects the request outright. `Host` is irrelevant here despite
HTTP/1.1 requiring it, and the receiver ignores the `FLAGS` TLV completely.
The 409-byte M2 decodes as state + 16-byte salt + 384-byte public key. 384
bytes is 3072 bits, which is independent confirmation that the receiver uses
the same RFC 5054 group #23 installed.
`pair_probe` now gets a real M2 ("server_pk_len=384 salt_len=16") and fails
later, at M4, with HAP error 2 (authentication) — the receiver rejecting our
SRP proof. That is a different bug and is not addressed here; see the issue.
Note when re-testing: the receiver backs off hard after a failed pair-setup
and answers with error 0x03 and a retry delay. Attempts less than a couple of
minutes apart return backoff rather than a real answer, which will silently
mislead anyone bisecting this.
Developer1010x
pushed a commit
that referenced
this pull request
Aug 26, 2026
…43) #42 was merged twelve seconds after #41, into `fix/airplay-hkp-header` — which #41 had just merged into master and left behind. The merge succeeded, so nothing looked wrong, but the commits landed on a branch nothing points at and master never received them. Master therefore has the `X-Apple-HKP` header from #41 and none of what it was a prerequisite for: the SRP proof still hashes g padded, transient pairing still runs M5/M6 and gets the connection closed, and there is no encrypted control channel. Pairing is broken on master in exactly the way #42 fixed. This restores #42's own diff — the eight `openplay-airplay` files it actually touched — on top of current master. Deliberately *not* a merge of `fix/airplay-hkp-header`. That branch was cut before #35, #36, #37, #39 and #40 merged, so a diff against it reads as deleting `openplay-discovery/src/address.rs` and reverting 244 lines of `openplay-sender/src/app.rs`. Merging it would silently undo five landed fixes. Only the range between #42 and its own parent is safe to replay, and that range touches nothing outside `openplay-airplay`. Verified after the replay: 233 tests pass, clippy and fmt clean, and the work from #35/#36/#37/#40 is still in the tree. Co-authored-by: Sandeepa Nadahalli <1698507+snadahalli@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
With AirPlay Receiver set to Everyone and Require Password off,
POST /pair-setupreturned400 Bad Requestwith an empty body — before the receiver looked at the TLV8 payload at all.GET /infosucceeded against the same host, so this was not access control.This is the third distinct wall on the way to #27, after
403(AirPlay Receiver set to "Current User") and470(Require Password on).Isolating it
Replayed the request against a Mac running AirTunes/950.7.1, changing one variable at a time:
FLAGS=0x02u8 — what this code sentHostHost,FLAGS=0x10u32Host,X-Apple-HKP: 4,FLAGS=0x10u32Host,X-Apple-HKP: 4, noFLAGSX-Apple-HKP: 4,FLAGS=0x10u32X-Apple-HKPis the only variable that matters. It selects the pairing flow, so without it the endpoint rejects the request outright.Two theories this disproves, both of which looked compelling:
Hostheader. HTTP/1.1 requires it and RFC 7230 §5.4 says a server MUST answer 400 without it — but adding it changes nothing, and the request succeeds without it onceX-Apple-HKPis present.FLAGSvalue. The code sends0x02in one byte where HAP defineskPairingFlag_Transientas0x00000010in a uint32. The receiver ignores the TLV completely —0x02, a correct0x10, and omitting it entirely all produce an identical M2.Since the flag demonstrably does not matter, it is left as-is rather than changed on spec-reading alone; the comment now records the measurement instead of the previous incorrect claim that
0x02is the transient flag.Bonus confirmation for #23
The 409-byte M2 decodes as state + 16-byte salt + 384-byte public key. 384 bytes is 3072 bits — independent confirmation that the receiver uses the same RFC 5054 group that #23 installed, from the receiver's own wire output rather than from our tests.
What this does not fix
pair_probenow reaches a genuine M2:So the SRP exchange finally happens, and fails at M4 with the receiver rejecting our client proof. That is a separate bug in the proof computation or the transient password, and is not addressed here.
I probed two candidate divergences (
H(g)overPAD(g)vs. minimal encoding, andK = H(S)padded vs. not); both reachable variants still returned error 2, and the rest were masked — see below.Warning for whoever picks up M4
The receiver backs off hard after a failed pair-setup, answering error
0x03with a retry delay. Attempts less than a couple of minutes apart return backoff rather than a real answer, and backoff looks nothing like an authentication failure — this silently invalidated a first round of my own results before I noticed. Space attempts generously when bisecting.Verification
fmt --checkclean,clippy --all-targets --all-features -D warningsclean,cargo test --all216 passed / 0 failed, plus the wire captures above against real hardware.