git » debian:kxd » commit 16ff15d

Imported Upstream version 0.11

author Maximiliano Curia
2014-05-02 12:27:46 UTC
committer Maximiliano Curia
2014-05-02 12:27:46 UTC
parent 11c3a60e3d15d3d3bd86b3a909d5db8f120b9784

Imported Upstream version 0.11

LICENSE +25 -0
Makefile +1 -1
README +23 -10
cryptsetup/initramfs-scripts/{premount-net => kxc-premount-net} +3 -1
doc/man/kxc-cryptsetup.rst +7 -0
doc/man/kxc.rst +7 -0
doc/man/kxd.rst +7 -0
doc/quick_start.rst +29 -13
kxc/kxc.go +28 -78
kxd/email.go +14 -4
kxd/key_config.go +11 -24
kxd/kxd.go +17 -5
scripts/create-kxd-config +1 -1
scripts/kxc-add-key +1 -1
tests/.pylintrc +1 -1
tests/openssl.cnf +276 -0
tests/run_tests +282 -82

diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..1626441
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,25 @@
+kxd is under the MIT licence, which is reproduced below (taken from
+http://opensource.org/licenses/MIT).
+
+-----
+
+Copyright (c) 2014  Alberto Bertogli
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/Makefile b/Makefile
index 22b86ac..7b0f602 100644
--- a/Makefile
+++ b/Makefile
@@ -59,7 +59,7 @@ install-initramfs: install-kxc
 	$(INSTALL) -m 0755 cryptsetup/initramfs-hooks/kxc \
 		$(PREFIX)/share/initramfs-tools/hooks/
 	$(INSTALL) -d $(PREFIX)/share/initramfs-tools/scripts/init-premount
-	$(INSTALL) -m 0755 cryptsetup/initramfs-scripts/premount-net \
+	$(INSTALL) -m 0755 cryptsetup/initramfs-scripts/kxc-premount-net \
 		$(PREFIX)/share/initramfs-tools/scripts/init-premount/
 
 
diff --git a/README b/README
index 41cb678..fff2d53 100644
--- a/README
+++ b/README
@@ -23,15 +23,15 @@ The server configuration is stored in a root directory (/etc/kxd/data), and
 within there, with per-key directories (e.g. /etc/kxd/data/host1/key1), each
 containing the following files:
 
-  - key: Contains the key to give to the client.
-  - allowed_clients: Contains one or more PEM-encoded client certificates that
-    will be allowed to request the key.
-    If not present, then no clients will be allowed to access this key.
-  - allowed_hosts: Contains one or more host names (one per line).
-    If not present, then all hosts will be allowed to access that key (as long
-    as they are authorized with a valid client certificate).
-  - email_to: Contains one or more email destinations to notify (one per line).
-    If not present, then no notifications will be sent upon key accesses.
+ - key: Contains the key to give to the client.
+ - allowed_clients: Contains one or more PEM-encoded client certificates that
+   will be allowed to request the key.
+   If not present, then no clients will be allowed to access this key.
+ - allowed_hosts: Contains one or more host names (one per line).
+   If not present, then all hosts will be allowed to access that key (as long
+   as they are authorized with a valid client certificate).
+ - email_to: Contains one or more email destinations to notify (one per line).
+   If not present, then no notifications will be sent upon key accesses.
 
 
 Client configuration
@@ -62,7 +62,7 @@ given on the command line, and will only accept keys from it.
 
 Note the server will return reasonably detailed information on errors, for
 example it will tell when a key is not found vs. when the client is not
-allowed. While this leaks some information about existance of keys, it makes
+allowed. While this leaks some information about existence of keys, it makes
 troubleshooting much easier.
 
 The server itself makes no effort to protect the data internally; for example,
@@ -70,6 +70,19 @@ there is no on-disk encryption, and memory is not locked. We work under the
 assumption that the server's host is secure and trusted.
 
 
+Dependencies
+------------
+
+There are no runtime dependencies for the kxd and kxc binaries.
+
+Building requires Go 1.2.
+
+The configuration helper scripts (create-kxd-config, kxc-add-key, etc.)
+depend on: bash, openssl (the binary), and core utilities (mkdir, dd, etc.).
+
+Testing needs Python 2.7, and openssl (the binary).
+
+
 Bugs and contact
 ----------------
 
diff --git a/cryptsetup/initramfs-scripts/premount-net b/cryptsetup/initramfs-scripts/kxc-premount-net
similarity index 76%
rename from cryptsetup/initramfs-scripts/premount-net
rename to cryptsetup/initramfs-scripts/kxc-premount-net
index b131058..5032ee1 100755
--- a/cryptsetup/initramfs-scripts/premount-net
+++ b/cryptsetup/initramfs-scripts/kxc-premount-net
@@ -22,6 +22,8 @@ esac
 configure_networking
 
 # Configure a basic resolv.conf based on our networking.
-echo "nameserver $IPV4DNS0" >> /etc/resolv.conf
+if ! [ -s /etc/resolv.conf ]; then
+	echo "nameserver $IPV4DNS0" >> /etc/resolv.conf
+fi
 
 
diff --git a/doc/man/kxc-cryptsetup.rst b/doc/man/kxc-cryptsetup.rst
index 299e156..a6769b0 100644
--- a/doc/man/kxc-cryptsetup.rst
+++ b/doc/man/kxc-cryptsetup.rst
@@ -60,3 +60,10 @@ SEE ALSO
 
 ``kxc(1)``, ``kxd(1)``, ``crypttab(5)``, ``cryptsetup(8)``.
 
+
+BUGS
+====
+
+If you want to report bugs, or have any questions or comments, just let me
+know. For more information, you can go to http://blitiri.com.ar/p/kxd.
+
diff --git a/doc/man/kxc.rst b/doc/man/kxc.rst
index c6cd762..54b25f2 100644
--- a/doc/man/kxc.rst
+++ b/doc/man/kxc.rst
@@ -50,3 +50,10 @@ SEE ALSO
 
 ``kxc-cryptsetup(1)``, ``kxd(1)``.
 
+
+BUGS
+====
+
+If you want to report bugs, or have any questions or comments, just let me
+know. For more information, you can go to http://blitiri.com.ar/p/kxd.
+
diff --git a/doc/man/kxd.rst b/doc/man/kxd.rst
index 5383da0..9525b86 100644
--- a/doc/man/kxd.rst
+++ b/doc/man/kxd.rst
@@ -102,3 +102,10 @@ SEE ALSO
 
 ``kxc(1)``, ``kxc-cryptsetup(1)``.
 
+
+BUGS
+====
+
+If you want to report bugs, or have any questions or comments, just let me
+know. For more information, you can go to http://blitiri.com.ar/p/kxd.
+
diff --git a/doc/quick_start.rst b/doc/quick_start.rst
index 5a53a46..648e146 100644
--- a/doc/quick_start.rst
+++ b/doc/quick_start.rst
@@ -3,31 +3,35 @@
  Key Exchange Daemon - Quick start
 ===================================
 
-In this guide we show how to set up a key exchange daemon and client
+In this guide we show how to set up a `key exchange daemon`_ and client
 on a typical scenario where the keys are used to open a device encrypted with
-dm-crypt (the standard Linux disk encryption).
+dm-crypt_ (the standard Linux disk encryption).
 
-``server`` is the hostname of the server.
-``client`` is the hostname of the client.
-``sda2`` is the encrypted drive.
+These steps have been checked on a Debian install, other distributions should
+be similar but may differ on some of the details (specially on the
+"`Configuring crypttab`_" section).
+
+- ``server`` is the hostname of the server.
+- ``client`` is the hostname of the client.
+- ``sda2`` is the encrypted drive.
 
 
 Initial server setup
 ====================
 
-First of all, install *kxd* on the server, usually via your distribution
+First of all, install kxd_ on the server, usually via your distribution
 packages, or directly from source.
 
 Then, run ``create-kxd-config``, which will create the configuration
-directories, and generate the server key/cert pair. Everything is in
-``/etc/kxd/``.
+directories, and generate a self-signed_ key/cert pair for the server.
+Everything is in ``/etc/kxd/``.
 
 
 Initial client setup
 ====================
 
-Install *kxc* on the client machine, usually via your distribution packages,
-or directly from source.
+Install kxc_ on the client machine, usually via your distribution packages, or
+directly from source.
 
 
 Then, run ``kxc-add-key server sda2``, which will create the configuration
@@ -85,9 +89,10 @@ In order to get kxc to be run automatically to fetch the key, we need to edit
   sda2_crypt UUID=blah-blah-blah sda2 luks,keyscript=kxc-cryptsetup
                                  ^^^^      ^^^^^^^^^^^^^^^^^^^^^^^^
 
-Note the "sda2" field corresponds to the name we've been passing around in
-previous sections. The ``keyscript=kxc-cryptsetup`` is our way of telling the
-cryptsetup infrastructure to use our script to fetch the key for this target.
+Note the ``sda2`` field corresponds to the name we've been passing around in
+previous sections. The ``keyscript=kxc-cryptsetup`` option is our way of
+telling the cryptsetup infrastructure to use our script to fetch the key for
+this target.
 
 
 You can test that this works by using::
@@ -96,3 +101,14 @@ You can test that this works by using::
   cryptdisks_start sda2_crypt
 
 The second command should issue a request to your server to get the key.
+
+Consider running ``update-initramfs`` if your device is the root device, or it
+is needed very early in the boot process.
+
+
+.. _key exchange daemon: http://blitiri.com.ar/p/kxd
+.. _kxd: http://blitiri.com.ar/p/kxd
+.. _kxc: http://blitiri.com.ar/p/kxd
+.. _dm-crypt: https://en.wikipedia.org/wiki/dm-crypt
+.. _self-signed: https://en.wikipedia.org/wiki/Self-signed_certificate
+
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/email.go b/kxd/email.go
index d6ca469..ea42121 100644
--- a/kxd/email.go
+++ b/kxd/email.go
@@ -19,6 +19,7 @@ type EmailBody struct {
 	TimeString string
 	Req        *Request
 	Cert       *x509.Certificate
+	Chains     [][]*x509.Certificate
 }
 
 const emailTmplBody = (`Date: {{.TimeString}}
@@ -32,16 +33,20 @@ On: {{.TimeString}}
 
 Client certificate:
   Signature: {{printf "%.16s" (printf "%x" .Cert.Signature)}}...
-  Issuer: {{NameToString .Cert.Issuer}}
   Subject: {{NameToString .Cert.Subject}}
 
+Authorizing chains:
+{{range .Chains}}  {{ChainToString .}}
+{{end}}
+
 `)
 
 var emailTmpl = template.New("email")
 
 func init() {
 	emailTmpl.Funcs(map[string]interface{}{
-		"NameToString": NameToString,
+		"NameToString":  NameToString,
+		"ChainToString": ChainToString,
 	})
 
 	template.Must(emailTmpl.Parse(emailTmplBody))
@@ -55,6 +60,9 @@ func NameToString(name pkix.Name) string {
 	for _, o := range name.Organization {
 		s = append(s, fmt.Sprintf("O=%s", o))
 	}
+	for _, o := range name.OrganizationalUnit {
+		s = append(s, fmt.Sprintf("OU=%s", o))
+	}
 
 	if name.CommonName != "" {
 		s = append(s, fmt.Sprintf("N=%s", name.CommonName))
@@ -63,7 +71,8 @@ func NameToString(name pkix.Name) string {
 	return strings.Join(s, " ")
 }
 
-func SendMail(kc *KeyConfig, req *Request, cert *x509.Certificate) error {
+func SendMail(kc *KeyConfig, req *Request,
+	chains [][]*x509.Certificate) error {
 	if *smtp_addr == "" {
 		req.Printf("Skipping notifications")
 		return nil
@@ -91,7 +100,8 @@ func SendMail(kc *KeyConfig, req *Request, cert *x509.Certificate) error {
 		Time:       now,
 		TimeString: now.Format(time.RFC1123Z),
 		Req:        req,
-		Cert:       cert,
+		Cert:       chains[0][0],
+		Chains:     chains,
 	}
 
 	msg := new(bytes.Buffer)
diff --git a/kxd/key_config.go b/kxd/key_config.go
index 5b9e491..b4b191d 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
@@ -149,12 +134,14 @@ func (kc *KeyConfig) LoadAllowedHosts() error {
 }
 
 func (kc *KeyConfig) IsAnyCertAllowed(
-	certs []*x509.Certificate) *x509.Certificate {
+	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 && len(chains) > 0 {
+			return chains
 		}
 	}
 	return nil
diff --git a/kxd/kxd.go b/kxd/kxd.go
index 3f1fd71..f5521e7 100644
--- a/kxd/kxd.go
+++ b/kxd/kxd.go
@@ -68,7 +68,19 @@ func (req *Request) KeyPath() (string, error) {
 
 func CertToString(cert *x509.Certificate) string {
 	return fmt.Sprintf(
-		"<signature:0x%.8s>", fmt.Sprintf("%x", cert.Signature))
+		"(0x%.8s ou:%s)",
+		fmt.Sprintf("%x", cert.Signature),
+		cert.Subject.OrganizationalUnit)
+}
+
+func ChainToString(chain []*x509.Certificate) (s string) {
+	for i, cert := range chain {
+		s += CertToString(cert)
+		if i < len(chain)-1 {
+			s += " -> "
+		}
+	}
+	return s
 }
 
 // HandlerV1 handles /v1/ key requests.
@@ -135,8 +147,8 @@ func HandlerV1(w http.ResponseWriter, httpreq *http.Request) {
 		return
 	}
 
-	validCert := keyConf.IsAnyCertAllowed(req.TLS.PeerCertificates)
-	if validCert == nil {
+	validChains := keyConf.IsAnyCertAllowed(req.TLS.PeerCertificates)
+	if validChains == nil {
 		req.Printf("No allowed certificate found")
 		http.Error(w, "No allowed certificate found",
 			http.StatusForbidden)
@@ -151,9 +163,9 @@ func HandlerV1(w http.ResponseWriter, httpreq *http.Request) {
 		return
 	}
 
-	req.Printf("Allowing request to cert %s", CertToString(validCert))
+	req.Printf("Allowing request to %s", CertToString(validChains[0][0]))
 
-	err = SendMail(keyConf, &req, validCert)
+	err = SendMail(keyConf, &req, validChains)
 	if err != nil {
 		req.Printf("Error sending notification: %s", err)
 		http.Error(w, "Error sending notification",
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/.pylintrc b/tests/.pylintrc
index 4ef871e..10e00cc 100644
--- a/tests/.pylintrc
+++ b/tests/.pylintrc
@@ -1,6 +1,6 @@
 
 [MESSAGES CONTROL]
-disable=missing-docstring, too-many-public-methods, fixme
+disable=missing-docstring, too-many-public-methods, fixme, locally-disabled
 
 [REPORTS]
 output-format=colorized
diff --git a/tests/openssl.cnf b/tests/openssl.cnf
new file mode 100644
index 0000000..9c8b4f3
--- /dev/null
+++ b/tests/openssl.cnf
@@ -0,0 +1,276 @@
+# OpenSSL configuration for kxd tests.
+#
+# This file is used in some of the kxd tests, to avoid depending on the local
+# configuration which can vary between different systems.
+#
+# It's only used for CA operations, so it focuses on that.
+# It is based on Debian's default configuration.
+#############################################################################
+
+# This definition stops the following lines choking if HOME isn't
+# defined.
+HOME			= .
+RANDFILE		= $ENV::HOME/.rnd
+
+[ ca ]
+default_ca	= CA_default
+
+[ CA_default ]
+
+dir		= ./kxd-ca		# Where everything is kept
+certs		= $dir/certs		# Where the issued certs are kept
+crl_dir		= $dir/crl		# Where the issued crl are kept
+database	= $dir/index.txt	# database index file.
+#unique_subject	= no			# Set to 'no' to allow creation of
+					# several ctificates with same subject.
+new_certs_dir	= $dir/newcerts		# default place for new certs.
+
+certificate	= $dir/cacert.pem 	# The CA certificate
+serial		= $dir/serial 		# The current serial number
+crlnumber	= $dir/crlnumber	# the current crl number
+					# must be commented out to leave a V1 CRL
+crl		= $dir/crl.pem 		# The current CRL
+private_key	= $dir/private/cakey.pem # The private key
+RANDFILE	= $dir/private/.rand	# private random number file
+
+x509_extensions	= usr_cert		# The extentions to add to the cert
+
+# Comment out the following two lines for the "traditional"
+# (and highly broken) format.
+name_opt 	= ca_default		# Subject Name options
+cert_opt 	= ca_default		# Certificate field options
+
+default_days	= 365			# how long to certify for
+default_crl_days= 30			# how long before next CRL
+default_md	= default		# use public key default MD
+preserve	= no			# keep passed DN ordering
+
+policy		= policy_anything
+
+[ policy_anything ]
+countryName		= optional
+stateOrProvinceName	= optional
+localityName		= optional
+organizationName	= optional
+organizationalUnitName	= optional
+commonName		= supplied
+emailAddress		= optional
+
+####################################################################
+[ req ]
+default_bits		= 2048
+default_keyfile 	= privkey.pem
+distinguished_name	= req_distinguished_name
+attributes		= req_attributes
+x509_extensions	= v3_ca	# The extentions to add to the self signed cert
+
+# Passwords for private keys if not present they will be prompted for
+# input_password = secret
+# output_password = secret
+
+# This sets a mask for permitted string types. There are several options. 
+# default: PrintableString, T61String, BMPString.
+# pkix	 : PrintableString, BMPString (PKIX recommendation before 2004)
+# utf8only: only UTF8Strings (PKIX recommendation after 2004).
+# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings).
+# MASK:XXXX a literal mask value.
+# WARNING: ancient versions of Netscape crash on BMPStrings or UTF8Strings.
+string_mask = utf8only
+
+# req_extensions = v3_req # The extensions to add to a certificate request
+
+[ req_distinguished_name ]
+countryName			= Country Name (2 letter code)
+countryName_default		= AU
+countryName_min			= 2
+countryName_max			= 2
+
+stateOrProvinceName		= State or Province Name (full name)
+stateOrProvinceName_default	= Some-State
+
+localityName			= Locality Name (eg, city)
+
+0.organizationName		= Organization Name (eg, company)
+0.organizationName_default	= Internet Widgits Pty Ltd
+
+# we can do this but it is not needed normally :-)
+#1.organizationName		= Second Organization Name (eg, company)
+#1.organizationName_default	= World Wide Web Pty Ltd
+
+organizationalUnitName		= Organizational Unit Name (eg, section)
+#organizationalUnitName_default	=
+
+commonName			= Common Name (e.g. server FQDN or YOUR name)
+commonName_max			= 64
+
+emailAddress			= Email Address
+emailAddress_max		= 64
+
+# SET-ex3			= SET extension number 3
+
+[ req_attributes ]
+challengePassword		= A challenge password
+challengePassword_min		= 4
+challengePassword_max		= 20
+
+unstructuredName		= An optional company name
+
+[ usr_cert ]
+
+# These extensions are added when 'ca' signs a request.
+
+# This goes against PKIX guidelines but some CAs do it and some software
+# requires this to avoid interpreting an end user certificate as a CA.
+
+basicConstraints=CA:FALSE
+
+# Here are some examples of the usage of nsCertType. If it is omitted
+# the certificate can be used for anything *except* object signing.
+
+# This is OK for an SSL server.
+# nsCertType			= server
+
+# For an object signing certificate this would be used.
+# nsCertType = objsign
+
+# For normal client use this is typical
+# nsCertType = client, email
+
+# and for everything including object signing:
+# nsCertType = client, email, objsign
+
+# This is typical in keyUsage for a client certificate.
+# keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+
+# This will be displayed in Netscape's comment listbox.
+nsComment			= "OpenSSL Generated Certificate"
+
+# PKIX recommendations harmless if included in all certificates.
+subjectKeyIdentifier=hash
+authorityKeyIdentifier=keyid,issuer
+
+# This stuff is for subjectAltName and issuerAltname.
+# Import the email address.
+# subjectAltName=email:copy
+# An alternative to produce certificates that aren't
+# deprecated according to PKIX.
+# subjectAltName=email:move
+
+# Copy subject details
+# issuerAltName=issuer:copy
+
+#nsCaRevocationUrl		= http://www.domain.dom/ca-crl.pem
+#nsBaseUrl
+#nsRevocationUrl
+#nsRenewalUrl
+#nsCaPolicyUrl
+#nsSslServerName
+
+# This is required for TSA certificates.
+# extendedKeyUsage = critical,timeStamping
+
+[ v3_req ]
+
+# Extensions to add to a certificate request
+
+basicConstraints = CA:FALSE
+keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+
+[ v3_ca ]
+
+
+# Extensions for a typical CA
+
+
+# PKIX recommendation.
+
+subjectKeyIdentifier=hash
+
+authorityKeyIdentifier=keyid:always,issuer
+
+# This is what PKIX recommends but some broken software chokes on critical
+# extensions.
+#basicConstraints = critical,CA:true
+# So we do this instead.
+basicConstraints = CA:true
+
+# Key usage: this is typical for a CA certificate. However since it will
+# prevent it being used as an test self-signed certificate it is best
+# left out by default.
+# keyUsage = cRLSign, keyCertSign
+
+# Some might want this also
+# nsCertType = sslCA, emailCA
+
+# Include email address in subject alt name: another PKIX recommendation
+# subjectAltName=email:copy
+# Copy issuer details
+# issuerAltName=issuer:copy
+
+# DER hex encoding of an extension: beware experts only!
+# obj=DER:02:03
+# Where 'obj' is a standard or added object
+# You can even override a supported extension:
+# basicConstraints= critical, DER:30:03:01:01:FF
+
+[ crl_ext ]
+
+# CRL extensions.
+# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL.
+
+# issuerAltName=issuer:copy
+authorityKeyIdentifier=keyid:always
+
+[ proxy_cert_ext ]
+# These extensions should be added when creating a proxy certificate
+
+# This goes against PKIX guidelines but some CAs do it and some software
+# requires this to avoid interpreting an end user certificate as a CA.
+
+basicConstraints=CA:FALSE
+
+# Here are some examples of the usage of nsCertType. If it is omitted
+# the certificate can be used for anything *except* object signing.
+
+# This is OK for an SSL server.
+# nsCertType			= server
+
+# For an object signing certificate this would be used.
+# nsCertType = objsign
+
+# For normal client use this is typical
+# nsCertType = client, email
+
+# and for everything including object signing:
+# nsCertType = client, email, objsign
+
+# This is typical in keyUsage for a client certificate.
+# keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+
+# This will be displayed in Netscape's comment listbox.
+nsComment			= "OpenSSL Generated Certificate"
+
+# PKIX recommendations harmless if included in all certificates.
+subjectKeyIdentifier=hash
+authorityKeyIdentifier=keyid,issuer
+
+# This stuff is for subjectAltName and issuerAltname.
+# Import the email address.
+# subjectAltName=email:copy
+# An alternative to produce certificates that aren't
+# deprecated according to PKIX.
+# subjectAltName=email:move
+
+# Copy subject details
+# issuerAltName=issuer:copy
+
+#nsCaRevocationUrl		= http://www.domain.dom/ca-crl.pem
+#nsBaseUrl
+#nsRevocationUrl
+#nsRenewalUrl
+#nsCaPolicyUrl
+#nsSslServerName
+
+# This really needs to be in place for it to be a proxy certificate.
+proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo
+
diff --git a/tests/run_tests b/tests/run_tests
index 8d7a98b..149380a 100755
--- a/tests/run_tests
+++ b/tests/run_tests
@@ -12,8 +12,11 @@ client under various conditions, to make sure they behave as intended.
 
 # NOTE: Please run "pylint --rcfile=.pylintrc run_tests" after making changes,
 # to make sure the file has a reasonably uniform coding style.
+# You can also use "autopep8 -d --ignore=E301,E26 run_tests" to help with
+# this, but make sure the output looks sane.
 
 
+import contextlib
 import httplib
 import os
 import platform
@@ -35,7 +38,11 @@ import unittest
 # Path to our built binaries; used to run the server and client for testing
 # purposes.
 BINS = os.path.abspath(
-        os.path.dirname(os.path.realpath(__file__)) + "/../out")
+    os.path.dirname(os.path.realpath(__file__)) + "/../out")
+
+# Path to the test OpenSSL configuration.
+OPENSSL_CONF = os.path.abspath(
+    os.path.dirname(os.path.realpath(__file__)) + "/openssl.cnf")
 
 DEVNULL = open("/dev/null", "w")
 
@@ -56,33 +63,41 @@ def tearDownModule():   # pylint: disable=invalid-name
     # Remove the temporary directory only on success.
     # Be extra paranoid about removing.
     # TODO: Only remove on success.
+    if os.environ.get('KEEPTMP'):
+        return
     if len(TEMPDIR) > 10 and not TEMPDIR.startswith("/home"):
         shutil.rmtree(TEMPDIR)
 
 
-def gen_certs(path):
-    subprocess.check_call(
-        ["openssl", "genrsa", "-out", "%s/key.pem" % path, "2048"],
-        stdout=DEVNULL, stderr=DEVNULL)
-    subprocess.check_call(
-        ["openssl", "req", "-new", "-x509", "-batch",
-         "-subj", "/organizationalUnitName=kxd-tests:%s@%s" % (
-            os.getlogin(), platform.node()),
-         "-key", "%s/key.pem" % path,
-         "-out", "%s/cert.pem" % path],
-        stdout=DEVNULL, stderr=DEVNULL)
+@contextlib.contextmanager
+def pushd(path):
+    prev = os.getcwd()
+    os.chdir(path)
+    yield
+    os.chdir(prev)
 
 
 class Config(object):
-    def __init__(self):
-        # Note we create this temporary path but never delete it.
-        # This is intentional, as we rather leave a test file around, than
-        # risk a bug doing a harmful recursive delete.
-        self.path = tempfile.mkdtemp(prefix="config-", dir=TEMPDIR)
-        print "Using temporary path", self.path
+    def __init__(self, name):
+        self.path = tempfile.mkdtemp(prefix="config-%s-" % name, dir=TEMPDIR)
+        self.name = name
+
+    def gen_certs(self, self_sign=True):
+        subprocess.check_call(
+            ["openssl", "genrsa", "-out", "%s/key.pem" % self.path, "2048"],
+            stdout=DEVNULL, stderr=DEVNULL)
+
+        req_args = ["openssl", "req", "-new", "-batch",
+                    "-subj", ("/commonName=*" +
+                              "/organizationalUnitName=kxd-tests-%s:%s@%s" % (
+                                  self.name, os.getlogin(), platform.node())),
+                    "-key", "%s/key.pem" % self.path]
+        if self_sign:
+            req_args.extend(["-x509", "-out", "%s/cert.pem" % self.path])
+        else:
+            req_args.extend(["-out", "%s/cert.csr" % self.path])
 
-    def gen_certs(self):
-        gen_certs(self.path)
+        subprocess.check_call(req_args, stdout=DEVNULL, stderr=DEVNULL)
 
     def cert_path(self):
         return self.path + "/cert.pem"
@@ -90,14 +105,63 @@ class Config(object):
     def key_path(self):
         return self.path + "/key.pem"
 
+    def csr_path(self):
+        return self.path + "/cert.csr"
+
     def cert(self):
         return open(self.path + "/cert.pem").read()
 
 
-class ServerConfig(Config):
+class CA(object):
     def __init__(self):
-        Config.__init__(self)
+        # TODO: This works because of Debian's default config; it needs to be
+        # generalized, probably by including an openssl config to use.
+        self.path = tempfile.mkdtemp(prefix="config-ca-", dir=TEMPDIR)
+        os.makedirs(self.path + "/kxd-ca/newcerts/")
+
+        try:
+            # We need to run the CA commands from within the path.
+            with pushd(self.path):
+                open("kxd-ca/index.txt", "w")
+                open("kxd-ca/serial", "w").write("1000\n")
+                subprocess.check_output(
+                    ["openssl", "req", "-new", "-x509", "-batch",
+                     "-config", OPENSSL_CONF,
+                     "-subj", ("/commonName=*" +
+                               "/organizationalUnitName=kxd-tests-ca:%s@%s" % (
+                                   os.getlogin(), platform.node())),
+                     "-extensions", "v3_ca", "-nodes",
+                     "-keyout", "cakey.pem",
+                     "-out", "cacert.pem"],
+                    stderr=subprocess.STDOUT)
+        except subprocess.CalledProcessError as err:
+            print "openssl call failed, output: %r" % err.output
+            raise
+
+    def sign(self, csr):
+        try:
+            with pushd(self.path):
+                subprocess.check_output(
+                    ["openssl", "ca", "-batch", "-config", OPENSSL_CONF,
+                     "-keyfile", "cakey.pem", "-cert", "cacert.pem",
+                     "-in", csr, "-out", "%s.pem" % os.path.splitext(csr)[0]],
+                    stderr=subprocess.STDOUT)
+        except subprocess.CalledProcessError as err:
+            print "openssl call failed, output: %r" % err.output
+            raise
+
+    def cert_path(self):
+        return self.path + "/cacert.pem"
+
+    def cert(self):
+        return open(self.path + "/cacert.pem").read()
+
+
+class ServerConfig(Config):
+    def __init__(self, self_sign=True, name="server"):
+        Config.__init__(self, name)
         self.keys = {}
+        self.gen_certs(self_sign)
 
     def new_key(self, name, allowed_clients=None, allowed_hosts=None):
         self.keys[name] = os.urandom(1024)
@@ -118,9 +182,9 @@ class ServerConfig(Config):
 
 
 class ClientConfig(Config):
-    def __init__(self):
-        Config.__init__(self)
-        self.gen_certs()
+    def __init__(self, self_sign=True, name="client"):
+        Config.__init__(self, name)
+        self.gen_certs(self_sign)
 
     def call(self, server_cert, url):
         args = [BINS + "/kxc",
@@ -128,7 +192,12 @@ class ClientConfig(Config):
                 "--client_key=%s/key.pem" % self.path,
                 "--server_cert=%s" % server_cert,
                 url]
-        return subprocess.check_output(args, stderr=subprocess.STDOUT)
+        try:
+            print "Running client:", " ".join(args)
+            return subprocess.check_output(args, stderr=subprocess.STDOUT)
+        except subprocess.CalledProcessError as err:
+            print "Client call failed, output: %r" % err.output
+            raise
 
 
 def launch_daemon(cfg):
@@ -137,16 +206,24 @@ def launch_daemon(cfg):
             "--key=%s/key.pem" % cfg,
             "--cert=%s/cert.pem" % cfg,
             "--logfile=%s/log" % cfg]
+    print "Launching server: ", " ".join(args)
     return subprocess.Popen(args)
 
 
 class TestCase(unittest.TestCase):
     def setUp(self):
         self.server = ServerConfig()
-        self.server.gen_certs()
-        self.daemon = launch_daemon(self.server.path)
-
         self.client = ClientConfig()
+        self.daemon = None
+        self.ca = None    # pylint: disable=invalid-name
+        self.launch_server(self.server)
+
+    def tearDown(self):
+        if self.daemon:
+            self.daemon.kill()
+
+    def launch_server(self, server):
+        self.daemon = launch_daemon(server.path)
 
         # Wait for the server to start accepting connections.
         deadline = time.time() + 5
@@ -159,16 +236,15 @@ class TestCase(unittest.TestCase):
         else:
             self.fail("Timeout waiting for the server")
 
-    def tearDown(self):
-        self.daemon.kill()
-
     # pylint: disable=invalid-name
-    def assertClientFails(self, url, regexp, client=None):
+    def assertClientFails(self, url, regexp, client=None, cert_path=None):
         if client is None:
             client = self.client
+        if cert_path is None:
+            cert_path = self.server.cert_path()
 
         try:
-            client.call(self.server.cert_path(), url)
+            client.call(cert_path, url)
         except subprocess.CalledProcessError as err:
             self.assertRegexpMatches(err.output, regexp)
         else:
@@ -183,33 +259,44 @@ class Simple(TestCase):
     """Simple test cases for common (mis)configurations."""
 
     def test_simple(self):
+        # There's no need to split these up; by doing all these within a
+        # single test, we speed things up significantly, as we avoid the
+        # overhead of creating the certificates and bringing up the server.
+
+        # Normal successful case.
         self.server.new_key("k1",
-                allowed_clients=[self.client.cert()],
-                allowed_hosts=["localhost"])
+                            allowed_clients=[self.client.cert()],
+                            allowed_hosts=["localhost"])
         key = self.client.call(self.server.cert_path(), "kxd://localhost/k1")
         self.assertEquals(key, self.server.keys["k1"])
 
-    def test_404(self):
-        self.assertClientFails("kxd://localhost/k1", "404 Not Found")
+        # Unknown key -> 404.
+        self.assertClientFails("kxd://localhost/k2", "404 Not Found")
 
-    def test_no_client_cert(self):
-        self.server.new_key("k1", allowed_hosts=["localhost"])
-        self.assertClientFails("kxd://localhost/k1",
-                "403 Forbidden.*No allowed certificate found")
+        # No certificates allowed -> 403.
+        self.server.new_key("k3", allowed_hosts=["localhost"])
+        self.assertClientFails("kxd://localhost/k3",
+                               "403 Forbidden.*No allowed certificate found")
 
-    def test_host_not_allowed(self):
-        self.server.new_key("k1",
-                allowed_clients=[self.client.cert()],
-                allowed_hosts=[])
-        self.assertClientFails("kxd://localhost/k1",
-                "403 Forbidden.*Host not allowed")
+        # Host not allowed -> 403.
+        self.server.new_key("k4",
+                            allowed_clients=[self.client.cert()],
+                            allowed_hosts=[])
+        self.assertClientFails("kxd://localhost/k4",
+                               "403 Forbidden.*Host not allowed")
 
-    def test_not_allowed(self):
-        self.server.new_key("k1")
+        # Nothing allowed -> 403.
         # We don't restrict the reason of failure, that's not defined in this
         # case, as it could be either the host or the cert that are validated
         # first.
-        self.assertClientFails("kxd://localhost/k1", "403 Forbidden")
+        self.server.new_key("k5")
+        self.assertClientFails("kxd://localhost/k5", "403 Forbidden")
+
+        # 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",
+                               "certificate signed by unknown authority",
+                               cert_path=self.client.cert_path())
 
 
 class Multiples(TestCase):
@@ -217,52 +304,78 @@ class Multiples(TestCase):
 
     def setUp(self):
         TestCase.setUp(self)
-        self.client2 = ClientConfig()
+        self.client2 = ClientConfig(name="client2")
 
     def test_two_clients(self):
         self.server.new_key("k1",
-                allowed_clients=[self.client.cert(), self.client2.cert()],
-                allowed_hosts=["localhost"])
+                            allowed_clients=[
+                                self.client.cert(), self.client2.cert()],
+                            allowed_hosts=["localhost"])
         key = self.client.call(self.server.cert_path(), "kxd://localhost/k1")
         self.assertEquals(key, self.server.keys["k1"])
 
         key = self.client2.call(self.server.cert_path(), "kxd://localhost/k1")
         self.assertEquals(key, self.server.keys["k1"])
 
-    def test_one_client_allowed(self):
-        self.server.new_key("k1",
-                allowed_clients=[self.client.cert()],
-                allowed_hosts=["localhost"])
-        key = self.client.call(self.server.cert_path(), "kxd://localhost/k1")
-        self.assertEquals(key, self.server.keys["k1"])
+        # Only one client allowed.
+        self.server.new_key("k2",
+                            allowed_clients=[self.client.cert()],
+                            allowed_hosts=["localhost"])
+        key = self.client.call(self.server.cert_path(), "kxd://localhost/k2")
+        self.assertEquals(key, self.server.keys["k2"])
 
-        self.assertClientFails("kxd://localhost/k1",
-                "403 Forbidden.*No allowed certificate found",
-                client=self.client2)
+        self.assertClientFails("kxd://localhost/k2",
+                               "403 Forbidden.*No allowed certificate found",
+                               client=self.client2)
 
     def test_many_keys(self):
         keys = ["a", "d/e", "a/b/c", "d/"]
         for key in keys:
-            self.server.new_key(key,
-                    allowed_clients=[self.client.cert(), self.client2.cert()],
-                    allowed_hosts=["localhost"])
+            self.server.new_key(
+                key,
+                allowed_clients=[self.client.cert(), self.client2.cert()],
+                allowed_hosts=["localhost"])
 
         for key in keys:
             data = self.client.call(self.server.cert_path(),
-                    "kxd://localhost/%s" % key)
+                                    "kxd://localhost/%s" % key)
             self.assertEquals(data, self.server.keys[key])
 
             data = self.client2.call(self.server.cert_path(),
-                    "kxd://localhost/%s" % key)
+                                     "kxd://localhost/%s" % key)
             self.assertEquals(data, self.server.keys[key])
 
         self.assertClientFails("kxd://localhost/a/b", "404 Not Found")
 
+    def test_two_servers(self):
+        server1 = self.server
+        server1.new_key("k1", allowed_clients=[self.client.cert()])
+        server2 = ServerConfig(name="server2")
+        server2.new_key("k1", allowed_clients=[self.client.cert()])
+
+        # Write a file containing the certs of both servers.
+        server_certs_path = self.client.path + "/server_certs.pem"
+        server_certs = open(server_certs_path, "w")
+        server_certs.write(open(server1.cert_path()).read())
+        server_certs.write(open(server2.cert_path()).read())
+        server_certs.close()
+
+        key = self.client.call(server_certs_path, "kxd://localhost/k1")
+        self.assertEquals(key, server1.keys["k1"])
+
+        self.daemon.kill()
+        time.sleep(0.5)
+        self.launch_server(server2)
+
+        key = self.client.call(server_certs_path, "kxd://localhost/k1")
+        self.assertEquals(key, server2.keys["k1"])
+
 
 class TrickyRequests(TestCase):
     """Tests for tricky requests."""
 
-    def test_no_local_cert(self):
+    def test_tricky(self):
+        # No local certificate.
         conn = httplib.HTTPSConnection("localhost", 19840)
         try:
             conn.request("GET", "/v1/")
@@ -271,10 +384,10 @@ class TrickyRequests(TestCase):
         else:
             self.fail("Client call did not fail as expected")
 
-    def test_dotdot(self):
+        # Requests with '..'.
         conn = httplib.HTTPSConnection("localhost", 19840,
-                key_file=self.client.key_path(),
-                cert_file=self.client.cert_path())
+                                       key_file=self.client.key_path(),
+                                       cert_file=self.client.cert_path())
         conn.request("GET", "/v1/a/../b")
         response = conn.getresponse()
 
@@ -285,15 +398,15 @@ class TrickyRequests(TestCase):
     def test_server_cert(self):
         rawsock = socket.create_connection(("localhost", 19840))
         sock = ssl.wrap_socket(rawsock,
-                keyfile=self.client.key_path(),
-                certfile=self.client.cert_path())
+                               keyfile=self.client.key_path(),
+                               certfile=self.client.cert_path())
 
         # We don't check the cipher itself, as it depends on the environment,
         # but we should be using > 128 bit secrets.
         self.assertTrue(sock.cipher()[2] > 128)
 
         server_cert = ssl.DER_cert_to_PEM_cert(
-                sock.getpeercert(binary_form=True))
+            sock.getpeercert(binary_form=True))
         self.assertEquals(server_cert, self.server.cert())
 
 
@@ -302,27 +415,114 @@ class BrokenServerConfig(TestCase):
 
     def test_broken_client_certs(self):
         self.server.new_key("k1",
-                allowed_clients=[self.client.cert()],
-                allowed_hosts=["localhost"])
+                            allowed_clients=[self.client.cert()],
+                            allowed_hosts=["localhost"])
 
         # 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()
 
-        self.assertClientFails("kxd://localhost/k1",
-                "500 Internal Server Error.*Error loading certs")
+        self.assertClientFails(
+            "kxd://localhost/k1",
+            "500 Internal Server Error.*Error loading certs")
 
     def test_missing_key(self):
         self.server.new_key("k1",
-                allowed_clients=[self.client.cert()],
-                allowed_hosts=["localhost"])
+                            allowed_clients=[self.client.cert()],
+                            allowed_hosts=["localhost"])
 
         os.unlink(self.server.path + "/data/k1/key")
         self.assertClientFails("kxd://localhost/k1", "404 Not Found")
 
 
+class Delegation(TestCase):
+    """Tests for CA delegations."""
+    def setUp(self):
+        # For these tests, we don't have a common setup, as each will create
+        # server and clients in a slightly different way.
+        pass
+
+    def prepare(self, server_self_sign=True, client_self_sign=True,
+                ca_sign_server=None, ca_sign_client=None):
+        self.server = ServerConfig(self_sign=server_self_sign)
+        self.client = ClientConfig(self_sign=client_self_sign)
+
+        self.ca = CA()
+        if ca_sign_server is None:
+            ca_sign_server = not server_self_sign
+        if ca_sign_client is None:
+            ca_sign_client = not client_self_sign
+
+        if ca_sign_server:
+            self.ca.sign(self.server.csr_path())
+        if ca_sign_client:
+            self.ca.sign(self.client.csr_path())
+
+        self.launch_server(self.server)
+
+    def test_server_delegated(self):
+        self.prepare(server_self_sign=False)
+
+        self.server.new_key("k1",
+                            allowed_clients=[self.client.cert()],
+                            allowed_hosts=["localhost"])
+
+        # Successful request.
+        key = self.client.call(self.ca.cert_path(), "kxd://localhost/k1")
+        self.assertEquals(key, self.server.keys["k1"])
+
+        # The server is signed by the CA, but the CA is unknown to the client,
+        # so it can't validate it, even if it knows the server directly.
+        self.assertClientFails("kxd://localhost/k1",
+                               "certificate signed by unknown authority",
+                               cert_path=self.server.cert_path())
+
+        # Same as above, but give the wrong CA.
+        ca2 = CA()
+        self.assertClientFails("kxd://localhost/k1",
+                               "certificate signed by unknown authority",
+                               cert_path=ca2.cert_path())
+
+    def test_client_delegated(self):
+        self.prepare(client_self_sign=False)
+
+        # Successful request.
+        self.server.new_key("k1",
+                            allowed_clients=[self.ca.cert()],
+                            allowed_hosts=["localhost"])
+        key = self.client.call(self.server.cert_path(), "kxd://localhost/k1")
+        self.assertEquals(key, self.server.keys["k1"])
+
+        # The CA signing the client is unknown to the server.
+        ca2 = CA()
+        self.server.new_key("k2",
+                            allowed_clients=[ca2.cert()],
+                            allowed_hosts=["localhost"])
+        self.assertClientFails("kxd://localhost/k2",
+                               "403 Forbidden.*No allowed certificate found",
+                               cert_path=self.server.cert_path())
+
+        # The client is signed by the CA, but the CA is unknown to the server,
+        # so it can't validate it, even if it knows the client directly.
+        self.server.new_key("k3",
+                            allowed_clients=[self.client.cert()],
+                            allowed_hosts=["localhost"])
+        self.assertClientFails("kxd://localhost/k3",
+                               "403 Forbidden.*No allowed certificate found",
+                               cert_path=self.server.cert_path())
+
+    def test_both_delegated(self):
+        self.prepare(server_self_sign=False, client_self_sign=False)
+        self.server.new_key("k1",
+                            allowed_clients=[self.ca.cert()],
+                            allowed_hosts=["localhost"])
+
+        key = self.client.call(self.ca.cert_path(), "kxd://localhost/k1")
+        self.assertEquals(key, self.server.keys["k1"])
+
+
 if __name__ == "__main__":
     unittest.main()