git » kxd » commit ff4f9eb

kxc: Support server certs with DNSName=`*`

author Alberto Bertogli
2024-08-15 20:44:53 UTC
committer Alberto Bertogli
2024-09-08 09:33:09 UTC
parent 352c4ea43d9167acf1ada6199ce253197691955c

kxc: Support server certs with DNSName=`*`

In Go 1.23, the Go TLS library started to reject certificates with a
DNSName of `*` (Go commit 375031d8dcec9ae74d2dbc437b201107dba3bb5f).

Unfortunately, the certificates generated before kxgencert existed
(using openssl to generate self-signed certs) defaulted to create them
with DNSName `*`.

To ensure that kxc can still talk to servers with those certificates, we
need to adjust the validation logic to make it ignore the server hostname
in cases where such a server certificate is expected.

kxc/kxc.go +73 -7

diff --git a/kxc/kxc.go b/kxc/kxc.go
index d79bf47..fca6532 100644
--- a/kxc/kxc.go
+++ b/kxc/kxc.go
@@ -9,12 +9,14 @@ package main
 import (
 	"crypto/tls"
 	"crypto/x509"
+	"encoding/pem"
 	"flag"
 	"fmt"
 	"io/ioutil"
 	"log"
 	"net/http"
 	"net/url"
+	"slices"
 	"strings"
 )
 
@@ -27,18 +29,48 @@ var clientCert = flag.String(
 var clientKey = flag.String(
 	"client_key", "", "File containing the client private key")
 
-func loadServerCerts() (*x509.CertPool, error) {
+func loadServerCerts() (*x509.CertPool, bool, error) {
 	pemData, err := ioutil.ReadFile(*serverCert)
 	if err != nil {
-		return nil, err
+		return nil, false, err
+	}
+
+	// Old server certificates can use the deprecated '*' for the server name.
+	// This is not supported by Go, but we still want to support them, so
+	// we need to identify them at parsing time.
+	hasWildcard := false
+	{
+		data := pemData[:]
+		for {
+			var block *pem.Block
+			block, data = pem.Decode(data)
+			if block == nil {
+				break
+			}
+
+			cert, err := x509.ParseCertificate(block.Bytes)
+			if err != nil {
+				return nil, false, fmt.Errorf(
+					"error parsing certificate: %s", err)
+			}
+
+			if strings.Contains(cert.Subject.CommonName, "*") {
+				hasWildcard = true
+				break
+			}
+			if slices.Contains(cert.DNSNames, "*") {
+				hasWildcard = true
+				break
+			}
+		}
 	}
 
 	pool := x509.NewCertPool()
 	if !pool.AppendCertsFromPEM(pemData) {
-		return nil, fmt.Errorf("error appending certificates")
+		return nil, false, fmt.Errorf("error appending certificates")
 	}
 
-	return pool, nil
+	return pool, hasWildcard, nil
 }
 
 // Check if the given network address has a port.
@@ -89,12 +121,46 @@ func makeTLSConf() *tls.Config {
 		log.Fatalf("Failed to load keys: %s", err)
 	}
 
-	// Compare against the server certificates.
-	serverCerts, err := loadServerCerts()
+	serverCerts, hasWildcard, err := loadServerCerts()
 	if err != nil {
 		log.Fatalf("Failed to load server certs: %s", err)
 	}
-	tlsConf.RootCAs = serverCerts
+
+	if hasWildcard {
+		// We want to do the standard verification, but ignoring the server name.
+		// This is because old certificates might not have the server name, or use
+		// '*' which was later deprecated and not supported by Go.
+		// This also makes deployment much more practical on small networks where
+		// the server name is not important.
+		//
+		// Unfortunately, there's no way to tell Go to ignore just that, so we need
+		// to do it manually.
+		// To do that, we need to set InsecureSkipVerify to true, and then provide
+		// a custom VerifyConnection function that does the verification we want.
+		// The verification is using the same logic Go does, and following the
+		// official example at
+		// https://pkg.go.dev/crypto/tls#example-Config-VerifyConnection.
+		tlsConf.InsecureSkipVerify = true
+		tlsConf.VerifyConnection = func(cs tls.ConnectionState) error {
+			opts := x509.VerifyOptions{
+				// Explicitly not care about the server name.
+				DNSName:       "",
+				Intermediates: x509.NewCertPool(),
+
+				// Compare against the server certificates.
+				Roots: serverCerts,
+			}
+			for _, cert := range cs.PeerCertificates[1:] {
+				opts.Intermediates.AddCert(cert)
+			}
+			_, err := cs.PeerCertificates[0].Verify(opts)
+			return err
+		}
+	} else {
+		// If none of the server certificates use the deprecated '*', we can
+		// use the standard verification.
+		tlsConf.RootCAs = serverCerts
+	}
 
 	return tlsConf
 }