|
|
@@ -530,3 +530,275 @@ func TestReadClientHelloMulti(t *testing.T) {
|
|
530
|
530
|
t.Parallel()
|
|
531
|
531
|
suite.Run(t, &ReadClientHelloMultiTestSuite{})
|
|
532
|
532
|
}
|
|
|
533
|
+
|
|
|
534
|
+// --- Fragmented TLS record tests ---
|
|
|
535
|
+
|
|
|
536
|
+// fragmentTLSRecord splits a single TLS record into n TLS records by
|
|
|
537
|
+// dividing the payload into roughly equal parts. Each part gets its own
|
|
|
538
|
+// TLS record header with the same record type and version.
|
|
|
539
|
+func fragmentTLSRecord(t testing.TB, full []byte, n int) []byte {
|
|
|
540
|
+ t.Helper()
|
|
|
541
|
+
|
|
|
542
|
+ recordType := full[0]
|
|
|
543
|
+ version := full[1:3]
|
|
|
544
|
+ payload := full[tls.SizeHeader:]
|
|
|
545
|
+
|
|
|
546
|
+ chunkSize := len(payload) / n
|
|
|
547
|
+ result := &bytes.Buffer{}
|
|
|
548
|
+
|
|
|
549
|
+ for i := 0; i < n; i++ {
|
|
|
550
|
+ start := i * chunkSize
|
|
|
551
|
+ end := start + chunkSize
|
|
|
552
|
+
|
|
|
553
|
+ if i == n-1 {
|
|
|
554
|
+ end = len(payload)
|
|
|
555
|
+ }
|
|
|
556
|
+
|
|
|
557
|
+ chunk := payload[start:end]
|
|
|
558
|
+ result.WriteByte(recordType)
|
|
|
559
|
+ result.Write(version)
|
|
|
560
|
+ require.NoError(t, binary.Write(result, binary.BigEndian, uint16(len(chunk))))
|
|
|
561
|
+ result.Write(chunk)
|
|
|
562
|
+ }
|
|
|
563
|
+
|
|
|
564
|
+ return result.Bytes()
|
|
|
565
|
+}
|
|
|
566
|
+
|
|
|
567
|
+// splitPayloadAt creates two TLS records from a single record by splitting
|
|
|
568
|
+// the payload at the given byte position.
|
|
|
569
|
+func splitPayloadAt(t testing.TB, full []byte, pos int) []byte {
|
|
|
570
|
+ t.Helper()
|
|
|
571
|
+
|
|
|
572
|
+ payload := full[tls.SizeHeader:]
|
|
|
573
|
+ buf := &bytes.Buffer{}
|
|
|
574
|
+
|
|
|
575
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
576
|
+ buf.Write(full[1:3])
|
|
|
577
|
+ require.NoError(t, binary.Write(buf, binary.BigEndian, uint16(pos)))
|
|
|
578
|
+ buf.Write(payload[:pos])
|
|
|
579
|
+
|
|
|
580
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
581
|
+ buf.Write(full[1:3])
|
|
|
582
|
+ require.NoError(t, binary.Write(buf, binary.BigEndian, uint16(len(payload)-pos)))
|
|
|
583
|
+ buf.Write(payload[pos:])
|
|
|
584
|
+
|
|
|
585
|
+ return buf.Bytes()
|
|
|
586
|
+}
|
|
|
587
|
+
|
|
|
588
|
+type ParseClientHelloFragmentedTestSuite struct {
|
|
|
589
|
+ suite.Suite
|
|
|
590
|
+
|
|
|
591
|
+ secret mtglib.Secret
|
|
|
592
|
+ snapshot *clientHelloSnapshot
|
|
|
593
|
+}
|
|
|
594
|
+
|
|
|
595
|
+func (s *ParseClientHelloFragmentedTestSuite) SetupSuite() {
|
|
|
596
|
+ parsed, err := mtglib.ParseSecret(
|
|
|
597
|
+ "ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d",
|
|
|
598
|
+ )
|
|
|
599
|
+ require.NoError(s.T(), err)
|
|
|
600
|
+
|
|
|
601
|
+ s.secret = parsed
|
|
|
602
|
+
|
|
|
603
|
+ fileData, err := os.ReadFile("testdata/client-hello-ok-19dfe38384b9884b.json")
|
|
|
604
|
+ require.NoError(s.T(), err)
|
|
|
605
|
+
|
|
|
606
|
+ s.snapshot = &clientHelloSnapshot{}
|
|
|
607
|
+ require.NoError(s.T(), json.Unmarshal(fileData, s.snapshot))
|
|
|
608
|
+}
|
|
|
609
|
+
|
|
|
610
|
+func (s *ParseClientHelloFragmentedTestSuite) makeConn(data []byte) *parseClientHelloConnMock {
|
|
|
611
|
+ readBuf := &bytes.Buffer{}
|
|
|
612
|
+ readBuf.Write(data)
|
|
|
613
|
+
|
|
|
614
|
+ connMock := &parseClientHelloConnMock{
|
|
|
615
|
+ readBuf: readBuf,
|
|
|
616
|
+ }
|
|
|
617
|
+
|
|
|
618
|
+ connMock.
|
|
|
619
|
+ On("SetReadDeadline", mock.AnythingOfType("time.Time")).
|
|
|
620
|
+ Twice().
|
|
|
621
|
+ Return(nil)
|
|
|
622
|
+
|
|
|
623
|
+ return connMock
|
|
|
624
|
+}
|
|
|
625
|
+
|
|
|
626
|
+func (s *ParseClientHelloFragmentedTestSuite) TestReassemblySuccess() {
|
|
|
627
|
+ full := s.snapshot.GetFull()
|
|
|
628
|
+
|
|
|
629
|
+ tests := []struct {
|
|
|
630
|
+ name string
|
|
|
631
|
+ data []byte
|
|
|
632
|
+ }{
|
|
|
633
|
+ {"two equal fragments", fragmentTLSRecord(s.T(), full, 2)},
|
|
|
634
|
+ {"three equal fragments", fragmentTLSRecord(s.T(), full, 3)},
|
|
|
635
|
+ {"single byte first fragment", splitPayloadAt(s.T(), full, 1)},
|
|
|
636
|
+ {"three byte first fragment", splitPayloadAt(s.T(), full, 3)},
|
|
|
637
|
+ }
|
|
|
638
|
+
|
|
|
639
|
+ for _, tt := range tests {
|
|
|
640
|
+ s.Run(tt.name, func() {
|
|
|
641
|
+ connMock := s.makeConn(tt.data)
|
|
|
642
|
+ defer connMock.AssertExpectations(s.T())
|
|
|
643
|
+
|
|
|
644
|
+ hello, err := fake.ReadClientHello(
|
|
|
645
|
+ connMock,
|
|
|
646
|
+ s.secret.Key[:],
|
|
|
647
|
+ s.secret.Host,
|
|
|
648
|
+ TolerateTime,
|
|
|
649
|
+ )
|
|
|
650
|
+ s.Require().NoError(err)
|
|
|
651
|
+
|
|
|
652
|
+ s.Equal(s.snapshot.GetRandom(), hello.Random[:])
|
|
|
653
|
+ s.Equal(s.snapshot.GetSessionID(), hello.SessionID)
|
|
|
654
|
+ s.Equal(uint16(s.snapshot.CipherSuite), hello.CipherSuite)
|
|
|
655
|
+ })
|
|
|
656
|
+ }
|
|
|
657
|
+}
|
|
|
658
|
+
|
|
|
659
|
+func (s *ParseClientHelloFragmentedTestSuite) TestReassemblyErrors() {
|
|
|
660
|
+ full := s.snapshot.GetFull()
|
|
|
661
|
+ payload := full[tls.SizeHeader:]
|
|
|
662
|
+
|
|
|
663
|
+ tests := []struct {
|
|
|
664
|
+ name string
|
|
|
665
|
+ buildData func() []byte
|
|
|
666
|
+ errMsg string
|
|
|
667
|
+ }{
|
|
|
668
|
+ {
|
|
|
669
|
+ name: "wrong continuation record type",
|
|
|
670
|
+ buildData: func() []byte {
|
|
|
671
|
+ buf := &bytes.Buffer{}
|
|
|
672
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
673
|
+ buf.Write(full[1:3])
|
|
|
674
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(10)))
|
|
|
675
|
+ buf.Write(payload[:10])
|
|
|
676
|
+ // Wrong type: application data instead of handshake
|
|
|
677
|
+ buf.WriteByte(tls.TypeApplicationData)
|
|
|
678
|
+ buf.Write(full[1:3])
|
|
|
679
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(len(payload)-10)))
|
|
|
680
|
+ buf.Write(payload[10:])
|
|
|
681
|
+ return buf.Bytes()
|
|
|
682
|
+ },
|
|
|
683
|
+ errMsg: "unexpected continuation record type",
|
|
|
684
|
+ },
|
|
|
685
|
+ {
|
|
|
686
|
+ name: "too many continuation records",
|
|
|
687
|
+ buildData: func() []byte {
|
|
|
688
|
+ // Handshake header claiming 256 bytes, but we only send 1 byte per continuation
|
|
|
689
|
+ handshakePayload := []byte{0x01, 0x00, 0x01, 0x00}
|
|
|
690
|
+ buf := &bytes.Buffer{}
|
|
|
691
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
692
|
+ buf.Write([]byte{3, 1})
|
|
|
693
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(len(handshakePayload))))
|
|
|
694
|
+ buf.Write(handshakePayload)
|
|
|
695
|
+ for range 11 {
|
|
|
696
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
697
|
+ buf.Write([]byte{3, 1})
|
|
|
698
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(1)))
|
|
|
699
|
+ buf.WriteByte(0xAB)
|
|
|
700
|
+ }
|
|
|
701
|
+ return buf.Bytes()
|
|
|
702
|
+ },
|
|
|
703
|
+ errMsg: "too many continuation records",
|
|
|
704
|
+ },
|
|
|
705
|
+ {
|
|
|
706
|
+ name: "zero-length continuation record",
|
|
|
707
|
+ buildData: func() []byte {
|
|
|
708
|
+ buf := &bytes.Buffer{}
|
|
|
709
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
710
|
+ buf.Write(full[1:3])
|
|
|
711
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(10)))
|
|
|
712
|
+ buf.Write(payload[:10])
|
|
|
713
|
+ // Valid header but zero-length payload
|
|
|
714
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
715
|
+ buf.Write(full[1:3])
|
|
|
716
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(0)))
|
|
|
717
|
+ return buf.Bytes()
|
|
|
718
|
+ },
|
|
|
719
|
+ errMsg: "zero-length continuation record",
|
|
|
720
|
+ },
|
|
|
721
|
+ {
|
|
|
722
|
+ name: "wrong continuation record version",
|
|
|
723
|
+ buildData: func() []byte {
|
|
|
724
|
+ buf := &bytes.Buffer{}
|
|
|
725
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
726
|
+ buf.Write(full[1:3])
|
|
|
727
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(10)))
|
|
|
728
|
+ buf.Write(payload[:10])
|
|
|
729
|
+ // Wrong version: 3.3 instead of 3.1
|
|
|
730
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
731
|
+ buf.Write([]byte{3, 3})
|
|
|
732
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(len(payload)-10)))
|
|
|
733
|
+ buf.Write(payload[10:])
|
|
|
734
|
+ return buf.Bytes()
|
|
|
735
|
+ },
|
|
|
736
|
+ errMsg: "unexpected continuation record version",
|
|
|
737
|
+ },
|
|
|
738
|
+ {
|
|
|
739
|
+ name: "handshake message too large",
|
|
|
740
|
+ buildData: func() []byte {
|
|
|
741
|
+ // Handshake header claiming 0x010000 (65536) bytes — exceeds 0xFFFF limit
|
|
|
742
|
+ handshakePayload := []byte{0x01, 0x01, 0x00, 0x00}
|
|
|
743
|
+ buf := &bytes.Buffer{}
|
|
|
744
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
745
|
+ buf.Write([]byte{3, 1})
|
|
|
746
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(len(handshakePayload))))
|
|
|
747
|
+ buf.Write(handshakePayload)
|
|
|
748
|
+ return buf.Bytes()
|
|
|
749
|
+ },
|
|
|
750
|
+ errMsg: "handshake message too large",
|
|
|
751
|
+ },
|
|
|
752
|
+ {
|
|
|
753
|
+ name: "truncated continuation record header",
|
|
|
754
|
+ buildData: func() []byte {
|
|
|
755
|
+ buf := &bytes.Buffer{}
|
|
|
756
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
757
|
+ buf.Write(full[1:3])
|
|
|
758
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(10)))
|
|
|
759
|
+ buf.Write(payload[:10])
|
|
|
760
|
+ // Connection ends mid-header (only 2 bytes)
|
|
|
761
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
762
|
+ buf.WriteByte(3)
|
|
|
763
|
+ return buf.Bytes()
|
|
|
764
|
+ },
|
|
|
765
|
+ errMsg: "cannot read continuation record header",
|
|
|
766
|
+ },
|
|
|
767
|
+ {
|
|
|
768
|
+ name: "truncated continuation record payload",
|
|
|
769
|
+ buildData: func() []byte {
|
|
|
770
|
+ buf := &bytes.Buffer{}
|
|
|
771
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
772
|
+ buf.Write(full[1:3])
|
|
|
773
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(10)))
|
|
|
774
|
+ buf.Write(payload[:10])
|
|
|
775
|
+ // Claims 100 bytes but no payload follows
|
|
|
776
|
+ buf.WriteByte(tls.TypeHandshake)
|
|
|
777
|
+ buf.Write(full[1:3])
|
|
|
778
|
+ require.NoError(s.T(), binary.Write(buf, binary.BigEndian, uint16(100)))
|
|
|
779
|
+ return buf.Bytes()
|
|
|
780
|
+ },
|
|
|
781
|
+ errMsg: "cannot read continuation record payload",
|
|
|
782
|
+ },
|
|
|
783
|
+ }
|
|
|
784
|
+
|
|
|
785
|
+ for _, tt := range tests {
|
|
|
786
|
+ s.Run(tt.name, func() {
|
|
|
787
|
+ connMock := s.makeConn(tt.buildData())
|
|
|
788
|
+ defer connMock.AssertExpectations(s.T())
|
|
|
789
|
+
|
|
|
790
|
+ _, err := fake.ReadClientHello(
|
|
|
791
|
+ connMock,
|
|
|
792
|
+ s.secret.Key[:],
|
|
|
793
|
+ s.secret.Host,
|
|
|
794
|
+ TolerateTime,
|
|
|
795
|
+ )
|
|
|
796
|
+ s.ErrorContains(err, tt.errMsg)
|
|
|
797
|
+ })
|
|
|
798
|
+ }
|
|
|
799
|
+}
|
|
|
800
|
+
|
|
|
801
|
+func TestParseClientHelloFragmented(t *testing.T) {
|
|
|
802
|
+ t.Parallel()
|
|
|
803
|
+ suite.Run(t, &ParseClientHelloFragmentedTestSuite{})
|
|
|
804
|
+}
|