diff --git a/mtglib/internal/tls/fake/client_side.go b/mtglib/internal/tls/fake/client_side.go index 66542bc..a5c46fc 100644 --- a/mtglib/internal/tls/fake/client_side.go +++ b/mtglib/internal/tls/fake/client_side.go @@ -130,18 +130,25 @@ func parseHandshake(r io.Reader) (*ClientHello, error) { 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) + // Pick the first non-GREASE cipher suite from the list. + // Real TLS servers never select GREASE values (RFC 8701, pattern 0x?a?a), + // so echoing them back is a trivial DPI fingerprint. + for remaining := cipherSuiteLen; remaining >= 2; remaining -= 2 { + if _, err := io.ReadFull(r, header[:]); err != nil { + return nil, fmt.Errorf("cannot read cipher suite: %w", err) + } + + cs := binary.BigEndian.Uint16(header[:]) + if hello.CipherSuite == 0 && cs&0x0f0f != 0x0a0a { + hello.CipherSuite = cs + } } - 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 hello.CipherSuite == 0 { + hello.CipherSuite = 0x1301 // fallback: TLS_AES_128_GCM_SHA256 } + if _, err := io.ReadFull(r, header[:1]); err != nil { return nil, fmt.Errorf("cannot read compression methods length: %w", err) } diff --git a/mtglib/internal/tls/fake/client_side_test.go b/mtglib/internal/tls/fake/client_side_test.go index bf23409..36f5ac8 100644 --- a/mtglib/internal/tls/fake/client_side_test.go +++ b/mtglib/internal/tls/fake/client_side_test.go @@ -234,12 +234,13 @@ func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadCipherSuiteLe } func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadFirstCipherSuite() { - body := make([]byte, 2+fake.RandomLen+1+2) + body := make([]byte, 2+fake.RandomLen+1+2+1) // cipherSuiteLen=2 but only 1 byte available + binary.BigEndian.PutUint16(body[2+fake.RandomLen+1:], 2) suite.writeBody(body) _, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime) - suite.ErrorContains(err, "cannot read first cipher suite") + suite.ErrorContains(err, "cannot read cipher suite") } func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotSkipRemainingCipherSuites() { @@ -249,7 +250,7 @@ func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotSkipRemainingCiph suite.writeBody(body) _, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime) - suite.ErrorContains(err, "cannot skip remaining cipher suites") + suite.ErrorContains(err, "cannot read cipher suite") } func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadCompressionMethodsLength() { diff --git a/mtglib/internal/tls/fake/server_side_test.go b/mtglib/internal/tls/fake/server_side_test.go index 1a9b5f2..6362a52 100644 --- a/mtglib/internal/tls/fake/server_side_test.go +++ b/mtglib/internal/tls/fake/server_side_test.go @@ -58,7 +58,7 @@ func (suite *SendServerHelloTestSuite) TestRecordStructure() { recordType, length, err := tls.ReadRecord(suite.buf, &rec) suite.NoError(err) suite.Equal(byte(tls.TypeApplicationData), recordType) - suite.Greater(length, int64(2500)) + suite.GreaterOrEqual(length, int64(2500)) suite.Empty(suite.buf.Bytes()) } diff --git a/mtglib/internal/tls/fake/testdata/client-hello-ok-grease-first.json b/mtglib/internal/tls/fake/testdata/client-hello-ok-grease-first.json new file mode 100644 index 0000000..0aedf10 --- /dev/null +++ b/mtglib/internal/tls/fake/testdata/client-hello-ok-grease-first.json @@ -0,0 +1,8 @@ +{ + "time": 1617181365, + "random": "w4TaDfYg/aUKdx1oi68vxMKvHJczRNvtRRppLETzeNE=", + "sessionId": "St2BZ2uHMFn3B2trD1jfdtpjoJOOg6JBeLhFcyCMCq4=", + "host": "storage.googleapis.com", + "cipherSuite": 4867, + "full": "FgMBAgIBAAH+AwPDhNoN9iD9pQp3HWiLry/Ewq8clzNE2+1FGmksRPN40SBK3YFna4cwWfcHa2sPWN922mOgk46DokF4uEVzIIwKrgA2WloTAxMBEwLALMArwCTAI8AKwAnMqcAwwC/AKMAnwBTAE8yoAJ0AnAA9ADwANQAvwAjAEgAKAQABf/8BAAEAAAAAGwAZAAAWc3RvcmFnZS5nb29nbGVhcGlzLmNvbQAXAAAADQAYABYEAwgEBAEFAwIDCAUIBQUBCAYGAQIBAAUABQEAAAAAM3QAAAASAAAAEAAwAC4CaDIFaDItMTYFaDItMTUFaDItMTQIc3BkeS8zLjEGc3BkeS8zCGh0dHAvMS4xAAsAAgEAADMAJgAkAB0AIAf+6C8fSRJSAC7CyUvdR9kDclNR9KLCsCFHpVZ3bC8iAC0AAgEBACsACQgDBAMDAwIDAQAKAAoACAAdABcAGAAZABUAoQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +} \ No newline at end of file