Merge pull request #1426 from shadow750d6/mtls
Add optional mTLS support for client & server
This commit is contained in:
commit
a4b1cfa5aa
10 changed files with 77 additions and 12 deletions
|
|
@ -2,6 +2,7 @@ package cmd
|
|||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
|
|
@ -96,10 +97,12 @@ type clientConfigObfs struct {
|
|||
}
|
||||
|
||||
type clientConfigTLS struct {
|
||||
SNI string `mapstructure:"sni"`
|
||||
Insecure bool `mapstructure:"insecure"`
|
||||
PinSHA256 string `mapstructure:"pinSHA256"`
|
||||
CA string `mapstructure:"ca"`
|
||||
SNI string `mapstructure:"sni"`
|
||||
Insecure bool `mapstructure:"insecure"`
|
||||
PinSHA256 string `mapstructure:"pinSHA256"`
|
||||
CA string `mapstructure:"ca"`
|
||||
ClientCertificate string `mapstructure:"clientCertificate"`
|
||||
ClientKey string `mapstructure:"clientKey"`
|
||||
}
|
||||
|
||||
type clientConfigQUIC struct {
|
||||
|
|
@ -294,6 +297,31 @@ func (c *clientConfig) fillTLSConfig(hyConfig *client.Config) error {
|
|||
}
|
||||
hyConfig.TLSConfig.RootCAs = cPool
|
||||
}
|
||||
if c.TLS.ClientCertificate != "" && c.TLS.ClientKey != "" {
|
||||
certLoader := &utils.LocalCertificateLoader{
|
||||
CertFile: c.TLS.ClientCertificate,
|
||||
KeyFile: c.TLS.ClientKey,
|
||||
}
|
||||
// Try loading the cert-key pair here to catch errors early
|
||||
err := certLoader.InitializeCache()
|
||||
if err != nil {
|
||||
var pathErr *os.PathError
|
||||
if errors.As(err, &pathErr) {
|
||||
if pathErr.Path == c.TLS.ClientCertificate {
|
||||
return configError{Field: "tls.clientCertificate", Err: pathErr}
|
||||
}
|
||||
if pathErr.Path == c.TLS.ClientKey {
|
||||
return configError{Field: "tls.clientKey", Err: pathErr}
|
||||
}
|
||||
}
|
||||
return configError{Field: "tls.clientCertificate", Err: err}
|
||||
}
|
||||
// Use GetClientCertificates so that users can update the cert without restarting the client.
|
||||
hyConfig.TLSConfig.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
||||
// For simplicity, always respond with the configured client certs, regardless of server requests.
|
||||
return certLoader.GetCertificate(nil)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,10 +33,12 @@ func TestClientConfig(t *testing.T) {
|
|||
},
|
||||
},
|
||||
TLS: clientConfigTLS{
|
||||
SNI: "another.example.com",
|
||||
Insecure: true,
|
||||
PinSHA256: "114515DEADBEEF",
|
||||
CA: "custom_ca.crt",
|
||||
SNI: "another.example.com",
|
||||
Insecure: true,
|
||||
PinSHA256: "114515DEADBEEF",
|
||||
CA: "custom_ca.crt",
|
||||
ClientCertificate: "client.crt",
|
||||
ClientKey: "client.key",
|
||||
},
|
||||
QUIC: clientConfigQUIC{
|
||||
InitStreamReceiveWindow: 1145141,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ tls:
|
|||
insecure: true
|
||||
pinSHA256: 114515DEADBEEF
|
||||
ca: custom_ca.crt
|
||||
clientCertificate: client.crt
|
||||
clientKey: client.key
|
||||
|
||||
quic:
|
||||
initStreamReceiveWindow: 1145141
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package cmd
|
|||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -86,6 +87,7 @@ type serverConfigTLS struct {
|
|||
Cert string `mapstructure:"cert"`
|
||||
Key string `mapstructure:"key"`
|
||||
SNIGuard string `mapstructure:"sniGuard"` // "disable", "dns-san", "strict"
|
||||
ClientCA string `mapstructure:"clientCA"`
|
||||
}
|
||||
|
||||
type serverConfigACME struct {
|
||||
|
|
@ -333,6 +335,18 @@ func (c *serverConfig) fillTLSConfig(hyConfig *server.Config) error {
|
|||
// Use GetCertificate instead of Certificates so that
|
||||
// users can update the cert without restarting the server.
|
||||
hyConfig.TLSConfig.GetCertificate = certLoader.GetCertificate
|
||||
// Client CA
|
||||
if c.TLS.ClientCA != "" {
|
||||
ca, err := os.ReadFile(c.TLS.ClientCA)
|
||||
if err != nil {
|
||||
return configError{Field: "tls.clientCA", Err: err}
|
||||
}
|
||||
cPool := x509.NewCertPool()
|
||||
if !cPool.AppendCertsFromPEM(ca) {
|
||||
return configError{Field: "tls.clientCA", Err: errors.New("failed to parse client CA certificate")}
|
||||
}
|
||||
hyConfig.TLSConfig.ClientCAs = cPool
|
||||
}
|
||||
} else {
|
||||
// ACME
|
||||
dataDir := c.ACME.Dir
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ func TestServerConfig(t *testing.T) {
|
|||
Cert: "some.crt",
|
||||
Key: "some.key",
|
||||
SNIGuard: "strict",
|
||||
ClientCA: "some_ca.crt",
|
||||
},
|
||||
ACME: &serverConfigACME{
|
||||
Domains: []string{
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ tls:
|
|||
cert: some.crt
|
||||
key: some.key
|
||||
sniGuard: strict
|
||||
clientCA: some_ca.crt
|
||||
|
||||
acme:
|
||||
domains:
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ func (c *clientImpl) connect() (*HandshakeInfo, error) {
|
|||
InsecureSkipVerify: c.config.TLSConfig.InsecureSkipVerify,
|
||||
VerifyPeerCertificate: c.config.TLSConfig.VerifyPeerCertificate,
|
||||
RootCAs: c.config.TLSConfig.RootCAs,
|
||||
GetClientCertificate: c.config.TLSConfig.GetClientCertificate,
|
||||
}
|
||||
quicConfig := &quic.Config{
|
||||
InitialStreamReceiveWindow: c.config.QUICConfig.InitialStreamReceiveWindow,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
"time"
|
||||
|
|
@ -92,6 +93,7 @@ type TLSConfig struct {
|
|||
InsecureSkipVerify bool
|
||||
VerifyPeerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error
|
||||
RootCAs *x509.CertPool
|
||||
GetClientCertificate func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
|
||||
}
|
||||
|
||||
// QUICConfig contains the QUIC configuration fields that we want to expose to the user.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package server
|
|||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
|
@ -101,6 +102,7 @@ func (c *Config) fill() error {
|
|||
type TLSConfig struct {
|
||||
Certificates []tls.Certificate
|
||||
GetCertificate func(info *tls.ClientHelloInfo) (*tls.Certificate, error)
|
||||
ClientCAs *x509.CertPool
|
||||
}
|
||||
|
||||
// QUICConfig contains the QUIC configuration fields that we want to expose to the user.
|
||||
|
|
|
|||
|
|
@ -26,14 +26,26 @@ type Server interface {
|
|||
Close() error
|
||||
}
|
||||
|
||||
func convertToStdTLSConfig(config *Config) *tls.Config {
|
||||
var clientAuth tls.ClientAuthType
|
||||
if config.TLSConfig.ClientCAs != nil {
|
||||
clientAuth = tls.RequireAndVerifyClientCert
|
||||
} else {
|
||||
clientAuth = tls.NoClientCert
|
||||
}
|
||||
return http3.ConfigureTLSConfig(&tls.Config{
|
||||
Certificates: config.TLSConfig.Certificates,
|
||||
GetCertificate: config.TLSConfig.GetCertificate,
|
||||
ClientCAs: config.TLSConfig.ClientCAs,
|
||||
ClientAuth: clientAuth,
|
||||
})
|
||||
}
|
||||
|
||||
func NewServer(config *Config) (Server, error) {
|
||||
if err := config.fill(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig := http3.ConfigureTLSConfig(&tls.Config{
|
||||
Certificates: config.TLSConfig.Certificates,
|
||||
GetCertificate: config.TLSConfig.GetCertificate,
|
||||
})
|
||||
tlsConfig := convertToStdTLSConfig(config)
|
||||
quicConfig := &quic.Config{
|
||||
InitialStreamReceiveWindow: config.QUICConfig.InitialStreamReceiveWindow,
|
||||
MaxStreamReceiveWindow: config.QUICConfig.MaxStreamReceiveWindow,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue