mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
Add support for TLS client certificate auth
This commit is contained in:
parent
02afba132f
commit
833c2947ec
@ -1,5 +1,12 @@
|
|||||||
package tls
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
VersionTLS10 = 0x0301
|
VersionTLS10 = 0x0301
|
||||||
VersionTLS11 = 0x0302
|
VersionTLS11 = 0x0302
|
||||||
@ -10,3 +17,31 @@ const (
|
|||||||
// supported by this package. See golang.org/issue/32716.
|
// supported by this package. See golang.org/issue/32716.
|
||||||
VersionSSL30 = 0x0300
|
VersionSSL30 = 0x0300
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func loadCertAsBytes(certificate string, certificatePath string) ([]byte, error) {
|
||||||
|
if certificate != "" {
|
||||||
|
return []byte(certificate), nil
|
||||||
|
} else if certificatePath != "" {
|
||||||
|
content, err := os.ReadFile(certificatePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "read certificate")
|
||||||
|
}
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadCertAsPool(certificate string, certificatePath string) (*x509.CertPool, error) {
|
||||||
|
certBytes, err := loadCertAsBytes(certificate, certificatePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if certBytes == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
certPool := x509.NewCertPool()
|
||||||
|
if !certPool.AppendCertsFromPEM(certBytes) {
|
||||||
|
return nil, E.New("failed to parse certificate:\n\n", certBytes)
|
||||||
|
}
|
||||||
|
return certPool, nil
|
||||||
|
}
|
||||||
|
@ -9,12 +9,11 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
|
||||||
|
|
||||||
cftls "github.com/sagernet/cloudflare-tls"
|
cftls "github.com/sagernet/cloudflare-tls"
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-dns"
|
dns "github.com/sagernet/sing-dns"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
mDNS "github.com/miekg/dns"
|
||||||
@ -140,23 +139,32 @@ func NewECHClient(router adapter.Router, serverAddress string, options option.Ou
|
|||||||
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var certificate []byte
|
certPool, err := loadCertAsPool(options.Certificate, options.CertificatePath)
|
||||||
if options.Certificate != "" {
|
if err != nil {
|
||||||
certificate = []byte(options.Certificate)
|
return nil, E.Cause(err, "load certificate")
|
||||||
} else if options.CertificatePath != "" {
|
|
||||||
content, err := os.ReadFile(options.CertificatePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "read certificate")
|
|
||||||
}
|
|
||||||
certificate = content
|
|
||||||
}
|
}
|
||||||
if len(certificate) > 0 {
|
if certPool != nil {
|
||||||
certPool := x509.NewCertPool()
|
|
||||||
if !certPool.AppendCertsFromPEM(certificate) {
|
|
||||||
return nil, E.New("failed to parse certificate:\n\n", certificate)
|
|
||||||
}
|
|
||||||
tlsConfig.RootCAs = certPool
|
tlsConfig.RootCAs = certPool
|
||||||
}
|
}
|
||||||
|
clientCert, err := loadCertAsBytes(options.ClientCertificate, options.ClientCertificatePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "load client certificate")
|
||||||
|
}
|
||||||
|
clientKey, err := loadCertAsBytes(options.ClientKey, options.ClientKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "load client certificate key")
|
||||||
|
}
|
||||||
|
if clientCert != nil && clientKey == nil {
|
||||||
|
return nil, E.New("Client certificate specified without a client key")
|
||||||
|
} else if clientCert == nil && clientKey != nil {
|
||||||
|
return nil, E.New("Client key specified without a client certificate")
|
||||||
|
} else if clientCert != nil && clientKey != nil {
|
||||||
|
clientKeyPair, err := cftls.X509KeyPair(clientCert, clientKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse client certificate/key")
|
||||||
|
}
|
||||||
|
tlsConfig.Certificates = []cftls.Certificate{clientKeyPair}
|
||||||
|
}
|
||||||
|
|
||||||
// ECH Config
|
// ECH Config
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
@ -107,22 +106,31 @@ func NewSTDClient(serverAddress string, options option.OutboundTLSOptions) (Conf
|
|||||||
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var certificate []byte
|
certPool, err := loadCertAsPool(options.Certificate, options.CertificatePath)
|
||||||
if options.Certificate != "" {
|
if err != nil {
|
||||||
certificate = []byte(options.Certificate)
|
return nil, E.Cause(err, "load certificate")
|
||||||
} else if options.CertificatePath != "" {
|
|
||||||
content, err := os.ReadFile(options.CertificatePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "read certificate")
|
|
||||||
}
|
|
||||||
certificate = content
|
|
||||||
}
|
}
|
||||||
if len(certificate) > 0 {
|
if certPool != nil {
|
||||||
certPool := x509.NewCertPool()
|
|
||||||
if !certPool.AppendCertsFromPEM(certificate) {
|
|
||||||
return nil, E.New("failed to parse certificate:\n\n", certificate)
|
|
||||||
}
|
|
||||||
tlsConfig.RootCAs = certPool
|
tlsConfig.RootCAs = certPool
|
||||||
}
|
}
|
||||||
|
clientCert, err := loadCertAsBytes(options.ClientCertificate, options.ClientCertificatePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "load client certificate")
|
||||||
|
}
|
||||||
|
clientKey, err := loadCertAsBytes(options.ClientKey, options.ClientKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "load client certificate key")
|
||||||
|
}
|
||||||
|
if clientCert != nil && clientKey == nil {
|
||||||
|
return nil, E.New("Client certificate specified without a client key")
|
||||||
|
} else if clientCert == nil && clientKey != nil {
|
||||||
|
return nil, E.New("Client key specified without a client certificate")
|
||||||
|
} else if clientCert != nil && clientKey != nil {
|
||||||
|
clientKeyPair, err := tls.X509KeyPair(clientCert, clientKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse client certificate/key")
|
||||||
|
}
|
||||||
|
tlsConfig.Certificates = []tls.Certificate{clientKeyPair}
|
||||||
|
}
|
||||||
return &STDClientConfig{&tlsConfig}, nil
|
return &STDClientConfig{&tlsConfig}, nil
|
||||||
}
|
}
|
||||||
|
@ -246,6 +246,14 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
|||||||
tlsConfig.Certificates = []tls.Certificate{keyPair}
|
tlsConfig.Certificates = []tls.Certificate{keyPair}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
clientCA, err := loadCertAsPool(options.ClientCA, options.ClientCAPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "load client CA")
|
||||||
|
}
|
||||||
|
if clientCA != nil {
|
||||||
|
tlsConfig.ClientCAs = clientCA
|
||||||
|
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||||
|
}
|
||||||
return &STDServerConfig{
|
return &STDServerConfig{
|
||||||
config: tlsConfig,
|
config: tlsConfig,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
@ -4,10 +4,8 @@ package tls
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
@ -127,23 +125,32 @@ func NewUTLSClient(router adapter.Router, serverAddress string, options option.O
|
|||||||
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var certificate []byte
|
certPool, err := loadCertAsPool(options.Certificate, options.CertificatePath)
|
||||||
if options.Certificate != "" {
|
if err != nil {
|
||||||
certificate = []byte(options.Certificate)
|
return nil, E.Cause(err, "load certificate")
|
||||||
} else if options.CertificatePath != "" {
|
|
||||||
content, err := os.ReadFile(options.CertificatePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "read certificate")
|
|
||||||
}
|
|
||||||
certificate = content
|
|
||||||
}
|
}
|
||||||
if len(certificate) > 0 {
|
if certPool != nil {
|
||||||
certPool := x509.NewCertPool()
|
|
||||||
if !certPool.AppendCertsFromPEM(certificate) {
|
|
||||||
return nil, E.New("failed to parse certificate:\n\n", certificate)
|
|
||||||
}
|
|
||||||
tlsConfig.RootCAs = certPool
|
tlsConfig.RootCAs = certPool
|
||||||
}
|
}
|
||||||
|
clientCert, err := loadCertAsBytes(options.ClientCertificate, options.ClientCertificatePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "load client certificate")
|
||||||
|
}
|
||||||
|
clientKey, err := loadCertAsBytes(options.ClientKey, options.ClientKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "load client certificate key")
|
||||||
|
}
|
||||||
|
if clientCert != nil && clientKey == nil {
|
||||||
|
return nil, E.New("Client certificate specified without a client key")
|
||||||
|
} else if clientCert == nil && clientKey != nil {
|
||||||
|
return nil, E.New("Client key specified without a client certificate")
|
||||||
|
} else if clientCert != nil && clientKey != nil {
|
||||||
|
clientKeyPair, err := utls.X509KeyPair(clientCert, clientKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "parse client certificate/key")
|
||||||
|
}
|
||||||
|
tlsConfig.Certificates = []utls.Certificate{clientKeyPair}
|
||||||
|
}
|
||||||
var id utls.ClientHelloID
|
var id utls.ClientHelloID
|
||||||
switch options.UTLS.Fingerprint {
|
switch options.UTLS.Fingerprint {
|
||||||
case "chrome", "":
|
case "chrome", "":
|
||||||
|
@ -12,22 +12,28 @@ type InboundTLSOptions struct {
|
|||||||
CertificatePath string `json:"certificate_path,omitempty"`
|
CertificatePath string `json:"certificate_path,omitempty"`
|
||||||
Key string `json:"key,omitempty"`
|
Key string `json:"key,omitempty"`
|
||||||
KeyPath string `json:"key_path,omitempty"`
|
KeyPath string `json:"key_path,omitempty"`
|
||||||
|
ClientCA string `json:"client_ca,omitempty"`
|
||||||
|
ClientCAPath string `json:"client_ca_path,omitempty"`
|
||||||
ACME *InboundACMEOptions `json:"acme,omitempty"`
|
ACME *InboundACMEOptions `json:"acme,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OutboundTLSOptions struct {
|
type OutboundTLSOptions struct {
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
DisableSNI bool `json:"disable_sni,omitempty"`
|
DisableSNI bool `json:"disable_sni,omitempty"`
|
||||||
ServerName string `json:"server_name,omitempty"`
|
ServerName string `json:"server_name,omitempty"`
|
||||||
Insecure bool `json:"insecure,omitempty"`
|
Insecure bool `json:"insecure,omitempty"`
|
||||||
ALPN Listable[string] `json:"alpn,omitempty"`
|
ALPN Listable[string] `json:"alpn,omitempty"`
|
||||||
MinVersion string `json:"min_version,omitempty"`
|
MinVersion string `json:"min_version,omitempty"`
|
||||||
MaxVersion string `json:"max_version,omitempty"`
|
MaxVersion string `json:"max_version,omitempty"`
|
||||||
CipherSuites Listable[string] `json:"cipher_suites,omitempty"`
|
CipherSuites Listable[string] `json:"cipher_suites,omitempty"`
|
||||||
Certificate string `json:"certificate,omitempty"`
|
Certificate string `json:"certificate,omitempty"`
|
||||||
CertificatePath string `json:"certificate_path,omitempty"`
|
CertificatePath string `json:"certificate_path,omitempty"`
|
||||||
ECH *OutboundECHOptions `json:"ech,omitempty"`
|
ClientCertificate string `json:"client_certificate,omitempty"`
|
||||||
UTLS *OutboundUTLSOptions `json:"utls,omitempty"`
|
ClientCertificatePath string `json:"client_certificate_path,omitempty"`
|
||||||
|
ClientKey string `json:"client_key,omitempty"`
|
||||||
|
ClientKeyPath string `json:"client_key_path,omitempty"`
|
||||||
|
ECH *OutboundECHOptions `json:"ech,omitempty"`
|
||||||
|
UTLS *OutboundUTLSOptions `json:"utls,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OutboundECHOptions struct {
|
type OutboundECHOptions struct {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user