|
|
@@ -3,8 +3,10 @@ package fake_test
|
|
3
|
3
|
import (
|
|
4
|
4
|
"bytes"
|
|
5
|
5
|
"encoding/binary"
|
|
|
6
|
+ "encoding/json"
|
|
6
|
7
|
"errors"
|
|
7
|
8
|
"io"
|
|
|
9
|
+ "os"
|
|
8
|
10
|
"testing"
|
|
9
|
11
|
"time"
|
|
10
|
12
|
|
|
|
@@ -393,3 +395,273 @@ func TestParseClientHelloSNI(t *testing.T) {
|
|
393
|
395
|
t.Parallel()
|
|
394
|
396
|
suite.Run(t, &ParseClientHelloSNITestSuite{})
|
|
395
|
397
|
}
|
|
|
398
|
+
|
|
|
399
|
+// fragmentTLSRecord splits a single TLS record into n TLS records by
|
|
|
400
|
+// dividing the payload into roughly equal parts. Each part gets its own
|
|
|
401
|
+// TLS record header with the same record type and version.
|
|
|
402
|
+func fragmentTLSRecord(t testing.TB, full []byte, n int) []byte {
|
|
|
403
|
+ t.Helper()
|
|
|
404
|
+
|
|
|
405
|
+ recordType := full[0]
|
|
|
406
|
+ version := full[1:3]
|
|
|
407
|
+ payload := full[tls.SizeHeader:]
|
|
|
408
|
+
|
|
|
409
|
+ chunkSize := len(payload) / n
|
|
|
410
|
+ result := &bytes.Buffer{}
|
|
|
411
|
+
|
|
|
412
|
+ for i := 0; i < n; i++ {
|
|
|
413
|
+ start := i * chunkSize
|
|
|
414
|
+ end := start + chunkSize
|
|
|
415
|
+
|
|
|
416
|
+ if i == n-1 {
|
|
|
417
|
+ end = len(payload)
|
|
|
418
|
+ }
|
|
|
419
|
+
|
|
|
420
|
+ chunk := payload[start:end]
|
|
|
421
|
+ result.WriteByte(recordType)
|
|
|
422
|
+ result.Write(version)
|
|
|
423
|
+ require.NoError(t, binary.Write(result, binary.BigEndian, uint16(len(chunk))))
|
|
|
424
|
+ result.Write(chunk)
|
|
|
425
|
+ }
|
|
|
426
|
+
|
|
|
427
|
+ return result.Bytes()
|
|
|
428
|
+}
|
|
|
429
|
+
|
|
|
430
|
+// splitPayloadAt creates two TLS records from a single record by splitting
|
|
|
431
|
+// the payload at the given byte position.
|
|
|
432
|
+func splitPayloadAt(t testing.TB, full []byte, pos int) []byte {
|
|
|
433
|
+ t.Helper()
|
|
|
434
|
+
|
|
|
435
|
+ payload := full[tls.SizeHeader:]
|
|
|
436
|
+ buf := &bytes.Buffer{}
|
|
|
437
|
+
|
|
|
438
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
439
|
+ buf.Write(full[1:3])
|
|
|
440
|
+ require.NoError(t, binary.Write(buf, binary.BigEndian, uint16(pos)))
|
|
|
441
|
+ buf.Write(payload[:pos])
|
|
|
442
|
+
|
|
|
443
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
444
|
+ buf.Write(full[1:3])
|
|
|
445
|
+ require.NoError(t, binary.Write(buf, binary.BigEndian, uint16(len(payload)-pos)))
|
|
|
446
|
+ buf.Write(payload[pos:])
|
|
|
447
|
+
|
|
|
448
|
+ return buf.Bytes()
|
|
|
449
|
+}
|
|
|
450
|
+
|
|
|
451
|
+type ParseClientHelloFragmentedTestSuite struct {
|
|
|
452
|
+ suite.Suite
|
|
|
453
|
+
|
|
|
454
|
+ secret mtglib.Secret
|
|
|
455
|
+ snapshot *clientHelloSnapshot
|
|
|
456
|
+}
|
|
|
457
|
+
|
|
|
458
|
+func (s *ParseClientHelloFragmentedTestSuite) SetupSuite() {
|
|
|
459
|
+ parsed, err := mtglib.ParseSecret(
|
|
|
460
|
+ "ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d",
|
|
|
461
|
+ )
|
|
|
462
|
+ require.NoError(s.T(), err)
|
|
|
463
|
+
|
|
|
464
|
+ s.secret = parsed
|
|
|
465
|
+
|
|
|
466
|
+ fileData, err := os.ReadFile("testdata/client-hello-ok-19dfe38384b9884b.json")
|
|
|
467
|
+ require.NoError(s.T(), err)
|
|
|
468
|
+
|
|
|
469
|
+ s.snapshot = &clientHelloSnapshot{}
|
|
|
470
|
+ require.NoError(s.T(), json.Unmarshal(fileData, s.snapshot))
|
|
|
471
|
+}
|
|
|
472
|
+
|
|
|
473
|
+func (s *ParseClientHelloFragmentedTestSuite) makeConn(data []byte) *parseClientHelloConnMock {
|
|
|
474
|
+ readBuf := &bytes.Buffer{}
|
|
|
475
|
+ readBuf.Write(data)
|
|
|
476
|
+
|
|
|
477
|
+ connMock := &parseClientHelloConnMock{
|
|
|
478
|
+ readBuf: readBuf,
|
|
|
479
|
+ }
|
|
|
480
|
+
|
|
|
481
|
+ connMock.
|
|
|
482
|
+ On("SetReadDeadline", mock.AnythingOfType("time.Time")).
|
|
|
483
|
+ Twice().
|
|
|
484
|
+ Return(nil)
|
|
|
485
|
+
|
|
|
486
|
+ return connMock
|
|
|
487
|
+}
|
|
|
488
|
+
|
|
|
489
|
+func (s *ParseClientHelloFragmentedTestSuite) TestReassemblySuccess() {
|
|
|
490
|
+ full := s.snapshot.GetFull()
|
|
|
491
|
+
|
|
|
492
|
+ tests := []struct {
|
|
|
493
|
+ name string
|
|
|
494
|
+ data []byte
|
|
|
495
|
+ }{
|
|
|
496
|
+ {"two equal fragments", fragmentTLSRecord(s.T(), full, 2)},
|
|
|
497
|
+ {"three equal fragments", fragmentTLSRecord(s.T(), full, 3)},
|
|
|
498
|
+ {"single byte first fragment", splitPayloadAt(s.T(), full, 1)},
|
|
|
499
|
+ {"three byte first fragment", splitPayloadAt(s.T(), full, 3)},
|
|
|
500
|
+ }
|
|
|
501
|
+
|
|
|
502
|
+ for _, tt := range tests {
|
|
|
503
|
+ s.Run(tt.name, func() {
|
|
|
504
|
+ connMock := s.makeConn(tt.data)
|
|
|
505
|
+ defer connMock.AssertExpectations(s.T())
|
|
|
506
|
+
|
|
|
507
|
+ hello, err := fake.ReadClientHello(
|
|
|
508
|
+ connMock,
|
|
|
509
|
+ s.secret.Key[:],
|
|
|
510
|
+ s.secret.Host,
|
|
|
511
|
+ TolerateTime,
|
|
|
512
|
+ )
|
|
|
513
|
+ s.Require().NoError(err)
|
|
|
514
|
+
|
|
|
515
|
+ s.Equal(s.snapshot.GetRandom(), hello.Random[:])
|
|
|
516
|
+ s.Equal(s.snapshot.GetSessionID(), hello.SessionID)
|
|
|
517
|
+ s.Equal(uint16(s.snapshot.CipherSuite), hello.CipherSuite)
|
|
|
518
|
+ })
|
|
|
519
|
+ }
|
|
|
520
|
+}
|
|
|
521
|
+
|
|
|
522
|
+func (s *ParseClientHelloFragmentedTestSuite) TestReassemblyErrors() {
|
|
|
523
|
+ full := s.snapshot.GetFull()
|
|
|
524
|
+ payload := full[tls.SizeHeader:]
|
|
|
525
|
+
|
|
|
526
|
+ tests := []struct {
|
|
|
527
|
+ name string
|
|
|
528
|
+ buildData func() []byte
|
|
|
529
|
+ errMsg string
|
|
|
530
|
+ }{
|
|
|
531
|
+ {
|
|
|
532
|
+ name: "wrong continuation record type",
|
|
|
533
|
+ buildData: func() []byte {
|
|
|
534
|
+ buf := &bytes.Buffer{}
|
|
|
535
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
536
|
+ buf.Write(full[1:3])
|
|
|
537
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(10)))
|
|
|
538
|
+ buf.Write(payload[:10])
|
|
|
539
|
+ // Wrong type: application data instead of handshake
|
|
|
540
|
+ buf.WriteByte(tls.TypeApplicationData)
|
|
|
541
|
+ buf.Write(full[1:3])
|
|
|
542
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(len(payload)-10)))
|
|
|
543
|
+ buf.Write(payload[10:])
|
|
|
544
|
+ return buf.Bytes()
|
|
|
545
|
+ },
|
|
|
546
|
+ errMsg: "unexpected continuation record type",
|
|
|
547
|
+ },
|
|
|
548
|
+ {
|
|
|
549
|
+ name: "too many continuation records",
|
|
|
550
|
+ buildData: func() []byte {
|
|
|
551
|
+ // Handshake header claiming 256 bytes, but we only send 1 byte per continuation
|
|
|
552
|
+ handshakePayload := []byte{0x01, 0x00, 0x01, 0x00}
|
|
|
553
|
+ buf := &bytes.Buffer{}
|
|
|
554
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
555
|
+ buf.Write([]byte{3, 1})
|
|
|
556
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(len(handshakePayload))))
|
|
|
557
|
+ buf.Write(handshakePayload)
|
|
|
558
|
+ for range 11 {
|
|
|
559
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
560
|
+ buf.Write([]byte{3, 1})
|
|
|
561
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(1)))
|
|
|
562
|
+ buf.WriteByte(0xAB)
|
|
|
563
|
+ }
|
|
|
564
|
+ return buf.Bytes()
|
|
|
565
|
+ },
|
|
|
566
|
+ errMsg: "too many continuation records",
|
|
|
567
|
+ },
|
|
|
568
|
+ {
|
|
|
569
|
+ name: "zero-length continuation record",
|
|
|
570
|
+ buildData: func() []byte {
|
|
|
571
|
+ buf := &bytes.Buffer{}
|
|
|
572
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
573
|
+ buf.Write(full[1:3])
|
|
|
574
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(10)))
|
|
|
575
|
+ buf.Write(payload[:10])
|
|
|
576
|
+ // Valid header but zero-length payload
|
|
|
577
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
578
|
+ buf.Write(full[1:3])
|
|
|
579
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(0)))
|
|
|
580
|
+ return buf.Bytes()
|
|
|
581
|
+ },
|
|
|
582
|
+ errMsg: "zero-length continuation record",
|
|
|
583
|
+ },
|
|
|
584
|
+ {
|
|
|
585
|
+ name: "wrong continuation record version",
|
|
|
586
|
+ buildData: func() []byte {
|
|
|
587
|
+ buf := &bytes.Buffer{}
|
|
|
588
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
589
|
+ buf.Write(full[1:3])
|
|
|
590
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(10)))
|
|
|
591
|
+ buf.Write(payload[:10])
|
|
|
592
|
+ // Wrong version: 3.3 instead of 3.1
|
|
|
593
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
594
|
+ buf.Write([]byte{3, 3})
|
|
|
595
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(len(payload)-10)))
|
|
|
596
|
+ buf.Write(payload[10:])
|
|
|
597
|
+ return buf.Bytes()
|
|
|
598
|
+ },
|
|
|
599
|
+ errMsg: "unexpected continuation record version",
|
|
|
600
|
+ },
|
|
|
601
|
+ {
|
|
|
602
|
+ name: "handshake message too large",
|
|
|
603
|
+ buildData: func() []byte {
|
|
|
604
|
+ // Handshake header claiming 0x010000 (65536) bytes — exceeds 0xFFFF limit
|
|
|
605
|
+ handshakePayload := []byte{0x01, 0x01, 0x00, 0x00}
|
|
|
606
|
+ buf := &bytes.Buffer{}
|
|
|
607
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
608
|
+ buf.Write([]byte{3, 1})
|
|
|
609
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(len(handshakePayload))))
|
|
|
610
|
+ buf.Write(handshakePayload)
|
|
|
611
|
+ return buf.Bytes()
|
|
|
612
|
+ },
|
|
|
613
|
+ errMsg: "handshake message too large",
|
|
|
614
|
+ },
|
|
|
615
|
+ {
|
|
|
616
|
+ name: "truncated continuation record header",
|
|
|
617
|
+ buildData: func() []byte {
|
|
|
618
|
+ buf := &bytes.Buffer{}
|
|
|
619
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
620
|
+ buf.Write(full[1:3])
|
|
|
621
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(10)))
|
|
|
622
|
+ buf.Write(payload[:10])
|
|
|
623
|
+ // Connection ends mid-header (only 2 bytes)
|
|
|
624
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
625
|
+ buf.WriteByte(3)
|
|
|
626
|
+ return buf.Bytes()
|
|
|
627
|
+ },
|
|
|
628
|
+ errMsg: "cannot read continuation record header",
|
|
|
629
|
+ },
|
|
|
630
|
+ {
|
|
|
631
|
+ name: "truncated continuation record payload",
|
|
|
632
|
+ buildData: func() []byte {
|
|
|
633
|
+ buf := &bytes.Buffer{}
|
|
|
634
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
635
|
+ buf.Write(full[1:3])
|
|
|
636
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(10)))
|
|
|
637
|
+ buf.Write(payload[:10])
|
|
|
638
|
+ // Claims 100 bytes but no payload follows
|
|
|
639
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
640
|
+ buf.Write(full[1:3])
|
|
|
641
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(100)))
|
|
|
642
|
+ return buf.Bytes()
|
|
|
643
|
+ },
|
|
|
644
|
+ errMsg: "cannot read continuation record payload",
|
|
|
645
|
+ },
|
|
|
646
|
+ }
|
|
|
647
|
+
|
|
|
648
|
+ for _, tt := range tests {
|
|
|
649
|
+ s.Run(tt.name, func() {
|
|
|
650
|
+ connMock := s.makeConn(tt.buildData())
|
|
|
651
|
+ defer connMock.AssertExpectations(s.T())
|
|
|
652
|
+
|
|
|
653
|
+ _, err := fake.ReadClientHello(
|
|
|
654
|
+ connMock,
|
|
|
655
|
+ s.secret.Key[:],
|
|
|
656
|
+ s.secret.Host,
|
|
|
657
|
+ TolerateTime,
|
|
|
658
|
+ )
|
|
|
659
|
+ s.ErrorContains(err, tt.errMsg)
|
|
|
660
|
+ })
|
|
|
661
|
+ }
|
|
|
662
|
+}
|
|
|
663
|
+
|
|
|
664
|
+func TestParseClientHelloFragmented(t *testing.T) {
|
|
|
665
|
+ t.Parallel()
|
|
|
666
|
+ suite.Run(t, &ParseClientHelloFragmentedTestSuite{})
|
|
|
667
|
+}
|