author | Maximiliano Curia
<maxy@gnuservers.com.ar> 2014-05-02 12:27:46 UTC |
committer | Maximiliano Curia
<maxy@gnuservers.com.ar> 2014-05-02 12:27:46 UTC |
parent | 11c3a60e3d15d3d3bd86b3a909d5db8f120b9784 |
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()