Skip to content

Widen FPSAP exchange coverage to 151 vectors - #22

Closed
objevovat wants to merge 1 commit into
omarroth:mainfrom
objevovat:fpsap-exchange-corpus
Closed

Widen FPSAP exchange coverage to 151 vectors#22
objevovat wants to merge 1 commit into
omarroth:mainfrom
objevovat:fpsap-exchange-corpus

Conversation

@objevovat

@objevovat objevovat commented Jul 31, 2026

Copy link
Copy Markdown

What this adds

One test: a 151-payload corpus for the FPSAP exchange, next to the seven vectors in TestFPSAPExchangeGoldenVectors.

It follows TestFairPlaySAPHashCorpus — deterministic payload generator, one aggregate SHA-256 — so it costs a single constant rather than a fixture file. No production code is touched.

Where the expected value came from

Not from this package. I derived an implementation of the same exchange independently, by partial evaluation of an ARM64 execution trace, and 68f4a4d3… is what that produces. Two separate reverse engineerings landing on the same 151 outputs.

One thing worth stating plainly: I have since retired that implementation in favour of your closed form — yours was smaller and I'd rather maintain one copy — so my tree now carries fairplay_sap.go and fairplay_md5.go derived from internal/airplay. The constant predates that swap and I've confirmed it is unchanged across it, but I can no longer re-derive it independently. Take it as a snapshot of agreement from when the two were genuinely separate.

Your seven existing vectors were what I used to validate my implementation when I built it. They all passed, while the two were still independent.

What it covers

  • one byte set on and around every 16-byte block boundary (offsets 0, 1, 2, 15, 16, 31, 32, 63, 64, 95, 96, 126, 127)
  • one bit set in each of the eight blocks
  • all-ones with a single byte cleared
  • solid fills and counter ramps
  • 32 pseudorandom payloads, from the same xorshift generator the SAP-hash corpus uses

It fails when it should

Perturbing the fpsapDigest32 IV by one changes the aggregate:

FPSAP exchange corpus = 9e497716..., want 68f4a4d3...

Restoring it passes. go test ./internal/airplay/ is green.

While I was in here I also fuzzed the receiver-facing parsing — exchangeM3, confirmM4, decryptFPSAPBody — for 10.3 million executions across the three, no crashes. Nothing to fix; just closing the loop in case it's useful to know.

TestFPSAPExchangeGoldenVectors covers seven payloads. This adds a corpus
test alongside it, built the same way as TestFairPlaySAPHashCorpus: a
deterministic payload generator and one aggregate SHA-256, so it costs
one constant rather than a fixture file.

The payloads are structural first -- one byte set on and around every
16-byte block boundary, one bit set in each of the eight blocks, all-ones
with a single byte cleared -- then a pseudorandom tail from the same
xorshift generator the SAP-hash corpus uses.

The expected aggregate was produced by a separate implementation of this
exchange, derived independently from Apple's binary, so the test compares
the two against each other rather than comparing this package with
itself. Both agree on all 151 payloads.

Verified to fail when it should: perturbing the fpsapDigest32 IV by one
changes the aggregate.
@omarroth

omarroth commented Aug 3, 2026

Copy link
Copy Markdown
Owner

I'm not quite sure I understand this change. It adds additional tests, but the core implementation is still correct?

@objevovat
objevovat force-pushed the fpsap-exchange-corpus branch from 13e1178 to 53b44d2 Compare August 4, 2026 21:06
@objevovat

objevovat commented Aug 6, 2026

Copy link
Copy Markdown
Author

Correct — implementation's fine, this is test-only. All 151 pass against main untouched.

Since I opened it: this exchange path is now confirmed on real hardware. Three HomePods (AudioAccessory5,1 ×2, AudioAccessory6,1, fw 23L471) accept the response and reject a single flipped bit in the same session. Shairport Sync 200 OKs a corrupted response, so acceptance alone means nothing — the refusal is the evidence. Three headers were needed to get there, none documented: POST /pair-pin-start before pair-setup, X-Apple-HKP: 4, and X-Apple-ET: 32 on /fp-setup. Without the last one you get a v2.5 record and everything is refused. Doesn't work on Apple TVs or Macs — they refuse before evaluating anything. Details, happy to PR any of it separately.

Why you'd want the corpus: TestFPSAPExchangeGoldenVectors pins 7 payloads. Nothing currently covers block boundaries, single-bit positions, or the fpsapDigest32 IV — I perturbed it by one and the existing seven still pass. This catches it. One const, no fixture file, 15s of CI.

One correction to my original description: I've since retired my own implementation for your closed form, so this is a self-consistency guard on your code, not two independent derivations. Weaker claim than I made.

Your call entirely — it's test surface on a package you maintain, and no is a fine answer.

@objevovat objevovat closed this Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants