@@ -28,6 +28,33 @@ func mustHex(s string) []byte {
2828 return b
2929}
3030
31+ func newTestPQProxy (epoch uint64 ) * Proxy {
32+ monitor := newNetworkMonitor ()
33+ monitor .epochValue .Store (epoch )
34+ proxy := & Proxy {netMonitor : monitor }
35+ proxy .questionSizeEstimator = NewQuestionSizeEstimator ()
36+ return proxy
37+ }
38+
39+ func newTestPQServerInfo () * ServerInfo {
40+ _ , pk := xwing .DeriveKeyPair (iotaBytes (32 , 0x20 ))
41+ pkb , _ := pk .MarshalBinary ()
42+ return & ServerInfo {
43+ Name : "test" ,
44+ CryptoConstruction : XWingPQ ,
45+ PqPublicKey : pkb ,
46+ PqCertContext : iotaBytes (48 , 0x01 ),
47+ MagicQuery : [8 ]byte {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 },
48+ pqSession : newPqSessionState (XWingPQ ),
49+ }
50+ }
51+
52+ // maxStorableTicketLen is the largest resumption ticket pqProcessControl
53+ // accepts; one byte more and a resumed query cannot fit a UDP datagram.
54+ func maxStorableTicketLen () int {
55+ return MaxDNSUDPPacketSize - PQResumedPaddingFloor - pqResumedOverhead (0 )
56+ }
57+
3158// TestPQAppendix3Vectors checks the client's PQ crypto against the pinned
3259// values of Appendix 3 of the draft, the same anchor the server validates.
3360func TestPQAppendix3Vectors (t * testing.T ) {
@@ -147,20 +174,8 @@ func TestPQSessionResumptionEpoch(t *testing.T) {
147174// single X-Wing key exchange instead of running a fresh one every time, and
148175// that a network change forces a new one.
149176func TestEncryptPQReusesEncapsulation (t * testing.T ) {
150- monitor := newNetworkMonitor ()
151- monitor .epochValue .Store (1 )
152- proxy := & Proxy {netMonitor : monitor }
153-
154- _ , pk := xwing .DeriveKeyPair (iotaBytes (32 , 0x20 ))
155- pkb , _ := pk .MarshalBinary ()
156- serverInfo := & ServerInfo {
157- Name : "test" ,
158- CryptoConstruction : XWingPQ ,
159- PqPublicKey : pkb ,
160- PqCertContext : iotaBytes (48 , 0x01 ),
161- MagicQuery : [8 ]byte {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 },
162- pqSession : newPqSessionState (XWingPQ ),
163- }
177+ proxy := newTestPQProxy (1 )
178+ serverInfo := newTestPQServerInfo ()
164179 query := mustHex ("12340100000100000000000003777777076578616d706c6503636f6d0000010001" )
165180
166181 ctOf := func (out []byte ) []byte {
@@ -182,7 +197,7 @@ func TestEncryptPQReusesEncapsulation(t *testing.T) {
182197 t .Fatal ("expected the shared key to be reused across queries" )
183198 }
184199
185- monitor .epochValue .Store (2 )
200+ proxy . netMonitor .epochValue .Store (2 )
186201 _ , out3 , _ , _ , err := proxy .encryptPQ (serverInfo , query , "udp" )
187202 if err != nil {
188203 t .Fatal (err )
@@ -192,14 +207,54 @@ func TestEncryptPQReusesEncapsulation(t *testing.T) {
192207 }
193208}
194209
195- func TestPQProcessControlDiscardsTicketAfterNetworkChange (t * testing.T ) {
196- monitor := newNetworkMonitor ()
197- monitor .epochValue .Store (2 )
198- proxy := & Proxy {netMonitor : monitor }
199- serverInfo := & ServerInfo {
200- Name : "test" ,
201- pqSession : newPqSessionState (XWingPQ ),
210+ // TestPQResumedQueryMeetsUDPSizeTarget checks that a resumed query sent over
211+ // UDP is padded up to the regular question size target, since the resolver
212+ // cannot send a UDP response larger than the query. Servers that cannot
213+ // receive fragmented datagrams keep the classic reduced cap, and over TCP
214+ // the smaller fixed floor is kept.
215+ func TestPQResumedQueryMeetsUDPSizeTarget (t * testing.T ) {
216+ proxy := newTestPQProxy (1 )
217+ serverInfo := newTestPQServerInfo ()
218+ ticket := iotaBytes (130 , 0x24 )
219+ serverInfo .pqSession .store (ticket , [32 ]byte {42 }, time .Now ().Add (time .Minute ), 1 )
220+ query := mustHex ("12340100000100000000000003777777076578616d706c6503636f6d0000010001" )
221+ overhead := pqResumedOverhead (len (ticket ))
222+
223+ _ , udpOut , _ , _ , err := proxy .encryptPQ (serverInfo , query , "udp" )
224+ if err != nil {
225+ t .Fatal (err )
226+ }
227+ if len (udpOut ) < InitialMinQuestionSize {
228+ t .Fatalf ("resumed UDP query is only %d bytes, below the %d response budget" , len (udpOut ), InitialMinQuestionSize )
229+ }
230+ if plaintextLen := len (udpOut ) - overhead ; plaintextLen % 64 != 0 {
231+ t .Fatalf ("resumed UDP query padding is not a multiple of 64: %d" , plaintextLen )
232+ }
233+
234+ _ , tcpOut , _ , _ , err := proxy .encryptPQ (serverInfo , query , "tcp" )
235+ if err != nil {
236+ t .Fatal (err )
202237 }
238+ if expected := overhead + PQResumedPaddingFloor ; len (tcpOut ) != expected {
239+ t .Fatalf ("resumed TCP query length %d, expected %d" , len (tcpOut ), expected )
240+ }
241+
242+ serverInfo .knownBugs .fragmentsBlocked = true
243+ for i := 0 ; i < 3 ; i ++ {
244+ proxy .questionSizeEstimator .blindAdjust ()
245+ }
246+ _ , safeOut , _ , _ , err := proxy .encryptPQ (serverInfo , query , "udp" )
247+ if err != nil {
248+ t .Fatal (err )
249+ }
250+ if len (safeOut ) > MaxDNSUDPSafePacketSize {
251+ t .Fatalf ("resumed UDP query is %d bytes despite the fragmentation workaround" , len (safeOut ))
252+ }
253+ }
254+
255+ func TestPQProcessControlDiscardsTicketAfterNetworkChange (t * testing.T ) {
256+ proxy := newTestPQProxy (2 )
257+ serverInfo := newTestPQServerInfo ()
203258 sharedKey := & [32 ]byte {1 , 2 , 3 }
204259 clientNonce := []byte ("abcdefghijkl" )
205260 control := buildTestPQControl ([]byte ("ticket" ), 60 )
@@ -215,6 +270,41 @@ func TestPQProcessControlDiscardsTicketAfterNetworkChange(t *testing.T) {
215270 }
216271}
217272
273+ func TestPQQueryTooLargeForUDPFallsBackToTCP (t * testing.T ) {
274+ proxy := newTestPQProxy (1 )
275+ serverInfo := newTestPQServerInfo ()
276+
277+ hugeQuery := iotaBytes (3000 , 0x02 )
278+ if _ , _ , _ , _ , err := proxy .Encrypt (serverInfo , hugeQuery , "udp" ); err == nil {
279+ t .Fatal ("expected an error for a full query that cannot fit in a UDP datagram" )
280+ }
281+ if _ , _ , _ , _ , err := proxy .Encrypt (serverInfo , hugeQuery , "tcp" ); err != nil {
282+ t .Fatal (err )
283+ }
284+
285+ ticket := iotaBytes (maxStorableTicketLen (), 0x01 )
286+ serverInfo .pqSession .store (ticket , [32 ]byte {42 }, time .Now ().Add (time .Minute ), 1 )
287+ query := iotaBytes (300 , 0x02 )
288+ if _ , _ , _ , _ , err := proxy .Encrypt (serverInfo , query , "udp" ); err == nil {
289+ t .Fatal ("expected an error for a resumed query that cannot fit in a UDP datagram" )
290+ }
291+ if _ , _ , _ , _ , err := proxy .Encrypt (serverInfo , query , "tcp" ); err != nil {
292+ t .Fatal (err )
293+ }
294+ }
295+
296+ func TestPQProcessControlDiscardsOversizedTicket (t * testing.T ) {
297+ proxy := newTestPQProxy (1 )
298+ serverInfo := newTestPQServerInfo ()
299+ oversized := iotaBytes (maxStorableTicketLen ()+ 1 , 0x01 )
300+ control := buildTestPQControl (oversized , 60 )
301+
302+ proxy .pqProcessControl (serverInfo , & [32 ]byte {1 }, []byte ("abcdefghijkl" ), control , 1 )
303+ if _ , _ , ok := serverInfo .pqSession .get (1 ); ok {
304+ t .Fatal ("expected an oversized ticket to be discarded" )
305+ }
306+ }
307+
218308func buildTestPQControl (ticket []byte , lifetime uint32 ) []byte {
219309 control := append ([]byte {}, PQControlMagic [:]... )
220310 control = append (control , PQControlVersion )
0 commit comments