From 0125cd97d8d8d68ce3638ebae1e8618008a6acfb Mon Sep 17 00:00:00 2001 From: hakansa <43675540+hakansa@users.noreply.github.com> Date: Tue, 4 Feb 2025 18:17:59 +0300 Subject: [PATCH] [client] use embedded root CA if system certpool is empty (#3272) * Implement custom TLS certificate handling with fallback to embedded roots --- client/internal/auth/device_flow.go | 17 ++++++++++++ relay/client/dialer/ws/ws.go | 12 +++++++++ relay/tls/client_dev.go | 16 ++++++++++- relay/tls/client_prod.go | 16 ++++++++++- util/embeddedroots/embeddedroots.go | 42 +++++++++++++++++++++++++++++ util/embeddedroots/isrg-root-x1.pem | 31 +++++++++++++++++++++ util/embeddedroots/isrg-root-x2.pem | 14 ++++++++++ util/grpc/dialer.go | 18 ++++++++++--- 8 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 util/embeddedroots/embeddedroots.go create mode 100644 util/embeddedroots/isrg-root-x1.pem create mode 100644 util/embeddedroots/isrg-root-x2.pem diff --git a/client/internal/auth/device_flow.go b/client/internal/auth/device_flow.go index 87d00de5e..da4f16c8d 100644 --- a/client/internal/auth/device_flow.go +++ b/client/internal/auth/device_flow.go @@ -2,6 +2,8 @@ package auth import ( "context" + "crypto/tls" + "crypto/x509" "encoding/json" "errors" "fmt" @@ -11,7 +13,10 @@ import ( "strings" "time" + log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/internal" + "github.com/netbirdio/netbird/util/embeddedroots" ) // HostedGrantType grant type for device flow on Hosted @@ -56,6 +61,18 @@ func NewDeviceAuthorizationFlow(config internal.DeviceAuthProviderConfig) (*Devi httpTransport := http.DefaultTransport.(*http.Transport).Clone() httpTransport.MaxIdleConns = 5 + certPool, err := x509.SystemCertPool() + if err != nil || certPool == nil { + log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err) + certPool = embeddedroots.Get() + } else { + log.Debug("Using system certificate pool.") + } + + httpTransport.TLSClientConfig = &tls.Config{ + RootCAs: certPool, + } + httpClient := &http.Client{ Timeout: 10 * time.Second, Transport: httpTransport, diff --git a/relay/client/dialer/ws/ws.go b/relay/client/dialer/ws/ws.go index df91a66d4..2adbd2451 100644 --- a/relay/client/dialer/ws/ws.go +++ b/relay/client/dialer/ws/ws.go @@ -2,6 +2,8 @@ package ws import ( "context" + "crypto/tls" + "crypto/x509" "errors" "fmt" "net" @@ -13,6 +15,7 @@ import ( "nhooyr.io/websocket" "github.com/netbirdio/netbird/relay/server/listener/ws" + "github.com/netbirdio/netbird/util/embeddedroots" nbnet "github.com/netbirdio/netbird/util/net" ) @@ -66,10 +69,19 @@ func prepareURL(address string) (string, error) { func httpClientNbDialer() *http.Client { customDialer := nbnet.NewDialer() + certPool, err := x509.SystemCertPool() + if err != nil || certPool == nil { + log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err) + certPool = embeddedroots.Get() + } + customTransport := &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { return customDialer.DialContext(ctx, network, addr) }, + TLSClientConfig: &tls.Config{ + RootCAs: certPool, + }, } return &http.Client{ diff --git a/relay/tls/client_dev.go b/relay/tls/client_dev.go index f6b8290a0..52e5535c5 100644 --- a/relay/tls/client_dev.go +++ b/relay/tls/client_dev.go @@ -2,11 +2,25 @@ package tls -import "crypto/tls" +import ( + "crypto/tls" + "crypto/x509" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/util/embeddedroots" +) func ClientQUICTLSConfig() *tls.Config { + certPool, err := x509.SystemCertPool() + if err != nil || certPool == nil { + log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err) + certPool = embeddedroots.Get() + } + return &tls.Config{ InsecureSkipVerify: true, // Debug mode allows insecure connections NextProtos: []string{nbalpn}, // Ensure this matches the server's ALPN + RootCAs: certPool, } } diff --git a/relay/tls/client_prod.go b/relay/tls/client_prod.go index 686093a37..62e218bc3 100644 --- a/relay/tls/client_prod.go +++ b/relay/tls/client_prod.go @@ -2,10 +2,24 @@ package tls -import "crypto/tls" +import ( + "crypto/tls" + "crypto/x509" + + log "github.com/sirupsen/logrus" + + "github.com/netbirdio/netbird/util/embeddedroots" +) func ClientQUICTLSConfig() *tls.Config { + certPool, err := x509.SystemCertPool() + if err != nil || certPool == nil { + log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err) + certPool = embeddedroots.Get() + } + return &tls.Config{ NextProtos: []string{nbalpn}, + RootCAs: certPool, } } diff --git a/util/embeddedroots/embeddedroots.go b/util/embeddedroots/embeddedroots.go new file mode 100644 index 000000000..d205f5b69 --- /dev/null +++ b/util/embeddedroots/embeddedroots.go @@ -0,0 +1,42 @@ +package embeddedroots + +import ( + "crypto/x509" + _ "embed" + "sync" +) + +func Get() *x509.CertPool { + rootsVar.load() + return rootsVar.p +} + +type roots struct { + once sync.Once + p *x509.CertPool +} + +var rootsVar roots + +func (r *roots) load() { + r.once.Do(func() { + p := x509.NewCertPool() + p.AppendCertsFromPEM([]byte(isrgRootX1RootPEM)) + p.AppendCertsFromPEM([]byte(isrgRootX2RootPEM)) + r.p = p + }) +} + +// Subject: O = Internet Security Research Group, CN = ISRG Root X1 +// Key type: RSA 4096 +// Validity: until 2030-06-04 (generated 2015-06-04) +// +//go:embed isrg-root-x1.pem +var isrgRootX1RootPEM string + +// Subject: O = Internet Security Research Group, CN = ISRG Root X2 +// Key type: ECDSA P-384 +// Validity: until 2035-09-04 (generated 2020-09-04) +// +//go:embed isrg-root-x2.pem +var isrgRootX2RootPEM string diff --git a/util/embeddedroots/isrg-root-x1.pem b/util/embeddedroots/isrg-root-x1.pem new file mode 100644 index 000000000..57d4a3766 --- /dev/null +++ b/util/embeddedroots/isrg-root-x1.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/util/embeddedroots/isrg-root-x2.pem b/util/embeddedroots/isrg-root-x2.pem new file mode 100644 index 000000000..7d903edc9 --- /dev/null +++ b/util/embeddedroots/isrg-root-x2.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- diff --git a/util/grpc/dialer.go b/util/grpc/dialer.go index 4fbffe342..83a11c65d 100644 --- a/util/grpc/dialer.go +++ b/util/grpc/dialer.go @@ -3,14 +3,16 @@ package grpc import ( "context" "crypto/tls" + "crypto/x509" "fmt" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" "net" "os/user" "runtime" "time" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "github.com/cenkalti/backoff/v4" log "github.com/sirupsen/logrus" "google.golang.org/grpc" @@ -18,6 +20,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/keepalive" + "github.com/netbirdio/netbird/util/embeddedroots" nbnet "github.com/netbirdio/netbird/util/net" ) @@ -57,9 +60,16 @@ func Backoff(ctx context.Context) backoff.BackOff { func CreateConnection(addr string, tlsEnabled bool) (*grpc.ClientConn, error) { transportOption := grpc.WithTransportCredentials(insecure.NewCredentials()) - if tlsEnabled { - transportOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})) + certPool, err := x509.SystemCertPool() + if err != nil || certPool == nil { + log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err) + certPool = embeddedroots.Get() + } + + transportOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: certPool, + })) } connCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)