git » kxd » commit d3c6e3f

Use tls/x509 validation

author Alberto Bertogli
2014-04-25 02:02:38 UTC
committer Alberto Bertogli
2014-04-26 03:28:12 UTC
parent 4b0d0f6b8a3bd8953459b7c70dbfd62c006eb62e

Use tls/x509 validation

Currently, in both the server and the client we validate the other side's
certificates by comparing them directly with each of the allowed ones.

While this works, it is a bit tedious, somewhat risky, and it means we don't
support delegation via signing.

This patch changes the code to use Go's libraries for validating the
certificates, which makes the code simpler (specially the client), and also
paves the road for more features.

Signed-off-by: Alberto Bertogli <albertito@blitiri.com.ar>

kxc/kxc.go +28 -78
kxd/key_config.go +16 -23
scripts/create-kxd-config +1 -1
scripts/kxc-add-key +1 -1
tests/run_tests +6 -4

diff --git a/kxc/kxc.go b/kxc/kxc.go
index 66860e9..5d5e02b 100644
--- a/kxc/kxc.go
+++ b/kxc/kxc.go
@@ -9,12 +9,10 @@ package main
 import (
 	"crypto/tls"
 	"crypto/x509"
-	"encoding/pem"
 	"flag"
 	"fmt"
 	"io/ioutil"
 	"log"
-	"net"
 	"net/http"
 	"net/url"
 	"strings"
@@ -29,70 +27,18 @@ var client_cert = flag.String(
 var client_key = flag.String(
 	"client_key", "", "File containing the client private key")
 
-func LoadServerCerts() ([]*x509.Certificate, error) {
+func LoadServerCerts() (*x509.CertPool, error) {
 	pemData, err := ioutil.ReadFile(*server_cert)
 	if err != nil {
 		return nil, err
 	}
 
-	var certs []*x509.Certificate
-	for len(pemData) > 0 {
-		var block *pem.Block
-		block, pemData = pem.Decode(pemData)
-		if block == nil {
-			return certs, nil
-		}
-
-		if block.Type != "CERTIFICATE" {
-			continue
-		}
-
-		cert, err := x509.ParseCertificate(block.Bytes)
-		if err != nil {
-			return certs, err
-		}
-
-		certs = append(certs, cert)
-	}
-
-	return certs, nil
-}
-
-// We can't ask the http client for the TLS connection, so we make it
-// ourselves and save it here.
-// It is hacky but it simplifies the code.
-var tlsConn *tls.Conn
-
-func OurDial(network, addr string) (net.Conn, error) {
-	var err error
-
-	tlsConf := &tls.Config{}
-	tlsConf.Certificates = make([]tls.Certificate, 1)
-	tlsConf.Certificates[0], err = tls.LoadX509KeyPair(
-		*client_cert, *client_key)
-	if err != nil {
-		log.Fatalf("Failed to load keys: %s", err)
+	pool := x509.NewCertPool()
+	if !pool.AppendCertsFromPEM(pemData) {
+		return nil, fmt.Errorf("Error appending certificates")
 	}
 
-	// We don't want the TLS stack to check the cert, as we will compare
-	// it ourselves.
-	tlsConf.InsecureSkipVerify = true
-
-	tlsConn, err = tls.Dial(network, addr, tlsConf)
-	return tlsConn, err
-}
-
-// Check if any cert from requestedCerts matches any cert in validCerts.
-func AnyCertMatches(requestedCerts, validCerts []*x509.Certificate) bool {
-	for _, cert := range requestedCerts {
-		for _, validCert := range validCerts {
-			if cert.Equal(validCert) {
-				return true
-			}
-		}
-	}
-
-	return false
+	return pool, nil
 }
 
 // Check if the given network address has a port.
@@ -103,24 +49,17 @@ func hasPort(s string) bool {
 }
 
 func ExtractURL(rawurl string) (*url.URL, error) {
-	// Because we handle the transport ourselves, the http library has to
-	// be told to use plain http; otherwise it will attempt to do its own
-	// TLS on top of our TLS.
 	serverURL, err := url.Parse(rawurl)
 	if err != nil {
 		return nil, err
 	}
 
-	// Fix up the scheme to use http.
-	// Because we handle the transport ourselves, the http library has to
-	// be told to use plain http; otherwise it will attempt to do its own
-	// TLS on top of our TLS (and obviously fails, in this case with an
-	// "local error: record overflow" error).
+	// Make sure we're using https.
 	switch serverURL.Scheme {
-	case "http":
+	case "https":
 		// Nothing to do here.
-	case "https", "kxd":
-		serverURL.Scheme = "http"
+	case "http", "kxd":
+		serverURL.Scheme = "https"
 	default:
 		return nil, fmt.Errorf("Unsupported URL schema (try kxd://)")
 	}
@@ -139,17 +78,33 @@ func ExtractURL(rawurl string) (*url.URL, error) {
 	return serverURL, nil
 }
 
-func main() {
+func MakeTLSConf() *tls.Config {
 	var err error
-	flag.Parse()
 
+	tlsConf := &tls.Config{}
+	tlsConf.Certificates = make([]tls.Certificate, 1)
+	tlsConf.Certificates[0], err = tls.LoadX509KeyPair(
+		*client_cert, *client_key)
+	if err != nil {
+		log.Fatalf("Failed to load keys: %s", err)
+	}
+
+	// Compare against the server certificates.
 	serverCerts, err := LoadServerCerts()
 	if err != nil {
 		log.Fatalf("Failed to load server certs: %s", err)
 	}
+	tlsConf.RootCAs = serverCerts
+
+	return tlsConf
+}
+
+func main() {
+	var err error
+	flag.Parse()
 
 	tr := &http.Transport{
-		Dial: OurDial,
+		TLSClientConfig: MakeTLSConf(),
 	}
 
 	client := &http.Client{
@@ -177,10 +132,5 @@ func main() {
 			resp.Status, content)
 	}
 
-	if !AnyCertMatches(
-		tlsConn.ConnectionState().PeerCertificates, serverCerts) {
-		log.Fatalf("No server certificate matches")
-	}
-
 	fmt.Printf("%s", content)
 }
diff --git a/kxd/key_config.go b/kxd/key_config.go
index 5b9e491..01b39bd 100644
--- a/kxd/key_config.go
+++ b/kxd/key_config.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"crypto/x509"
-	"encoding/pem"
 	"fmt"
 	"io/ioutil"
 	"net"
@@ -49,7 +48,7 @@ type KeyConfig struct {
 	emailToPath        string
 
 	// Allowed certificates.
-	allowedClientCerts []*x509.Certificate
+	allowedClientCerts *x509.CertPool
 
 	// Allowed hosts.
 	allowedHosts []string
@@ -62,6 +61,7 @@ func NewKeyConfig(configPath string) *KeyConfig {
 		allowedClientsPath: configPath + "/allowed_clients",
 		allowedHostsPath:   configPath + "/allowed_hosts",
 		emailToPath:        configPath + "/email_to",
+		allowedClientCerts: x509.NewCertPool(),
 	}
 }
 
@@ -94,23 +94,8 @@ func (kc *KeyConfig) LoadClientCerts() error {
 		return err
 	}
 
-	for len(rawContents) > 0 {
-		var block *pem.Block
-		block, rawContents = pem.Decode(rawContents)
-		if block == nil {
-			return nil
-		}
-
-		if block.Type != "CERTIFICATE" {
-			continue
-		}
-
-		cert, err := x509.ParseCertificate(block.Bytes)
-		if err != nil {
-			return err
-		}
-
-		kc.allowedClientCerts = append(kc.allowedClientCerts, cert)
+	if !kc.allowedClientCerts.AppendCertsFromPEM(rawContents) {
+		return fmt.Errorf("Error parsing client certificate file")
 	}
 
 	return nil
@@ -150,11 +135,19 @@ func (kc *KeyConfig) LoadAllowedHosts() error {
 
 func (kc *KeyConfig) IsAnyCertAllowed(
 	certs []*x509.Certificate) *x509.Certificate {
+	opts := x509.VerifyOptions{
+		Roots: kc.allowedClientCerts,
+	}
 	for _, cert := range certs {
-		for _, allowedCert := range kc.allowedClientCerts {
-			if cert.Equal(allowedCert) {
-				return cert
-			}
+		chains, err := cert.Verify(opts)
+		if err != nil {
+			continue
+		}
+
+		// Our clients have only one certificate, so no need to complicate
+		// lookups.
+		if len(chains) > 0 && len(chains[0]) > 0 {
+			return chains[0][len(chains[0])-1]
 		}
 	}
 	return nil
diff --git a/scripts/create-kxd-config b/scripts/create-kxd-config
index 2513b48..d73ee0a 100755
--- a/scripts/create-kxd-config
+++ b/scripts/create-kxd-config
@@ -29,7 +29,7 @@ fi
 if ! [ -e /etc/kxd/cert.pem ]; then
 	echo "Generating certificate (/etc/kxd/cert.pem)"
 	openssl req -new -x509 -batch \
-		-subj "/organizationalUnitName=kxd@$HOSTNAME/" \
+		-subj "/commonName=*/organizationalUnitName=kxd@$HOSTNAME/" \
 		-key /etc/kxd/key.pem -out /etc/kxd/cert.pem
 else
 	echo "Certificate already exists (/etc/kxd/cert.pem)"
diff --git a/scripts/kxc-add-key b/scripts/kxc-add-key
index 6d57a81..1606e1c 100755
--- a/scripts/kxc-add-key
+++ b/scripts/kxc-add-key
@@ -40,7 +40,7 @@ fi
 if ! [ -e /etc/kxc/cert.pem ]; then
 	echo "Generating certificate (/etc/kxc/cert.pem)"
 	openssl req -new -x509 -batch \
-		-subj "/organizationalUnitName=kxc@$HOSTNAME/" \
+		-subj "/commonName=*/organizationalUnitName=kxc@$HOSTNAME/" \
 		-key /etc/kxc/key.pem -out /etc/kxc/cert.pem
 else
 	echo "Certificate already exists (/etc/kxc/cert.pem)"
diff --git a/tests/run_tests b/tests/run_tests
index cef7248..c02c722 100755
--- a/tests/run_tests
+++ b/tests/run_tests
@@ -66,8 +66,10 @@ def gen_certs(path):
         stdout=DEVNULL, stderr=DEVNULL)
     subprocess.check_call(
         ["openssl", "req", "-new", "-x509", "-batch",
-         "-subj", "/organizationalUnitName=kxd-tests:%s@%s" % (
-            os.getlogin(), platform.node()),
+         "-subj",
+            "/commonName=*" +
+            "/organizationalUnitName=kxd-tests:%s@%s" % (
+                os.getlogin(), platform.node()),
          "-key", "%s/key.pem" % path,
          "-out", "%s/cert.pem" % path],
         stdout=DEVNULL, stderr=DEVNULL)
@@ -227,7 +229,7 @@ class Simple(TestCase):
         # We tell the client to expect the server certificate to be the client
         # one, which is never going to work.
         self.assertClientFails("kxd://localhost/k1",
-                "No server certificate matches",
+                "certificate signed by unknown authority",
                 cert_path=self.client.cert_path())
 
 
@@ -350,7 +352,7 @@ class BrokenServerConfig(TestCase):
 
         # Corrupt the client certificate.
         cfd = open(self.server.path + "/data/k1/allowed_clients", "r+")
-        for _ in range(4):
+        for _ in range(5):
             cfd.readline()
         cfd.write('+/+BROKEN+/+')
         cfd.close()