diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 31701fe0a..e58a519ff 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -603,6 +603,10 @@ func (c *TLSCertConfig) Build() (*tls.Certificate, error) { certificate.Usage = tls.Certificate_AUTHORITY_VERIFY case "issue": certificate.Usage = tls.Certificate_AUTHORITY_ISSUE + case "client-cert": + certificate.Usage = tls.Certificate_MTLS_CLIENT_CERT + case "client-ca": + certificate.Usage = tls.Certificate_MTLS_CLIENT_CA default: certificate.Usage = tls.Certificate_ENCIPHERMENT } @@ -653,6 +657,7 @@ type TLSConfig struct { ECHServerKeys string `json:"echServerKeys"` ECHConfigList string `json:"echConfigList"` ECHSocketSettings *SocketConfig `json:"echSockopt"` + ClientAuth string `json:"clientAuth"` } // Build implements Buildable. @@ -741,6 +746,7 @@ func (c *TLSConfig) Build() (proto.Message, error) { config.EchSocketSettings = ss } + config.ClientAuth = c.ClientAuth return config, nil } diff --git a/transport/internet/tls/config.go b/transport/internet/tls/config.go index 2dc317c55..0f5a0246f 100644 --- a/transport/internet/tls/config.go +++ b/transport/internet/tls/config.go @@ -87,6 +87,27 @@ func (c *Config) getCustomCA() []*Certificate { return certs } +func (c *Config) getClientCert() []*Certificate { + certs := make([]*Certificate, 0, len(c.Certificate)) + for _, certificate := range c.Certificate { + if certificate.Usage == Certificate_MTLS_CLIENT_CERT { + certs = append(certs, certificate) + setupHotReload(certificate) + } + } + return certs +} + +func (c *Config) getClientCA() []*Certificate { + certs := make([]*Certificate, 0, len(c.Certificate)) + for _, certificate := range c.Certificate { + if certificate.Usage == Certificate_MTLS_CLIENT_CA { + certs = append(certs, certificate) + } + } + return certs +} + func getGetCertificateFunc(c *tls.Config, ca []*Certificate) func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { var access sync.RWMutex @@ -234,6 +255,19 @@ func (c *Config) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, e return defaultCert, nil } +func (c *Config) getClientCertificate(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) { + for _, cert := range c.Certificate { + parsed := cert.getX509KeyPair() + if cert.Usage != Certificate_MTLS_CLIENT_CERT || parsed == nil { + continue + } + if err := cri.SupportsCertificate(parsed); err == nil { + return parsed, nil + } + } + return nil, errNoCertificates +} + func (c *Config) parseServerName() string { if IsFromMitm(c.ServerName) { return "" @@ -376,6 +410,18 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { } config.GetCertificate = c.getCertificate } + if len(c.getClientCert()) > 0 { + config.GetClientCertificate = c.getClientCertificate + } + if clientCA := c.getClientCA(); len(clientCA) > 0 { + clientCAPool := x509.NewCertPool() + for _, cert := range clientCA { + if !clientCAPool.AppendCertsFromPEM(cert.Certificate) { + errors.LogError(context.Background(), errors.New("failed to append client CA certificate")) + } + } + config.ClientCAs = clientCAPool + } if sn := c.parseServerName(); len(sn) > 0 { config.ServerName = sn @@ -438,6 +484,11 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { } } + config.ClientAuth = ParseClientAuth(c.ClientAuth) + if config.ClientAuth >= tls.VerifyClientCertIfGiven && config.ClientCAs == nil { + errors.LogWarning(context.Background(), "clientAuth is set to ", c.ClientAuth, " but no client CA is provided") + } + return config } @@ -483,17 +534,17 @@ func ConfigFromStreamSettings(settings *internet.MemoryStreamConfig) *Config { return config } -func ParseCurveName(curveNames []string) []tls.CurveID { - curveMap := map[string]tls.CurveID{ - "curvep256": tls.CurveP256, - "curvep384": tls.CurveP384, - "curvep521": tls.CurveP521, - "x25519": tls.X25519, - "x25519mlkem768": tls.X25519MLKEM768, - "secp256r1mlkem768": tls.SecP256r1MLKEM768, - "secp384r1mlkem1024": tls.SecP384r1MLKEM1024, - } +var curveMap = map[string]tls.CurveID{ + "curvep256": tls.CurveP256, + "curvep384": tls.CurveP384, + "curvep521": tls.CurveP521, + "x25519": tls.X25519, + "x25519mlkem768": tls.X25519MLKEM768, + "secp256r1mlkem768": tls.SecP256r1MLKEM768, + "secp384r1mlkem1024": tls.SecP384r1MLKEM1024, +} +func ParseCurveName(curveNames []string) []tls.CurveID { var curveIDs []tls.CurveID for _, name := range curveNames { if curveID, ok := curveMap[strings.ToLower(name)]; ok { @@ -505,6 +556,25 @@ func ParseCurveName(curveNames []string) []tls.CurveID { return curveIDs } +var clientAuthMap = map[string]tls.ClientAuthType{ + "noclientcert": tls.NoClientCert, + "requestclientcert": tls.RequestClientCert, + "requireanyclientcert": tls.RequireAnyClientCert, + "verifyclientcertifgiven": tls.VerifyClientCertIfGiven, + "requireandverifyclientcert": tls.RequireAndVerifyClientCert, +} + +func ParseClientAuth(clientAuth string) tls.ClientAuthType { + if clientAuth == "" { + return tls.NoClientCert + } + if clientAuthType, ok := clientAuthMap[strings.ToLower(clientAuth)]; ok { + return clientAuthType + } + errors.LogWarning(context.Background(), "unsupported clientAuth: "+clientAuth) + return tls.NoClientCert +} + func IsFromMitm(str string) bool { return strings.ToLower(str) == "frommitm" } diff --git a/transport/internet/tls/config.pb.go b/transport/internet/tls/config.pb.go index 01822d2cd..5b727553e 100644 --- a/transport/internet/tls/config.pb.go +++ b/transport/internet/tls/config.pb.go @@ -28,6 +28,8 @@ const ( Certificate_ENCIPHERMENT Certificate_Usage = 0 Certificate_AUTHORITY_VERIFY Certificate_Usage = 1 Certificate_AUTHORITY_ISSUE Certificate_Usage = 2 + Certificate_MTLS_CLIENT_CERT Certificate_Usage = 3 + Certificate_MTLS_CLIENT_CA Certificate_Usage = 4 ) // Enum value maps for Certificate_Usage. @@ -36,11 +38,15 @@ var ( 0: "ENCIPHERMENT", 1: "AUTHORITY_VERIFY", 2: "AUTHORITY_ISSUE", + 3: "MTLS_CLIENT_CERT", + 4: "MTLS_CLIENT_CA", } Certificate_Usage_value = map[string]int32{ "ENCIPHERMENT": 0, "AUTHORITY_VERIFY": 1, "AUTHORITY_ISSUE": 2, + "MTLS_CLIENT_CERT": 3, + "MTLS_CLIENT_CA": 4, } ) @@ -231,6 +237,7 @@ type Config struct { EchConfigList string `protobuf:"bytes,19,opt,name=ech_config_list,json=echConfigList,proto3" json:"ech_config_list,omitempty"` EchSocketSettings *internet.SocketConfig `protobuf:"bytes,21,opt,name=ech_socket_settings,json=echSocketSettings,proto3" json:"ech_socket_settings,omitempty"` PinnedPeerCertSha256 [][]byte `protobuf:"bytes,22,rep,name=pinned_peer_cert_sha256,json=pinnedPeerCertSha256,proto3" json:"pinned_peer_cert_sha256,omitempty"` + ClientAuth string `protobuf:"bytes,23,opt,name=client_auth,json=clientAuth,proto3" json:"client_auth,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -384,11 +391,18 @@ func (x *Config) GetPinnedPeerCertSha256() [][]byte { return nil } +func (x *Config) GetClientAuth() string { + if x != nil { + return x.ClientAuth + } + return "" +} + var File_transport_internet_tls_config_proto protoreflect.FileDescriptor const file_transport_internet_tls_config_proto_rawDesc = "" + "\n" + - "#transport/internet/tls/config.proto\x12\x1bxray.transport.internet.tls\x1a\x1ftransport/internet/config.proto\"\xe4\x03\n" + + "#transport/internet/tls/config.proto\x12\x1bxray.transport.internet.tls\x1a\x1ftransport/internet/config.proto\"\x8e\x04\n" + "\vCertificate\x12 \n" + "\vcertificate\x18\x01 \x01(\fR\vcertificate\x12\x10\n" + "\x03key\x18\x02 \x01(\fR\x03key\x12D\n" + @@ -403,11 +417,13 @@ const file_transport_internet_tls_config_proto_rawDesc = "" + "\tocsp_data\x18\n" + " \x01(\fR\bocspData\x12\x1f\n" + "\vlast_reload\x18\v \x01(\x03R\n" + - "lastReload\"D\n" + + "lastReload\"n\n" + "\x05Usage\x12\x10\n" + "\fENCIPHERMENT\x10\x00\x12\x14\n" + "\x10AUTHORITY_VERIFY\x10\x01\x12\x13\n" + - "\x0fAUTHORITY_ISSUE\x10\x02\"\xa6\x06\n" + + "\x0fAUTHORITY_ISSUE\x10\x02\x12\x14\n" + + "\x10MTLS_CLIENT_CERT\x10\x03\x12\x12\n" + + "\x0eMTLS_CLIENT_CA\x10\x04\"\xc7\x06\n" + "\x06Config\x12J\n" + "\vcertificate\x18\x02 \x03(\v2(.xray.transport.internet.tls.CertificateR\vcertificate\x12\x1f\n" + "\vserver_name\x18\x03 \x01(\tR\n" + @@ -428,7 +444,9 @@ const file_transport_internet_tls_config_proto_rawDesc = "" + "\x0fech_server_keys\x18\x12 \x01(\fR\rechServerKeys\x12&\n" + "\x0fech_config_list\x18\x13 \x01(\tR\rechConfigList\x12U\n" + "\x13ech_socket_settings\x18\x15 \x01(\v2%.xray.transport.internet.SocketConfigR\x11echSocketSettings\x125\n" + - "\x17pinned_peer_cert_sha256\x18\x16 \x03(\fR\x14pinnedPeerCertSha256Bs\n" + + "\x17pinned_peer_cert_sha256\x18\x16 \x03(\fR\x14pinnedPeerCertSha256\x12\x1f\n" + + "\vclient_auth\x18\x17 \x01(\tR\n" + + "clientAuthBs\n" + "\x1fcom.xray.transport.internet.tlsP\x01Z0github.com/xtls/xray-core/transport/internet/tls\xaa\x02\x1bXray.Transport.Internet.Tlsb\x06proto3" var ( diff --git a/transport/internet/tls/config.proto b/transport/internet/tls/config.proto index 8801b4998..fd5f38807 100644 --- a/transport/internet/tls/config.proto +++ b/transport/internet/tls/config.proto @@ -19,6 +19,8 @@ message Certificate { ENCIPHERMENT = 0; AUTHORITY_VERIFY = 1; AUTHORITY_ISSUE = 2; + MTLS_CLIENT_CERT = 3; + MTLS_CLIENT_CA = 4; } Usage usage = 3; @@ -91,4 +93,6 @@ message Config { SocketConfig ech_socket_settings = 21; repeated bytes pinned_peer_cert_sha256 = 22; + + string client_auth = 23; }