ReadClientHello function

This commit is contained in:
9seconds
2026-03-12 19:07:10 +01:00
parent 1182b9ef6f
commit 59557059df
12 changed files with 957 additions and 1 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ depends = [
[tasks."test:fuzz:client-hello"] [tasks."test:fuzz:client-hello"]
description = "Run fuzzy test for ClientHello" description = "Run fuzzy test for ClientHello"
run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzClientHello ./mtglib/internal/faketls" run = "go test -v {{ vars.fuzzflags }} -fuzz=FuzzReadClientHello ./mtglib/internal/tls/fake"
[tasks."test:fuzz:client-handshake"] [tasks."test:fuzz:client-handshake"]
description = "Run fuzzy test for ClientHandshake" description = "Run fuzzy test for ClientHandshake"
+305
View File
@@ -0,0 +1,305 @@
package fake
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/binary"
"fmt"
"io"
"net"
"slices"
"time"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/9seconds/mtg/v2/mtglib/internal/tls"
)
const (
TypeHandshakeClient = 0x01
RandomLen = 32
// record_type(1) + version(2) + size(2) + handshake_type(1) + uint24_length(3) + client_version(2)
clientRandomOffset = 1 + 2 + 2 + 1 + 3 + 2
sniDNSNamesListType = 0
)
var (
emptyRandom = [RandomLen]byte{}
extTypeSNI = [2]byte{}
)
type ClientHello struct {
Random [RandomLen]byte
SessionID []byte
CipherSuite uint16
}
func ReadClientHello(conn net.Conn, secret mtglib.Secret, tolerateTimeSkewness time.Duration) (*ClientHello, error) {
if err := conn.SetReadDeadline(time.Now().Add(ClientHelloReadTimeout)); err != nil {
return nil, fmt.Errorf("cannot set read deadline: %w", err)
}
defer conn.SetReadDeadline(resetDeadline) //nolint: errcheck
// This is how FakeTLS is organized:
// 1. We create sha256 HMAC with a given secret
// 2. We dump there a whole TLS frame except of the fact that random
// is filled with all zeroes
// 3. Digest is computed. This digest should be XORed with
// original client random
// 4. New digest should be all 0 except of last 4 bytes
// 5. Last 4 bytes are little endian uint32 of UNIX timestamp when
// this message was created.
handshakeCopyBuf := &bytes.Buffer{}
reader := io.TeeReader(conn, handshakeCopyBuf)
reader, err := parseTLSHeader(reader)
if err != nil {
return nil, fmt.Errorf("cannot parse tls header: %w", err)
}
reader, err = parseHandshakeHeader(reader)
if err != nil {
return nil, fmt.Errorf("cannot parse handshake header: %w", err)
}
hello, err := parseHandshake(reader)
if err != nil {
return nil, fmt.Errorf("cannot parse handshake: %w", err)
}
sniHostnames, err := parseSNI(reader)
if err != nil {
return nil, fmt.Errorf("cannot parse SNI: %w", err)
}
if !slices.Contains(sniHostnames, secret.Host) {
return nil, fmt.Errorf("cannot find %s in %v", secret.Host, sniHostnames)
}
digest := hmac.New(sha256.New, secret.Key[:])
// we write a copy of the handshake with client random all nullified.
digest.Write(handshakeCopyBuf.Next(clientRandomOffset))
handshakeCopyBuf.Next(RandomLen)
digest.Write(emptyRandom[:])
digest.Write(handshakeCopyBuf.Bytes())
computed := digest.Sum(nil)
for i := range RandomLen {
computed[i] ^= hello.Random[i]
}
if subtle.ConstantTimeCompare(emptyRandom[:RandomLen-4], computed[:RandomLen-4]) != 1 {
return nil, ErrBadDigest
}
timestamp := int64(binary.LittleEndian.Uint32(computed[RandomLen-4:]))
createdAt := time.Unix(timestamp, 0)
if tdiff := time.Since(createdAt).Abs(); tdiff > tolerateTimeSkewness {
return nil, fmt.Errorf("timestamp %q is too old %s", createdAt, tdiff)
}
return hello, nil
}
func parseTLSHeader(r io.Reader) (io.Reader, error) {
// record_type(1) + version(2) + size(2)
// 16 - type is 0x16 (handshake record)
// 03 01 - protocol version is "3,1" (also known as TLS 1.0)
// 00 f8 - 0xF8 (248) bytes of handshake message follows
header := [1 + 2 + 2]byte{}
if _, err := io.ReadFull(r, header[:]); err != nil {
return nil, fmt.Errorf("cannot read record header: %w", err)
}
if header[0] != tls.TypeHandshake {
return nil, fmt.Errorf("unexpected record type %#x", header[0])
}
if header[1] != 3 || header[2] != 1 {
return nil, fmt.Errorf("unexpected protocol version %#x %#x", header[1], header[2])
}
length := int64(binary.BigEndian.Uint16(header[3:]))
buf := &bytes.Buffer{}
_, err := io.CopyN(buf, r, length)
return buf, err
}
func parseHandshakeHeader(r io.Reader) (io.Reader, error) {
// type(1) + size(3 / uint24)
// 01 - handshake message type 0x01 (client hello)
// 00 00 f4 - 0xF4 (244) bytes of client hello data follows
header := [1 + 3]byte{}
if _, err := io.ReadFull(r, header[:]); err != nil {
return nil, fmt.Errorf("cannot read handshake header: %w", err)
}
if header[0] != TypeHandshakeClient {
return nil, fmt.Errorf("incorrect handshake type: %#x", header[0])
}
// unfortunately there is not uint24 in golang, so we just reust header
header[0] = 0
length := int64(binary.BigEndian.Uint32(header[:]))
buf := &bytes.Buffer{}
_, err := io.CopyN(buf, r, length)
return buf, err
}
func parseHandshake(r io.Reader) (*ClientHello, error) {
// A protocol version of "3,3" (meaning TLS 1.2) is given.
header := [2]byte{}
if _, err := io.ReadFull(r, header[:]); err != nil {
return nil, fmt.Errorf("cannot read client version: %w", err)
}
hello := &ClientHello{}
if _, err := io.ReadFull(r, hello.Random[:]); err != nil {
return nil, fmt.Errorf("cannot read client random: %w", err)
}
if _, err := io.ReadFull(r, header[:1]); err != nil {
return nil, fmt.Errorf("cannot read session ID length: %w", err)
}
hello.SessionID = make([]byte, int(header[0]))
if _, err := io.ReadFull(r, hello.SessionID); err != nil {
return nil, fmt.Errorf("cannot read session id: %w", err)
}
if _, err := io.ReadFull(r, header[:]); err != nil {
return nil, fmt.Errorf("cannot read cipher suite length: %w", err)
}
cipherSuiteLen := int64(binary.BigEndian.Uint16(header[:]))
// we do not care about picking up any cipher. we pick the first one,
// so it is always should be present.
if _, err := io.ReadFull(r, header[:]); err != nil {
return nil, fmt.Errorf("cannot read first cipher suite: %w", err)
}
hello.CipherSuite = binary.BigEndian.Uint16(header[:])
if _, err := io.CopyN(io.Discard, r, cipherSuiteLen-2); err != nil {
return nil, fmt.Errorf("cannot skip remaining cipher suites: %w", err)
}
if _, err := io.ReadFull(r, header[:1]); err != nil {
return nil, fmt.Errorf("cannot read compression methods length: %w", err)
}
if _, err := io.CopyN(io.Discard, r, int64(header[0])); err != nil {
return nil, fmt.Errorf("cannot skip compression methods: %w", err)
}
return hello, nil
}
func parseSNI(r io.Reader) ([]string, error) {
header := [2]byte{}
if _, err := io.ReadFull(r, header[:]); err != nil {
return nil, fmt.Errorf("cannot read length of TLS extensions: %w", err)
}
extensionsLength := int64(binary.BigEndian.Uint16(header[:]))
buf := &bytes.Buffer{}
buf.Grow(int(extensionsLength))
if _, err := io.CopyN(buf, r, extensionsLength); err != nil {
return nil, fmt.Errorf("cannot read extensions: %w", err)
}
for buf.Len() > 0 {
// 00 00 - assigned value for extension "server name"
// 00 18 - 0x18 (24) bytes of "server name" extension data follows
// 00 16 - 0x16 (22) bytes of first (and only) list entry follows
// 00 - list entry is type 0x00 "DNS hostname"
// 00 13 - 0x13 (19) bytes of hostname follows
// 65 78 61 ... 6e 65 74 - "example.ulfheim.net"
// 00 00 - assigned value for extension "server name"
extTypeB := buf.Next(2)
if len(extTypeB) != 2 {
return nil, fmt.Errorf("cannot read extension type: %v", extTypeB)
}
// 00 18 - 0x18 (24) bytes of "server name" extension data follows
lengthB := buf.Next(2)
if len(lengthB) != 2 {
return nil, fmt.Errorf("cannot read extension %v length: %v", extTypeB, lengthB)
}
length := int(binary.BigEndian.Uint16(lengthB))
extDataB := buf.Next(length)
if len(extDataB) != length {
return nil, fmt.Errorf("cannot read extension %v data: len %d != %d", extTypeB, length, len(extDataB))
}
if !bytes.Equal(extTypeB, extTypeSNI[:]) {
continue
}
buf.Reset()
buf.Write(extDataB)
// 00 16 - 0x16 (22) bytes of first (and only) list entry follows
lengthB = buf.Next(2)
if len(lengthB) != 2 {
return nil, fmt.Errorf("cannot read the length of the SNI record: %v", lengthB)
}
length = int(binary.BigEndian.Uint16(lengthB))
if length == 0 {
return nil, nil
}
listType, err := buf.ReadByte()
if err != nil {
return nil, fmt.Errorf("cannot read SNI list type: %w", err)
}
// 00 - list entry is type 0x00 "DNS hostname"
if listType != sniDNSNamesListType {
return nil, fmt.Errorf("incorrect SNI list type %#x", listType)
}
names := []string{}
for buf.Len() > 0 {
// 00 13 - 0x13 (19) bytes of hostname follows
lengthB = buf.Next(2)
if len(lengthB) != 2 {
return nil, fmt.Errorf("incorrect length of the hostname: %v", lengthB)
}
length = int(binary.BigEndian.Uint16(lengthB))
name := buf.Next(length)
if len(name) != length {
return nil, fmt.Errorf("incorrect length of SNI hostname: len %d != %d", length, len(name))
}
names = append(names, string(name))
}
return names, nil
}
return nil, nil
}
@@ -0,0 +1,49 @@
package fake_test
import (
"bytes"
"testing"
"time"
"github.com/9seconds/mtg/v2/internal/testlib"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/9seconds/mtg/v2/mtglib/internal/tls/fake"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type connMock struct {
testlib.EssentialsConnMock
readBuf *bytes.Buffer
}
func (f *connMock) Read(p []byte) (int, error) {
return f.readBuf.Read(p)
}
func FuzzReadClientHello(f *testing.F) {
seed := [248]byte{}
secret, err := mtglib.ParseSecret(
"ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d",
)
require.NoError(f, err)
f.Add(seed[:])
f.Fuzz(func(t *testing.T, value []byte) {
r := &connMock{
readBuf: bytes.NewBuffer(value),
}
r.
On("SetReadDeadline", mock.AnythingOfType("time.Time")).
Twice().
Return(nil)
_, err := fake.ReadClientHello(r, secret, time.Hour)
assert.Error(t, err)
})
}
@@ -0,0 +1,143 @@
package fake_test
import (
"bytes"
"encoding/base64"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/9seconds/mtg/v2/mtglib/internal/tls/fake"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
type clientHelloSnapshot struct {
Time int `json:"time"`
Random string `json:"random"`
SessionID string `json:"sessionId"`
Host string `json:"host"`
CipherSuite int `json:"cipherSuite"`
Full string `json:"full"`
}
func (c clientHelloSnapshot) GetRandom() []byte {
data, _ := base64.StdEncoding.DecodeString(c.Random)
return data
}
func (c clientHelloSnapshot) GetSessionID() []byte {
data, _ := base64.StdEncoding.DecodeString(c.SessionID)
return data
}
func (c clientHelloSnapshot) GetCipherSuite() uint16 {
return uint16(c.CipherSuite)
}
func (c clientHelloSnapshot) GetFull() []byte {
data, _ := base64.StdEncoding.DecodeString(c.Full)
return data
}
type ParseClientHelloSnapshotTestSuite struct {
suite.Suite
secret mtglib.Secret
}
func (suite *ParseClientHelloSnapshotTestSuite) SetupSuite() {
parsed, err := mtglib.ParseSecret(
"ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d",
)
require.NoError(suite.T(), err)
suite.secret = parsed
}
func (suite *ParseClientHelloSnapshotTestSuite) makeConn(data []byte) *parseClientHelloConnMock {
readBuf := &bytes.Buffer{}
readBuf.Write(data)
connMock := &parseClientHelloConnMock{
readBuf: readBuf,
}
connMock.
On("SetReadDeadline", mock.AnythingOfType("time.Time")).
Twice().
Return(nil)
return connMock
}
func (suite *ParseClientHelloSnapshotTestSuite) TestSnapshotOk() {
files, err := os.ReadDir("testdata")
require.NoError(suite.T(), err)
for _, v := range files {
if !strings.HasPrefix(v.Name(), "client-hello-ok") {
continue
}
path := filepath.Join("testdata", v.Name())
suite.T().Run(v.Name(), func(t *testing.T) {
fileData, err := os.ReadFile(path)
assert.NoError(t, err)
snapshot := &clientHelloSnapshot{}
assert.NoError(t, json.Unmarshal(fileData, snapshot))
connMock := suite.makeConn(snapshot.GetFull())
defer connMock.AssertExpectations(t)
hello, err := fake.ReadClientHello(connMock, suite.secret, TolerateTime)
require.NoError(t, err)
assert.Equal(t, snapshot.GetRandom(), hello.Random[:])
assert.Equal(t, snapshot.GetSessionID(), hello.SessionID)
assert.Equal(t, snapshot.GetCipherSuite(), hello.CipherSuite)
})
}
}
func (suite *ParseClientHelloSnapshotTestSuite) TestSnapshotBad() {
files, err := os.ReadDir("testdata")
require.NoError(suite.T(), err)
for _, v := range files {
if !strings.HasPrefix(v.Name(), "client-hello-bad") {
continue
}
path := filepath.Join("testdata", v.Name())
suite.T().Run(v.Name(), func(t *testing.T) {
fileData, err := os.ReadFile(path)
assert.NoError(t, err)
snapshot := &clientHelloSnapshot{}
assert.NoError(t, json.Unmarshal(fileData, snapshot))
connMock := suite.makeConn(snapshot.GetFull())
defer connMock.AssertExpectations(t)
_, err = fake.ReadClientHello(connMock, suite.secret, TolerateTime)
assert.ErrorIs(t, err, fake.ErrBadDigest)
})
}
}
func TestParseClientHelloSnapshot(t *testing.T) {
t.Parallel()
suite.Run(t, &ParseClientHelloSnapshotTestSuite{})
}
@@ -0,0 +1,395 @@
package fake_test
import (
"bytes"
"encoding/binary"
"errors"
"io"
"testing"
"time"
"github.com/9seconds/mtg/v2/internal/testlib"
"github.com/9seconds/mtg/v2/mtglib"
"github.com/9seconds/mtg/v2/mtglib/internal/tls"
"github.com/9seconds/mtg/v2/mtglib/internal/tls/fake"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
const (
TolerateTime = 365 * 30 * 24 * time.Hour
)
type parseClientHelloConnMock struct {
testlib.EssentialsConnMock
readBuf *bytes.Buffer
}
func (m *parseClientHelloConnMock) Read(p []byte) (int, error) {
return m.readBuf.Read(p)
}
type ParseClientHelloTestSuite struct {
suite.Suite
secret mtglib.Secret
readBuf *bytes.Buffer
connMock *parseClientHelloConnMock
}
func (suite *ParseClientHelloTestSuite) SetupSuite() {
parsed, err := mtglib.ParseSecret("ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d")
require.NoError(suite.T(), err)
suite.secret = parsed
}
func (suite *ParseClientHelloTestSuite) SetupTest() {
suite.readBuf = &bytes.Buffer{}
suite.connMock = &parseClientHelloConnMock{
readBuf: suite.readBuf,
}
suite.connMock.
On("SetReadDeadline", mock.AnythingOfType("time.Time")).
Twice().
Return(nil)
}
func (suite *ParseClientHelloTestSuite) TearDownTest() {
suite.connMock.AssertExpectations(suite.T())
}
type ParseClientHello_TLSHeaderTestSuite struct {
ParseClientHelloTestSuite
}
func (suite *ParseClientHello_TLSHeaderTestSuite) TestEmpty() {
suite.connMock.ExpectedCalls = []*mock.Call{}
suite.connMock.
On("SetReadDeadline", mock.AnythingOfType("time.Time")).
Once().
Return(errors.New("fail"))
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "fail")
}
func (suite *ParseClientHello_TLSHeaderTestSuite) TestNothing() {
suite.connMock.ExpectedCalls = []*mock.Call{}
suite.connMock.
On("SetReadDeadline", mock.AnythingOfType("time.Time")).
Twice().
Return(nil)
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorIs(err, io.EOF)
}
func (suite *ParseClientHello_TLSHeaderTestSuite) TestUnknownRecord() {
suite.readBuf.Write([]byte{
10,
3, 3,
0, 0,
})
suite.readBuf.WriteByte(10)
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "unexpected record type 0xa")
}
func (suite *ParseClientHello_TLSHeaderTestSuite) TestUnknownProtocolVersion() {
suite.readBuf.Write([]byte{
tls.TypeHandshake,
3, 3,
0, 0,
})
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "unexpected protocol version")
}
func (suite *ParseClientHello_TLSHeaderTestSuite) TestCannotReadRestOfRecord() {
suite.readBuf.Write([]byte{
tls.TypeHandshake,
3, 1,
0, 10,
})
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorIs(err, io.EOF)
}
type ParseClientHelloHandshakeTestSuite struct {
ParseClientHelloTestSuite
}
func (suite *ParseClientHelloHandshakeTestSuite) SetupTest() {
suite.ParseClientHelloTestSuite.SetupTest()
suite.readBuf.Write([]byte{
tls.TypeHandshake,
3, 1,
0,
})
}
func (suite *ParseClientHelloHandshakeTestSuite) TestCannotReadHeader() {
suite.readBuf.Write([]byte{
1,
10,
})
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "cannot read handshake header")
}
func (suite *ParseClientHelloHandshakeTestSuite) TestIncorrectHandshakeType() {
suite.readBuf.Write([]byte{
4,
10, 0, 0, 0,
})
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "incorrect handshake type")
}
func (suite *ParseClientHelloHandshakeTestSuite) TestCannotReadHandshake() {
suite.readBuf.Write([]byte{
4 + 3,
10, 0, 0, 0,
})
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorIs(err, io.EOF)
}
type ParseClientHelloHandshakeBodyTestSuite struct {
ParseClientHelloTestSuite
}
func (suite *ParseClientHelloHandshakeBodyTestSuite) SetupTest() {
suite.ParseClientHelloTestSuite.SetupTest()
suite.readBuf.Write([]byte{
tls.TypeHandshake,
3, 1,
0,
})
}
func (suite *ParseClientHelloHandshakeBodyTestSuite) writeBody(body []byte) {
suite.readBuf.WriteByte(byte(4 + len(body)))
suite.readBuf.Write([]byte{
fake.TypeHandshakeClient,
0, 0, byte(len(body)),
})
suite.readBuf.Write(body)
}
func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadVersion() {
suite.writeBody(nil)
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "cannot read client version")
}
func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadRandom() {
suite.writeBody([]byte{3, 3})
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "cannot read client random")
}
func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadSessionIDLength() {
body := make([]byte, 2+fake.RandomLen)
suite.writeBody(body)
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "cannot read session ID length")
}
func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadSessionID() {
body := make([]byte, 2+fake.RandomLen+1)
body[2+fake.RandomLen] = 32
suite.writeBody(body)
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "cannot read session id")
}
func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadCipherSuiteLength() {
body := make([]byte, 2+fake.RandomLen+1)
suite.writeBody(body)
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "cannot read cipher suite length")
}
func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadFirstCipherSuite() {
body := make([]byte, 2+fake.RandomLen+1+2)
suite.writeBody(body)
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "cannot read first cipher suite")
}
func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotSkipRemainingCipherSuites() {
body := make([]byte, 2+fake.RandomLen+1+2+2)
binary.BigEndian.PutUint16(body[2+fake.RandomLen+1:], 4)
suite.writeBody(body)
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "cannot skip remaining cipher suites")
}
func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadCompressionMethodsLength() {
body := make([]byte, 2+fake.RandomLen+1+2+2)
binary.BigEndian.PutUint16(body[2+fake.RandomLen+1:], 2)
suite.writeBody(body)
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "cannot read compression methods length")
}
func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotSkipCompressionMethods() {
body := make([]byte, 2+fake.RandomLen+1+2+2+1)
binary.BigEndian.PutUint16(body[2+fake.RandomLen+1:], 2)
body[2+fake.RandomLen+1+2+2] = 1
suite.writeBody(body)
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "cannot skip compression methods")
}
type ParseClientHelloSNITestSuite struct {
ParseClientHelloTestSuite
}
func (suite *ParseClientHelloSNITestSuite) SetupTest() {
suite.ParseClientHelloTestSuite.SetupTest()
suite.readBuf.Write([]byte{
tls.TypeHandshake,
3, 1,
0,
})
}
func (suite *ParseClientHelloSNITestSuite) writeExtensions(extensions []byte) {
handshakeBodyLen := 41 + len(extensions)
suite.readBuf.WriteByte(byte(4 + handshakeBodyLen))
suite.readBuf.Write([]byte{
fake.TypeHandshakeClient,
0, 0, byte(handshakeBodyLen),
})
// version(2) + random(32) + sessionIDLen(1) + cipherSuiteLen(2) +
// cipherSuite(2) + compressionLen(1) + compression(1) = 41
body := make([]byte, 41)
binary.BigEndian.PutUint16(body[35:], 2)
body[39] = 1
suite.readBuf.Write(body)
suite.readBuf.Write(extensions)
}
func (suite *ParseClientHelloSNITestSuite) TestCannotReadExtensionsLength() {
suite.writeExtensions(nil)
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "cannot read length of TLS extensions")
}
func (suite *ParseClientHelloSNITestSuite) TestCannotReadExtensions() {
suite.writeExtensions([]byte{0, 10})
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "cannot read extensions")
}
func (suite *ParseClientHelloSNITestSuite) TestCannotReadExtensionType() {
suite.writeExtensions([]byte{0, 1, 0xAB})
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "cannot read extension type")
}
func (suite *ParseClientHelloSNITestSuite) TestCannotReadExtensionLength() {
suite.writeExtensions([]byte{0, 2, 0xFF, 0xFF})
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "length:")
}
func (suite *ParseClientHelloSNITestSuite) TestCannotReadExtensionData() {
suite.writeExtensions([]byte{0, 4, 0xFF, 0xFF, 0, 5})
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "data: len")
}
func (suite *ParseClientHelloSNITestSuite) TestCannotReadSNIRecordLength() {
suite.writeExtensions([]byte{0, 5, 0, 0, 0, 1, 0xAB})
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "cannot read the length of the SNI record")
}
func (suite *ParseClientHelloSNITestSuite) TestCannotReadSNIListType() {
suite.writeExtensions([]byte{0, 6, 0, 0, 0, 2, 0, 1})
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "cannot read SNI list type")
}
func (suite *ParseClientHelloSNITestSuite) TestIncorrectSNIListType() {
suite.writeExtensions([]byte{0, 7, 0, 0, 0, 3, 0, 1, 5})
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "incorrect SNI list type")
}
func (suite *ParseClientHelloSNITestSuite) TestCannotReadHostnameLength() {
suite.writeExtensions([]byte{0, 8, 0, 0, 0, 4, 0, 2, 0, 0xAB})
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "incorrect length of the hostname")
}
func (suite *ParseClientHelloSNITestSuite) TestCannotReadHostname() {
suite.writeExtensions([]byte{0, 9, 0, 0, 0, 5, 0, 3, 0, 0, 5})
_, err := fake.ReadClientHello(suite.connMock, suite.secret, TolerateTime)
suite.ErrorContains(err, "incorrect length of SNI hostname")
}
func TestParseClientHelloTLSHeader(t *testing.T) {
t.Parallel()
suite.Run(t, &ParseClientHello_TLSHeaderTestSuite{})
}
func TestParseClientHelloHandshake(t *testing.T) {
t.Parallel()
suite.Run(t, &ParseClientHelloHandshakeTestSuite{})
}
func TestParseClientHelloHandshakeBody(t *testing.T) {
t.Parallel()
suite.Run(t, &ParseClientHelloHandshakeBodyTestSuite{})
}
func TestParseClientHelloSNI(t *testing.T) {
t.Parallel()
suite.Run(t, &ParseClientHelloSNITestSuite{})
}
+16
View File
@@ -0,0 +1,16 @@
package fake
import (
"errors"
"time"
)
const (
ClientHelloReadTimeout = 5 * time.Second
)
var (
resetDeadline time.Time
ErrBadDigest = errors.New("incorrect client random")
)
@@ -0,0 +1,8 @@
{
"time": 1617181365,
"random": "XvCPc3aAbHbhRLv0kUmy6BfPZOGvsused5/HNsKXEPs=",
"sessionId": "St2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCMCq4=",
"host": "storage.googleapis.com",
"cipherSuite": 4867,
"full": "FgMBAgABAAH8AwNe8I9zdoBsduFEu/SRSbLoF89k4a+y6x53n8c2wpcQ+yBK3YFna4cwWfcHa2sPWN922mOgk46DokF4uEVzIIAKrgA0EwMTARMCwCzAK8AkwCPACsAJzKnAMMAvwCjAJ8AUwBPMqACdAJwAPQA8ADUAL8AIwBIACgEAAX//AQABAAAAABsAGQAAFnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20AFwAAAA0AGAAWBAMIBAQBBQMCAwgFCAUFAQgGBgECAQAFAAUA0AAAADN0AAAAEgAAABAAMAAuAmgyBWgyLTE2BWgyLTE1BWgyLTE0CHNwZHkvMy4xBnNwZHkvMwhodHRwLzEuMQALAAIBAAAzACYAJAAdACAH/ugvH0kSUgAuwslL3UfZA3JTUfSiwrAhR6VWd2wvIgAtAAIBAQArAAkIAwQDAwMCAwEACgAKAAgAHQAXABgAGQAVAKEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
}
@@ -0,0 +1,8 @@
{
"time": 1617181365,
"random": "XvCPc3aAbHbhRLv0kUmy6BfPZOGvsused5/HNsKXEPs=",
"sessionId": "St2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCMCq4=",
"host": "storage.googleapis.com",
"cipherSuite": 4867,
"full": "FgMBAgABAAH8AwNe8I9zdoBsduFEu/SRSbLoF89k4a+y6x53n8c2wpcQ+yBK3YFna4cwWfcHa2sPWN922mOgk46DokF4uEVzIIwKrgA0EwMTARMCwCzAK8AkwCPACsAJzKnAMMAvwCjAJ8AUwBPMqACdAJwAPQA8ADUAL8AIwBIACgEAAX//AQABAAAAABsAGQAAFnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20AFwAAAA0AGAAWBAMIBAQBBQMCAwgFCAUFAQgGBgECAQAFAAUBAAAAADN0AAAAEgAAABAAMAAuAmgyBWgyLTE2BWgyLTE1BWgyLTE0CHNwZHkvMy4xBnNwZHkvMwhodHRwLzEuMQALAAIBAAAzACYAJAAdACAH/ugvH0kSUgAuwslL3UfZA3JTUfSiwrAhR6VWd2wvIgAtAAIBAQArAAkIAwQDAwMCAwEACgAKAAgAHQAXABgAGQAVAKEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
}
@@ -0,0 +1,8 @@
{
"time": 1617181352,
"random": "oYEu33jl+zQbUKMtQbV1OHB0gXIM2y2aq9iY0QX12os=",
"sessionId": "FGqA3ZFYrSlj//xl7lammNn64K9/MK2mQ3HJUGvP+8g=",
"host": "storage.googleapis.com",
"cipherSuite": 4867,
"full": "FgMBAgABAAH8AwOhgS7feOX7NBtQoy1BtXU4cHSBcgzbLZqr2JjRBfXaiyAUaoDdkVitKWP//GXuVqaY2frgr38wraZDcclQa8/7yAA0EwMTARMCwCzAK8AkwCPACsAJzKnAMMAvwCjAJ8AUwBPMqACdAJwAPQA8ADUAL8AIwBIACgEAAX//AQABAAAAABsAGQAAFnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20AFwAAAA0AGAAWBAMIBAQBBQMCAwgFCAUFAQgGBgECAQAFAAUBAAAAADN0AAAAEgAAABAAMAAuAmgyBWgyLTE2BWgyLTE1BWgyLTE0CHNwZHkvMy4xBnNwZHkvMwhodHRwLzEuMQALAAIBAAAzACYAJAAdACBroKhykU/xB3hgIVH2mRoKv3umjYAuPQ/mcj02dvdRYwAtAAIBAQArAAkIAwQDAwMCAwEACgAKAAgAHQAXABgAGQAVAKEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
}
@@ -0,0 +1,8 @@
{
"time": 1617181352,
"random": "5V5sSprk/tFIgy+x1BeKNGhLlFkqfggLpgN7GYOA1ro=",
"sessionId": "jxr4d6PXPDk+Lwx3WUp9wvj8TGlOxEdrRJ0ydyJ9+H8=",
"host": "storage.googleapis.com",
"cipherSuite": 4867,
"full": "FgMBAgABAAH8AwPlXmxKmuT+0UiDL7HUF4o0aEuUWSp+CAumA3sZg4DWuiCPGvh3o9c8OT4vDHdZSn3C+PxMaU7ER2tEnTJ3In34fwA0EwMTARMCwCzAK8AkwCPACsAJzKnAMMAvwCjAJ8AUwBPMqACdAJwAPQA8ADUAL8AIwBIACgEAAX//AQABAAAAABsAGQAAFnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20AFwAAAA0AGAAWBAMIBAQBBQMCAwgFCAUFAQgGBgECAQAFAAUBAAAAADN0AAAAEgAAABAAMAAuAmgyBWgyLTE2BWgyLTE1BWgyLTE0CHNwZHkvMy4xBnNwZHkvMwhodHRwLzEuMQALAAIBAAAzACYAJAAdACCu6UBqpR0p5VgzQX6m7qif+HosGk7LM4objEUgpygWTgAtAAIBAQArAAkIAwQDAwMCAwEACgAKAAgAHQAXABgAGQAVAKEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
}
@@ -0,0 +1,8 @@
{
"time": 1617181365,
"random": "8xljlOhkDlkafEF5vu3e1r3fWvh8AX548wC3hLZ3szQ=",
"sessionId": "00uvDYKnFyZFKyf3HlLwWGCOyeHsPFiU5UZ+Fs5pDAU=",
"host": "storage.googleapis.com",
"cipherSuite": 4867,
"full": "FgMBAgABAAH8AwPzGWOU6GQOWRp8QXm+7d7Wvd9a+HwBfnjzALeEtnezNCDTS68NgqcXJkUrJ/ceUvBYYI7J4ew8WJTlRn4WzmkMBQA0EwMTARMCwCzAK8AkwCPACsAJzKnAMMAvwCjAJ8AUwBPMqACdAJwAPQA8ADUAL8AIwBIACgEAAX//AQABAAAAABsAGQAAFnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20AFwAAAA0AGAAWBAMIBAQBBQMCAwgFCAUFAQgGBgECAQAFAAUBAAAAADN0AAAAEgAAABAAMAAuAmgyBWgyLTE2BWgyLTE1BWgyLTE0CHNwZHkvMy4xBnNwZHkvMwhodHRwLzEuMQALAAIBAAAzACYAJAAdACD/0/vXjQ20rOPIPAF/32Y7LX4WNE8A8dM1D1bEc4qlXgAtAAIBAQArAAkIAwQDAwMCAwEACgAKAAgAHQAXABgAGQAVAKEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
}
@@ -0,0 +1,8 @@
{
"time": 1617181352,
"random": "zja3MLZ8WGSfsQRtPV75+tY6gbK3zKPi1Sy7SBBafg4=",
"sessionId": "qPut2yMqXa9zGLII/872SQ3d4Tfqo0uoDb7tpkRfBnA=",
"host": "storage.googleapis.com",
"cipherSuite": 4867,
"full": "FgMBAgABAAH8AwPONrcwtnxYZJ+xBG09Xvn61jqBsrfMo+LVLLtIEFp+DiCo+63bIypdr3MYsgj/zvZJDd3hN+qjS6gNvu2mRF8GcAA0EwMTARMCwCzAK8AkwCPACsAJzKnAMMAvwCjAJ8AUwBPMqACdAJwAPQA8ADUAL8AIwBIACgEAAX//AQABAAAAABsAGQAAFnN0b3JhZ2UuZ29vZ2xlYXBpcy5jb20AFwAAAA0AGAAWBAMIBAQBBQMCAwgFCAUFAQgGBgECAQAFAAUBAAAAADN0AAAAEgAAABAAMAAuAmgyBWgyLTE2BWgyLTE1BWgyLTE0CHNwZHkvMy4xBnNwZHkvMwhodHRwLzEuMQALAAIBAAAzACYAJAAdACBe+ItECoBgnzE4t2VyxSGV0jheXSD+z37LZCt3yto8SAAtAAIBAQArAAkIAwQDAwMCAwEACgAKAAgAHQAXABgAGQAVAKEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
}