pax_global_header00006660000000000000000000000064131374107360014517gustar00rootroot0000000000000052 comment=f5d6db51db685db41cd53cbcfff2c4eacfbbeb62 kxd-0.13+git20170730.6182dc8/000077500000000000000000000000001313741073600146315ustar00rootroot00000000000000kxd-0.13+git20170730.6182dc8/.gitignore000066400000000000000000000001301313741073600166130ustar00rootroot00000000000000out/ *.swp # Just in case, we ignore all .pem so noone commits them by accident. *.pem kxd-0.13+git20170730.6182dc8/.travis.yml000066400000000000000000000005421313741073600167430ustar00rootroot00000000000000# Configuration for https://travis-ci.org/ language: go go_import_path: blitiri.com.ar/go/kxd go: - 1.7 # Debian stable relies on this. - 1.8 - tip addons: apt: packages: - python - openssl # Nothing to do for install, the tests will build the binaries anyway. install: true script: - make test kxd-0.13+git20170730.6182dc8/LICENSE000066400000000000000000000022271313741073600156410ustar00rootroot00000000000000kxd 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. kxd-0.13+git20170730.6182dc8/Makefile000066400000000000000000000040111313741073600162650ustar00rootroot00000000000000 GO = go OUTDIR = ./out default: kxd kxc kxd: $(GO) build -o $(OUTDIR)/kxd ./kxd # For the client, because it can be run in a very limited environment without # glibc (like initramfs), we build it using the native go networking so it can # work even when glibc's resolvers are missing. kxc: $(GO) build --tags netgo -a -o $(OUTDIR)/kxc ./kxc fmt: gofmt -w . vet: $(GO) tool vet . test: kxd kxc python tests/run_tests -b tests: test # Prefixes for installing the files. PREFIX=/usr ETCDIR=/etc SYSTEMDDIR=$(shell pkg-config systemd --variable=systemdsystemunitdir) # Install utility, we assume it's GNU/BSD compatible. INSTALL=install install-all: install-kxd install-init.d install-kxc install-initramfs install-kxd: kxd $(INSTALL) -d $(PREFIX)/bin $(INSTALL) -m 0755 out/kxd $(PREFIX)/bin/ $(INSTALL) -m 0755 scripts/create-kxd-config $(PREFIX)/bin/ $(INSTALL) -m 0755 scripts/kxd-add-client-key $(PREFIX)/bin/ install-init.d: install-kxd $(INSTALL) -m 0755 scripts/init.d/kxd $(ETCDIR)/init.d/kxd $(INSTALL) -m 0644 scripts/default/kxd $(ETCDIR)/default/kxd install-systemd: install-kxd $(INSTALL) -m 0644 scripts/default/kxd $(ETCDIR)/default/kxd $(INSTALL) -m 0644 scripts/systemd/kxd.service $(SYSTEMDDIR) install-upstart: install-kxd $(INSTALL) -m 0644 scripts/default/kxd $(ETCDIR)/default/kxd $(INSTALL) -m 0644 scripts/upstart/kxd.conf $(ETCDIR)/init/ install-kxc: kxc $(INSTALL) -m 0755 out/kxc $(PREFIX)/bin/ $(INSTALL) -m 0755 cryptsetup/kxc-cryptsetup $(PREFIX)/bin/ $(INSTALL) -m 0755 scripts/kxc-add-key $(PREFIX)/bin/ install-initramfs: install-kxc $(INSTALL) -d $(PREFIX)/share/initramfs-tools/hooks/ $(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/kxc-premount-net \ $(PREFIX)/share/initramfs-tools/scripts/init-premount/ .PHONY: kxd kxc .PHONY: install-all install-kxd install-init.d install-kxc install-initramfs .PHONY: test tests kxd-0.13+git20170730.6182dc8/README000066400000000000000000000060761313741073600155220ustar00rootroot00000000000000 kxd - Key exchange daemon ========================= kxd is a key exchange daemon, which serves blobs of data (keys) over https. It can be used to get keys remotely instead of using local storage. The main use case is to get keys to open dm-crypt devices automatically, without having to store them on the local machine. Quick start ----------- The document at doc/quick_start.rst contains a step by step guide of a typical server and client setups. Server configuration -------------------- 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. Client configuration -------------------- The basic command line client (kxc) will take the client key and certificate, the expected server certificate, and a URL to the server (like kxd://server/host1/key1), and it will print on standard output the returned key (the contents of the corresponding key file). There are scripts to tie this with cryptsetup's infrastructure to make the opening of encrypted devices automatic; see cryptsetup/ for the details. Security -------- All traffic between the server and the clients goes over SSL, using the provided server certificate. The clients are authenticated and authorized based on their SSL client certificates matching the ones associated with the key in the server configuration, not using a root of trust (for now). Likewise, the clients will authenticate the server based on a certificate 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 existence of keys, it makes troubleshooting much easier. The server itself makes no effort to protect the data internally; for example, 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.7. 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 ---------------- Please report bugs to albertito@blitiri.com.ar. The latest version can be found at http://blitiri.com.ar/p/kxd/. kxd-0.13+git20170730.6182dc8/cryptsetup/000077500000000000000000000000001313741073600170535ustar00rootroot00000000000000kxd-0.13+git20170730.6182dc8/cryptsetup/README000066400000000000000000000030301313741073600177270ustar00rootroot00000000000000 These are scripts for integration with cryptsetup (and initramfs). They are tested on a Debian install, so they may not be vendor-neutral although they should work with an standard initramfs-tools and cryptsetup environment. For an example of how to use it, see doc/quick_start.rst. What if something goes wrong ============================ If the key fetch fails or is incorrect it will be retried, and after 3 attempts, it will give up and return an initramfs prompt, which you can use to manually recover. In modern Debian installs, you can just unlock the device (for example using "cryptsetup luksOpen /dev/sdXX sdXX_crypt"), and then exit. The init scripts will recognise they can now proceed with the usual boot process. How does it work ================ The first part of the work happens when update-initramfs runs: - The initramfs hook script copies the kxc binary and all the configuration from /etc/kxc. - The standard cryptsetup hook will copy kxc-cryptsetup if it sees it appearing in /etc/crypttab. - The premount-net script will be copied. Then, when the machine boots: - Before attempting to mount root, the premount-net script will run, configure networking, and create a minimal /etc/resolv.conf. - When attempting to mount root, assuming it is encrypted and properly configured, the cryptsetup scripts will invoke the keyfile, kxc-cryptsetup. - kxc-cryptsetup will run the kxc client with the right configuration taken from /etc/kxc. - The device is unlocked with the key, and boot continues as usual. kxd-0.13+git20170730.6182dc8/cryptsetup/initramfs-hooks/000077500000000000000000000000001313741073600221705ustar00rootroot00000000000000kxd-0.13+git20170730.6182dc8/cryptsetup/initramfs-hooks/kxc000077500000000000000000000007131313741073600227040ustar00rootroot00000000000000#!/bin/sh set -e PREREQ="cryptroot" prereqs() { echo "$PREREQ" } case $1 in prereqs) prereqs exit 0 ;; esac . /usr/share/initramfs-tools/hook-functions # Install binaries into initramfs. # Note we don't need to install kxc-cryptsetup, as the cryptroot hook will do # it for us if it sees it being used as a keyscript. copy_exec /usr/bin/kxc /bin # Install the configuration into initramfs cp -a /etc/kxc/ ${DESTDIR}/etc kxd-0.13+git20170730.6182dc8/cryptsetup/initramfs-scripts/000077500000000000000000000000001313741073600225345ustar00rootroot00000000000000kxd-0.13+git20170730.6182dc8/cryptsetup/initramfs-scripts/kxc-premount-net000077500000000000000000000005651313741073600257100ustar00rootroot00000000000000#!/bin/sh # Configure networking before mounting. PREREQ="" prereqs() { echo "$PREREQ" } case $1 in # get pre-requisites prereqs) prereqs exit 0 ;; esac . /scripts/functions configure_networking # Configure a basic resolv.conf based on our networking. if ! [ -s /etc/resolv.conf ]; then echo "nameserver $IPV4DNS0" >> /etc/resolv.conf fi kxd-0.13+git20170730.6182dc8/cryptsetup/kxc-cryptsetup000077500000000000000000000014001313741073600220010ustar00rootroot00000000000000#!/bin/sh # Script to use as a crypttab keyscript, to automatically get keys with kxc. # It will use the configuration from /etc/kxc/. # # The only argument is the base name of the configuration. CONFIG_BASE="/etc/kxc" CLIENT_CERT="${CONFIG_BASE}/cert.pem" CLIENT_KEY="${CONFIG_BASE}/key.pem" SERVER_CERT="${CONFIG_BASE}/${1}.server_cert.pem" SERVER_URL=$(cat "${CONFIG_BASE}/${1}.url") # Find the binary. We search because it can be in one place in the initramfs, # and in another in the normal distribution, and we want to support both # easily. for KXC in /bin/kxc /sbin/kxc /usr/bin/kxc /usr/sbin/kxc; do if [ -x $KXC ]; then break; fi done exec $KXC \ --client_cert=$CLIENT_CERT \ --client_key=$CLIENT_KEY \ --server_cert=$SERVER_CERT \ $SERVER_URL kxd-0.13+git20170730.6182dc8/doc/000077500000000000000000000000001313741073600153765ustar00rootroot00000000000000kxd-0.13+git20170730.6182dc8/doc/man/000077500000000000000000000000001313741073600161515ustar00rootroot00000000000000kxd-0.13+git20170730.6182dc8/doc/man/.gitignore000066400000000000000000000000041313741073600201330ustar00rootroot00000000000000*.1 kxd-0.13+git20170730.6182dc8/doc/man/Makefile000066400000000000000000000002071313741073600176100ustar00rootroot00000000000000 default: manpages %.1: %.rst rst2man < $^ > $@ manpages: kxd.1 kxc.1 kxc-cryptsetup.1 clean: rm -f kxd.1 kxc.1 kxc-cryptsetup.1 kxd-0.13+git20170730.6182dc8/doc/man/kxc-cryptsetup.rst000066400000000000000000000024531313741073600217140ustar00rootroot00000000000000 ================ kxc-cryptsetup ================ ------------------------ Cryptsetup helper to kxc ------------------------ :Author: Alberto Bertogli :Manual section: 1 SYNOPSIS ======== kxc-cryptsetup DESCRIPTION =========== ``kxc(1)`` is a client for kxd, a key exchange daemon. kxc-cryptsetup is a convenience wrapper for invoking kxc while taking the options from the files in ``/etc/kxc/``. It can be used as a cryptsetup keyscript, to automatically get keys to open encrypted devices with kxc. OPTIONS ======= Its only command-line argument is a descriptive name, which will be used to find the configuration files. FILES ===== For a given *NAME* that is passed as the only command-line argument, the following files are needed: /etc/kxc/NAME.key.pem Private key to use. /etc/kxc/NAME.cert.pem Certificate to use. Must match the given key. /etc/kxc/NAME.server_cert.pem Server certificate, used to validate the server. /etc/kxc/NAME.url Contains the URL to the key; usually in the form of ``kxd://server/name``. 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. kxd-0.13+git20170730.6182dc8/doc/man/kxc.rst000066400000000000000000000023131313741073600174670ustar00rootroot00000000000000 ===== kxc ===== ------------------- Key exchange client ------------------- :Author: Alberto Bertogli :Manual section: 1 SYNOPSIS ======== kxc --client_cert= --client_key= --server_cert= DESCRIPTION =========== kxc is a client for kxd, a key exchange daemon. It will take a client key and certificate, the expected server certificate, and a URL to the server (like ``kxd://server/host1/key1``), and it will print on standard output the returned key (the contents of the corresponding key file on the server). There are scripts to tie this with cryptsetup's infrastructure to make the opening of encrypted devices automatic; see ``kxc-cryptsetup(1)`` for the details. OPTIONS ======= --client_key= File containing the client private key (in PAM format). --client_cert= File containing the client certificate that corresponds to the given key (in PAM format). --server_cert= File containing valid server certificate(s). 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. kxd-0.13+git20170730.6182dc8/doc/man/kxd.rst000066400000000000000000000052261313741073600174760ustar00rootroot00000000000000 ===== kxd ===== ------------------- Key exchange daemon ------------------- :Author: Alberto Bertogli :Manual section: 1 SYNOPSIS ======== kxd [--key=] [--cert=] [--data_dir=] [--email_from=] [--ip_addr=] [--logfile=] [--port=] [--smtp_addr=] DESCRIPTION =========== kxd is a key exchange daemon, which serves blobs of data (keys) over https. It can be used to get keys remotely instead of using local storage. The main use case is to get keys to open dm-crypt devices automatically, without having to store them on the local machine. SETUP ===== The server configuration is stored in a root directory (``/etc/kxd/data/`` by default), 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. OPTIONS ======= --key= Private key to use. Defaults to /etc/kxd/key.pem. --cert= Certificate to use; must match the given key. Defaults to /etc/kxd/cert.pem. --data_dir= Data directory, where the key and configuration live (see the SETUP section above). Defaults to /etc/kxd/data. --email_from= Email address to send email from. --ip_addr= IP address to listen on. Defaults to 0.0.0.0, which means all. --logfile= File to write logs to, use '-' for stdout. By default, the daemon will log to syslog. --port= Port to listen on. The default port is 19840. --smtp_addr= Address of the SMTP server to use to send emails. If none is given, then emails will not be sent. FILES ===== /etc/kxd/key.pem Private key to use for SSL. /etc/kxd/cert.pem Certificate to use for SSL. Must match the given private key. /etc/kxd/data/ Directory where the keys and their configuration are stored. 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. kxd-0.13+git20170730.6182dc8/doc/quick_start.rst000066400000000000000000000072601313741073600204660ustar00rootroot00000000000000 =================================== Key Exchange Daemon - Quick start =================================== 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). 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 packages, or directly from source. Then, run ``create-kxd-config``, which will create the configuration directories, and generate a self-signed_ key/cert pair for the server (valid for 10 years). Everything is in ``/etc/kxd/``. Initial client setup ==================== 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 directories, generate the client key/cert pair (valid for 10 years), and also create an entry for an ``client/sda2`` key to be fetched from the server. Everything is in ``/etc/kxc/``. Finally, copy the server public certificate over, using ``scp server:/etc/kxd/cert.pem /etc/kxc/sda2.server_cert.pem`` (or something equivalent). Adding the key to the server ============================ On the server, run ``kxd-add-client-key client sda2`` to generate the basic configuration for that client's key, including the key itself (generated randomly). Then, copy the client public certificate over, using ``scp client:/etc/kxc/cert.pem /etc/kxd/data/client/sda2/allowed_clients`` (or something equivalent). That allows the client to fetch the key. Updating the drive's key ======================== On the client, run ``kxc-cryptsetup sda2 | wc -c`` to double-check that the output length is as expected (you could also compare it by running sha256 or something equivalent). Assuming that goes well, all you need is to add that key to your drives' key ring so it can be decrypted with it:: # Note we copy to /dev/shm which should not be written to disk. kxc-cryptsetup sda2 > /dev/shm/key cryptsetup luksAddKey /dev/sda2 /dev/shm/key rm /dev/shm/key Note this *adds* a new key, but your existing ones are still valid. Always have more than one key, so if something goes wrong with kxd, you can still unlock the drive manually. Configuring crypttab ==================== In order to get kxc to be run automatically to fetch the key, we need to edit ``/etc/crypttab`` and tell it to use a keyscript:: sda2_crypt UUID=blah-blah-blah sda2 luks,keyscript=/usr/bin/kxc-cryptsetup ^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Note the ``sda2`` field corresponds to the name we've been passing around in previous sections. The ``keyscript=/usr/bin/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:: cryptdisks_stop sda2_crypt cryptdisks_start sda2_crypt The second command should issue a request to your server to get the key. Consider running ``update-initramfs -u`` 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 kxd-0.13+git20170730.6182dc8/kxc/000077500000000000000000000000001313741073600154165ustar00rootroot00000000000000kxd-0.13+git20170730.6182dc8/kxc/kxc.go000066400000000000000000000060261313741073600165360ustar00rootroot00000000000000// kxc is a client for the key exchange daemon kxd. // // It connects to the given server using the provided certificate, // and authorizes the server against the given server certificate. // // If everything goes well, it prints the obtained key to standard output. package main import ( "crypto/tls" "crypto/x509" "flag" "fmt" "io/ioutil" "log" "net/http" "net/url" "strings" ) const defaultPort = 19840 var serverCert = flag.String( "server_cert", "", "File containing valid server certificate(s)") var clientCert = flag.String( "client_cert", "", "File containing the client certificate") var clientKey = flag.String( "client_key", "", "File containing the client private key") func loadServerCerts() (*x509.CertPool, error) { pemData, err := ioutil.ReadFile(*serverCert) if err != nil { return nil, err } pool := x509.NewCertPool() if !pool.AppendCertsFromPEM(pemData) { return nil, fmt.Errorf("Error appending certificates") } return pool, nil } // Check if the given network address has a port. func hasPort(s string) bool { // Consider the IPv6 case (where the host part contains ':') by // checking if the last ':' comes after the ']' which closes the host. return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") } func extractURL(rawurl string) (*url.URL, error) { serverURL, err := url.Parse(rawurl) if err != nil { return nil, err } // Make sure we're using https. switch serverURL.Scheme { case "https": // Nothing to do here. case "http", "kxd": serverURL.Scheme = "https" default: return nil, fmt.Errorf("Unsupported URL schema (try kxd://)") } // The path must begin with /v1/, although we hide that from the user // for forward compatibility. if !strings.HasPrefix(serverURL.Path, "/v1/") { serverURL.Path = "/v1" + serverURL.Path } // Add the default port, if none was given. if !hasPort(serverURL.Host) { serverURL.Host += fmt.Sprintf(":%d", defaultPort) } return serverURL, nil } func makeTLSConf() *tls.Config { var err error tlsConf := &tls.Config{} tlsConf.Certificates = make([]tls.Certificate, 1) tlsConf.Certificates[0], err = tls.LoadX509KeyPair( *clientCert, *clientKey) 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{ TLSClientConfig: makeTLSConf(), } client := &http.Client{ Transport: tr, } serverURL, err := extractURL(flag.Arg(0)) if err != nil { log.Fatalf("Failed to extract the URL: %s", err) } resp, err := client.Get(serverURL.String()) if err != nil { log.Fatalf("Failed to get key: %s", err) } content, err := ioutil.ReadAll(resp.Body) resp.Body.Close() if err != nil { log.Fatalf("Error reading key body: %s", err) } if resp.StatusCode != 200 { log.Fatalf("HTTP error %q getting key: %s", resp.Status, content) } fmt.Printf("%s", content) } kxd-0.13+git20170730.6182dc8/kxd/000077500000000000000000000000001313741073600154175ustar00rootroot00000000000000kxd-0.13+git20170730.6182dc8/kxd/email.go000066400000000000000000000044731313741073600170450ustar00rootroot00000000000000package main import ( "bytes" "crypto/x509" "crypto/x509/pkix" "fmt" "net/smtp" "strings" "text/template" "time" ) // EmailBody represents the body of an email message to sent. type EmailBody struct { From string To string Key string Time time.Time TimeString string Req *Request Cert *x509.Certificate Chains [][]*x509.Certificate } const emailTmplBody = (`Date: {{.TimeString}} From: Key Exchange Daemon <{{.From}}> To: {{.To}} Subject: Access to key {{.Key}} Key: {{.Key}} Accessed by: {{.Req.RemoteAddr}} On: {{.TimeString}} Client certificate: Signature: {{printf "%.16s" (printf "%x" .Cert.Signature)}}... Subject: {{NameToString .Cert.Subject}} Authorizing chains: {{range .Chains}} {{ChainToString .}} {{end}} `) var emailTmpl = template.New("email") func init() { emailTmpl.Funcs(map[string]interface{}{ "NameToString": NameToString, "ChainToString": ChainToString, }) template.Must(emailTmpl.Parse(emailTmplBody)) } // NameToString converts a pkix.Name from a certificate to a human-friendly // string. func NameToString(name pkix.Name) string { s := make([]string, 0) for _, c := range name.Country { s = append(s, fmt.Sprintf("C=%s", c)) } 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)) } return strings.Join(s, " ") } // SendMail sends an email notifying of an access to the given key. func SendMail(kc *KeyConfig, req *Request, chains [][]*x509.Certificate) error { if *smtpAddr == "" { req.Printf("Skipping notifications") return nil } emailTo, err := kc.EmailTo() if err != nil { return err } if emailTo == nil { return nil } keyPath, err := req.KeyPath() if err != nil { return err } now := time.Now() body := EmailBody{ From: *emailFrom, To: strings.Join(emailTo, ", "), Key: keyPath, Time: now, TimeString: now.Format(time.RFC1123Z), Req: req, Cert: chains[0][0], Chains: chains, } msg := new(bytes.Buffer) err = emailTmpl.Execute(msg, body) if err != nil { return err } return smtp.SendMail(*smtpAddr, nil, *emailFrom, emailTo, msg.Bytes()) } kxd-0.13+git20170730.6182dc8/kxd/hook.go000066400000000000000000000036321313741073600167120ustar00rootroot00000000000000package main import ( "context" "crypto/x509" "fmt" "os" "os/exec" "strings" "time" ) // RunHook runs the hook, returns an error if the request is not allowed (or // there were problems with the hook; we don't make the distinction for now). // // Note that if the hook flag is not set, or points to a non-existing path, // then we allow the request. func RunHook(kc *KeyConfig, req *Request, chains [][]*x509.Certificate) error { if *hookPath == "" { return nil } if _, err := os.Stat(*hookPath); os.IsNotExist(err) { req.Printf("Hook not present, skipping") return nil } ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Minute)) defer cancel() cmd := exec.CommandContext(ctx, *hookPath) // Run the hook from the data directory. cmd.Dir = *dataDir // Prepare the environment, copying some common variables so the hook has // someting reasonable, and then setting the specific ones for this case. for _, v := range strings.Fields("USER PWD SHELL PATH") { cmd.Env = append(cmd.Env, v+"="+os.Getenv(v)) } keyPath, err := req.KeyPath() if err != nil { return err } cmd.Env = append(cmd.Env, "KEY_PATH="+keyPath) cmd.Env = append(cmd.Env, "REMOTE_ADDR="+req.RemoteAddr) cmd.Env = append(cmd.Env, "MAIL_FROM="+*emailFrom) if emailTo, _ := kc.EmailTo(); emailTo != nil { cmd.Env = append(cmd.Env, "EMAIL_TO="+strings.Join(emailTo, " ")) } clientCert := chains[0][0] cmd.Env = append(cmd.Env, fmt.Sprintf("CLIENT_CERT_SIGNATURE=%x", clientCert.Signature)) cmd.Env = append(cmd.Env, "CLIENT_CERT_SUBJECT="+NameToString(clientCert.Subject)) for i, chain := range chains { cmd.Env = append(cmd.Env, fmt.Sprintf("CHAIN_%d=%s", i, ChainToString(chain))) } _, err = cmd.Output() if err != nil { if ee, ok := err.(*exec.ExitError); ok { err = fmt.Errorf("exited with error: %v -- stderr: %q", ee.String(), ee.Stderr) } return err } return nil } kxd-0.13+git20170730.6182dc8/kxd/key_config.go000066400000000000000000000105501313741073600200640ustar00rootroot00000000000000package main import ( "crypto/x509" "fmt" "io/ioutil" "net" "os" "strings" ) func fileStat(path string) (os.FileInfo, error) { fd, err := os.Open(path) if err != nil { return nil, err } return fd.Stat() } func isDir(path string) (bool, error) { fi, err := fileStat(path) if err != nil { return false, err } return fi.IsDir(), nil } func isRegular(path string) (bool, error) { fi, err := fileStat(path) if err != nil { return false, err } return fi.Mode().IsRegular(), nil } // KeyConfig holds the configuration data for a single key. type KeyConfig struct { // Path to the configuration directory. ConfigPath string // Paths to the files themselves. keyPath string allowedClientsPath string allowedHostsPath string emailToPath string // Allowed certificates. allowedClientCerts *x509.CertPool // Allowed hosts. allowedHosts []string } // NewKeyConfig makes a new KeyConfig based on the given path. Note that there // is no check about the key existing or being valid. func NewKeyConfig(configPath string) *KeyConfig { return &KeyConfig{ ConfigPath: configPath, keyPath: configPath + "/key", allowedClientsPath: configPath + "/allowed_clients", allowedHostsPath: configPath + "/allowed_hosts", emailToPath: configPath + "/email_to", allowedClientCerts: x509.NewCertPool(), } } // Exists checks if this key exists. func (kc *KeyConfig) Exists() (bool, error) { isDir, err := isDir(kc.ConfigPath) if os.IsNotExist(err) { return false, nil } else if err != nil { return false, err } if !isDir { return false, nil } isRegular, err := isRegular(kc.keyPath) if os.IsNotExist(err) { return false, nil } else if err != nil { return false, err } return isRegular, nil } // LoadClientCerts loads the client certificates allowed for this key. func (kc *KeyConfig) LoadClientCerts() error { rawContents, err := ioutil.ReadFile(kc.allowedClientsPath) if os.IsNotExist(err) { return nil } else if err != nil { return err } if !kc.allowedClientCerts.AppendCertsFromPEM(rawContents) { return fmt.Errorf("Error parsing client certificate file") } return nil } // LoadAllowedHosts loads the hosts allowed for this key. func (kc *KeyConfig) LoadAllowedHosts() error { contents, err := ioutil.ReadFile(kc.allowedHostsPath) if os.IsNotExist(err) { return nil } else if err != nil { return err } // If the file is there, we want our array to exist, even if it's // empty, to avoid authorizing everyone on an empty file (which means // authorize noone). kc.allowedHosts = make([]string, 1) for _, line := range strings.Split(string(contents), "\n") { line = strings.TrimSpace(line) if line == "" { continue } if net.ParseIP(line) != nil { kc.allowedHosts = append(kc.allowedHosts, line) } else { names, err := net.LookupHost(line) if err != nil { continue } kc.allowedHosts = append(kc.allowedHosts, names...) } } return nil } // IsAnyCertAllowed checks if any of the given certificates is allowed to // access this key. If so, it returns the chain for each of them. func (kc *KeyConfig) IsAnyCertAllowed( certs []*x509.Certificate) [][]*x509.Certificate { opts := x509.VerifyOptions{ Roots: kc.allowedClientCerts, } for _, cert := range certs { chains, err := cert.Verify(opts) if err == nil && len(chains) > 0 { return chains } } return nil } // IsHostAllowed checks if the given host is allowed to access this key. func (kc *KeyConfig) IsHostAllowed(addr string) error { if kc.allowedHosts == nil { return nil } host, _, err := net.SplitHostPort(addr) if err != nil { return err } for _, allowedHost := range kc.allowedHosts { if allowedHost == host { return nil } } return fmt.Errorf("Host %q not allowed", host) } // Key returns the private key. func (kc *KeyConfig) Key() (key []byte, err error) { return ioutil.ReadFile(kc.keyPath) } // EmailTo returns the list of addresses to email when this key is accessed. func (kc *KeyConfig) EmailTo() ([]string, error) { contents, err := ioutil.ReadFile(kc.emailToPath) if os.IsNotExist(err) { return nil, nil } if err != nil { return nil, err } var emails []string for _, line := range strings.Split(string(contents), "\n") { email := strings.TrimSpace(line) if !strings.Contains(email, "@") { continue } emails = append(emails, email) } return emails, nil } kxd-0.13+git20170730.6182dc8/kxd/kxd.go000066400000000000000000000146561313741073600165500ustar00rootroot00000000000000// kxd is a key exchange daemon. // // It serves blobs of data (keys) over https, authenticating and authorizing // the clients using SSL certificates, and notifying upon key accesses. // // It can be used to get keys remotely instead of using local storage. // The main use case is to get keys to open dm-crypt devices automatically, // without having to store them on the machine. package main import ( "crypto/tls" "crypto/x509" "flag" "fmt" "io" "log" "log/syslog" "net/http" "os" "path" "strings" ) var port = flag.Int( "port", 19840, "Port to listen on") var ipAddr = flag.String( "ip_addr", "", "IP address to listen on") var dataDir = flag.String( "data_dir", "/etc/kxd/data", "Data directory") var certFile = flag.String( "cert", "/etc/kxd/cert.pem", "Certificate") var keyFile = flag.String( "key", "/etc/kxd/key.pem", "Private key") var smtpAddr = flag.String( "smtp_addr", "", "Address of the SMTP server to use to send emails") var emailFrom = flag.String( "email_from", "", "Email address to send email from") var logFile = flag.String( "logfile", "", "File to write logs to, use '-' for stdout") var hookPath = flag.String( "hook", "/etc/kxd/hook", "Hook to run before authorizing keys (skipped if it doesn't exist)") // Logger we will use to log entries. var logging *log.Logger // Request is our wrap around http.Request, so we can augment it with custom // methods. type Request struct { *http.Request } // Printf is a wrapper for fmt.Printf+logging.Output, which prefixes a string // identifying this request. func (req *Request) Printf(format string, a ...interface{}) { msg := fmt.Sprintf(format, a...) msg = fmt.Sprintf("%s %s %s", req.RemoteAddr, req.URL.Path, msg) logging.Output(2, msg) } // KeyPath returns the path to the requested key, extracting it from the URL. func (req *Request) KeyPath() (string, error) { s := strings.Split(req.URL.Path, "/") // We expect the path to be "/v1/path/to/key". if len(s) < 2 || !(s[0] == "" || s[1] == "v1") { return "", fmt.Errorf("Invalid path %q", s) } return strings.Join(s[2:], "/"), nil } func certToString(cert *x509.Certificate) string { return fmt.Sprintf( "(0x%.8s ou:%s)", fmt.Sprintf("%x", cert.Signature), cert.Subject.OrganizationalUnit) } // ChainToString makes a human-readable string out of the given certificate // chain. 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. func HandlerV1(w http.ResponseWriter, httpreq *http.Request) { req := Request{httpreq} if len(req.TLS.PeerCertificates) <= 0 { req.Printf("Rejecting request without certificate") http.Error(w, "Client certificate not provided", http.StatusNotAcceptable) return } keyPath, err := req.KeyPath() if err != nil { req.Printf("Rejecting request with invalid key path: %s", err) http.Error(w, "Invalid key path", http.StatusNotAcceptable) return } // Be extra paranoid and reject keys with "..", even if they're valid // (e.g. "/v1/x..y" is valid, but will get rejected anyway). if strings.Contains(keyPath, "..") { req.Printf("Rejecting because requested key %q contained '..'", keyPath) req.Printf("Full request: %+v", *req.Request) http.Error(w, "Invalid key path", http.StatusNotAcceptable) return } realKeyPath := path.Clean(*dataDir + "/" + keyPath) keyConf := NewKeyConfig(realKeyPath) exists, err := keyConf.Exists() if err != nil { req.Printf("Error checking key path %q: %s", keyPath, err) http.Error(w, "Error checking key", http.StatusInternalServerError) return } if !exists { req.Printf("Unknown key path %q", keyPath) http.Error(w, "Unknown key", http.StatusNotFound) return } if err = keyConf.LoadClientCerts(); err != nil { req.Printf("Error loading certs: %s", err) http.Error(w, "Error loading certs", http.StatusInternalServerError) return } if err = keyConf.LoadAllowedHosts(); err != nil { req.Printf("Error loading allowed hosts: %s", err) http.Error(w, "Error loading allowed hosts", http.StatusInternalServerError) return } err = keyConf.IsHostAllowed(req.RemoteAddr) if err != nil { req.Printf("Host not allowed: %s", err) http.Error(w, "Host not allowed", http.StatusForbidden) return } validChains := keyConf.IsAnyCertAllowed(req.TLS.PeerCertificates) if validChains == nil { req.Printf("No allowed certificate found") http.Error(w, "No allowed certificate found", http.StatusForbidden) return } keyData, err := keyConf.Key() if err != nil { req.Printf("Error getting key data: %s", err) http.Error(w, "Error getting key data", http.StatusInternalServerError) return } err = RunHook(keyConf, &req, validChains) if err != nil { req.Printf("Prevented by hook: %s", err) http.Error(w, "Prevented by hook", http.StatusForbidden) return } req.Printf("Allowing request to %s", certToString(validChains[0][0])) err = SendMail(keyConf, &req, validChains) if err != nil { req.Printf("Error sending notification: %s", err) http.Error(w, "Error sending notification", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/octet-stream") w.Write(keyData) } func initLog() { var err error var logfd io.Writer if *logFile == "-" { logfd = os.Stdout } else if *logFile != "" { logfd, err = os.OpenFile(*logFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) if err != nil { log.Fatalf("Error opening log file %s: %s", *logFile, err) } } else { logfd, err = syslog.New( syslog.LOG_INFO|syslog.LOG_DAEMON, "kxd") if err != nil { log.Fatalf("Error opening syslog: %s", err) } } logging = log.New(logfd, "", log.Ldate|log.Ltime|log.Lmicroseconds|log.Lshortfile) } func main() { flag.Parse() initLog() if *smtpAddr == "" { logging.Print( "WARNING: No emails will be sent, use --smtp_addr") } if *emailFrom == "" { // Try to get a sane default if not provided, using // kxd@. *emailFrom = fmt.Sprintf("kxd@%s", strings.Split(*smtpAddr, ":")[0]) } listenAddr := fmt.Sprintf("%s:%d", *ipAddr, *port) tlsConfig := tls.Config{ ClientAuth: tls.RequireAnyClientCert, } server := http.Server{ Addr: listenAddr, TLSConfig: &tlsConfig, ErrorLog: logging, } http.HandleFunc("/v1/", HandlerV1) logging.Printf("Listening on %s", listenAddr) err := server.ListenAndServeTLS(*certFile, *keyFile) if err != nil { logging.Fatal(err) } } kxd-0.13+git20170730.6182dc8/scripts/000077500000000000000000000000001313741073600163205ustar00rootroot00000000000000kxd-0.13+git20170730.6182dc8/scripts/create-kxd-config000077500000000000000000000017561313741073600215510ustar00rootroot00000000000000#!/bin/bash # # Create a basic but functional kxd configuration. # # This script creates the /etc/kxd directory, and generates a certificate for # the server to use. # # It should be run under the same user as kxd itself. set -e # Create the base configuration directory. echo "Creating directories (/etc/kxd/)" mkdir -p /etc/kxd/ # And the data directory where the keys are stored. mkdir -p /etc/kxd/data # Create a private key for the server. if ! [ -e /etc/kxd/key.pem ]; then echo "Generating private key (/etc/kxd/key.pem)" openssl genrsa -out /etc/kxd/key.pem 2048 chmod 400 /etc/kxd/key.pem else echo "Private key already exists (/etc/kxd/key.pem)" fi # And a self-signed certificate. if ! [ -e /etc/kxd/cert.pem ]; then echo "Generating certificate (/etc/kxd/cert.pem)" openssl req -new -x509 -batch -days 3650 \ -subj "/commonName=*/organizationalUnitName=kxd@$HOSTNAME/" \ -key /etc/kxd/key.pem -out /etc/kxd/cert.pem else echo "Certificate already exists (/etc/kxd/cert.pem)" fi kxd-0.13+git20170730.6182dc8/scripts/default/000077500000000000000000000000001313741073600177445ustar00rootroot00000000000000kxd-0.13+git20170730.6182dc8/scripts/default/kxd000066400000000000000000000004311313741073600204530ustar00rootroot00000000000000# Options for kxd. # Set this if you don't want the daemon to be started automatically. # Note this is only useful for sysv-like init; systemd will ignore it (use # "sysctl enable/disable" instead). #DISABLE=1 # Set kxd options here. # OPTS="--smtp_addr example.org:25" OPTS="" kxd-0.13+git20170730.6182dc8/scripts/init.d/000077500000000000000000000000001313741073600175055ustar00rootroot00000000000000kxd-0.13+git20170730.6182dc8/scripts/init.d/kxd000077500000000000000000000026371313741073600202310ustar00rootroot00000000000000#! /bin/sh ### BEGIN INIT INFO # Provides: kxd # Required-Start: $remote_fs $syslog # Required-Stop: $remote_fs $syslog # Default-Start: 2 3 4 5 # Default-Stop: # Short-Description: key exchange daemon # Description: kxd is a program that serves keys to authorized clients. ### END INIT INFO DAEMON=/usr/bin/kxd DEFAULTS_FILE=/etc/default/kxd # These variables can be overriden in the defaults file. DISABLE= OPTS='' PID_FILE=/var/run/kxd.pid test -x $DAEMON || exit 0 . /lib/lsb/init-functions if [ -s $DEFAULTS_FILE ]; then . $DEFAULTS_FILE fi case "$1" in start) if [ "$DISABLE" != "" ]; then log_warning_msg "kxd not enabled in $DEFAULTS_FILE" exit 0 fi log_daemon_msg "Starting kxd" start-stop-daemon --start --quiet --background \ --pidfile $PID_FILE --make-pidfile \ --exec $DAEMON -- $OPTS case "$?" in 0) log_progress_msg "kxd" log_end_msg 0 exit 0 ;; 1) log_warning_msg "already running" exit 0 ;; *) log_failure_msg "failed to start daemon" exit 1 ;; esac ;; stop) log_daemon_msg "Stopping kxd daemon" "kxd" start-stop-daemon --stop --quiet --oknodo --pidfile $PID_FILE log_end_msg $? rm -f $PID_FILE ;; restart) set +e $0 stop sleep 2 $0 start ;; status) status_of_proc -p $PID_FILE "$DAEMON" kxd exit $? ;; *) echo "Usage: /etc/init.d/kxd {start|stop|restart|status}" exit 1 esac exit 0 kxd-0.13+git20170730.6182dc8/scripts/kxc-add-key000077500000000000000000000027551313741073600203600ustar00rootroot00000000000000#!/bin/bash # # Add a new key to kxc's configuration (initializing it if necessary). # # If /etc/kxc is missing, this script creates it, as well as the required # client certificates. # # Then, it adds configuration for fetching a given key. set -e SERVER="$1" KEYNAME="$2" if [ "$SERVER" = "" ] || [ "$KEYNAME" = "" ]; then echo " Usage: kxc-add-key This command adds a new key to kxc's configuration, initializing it if necessary. " exit 1 fi # Create the base configuration directory. echo "Creating directories (/etc/kxc/)" mkdir -p /etc/kxc/ # Create a private key for the client. if ! [ -e /etc/kxc/key.pem ]; then echo "Generating private key (/etc/kxc/key.pem)" openssl genrsa -out /etc/kxc/key.pem 2048 chmod 400 /etc/kxc/key.pem else echo "Private key already exists (/etc/kxc/key.pem)" fi # And a self-signed certificate. if ! [ -e /etc/kxc/cert.pem ]; then echo "Generating certificate (/etc/kxc/cert.pem)" openssl req -new -x509 -batch -days 3650 \ -subj "/commonName=*/organizationalUnitName=kxc@$HOSTNAME/" \ -key /etc/kxc/key.pem -out /etc/kxc/cert.pem else echo "Certificate already exists (/etc/kxc/cert.pem)" fi echo "Setting URL to kxd://$SERVER/$HOSTNAME/$KEYNAME" echo "kxd://$SERVER/$HOSTNAME/$KEYNAME" > "/etc/kxc/${KEYNAME}.url" echo echo echo "YOU need to copy the server certificate to" echo "/etc/kxc/${KEYNAME}.server_cert.pem. For example, using:" echo echo " $ scp $SERVER:/etc/kxd/cert.pem /etc/kxc/${KEYNAME}.server_cert.pem" echo kxd-0.13+git20170730.6182dc8/scripts/kxd-add-client-key000077500000000000000000000016421313741073600216270ustar00rootroot00000000000000#!/bin/bash set -e CLIENT="$1" KEYNAME="$2" if [ "$CLIENT" = "" ] || [ "$KEYNAME" = "" ]; then echo " Usage: kxd-add-client-key This command is a helper for adding a new key to kxd's configuration. It takes the hostname of the client and the key name, and puts the corresponding configuration (including a randomly generated key) in /etc/kxd/data///. " exit 1 fi CONFIGPATH="/etc/kxd/data/$CLIENT/$KEYNAME" echo "Creating directory ($CONFIGPATH)" mkdir -p "$CONFIGPATH" echo "Generating random key from /dev/urandom ($CONFIGPATH/key)" dd if=/dev/urandom of="$CONFIGPATH/key" bs=1k count=2 echo echo "Allowing host $CLIENT" echo "$CLIENT" >> "$CONFIGPATH/allowed_hosts" echo echo echo "YOU need to copy the client certificate to" echo "$CONFIGPATH/allowed_clients. For example, using:" echo echo " $ scp $CLIENT:/etc/kxc/cert.pem $CONFIGPATH/allowed_clients" echo kxd-0.13+git20170730.6182dc8/scripts/systemd/000077500000000000000000000000001313741073600200105ustar00rootroot00000000000000kxd-0.13+git20170730.6182dc8/scripts/systemd/kxd.service000066400000000000000000000002541313741073600221610ustar00rootroot00000000000000[Unit] Description = Key exchange daemon [Service] EnvironmentFile = /etc/default/kxd ExecStart = /usr/bin/kxd $OPTS Type = simple [Install] WantedBy = multi-user.target kxd-0.13+git20170730.6182dc8/scripts/upstart/000077500000000000000000000000001313741073600200225ustar00rootroot00000000000000kxd-0.13+git20170730.6182dc8/scripts/upstart/kxd.conf000066400000000000000000000003741313741073600214630ustar00rootroot00000000000000description "kxd - Key exchange daemon" start on filesystem stop on runlevel [016] respawn pre-start exec test -x /usr/bin/kxd || { stop; exit 0; } script test ! -r /etc/default/kxd || . /etc/default/kxd exec /usr/bin/kxd $OPTS end script kxd-0.13+git20170730.6182dc8/tests/000077500000000000000000000000001313741073600157735ustar00rootroot00000000000000kxd-0.13+git20170730.6182dc8/tests/.pylintrc000066400000000000000000000002171313741073600176400ustar00rootroot00000000000000 [MESSAGES CONTROL] disable=missing-docstring, too-many-public-methods, fixme, locally-disabled [REPORTS] output-format=colorized reports=no kxd-0.13+git20170730.6182dc8/tests/openssl.cnf000066400000000000000000000200711313741073600201460ustar00rootroot00000000000000# 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 kxd-0.13+git20170730.6182dc8/tests/run_tests000077500000000000000000000524671313741073600177650ustar00rootroot00000000000000#!/usr/bin/env python """ Tests for kxd and kxc --------------------- This file contains various integration and validation tests for kxc and kxd. It will create different test configurations and run the compiled server and 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 import shutil import socket import ssl import subprocess import sys import tempfile import textwrap import time import unittest ############################################################ # Test infrastructure. # # These functions and classes are used to make the individual tests easier to # write. For the individual test cases, see below. # 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") # Path to the test OpenSSL configuration. OPENSSL_CONF = os.path.abspath( os.path.dirname(os.path.realpath(__file__)) + "/openssl.cnf") TEMPDIR = "/does/not/exist" # User the script is running as. Just informational, for troubleshooting # purposes, so we don't care if it's missing. LOGNAME = os.environ.get('LOGNAME', 'unknown') def setUpModule(): # pylint: disable=invalid-name if not os.path.isfile(BINS + "/kxd"): raise RuntimeError("kxd not found at " + BINS + "/kxd") if not os.path.isfile(BINS + "/kxc"): raise RuntimeError("kxc not found at " + BINS + "/kxc") global TEMPDIR # pylint: disable=global-statement TEMPDIR = tempfile.mkdtemp(prefix="kxdtest-") 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) @contextlib.contextmanager def pushd(path): prev = os.getcwd() os.chdir(path) yield os.chdir(prev) class Config(object): def __init__(self, name): self.path = tempfile.mkdtemp(prefix="config-%s-" % name, dir=TEMPDIR) self.name = name def gen_certs(self, self_sign=True): try: cmd = ["openssl", "genrsa", "-out", "%s/key.pem" % self.path, "2048"] subprocess.check_output(cmd, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as err: print "openssl call failed, output: %r" % err.output raise ouname = "kxd-tests-%s" % self.name req_args = ["openssl", "req", "-new", "-batch", "-subj", ("/commonName=*" + "/organizationalUnitName=%s" % ouname), "-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]) try: subprocess.check_output(req_args, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as err: print "openssl call failed, output: %r" % err.output raise def cert_path(self): return self.path + "/cert.pem" 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 CA(object): def __init__(self): 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"), "-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) key_path = self.path + "/data/" + name + "/" if not os.path.isdir(key_path): os.makedirs(key_path) open(key_path + "key", "w").write(self.keys[name]) if allowed_clients is not None: cfd = open(key_path + "/allowed_clients", "a") for cli in allowed_clients: cfd.write(cli) if allowed_hosts is not None: hfd = open(key_path + "/allowed_hosts", "a") for host in allowed_hosts: hfd.write(host + "\n") class ClientConfig(Config): 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", "--client_cert=%s/cert.pem" % self.path, "--client_key=%s/key.pem" % self.path, "--server_cert=%s" % server_cert, url] 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): args = [BINS + "/kxd", "--data_dir=%s/data" % cfg, "--key=%s/key.pem" % cfg, "--cert=%s/cert.pem" % cfg, "--logfile=%s/log" % cfg, "--hook=%s/hook" % cfg ] print "Launching server: ", " ".join(args) return subprocess.Popen(args) class TestCase(unittest.TestCase): def setUp(self): self.server = ServerConfig() self.client = ClientConfig() self.daemon = None self.ca = None # pylint: disable=invalid-name self.launch_server(self.server) self.longMessage = True 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 while time.time() < deadline: try: socket.create_connection(("localhost", 19840), timeout=5) break except socket.error: continue else: self.fail("Timeout waiting for the server") # pylint: disable=invalid-name 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(cert_path, url) except subprocess.CalledProcessError as err: self.assertRegexpMatches(err.output, regexp) else: self.fail("Client call did not fail as expected") ############################################################ # Test cases. # 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"]) key = self.client.call(self.server.cert_path(), "kxd://localhost/k1") self.assertEquals(key, self.server.keys["k1"]) # Unknown key -> 404. self.assertClientFails("kxd://localhost/k2", "404 Not Found") # No certificates allowed -> 403. self.server.new_key("k3", allowed_hosts=["localhost"]) self.assertClientFails("kxd://localhost/k3", "403 Forbidden.*No allowed certificate found") # 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") # 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.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): """Tests for multiple clients and keys.""" def setUp(self): TestCase.setUp(self) 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"]) 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"]) # 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/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"]) for key in keys: data = self.client.call(self.server.cert_path(), "kxd://localhost/%s" % key) self.assertEquals(data, self.server.keys[key]) data = self.client2.call(self.server.cert_path(), "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 HTTPSConnection(self, host, port, key_file=None, cert_file=None): # httplib.HTTPSConnection() wrapper that works with versions before # and after Python 2.7.9, which introduced default server validation # with no backwards-compatible way of turning it off. if sys.hexversion < 0x2070900: return httplib.HTTPSConnection( host, port, key_file=key_file, cert_file=cert_file) # Get an SSL context that can validate our server certificate. context = ssl.create_default_context(cafile=self.server.cert_path()) return httplib.HTTPSConnection( host, port, key_file=key_file, cert_file=cert_file, context=context) def test_tricky(self): # No local certificate. conn = self.HTTPSConnection("localhost", 19840) try: conn.request("GET", "/v1/") except ssl.SSLError as err: self.assertRegexpMatches(str(err), "alert bad certificate") else: self.fail("Client call did not fail as expected") # Requests with '..'. conn = self.HTTPSConnection("localhost", 19840, key_file=self.client.key_path(), cert_file=self.client.cert_path()) conn.request("GET", "/v1/a/../b") response = conn.getresponse() # Go's http server intercepts these and gives us a 301 Moved # Permanently. self.assertEquals(response.status, 301) 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()) # 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)) self.assertEquals(server_cert, self.server.cert()) class BrokenServerConfig(TestCase): """Tests for a broken server config.""" def test_broken_client_certs(self): self.server.new_key("k1", 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(5): cfd.readline() cfd.write('+/+BROKEN+/+') cfd.close() self.assertClientFails( "kxd://localhost/k1", "Error loading certs|No allowed certificate found") def test_missing_key(self): self.server.new_key("k1", 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. self.daemon = None 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. # But the client knows the server directly, so it's allowed. # # NOTE: go <= 1.7 rejected this case, it was only allowed during the # 1.8 development cycle (8ad70a5), so comment it out for now. # #key = self.client.call(self.server.cert_path(), "kxd://localhost/k1") #self.assertEquals(key, self.server.keys["k1"]) # 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. # But the server it knows the client directly, so it's allowed. # # NOTE: go <= 1.7 rejected this case, it was only allowed during the # 1.8 development cycle (8ad70a5), so comment it out for now. # #self.server.new_key("k3", # allowed_clients=[self.client.cert()], # allowed_hosts=["localhost"]) #key = self.client.call(self.server.cert_path(), "kxd://localhost/k3") #self.assertEquals(key, self.server.keys["k3"]) 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"]) class Hook(TestCase): """Test cases for hook support.""" HOOK_SCRIPT_TMPL = textwrap.dedent(""" #!/bin/sh pwd > hook-output env >> hook-output exit {exit_code} """.strip()) def write_hook(self, exit_code): path = self.server.path + "/hook" script = self.HOOK_SCRIPT_TMPL.format(exit_code=exit_code) open(path, "w").write(script) os.chmod(path, 0770) def test_simple(self): self.write_hook(exit_code=0) # Normal successful case. 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"]) hook_out = open(self.server.path + "/data/hook-output").read() self.assertIn("CLIENT_CERT_SUBJECT=OU=kxd-tests-client", hook_out) # Failure caused by the hook exiting with error. self.write_hook(exit_code=1) self.assertClientFails("kxd://localhost/k1", "Prevented by hook") # Failure caused by the hook not being executable. self.write_hook(exit_code=0) os.chmod(self.server.path + "/hook", 0660) self.assertClientFails("kxd://localhost/k1", "Prevented by hook") if __name__ == "__main__": unittest.main()