From da0b13cca0517f213c7949ac44afeb4dad8f4a81 Mon Sep 17 00:00:00 2001
From: Hirbod Behnam <chrome.hiri.angry@gmail.com>
Date: Sat, 22 Oct 2022 04:36:36 +0330
Subject: [PATCH] Added uTLS to gRPC (#1264)

* Added uTLS to gRPC

* Use base 16 of ciphers as StandardName
---
 transport/internet/grpc/dial.go |   8 ++-
 transport/internet/tls/grpc.go  | 107 ++++++++++++++++++++++++++++++++
 2 files changed, 114 insertions(+), 1 deletion(-)
 create mode 100644 transport/internet/tls/grpc.go

diff --git a/transport/internet/grpc/dial.go b/transport/internet/grpc/dial.go
index 07fe40e8..9836d93a 100644
--- a/transport/internet/grpc/dial.go
+++ b/transport/internet/grpc/dial.go
@@ -121,7 +121,13 @@ func getGrpcClient(ctx context.Context, dest net.Destination, streamSettings *in
 	}
 
 	if tlsConfig != nil {
-		dialOptions = append(dialOptions, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig.GetTLSConfig())))
+		var transportCredential credentials.TransportCredentials
+		if fingerprint, exists := tls.Fingerprints[tlsConfig.Fingerprint]; exists {
+			transportCredential = tls.NewGrpcUtls(tlsConfig.GetTLSConfig(), fingerprint)
+		} else { // Fallback to normal gRPC TLS
+			transportCredential = credentials.NewTLS(tlsConfig.GetTLSConfig())
+		}
+		dialOptions = append(dialOptions, grpc.WithTransportCredentials(transportCredential))
 	} else {
 		dialOptions = append(dialOptions, grpc.WithInsecure())
 	}
diff --git a/transport/internet/tls/grpc.go b/transport/internet/tls/grpc.go
new file mode 100644
index 00000000..ede921b7
--- /dev/null
+++ b/transport/internet/tls/grpc.go
@@ -0,0 +1,107 @@
+package tls
+
+import (
+	"context"
+	gotls "crypto/tls"
+	utls "github.com/refraction-networking/utls"
+	"google.golang.org/grpc/credentials"
+	"net"
+	"net/url"
+	"strconv"
+)
+
+// grpcUtlsInfo contains the auth information for a TLS authenticated connection.
+// It implements the AuthInfo interface.
+type grpcUtlsInfo struct {
+	State utls.ConnectionState
+	credentials.CommonAuthInfo
+	// This API is experimental.
+	SPIFFEID *url.URL
+}
+
+// AuthType returns the type of TLSInfo as a string.
+func (t grpcUtlsInfo) AuthType() string {
+	return "utls"
+}
+
+// GetSecurityValue returns security info requested by channelz.
+func (t grpcUtlsInfo) GetSecurityValue() credentials.ChannelzSecurityValue {
+	v := &credentials.TLSChannelzSecurityValue{
+		StandardName: "0x" + strconv.FormatUint(uint64(t.State.CipherSuite), 16),
+	}
+	// Currently there's no way to get LocalCertificate info from tls package.
+	if len(t.State.PeerCertificates) > 0 {
+		v.RemoteCertificate = t.State.PeerCertificates[0].Raw
+	}
+	return v
+}
+
+// grpcUtls is the credentials required for authenticating a connection using TLS.
+type grpcUtls struct {
+	config      *gotls.Config
+	fingerprint *utls.ClientHelloID
+}
+
+func (c grpcUtls) Info() credentials.ProtocolInfo {
+	return credentials.ProtocolInfo{
+		SecurityProtocol: "tls",
+		SecurityVersion:  "1.2",
+		ServerName:       c.config.ServerName,
+	}
+}
+
+func (c *grpcUtls) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (_ net.Conn, _ credentials.AuthInfo, err error) {
+	// use local cfg to avoid clobbering ServerName if using multiple endpoints
+	cfg := c.config.Clone()
+	if cfg.ServerName == "" {
+		serverName, _, err := net.SplitHostPort(authority)
+		if err != nil {
+			// If the authority had no host port or if the authority cannot be parsed, use it as-is.
+			serverName = authority
+		}
+		cfg.ServerName = serverName
+	}
+	conn := UClient(rawConn, cfg, c.fingerprint).(*UConn)
+	errChannel := make(chan error, 1)
+	go func() {
+		errChannel <- conn.Handshake()
+		close(errChannel)
+	}()
+	select {
+	case err := <-errChannel:
+		if err != nil {
+			conn.Close()
+			return nil, nil, err
+		}
+	case <-ctx.Done():
+		conn.Close()
+		return nil, nil, ctx.Err()
+	}
+	tlsInfo := grpcUtlsInfo{
+		State: conn.ConnectionState(),
+		CommonAuthInfo: credentials.CommonAuthInfo{
+			SecurityLevel: credentials.PrivacyAndIntegrity,
+		},
+	}
+	return conn, tlsInfo, nil
+}
+
+// ServerHandshake will always panic. We don't support running uTLS as server.
+func (c *grpcUtls) ServerHandshake(net.Conn) (net.Conn, credentials.AuthInfo, error) {
+	panic("not available!")
+}
+
+func (c *grpcUtls) Clone() credentials.TransportCredentials {
+	return NewGrpcUtls(c.config, c.fingerprint)
+}
+
+func (c *grpcUtls) OverrideServerName(serverNameOverride string) error {
+	c.config.ServerName = serverNameOverride
+	return nil
+}
+
+// NewGrpcUtls uses c to construct a TransportCredentials based on uTLS.
+func NewGrpcUtls(c *gotls.Config, fingerprint *utls.ClientHelloID) credentials.TransportCredentials {
+	tc := &grpcUtls{c.Clone(), fingerprint}
+	return tc
+}