pax_global_header00006660000000000000000000000064137344304600014516gustar00rootroot0000000000000052 comment=c0e5b3d86e3e8c1afec74d0bc736e09243a5eaea kxd-0.15/000077500000000000000000000000001373443046000122315ustar00rootroot00000000000000kxd-0.15/.gitignore000066400000000000000000000001331373443046000142160ustar00rootroot00000000000000out/ *.swp .* # Just in case, we ignore all .pem so noone commits them by accident. *.pem kxd-0.15/.mkdocs.yml000066400000000000000000000001751373443046000143150ustar00rootroot00000000000000site_name: kxd docs_dir: doc theme: readthedocs markdown_extensions: - codehilite: guess_lang: false - attr_list kxd-0.15/.travis.yml000066400000000000000000000005231373443046000143420ustar00rootroot00000000000000# Configuration for https://travis-ci.org/ dist: bionic language: go go_import_path: blitiri.com.ar/go/kxd go: - 1.11 # Debian stable. - stable - master addons: apt: packages: - python3 # Nothing to do for install, the tests will build the binaries anyway. install: true script: - make test kxd-0.15/LICENSE000066400000000000000000000022271373443046000132410ustar00rootroot00000000000000kxd 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.15/Makefile000066400000000000000000000043631373443046000136770ustar00rootroot00000000000000 GO = go OUTDIR = ./out default: kxd kxc kxgencert kxd: $(GO) build -o $(OUTDIR)/kxd ./kxd kxgencert: $(GO) build -o $(OUTDIR)/kxgencert ./kxgencert # 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 . black tests/run_tests vet: $(GO) vet ./... test: kxd kxc kxgencert 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-kxc install-kxgencert \ install-init.d 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-kxgencert: kxgencert $(INSTALL) -d $(PREFIX)/bin $(INSTALL) -m 0755 out/kxgencert $(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 kxgencert .PHONY: install-all install-kxd install-init.d install-kxc install-initramfs .PHONY: test tests kxd-0.15/README.md000066400000000000000000000062721373443046000135170ustar00rootroot00000000000000 # Key exchange daemon [kxd](https://blitiri.com.ar/p/kxd) is a key exchange daemon, and corresponding client, 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. [![Travis-CI build status](https://travis-ci.org/albertito/kxd.svg?branch=master)](https://travis-ci.org/albertito/kxd) ## Quick start Please see the [quick start](https://blitiri.com.ar/p/kxd/docs/quick_start) document for 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.11. The configuration helper scripts (`create-kxd-config`, `kxc-add-key`, etc.) depend on: `bash` and core utilities (`mkdir`, `dd`, etc.). Testing needs Python 3. ## Bugs and contact Please report bugs to albertito@blitiri.com.ar. The latest version can be found at [https://blitiri.com.ar/p/kxd/](https://blitiri.com.ar/p/kxd/) kxd-0.15/cryptsetup/000077500000000000000000000000001373443046000144535ustar00rootroot00000000000000kxd-0.15/cryptsetup/README000066400000000000000000000030301373443046000153270ustar00rootroot00000000000000 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.15/cryptsetup/initramfs-hooks/000077500000000000000000000000001373443046000175705ustar00rootroot00000000000000kxd-0.15/cryptsetup/initramfs-hooks/kxc000077500000000000000000000007131373443046000203040ustar00rootroot00000000000000#!/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.15/cryptsetup/initramfs-scripts/000077500000000000000000000000001373443046000201345ustar00rootroot00000000000000kxd-0.15/cryptsetup/initramfs-scripts/kxc-premount-net000077500000000000000000000005651373443046000233100ustar00rootroot00000000000000#!/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.15/cryptsetup/kxc-cryptsetup000077500000000000000000000014001373443046000174010ustar00rootroot00000000000000#!/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.15/doc/000077500000000000000000000000001373443046000127765ustar00rootroot00000000000000kxd-0.15/doc/index.md000077700000000000000000000000001373443046000161142../README.mdustar00rootroot00000000000000kxd-0.15/doc/man/000077500000000000000000000000001373443046000135515ustar00rootroot00000000000000kxd-0.15/doc/man/generate.sh000077500000000000000000000011121373443046000156750ustar00rootroot00000000000000#!/bin/bash # # Convert pod files to manual pages, using pod2man. # # Assumes files are named like: # .
.pod set -e for IN in *.pod; do OUT=$( basename $IN .pod ) SECTION=${OUT##*.} NAME=${OUT%.*} # If it has not changed in git, set the mtime to the last commit that # touched the file. CHANGED=$( git status --porcelain -- "$IN" | wc -l ) if [ $CHANGED -eq 0 ]; then GIT_MTIME=$( git log --pretty=%at -n1 -- "$IN" ) touch -d "@$GIT_MTIME" "$IN" fi podchecker $IN pod2man --section=$SECTION --name=$NAME \ --release "" --center "" \ $IN $OUT done kxd-0.15/doc/man/kxc-cryptsetup.1000066400000000000000000000124621373443046000166450ustar00rootroot00000000000000.\" Automatically generated by Pod::Man 4.10 (Pod::Simple 3.35) .\" .\" Standard preamble: .\" ======================================================================== .de Sp \" Vertical space (when we can't use .PP) .if t .sp .5v .if n .sp .. .de Vb \" Begin verbatim text .ft CW .nf .ne \\$1 .. .de Ve \" End verbatim text .ft R .fi .. .\" Set up some character translations and predefined strings. \*(-- will .\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left .\" double quote, and \*(R" will give a right double quote. \*(C+ will .\" give a nicer C++. Capital omega is used to do unbreakable dashes and .\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff, .\" nothing in troff, for use with C<>. .tr \(*W- .ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p' .ie n \{\ . ds -- \(*W- . ds PI pi . if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch . if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch . ds L" "" . ds R" "" . ds C` "" . ds C' "" 'br\} .el\{\ . ds -- \|\(em\| . ds PI \(*p . ds L" `` . ds R" '' . ds C` . ds C' 'br\} .\" .\" Escape single quotes in literal strings from groff's Unicode transform. .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" .\" If the F register is >0, we'll generate index entries on stderr for .\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index .\" entries marked with X<> in POD. Of course, you'll have to process the .\" output yourself in some meaningful fashion. .\" .\" Avoid warning from groff about undefined register 'F'. .de IX .. .nr rF 0 .if \n(.g .if rF .nr rF 1 .if (\n(rF:(\n(.g==0)) \{\ . if \nF \{\ . de IX . tm Index:\\$1\t\\n%\t"\\$2" .. . if !\nF==2 \{\ . nr % 0 . nr F 2 . \} . \} .\} .rr rF .\" .\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). .\" Fear. Run. Save yourself. No user-serviceable parts. . \" fudge factors for nroff and troff .if n \{\ . ds #H 0 . ds #V .8m . ds #F .3m . ds #[ \f1 . ds #] \fP .\} .if t \{\ . ds #H ((1u-(\\\\n(.fu%2u))*.13m) . ds #V .6m . ds #F 0 . ds #[ \& . ds #] \& .\} . \" simple accents for nroff and troff .if n \{\ . ds ' \& . ds ` \& . ds ^ \& . ds , \& . ds ~ ~ . ds / .\} .if t \{\ . ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u" . ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u' . ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u' . ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u' . ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u' . ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u' .\} . \" troff and (daisy-wheel) nroff accents .ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V' .ds 8 \h'\*(#H'\(*b\h'-\*(#H' .ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#] .ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H' .ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u' .ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#] .ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#] .ds ae a\h'-(\w'a'u*4/10)'e .ds Ae A\h'-(\w'A'u*4/10)'E . \" corrections for vroff .if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u' .if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u' . \" for low resolution devices (crt and lpr) .if \n(.H>23 .if \n(.V>19 \ \{\ . ds : e . ds 8 ss . ds o a . ds d- d\h'-1'\(ga . ds D- D\h'-1'\(hy . ds th \o'bp' . ds Th \o'LP' . ds ae ae . ds Ae AE .\} .rm #[ #] #H #V #F C .\" ======================================================================== .\" .IX Title "kxc-cryptsetup 1" .TH kxc-cryptsetup 1 "2019-08-10" "" "" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l .nh .SH "NAME" kxc\-cryptsetup \- Cryptsetup helper to kxc .SH "SYNOPSIS" .IX Header "SYNOPSIS" \&\fBkxc-cryptsetup\fR \fI\s-1NAME\s0\fR .SH "DESCRIPTION" .IX Header "DESCRIPTION" \&\fBkxc\fR\|(1) is a client for \fBkxd\fR\|(1), a key exchange daemon. .PP kxc-cryptsetup is a convenience wrapper for invoking kxc while taking the options from the files in \fI/etc/kxc/\fR. .SH "OPTIONS" .IX Header "OPTIONS" Its only command-line argument is a descriptive name, which will be used to find the configuration files. .SH "FILES" .IX Header "FILES" For a given \fI\s-1NAME\s0\fR that is passed as the only command-line argument, the following files are needed: .IP "\fI/etc/kxc/NAME.key.pem\fR" 8 .IX Item "/etc/kxc/NAME.key.pem" Private key to use. .IP "\fI/etc/kxc/NAME.cert.pem\fR" 8 .IX Item "/etc/kxc/NAME.cert.pem" Certificate to use. Must match the given key. .IP "\fI/etc/kxc/NAME.server_cert.pem\fR" 8 .IX Item "/etc/kxc/NAME.server_cert.pem" Server certificate, used to validate the server. .IP "\fI/etc/kxc/NAME.url\fR" 8 .IX Item "/etc/kxc/NAME.url" Contains the \s-1URL\s0 to the key; usually in the form of \f(CW\*(C`kxd://server/name\*(C'\fR. .SH "CONTACT" .IX Header "CONTACT" Main website . .PP If you have any questions, comments or patches please send them to \&\f(CW\*(C`albertito@blitiri.com.ar\*(C'\fR. .SH "SEE ALSO" .IX Header "SEE ALSO" \&\fBkxc\fR\|(1), \fBkxd\fR\|(1), \fBcrypttab\fR\|(5), \fBcryptsetup\fR\|(8). kxd-0.15/doc/man/kxc-cryptsetup.1.pod000066400000000000000000000021341373443046000174210ustar00rootroot00000000000000=head1 NAME kxc-cryptsetup - Cryptsetup helper to kxc =head1 SYNOPSIS B I =head1 DESCRIPTION L is a client for L, a key exchange daemon. kxc-cryptsetup is a convenience wrapper for invoking kxc while taking the options from the files in F. =head1 OPTIONS Its only command-line argument is a descriptive name, which will be used to find the configuration files. =head1 FILES For a given I that is passed as the only command-line argument, the following files are needed: =over 8 =item F Private key to use. =item F Certificate to use. Must match the given key. =item F Server certificate, used to validate the server. =item F Contains the URL to the key; usually in the form of C. =back =head1 CONTACT L
. If you have any questions, comments or patches please send them to C. =head1 SEE ALSO L, L, L, L. kxd-0.15/doc/man/kxc.1000066400000000000000000000123411373443046000144210ustar00rootroot00000000000000.\" Automatically generated by Pod::Man 4.10 (Pod::Simple 3.35) .\" .\" Standard preamble: .\" ======================================================================== .de Sp \" Vertical space (when we can't use .PP) .if t .sp .5v .if n .sp .. .de Vb \" Begin verbatim text .ft CW .nf .ne \\$1 .. .de Ve \" End verbatim text .ft R .fi .. .\" Set up some character translations and predefined strings. \*(-- will .\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left .\" double quote, and \*(R" will give a right double quote. \*(C+ will .\" give a nicer C++. Capital omega is used to do unbreakable dashes and .\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff, .\" nothing in troff, for use with C<>. .tr \(*W- .ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p' .ie n \{\ . ds -- \(*W- . ds PI pi . if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch . if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch . ds L" "" . ds R" "" . ds C` "" . ds C' "" 'br\} .el\{\ . ds -- \|\(em\| . ds PI \(*p . ds L" `` . ds R" '' . ds C` . ds C' 'br\} .\" .\" Escape single quotes in literal strings from groff's Unicode transform. .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" .\" If the F register is >0, we'll generate index entries on stderr for .\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index .\" entries marked with X<> in POD. Of course, you'll have to process the .\" output yourself in some meaningful fashion. .\" .\" Avoid warning from groff about undefined register 'F'. .de IX .. .nr rF 0 .if \n(.g .if rF .nr rF 1 .if (\n(rF:(\n(.g==0)) \{\ . if \nF \{\ . de IX . tm Index:\\$1\t\\n%\t"\\$2" .. . if !\nF==2 \{\ . nr % 0 . nr F 2 . \} . \} .\} .rr rF .\" .\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). .\" Fear. Run. Save yourself. No user-serviceable parts. . \" fudge factors for nroff and troff .if n \{\ . ds #H 0 . ds #V .8m . ds #F .3m . ds #[ \f1 . ds #] \fP .\} .if t \{\ . ds #H ((1u-(\\\\n(.fu%2u))*.13m) . ds #V .6m . ds #F 0 . ds #[ \& . ds #] \& .\} . \" simple accents for nroff and troff .if n \{\ . ds ' \& . ds ` \& . ds ^ \& . ds , \& . ds ~ ~ . ds / .\} .if t \{\ . ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u" . ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u' . ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u' . ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u' . ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u' . ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u' .\} . \" troff and (daisy-wheel) nroff accents .ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V' .ds 8 \h'\*(#H'\(*b\h'-\*(#H' .ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#] .ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H' .ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u' .ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#] .ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#] .ds ae a\h'-(\w'a'u*4/10)'e .ds Ae A\h'-(\w'A'u*4/10)'E . \" corrections for vroff .if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u' .if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u' . \" for low resolution devices (crt and lpr) .if \n(.H>23 .if \n(.V>19 \ \{\ . ds : e . ds 8 ss . ds o a . ds d- d\h'-1'\(ga . ds D- D\h'-1'\(hy . ds th \o'bp' . ds Th \o'LP' . ds ae ae . ds Ae AE .\} .rm #[ #] #H #V #F C .\" ======================================================================== .\" .IX Title "kxc 1" .TH kxc 1 "2019-08-10" "" "" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l .nh .SH "NAME" kxc \- Key exchange client .SH "SYNOPSIS" .IX Header "SYNOPSIS" \&\fBkxc\fR [\fIoptions\fR...] .SH "DESCRIPTION" .IX Header "DESCRIPTION" kxc is a client for \fBkxd\fR\|(1), a key exchange daemon. .PP It will take a client key and certificate, the expected server certificate, and a \s-1URL\s0 to the server (like \f(CW\*(C`kxd://server/host1/key1\*(C'\fR), and it will print on standard output the returned key (the contents of the corresponding key file on the server). .PP There are scripts to tie this with cryptsetup's infrastructure to make the opening of encrypted devices automatic; see \fBkxc\-cryptsetup\fR\|(1) for the details. .SH "OPTIONS" .IX Header "OPTIONS" .IP "\fB\-\-client_key\fR=\fIfile\fR" 8 .IX Item "--client_key=file" File containing the client private key (in \s-1PAM\s0 format). .IP "\fB\-\-client_cert\fR=\fIfile\fR" 8 .IX Item "--client_cert=file" File containing the client certificate that corresponds to the given key (in \&\s-1PAM\s0 format). .IP "\fB\-\-server_cert\fR=\fIfile\fR" 8 .IX Item "--server_cert=file" File containing valid server certificate (in \s-1PAM\s0 format). .SH "CONTACT" .IX Header "CONTACT" Main website . .PP If you have any questions, comments or patches please send them to \&\f(CW\*(C`albertito@blitiri.com.ar\*(C'\fR. .SH "SEE ALSO" .IX Header "SEE ALSO" \&\fBkxc\-cryptsetup\fR\|(1), \fBkxd\fR\|(1). kxd-0.15/doc/man/kxc.1.pod000066400000000000000000000021301373443046000151750ustar00rootroot00000000000000=head1 NAME kxc - Key exchange client =head1 SYNOPSIS B [I...] =head1 DESCRIPTION kxc is a client for L, a key exchange daemon. It will take a client key and certificate, the expected server certificate, and a URL to the server (like C), 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 L for the details. =head1 OPTIONS =over 8 =item B<--client_key>=I File containing the client private key (in PAM format). =item B<--client_cert>=I File containing the client certificate that corresponds to the given key (in PAM format). =item B<--server_cert>=I File containing valid server certificate (in PAM format). =back =head1 CONTACT L
. If you have any questions, comments or patches please send them to C. =head1 SEE ALSO L, L. kxd-0.15/doc/man/kxd.1000066400000000000000000000164601373443046000144300ustar00rootroot00000000000000.\" Automatically generated by Pod::Man 4.10 (Pod::Simple 3.35) .\" .\" Standard preamble: .\" ======================================================================== .de Sp \" Vertical space (when we can't use .PP) .if t .sp .5v .if n .sp .. .de Vb \" Begin verbatim text .ft CW .nf .ne \\$1 .. .de Ve \" End verbatim text .ft R .fi .. .\" Set up some character translations and predefined strings. \*(-- will .\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left .\" double quote, and \*(R" will give a right double quote. \*(C+ will .\" give a nicer C++. Capital omega is used to do unbreakable dashes and .\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff, .\" nothing in troff, for use with C<>. .tr \(*W- .ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p' .ie n \{\ . ds -- \(*W- . ds PI pi . if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch . if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch . ds L" "" . ds R" "" . ds C` "" . ds C' "" 'br\} .el\{\ . ds -- \|\(em\| . ds PI \(*p . ds L" `` . ds R" '' . ds C` . ds C' 'br\} .\" .\" Escape single quotes in literal strings from groff's Unicode transform. .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" .\" If the F register is >0, we'll generate index entries on stderr for .\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index .\" entries marked with X<> in POD. Of course, you'll have to process the .\" output yourself in some meaningful fashion. .\" .\" Avoid warning from groff about undefined register 'F'. .de IX .. .nr rF 0 .if \n(.g .if rF .nr rF 1 .if (\n(rF:(\n(.g==0)) \{\ . if \nF \{\ . de IX . tm Index:\\$1\t\\n%\t"\\$2" .. . if !\nF==2 \{\ . nr % 0 . nr F 2 . \} . \} .\} .rr rF .\" .\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). .\" Fear. Run. Save yourself. No user-serviceable parts. . \" fudge factors for nroff and troff .if n \{\ . ds #H 0 . ds #V .8m . ds #F .3m . ds #[ \f1 . ds #] \fP .\} .if t \{\ . ds #H ((1u-(\\\\n(.fu%2u))*.13m) . ds #V .6m . ds #F 0 . ds #[ \& . ds #] \& .\} . \" simple accents for nroff and troff .if n \{\ . ds ' \& . ds ` \& . ds ^ \& . ds , \& . ds ~ ~ . ds / .\} .if t \{\ . ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u" . ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u' . ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u' . ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u' . ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u' . ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u' .\} . \" troff and (daisy-wheel) nroff accents .ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V' .ds 8 \h'\*(#H'\(*b\h'-\*(#H' .ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#] .ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H' .ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u' .ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#] .ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#] .ds ae a\h'-(\w'a'u*4/10)'e .ds Ae A\h'-(\w'A'u*4/10)'E . \" corrections for vroff .if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u' .if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u' . \" for low resolution devices (crt and lpr) .if \n(.H>23 .if \n(.V>19 \ \{\ . ds : e . ds 8 ss . ds o a . ds d- d\h'-1'\(ga . ds D- D\h'-1'\(hy . ds th \o'bp' . ds Th \o'LP' . ds ae ae . ds Ae AE .\} .rm #[ #] #H #V #F C .\" ======================================================================== .\" .IX Title "kxd 1" .TH kxd 1 "2019-08-10" "" "" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l .nh .SH "NAME" kxd \- Key exchange daemon .SH "SYNOPSIS" .IX Header "SYNOPSIS" \&\fBkxd\fR [\fIoptions\fR...] .SH "DESCRIPTION" .IX Header "DESCRIPTION" kxd is a key exchange daemon, which serves blobs of data (keys) over https. .PP 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. .SH "SETUP" .IX Header "SETUP" The server configuration is stored in a root directory (\fI/etc/kxd/data/\fR by default), and within there, with per-key directories (e.g. \&\fI/etc/kxd/data/host1/key1/\fR), each containing the following files: .IP "\fIkey\fR" 8 .IX Item "key" Contains the key to give to the client. .IP "\fIallowed_clients\fR" 8 .IX Item "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. .IP "\fIallowed_hosts\fR" 8 .IX Item "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). .IP "\fIemail_to\fR" 8 .IX Item "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. .SH "OPTIONS" .IX Header "OPTIONS" .IP "\fB\-\-key\fR=\fIfile\fR" 8 .IX Item "--key=file" Private key to use (in \s-1PAM\s0 format). Defaults to \fI/etc/kxd/key.pem\fR. .IP "\fB\-\-cert\fR=\fIfile\fR" 8 .IX Item "--cert=file" Certificate to use (in \s-1PAM\s0 format); must match the given key. Defaults to \&\fI/etc/kxd/cert.pem\fR. .IP "\fB\-\-data_dir\fR=\fIdirectory\fR" 8 .IX Item "--data_dir=directory" Data directory, where the key and configuration live (see the \s-1SETUP\s0 section above). Defaults to \fI/etc/kxd/data\fR. .IP "\fB\-\-ip_addr\fR=\fIip-address\fR" 8 .IX Item "--ip_addr=ip-address" \&\s-1IP\s0 address to listen on. Defaults to all. .IP "\fB\-\-logfile\fR=\fIfile\fR" 8 .IX Item "--logfile=file" File to write logs to, use \*(L"\-\*(R" for stdout. By default, the daemon will log to syslog. .IP "\fB\-\-port\fR=\fIport\fR" 8 .IX Item "--port=port" Port to listen on. The default port is 19840. .IP "\fB\-\-email_from\fR=\fIemail-address\fR" 8 .IX Item "--email_from=email-address" Email address to send email from. .IP "\fB\-\-smtp_addr\fR=\fIhost:port\fR" 8 .IX Item "--smtp_addr=host:port" Address of the \s-1SMTP\s0 server to use to send emails. If none is given, then emails will not be sent. .IP "\fB\-\-hook\fR=\fIfile\fR" 8 .IX Item "--hook=file" Script to run before authorizing keys. Skipped if it doesn't exist. Defaults to \fI/etc/kxd/hook\fR. .SH "FILES" .IX Header "FILES" .IP "\fI/etc/kxd/key.pem\fR" 8 .IX Item "/etc/kxd/key.pem" Private key to use (in \s-1PAM\s0 format). .IP "\fI/etc/kxd/cert.pem\fR" 8 .IX Item "/etc/kxd/cert.pem" Certificate to use (in \s-1PAM\s0 format); must match the given key. .IP "\fI/etc/kxd/hook\fR" 8 .IX Item "/etc/kxd/hook" Script to run before authorizing keys. Skipped if it doesn't exist. .IP "\fI/etc/kxd/data/\fR" 8 .IX Item "/etc/kxd/data/" Data directory, where the keys and their configuration live. .SH "CONTACT" .IX Header "CONTACT" Main website . .PP If you have any questions, comments or patches please send them to \&\f(CW\*(C`albertito@blitiri.com.ar\*(C'\fR. .SH "SEE ALSO" .IX Header "SEE ALSO" \&\fBkxc\fR\|(1), \fBkxc\-cryptsetup\fR\|(1). kxd-0.15/doc/man/kxd.1.pod000066400000000000000000000053421373443046000152060ustar00rootroot00000000000000=head1 NAME kxd - Key exchange daemon =head1 SYNOPSIS B [I...] =head1 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. =head1 SETUP The server configuration is stored in a root directory (F by default), and within there, with per-key directories (e.g. F), each containing the following files: =over 8 =item F Contains the key to give to the client. =item F 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. =item F 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). =item F Contains one or more email destinations to notify (one per line). If not present, then no notifications will be sent upon key accesses. =back =head1 OPTIONS =over 8 =item B<--key>=I Private key to use (in PAM format). Defaults to F. =item B<--cert>=I Certificate to use (in PAM format); must match the given key. Defaults to F. =item B<--data_dir>=I Data directory, where the key and configuration live (see the SETUP section above). Defaults to F. =item B<--ip_addr>=I IP address to listen on. Defaults to all. =item B<--logfile>=I File to write logs to, use "-" for stdout. By default, the daemon will log to syslog. =item B<--port>=I Port to listen on. The default port is 19840. =item B<--email_from>=I Email address to send email from. =item B<--smtp_addr>=I Address of the SMTP server to use to send emails. If none is given, then emails will not be sent. =item B<--hook>=I Script to run before authorizing keys. Skipped if it doesn't exist. Defaults to F. =back =head1 FILES =over 8 =item F Private key to use (in PAM format). =item F Certificate to use (in PAM format); must match the given key. =item F Script to run before authorizing keys. Skipped if it doesn't exist. =item F Data directory, where the keys and their configuration live. =back =head1 CONTACT L
. If you have any questions, comments or patches please send them to C. =head1 SEE ALSO L, L. kxd-0.15/doc/quick_start.md000066400000000000000000000070431373443046000156550ustar00rootroot00000000000000 # Quick start In this guide we show how to set up a [key exchange daemon][kxd] 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. ## Server setup Install [kxd] on the server, via your distribution packages (e.g. `apt install kxd`), 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). For the hostname, use the name or IP that the clients will use to reach the server. You can use more than one, separated with commas. ## Client setup Install [kxc][kxd] on the client machine, via your distribution packages (e.g. `apt install kxc`), 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: ```shell # 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: ```shell 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. [kxd]: https://blitiri.com.ar/p/kxd [kxc]: https://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.15/kxc/000077500000000000000000000000001373443046000130165ustar00rootroot00000000000000kxd-0.15/kxc/kxc.go000066400000000000000000000060261373443046000141360ustar00rootroot00000000000000// 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.15/kxd/000077500000000000000000000000001373443046000130175ustar00rootroot00000000000000kxd-0.15/kxd/email.go000066400000000000000000000044731373443046000144450ustar00rootroot00000000000000package 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.15/kxd/hook.go000066400000000000000000000036321373443046000143120ustar00rootroot00000000000000package 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.15/kxd/key_config.go000066400000000000000000000103361373443046000154660ustar00rootroot00000000000000package main import ( "crypto/x509" "fmt" "io/ioutil" "net" "os" "strings" ) func isDir(path string) (bool, error) { fi, err := os.Stat(path) if err != nil { return false, err } return fi.IsDir(), nil } func isRegular(path string) (bool, error) { fi, err := os.Stat(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.15/kxd/kxd.go000066400000000000000000000146561373443046000141500ustar00rootroot00000000000000// 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.15/kxgencert/000077500000000000000000000000001373443046000142235ustar00rootroot00000000000000kxd-0.15/kxgencert/kxgencert.go000066400000000000000000000047401373443046000165510ustar00rootroot00000000000000// Utility to generate self-signed certificates. // It generates a self-signed x509 certificate and key pair. package main import ( crand "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "flag" "fmt" "math/big" "net" "os" "strings" "time" ) var ( host = flag.String("host", "*", "Hostnames/IPs to generate the certificate for (comma separated)") validFor = flag.Duration("validfor", 24*time.Hour*365*10, "How long will the certificate be valid for (default: 10y)") orgName = flag.String("organization", "", "Organization to use in the certificate, useful for debugging") certPath = flag.String("cert", "cert.pem", "Where to write the generated certificate") keyPath = flag.String("key", "key.pem", "Where to write the generated key") ) func fatalf(f string, a ...interface{}) { fmt.Printf(f, a...) os.Exit(1) } func main() { flag.Parse() // Build the certificate template. serial, err := crand.Int(crand.Reader, big.NewInt(1<<62)) if err != nil { fatalf("Error generating serial number: %v\n", err) } tmpl := x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{Organization: []string{*orgName}}, NotBefore: time.Now(), NotAfter: time.Now().Add(*validFor), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, } hosts := strings.Split(*host, ",") for _, h := range hosts { if ip := net.ParseIP(h); ip != nil { tmpl.IPAddresses = append(tmpl.IPAddresses, ip) } else { tmpl.DNSNames = append(tmpl.DNSNames, h) } } // Generate a private key (RSA 2048). privK, err := rsa.GenerateKey(crand.Reader, 2048) if err != nil { fatalf("Error generating key: %v\n", err) } // Write the certificate. { derBytes, err := x509.CreateCertificate( crand.Reader, &tmpl, &tmpl, &privK.PublicKey, privK) if err != nil { fatalf("Failed to create certificate: %v\n", err) } fullchain, err := os.Create(*certPath) if err != nil { fatalf("Failed to open %q: %v\n", *certPath, err) } pem.Encode(fullchain, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) fullchain.Close() } // Write the private key. { privkey, err := os.Create(*keyPath) if err != nil { fatalf("failed to open %q: %v\n", *keyPath, err) } block := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privK)} pem.Encode(privkey, block) privkey.Close() } } kxd-0.15/scripts/000077500000000000000000000000001373443046000137205ustar00rootroot00000000000000kxd-0.15/scripts/create-kxd-config000077500000000000000000000013641373443046000171440ustar00rootroot00000000000000#!/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. if [ "$1" == "" ]; then echo "Usage: $0 " exit 1 fi 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 kxgencert \ -host "${1?}" \ -organization "kxd@$HOSTNAME" \ -key /etc/kxd/key.pem \ -cert /etc/kxd/cert.pem chmod 400 /etc/kxd/key.pem else echo "Private key already exists (/etc/kxd/key.pem)" fi kxd-0.15/scripts/default/000077500000000000000000000000001373443046000153445ustar00rootroot00000000000000kxd-0.15/scripts/default/kxd000066400000000000000000000004311373443046000160530ustar00rootroot00000000000000# 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.15/scripts/hook000077500000000000000000000007611373443046000146120ustar00rootroot00000000000000#!/bin/bash # # Example kxd hook, which uses the system's sendmail to send a notification # via email. # # Note that if the script fails, kxd will NOT send the key. # echo "Date: $(date --rfc-2822) From: $MAIL_FROM To: $EMAIL_TO Subject: Access to key $KEY_PATH Key: $KEY_PATH Accessed by: $REMOTE_ADDR On: $(date) Client certificate: Signature: ${CLIENT_CERT_SIGNATURE:0:40}... Subject: $CLIENT_CERT_SUBJECT Authorizing chains: $CHAIN_0 $CHAIN_1 $CHAIN_2 " | sendmail -t exit $? kxd-0.15/scripts/init.d/000077500000000000000000000000001373443046000151055ustar00rootroot00000000000000kxd-0.15/scripts/init.d/kxd000077500000000000000000000026371373443046000156310ustar00rootroot00000000000000#! /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.15/scripts/kxc-add-key000077500000000000000000000022401373443046000157450ustar00rootroot00000000000000#!/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 kxgencert \ -organization "kxc@$HOSTNAME" \ -key /etc/kxc/key.pem \ -cert /etc/kxc/cert.pem chmod 400 /etc/kxc/key.pem else echo "Private key already exists (/etc/kxc/key.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.15/scripts/kxd-add-client-key000077500000000000000000000016421373443046000172270ustar00rootroot00000000000000#!/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.15/scripts/systemd/000077500000000000000000000000001373443046000154105ustar00rootroot00000000000000kxd-0.15/scripts/systemd/kxd.service000066400000000000000000000002541373443046000175610ustar00rootroot00000000000000[Unit] Description = Key exchange daemon [Service] EnvironmentFile = /etc/default/kxd ExecStart = /usr/bin/kxd $OPTS Type = simple [Install] WantedBy = multi-user.target kxd-0.15/scripts/upstart/000077500000000000000000000000001373443046000154225ustar00rootroot00000000000000kxd-0.15/scripts/upstart/kxd.conf000066400000000000000000000003741373443046000170630ustar00rootroot00000000000000description "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.15/tests/000077500000000000000000000000001373443046000133735ustar00rootroot00000000000000kxd-0.15/tests/.pylintrc000066400000000000000000000002171373443046000152400ustar00rootroot00000000000000 [MESSAGES CONTROL] disable=missing-docstring, too-many-public-methods, fixme, locally-disabled [REPORTS] output-format=colorized reports=no kxd-0.15/tests/run_tests000077500000000000000000000354211373443046000153540ustar00rootroot00000000000000#!/usr/bin/env python3 """ 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 "black run_tests" after making changes, to to make sure the # file has a reasonably uniform coding style. import contextlib import http.client import os import shutil import socket import ssl import subprocess import tempfile import textwrap import time import tracemalloc import unittest tracemalloc.start() ############################################################ # 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") 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") if not os.path.isfile(BINS + "/kxgencert"): raise RuntimeError("kxgencert not found at " + BINS + "/kxgencert") 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: def __init__(self, name): self.path = tempfile.mkdtemp(prefix="config-%s-" % name, dir=TEMPDIR) self.name = name def gen_cert(self): try: cmd = [ BINS + "/kxgencert", "-organization=kxd-tests-%s" % self.name, "-key=" + self.key_path(), "-cert=" + self.cert_path(), ] subprocess.check_output(cmd, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as err: print("kxgencert 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 cert(self): return read_all(self.path + "/cert.pem") class ServerConfig(Config): def __init__(self, name="server"): Config.__init__(self, name) self.keys = {} self.gen_cert() 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) with open(key_path + "key", "bw") as key: key.write(self.keys[name]) if allowed_clients is not None: with open(key_path + "/allowed_clients", "a") as cfd: for cli in allowed_clients: cfd.write(cli) if allowed_hosts is not None: with open(key_path + "/allowed_hosts", "a") as hfd: for host in allowed_hosts: hfd.write(host + "\n") class ClientConfig(Config): def __init__(self, name="client"): Config.__init__(self, name) self.gen_cert() 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) def read_all(fname): with open(fname) as fd: # pylint: disable=invalid-name return fd.read() 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) def tearDown(self): if self.daemon: self.daemon.kill() self.daemon.wait() 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: with 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.assertRegex(err.output.decode(), 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.assertEqual(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.assertEqual(key, self.server.keys["k1"]) key = self.client2.call(self.server.cert_path(), "kxd://localhost/k1") self.assertEqual(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.assertEqual(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.assertEqual(data, self.server.keys[key]) data = self.client2.call( self.server.cert_path(), "kxd://localhost/%s" % key ) self.assertEqual(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(read_all(server1.cert_path())) server_certs.write(read_all(server2.cert_path())) server_certs.close() key = self.client.call(server_certs_path, "kxd://localhost/k1") self.assertEqual(key, server1.keys["k1"]) self.daemon.kill() self.daemon.wait() time.sleep(0.5) self.launch_server(server2) key = self.client.call(server_certs_path, "kxd://localhost/k1") self.assertEqual(key, server2.keys["k1"]) class TrickyRequests(TestCase): """Tests for tricky requests.""" def https_connection(self, host, port, key_file=None, cert_file=None): # Get an SSL context that can validate our server certificate. context = ssl.create_default_context(cafile=self.server.cert_path()) context.check_hostname = False if cert_file: context.load_cert_chain(cert_file, key_file) return http.client.HTTPSConnection(host, port, context=context) def test_no_local_cert(self): """No local certificate.""" conn = self.https_connection("localhost", 19840) try: conn.request("GET", "/v1/") conn.getresponse() conn.close() except ssl.SSLError as err: self.assertEqual(err.reason, "SSLV3_ALERT_BAD_CERTIFICATE") else: self.fail("Client call did not fail as expected") def test_path_with_dotdot(self): """Requests with '..'.""" conn = self.https_connection( "localhost", 19840, key_file=self.client.key_path(), cert_file=self.client.cert_path(), ) conn.request("GET", "/v1/a/../b") response = conn.getresponse() conn.close() # Go's http server intercepts these and gives us a 301 Moved # Permanently. self.assertEqual(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.assertEqual(server_cert, self.server.cert()) sock.close() 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. with open(self.server.path + "/data/k1/allowed_clients", "tr+") as cfd: cfd.seek(30) cfd.write("+/+BROKEN+/+") 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 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) with open(path, "w") as hook: hook.write(script) os.chmod(path, 0o770) 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.assertEqual(key, self.server.keys["k1"]) hook_out = read_all(self.server.path + "/data/hook-output") self.assertIn("CLIENT_CERT_SUBJECT=O=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", 0o660) self.assertClientFails("kxd://localhost/k1", "Prevented by hook") if __name__ == "__main__": unittest.main()