pax_global_header00006660000000000000000000000064146765737270014542gustar00rootroot0000000000000052 comment=4e671badc2929741c7191f9fa682070eedb7b361 kxd-0.17/000077500000000000000000000000001467657372700122575ustar00rootroot00000000000000kxd-0.17/.github/000077500000000000000000000000001467657372700136175ustar00rootroot00000000000000kxd-0.17/.github/workflows/000077500000000000000000000000001467657372700156545ustar00rootroot00000000000000kxd-0.17/.github/workflows/govulncheck.yaml000066400000000000000000000006641467657372700210560ustar00rootroot00000000000000name: "govulncheck" on: push: branches: [ "main", "next" ] pull_request: # The branches below must be a subset of the branches above branches: [ "main", "next" ] schedule: - cron: '29 21 * * 6' jobs: govulncheck: runs-on: ubuntu-latest timeout-minutes: 5 name: Run govulncheck steps: - id: govulncheck uses: golang/govulncheck-action@v1 with: check-latest: true kxd-0.17/.github/workflows/tests.yaml000066400000000000000000000020371467657372700177040ustar00rootroot00000000000000name: "tests" on: push: branches: [ "main", "next" ] pull_request: # The branches below must be a subset of the branches above branches: [ "main", "next" ] schedule: - cron: '26 21 * * 6' jobs: tests: runs-on: ubuntu-latest timeout-minutes: 5 strategy: matrix: # Test against stable, and the oldest supported version. go: [ 'stable', '1.21.x' ] steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} check-latest: true - name: test run: make test coverage: needs: tests runs-on: ubuntu-latest timeout-minutes: 5 steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: 'stable' check-latest: true - name: coverage tests run: ./tests/cover.sh - name: upload uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} file: .cover-merged.out kxd-0.17/.gitignore000066400000000000000000000001611467657372700142450ustar00rootroot00000000000000out/ *.swp .* # Just in case, we ignore all .pem so noone commits them by accident. *.pem !.gitignore !.github kxd-0.17/.mkdocs.yml000066400000000000000000000001751467657372700143430ustar00rootroot00000000000000site_name: kxd docs_dir: doc theme: readthedocs markdown_extensions: - codehilite: guess_lang: false - attr_list kxd-0.17/LICENSE000066400000000000000000000022271467657372700132670ustar00rootroot00000000000000kxd 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.17/Makefile000066400000000000000000000044351467657372700137250ustar00rootroot00000000000000 GO = go OUTDIR = ./out default: kxd kxc kxgencert kxd: $(GO) build ${GOFLAGS} -o $(OUTDIR)/kxd ./kxd kxgencert: $(GO) build ${GOFLAGS} -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 ${GOFLAGS} --tags netgo -a -o $(OUTDIR)/kxc ./kxc fmt: gofmt -s -w . black -l 80 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.17/README.md000066400000000000000000000066441467657372700135500ustar00rootroot00000000000000 # 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. [![Docs](https://img.shields.io/badge/docs-reference-blue.svg)](https://blitiri.com.ar/p/kxd/) [![Tests](https://github.com/albertito/kxd/actions/workflows/tests.yaml/badge.svg)](https://github.com/albertito/kxd/actions) [![Coverage](https://codecov.io/gh/albertito/kxd/branch/next/graph/badge.svg?token=WMRDGeHOUK)](https://codecov.io/gh/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.21. 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.17/cryptsetup/000077500000000000000000000000001467657372700145015ustar00rootroot00000000000000kxd-0.17/cryptsetup/README000066400000000000000000000030301467657372700153550ustar00rootroot00000000000000 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.17/cryptsetup/initramfs-hooks/000077500000000000000000000000001467657372700176165ustar00rootroot00000000000000kxd-0.17/cryptsetup/initramfs-hooks/kxc000077500000000000000000000007711467657372700203360ustar00rootroot00000000000000#!/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 (if it exists). if [ -d /etc/kxc/ ]; then cp -a /etc/kxc/ ${DESTDIR}/etc fi kxd-0.17/cryptsetup/initramfs-scripts/000077500000000000000000000000001467657372700201625ustar00rootroot00000000000000kxd-0.17/cryptsetup/initramfs-scripts/kxc-premount-net000077500000000000000000000005651467657372700233360ustar00rootroot00000000000000#!/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.17/cryptsetup/kxc-cryptsetup000077500000000000000000000014001467657372700174270ustar00rootroot00000000000000#!/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.17/doc/000077500000000000000000000000001467657372700130245ustar00rootroot00000000000000kxd-0.17/doc/index.md000077700000000000000000000000001467657372700161422../README.mdustar00rootroot00000000000000kxd-0.17/doc/man/000077500000000000000000000000001467657372700135775ustar00rootroot00000000000000kxd-0.17/doc/man/generate.sh000077500000000000000000000011121467657372700157230ustar00rootroot00000000000000#!/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.17/doc/man/kxc-cryptsetup.1000066400000000000000000000124621467657372700166730ustar00rootroot00000000000000.\" 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.17/doc/man/kxc-cryptsetup.1.pod000066400000000000000000000021341467657372700174470ustar00rootroot00000000000000=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.17/doc/man/kxc.1000066400000000000000000000123411467657372700144470ustar00rootroot00000000000000.\" 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.17/doc/man/kxc.1.pod000066400000000000000000000021301467657372700152230ustar00rootroot00000000000000=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.17/doc/man/kxd.1000066400000000000000000000164601467657372700144560ustar00rootroot00000000000000.\" 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.17/doc/man/kxd.1.pod000066400000000000000000000053421467657372700152340ustar00rootroot00000000000000=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.17/doc/quick_start.md000066400000000000000000000070431467657372700157030ustar00rootroot00000000000000 # 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.17/go.mod000066400000000000000000000000461467657372700133650ustar00rootroot00000000000000module blitiri.com.ar/go/kxd go 1.20 kxd-0.17/kxc/000077500000000000000000000000001467657372700130445ustar00rootroot00000000000000kxd-0.17/kxc/kxc.go000066400000000000000000000120711467657372700141610ustar00rootroot00000000000000// 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" "encoding/pem" "flag" "fmt" "io/ioutil" "log" "net/http" "net/url" "slices" "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, bool, error) { pemData, err := ioutil.ReadFile(*serverCert) if err != nil { return nil, false, err } // Old server certificates can use the deprecated '*' for the server name. // This is not supported by Go, but we still want to support them, so // we need to identify them at parsing time. hasWildcard := false { data := pemData[:] for { var block *pem.Block block, data = pem.Decode(data) if block == nil { break } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, false, fmt.Errorf( "error parsing certificate: %s", err) } if strings.Contains(cert.Subject.CommonName, "*") { hasWildcard = true break } if slices.Contains(cert.DNSNames, "*") { hasWildcard = true break } } } pool := x509.NewCertPool() if !pool.AppendCertsFromPEM(pemData) { return nil, false, fmt.Errorf("error appending certificates") } return pool, hasWildcard, 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) } serverCerts, hasWildcard, err := loadServerCerts() if err != nil { log.Fatalf("Failed to load server certs: %s", err) } if hasWildcard { // We want to do the standard verification, but ignoring the server name. // This is because old certificates might not have the server name, or use // '*' which was later deprecated and not supported by Go. // This also makes deployment much more practical on small networks where // the server name is not important. // // Unfortunately, there's no way to tell Go to ignore just that, so we need // to do it manually. // To do that, we need to set InsecureSkipVerify to true, and then provide // a custom VerifyConnection function that does the verification we want. // The verification is using the same logic Go does, and following the // official example at // https://pkg.go.dev/crypto/tls#example-Config-VerifyConnection. tlsConf.InsecureSkipVerify = true tlsConf.VerifyConnection = func(cs tls.ConnectionState) error { opts := x509.VerifyOptions{ // Explicitly not care about the server name. DNSName: "", Intermediates: x509.NewCertPool(), // Compare against the server certificates. Roots: serverCerts, } for _, cert := range cs.PeerCertificates[1:] { opts.Intermediates.AddCert(cert) } _, err := cs.PeerCertificates[0].Verify(opts) return err } } else { // If none of the server certificates use the deprecated '*', we can // use the standard verification. 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.17/kxd/000077500000000000000000000000001467657372700130455ustar00rootroot00000000000000kxd-0.17/kxd/email.go000066400000000000000000000033511467657372700144650ustar00rootroot00000000000000package main import ( "bytes" "crypto/x509" "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: {{.Cert.Subject}} Authorizing chains: {{range .Chains}} {{ChainToString .}} {{end}} `) var emailTmpl = template.New("email") func init() { emailTmpl.Funcs(map[string]interface{}{ "ChainToString": ChainToString, }) template.Must(emailTmpl.Parse(emailTmplBody)) } // 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.17/kxd/hook.go000066400000000000000000000036251467657372700143420ustar00rootroot00000000000000package 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="+clientCert.Subject.String()) 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.17/kxd/key_config.go000066400000000000000000000103361467657372700155140ustar00rootroot00000000000000package 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.17/kxd/kxd.go000066400000000000000000000157611467657372700141740ustar00rootroot00000000000000// 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" "errors" "flag" "fmt" "io" "log" "log/syslog" "net/http" "os" "os/signal" "path" "strings" "syscall" ) 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) } var ( errInvalidVersion = errors.New("invalid version") errHasDotDot = errors.New("path contains '..'") ) // KeyPath returns the path to the requested key, extracting it from the URL. func (req *Request) KeyPath() (string, error) { // Clean the path to remove any noise. kp := path.Clean(req.URL.Path) // Must start with "/v1/". Doing this after the Clean also ensures there // is something else after the version (because Clean removes trailing // slashes). kp, hasVersion := strings.CutPrefix(kp, "/v1/") if !hasVersion { return "", errInvalidVersion } // Be extra paranoid and reject keys with "..", even if they're valid // (e.g. "/v1/x..y" is valid, but will get rejected anyway). // Note requests like this shouldn't reach this stage anyway, due to the // http library processing and the path.Clean above. if strings.Contains(kp, "..") { return "", errHasDotDot } return kp, 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 } realKeyPath := path.Join(*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 signalHandler() { signals := make(chan os.Signal, 1) signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT) for { switch sig := <-signals; sig { case syscall.SIGTERM, syscall.SIGINT: logging.Printf("Received signal %s, exiting", sig) os.Exit(0) default: logging.Printf("Received unexpected signal %s", sig) } } } func main() { flag.Parse() initLog() go signalHandler() 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.17/kxd/kxd_test.go000066400000000000000000000030011467657372700152130ustar00rootroot00000000000000package main import ( "crypto/tls" "errors" "log" "net/http" "net/http/httptest" "net/url" "testing" ) func init() { // Initialize the global logger. The testing framework will capture the // output and use it as needed. logging = log.Default() } func TestKeyPath(t *testing.T) { cases := []struct { url string want string err error }{ {"/v1/key", "key", nil}, {"/v1/path/to/key", "path/to/key", nil}, {"/v1/path/to/key/", "path/to/key", nil}, {"", "", errInvalidVersion}, {"/", "", errInvalidVersion}, {"/v1", "", errInvalidVersion}, {"/v1/", "", errInvalidVersion}, {"/v1//", "", errInvalidVersion}, {"v1/path/to/key/", "", errInvalidVersion}, {"/v2/path/to/key", "", errInvalidVersion}, {"/v1/a..b", "", errHasDotDot}, } for _, c := range cases { u, _ := url.Parse(c.url) req := Request{&http.Request{ URL: u, }} got, err := req.KeyPath() if got != c.want { t.Errorf("%q KeyPath == %q, want %q", c.url, got, c.want) } if !errors.Is(err, c.err) { t.Errorf("%q KeyPath error == %v, want %v", c.url, err, c.err) } } } func TestHandlerWithoutCert(t *testing.T) { // Reject request without a client certificate. // Usually the http server doesn't let it get this far, so we have a // custom test for it. req := &http.Request{ URL: &url.URL{}, TLS: &tls.ConnectionState{}, } w := httptest.NewRecorder() HandlerV1(w, req) if w.Code != http.StatusNotAcceptable { t.Errorf("HandlerV1(%v) == %d, want %d", req, w.Code, http.StatusNotAcceptable) } } kxd-0.17/kxgencert/000077500000000000000000000000001467657372700142515ustar00rootroot00000000000000kxd-0.17/kxgencert/kxgencert.go000066400000000000000000000047501467657372700166000ustar00rootroot00000000000000// 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", "localhost", "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.17/scripts/000077500000000000000000000000001467657372700137465ustar00rootroot00000000000000kxd-0.17/scripts/create-kxd-config000077500000000000000000000013641467657372700171720ustar00rootroot00000000000000#!/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.17/scripts/default/000077500000000000000000000000001467657372700153725ustar00rootroot00000000000000kxd-0.17/scripts/default/kxd000066400000000000000000000004311467657372700161010ustar00rootroot00000000000000# 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.17/scripts/hook000077500000000000000000000007611467657372700146400ustar00rootroot00000000000000#!/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.17/scripts/init.d/000077500000000000000000000000001467657372700151335ustar00rootroot00000000000000kxd-0.17/scripts/init.d/kxd000077500000000000000000000026371467657372700156570ustar00rootroot00000000000000#! /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.17/scripts/kxc-add-key000077500000000000000000000022401467657372700157730ustar00rootroot00000000000000#!/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.17/scripts/kxd-add-client-key000077500000000000000000000016421467657372700172550ustar00rootroot00000000000000#!/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.17/scripts/systemd/000077500000000000000000000000001467657372700154365ustar00rootroot00000000000000kxd-0.17/scripts/systemd/kxd.service000066400000000000000000000002541467657372700176070ustar00rootroot00000000000000[Unit] Description = Key exchange daemon [Service] EnvironmentFile = /etc/default/kxd ExecStart = /usr/bin/kxd $OPTS Type = simple [Install] WantedBy = multi-user.target kxd-0.17/scripts/upstart/000077500000000000000000000000001467657372700154505ustar00rootroot00000000000000kxd-0.17/scripts/upstart/kxd.conf000066400000000000000000000003741467657372700171110ustar00rootroot00000000000000description "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.17/tests/000077500000000000000000000000001467657372700134215ustar00rootroot00000000000000kxd-0.17/tests/.pylintrc000066400000000000000000000002171467657372700152660ustar00rootroot00000000000000 [MESSAGES CONTROL] disable=missing-docstring, too-many-public-methods, fixme, locally-disabled [REPORTS] output-format=colorized reports=no kxd-0.17/tests/bad_cert/000077500000000000000000000000001467657372700151645ustar00rootroot00000000000000kxd-0.17/tests/bad_cert/cert.pem000066400000000000000000000021061467657372700166230ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- abcdefCCAeOgAwIBAgIIJua2egAlPEQwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UE ChMQa3hkLXRlc3RzLXNlcnZlcjAeFw0yNDA4MTUxOTAxMTdaFw0zNDA4MTMxOTAx MTdaMBsxGTAXBgNVBAoTEGt4ZC10ZXN0cy1zZXJ2ZXIwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQDy3sYvXUnIm591qIWWbn3iTUyKl7ncWmGSQJ8XWXaz NwbNuINBy6bTf8H6BP20sCWQWYRTKYELGmZLp4fUme33y0xNnFpcbR5tqKI8SQOX wvCk3ox3xUnBEr7t0gHhSDVrhPRMWsWVlgPw4t3MgoDk/J97uAwGnfCXiMWZL3tM fFgrJja73RmzsbjamS/yjp7A6jLLe0MH/o73zaNJsyTeit6uTwU9PBEHzLgH0Udi 97y/MQ/MQs0Wn1JtTbvV5HQSHrlwVkSQY1feSNBERnCs/l5B9CYoPX3o3a3UtyyX jMy6eSt3bio0Du5jYR9VoIi94FJ8SJlNgPXVgnPLr8XjAgMBAAGjQzBBMA4GA1Ud DwEB/wQEAwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMAwG A1UdEQQFMAOCASowDQYJKoZIhvcNAQELBQADggEBACAGsja7xV92lyefwXDX3ZWw hQ5b/t5WGSXdQNG4oYWpU+EfTNU79j2Sr/RcZ/8YD6weYL75ONW2BeOTQ9Iev5du XAVcfgiAjz8zRru+hH7xausFJM1I5e3nhWSzAyBiyJxzSBcBUWhiNrSvStjvigTs XT34FYR9p+7ZGFljQlD5eeh7E5cV6UA9wIE2vXTshDCyZdoDqEoXj3HYw/sJtOdf kRZlOuK6GXJFxoZBIa5cJFSgq618UjwErFG7frsAKr9Da8sXBQMThMDz37mZlRST r3M2b4+Z13YRtFlWmqpg8HbOjBJEgPvWf6GfXiqWf6d9IxptMFk2u80IIDi38A8= -----END CERTIFICATE----- kxd-0.17/tests/cover.sh000077500000000000000000000011641467657372700151000ustar00rootroot00000000000000#!/bin/bash set -e cd "$(realpath `dirname ${0}`)/../" make GOFLAGS="-cover -covermode=count" rm -rf .coverage/ mkdir -p .coverage/{go,sh,all} export GOCOVERDIR="${PWD}/.coverage" go test -covermode=count -coverpkg=./... ./... \ -args -test.gocoverdir="${GOCOVERDIR}/go" GOCOVERDIR="${GOCOVERDIR}/sh" tests/run_tests -b go tool covdata merge -i "${GOCOVERDIR}/go,${GOCOVERDIR}/sh" -o "${GOCOVERDIR}/all" go tool covdata textfmt -i "${GOCOVERDIR}/all" -o .cover-merged.out go tool cover -func=.cover-merged.out | grep -i total go tool cover -html=.cover-merged.out -o .cover-kxd.html echo "file:///$PWD/.cover-kxd.html" kxd-0.17/tests/run_tests000077500000000000000000000634011467657372700154010ustar00rootroot00000000000000#!/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 threading 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, host=""): self.path = tempfile.mkdtemp(prefix="config-%s-" % name, dir=TEMPDIR) self.name = name self.host = host self.keys = {} def gen_cert(self): try: cmd = [ BINS + "/kxgencert", "-organization=kxd-tests-%s" % self.name, "-key=" + self.key_path(), "-cert=" + self.cert_path(), ] if self.host: cmd.append("-host=" + self.host) 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") def new_key( self, name, allowed_clients=None, allowed_hosts=None, email_to=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") if email_to is not None: with open(key_path + "/email_to", "a") as efd: efd.write(email_to + "\n") 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 class ServerConfig(Config): def __init__(self, name="server", host="localhost"): Config.__init__(self, name, host) self.gen_cert() def call(self, server_cert, url): # To prevent accidental calls and to enforce that the test cases # don't mix server and client. return NotImplementedError("ServerConfig does not support call") class ClientConfig(Config): def __init__(self, name="client", host="localhost"): Config.__init__(self, name, host) self.gen_cert() def new_key(self, name, allowed_clients=None, allowed_hosts=None): # To prevent accidental calls and to enforce that the test cases # don't mix server and client. raise NotImplementedError("ClientConfig does not support new_key") class StaticConfig(Config): def __init__(self, path): self.path = path self.host = "localhost" self.keys = {} def gen_cert(self): raise NotImplementedError("StaticConfig does not support gen_cert") def launch_daemon(cfg, smtp_addr=None): 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, ] if smtp_addr: args.append("--smtp_addr=%s:%s" % smtp_addr) 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() def receive_emails(): """Receive emails from the server. Return: - The server address. - A list where we will put the emails: each email is a tuple of (destination addresses, body). """ server = socket.create_server(("localhost", 0)) server.listen(1) addr = server.getsockname() emails = [] def handler(): # Complete a simple SMTP transaction. sock, addr = server.accept() sock.sendall(b"220 localhost SMTP\n") body = b"" rcpt_to = [] while True: data = sock.recv(1024) if not data: break if data.startswith(b"HELO"): sock.sendall(b"250 localhost\n") elif data.startswith(b"MAIL FROM"): sock.sendall(b"250 OK\n") elif data.startswith(b"RCPT TO"): rcpt_to.append(data.split(b"<")[1].split(b">")[0].decode()) sock.sendall(b"250 OK\n") elif data.startswith(b"DATA"): sock.sendall(b"354 Start data\n") body = b"" while True: body += sock.recv(1024) if body.endswith(b"\r\n.\r\n"): sock.sendall(b"250 OK\n") break elif data.startswith(b"QUIT"): sock.sendall(b"221 Chau\n") break else: sock.sendall(b"500 Unknown command\n") emails.append((rcpt_to, body)) sock.close() server.close() server_thread = threading.Thread(target=handler, daemon=True) server_thread.start() return addr, emails 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.terminate() self.daemon.wait() def launch_server(self, server, smtp_addr=None): self.daemon = launch_daemon(server.path, smtp_addr) # 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. server_url = "kxd://" + self.server.host # Normal successful case. self.server.new_key( "k1", allowed_clients=[self.client.cert()], # Depending on the self.server.host, the client may connect to it # from localhost or from another IP; since it is unpredictable we # should allow both. allowed_hosts=["localhost", self.server.host], ) key = self.client.call(self.server.cert_path(), server_url + "/k1") self.assertEqual(key, self.server.keys["k1"]) # Unknown key -> 404. self.assertClientFails(server_url + "/k2", "404 Not Found") # No certificates allowed -> 403. self.server.new_key("k3", allowed_hosts=["localhost", self.server.host]) self.assertClientFails( server_url + "/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( server_url + "/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(server_url + "/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( server_url + "/k1", "certificate signed by unknown authority", cert_path=self.client.cert_path(), ) class WildcardHostnamesKxgencert(Simple): """Tests for certificates with DNSNames='*'. In kxd <= 0.16, by default kxgencert (and before than, the equivalent scripts) would generate certificates with DNSNames='*', and everything worked okay. But in Go 1.23, the Go TLS library started to reject such certificates (Go commit 375031d8dcec9ae74d2dbc437b201107dba3bb5f). We changed the defaults since then, but we still want to make sure that the server can handle such certificates, as they might still be in use in the wild. This test uses static certificates generated with the old defaults, to be absolutely sure they survive even through changes in the key generation code. """ _cert_dir = "wildcard_test_certs_kxgencert" def setUp(self): test_dir = os.path.dirname(os.path.realpath(__file__)) self.server = StaticConfig(test_dir + "/" + self._cert_dir + "/server") self.client = StaticConfig(test_dir + "/" + self._cert_dir + "/client") self.daemon = None self.ca = None # pylint: disable=invalid-name self.launch_server(self.server) def test_minimal(self): self.server.new_key( "k1", allowed_clients=[self.client.cert()], allowed_hosts=["localhost"], ) key = self.client.call(self.server.cert_path(), "kxd://localhost/k1") self.assertEqual(key, self.server.keys["k1"]) # Reuse the rest of the Simple test cases. class WildcardHostnamesOpenSSL(WildcardHostnamesKxgencert): """Tests for certificates with DNSNames='*' generated with OpenSSL.""" _cert_dir = "wildcard_test_certs_openssl" class SpecificHostnames(Simple): """Tests for certificates with specific hostnames. These tests check that the server ignores the client certificate's hostname, and that the client validates the server certificate's hostname in scenarios that it is different than `localhost`. """ def setUp(self): self.server = ServerConfig(host=socket.gethostname()) self.client = ClientConfig(host="client-" + os.urandom(8).hex()) self.daemon = None self.ca = None # pylint: disable=invalid-name self.launch_server(self.server) def test_minimal(self): self.server.new_key( "k1", allowed_clients=[self.client.cert()], allowed_hosts=["localhost", self.server.host], ) key = self.client.call( self.server.cert_path(), "kxd://" + self.server.host + "/k1" ) self.assertEqual(key, self.server.keys["k1"]) class CertFor127_0_0_1(Simple): """Tests for server certificate for 127.0.0.1 (instead of a name).""" def setUp(self): self.server = ServerConfig(host="127.0.0.1") self.client = ClientConfig(host="client-" + os.urandom(8).hex()) self.daemon = None self.ca = None # pylint: disable=invalid-name self.launch_server(self.server) def test_minimal(self): self.server.new_key( "k1", allowed_clients=[self.client.cert()], allowed_hosts=["localhost"], ) key = self.client.call( self.server.cert_path(), "kxd://" + self.server.host + "/k1" ) self.assertEqual(key, self.server.keys["k1"]) class CommonErrors(TestCase): """Simple test cases for common errors.""" def test_invalid_schema(self): self.assertClientFails("invalidschema://a/b", "unsupported URL schema") def test_invalid_url(self): self.assertClientFails( " http://invalid/url", "first path segment in URL cannot contain colon", ) def test_empty_server_cert(self): self.assertClientFails( "kxd://localhost/k1", "Failed to load server certs: error appending certificates", cert_path="/dev/null", ) def test_unknown_server_cert_file(self): self.assertClientFails( "kxd://localhost/k1", "open /does/not/exist: no such file or directory", cert_path="/does/not/exist", ) def test_bad_server_cert_file(self): test_dir = os.path.dirname(os.path.realpath(__file__)) self.assertClientFails( "kxd://localhost/k1", "x509: malformed certificate", cert_path=test_dir + "/bad_cert/cert.pem", ) 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.terminate() 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: # Expect one of these errors (the specific one can change # depending on the version of OpenSSL). self.assertIn( err.reason, [ "SSLV3_ALERT_BAD_CERTIFICATE", "TLSV13_ALERT_CERTIFICATE_REQUIRED", ], ) 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_invalid_path(self): 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() self.assertEqual(response.status, 406) def test_server_cert(self): rawsock = socket.create_connection(("localhost", 19840)) context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) context.load_verify_locations(self.server.cert_path()) context.check_hostname = False sock = context.wrap_socket(rawsock) # 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") EMAIL_TO_FILE = textwrap.dedent( """ # Comment. me@example.com you@test.net """.strip() ) 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) self.assertNotIn("EMAIL_TO=", 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") def test_email_to(self): self.write_hook(exit_code=0) self.server.new_key( "k2", allowed_clients=[self.client.cert()], allowed_hosts=["localhost"], email_to=EMAIL_TO_FILE, ) key = self.client.call(self.server.cert_path(), "kxd://localhost/k2") self.assertEqual(key, self.server.keys["k2"]) hook_out = read_all(self.server.path + "/data/hook-output") self.assertIn("CLIENT_CERT_SUBJECT=O=kxd-tests-client", hook_out) self.assertIn("EMAIL_TO=me@example.com you@test.net", hook_out) class Emails(TestCase): """Tests for email notifications.""" def setUp(self): self.smtp_addr, self.emails = receive_emails() self.server = ServerConfig() self.client = ClientConfig() self.daemon = None self.ca = None # pylint: disable=invalid-name self.launch_server(self.server, smtp_addr=self.smtp_addr) def test_emails(self): self.server.new_key( "k1", allowed_clients=[self.client.cert()], allowed_hosts=["localhost", self.server.host], email_to=EMAIL_TO_FILE, ) key = self.client.call( self.server.cert_path(), "kxd://" + self.server.host + "/k1" ) self.assertEqual(key, self.server.keys["k1"]) self.assertEqual(len(self.emails), 1) self.assertEqual(self.emails[0][0], ["me@example.com", "you@test.net"]) self.assertRegex( self.emails[0][1].decode().replace("\r\n", "\n"), textwrap.dedent( """\ Date: .* From: Key Exchange Daemon To: me@example.com, you@test.net Subject: Access to key k1 Key: k1 Accessed by: .* On: .* Client certificate: Signature: .* Subject: O=kxd-tests-client Authorizing chains: .* """ ), ) def test_no_emails(self): self.server.new_key( "k2", allowed_clients=[self.client.cert()], allowed_hosts=["localhost", self.server.host], ) key = self.client.call( self.server.cert_path(), "kxd://" + self.server.host + "/k2" ) self.assertEqual(key, self.server.keys["k2"]) # Note that we did not set up email_to, so we don't expect any emails. self.assertEqual(self.emails, []) if __name__ == "__main__": unittest.main() kxd-0.17/tests/wildcard_test_certs_kxgencert/000077500000000000000000000000001467657372700215235ustar00rootroot00000000000000kxd-0.17/tests/wildcard_test_certs_kxgencert/client/000077500000000000000000000000001467657372700230015ustar00rootroot00000000000000kxd-0.17/tests/wildcard_test_certs_kxgencert/client/cert.pem000066400000000000000000000021061467657372700244400ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIC+zCCAeOgAwIBAgIINanW2Zy8uCMwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UE ChMQa3hkLXRlc3RzLWNsaWVudDAeFw0yNDA4MTUxOTAxMTdaFw0zNDA4MTMxOTAx MTdaMBsxGTAXBgNVBAoTEGt4ZC10ZXN0cy1jbGllbnQwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQDEH+gVG8EbdLsAlaO27nYxtEUH+hU6R971qg3zOWyn 5+PyZ2HCz5GTSHMDNhCXBxpCZVEChxoyBbclZqo5OhPeJ3u8I/nsQD9WFLdHal8R 8YXxUbp8WhvO67L5ojlQNUcxsXdF2jTgPYOfgHk0BbReafJ4xOa+xw/ClSdZ6bCs jIWwlkZ6VtRXhOy4vdi5cxvDYCnOr1tZfUs3iEhcyeIidruKEEVOu/EZ8igdJt5M DWDBRJk6BOQbCX0gTIRJodIQGrM2jlFaSS1buQF1ONFv+/H3+UClU7MUebObeYYS 9gq+BiV6QRiIQ/hkMT/Ntzmj8i5/6/J075AGw5cm8f4rAgMBAAGjQzBBMA4GA1Ud DwEB/wQEAwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMAwG A1UdEQQFMAOCASowDQYJKoZIhvcNAQELBQADggEBAIrJ5T/PGBtvMfVcFZdut66s wOG4U9sW9EX9d4fTbHkr6ebG0XDf9IjjKCm3hSNsRxz07N7hv6ls3cOsYBsFcm2+ 1D73uYabNUfo7fE91AhMruYjXhTWB32zHv8iAGzgpLH+hhZxkUgpNPH8vsNc8onj SFl3beGuaM7dCkSIUWbYtfF8xRi+mZZxJMgcIFLJbSZmdglU+9HtOuvPiWQieiys tye4Vi/GHRHj+5Q5H06TDUdkhWd85TEzhjsMQ9+iuZ/QxJJtqcYxPNFgF4k+8Wim CR9ev4ecm/1mreDO9fMcx03D2vQk11ss5r5wfMSs1SwtVuuspM15PJ+uTaqEFIg= -----END CERTIFICATE----- kxd-0.17/tests/wildcard_test_certs_kxgencert/client/key.pem000066400000000000000000000032131467657372700242730ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEAxB/oFRvBG3S7AJWjtu52MbRFB/oVOkfe9aoN8zlsp+fj8mdh ws+Rk0hzAzYQlwcaQmVRAocaMgW3JWaqOToT3id7vCP57EA/VhS3R2pfEfGF8VG6 fFobzuuy+aI5UDVHMbF3Rdo04D2Dn4B5NAW0XmnyeMTmvscPwpUnWemwrIyFsJZG elbUV4TsuL3YuXMbw2Apzq9bWX1LN4hIXMniIna7ihBFTrvxGfIoHSbeTA1gwUSZ OgTkGwl9IEyESaHSEBqzNo5RWkktW7kBdTjRb/vx9/lApVOzFHmzm3mGEvYKvgYl ekEYiEP4ZDE/zbc5o/Iuf+vydO+QBsOXJvH+KwIDAQABAoIBAG6CjANY4DbYT4bE yrsJIxBew4b7I3rzhG5oo/OpJ9B0mby9BBkBXMXgzO3CSRbQqbs/26XQ+rG1Br3M W55jW06ScOZSX0D/8rBOe/eBuJAjx5Vyt+HZ5FFz/iUrg5/uZW9a0BpMGf9Aqin1 +lWV4UxR5o6mZF6bTAYYhVPkmeifSNAhcPnKCR3TVGUM/BpOUI1oH49TYemOoL3B iiR1oBaqAXAiCvZTC9uzHNm/zsvj5URCdFDeHwpxk5nHxTjvp8Lh0ATbOaFysAy3 GXA9luBK5Pe16js46mGAb7uV7bCYFl7ekXnRrMrGYGc11N7Pg/n1x1Vv9km7CXws HiRN0gECgYEA1CPjiGC15xT2ovYXH85Nt86sNbjf5wrRTolMh3IJq+fjh1TMzcpf gne3ARhBAF1A9R83s9V0zgvkoHgn4MONXUsyWtybnNoii3ckQ0wkzzvp28ixUvCN v4FOieHQLIFLCTgppbN2hliLJh/VuvfevWVIg3X9H4kLuC/znWCNQS0CgYEA7KxZ jnAt/rsJuZ57dMOWOvIXd+prut9qsg63e+JSl8B+oLoerQheMRUmWfPW8dEqI3q7 T72dEHQvvtoWq4FGzu2oxMiRG3TAd03ncgT7HQ90i72Dltlaw+sptmXEklBoaBMG wbcE3k0X6Xc506qOmCFsbp8RWWYAjHm2gPtUY7cCgYBuRfWZx8PmyiPm1AtzMhd+ K4WjK2XgQORKcd6BLctPO+wvRepsMv1w4XAUtpnbaZ5BjSe0aIoeLVp7+9mm4aAT VepoBvMxFscMPjNwdB1SSC+pWuqqVXcpjDraO7Kt38u0kCg+BUrgTRiQCc5dMUns o8CM7YFVqjSYWvzE2xKyRQKBgDMdPfI/VA+xwXXvPmaHX0i3xE3HuSCQ4/A0sXf1 9zSDBFYeHEXuirk7Ah9nRELRk7I57X5ZSzSkgzNK0p6TuwEx3sMxNfWiD3c0wgmj /b/W+Kq9cVAA/VNW1Jlp/TxEVWg0w77OkiSYrdNkRn7qVQWSImL5w7t1BiVQnBPb M+ydAoGAHh3nUquGdIsR1/QcAs6i12XcxzmFokQIut5FTnd3OZxX+D7nMcupDdAg NA5yGzW1/uzH5nM28wdv9VU6Nxaam/cXPMu8luQaMheWkhcrBFr547w3qs+iQkTI FY2+8x2DCMQQGvbLK9JmeJnhvMkjB3y8iXrL6H60pjN59NlSdas= -----END RSA PRIVATE KEY----- kxd-0.17/tests/wildcard_test_certs_kxgencert/server/000077500000000000000000000000001467657372700230315ustar00rootroot00000000000000kxd-0.17/tests/wildcard_test_certs_kxgencert/server/.gitignore000066400000000000000000000000121467657372700250120ustar00rootroot00000000000000log data/ kxd-0.17/tests/wildcard_test_certs_kxgencert/server/cert.pem000066400000000000000000000021061467657372700244700ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIC+zCCAeOgAwIBAgIIJua2egAlPEQwDQYJKoZIhvcNAQELBQAwGzEZMBcGA1UE ChMQa3hkLXRlc3RzLXNlcnZlcjAeFw0yNDA4MTUxOTAxMTdaFw0zNDA4MTMxOTAx MTdaMBsxGTAXBgNVBAoTEGt4ZC10ZXN0cy1zZXJ2ZXIwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQDy3sYvXUnIm591qIWWbn3iTUyKl7ncWmGSQJ8XWXaz NwbNuINBy6bTf8H6BP20sCWQWYRTKYELGmZLp4fUme33y0xNnFpcbR5tqKI8SQOX wvCk3ox3xUnBEr7t0gHhSDVrhPRMWsWVlgPw4t3MgoDk/J97uAwGnfCXiMWZL3tM fFgrJja73RmzsbjamS/yjp7A6jLLe0MH/o73zaNJsyTeit6uTwU9PBEHzLgH0Udi 97y/MQ/MQs0Wn1JtTbvV5HQSHrlwVkSQY1feSNBERnCs/l5B9CYoPX3o3a3UtyyX jMy6eSt3bio0Du5jYR9VoIi94FJ8SJlNgPXVgnPLr8XjAgMBAAGjQzBBMA4GA1Ud DwEB/wQEAwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMAwG A1UdEQQFMAOCASowDQYJKoZIhvcNAQELBQADggEBACAGsja7xV92lyefwXDX3ZWw hQ5b/t5WGSXdQNG4oYWpU+EfTNU79j2Sr/RcZ/8YD6weYL75ONW2BeOTQ9Iev5du XAVcfgiAjz8zRru+hH7xausFJM1I5e3nhWSzAyBiyJxzSBcBUWhiNrSvStjvigTs XT34FYR9p+7ZGFljQlD5eeh7E5cV6UA9wIE2vXTshDCyZdoDqEoXj3HYw/sJtOdf kRZlOuK6GXJFxoZBIa5cJFSgq618UjwErFG7frsAKr9Da8sXBQMThMDz37mZlRST r3M2b4+Z13YRtFlWmqpg8HbOjBJEgPvWf6GfXiqWf6d9IxptMFk2u80IIDi38A8= -----END CERTIFICATE----- kxd-0.17/tests/wildcard_test_certs_kxgencert/server/key.pem000066400000000000000000000032171467657372700243270ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA8t7GL11JyJufdaiFlm594k1Mipe53FphkkCfF1l2szcGzbiD Qcum03/B+gT9tLAlkFmEUymBCxpmS6eH1Jnt98tMTZxaXG0ebaiiPEkDl8LwpN6M d8VJwRK+7dIB4Ug1a4T0TFrFlZYD8OLdzIKA5Pyfe7gMBp3wl4jFmS97THxYKyY2 u90Zs7G42pkv8o6ewOoyy3tDB/6O982jSbMk3orerk8FPTwRB8y4B9FHYve8vzEP zELNFp9SbU271eR0Eh65cFZEkGNX3kjQREZwrP5eQfQmKD196N2t1Lcsl4zMunkr d24qNA7uY2EfVaCIveBSfEiZTYD11YJzy6/F4wIDAQABAoIBAFhcvPZWlpWqKSYB njjREnPXc9WoxjJpn746TKeOISWrC4qlavvxQE0K2mRAlJ28yK3wI7iuDQkhHb7A wSaUqoPRL329ORMPkFaNWBle5zO79RnG7oxU9zSLwXN9SLnSL71irg9ppyF5Zw2p PNE/We32C4BTwexWYkZ/uIS3RcDZdzLN3ouPkIIvnG7E4oNZdMkMknFVEhXYg8c0 yaTN4FOXr5J55n8WReT24n5574BUsPAMHd56oV6TVLV+3P7ZNLlFkS6Mh83fOgRr uu3DfXWCJXVeliRBOzk9ZCfJmPp3SbNsqiqyhkLrlaGkqzSDbWBPoQZ8q2+G2KEH RoJK/EECgYEA9Rz8uUL5douMzLi+l0wgUbnvUBVNNsO3nKIbRDofhW1HVUieieDw gawfqXpcRK8MPhvKOlw+r4Z/sNFjKClwEzuLWRAN2AM6hxweBfbMG1deqXcNnqsh z0YMgTIB+8kSDVKHhlgsAW+BACzfnUlKEq6sQYnSjJ8NXRsAapxVLCkCgYEA/ahI e4Y5A0svzXGBFVy8dfh+IipEg8vqQIrFjxmhkW0kEP80caTohDNMb0B+WfzBAfVq uKLQy/71jVRqh1RvW7N24IZxf3b3g82oaeqMNQdLgCVuB07RvC4rbnV3jiCLy53Q xj18IDTAg8euVZYqYOPRO8jJFgqht/6T/gCR4ysCgYEAx6+w2AEPGJuBvnq3kqzL G6mdpCFmgTobSLjr+75aVan8VidOOUeOyCWAueJcbvwoviSHsNnbm8W4G0XKQTrY +mIlGQ6yKIwgz1eBwhwYliGsleTOrLgAWEtZ/prN8OETGtVkYXdNinHbp1fnaMz/ Uo8I+G+enz8odPR9d10bKlkCgYEAhwsvw6u68Lj3sy3JpmDf6QKpzHCqV2yZw1De Swg/T1hGylHETviX3cId4GD6o9f/vZY2AiSUeva7LkoSCQh33N9X28NX65+fuOkl z3XSvWyr0oaa5IMfAEuoTb92LhG/u2DCxLf5PIA5Oi917hTrbPf87hJAlF6GqJl9 ms4c3U8CgYArvKICxvSN+XZdW0LXW4MPNkb4JhR1MY/A+WJG3CwS5DWD0K4iuXPW NtavHNtc57tiMukoHrdzFcYwOslrVEkLljlrKxmmXrxUuCTbd+H28T3PBEwaTC2e O9Rf3esazCnN4fsZ8daxQd314+yY5r9WJwoC982zCL1BP8y9dV7mvQ== -----END RSA PRIVATE KEY----- kxd-0.17/tests/wildcard_test_certs_openssl/000077500000000000000000000000001467657372700212145ustar00rootroot00000000000000kxd-0.17/tests/wildcard_test_certs_openssl/client/000077500000000000000000000000001467657372700224725ustar00rootroot00000000000000kxd-0.17/tests/wildcard_test_certs_openssl/client/cert.pem000066400000000000000000000021671467657372700241400ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDHzCCAgegAwIBAgIUHpvfYnyEmyzzTaltXOgnEq8+r5swDQYJKoZIhvcNAQEL BQAwHzEKMAgGA1UEAwwBKjERMA8GA1UECwwIa3hjQHRlc3QwHhcNMjQwODE1MjMz NTE3WhcNMzQwODEzMjMzNTE3WjAfMQowCAYDVQQDDAEqMREwDwYDVQQLDAhreGNA dGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALBubPX4RwvOatpO c1eNHR5Wjz2x+NMzIlir+hkURTi/7VZ3q9S27J0y1wBk7ckLOdTVpAJkcopfOhNd dhUtWrUexi7ATsRuu+/JmVJ3hNhdFM76hT0Ne5op3fh6T3MuNgtu267rQ5S/4KJX afSOkslTTym2WnoTvkSx2gOu64ZE619jcI+8cbsM/D/K5+lNfdipuhuKAGFO6dKP xLkCIli1mxCSy6oIDBDhBlDB31tfAQkAJiIfC1UgXnEZFMbpQ4rnfJEhcHbGjKAq RMsSDw1ZoD40TBXFncRl2NtKjqAxHRV/rM/N8c6EiWDzBdW2LreN7hIRFkyRZ8MJ Nv+ujgMCAwEAAaNTMFEwHQYDVR0OBBYEFFh9Dlf/zK7Xp+SOybexQ3O2acndMB8G A1UdIwQYMBaAFFh9Dlf/zK7Xp+SOybexQ3O2acndMA8GA1UdEwEB/wQFMAMBAf8w DQYJKoZIhvcNAQELBQADggEBAAwZWshJJ6yu2N1ymPe66Dq/xHVIBDXqLCMB8Vhm rouj8ThkJuAwDxFwY516hQis918o6DPADiJPctnpJTgbgZYNTr02f44YngJbBXWp xGVH3FDKnzzJh0QJIXenMVDkJczf2bi7XK5FX4a/WCpa4z4CmXTHQSRYyNV3p7YM tuvqbzEOfGaFX7yoqFKk31+hSL5JxT7hzRtA7qzXEKHXUxMz+8wLifJPo90c2ayA dzfgml5g/BNjqfIXNT7N+d9RkLcjKNuWZnt7HsvBPx/QIkrTVfZipZBedi4AA1r8 3iljopAaqt5fs1adqJrjOe/TWi3VrqIO9wfb+qI5ZCtGhNA= -----END CERTIFICATE----- kxd-0.17/tests/wildcard_test_certs_openssl/client/key.pem000066400000000000000000000032541467657372700237710ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCwbmz1+EcLzmra TnNXjR0eVo89sfjTMyJYq/oZFEU4v+1Wd6vUtuydMtcAZO3JCznU1aQCZHKKXzoT XXYVLVq1HsYuwE7EbrvvyZlSd4TYXRTO+oU9DXuaKd34ek9zLjYLbtuu60OUv+Ci V2n0jpLJU08ptlp6E75EsdoDruuGROtfY3CPvHG7DPw/yufpTX3YqbobigBhTunS j8S5AiJYtZsQksuqCAwQ4QZQwd9bXwEJACYiHwtVIF5xGRTG6UOK53yRIXB2xoyg KkTLEg8NWaA+NEwVxZ3EZdjbSo6gMR0Vf6zPzfHOhIlg8wXVti63je4SERZMkWfD CTb/ro4DAgMBAAECggEARopwIEw7Q7otOMmjDj9KhCXBsb4lqXPJaMCiB0L2hkEe iLLiTfMxWYzJL8wq0nYdkj41DAvTTAXU94cnvkHbAY2jWW/kTl+j3rSxC6rjv4o/ 1p2NhiKM58+Tg1SLzZaNXzbcuOwxohixnNbscy5J/BrGDPxu4l5gdaDSdSSL4Kuk h6bHLzjZQVq4/Sx93i/uu8cXveIJRts0RAEs8u2aFZRU5B2+Gzh+T2WSqnAg5L4m HHDsOdid3TpdQN5/Eipwo/1JbN8454GsxB9h4WQlB81ISXet/Peh+6dmVArXa5z9 6QCgEKH8vGDu9urVndZU5nIzBOi1F7FT5mNJZK6YaQKBgQDuigAw0odw1FVrsZF9 +aXsDcOZJmvpQi9dRK3D4z5dWZvtHGGZ/6AH1V8Q8PJDVQ+gSy554lbn3I/0iaX1 nVpVrK1DlEtkQoOzF6X3nKWbtYxViA8rnL3bfUwfJ3QwMiGAqZU1F5tzkmoZnsAx 6hxhixTFmS6O50F5VIfhPyi7dwKBgQC9WJWVwMhRm7HEjUWbMHDH5CVfsynNhrLg prkqZ6IhuHhGzxb6sOC82FW1yrSGLjM2eKNi7bJkxjj8970PiyoEni/GSat7hlUJ 4Ge2/cT4pMaUPfTUdPNAnnL+L1syD3wNL3vmvel/r9mmMCGRqguiALkJ67Z+/WBP Rf9b1EcM1QKBgQC8H+4CkybqHyu3IXWjKo5m2nwWfqzAa8g7AH0ibkezC4bju2xm LaoiQ28UR1JpM959Bo4C5jSv681EiIJwcMbbprGHCJ9k1OhVCCOGYu5hHQ8uLX35 YUaCohC0yULi98ZgWF4qXxHkVeaDiiX9t6rmau/Y3vRPE6cZb0cyp8MSBQKBgQCZ 2Ze51aou/UZFgdC0F3kcQpnHl+l4kWFZPr8n4IsRwTUhu/Vc/0msyE9kZm+ms3Vz ZjTEFoWkcpgtnBLnxVj/5ZTGFmga93yziL5dJvfcXO7p1ynPU7Ovps+jD9GW7JQM lq+jPl6zHKzJ50Pveu721IWFtRxVNQYDg8nI0MRmfQKBgQDszoZa8EUh6cFhjwC0 Fj94xqVXVmGqigTpUGbP344eo565UziA8O7IDJNFZdwmNuhzN/AbeRcdiyS+SY7g XVMiPw2+k4dNLt+P5aRqneuArgZ/Y0gy/X5rsDaLRVG8pjyn6bbd0AFcZIJtGr1M +sOI8ta2NYMq+MYDLt4G4kLF3w== -----END PRIVATE KEY----- kxd-0.17/tests/wildcard_test_certs_openssl/server/000077500000000000000000000000001467657372700225225ustar00rootroot00000000000000kxd-0.17/tests/wildcard_test_certs_openssl/server/.gitignore000066400000000000000000000000121467657372700245030ustar00rootroot00000000000000log data/ kxd-0.17/tests/wildcard_test_certs_openssl/server/cert.pem000066400000000000000000000021671467657372700241700ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDHzCCAgegAwIBAgIUdtiBDx/LuGniRXGCkt+H6+bSwGcwDQYJKoZIhvcNAQEL BQAwHzEKMAgGA1UEAwwBKjERMA8GA1UECwwIa3hkQHRlc3QwHhcNMjQwODE1MjM0 MTI5WhcNMzQwODEzMjM0MTI5WjAfMQowCAYDVQQDDAEqMREwDwYDVQQLDAhreGRA dGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM+2PlECOw8qSqUj 1uK0LgkGVMzijC9tmTEAL+eBjrcIdS3E0Ckg5nQiJ2wdfR7D2Qfvw/MmOTnFYnJ1 X/7gqpbqe/Cgrit7DCQW3vf5WbULk4mKnT5i8no2btFZJ2la4IANJ41H19tKKq4Q 9YPhJWPCTp2LqOPDQF7mZ+4MN/XyNeqKVvqTIR55b2Y2+y67NVkZVTTilRGbssqS JP84V8WZCn3zPriQFh1iIcQegE7j6/8VMZmDq+GSNKAcDDM7HGP0BBeRoQGAqUa1 LYXSY9V0ygajeYUPwT13hM/lpr4q42Tfdl51HSN24I4SXI8qxpVfn94noCWa6TNb I0UU4pECAwEAAaNTMFEwHQYDVR0OBBYEFHZkBSlLn+r9qzQKrLMzZPGAYvLoMB8G A1UdIwQYMBaAFHZkBSlLn+r9qzQKrLMzZPGAYvLoMA8GA1UdEwEB/wQFMAMBAf8w DQYJKoZIhvcNAQELBQADggEBAEPSdGoT7yDw2nX9M1awsdxFQQsRRlQtznZSfl24 QvMSvkFmOo7zEqdq7gIFbnEsv+BcuoFIBj7nrTg3SxAFRBfQamtjcBwLazwUe88n rWmBBhVmLTiGmZSD7n+m2NoJM1gR4U/sG1S+o8ez2Q91tZD/ttoqS+bO0Ww6eTQq 0DoduTfwx0gf3G//aUzug3lS6mQ56DOAZwq4kmz2qU+o+xC1iZXvw4XNCFW4Mk4E SWeLFExiGmqb/DC8bjB/Y9rpqgRSbZA+VkHeqgO3Hk2Rean3VUFb7jsJdCULhw26 mAjQHX7F7LEZgmTqAKnLAcdT8BPRQtaStsOX6ue089XUNVU= -----END CERTIFICATE----- kxd-0.17/tests/wildcard_test_certs_openssl/server/key.pem000066400000000000000000000032501467657372700240150ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDPtj5RAjsPKkql I9bitC4JBlTM4owvbZkxAC/ngY63CHUtxNApIOZ0IidsHX0ew9kH78PzJjk5xWJy dV/+4KqW6nvwoK4rewwkFt73+Vm1C5OJip0+YvJ6Nm7RWSdpWuCADSeNR9fbSiqu EPWD4SVjwk6di6jjw0Be5mfuDDf18jXqilb6kyEeeW9mNvsuuzVZGVU04pURm7LK kiT/OFfFmQp98z64kBYdYiHEHoBO4+v/FTGZg6vhkjSgHAwzOxxj9AQXkaEBgKlG tS2F0mPVdMoGo3mFD8E9d4TP5aa+KuNk33ZedR0jduCOElyPKsaVX5/eJ6Almukz WyNFFOKRAgMBAAECggEAAwNnEIhKgIwRxKcKM0Q/ZbieitFfjMGXhxc0WrSttBiO kKEqDuqwBS/IqCAeZE+diqivDEw56M1lAfmTcLBkMSfKMAG4vR8+HBjr16eZpk96 brSg1tqbH2xCO83CDVx45MqsD/fimQcQqvFKioT95J5ZQx5XTySSPr4zdEffmPUJ m4kPpSO1geLAnGYSbR9Y043cwUxoLsvs0Q5jv+M1G9/HsfTyu+yF1VurOgDleoId /9pUZDOJ6V1p5zh0PjU4j1RG+wAJXALeLI8kYODAyUGc2S9Bqaf3cIYCzrrBK3xE AAygYEe2l85G3uiuoCKxJVP9rFtP0aooq13jIQbuNQKBgQD+iWB2N7ZpZEjyibkS xhWb1UFsetylmSkIqPOoXNcSa+1Uy01a/6G1slc4WsXowQfYR3nhkyAhVKg3Tazn McPhwOo5CLVKFoqo7KJQgNtDU6syu/J42ShQmHeyC7RDHcVt/LTlh0G4vkI4FtX+ QY2XxXBF6Ctq5v12o626lUfm/QKBgQDQ5/NfDtVGAZu4zGvqG0uc6EtWvmwgBG9E sTbfpdMaHKVL0H9Vc8vow36JX56az5nWMvlrwCFoPuAW3Zz+/w1WW3VqDKJ8d4V3 uLN+khcOu7Sx75XqaeZ4ytgEpknIzdn8OM89MWEQ61Uw7l6fYPuM6eHA3BKk3BJI wGX9EpqAJQKBgQD7HG6U0kvcV7p7xJFYSyGgVlgv/FnX6W0JBR00uTrZCq7eW59a Kh3QEjxn9W2QPXdO0N0WRL6LA2jc/n2YrIjyHA826zdm+ywakTFkuGsYVd/ssmz4 +kwCjxhvB4r0N9fBtXCFjNWyu8i6axT8vJFC7N7hqLXExlPCCqJnE8UWxQKBgQDA JunjYKhpaSdMFrOYNR0aqUxK8IJR/OI/w+VeV4/SL9EW6COHfShs5Ayq3QntCdFN hbuIEcRot5S1U4iJwB4LdbqNHiwC4okgcwKfBE8zHRJ6rI4vfNMh/iouNKofisDb z4FHnvjScDP++vKMFM+scKBXHdYET+x9gIMPAaKdBQKBgC2pJ1WsCu/bGnqAiMoW gY4T8ypC7BYGHfSM682nowhQxJaQyhPx47OwjGNn9I4nK9ZHh+/DZRCbFqLdWiG9 hOyFPXvLyphyx8JpxvexeWZIWCKliilV2MP/b6cHZZ2VvjLv7MW19g53J/QmZDh2 ELZARJIIBOCzOq1j5i3BbykD -----END PRIVATE KEY-----