From 40a0f31f1838d4774ebd960640bfb230dc562ea1 Mon Sep 17 00:00:00 2001 From: Christian Hesse Date: Sat, 16 Jan 2016 02:13:22 +0100 Subject: We have support for second factor. Yeah! --- .gitignore | 1 + Makefile | 12 ++- README-mkinitcpio.md | 16 ++++ README.md | 3 - bin/Makefile | 6 +- bin/ykfde.c | 230 +++++++++++++++++++++++++++++++++------------- config.def.h | 2 + mkinitcpio/ykfde-2f | 14 +++ systemd/ykfde-2f | 20 ++++ systemd/ykfde-2f.service | 16 ++++ udev/Makefile | 2 +- udev/ykfde.c | 234 +++++++++++++++++++++++++++++------------------ 12 files changed, 396 insertions(+), 160 deletions(-) create mode 100644 mkinitcpio/ykfde-2f create mode 100644 systemd/ykfde-2f create mode 100644 systemd/ykfde-2f.service diff --git a/.gitignore b/.gitignore index 7a94410..5bcbdc8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ config.h bin/ykfde bin/ykfde-cpio udev/ykfde +version.h *.html diff --git a/Makefile b/Makefile index 77065ac..a7b2c46 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ VERSION := 0.5.2 all: bin/ykfde bin/ykfde-cpio udev/ykfde README.html README-mkinitcpio.html README-dracut.html -bin/ykfde: bin/ykfde.c config.h +bin/ykfde: bin/ykfde.c config.h version.h $(MAKE) -C bin ykfde bin/ykfde-cpio: bin/ykfde-cpio.c config.h @@ -24,6 +24,11 @@ udev/ykfde: udev/ykfde.c config.h config.h: config.def.h $(CP) config.def.h config.h +version.h: $(wildcard .git/HEAD .git/index .git/refs/tags/*) Makefile + echo "#ifndef VERSION" > $@ + echo "#define VERSION \"$(shell git describe --tags --long 2>/dev/null || echo ${VERSION})\"" >> $@ + echo "#endif" >> $@ + %.html: %.md $(MD) $< > $@ $(SED) -i 's/\(README[-[:alnum:]]*\).md/\1.html/g' $@ @@ -35,6 +40,8 @@ install-bin: bin/ykfde udev/ykfde $(MAKE) -C udev install $(INSTALL) -D -m0644 conf/ykfde.conf $(DESTDIR)/etc/ykfde.conf $(INSTALL) -D -m0644 systemd/ykfde-cpio.service $(DESTDIR)/usr/lib/systemd/system/ykfde-cpio.service + $(INSTALL) -D -m0644 systemd/ykfde-2f.service $(DESTDIR)/usr/lib/systemd/system/ykfde-2f.service + $(INSTALL) -D -m0755 systemd/ykfde-2f $(DESTDIR)/usr/lib/systemd/scripts/ykfde-2f $(INSTALL) -d -m0700 $(DESTDIR)/etc/ykfde.d/ install-doc: README.html README-mkinitcpio.html README-dracut.html @@ -48,6 +55,7 @@ install-doc: README.html README-mkinitcpio.html README-dracut.html install-mkinitcpio: install-bin install-doc $(INSTALL) -D -m0644 mkinitcpio/ykfde $(DESTDIR)/usr/lib/initcpio/install/ykfde $(INSTALL) -D -m0644 mkinitcpio/ykfde-cpio $(DESTDIR)/usr/lib/initcpio/install/ykfde-cpio + $(INSTALL) -D -m0644 mkinitcpio/ykfde-2f $(DESTDIR)/usr/lib/initcpio/install/ykfde-2f $(INSTALL) -D -m0644 udev/20-ykfde.rules $(DESTDIR)/usr/lib/initcpio/udev/20-ykfde.rules install-dracut: install-bin install-doc @@ -59,7 +67,7 @@ install-dracut: install-bin install-doc clean: $(MAKE) -C bin clean $(MAKE) -C udev clean - $(RM) -f README.html README-mkinitcpio.html README-dracut.html + $(RM) -f README.html README-mkinitcpio.html README-dracut.html version.h distclean: clean $(RM) -f config.h diff --git a/README-mkinitcpio.md b/README-mkinitcpio.md index 8f25819..8043277 100644 --- a/README-mkinitcpio.md +++ b/README-mkinitcpio.md @@ -13,6 +13,7 @@ To compile and use yubikey full disk encryption you need: * [iniparser](http://ndevilla.free.fr/iniparser/) * [systemd](http://www.freedesktop.org/wiki/Software/systemd/) * [cryptsetup](http://code.google.com/p/cryptsetup/) +* keyutils and linux with `CONFIG_KEYS` * [mkinitcpio](https://projects.archlinux.org/mkinitcpio.git/) * [markdown](http://daringfireball.net/projects/markdown/) (HTML documentation) * [libarchive](http://www.libarchive.org/) (Update challenge on boot) @@ -89,4 +90,19 @@ Additionally enable `systemd` service `ykfde-cpio.service` and make your bootloader load the new `cpio` image `/boot/ykfde-challenges.img` (in addition to your usual initramfs). +### Optional `ykfde-2f` hook for second factor + +This gives the option to add a second factor for authentication. +With this you need your Yubikey and an additional passphrase to boot +your systemd. + +Add a second factor with `ykfde`: + +> ykfde -s xyz + +Add `ykfde-2f` to your hook list in `/etc/mkinitcpio.conf` and rebuild +your initramfs with: + +> mkinitcpio -p linux + Reboot and have fun! diff --git a/README.md b/README.md index a27d499..61e6e1d 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,6 @@ distributions. Please look at what matches best for you. Limitation / TODO ----------------- -* [systemd password agents](http://www.freedesktop.org/wiki/Software/systemd/PasswordAgents/) - do not support nested queries. That is why we can not ask for a - password ourselfs, breaking two factor authentication (2FA). * When using your additional initramfs `grub-mkconfig` does not know about that. Regenerating `grub` configuration file `grub.cfg` will overwrite our changes. diff --git a/bin/Makefile b/bin/Makefile index 6e77de8..25faea9 100644 --- a/bin/Makefile +++ b/bin/Makefile @@ -3,12 +3,12 @@ CC := gcc INSTALL := install RM := rm # flags -CFLAGS += -std=c11 -O2 -fpic -pie -Wall -Werror +CFLAGS += -std=gnu11 -O2 -fpic -pie -Wall -Werror all: ykfde ykfde-cpio -ykfde: ykfde.c ../config.h - $(CC) $(CFLAGS) -lykpers-1 -lyubikey -liniparser -lcryptsetup $(LDFLAGS) -o ykfde ykfde.c +ykfde: ykfde.c ../config.h ../version.h + $(CC) $(CFLAGS) -lcryptsetup -liniparser -lkeyutils -lykpers-1 -lyubikey $(LDFLAGS) -o ykfde ykfde.c ykfde-cpio: ykfde-cpio.c ../config.h $(CC) $(CFLAGS) -larchive $(LDFLAGS) -o ykfde-cpio ykfde-cpio.c diff --git a/bin/ykfde.c b/bin/ykfde.c index 075a7a2..cace8b2 100644 --- a/bin/ykfde.c +++ b/bin/ykfde.c @@ -5,17 +5,11 @@ * of the GNU General Public License, incorporated herein by reference. * * compile with: - * $ gcc -o ykfde ykfde.c -lykpers-1 -lyubikey -lcryptsetup -liniparser + * $ gcc -o ykfde ykfde.c -lcryptsetup -liniparser -lkeyutils -lykpers-1 -lyubikey */ -#ifndef _XOPEN_SOURCE -# define _XOPEN_SOURCE -# ifndef _XOPEN_SOURCE_EXTENDED -# define _XOPEN_SOURCE_EXTENDED -# endif -#endif - #include +#include #include #include #include @@ -24,6 +18,8 @@ #include +#include + #include #include #include @@ -31,18 +27,40 @@ #include #include "../config.h" +#include "../version.h" -/* challenge is 64 byte, - * HMAC-SHA1 response is 40 byte */ -#define CHALLENGELEN 64 +#define PROGNAME "ykfde" + +/* Yubikey supports write of 64 byte challenge to slot, returns + * HMAC-SHA1 response. + * + * Lengths are defined in ykpers-1/ykdef.h: + * SHA1_MAX_BLOCK_SIZE 64 + * SHA1_DIGEST_SIZE 20 + * + * For passphrase we use hex encoded digest, that is twice the + * length of binary digest. */ +#define CHALLENGELEN SHA1_MAX_BLOCK_SIZE #define RESPONSELEN SHA1_MAX_BLOCK_SIZE #define PASSPHRASELEN SHA1_DIGEST_SIZE * 2 +const static char optstring[] = "hn:s:V"; +const static struct option options_long[] = { + /* name has_arg flag val */ + { "help", no_argument, NULL, 'h' }, + { "2nd-factor", required_argument, NULL, 's' }, + { "new-2nd-factor", required_argument, NULL, 'n' }, + { "version", no_argument, NULL, 'V' }, + { 0, 0, 0, 0 } +}; + int main(int argc, char **argv) { + unsigned int version = 0, help = 0; char challenge_old[CHALLENGELEN + 1], + challenge_old_no_2f[CHALLENGELEN + 1], challenge_new[CHALLENGELEN + 1], - repose_old[RESPONSELEN], - repose_new[RESPONSELEN], + response_old[RESPONSELEN], + response_new[RESPONSELEN], passphrase_old[PASSPHRASELEN + 1], passphrase_new[PASSPHRASELEN + 1]; char challengefilename[sizeof(CHALLENGEDIR) + 11 /* "/challenge-" */ + 10 /* unsigned int in char */ + 1], @@ -54,9 +72,15 @@ int main(int argc, char **argv) { /* cryptsetup */ const char * device_name; int8_t luks_slot = -1; - struct crypt_device *cd; - crypt_status_info cs; - crypt_keyslot_info ck; + struct crypt_device *cryptdevice; + crypt_status_info cryptstatus; + crypt_keyslot_info cryptkeyslot; + /* keyutils */ + int second_factor = 1; + key_serial_t key; + void * payload = NULL; + char * new_2nd_factor = NULL; + size_t plen = 0; /* yubikey */ YK_KEY * yk; uint8_t yk_slot = SLOT_CHAL_HMAC2; @@ -64,16 +88,47 @@ int main(int argc, char **argv) { /* iniparser */ dictionary * ini; char section_ykslot[10 /* unsigned int in char */ + 1 + sizeof(CONFYKSLOT) + 1]; - char section_luksslot[10 /* unsigned int in char */ + 1 + sizeof(CONFLUKSSLOT) + 1]; + char section_luksslot[10 + 1 + sizeof(CONFLUKSSLOT) + 1]; + + /* get command line options */ + while ((i = getopt_long(argc, argv, optstring, options_long, NULL)) != -1) + switch (i) { + case 'h': + help++; + break; + case 'n': + new_2nd_factor = strdup(optarg); + memset(optarg, '*', strlen(optarg)); + break; + case 's': + payload = strdup(optarg); + memset(optarg, '*', strlen(optarg)); + break; + case 'V': + version++; + break; + } + + if (version > 0) + printf("%s: %s v%s (compiled: " __DATE__ ", " __TIME__ ")\n", argv[0], PROGNAME, VERSION); + + if (help > 0) + fprintf(stderr, "usage: %s [-h] [-n ] [-s <2nd-factor>] [-V]\n", argv[0]); + + if (version > 0 || help > 0) + return EXIT_SUCCESS; + /* initialize random seed */ gettimeofday(&tv, NULL); srand(tv.tv_usec * tv.tv_sec); + /* initialize static buffers */ memset(challenge_old, 0, CHALLENGELEN + 1); + memset(challenge_old_no_2f, 0, CHALLENGELEN + 1); memset(challenge_new, 0, CHALLENGELEN + 1); - memset(repose_old, 0, RESPONSELEN); - memset(repose_new, 0, RESPONSELEN); + memset(response_old, 0, RESPONSELEN); + memset(response_new, 0, RESPONSELEN); memset(passphrase_old, 0, PASSPHRASELEN + 1); memset(passphrase_new, 0, PASSPHRASELEN + 1); @@ -130,30 +185,32 @@ int main(int argc, char **argv) { luks_slot = iniparser_getint(ini, section_luksslot, luks_slot); if (luks_slot < 0) { rc = EXIT_FAILURE; - fprintf(stderr, "Please set LUKS key slot for Yubikey with serial %d!\n", serial); - printf("Add something like this to " CONFIGFILE ":\n\n[%d]\nluks slot = 1\n", serial); + fprintf(stderr, "Please set LUKS key slot for Yubikey with serial %d!\n" + "Add something like this to " CONFIGFILE ":\n\n" + "[%d]\nluks slot = 1\n", serial, serial); goto out40; } + if (payload == NULL) { + /* get second factor from key store */ + if ((key = request_key("user", "ykfde-2f", NULL, 0)) < 0) + fprintf(stderr, "Failed requesting key. That's ok if you do not use\n" + "second factor. Give it manually if required.\n"); + + if (key > -1) { + /* if we have a key id we have a key - so this should succeed */ + if ((rc = keyctl_read_alloc(key, &payload)) < 0) { + perror("Failed reading payload from key"); + goto out40; + } + } else + payload = strdup(""); + } + /* get random number and limit to printable ASCII character (32 to 126) */ for(i = 0; i < CHALLENGELEN; i++) challenge_new[i] = (rand() % (126 - 32)) + 32; - /* do challenge/response and encode to hex */ - if ((rc = yk_challenge_response(yk, yk_slot, true, - CHALLENGELEN, (unsigned char *)challenge_new, - RESPONSELEN, (unsigned char *)repose_new)) < 0) { - perror("yk_challenge_response() failed"); - goto out40; - } - yubikey_hex_encode((char *)passphrase_new, (char *)repose_new, 20); - - /* initialize crypt device */ - if ((rc = crypt_init_by_name(&cd, device_name)) < 0) { - fprintf(stderr, "Device %s failed to initialize.\n", device_name); - goto out40; - } - /* these are the filenames for challenge * we need this for reading and writing */ sprintf(challengefilename, CHALLENGEDIR "/challenge-%d", serial); @@ -162,29 +219,53 @@ int main(int argc, char **argv) { /* write new challenge to file */ if ((rc = challengefiletmp = mkstemp(challengefiletmpname)) < 0) { fprintf(stderr, "Could not open file %s for writing.\n", challengefiletmpname); - goto out50; + goto out40; } if ((rc = write(challengefiletmp, challenge_new, CHALLENGELEN)) < 0) { fprintf(stderr, "Failed to write challenge to file.\n"); - goto out60; + goto out50; } challengefiletmp = close(challengefiletmp); + /* now that the new challenge has been written to file... + * add second factor to new challenge */ + if (second_factor) { + const char * tmp = new_2nd_factor ? new_2nd_factor : payload; + plen = strlen(tmp); + memcpy(challenge_new, tmp, plen < CHALLENGELEN / 2 ? plen : CHALLENGELEN / 2); + } + + /* do challenge/response and encode to hex */ + if ((rc = yk_challenge_response(yk, yk_slot, true, + CHALLENGELEN, (unsigned char *) challenge_new, + RESPONSELEN, (unsigned char *) response_new)) < 0) { + perror("yk_challenge_response() failed"); + goto out50; + } + yubikey_hex_encode((char *) passphrase_new, (char *) response_new, SHA1_DIGEST_SIZE); + /* get status of crypt device * We expect this to be active (or busy). It is the actual root device, no? */ - cs = crypt_status(cd, device_name); - if (cs != CRYPT_ACTIVE && cs != CRYPT_BUSY) { + cryptstatus = crypt_status(cryptdevice, device_name); + if (cryptstatus != CRYPT_ACTIVE && cryptstatus != CRYPT_BUSY) { rc = EXIT_FAILURE; fprintf(stderr, "Device %s is invalid or inactive.\n", device_name); goto out60; } - ck = crypt_keyslot_status(cd, luks_slot); - if (ck == CRYPT_SLOT_INVALID) { + /* initialize crypt device */ + if ((rc = crypt_init_by_name(&cryptdevice, device_name)) < 0) { + fprintf(stderr, "Device %s failed to initialize.\n", device_name); + goto out60; + } + + cryptkeyslot = crypt_keyslot_status(cryptdevice, luks_slot); + + if (cryptkeyslot == CRYPT_SLOT_INVALID) { rc = EXIT_FAILURE; fprintf(stderr, "Key slot %d is invalid.\n", luks_slot); goto out60; - } else if (ck == CRYPT_SLOT_ACTIVE || ck == CRYPT_SLOT_ACTIVE_LAST) { + } else if (cryptkeyslot == CRYPT_SLOT_ACTIVE || cryptkeyslot == CRYPT_SLOT_ACTIVE_LAST) { /* read challenge from file */ if ((rc = challengefile = open(challengefilename, O_RDONLY)) < 0) { perror("Failed opening challenge file for reading"); @@ -199,20 +280,38 @@ int main(int argc, char **argv) { challengefile = close(challengefile); /* finished reading challenge */ - /* do challenge/response and encode to hex */ - if ((rc = yk_challenge_response(yk, yk_slot, true, - CHALLENGELEN, (unsigned char *)challenge_old, - RESPONSELEN, (unsigned char *)repose_old)) < 0) { - perror("yk_challenge_response() failed"); - goto out60; + /* create a copy for second run in loop */ + memcpy(challenge_old_no_2f, challenge_old, CHALLENGELEN); + + /* copy the second factor */ + if (second_factor) { + plen = strlen(payload); + memcpy(challenge_old, payload, plen < CHALLENGELEN / 2 ? plen : CHALLENGELEN / 2); } - yubikey_hex_encode((char *)passphrase_old, (char *)repose_old, 20); - if ((rc = crypt_keyslot_change_by_passphrase(cd, luks_slot, luks_slot, - passphrase_old, PASSPHRASELEN, - passphrase_new, PASSPHRASELEN)) < 0) { - fprintf(stderr, "Could not update passphrase for key slot %d.\n", luks_slot); - goto out60; + /* try old with and without 2nd factor */ + for (uint8_t i = 0; i < 1 + second_factor; i++) { + /* do challenge/response and encode to hex */ + if ((rc = yk_challenge_response(yk, yk_slot, true, + CHALLENGELEN, (unsigned char *) challenge_old, + RESPONSELEN, (unsigned char *) response_old)) < 0) { + perror("yk_challenge_response() failed"); + goto out60; + } + yubikey_hex_encode((char *) passphrase_old, (char *) response_old, SHA1_DIGEST_SIZE); + + if ((rc = crypt_keyslot_change_by_passphrase(cryptdevice, luks_slot, luks_slot, + passphrase_old, PASSPHRASELEN, + passphrase_new, PASSPHRASELEN)) < 0) { + fprintf(stderr, "Could not update passphrase for key slot %d on %s try.\n", + luks_slot, i ? "second" : "first"); + if (!second_factor || i > 0) + goto out60; + } else + break; + + /* copy back... */ + memcpy(challenge_old, challenge_old_no_2f, CHALLENGELEN); } if ((rc = unlink(challengefilename)) < 0) { @@ -220,7 +319,7 @@ int main(int argc, char **argv) { goto out60; } } else { /* ck == CRYPT_SLOT_INACTIVE */ - if ((rc = crypt_keyslot_add_by_passphrase(cd, luks_slot, NULL, 0, + if ((rc = crypt_keyslot_add_by_passphrase(cryptdevice, luks_slot, NULL, 0, passphrase_new, PASSPHRASELEN)) < 0) { fprintf(stderr, "Could add passphrase for key slot %d.\n", luks_slot); goto out60; @@ -235,18 +334,18 @@ int main(int argc, char **argv) { rc = EXIT_SUCCESS; out60: + /* free crypt context */ + crypt_free(cryptdevice); + +out50: /* close the challenge file */ if (challengefile) close(challengefile); if (challengefiletmp) close(challengefiletmp); - if (access(challengefiletmpname, F_OK) == 0 ) + if (access(challengefiletmpname, F_OK) == 0) unlink(challengefiletmpname); -out50: - /* free crypt context */ - crypt_free(cd); - out40: /* close Yubikey */ if (!yk_close_key(yk)) @@ -261,17 +360,20 @@ out20: /* free iniparser dictionary */ iniparser_freedict(ini); - out10: /* wipe response (cleartext password!) from memory */ /* This is statically allocated and always save to wipe! */ memset(challenge_old, 0, CHALLENGELEN + 1); + memset(challenge_old_no_2f, 0, CHALLENGELEN + 1); memset(challenge_new, 0, CHALLENGELEN + 1); - memset(repose_old, 0, RESPONSELEN); - memset(repose_new, 0, RESPONSELEN); + memset(response_old, 0, RESPONSELEN); + memset(response_new, 0, RESPONSELEN); memset(passphrase_old, 0, PASSPHRASELEN + 1); memset(passphrase_new, 0, PASSPHRASELEN + 1); + free(new_2nd_factor); + free(payload); + return rc; } diff --git a/config.def.h b/config.def.h index f77c141..23518be 100644 --- a/config.def.h +++ b/config.def.h @@ -21,6 +21,8 @@ #define CONFYKSLOT "yk slot" /* config file LUKS slot */ #define CONFLUKSSLOT "luks slot" +/* config file second factor */ +#define CONF2NDFACTOR "second factor" /* path to cpio archive (initramfs image) */ #define CPIOFILE "/boot/ykfde-challenges.img" diff --git a/mkinitcpio/ykfde-2f b/mkinitcpio/ykfde-2f new file mode 100644 index 0000000..5e09dd9 --- /dev/null +++ b/mkinitcpio/ykfde-2f @@ -0,0 +1,14 @@ +#!/bin/sh + +build() { + add_systemd_unit cryptsetup-pre.target + add_systemd_unit ykfde-2f.service + add_symlink "/usr/lib/systemd/system/sysinit.target.wants/ykfde-2f.service" "../ykfde-2f.service" + add_file /usr/lib/systemd/scripts/ykfde-2f + add_binary keyctl + add_binary systemd-ask-password +} + +help() { + echo "This hook adds 2nd factor support for Yubikey full disk encryption." +} diff --git a/systemd/ykfde-2f b/systemd/ykfde-2f new file mode 100644 index 0000000..3aac298 --- /dev/null +++ b/systemd/ykfde-2f @@ -0,0 +1,20 @@ +#!/bin/sh + +# (C) 2016 by Christian Hesse +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +YKFDEFACTOR="$(systemd-ask-password --no-tty 'Please enter second factor for Yubikey full disk encryption!')" +YKFDESERIAL="$(keyctl 'add' 'user' 'ykfde-2f' "${YKFDEFACTOR}" '@u')" +keyctl 'timeout' "${YKFDESERIAL}" '150' + +if [ -s '/run/ykfde.pid' ]; then + kill -USR1 $(cat '/run/ykfde.pid') + # ykfde started from udev needs a moment to set up the key + # in store. It is out of systemd control, so wait a moment + # here. + sleep 0.2 +fi + +true diff --git a/systemd/ykfde-2f.service b/systemd/ykfde-2f.service new file mode 100644 index 0000000..acb6d67 --- /dev/null +++ b/systemd/ykfde-2f.service @@ -0,0 +1,16 @@ +# (C) 2016 by Christian Hesse +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. + +[Unit] +Description=Get 2nd Factor for YKFDE +DefaultDependencies=no +Before=cryptsetup-pre.target +Wants=cryptsetup-pre.target + +[Service] +Type=oneshot +RemainAfterExit=yes +TimeoutSec=0 +ExecStart=/usr/lib/systemd/scripts/ykfde-2f diff --git a/udev/Makefile b/udev/Makefile index b2b8994..2ee1319 100644 --- a/udev/Makefile +++ b/udev/Makefile @@ -8,7 +8,7 @@ CFLAGS += -std=c11 -O2 -fpic -pie -Wall -Werror all: ykfde ykfde: ykfde.c ../config.h - $(CC) $(CFLAGS) -lykpers-1 -lyubikey -liniparser $(LDFLAGS) -o ykfde ykfde.c + $(CC) $(CFLAGS) -liniparser -lkeyutils -lykpers-1 -lyubikey $(LDFLAGS) -o ykfde ykfde.c install: ykfde $(INSTALL) -D -m0755 ykfde $(DESTDIR)/usr/lib/udev/ykfde diff --git a/udev/ykfde.c b/udev/ykfde.c index bf53eb4..9e0d1d3 100644 --- a/udev/ykfde.c +++ b/udev/ykfde.c @@ -5,7 +5,7 @@ * of the GNU General Public License, incorporated herein by reference. * * compile with: - * $ gcc -o ykfde ykfde.c -lykpers-1 -lyubikey -liniparser + * $ gcc -o ykfde ykfde.c -liniparser -lkeyutils -lykpers-1 -lyubikey * * test with: * $ systemd-ask-password --no-tty "Please enter passphrase for disk foobar..." @@ -15,36 +15,49 @@ #include #include #include +#include #include #include #include #include -#include #include -#include #include #include #include #include #include +#include + +#include + #include #include #include -#include - #include "../config.h" -#define EVENT_SIZE (sizeof (struct inotify_event)) -#define EVENT_BUF_LEN (1024 * (EVENT_SIZE + 16)) - -#define CHALLENGELEN 64 +/* Yubikey supports write of 64 byte challenge to slot, + * returns HMAC-SHA1 response. + * + * Lengths are defined in ykpers-1/ykdef.h: + * SHA1_MAX_BLOCK_SIZE 64 + * SHA1_DIGEST_SIZE 20 + * + * For passphrase we use hex encoded digest, that is + * twice the length of binary digest. */ +#define CHALLENGELEN SHA1_MAX_BLOCK_SIZE #define RESPONSELEN SHA1_MAX_BLOCK_SIZE #define PASSPHRASELEN SHA1_DIGEST_SIZE * 2 #define ASK_PATH "/run/systemd/ask-password/" #define ASK_MESSAGE "Please enter passphrase for disk" +#define PID_PATH "/run/ykfde.pid" + +void received_signal(int signal) { + /* Do nothing, just interrupt the sleep. */ + return; +} static int send_on_socket(int fd, const char *socket_name, const void *packet, size_t size) { union { @@ -64,50 +77,121 @@ static int send_on_socket(int fd, const char *socket_name, const void *packet, s return EXIT_SUCCESS; } -static int try_answer(char * ask_file, char * response) { +static int try_answer(YK_KEY * yk, uint8_t slot, const char * ask_file, char * challenge) { int8_t rc = EXIT_FAILURE; dictionary * ini; const char * ask_message, * ask_socket; int fd_askpass; + char response[RESPONSELEN], + passphrase[PASSPHRASELEN + 1], + passphrase_askpass[PASSPHRASELEN + 2]; + /* keyutils */ + key_serial_t key; + void * payload = NULL; + size_t plen; + + memset(response, 0, RESPONSELEN); + memset(passphrase, 0, PASSPHRASELEN + 1); + memset(passphrase_askpass, 0, PASSPHRASELEN + 2); + + /* get second factor from key store + * if this fails it is not critical... possibly we just do not + * use second factor */ + key = request_key("user", "ykfde-2f", NULL, 0); + + if (key > 0) { + /* if we have a key id we have a key - so this should succeed */ + if ((rc = keyctl_read_alloc(key, &payload)) < 0) { + perror("Failed reading payload from key"); + goto out1; + } + + /* we replace part of the challenge with the second factor */ + plen = strlen(payload); + memcpy(challenge, payload, plen < CHALLENGELEN / 2 ? plen : CHALLENGELEN / 2); + + free(payload); + } + + /* do challenge/response and encode to hex */ + if ((rc = yk_challenge_response(yk, slot, true, + CHALLENGELEN, (unsigned char *) challenge, + RESPONSELEN, (unsigned char *) response)) < 0) { + perror("yk_challenge_response() failed"); + goto out1; + } + yubikey_hex_encode((char *) passphrase, (char *) response, SHA1_DIGEST_SIZE); + + /* add key to kernel key store */ + if ((key = add_key("user", "cryptsetup", passphrase, PASSPHRASELEN, KEY_SPEC_USER_KEYRING)) > 0) { + if (keyctl_set_timeout(key, 150) < 0) + perror("keyctl_set_timeout() failed"); + } else + perror("add_key() failed"); + + /* key is placed, no ask file... quit here */ + if (ask_file == NULL) { + rc = key > 0 ? EXIT_SUCCESS : EXIT_FAILURE; + goto out1; + } - if ((ini = iniparser_load(ask_file)) == NULL) + if ((ini = iniparser_load(ask_file)) == NULL) { + rc = EXIT_FAILURE; perror("cannot parse file"); + goto out1; + } ask_message = iniparser_getstring(ini, "Ask:Message", NULL); - if (strncmp(ask_message, ASK_MESSAGE, strlen(ASK_MESSAGE)) != 0) - goto out1; + if (strncmp(ask_message, ASK_MESSAGE, strlen(ASK_MESSAGE)) != 0) { + rc = EXIT_FAILURE; + goto out2; + } - ask_socket = iniparser_getstring(ini, "Ask:Socket", NULL); + if ((ask_socket = iniparser_getstring(ini, "Ask:Socket", NULL)) == NULL) { + rc = EXIT_FAILURE; + perror("Could not get socket name"); + goto out2; + } + + sprintf(passphrase_askpass, "+%s", passphrase); if ((fd_askpass = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0)) < 0) { + rc = EXIT_FAILURE; perror("socket() failed"); - goto out1; + goto out2; } - if (send_on_socket(fd_askpass, ask_socket, response, PASSPHRASELEN + 1) < 0) { + if (send_on_socket(fd_askpass, ask_socket, passphrase_askpass, PASSPHRASELEN + 1) < 0) { + rc = EXIT_FAILURE; perror("send_on_socket() failed"); - goto out2; + goto out3; } rc = EXIT_SUCCESS; -out2: +out3: close(fd_askpass); -out1: +out2: iniparser_freedict(ini); +out1: + /* wipe response (cleartext password!) from memory */ + memset(response, 0, RESPONSELEN); + memset(passphrase, 0, PASSPHRASELEN + 1); + memset(passphrase_askpass, 0, PASSPHRASELEN + 2); + return rc; } int main(int argc, char **argv) { int8_t rc = EXIT_FAILURE; + FILE *pidfile; /* Yubikey */ YK_KEY * yk; uint8_t slot = SLOT_CHAL_HMAC2; unsigned int serial = 0; - char response[RESPONSELEN], passphrase[PASSPHRASELEN + 1], passphrase_askpass[PASSPHRASELEN + 2]; /* iniparser */ dictionary * ini; char section_ykslot[10 /* unsigned int in char */ + 1 + sizeof(CONFYKSLOT) + 1]; @@ -118,17 +202,32 @@ int main(int argc, char **argv) { /* read dir */ DIR * dir; struct dirent * ent; - /* inotify */ - struct inotify_event * event; - int fd_inotify, watch, length, i = 0; - char buffer[EVENT_BUF_LEN]; -#if DEBUG +#ifdef DEBUG /* reopening stderr to /dev/console may help debugging... */ - freopen("/dev/console", "w", stderr); + FILE * tmp = freopen("/dev/console", "w", stderr); + (void) tmp; #endif - memset(challenge, 0, CHALLENGELEN); + if ((pidfile = fopen(PID_PATH, "w")) != NULL) { + if (fprintf(pidfile, "%d", getpid()) < 0) { + rc = EXIT_FAILURE; + perror("Failed writing pid"); + fclose(pidfile); + goto out10; + } + fclose(pidfile); + } else { + rc = EXIT_FAILURE; + perror("Failed opening pid file"); + goto out10; + } + + /* connect to signal */ + signal(SIGUSR1, received_signal); + + /* initialize static memory */ + memset(challenge, 0, CHALLENGELEN + 1); /* init and open first Yubikey */ if ((rc = yk_init()) < 0) { @@ -143,7 +242,7 @@ int main(int argc, char **argv) { } /* read the serial number from key */ - if((rc = !yk_get_serial(yk, 0, 0, &serial)) < 0) { + if ((rc = yk_get_serial(yk, 0, 0, &serial)) < 0) { perror("yk_get_serial() failed"); goto out30; } @@ -151,8 +250,10 @@ int main(int argc, char **argv) { sprintf(challengefilename, CHALLENGEDIR "/challenge-%d", serial); /* check if challenge file exists */ - if (access(challengefilename, R_OK) == -1) + if (access(challengefilename, R_OK) == -1) { + rc = EXIT_FAILURE; goto out30; + } /* read challenge from file */ if ((rc = challengefile = open(challengefilename, O_RDONLY)) < 0) { @@ -162,10 +263,8 @@ int main(int argc, char **argv) { if ((rc = read(challengefile, challenge, CHALLENGELEN)) < 0) { perror("Failed reading challenge from file"); - goto out50; + goto out40; } - challengefile = close(challengefile); - /* finished reading challenge */ /* try to read config file * if anything here fails we do not care... slot 2 is the default */ @@ -193,75 +292,39 @@ int main(int argc, char **argv) { iniparser_freedict(ini); } - memset(response, 0, RESPONSELEN); - memset(passphrase, 0, PASSPHRASELEN + 1); - - /* do challenge/response and encode to hex */ - if ((rc = yk_challenge_response(yk, slot, true, - CHALLENGELEN, (unsigned char *)challenge, - RESPONSELEN, (unsigned char *)response)) < 0) { - perror("yk_challenge_response() failed"); - goto out50; - } - yubikey_hex_encode((char *)passphrase, (char *)response, 20); - - sprintf(passphrase_askpass, "+%s", passphrase); - /* change to directory so we do not have to assemble complete/absolute path */ if ((rc = chdir(ASK_PATH)) != 0) { perror("chdir() failed"); - goto out50; - } - - /* creating the INOTIFY instance and add ASK_PATH directory into watch list */ - if ((rc = fd_inotify = inotify_init()) < 0) { - perror("inotify_init() failed"); - goto out50; + goto out40; } - watch = inotify_add_watch(fd_inotify, ASK_PATH, IN_MOVED_TO); - - /* Is the request already there? - * We do this AFTER setting up the inotify watch. This way we do not have race condition. */ + /* Is the request already there? */ if ((dir = opendir(ASK_PATH)) != NULL) { while ((ent = readdir(dir)) != NULL) { if (strncmp(ent->d_name, "ask.", 4) == 0) { - if ((rc = try_answer(ent->d_name, passphrase_askpass)) == EXIT_SUCCESS) - goto out70; + if ((rc = try_answer(yk, slot, ent->d_name, challenge)) == EXIT_SUCCESS) + goto out50; } } } else { rc = EXIT_FAILURE; perror ("opendir() failed"); - goto out60; + goto out50; } - /* read to determine the event change happens. Actually this read blocks until the change event occurs */ - if ((rc = length = read(fd_inotify, buffer, EVENT_BUF_LEN)) < 0) { - perror("read() failed"); - goto out70; - } + /* Wait for 90 seconds. + * The user has this time to enter his second factor, resulting in + * SIGUSR1 being sent to us. */ + sleep(90); - /* actually read return the list of change events happens. - * Here, read the change event one by one and process it accordingly. */ - while (i < length) { - event = (struct inotify_event *)&buffer[i]; - if (event->len > 0) - if ((rc = try_answer(event->name, passphrase_askpass)) == EXIT_SUCCESS) - goto out70; - i += EVENT_SIZE + event->len; - } + /* try again, but for key store this time */ + rc = try_answer(yk, slot, NULL, challenge); -out70: +out50: /* close dir */ closedir(dir); -out60: - /* remove inotify watch and remove file handle */ - inotify_rm_watch(fd_inotify, watch); - close(fd_inotify); - -out50: +out40: /* close the challenge file */ if (challengefile) close(challengefile); @@ -270,12 +333,6 @@ out50: unlink(challengefilename); out30: - /* wipe response (cleartext password!) from memory */ - memset(challenge, 0, CHALLENGELEN); - memset(response, 0, RESPONSELEN); - memset(passphrase, 0, PASSPHRASELEN + 1); - memset(passphrase_askpass, 0, PASSPHRASELEN + 2); - /* close Yubikey */ if (yk_close_key(yk) < 0) perror("yk_close_key() failed"); @@ -286,6 +343,9 @@ out20: perror("yk_release() failed"); out10: + /* wipe challenge from memory */ + memset(challenge, 0, CHALLENGELEN + 1); + return rc; } -- cgit v1.2.3-70-g09d2