From 53b44d2fffb59fe52fff5500a789a3337019c517 Mon Sep 17 00:00:00 2001 From: objevovat Date: Fri, 31 Jul 2026 01:27:34 -0700 Subject: [PATCH] Widen FPSAP exchange coverage to 151 vectors 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. --- internal/airplay/fpsap_corpus_test.go | 95 +++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 internal/airplay/fpsap_corpus_test.go diff --git a/internal/airplay/fpsap_corpus_test.go b/internal/airplay/fpsap_corpus_test.go new file mode 100644 index 0000000..bb8abf7 --- /dev/null +++ b/internal/airplay/fpsap_corpus_test.go @@ -0,0 +1,95 @@ +package airplay + +import ( + "crypto/sha256" + "testing" +) + +// fpsapCorpusPayloads builds a deterministic set of 128-byte challenge bodies: +// structural edge cases first -- block boundaries, single bits, all-ones with +// one byte cleared -- then a pseudorandom tail from the same xorshift generator +// TestFairPlaySAPHashCorpus uses. +func fpsapCorpusPayloads() [][128]byte { + var out [][128]byte + add := func(p [128]byte) { out = append(out, p) } + + var zero [128]byte + add(zero) + + for _, v := range []byte{0x01, 0x42, 0x55, 0x7f, 0x80, 0xaa, 0xff} { + var p [128]byte + for i := range p { + p[i] = v + } + add(p) + } + + for _, s := range []byte{0x00, 0x40, 0x80, 0xc0} { + var p [128]byte + for i := range p { + p[i] = s + byte(i) + } + add(p) + } + + // One byte set, on and around every 16-byte block boundary. + for _, idx := range []int{0, 1, 2, 15, 16, 31, 32, 63, 64, 95, 96, 126, 127} { + for _, v := range []byte{0x01, 0x80, 0xff} { + var p [128]byte + p[idx] = v + add(p) + } + } + + // One bit set in each of the eight blocks. + for blk := 0; blk < 8; blk++ { + for bit := 0; bit < 8; bit++ { + var p [128]byte + p[blk*16] = 1 << bit + add(p) + } + } + + for _, idx := range []int{0, 63, 64, 127} { + var p [128]byte + for i := range p { + p[i] = 0xff + } + p[idx] = 0 + add(p) + } + + var state uint64 = 0x6a09e667f3bcc909 + for range 32 { + var p [128]byte + for i := range p { + state ^= state << 13 + state ^= state >> 7 + state ^= state << 17 + p[i] = byte(state) + } + add(p) + } + + return out +} + +// TestFPSAPExchangeCorpus widens the exchange coverage from the seven vectors in +// TestFPSAPExchangeGoldenVectors to 151. +// +// The expected aggregate was produced by a separate, independently derived +// implementation of this exchange, not by this package -- so it checks the two +// against each other rather than checking this package against itself. +func TestFPSAPExchangeCorpus(t *testing.T) { + corpusHash := sha256.New() + payloads := fpsapCorpusPayloads() + for _, payload := range payloads { + digest := fpsapExchangeForSAP(fpsapReferenceLocalSAP(), mustDecryptFPSAPBody(t, 3, payload)) + _, _ = corpusHash.Write(digest[:]) + } + if len(payloads) != 151 { + t.Fatalf("corpus length = %d, want 151", len(payloads)) + } + requireFairPlayHex(t, "FPSAP exchange corpus", corpusHash.Sum(nil), + "68f4a4d3ad5603fc569ee7deb013d3a5de91d7590274272a16750fe02370fd9d") +}