Do not use default TLS cipher

As per RFC, if TLS server cannot pickup a suitable cipher from a client
list, it has to send handshake_failure alert. For us it means that we
have to route a request to a fronting domain, because we want to have it
exactly like a real webserver does. So, if it misbehaves, so do we.
This commit is contained in:
9seconds
2026-04-07 12:08:14 +02:00
parent efc65f317e
commit 5bf218f9ab
2 changed files with 28 additions and 9 deletions
+10 -9
View File
@@ -5,8 +5,8 @@ import (
"crypto/hmac" "crypto/hmac"
"crypto/sha256" "crypto/sha256"
"crypto/subtle" "crypto/subtle"
"crypto/tls"
"encoding/binary" "encoding/binary"
"errors"
"fmt" "fmt"
"io" "io"
"net" "net"
@@ -25,7 +25,6 @@ const (
// https://medium.com/asecuritysite-when-bob-met-alice/in-cybersecurity-what-is-grease-9f8850558dea // https://medium.com/asecuritysite-when-bob-met-alice/in-cybersecurity-what-is-grease-9f8850558dea
GreaseMask = 0x0f0f GreaseMask = 0x0f0f
GreaseValueType = 0x0a0a GreaseValueType = 0x0a0a
DefaultCipher = tls.TLS_AES_128_GCM_SHA256
sniDNSNamesListType = 0 sniDNSNamesListType = 0
) )
@@ -33,6 +32,8 @@ const (
var ( var (
emptyRandom = [RandomLen]byte{} emptyRandom = [RandomLen]byte{}
extTypeSNI = [2]byte{} extTypeSNI = [2]byte{}
ErrCannotFindCipher = errors.New("cannot find a cipher")
) )
type ClientHello struct { type ClientHello struct {
@@ -110,9 +111,7 @@ func parseHandshake(r io.Reader) (*ClientHello, error) {
return nil, fmt.Errorf("cannot read client version: %w", err) return nil, fmt.Errorf("cannot read client version: %w", err)
} }
hello := &ClientHello{ hello := &ClientHello{}
CipherSuite: DefaultCipher,
}
if _, err := io.ReadFull(r, hello.Random[:]); err != nil { if _, err := io.ReadFull(r, hello.Random[:]); err != nil {
return nil, fmt.Errorf("cannot read client random: %w", err) return nil, fmt.Errorf("cannot read client random: %w", err)
@@ -133,7 +132,6 @@ func parseHandshake(r io.Reader) (*ClientHello, error) {
} }
cipherSuiteLen := int64(binary.BigEndian.Uint16(header[:])) cipherSuiteLen := int64(binary.BigEndian.Uint16(header[:]))
foundCipher := false
// Pick the first non-GREASE cipher suite from the list. // Pick the first non-GREASE cipher suite from the list.
// Real TLS servers never select GREASE values (RFC 8701, pattern 0x?a?a), // Real TLS servers never select GREASE values (RFC 8701, pattern 0x?a?a),
@@ -144,17 +142,20 @@ func parseHandshake(r io.Reader) (*ClientHello, error) {
return nil, fmt.Errorf("cannot read cipher suite: %w", err) return nil, fmt.Errorf("cannot read cipher suite: %w", err)
} }
if foundCipher { if hello.CipherSuite != 0 {
// do not forget we have to scan until the end
continue continue
} }
if cs := binary.BigEndian.Uint16(header[:]); cs&GreaseMask != GreaseValueType { if cs := binary.BigEndian.Uint16(header[:]); cs&GreaseMask != GreaseValueType {
hello.CipherSuite = cs hello.CipherSuite = cs
// do not forget we have to scan until the end
foundCipher = true
} }
} }
if hello.CipherSuite == 0 {
return nil, ErrCannotFindCipher
}
if _, err := io.ReadFull(r, header[:1]); err != nil { if _, err := io.ReadFull(r, header[:1]); err != nil {
return nil, fmt.Errorf("cannot read compression methods length: %w", err) return nil, fmt.Errorf("cannot read compression methods length: %w", err)
} }
@@ -2,6 +2,7 @@ package fake_test
import ( import (
"bytes" "bytes"
cryptotls "crypto/tls"
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"io" "io"
@@ -234,9 +235,24 @@ func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotSkipRemainingCiph
suite.ErrorContains(err, "cannot read cipher suite") suite.ErrorContains(err, "cannot read cipher suite")
} }
func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotFindCipher() {
// All cipher suites are GREASE values — must return ErrCannotFindCipher.
body := make([]byte, 2+fake.RandomLen+1+2+4+1)
binary.BigEndian.PutUint16(body[2+fake.RandomLen+1:], 4)
binary.BigEndian.PutUint16(body[2+fake.RandomLen+1+2:], 0x0a0a)
binary.BigEndian.PutUint16(body[2+fake.RandomLen+1+2+2:], 0x1a1a)
body[2+fake.RandomLen+1+2+4] = 1
suite.writeBody(body)
_, err := fake.ReadClientHello(suite.connMock, suite.secret.Key[:], suite.secret.Host, TolerateTime)
suite.ErrorIs(err, fake.ErrCannotFindCipher)
}
func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadCompressionMethodsLength() { func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadCompressionMethodsLength() {
body := make([]byte, 2+fake.RandomLen+1+2+2) body := make([]byte, 2+fake.RandomLen+1+2+2)
binary.BigEndian.PutUint16(body[2+fake.RandomLen+1:], 2) binary.BigEndian.PutUint16(body[2+fake.RandomLen+1:], 2)
binary.BigEndian.PutUint16(body[2+fake.RandomLen+1+2:], cryptotls.TLS_AES_128_GCM_SHA256)
suite.writeBody(body) suite.writeBody(body)
@@ -247,6 +263,7 @@ func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotReadCompressionMe
func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotSkipCompressionMethods() { func (suite *ParseClientHelloHandshakeBodyTestSuite) TestCannotSkipCompressionMethods() {
body := make([]byte, 2+fake.RandomLen+1+2+2+1) body := make([]byte, 2+fake.RandomLen+1+2+2+1)
binary.BigEndian.PutUint16(body[2+fake.RandomLen+1:], 2) binary.BigEndian.PutUint16(body[2+fake.RandomLen+1:], 2)
binary.BigEndian.PutUint16(body[2+fake.RandomLen+1+2:], cryptotls.TLS_AES_128_GCM_SHA256)
body[2+fake.RandomLen+1+2+2] = 1 body[2+fake.RandomLen+1+2+2] = 1
suite.writeBody(body) suite.writeBody(body)
@@ -282,6 +299,7 @@ func (suite *ParseClientHelloSNITestSuite) writeExtensions(extensions []byte) {
// cipherSuite(2) + compressionLen(1) + compression(1) = 41 // cipherSuite(2) + compressionLen(1) + compression(1) = 41
body := make([]byte, 41) body := make([]byte, 41)
binary.BigEndian.PutUint16(body[35:], 2) binary.BigEndian.PutUint16(body[35:], 2)
binary.BigEndian.PutUint16(body[37:], cryptotls.TLS_AES_128_GCM_SHA256)
body[39] = 1 body[39] = 1
suite.readBuf.Write(body) suite.readBuf.Write(body)