diff --git a/common/tls/std_client.go b/common/tls/std_client.go index 90f51821..c6be4d57 100644 --- a/common/tls/std_client.go +++ b/common/tls/std_client.go @@ -128,5 +128,40 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb } tlsConfig.RootCAs = certPool } + if options.ClientAuth { + // explicitly call mutual verification + var cert []byte + var key []byte + + if len(options.ClientCertificate) > 0 { + cert = []byte(strings.Join(options.ClientCertificate, "\n")) + } else if options.ClientCertificatePath != "" { + content, err := os.ReadFile(options.ClientCertificatePath) + if err != nil { + return nil, E.Cause(err, "read client certificate") + } + cert = content + } + if len(options.ClientKey) > 0 { + key = []byte(strings.Join(options.ClientKey, "\n")) + } else if options.ClientKeyPath != "" { + content, err := os.ReadFile(options.ClientKeyPath) + if err != nil { + return nil, E.Cause(err, "read client key") + } + key = content + } + if cert == nil { + return nil, E.New("missing client certificate") + } else if key == nil { + return nil, E.New("missing client key") + } + + keyPair, err := tls.X509KeyPair(cert, key) + if err != nil { + return nil, E.New(err, "parse x509 key pair") + } + tlsConfig.Certificates = []tls.Certificate{keyPair} + } return &STDClientConfig{&tlsConfig}, nil } diff --git a/common/tls/std_server.go b/common/tls/std_server.go index 28a94cf1..3ee25cf1 100644 --- a/common/tls/std_server.go +++ b/common/tls/std_server.go @@ -3,6 +3,7 @@ package tls import ( "context" "crypto/tls" + "crypto/x509" "net" "os" "strings" @@ -212,6 +213,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound } var certificate []byte var key []byte + var clientca []byte if acmeService == nil { if len(options.Certificate) > 0 { certificate = []byte(strings.Join(options.Certificate, "\n")) @@ -249,6 +251,29 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound tlsConfig.Certificates = []tls.Certificate{keyPair} } } + if options.ClientAuth { + // require client authentication + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + if len(options.ClientCA) > 0 { + clientca = []byte(strings.Join(options.ClientCA, "\n")) + } else if options.ClientCAPath != "" { + content, err := os.ReadFile(options.ClientCAPath) + if err != nil { + return nil, E.Cause(err, "read client CA") + } + clientca = content + } + // add client ca certpool + if len(clientca) > 0 { + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM(clientca) { + return nil, E.New("failed to parse client CA:\n\n", clientca) + } + tlsConfig.ClientCAs = certPool + } + } else { + tlsConfig.ClientAuth = tls.NoClientCert + } return &STDServerConfig{ config: tlsConfig, logger: logger, @@ -257,5 +282,6 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound key: key, certificatePath: options.CertificatePath, keyPath: options.KeyPath, + watcher: &fsnotify.Watcher{}, }, nil } diff --git a/option/tls.go b/option/tls.go index a38b4ef5..94788149 100644 --- a/option/tls.go +++ b/option/tls.go @@ -1,36 +1,45 @@ package option type InboundTLSOptions struct { - Enabled bool `json:"enabled,omitempty"` - ServerName string `json:"server_name,omitempty"` - Insecure bool `json:"insecure,omitempty"` - ALPN Listable[string] `json:"alpn,omitempty"` - MinVersion string `json:"min_version,omitempty"` - MaxVersion string `json:"max_version,omitempty"` - CipherSuites Listable[string] `json:"cipher_suites,omitempty"` - Certificate Listable[string] `json:"certificate,omitempty"` - CertificatePath string `json:"certificate_path,omitempty"` - Key Listable[string] `json:"key,omitempty"` - KeyPath string `json:"key_path,omitempty"` - ACME *InboundACMEOptions `json:"acme,omitempty"` - ECH *InboundECHOptions `json:"ech,omitempty"` - Reality *InboundRealityOptions `json:"reality,omitempty"` + Enabled bool `json:"enabled,omitempty"` + ServerName string `json:"server_name,omitempty"` + Insecure bool `json:"insecure,omitempty"` + ALPN Listable[string] `json:"alpn,omitempty"` + MinVersion string `json:"min_version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + CipherSuites Listable[string] `json:"cipher_suites,omitempty"` + Certificate Listable[string] `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + Key Listable[string] `json:"key,omitempty"` + KeyPath string `json:"key_path,omitempty"` + ClientAuth bool `json:"clientAuth,omitempty"` + // , Requireandverify + ClientCA Listable[string] `json:"clientCA,omitempty"` + ClientCAPath string `json:"clientCA_path,omitempty"` + ACME *InboundACMEOptions `json:"acme,omitempty"` + ECH *InboundECHOptions `json:"ech,omitempty"` + Reality *InboundRealityOptions `json:"reality,omitempty"` } type OutboundTLSOptions struct { - Enabled bool `json:"enabled,omitempty"` - DisableSNI bool `json:"disable_sni,omitempty"` - ServerName string `json:"server_name,omitempty"` - Insecure bool `json:"insecure,omitempty"` - ALPN Listable[string] `json:"alpn,omitempty"` - MinVersion string `json:"min_version,omitempty"` - MaxVersion string `json:"max_version,omitempty"` - CipherSuites Listable[string] `json:"cipher_suites,omitempty"` - Certificate Listable[string] `json:"certificate,omitempty"` - CertificatePath string `json:"certificate_path,omitempty"` - ECH *OutboundECHOptions `json:"ech,omitempty"` - UTLS *OutboundUTLSOptions `json:"utls,omitempty"` - Reality *OutboundRealityOptions `json:"reality,omitempty"` + Enabled bool `json:"enabled,omitempty"` + DisableSNI bool `json:"disable_sni,omitempty"` + ServerName string `json:"server_name,omitempty"` + Insecure bool `json:"insecure,omitempty"` + ALPN Listable[string] `json:"alpn,omitempty"` + MinVersion string `json:"min_version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + CipherSuites Listable[string] `json:"cipher_suites,omitempty"` + Certificate Listable[string] `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + ClientAuth bool `json:"clientAuth,omitempty"` + ClientKey Listable[string] `json:"client_key,omitempty"` + ClientKeyPath string `json:"client_key_path,omitempty"` + ClientCertificate Listable[string] `json:"client_certificate,omitempty"` + ClientCertificatePath string `json:"client_certificate_path,omitempty"` + ECH *OutboundECHOptions `json:"ech,omitempty"` + UTLS *OutboundUTLSOptions `json:"utls,omitempty"` + Reality *OutboundRealityOptions `json:"reality,omitempty"` } type InboundRealityOptions struct {