mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2026-10-10 12:13:10 +08:00
chore: allow jls with utls client-fingerprint
This commit is contained in:
@@ -450,6 +450,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ss %s initialize jls-plugin error: %w", addr, err)
|
||||
}
|
||||
jlsConfig.ClientFingerprint = option.ClientFingerprint
|
||||
} else if option.Plugin == kcptun.Mode {
|
||||
obfsMode = kcptun.Mode
|
||||
kcptunOpt := &kcpTunOption{}
|
||||
|
||||
@@ -543,6 +543,7 @@ proxies: # socks5
|
||||
cipher: chacha20-ietf-poly1305
|
||||
password: "password"
|
||||
plugin: jls
|
||||
client-fingerprint: chrome
|
||||
plugin-opts:
|
||||
host: "www.example.com"
|
||||
username: "jls-user"
|
||||
|
||||
@@ -200,6 +200,19 @@ func TestInboundShadowSocks_Restls_tls13(t *testing.T) {
|
||||
testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, false)
|
||||
}
|
||||
|
||||
func testInboundShadowSocksJLS(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption) {
|
||||
t.Parallel()
|
||||
t.Run("Conn", func(t *testing.T) {
|
||||
inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value
|
||||
testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, false)
|
||||
})
|
||||
t.Run("UConn", func(t *testing.T) {
|
||||
inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value
|
||||
outboundOptions.ClientFingerprint = "chrome"
|
||||
testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInboundShadowSocks_JLS(t *testing.T) {
|
||||
username := "jls-user"
|
||||
password := "jls-password"
|
||||
@@ -219,7 +232,7 @@ func TestInboundShadowSocks_JLS(t *testing.T) {
|
||||
"password": password,
|
||||
},
|
||||
}
|
||||
testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, false)
|
||||
testInboundShadowSocksJLS(t, inboundOptions, outboundOptions)
|
||||
}
|
||||
|
||||
func TestInboundShadowSocks_SimpleObfs_Http(t *testing.T) {
|
||||
|
||||
+23
-22
@@ -26,15 +26,17 @@ const (
|
||||
|
||||
var (
|
||||
DefaultALPN = []string{"h2", "http/1.1"}
|
||||
ErrJLSAuthFailed = tls.ErrJLSAuthFailed
|
||||
ErrFallbackCompleted = errors.New("jls: connection relayed to fallback")
|
||||
)
|
||||
|
||||
type User = tls.JLSUser
|
||||
|
||||
type ClientConfig struct {
|
||||
ServerName string
|
||||
User User
|
||||
ALPN []string
|
||||
ServerName string
|
||||
User User
|
||||
ALPN []string
|
||||
ClientFingerprint string
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
@@ -44,14 +46,6 @@ type ServerConfig struct {
|
||||
DialContext func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
}
|
||||
|
||||
type Conn struct {
|
||||
*tls.Conn
|
||||
}
|
||||
|
||||
func (c *Conn) Upstream() any {
|
||||
return c.Conn.NetConn()
|
||||
}
|
||||
|
||||
func NewClientConfig(serverName, username, password string, alpn []string) (*ClientConfig, error) {
|
||||
if serverName == "" {
|
||||
return nil, errors.New("jls: server name is required")
|
||||
@@ -62,23 +56,30 @@ func NewClientConfig(serverName, username, password string, alpn []string) (*Cli
|
||||
if password == "" {
|
||||
return nil, errors.New("jls: password is required")
|
||||
}
|
||||
if alpn == nil {
|
||||
alpn = DefaultALPN
|
||||
}
|
||||
return &ClientConfig{
|
||||
config := &ClientConfig{
|
||||
ServerName: serverName,
|
||||
User: User{Username: username, Password: password},
|
||||
ALPN: append([]string(nil), alpn...),
|
||||
}, nil
|
||||
}
|
||||
if alpn != nil {
|
||||
config.ALPN = append([]string{}, alpn...)
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, conn net.Conn, config *ClientConfig) (net.Conn, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("jls: nil client config")
|
||||
}
|
||||
if client, ok, err := newUTLSClient(ctx, conn, config); ok {
|
||||
return client, err
|
||||
}
|
||||
alpn := config.ALPN
|
||||
if alpn == nil {
|
||||
alpn = DefaultALPN
|
||||
}
|
||||
tlsConn := tls.Client(conn, &tls.Config{
|
||||
ServerName: config.ServerName,
|
||||
NextProtos: append([]string(nil), config.ALPN...),
|
||||
NextProtos: append([]string(nil), alpn...),
|
||||
RootCAs: ca.GetCertPool(),
|
||||
Time: ntp.Now,
|
||||
JLSConfig: &tls.JLSConfig{
|
||||
@@ -90,9 +91,9 @@ func NewClient(ctx context.Context, conn net.Conn, config *ClientConfig) (net.Co
|
||||
return nil, err
|
||||
}
|
||||
if !tlsConn.ConnectionState().JLS.Authenticated {
|
||||
return nil, tls.ErrJLSAuthFailed
|
||||
return nil, ErrJLSAuthFailed
|
||||
}
|
||||
return &Conn{Conn: tlsConn}, nil
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
func NewServerConfig(sni, dest string, users []User, alpn []string, rateLimit uint64, dialContext func(context.Context, string, string) (net.Conn, error)) (*ServerConfig, error) {
|
||||
@@ -166,9 +167,9 @@ func Server(ctx context.Context, conn net.Conn, config *ServerConfig) (net.Conn,
|
||||
}
|
||||
recorder.discard()
|
||||
if !tlsConn.ConnectionState().JLS.Authenticated {
|
||||
return nil, tls.ErrJLSAuthFailed
|
||||
return nil, ErrJLSAuthFailed
|
||||
}
|
||||
return &Conn{Conn: tlsConn}, nil
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
func relayFallback(ctx context.Context, inbound net.Conn, prefix []byte, config *ServerConfig) error {
|
||||
|
||||
+156
-2
@@ -11,10 +11,24 @@ import (
|
||||
|
||||
"github.com/metacubex/mihomo/component/ca"
|
||||
|
||||
"github.com/metacubex/http"
|
||||
"github.com/metacubex/http/httptest"
|
||||
tls "github.com/metacubex/jls-tls"
|
||||
)
|
||||
|
||||
func TestJLSClientServer(t *testing.T) {
|
||||
for _, clientFingerprint := range []string{"", "chrome"} {
|
||||
name := "Go"
|
||||
if clientFingerprint != "" {
|
||||
name = "uTLS"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
testJLSClientServer(t, clientFingerprint)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testJLSClientServer(t *testing.T, clientFingerprint string) {
|
||||
user := User{Username: "test-user", Password: "test-password"}
|
||||
serverConfig, err := NewServerConfig("camouflage.example", "camouflage.example:443", []User{user}, nil, 0, func(context.Context, string, string) (net.Conn, error) {
|
||||
return nil, errors.New("authenticated JLS connection dialed fallback")
|
||||
@@ -26,8 +40,9 @@ func TestJLSClientServer(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clientConfig.ClientFingerprint = clientFingerprint
|
||||
|
||||
serverSide, clientSide := net.Pipe()
|
||||
serverSide, clientSide := newLocalTCPPair(t)
|
||||
serverDone := make(chan error, 1)
|
||||
go func() {
|
||||
conn, err := Server(context.Background(), serverSide, serverConfig)
|
||||
@@ -36,7 +51,7 @@ func TestJLSClientServer(t *testing.T) {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
state := conn.(*Conn).ConnectionState()
|
||||
state := conn.(*tls.Conn).ConnectionState()
|
||||
if !state.JLS.Authenticated || state.JLS.User != user.Username {
|
||||
serverDone <- errors.New("server did not authenticate JLS user")
|
||||
return
|
||||
@@ -66,6 +81,111 @@ func TestJLSClientServer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestJLSUTLSClientRejectsInvalidFallbackCertificate(t *testing.T) {
|
||||
tlsConfig := newTestTLSServerConfig(t, tls.VersionTLS13)
|
||||
serverSide, clientSide := newLocalTCPPair(t)
|
||||
serverDone := make(chan error, 1)
|
||||
go func() {
|
||||
server := tls.Server(serverSide, tlsConfig)
|
||||
serverDone <- server.Handshake()
|
||||
_ = serverSide.Close()
|
||||
}()
|
||||
|
||||
config, err := NewClientConfig("camouflage.example", "user", "password", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config.ClientFingerprint = "chrome"
|
||||
if conn, clientErr := NewClient(context.Background(), clientSide, config); clientErr == nil || errors.Is(clientErr, ErrJLSAuthFailed) {
|
||||
if conn != nil {
|
||||
_ = conn.Close()
|
||||
}
|
||||
t.Fatalf("client error = %v, want fallback certificate verification error", clientErr)
|
||||
}
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func TestJLSUTLSClientFallback(t *testing.T) {
|
||||
for _, protocol := range []string{"http/1.1", "h2"} {
|
||||
t.Run(protocol, func(t *testing.T) {
|
||||
requestProtocol := make(chan string, 1)
|
||||
server := httptest.NewUnstartedServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
requestProtocol <- request.Proto
|
||||
writer.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
server.EnableHTTP2 = protocol == "h2"
|
||||
server.StartTLS()
|
||||
defer func() {
|
||||
server.CloseClientConnections()
|
||||
server.Close()
|
||||
}()
|
||||
ca.GetCertPool().AddCert(server.Certificate())
|
||||
|
||||
clientSide, err := net.Dial("tcp", server.Listener.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config, err := NewClientConfig("example.com", "user", "password", []string{protocol})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config.ClientFingerprint = "chrome"
|
||||
if conn, clientErr := NewClient(context.Background(), clientSide, config); !errors.Is(clientErr, ErrJLSAuthFailed) {
|
||||
if conn != nil {
|
||||
_ = conn.Close()
|
||||
}
|
||||
t.Fatalf("client error = %v, want %v", clientErr, ErrJLSAuthFailed)
|
||||
}
|
||||
|
||||
wantProtocol := "HTTP/1.1"
|
||||
if protocol == "h2" {
|
||||
wantProtocol = "HTTP/2.0"
|
||||
}
|
||||
select {
|
||||
case got := <-requestProtocol:
|
||||
if got != wantProtocol {
|
||||
t.Fatalf("fallback protocol = %q, want %q", got, wantProtocol)
|
||||
}
|
||||
default:
|
||||
t.Fatal("fallback HTTP request was not sent")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJLSUTLSClientRejectsHelloRetryRequest(t *testing.T) {
|
||||
user := User{Username: "user", Password: "password"}
|
||||
serverConfig, err := NewServerConfig("camouflage.example", "camouflage.example:443", []User{user}, nil, 0, func(context.Context, string, string) (net.Conn, error) {
|
||||
return nil, errors.New("HRR failure dialed fallback")
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
serverConfig.TLSConfig.CurvePreferences = []tls.CurveID{tls.CurveP256}
|
||||
|
||||
serverSide, clientSide := newLocalTCPPair(t)
|
||||
serverDone := make(chan error, 1)
|
||||
go func() {
|
||||
defer serverSide.Close()
|
||||
_, serverErr := Server(context.Background(), serverSide, serverConfig)
|
||||
serverDone <- serverErr
|
||||
}()
|
||||
|
||||
clientConfig, err := NewClientConfig("camouflage.example", user.Username, user.Password, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clientConfig.ClientFingerprint = "chrome"
|
||||
if conn, clientErr := NewClient(context.Background(), clientSide, clientConfig); clientErr == nil {
|
||||
_ = conn.Close()
|
||||
t.Fatal("uTLS client unexpectedly completed a HelloRetryRequest handshake")
|
||||
}
|
||||
_ = clientSide.Close()
|
||||
if err = <-serverDone; err == nil {
|
||||
t.Fatal("JLS server unexpectedly completed a HelloRetryRequest handshake")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewServerConfigRequiresDialContext(t *testing.T) {
|
||||
_, err := NewServerConfig(
|
||||
"camouflage.example",
|
||||
@@ -263,3 +383,37 @@ func newTestTLSServerConfig(t *testing.T, version uint16) *tls.Config {
|
||||
MaxVersion: version,
|
||||
}
|
||||
}
|
||||
|
||||
// newLocalTCPPair mirrors crypto/tls's test helper. A real TCP connection has
|
||||
// enough buffering to avoid net.Pipe deadlocks when TLS handshake writes cross,
|
||||
// such as a server ticket flight and the client's Finished message.
|
||||
func newLocalTCPPair(t *testing.T) (server, client net.Conn) {
|
||||
t.Helper()
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
accepted := make(chan struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
}, 1)
|
||||
go func() {
|
||||
conn, acceptErr := listener.Accept()
|
||||
accepted <- struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
}{conn: conn, err: acceptErr}
|
||||
}()
|
||||
client, err = net.Dial("tcp", listener.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result := <-accepted
|
||||
if result.err != nil {
|
||||
_ = client.Close()
|
||||
t.Fatal(result.err)
|
||||
}
|
||||
return result.conn, client
|
||||
}
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
package jls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/metacubex/mihomo/component/ca"
|
||||
tlsC "github.com/metacubex/mihomo/component/tls"
|
||||
"github.com/metacubex/mihomo/ntp"
|
||||
|
||||
"github.com/metacubex/http"
|
||||
"github.com/metacubex/randv2"
|
||||
utls "github.com/metacubex/utls"
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
)
|
||||
|
||||
const (
|
||||
jlsClientHelloType = 1
|
||||
jlsServerHelloType = 2
|
||||
jlsHandshakeHeaderLen = 4
|
||||
jlsHelloLegacyVersionLen = 2
|
||||
jlsHelloRandomLen = 32
|
||||
jlsHelloRandomOffset = jlsHandshakeHeaderLen + jlsHelloLegacyVersionLen
|
||||
jlsRandomSeedLen = jlsHelloRandomLen / 2
|
||||
jlsExtensionPreSharedKey = 41
|
||||
jlsExtensionSupportedVers = 43
|
||||
jlsExtensionKeyShare = 51
|
||||
)
|
||||
|
||||
func newUTLSClient(ctx context.Context, conn net.Conn, config *ClientConfig) (net.Conn, bool, error) {
|
||||
fingerprint, ok := tlsC.GetFingerprint(config.ClientFingerprint)
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
verifier := &utlsJLSVerifier{
|
||||
user: config.User,
|
||||
serverName: config.ServerName,
|
||||
}
|
||||
alpn := config.ALPN
|
||||
if alpn == nil {
|
||||
alpn = DefaultALPN
|
||||
}
|
||||
// Resumption would require recalculating PSK binders after replacing the
|
||||
// ClientHello random. uTLS does not expose a hook for that operation, so this
|
||||
// client deliberately disables session tickets and 0-RTT.
|
||||
uConn := utls.UClient(conn, &utls.Config{
|
||||
ServerName: config.ServerName,
|
||||
NextProtos: append([]string(nil), alpn...),
|
||||
// JLS authenticates the server in VerifyConnection. TLS still verifies
|
||||
// CertificateVerify against the camouflage certificate's public key.
|
||||
InsecureSkipVerify: true,
|
||||
SessionTicketsDisabled: true,
|
||||
Time: ntp.Now,
|
||||
VerifyConnection: verifier.VerifyConnection,
|
||||
}, fingerprint)
|
||||
verifier.UConn = uConn
|
||||
// uTLS has no hook for JLS, so first let it build the complete fingerprint.
|
||||
// JLS authenticates the exact serialized ClientHello with random zeroed;
|
||||
// changing the random before the fingerprint is finalized would authenticate
|
||||
// bytes that can differ from the ClientHello sent on the wire.
|
||||
if err := uConn.BuildHandshakeState(); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
if config.ALPN != nil {
|
||||
overrideUTLSALPN(uConn, config.ALPN)
|
||||
if err := uConn.BuildHandshakeState(); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
}
|
||||
|
||||
hello := uConn.HandshakeState.Hello
|
||||
if !utlsClientHelloSupportsTLS13(hello) {
|
||||
return nil, true, errors.New("jls: uTLS fingerprint does not support TLS 1.3")
|
||||
}
|
||||
authData, err := jlsClientHelloAuthData(hello.Raw)
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
if len(hello.Random) != jlsHelloRandomLen {
|
||||
return nil, true, errors.New("jls: invalid uTLS client random")
|
||||
}
|
||||
fakeRandom, err := jlsBuildFakeRandom(config.User, hello.Random[:jlsRandomSeedLen], authData)
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
if err = uConn.SetClientRandom(fakeRandom); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
if err = uConn.BuildHandshakeState(); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
sentClientHello := append([]byte(nil), uConn.HandshakeState.Hello.Raw...)
|
||||
|
||||
if err = uConn.HandshakeContext(ctx); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
if !verifier.authenticated {
|
||||
// The fallback certificate is valid, so finish with a plausible HTTP
|
||||
// request before returning the JLS authentication error. This is
|
||||
// synchronous because the Shadowsocks caller closes conn on return.
|
||||
jlsClientHTTPFallback(ctx, uConn, config.ServerName, fingerprint)
|
||||
return nil, true, ErrJLSAuthFailed
|
||||
}
|
||||
// A HelloRetryRequest makes uTLS generate another ClientHello. Its public API
|
||||
// cannot recalculate the JLS random at that point, so reject it instead of
|
||||
// accepting a connection whose second ClientHello was not authenticated.
|
||||
if !bytes.Equal(sentClientHello, uConn.HandshakeState.Hello.Raw) {
|
||||
_ = uConn.Close()
|
||||
return nil, true, errors.New("jls: uTLS HelloRetryRequest is not supported")
|
||||
}
|
||||
if uConn.ConnectionState().Version != utls.VersionTLS13 {
|
||||
_ = uConn.Close()
|
||||
return nil, true, ErrJLSAuthFailed
|
||||
}
|
||||
return uConn, true, nil
|
||||
}
|
||||
|
||||
func jlsClientHTTPFallback(ctx context.Context, uConn net.Conn, serverName string, fingerprint utls.ClientHelloID) {
|
||||
defer uConn.Close()
|
||||
// The TLS layer is already established, so HTTP/2 must use h2c mode to avoid
|
||||
// another TLS handshake. Otherwise use HTTP/1 as negotiated by the server.
|
||||
protocols := new(http.Protocols)
|
||||
if conn, ok := uConn.(interface{ ConnectionState() utls.ConnectionState }); ok && conn.ConnectionState().NegotiatedProtocol == "h2" {
|
||||
protocols.SetUnencryptedHTTP2(true)
|
||||
} else {
|
||||
protocols.SetHTTP1(true)
|
||||
}
|
||||
client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return uConn, nil
|
||||
},
|
||||
Protocols: protocols,
|
||||
},
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, "GET", "https://"+serverName, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
request.Header.Set("User-Agent", fingerprint.Client)
|
||||
request.AddCookie(&http.Cookie{Name: "padding", Value: strings.Repeat("0", randv2.IntN(32)+30)})
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
response.Body.Close()
|
||||
client.CloseIdleConnections()
|
||||
}
|
||||
|
||||
func utlsClientHelloSupportsTLS13(hello *utls.PubClientHelloMsg) bool {
|
||||
for _, version := range hello.SupportedVersions {
|
||||
if version == utls.VersionTLS13 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type utlsJLSVerifier struct {
|
||||
*utls.UConn
|
||||
user User
|
||||
serverName string
|
||||
authenticated bool
|
||||
}
|
||||
|
||||
func (v *utlsJLSVerifier) VerifyConnection(state utls.ConnectionState) error {
|
||||
serverHello := v.HandshakeState.ServerHello
|
||||
if serverHello == nil {
|
||||
return errors.New("jls: uTLS server hello is unavailable")
|
||||
}
|
||||
authData, err := jlsServerHelloAuthData(serverHello.Raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !jlsCheckFakeRandom(v.user, serverHello.Random, authData) {
|
||||
return verifyUTLSCertificate(state, v.serverName)
|
||||
}
|
||||
v.authenticated = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyUTLSCertificate(state utls.ConnectionState, serverName string) error {
|
||||
certificates := state.PeerCertificates
|
||||
if len(certificates) == 0 {
|
||||
return errors.New("jls: fallback server sent no certificates")
|
||||
}
|
||||
opts := x509.VerifyOptions{
|
||||
Roots: ca.GetCertPool(),
|
||||
DNSName: serverName,
|
||||
Intermediates: x509.NewCertPool(),
|
||||
CurrentTime: ntp.Now(),
|
||||
}
|
||||
for _, certificate := range certificates[1:] {
|
||||
opts.Intermediates.AddCert(certificate)
|
||||
}
|
||||
_, err := certificates[0].Verify(opts)
|
||||
return err
|
||||
}
|
||||
|
||||
// overrideUTLSALPN keeps ALPS only when h2 remains advertised.
|
||||
func overrideUTLSALPN(conn *utls.UConn, protocols []string) {
|
||||
hasH2 := false
|
||||
for _, protocol := range protocols {
|
||||
if protocol == "h2" {
|
||||
hasH2 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
hasALPN := false
|
||||
extensions := conn.Extensions[:0]
|
||||
for _, extension := range conn.Extensions {
|
||||
switch extension := extension.(type) {
|
||||
case *utls.ALPNExtension:
|
||||
if len(protocols) == 0 {
|
||||
continue
|
||||
}
|
||||
extension.AlpnProtocols = append([]string(nil), protocols...)
|
||||
hasALPN = true
|
||||
case *utls.ApplicationSettingsExtension, *utls.ApplicationSettingsExtensionNew:
|
||||
if !hasH2 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
extensions = append(extensions, extension)
|
||||
}
|
||||
if !hasALPN && len(protocols) > 0 {
|
||||
extensions = append(extensions, &utls.ALPNExtension{
|
||||
AlpnProtocols: append([]string(nil), protocols...),
|
||||
})
|
||||
}
|
||||
conn.Extensions = extensions
|
||||
}
|
||||
|
||||
func jlsClientHelloAuthData(raw []byte) ([]byte, error) {
|
||||
msg, err := cloneJLSHello(raw, jlsClientHelloType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
zeroJLSBytes(msg[jlsHelloRandomOffset : jlsHelloRandomOffset+jlsHelloRandomLen])
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func jlsServerHelloAuthData(raw []byte) ([]byte, error) {
|
||||
msg, err := cloneJLSHello(raw, jlsServerHelloType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := cryptobyte.String(msg)
|
||||
var messageType, compressionMethod uint8
|
||||
var legacyVersion, cipherSuite uint16
|
||||
var body, sessionID, extensions cryptobyte.String
|
||||
var random []byte
|
||||
if !s.ReadUint8(&messageType) ||
|
||||
!s.ReadUint24LengthPrefixed(&body) ||
|
||||
!s.Empty() ||
|
||||
!body.ReadUint16(&legacyVersion) ||
|
||||
!body.ReadBytes(&random, jlsHelloRandomLen) ||
|
||||
!body.ReadUint8LengthPrefixed(&sessionID) ||
|
||||
!body.ReadUint16(&cipherSuite) ||
|
||||
!body.ReadUint8(&compressionMethod) ||
|
||||
!body.ReadUint16LengthPrefixed(&extensions) ||
|
||||
!body.Empty() {
|
||||
return nil, errors.New("jls: invalid uTLS server hello")
|
||||
}
|
||||
|
||||
type extension struct {
|
||||
typeID uint16
|
||||
wire []byte
|
||||
}
|
||||
extensionBytes := extensions
|
||||
parsed := make([]extension, 0, 3)
|
||||
for len(extensions) > 0 {
|
||||
remaining := extensions
|
||||
var typeID uint16
|
||||
var data cryptobyte.String
|
||||
if !extensions.ReadUint16(&typeID) || !extensions.ReadUint16LengthPrefixed(&data) {
|
||||
return nil, errors.New("jls: invalid uTLS server hello extensions")
|
||||
}
|
||||
wireLen := len(remaining) - len(extensions)
|
||||
parsed = append(parsed, extension{typeID: typeID, wire: remaining[:wireLen]})
|
||||
}
|
||||
|
||||
// rustls decodes ServerHello extensions into fields and serializes them in
|
||||
// this order when it calculates the JLS random. The wire order is irrelevant
|
||||
// to TLS but not to JLS, whose authentication input must match byte for byte.
|
||||
canonicalTypes := [...]uint16{
|
||||
jlsExtensionKeyShare,
|
||||
jlsExtensionPreSharedKey,
|
||||
jlsExtensionSupportedVers,
|
||||
}
|
||||
canonical := make(map[uint16][]byte, len(canonicalTypes))
|
||||
for _, ext := range parsed {
|
||||
for _, typeID := range canonicalTypes {
|
||||
if ext.typeID == typeID {
|
||||
canonical[typeID] = ext.wire
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reinsert the canonical extensions at their first original position. Their
|
||||
// encoded bytes and all unrelated extensions remain untouched, and the total
|
||||
// length does not change.
|
||||
extensionOffset := len(msg) - len(extensionBytes)
|
||||
result := append([]byte(nil), msg[:extensionOffset]...)
|
||||
canonicalWritten := false
|
||||
for _, ext := range parsed {
|
||||
if _, ok := canonical[ext.typeID]; ok {
|
||||
if !canonicalWritten {
|
||||
for _, typeID := range canonicalTypes {
|
||||
result = append(result, canonical[typeID]...)
|
||||
}
|
||||
canonicalWritten = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
result = append(result, ext.wire...)
|
||||
}
|
||||
zeroJLSBytes(result[jlsHelloRandomOffset : jlsHelloRandomOffset+jlsHelloRandomLen])
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func cloneJLSHello(raw []byte, messageType byte) ([]byte, error) {
|
||||
if len(raw) < jlsHelloRandomOffset+jlsHelloRandomLen ||
|
||||
raw[0] != messageType ||
|
||||
int(raw[1])<<16|int(raw[2])<<8|int(raw[3]) != len(raw)-jlsHandshakeHeaderLen {
|
||||
return nil, errors.New("jls: invalid uTLS hello")
|
||||
}
|
||||
return append([]byte(nil), raw...), nil
|
||||
}
|
||||
|
||||
func jlsBuildFakeRandom(user User, random16, authData []byte) ([]byte, error) {
|
||||
if len(random16) != jlsRandomSeedLen {
|
||||
return nil, errors.New("jls: random seed must be 16 bytes")
|
||||
}
|
||||
aead, nonce, err := newJLSAEAD(user, authData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return aead.Seal(nil, nonce[:], random16, nil), nil
|
||||
}
|
||||
|
||||
func jlsCheckFakeRandom(user User, fakeRandom, authData []byte) bool {
|
||||
if len(fakeRandom) != jlsHelloRandomLen {
|
||||
return false
|
||||
}
|
||||
aead, nonce, err := newJLSAEAD(user, authData)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
plain, err := aead.Open(nil, nonce[:], fakeRandom, nil)
|
||||
return err == nil && len(plain) == jlsRandomSeedLen
|
||||
}
|
||||
|
||||
func newJLSAEAD(user User, authData []byte) (cipher.AEAD, [sha256.Size]byte, error) {
|
||||
nonce := jlsHash(user.Username, authData)
|
||||
key := jlsHash(user.Password, authData)
|
||||
block, err := aes.NewCipher(key[:])
|
||||
if err != nil {
|
||||
return nil, nonce, err
|
||||
}
|
||||
aead, err := cipher.NewGCMWithNonceSize(block, len(nonce))
|
||||
return aead, nonce, err
|
||||
}
|
||||
|
||||
func jlsHash(value string, authData []byte) (sum [sha256.Size]byte) {
|
||||
hash := sha256.New()
|
||||
_, _ = hash.Write([]byte(value))
|
||||
_, _ = hash.Write(authData)
|
||||
hash.Sum(sum[:0])
|
||||
return sum
|
||||
}
|
||||
|
||||
func zeroJLSBytes(data []byte) {
|
||||
for i := range data {
|
||||
data[i] = 0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user