diff options
Diffstat (limited to 'bin/worker.c')
-rw-r--r-- | bin/worker.c | 418 |
1 files changed, 418 insertions, 0 deletions
diff --git a/bin/worker.c b/bin/worker.c new file mode 100644 index 0000000..ef0cefc --- /dev/null +++ b/bin/worker.c @@ -0,0 +1,418 @@ +/* + * (C) 2014-2017 by Christian Hesse <mail@eworm.de> + * + * This software may be used and distributed according to the terms + * of the GNU General Public License, incorporated herein by reference. + * + * compile with: + * $ gcc -o ykfde ykfde.c -liniparser -lkeyutils -lykpers-1 -lyubikey + * + * test with: + * $ systemd-ask-password --no-tty "Please enter passphrase for disk foobar..." + */ + +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <stddef.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/poll.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/un.h> +#include <unistd.h> + +#include <iniparser.h> + +#include <keyutils.h> + +#include <yubikey.h> +#include <ykpers-1/ykdef.h> +#include <ykpers-1/ykcore.h> + +#include "../config.h" + +/* 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" + +/*** send_on_socket ***/ +static int send_on_socket(int fd, const char *socket_name, const void *packet, size_t size) { + union { + struct sockaddr sa; + struct sockaddr_un un; + } sa = { + .un.sun_family = AF_UNIX, + }; + + strncpy(sa.un.sun_path, socket_name, sizeof(sa.un.sun_path)); + + if (sendto(fd, packet, size, MSG_NOSIGNAL, &sa.sa, offsetof(struct sockaddr_un, sun_path) + strlen(socket_name)) < 0) { + perror("sendto() failed"); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} + +/*** yk_open_and_check ***/ +static YK_KEY * yk_open_and_check(const unsigned int expected, unsigned int * serial) { + YK_KEY * yk; + + if ((yk = yk_open_first_key()) == NULL) { + if (errno != EAGAIN) + perror("yk_open_first_key() failed"); + goto out1; + } + + if (serial != NULL) { + /* read the serial number from key */ + if (yk_get_serial(yk, 0, 0, serial) == 0) { + perror("yk_get_serial() failed"); + goto out2; + } + + if (expected > 0 && expected != *serial) { + fprintf(stderr, "Opened Yubikey with unexpected serial number (%d != %d)...\n", expected, *serial); + goto out2; + } + } + + return yk; + +out2: + /* close Yubikey */ + if (yk_close_key(yk) == 0) + perror("yk_close_key() failed"); + +out1: + return NULL; +} + +/*** read_challenge ***/ +static int read_challenge(const unsigned int serial, char * challenge) { + int rc = EXIT_FAILURE; + char challengefilename[sizeof(CHALLENGEDIR) + 11 /* "/challenge-" */ + 10 /* unsigned int in char */ + 1]; + int challengefile; + + snprintf(challengefilename, sizeof(challengefilename), CHALLENGEDIR "/challenge-%d", serial); + + /* check if challenge file exists */ + if (access(challengefilename, R_OK) == -1) { + goto out1; + } + + /* read challenge from file */ + if ((challengefile = open(challengefilename, O_RDONLY)) < 0) { + perror("Failed opening challenge file for reading"); + goto out1; + } + + if (read(challengefile, challenge, CHALLENGELEN) < 0) { + perror("Failed reading challenge from file"); + goto out2; + } + + rc = EXIT_SUCCESS; + +out2: + close(challengefile); + +out1: + return rc; +} + +/*** get_second_factor ***/ +static char * get_second_factor(void) { + key_serial_t key; + void * payload = NULL; + + /* get second factor from key store + * If this fails it is not critical... possibly we just do not + * use second factor. */ + key = keyctl_search(KEY_SPEC_USER_KEYRING, "user", "ykfde-2f", 0); + + if (key > 0) { + /* if we have a key id we have a key - so this should succeed */ + if (keyctl_read_alloc(key, &payload) < 0) { + perror("Failed reading payload from key"); + return NULL; + } + + return payload; + } + + return NULL; +} + +/*** get_response ***/ +static int get_response(const unsigned int serial, uint8_t slot, char * challenge, char * passphrase) { + YK_KEY * yk; + char response[RESPONSELEN]; + char * second_factor; + size_t second_factor_len; + /* iniparser */ + dictionary * ini; + char section_ykslot[10 /* unsigned int in char */ + 1 + sizeof(CONFYKSLOT) + 1]; + + memset(response, 0, RESPONSELEN); + + if ((second_factor = get_second_factor()) != NULL) { + /* we replace part of the challenge with the second factor */ + second_factor_len = strlen(second_factor); + memcpy(challenge, second_factor, second_factor_len < CHALLENGELEN / 2 ? + second_factor_len : CHALLENGELEN / 2); + memset(second_factor, 0, second_factor_len); + free(second_factor); + } + + /* try to read config file + * If anything here fails we do not care... slot 2 is the default. */ + if ((ini = iniparser_load(CONFIGFILE)) != NULL) { + /* first try the general setting */ + slot = iniparser_getint(ini, "general:" CONFYKSLOT, slot); + + sprintf(section_ykslot, "%d:" CONFYKSLOT, serial); + + /* then probe for setting with serial number */ + slot = iniparser_getint(ini, section_ykslot, slot); + + switch (slot) { + case 1: + case SLOT_CHAL_HMAC1: + slot = SLOT_CHAL_HMAC1; + break; + case 2: + case SLOT_CHAL_HMAC2: + default: + slot = SLOT_CHAL_HMAC2; + break; + } + + iniparser_freedict(ini); + } + + /* open Yubikey and check serial */ + if ((yk = yk_open_and_check(serial, NULL)) == NULL) { + fprintf(stderr, "yk_open_and_check() failed\n"); + goto out1; + } + + /* do challenge/response and encode to hex */ + if (yk_challenge_response(yk, slot, true, + CHALLENGELEN, (unsigned char *) challenge, + RESPONSELEN, (unsigned char *) response) == 0) { + perror("yk_challenge_response() failed"); + goto out2; + } + + yubikey_hex_encode((char *) passphrase, (char *) response, SHA1_DIGEST_SIZE); + +out2: + /* close Yubikey */ + if (yk_close_key(yk) == 0) + perror("yk_close_key() failed"); + +out1: + memset(response, 0, RESPONSELEN); + + return EXIT_SUCCESS; +} + +/*** add_keyring ***/ +static int add_keyring(const char * passphrase) { + key_serial_t key; + + /* add key to kernel key store + * Put it into session keyring first, set permissions and + * move it to user keyring. */ + if ((key = add_key("user", "cryptsetup", passphrase, + PASSPHRASELEN, KEY_SPEC_SESSION_KEYRING)) < 0) { + perror("add_key() failed"); + return -1; + } + + if (keyctl_set_timeout(key, 150) < 0) { + perror("keyctl_set_timeout() failed"); + return -1; + } + + if (keyctl_setperm(key, KEY_POS_ALL|KEY_USR_ALL) < 0) { + perror("keyctl_setperm() failed"); + return -1; + } + + if (keyctl_link(key, KEY_SPEC_USER_KEYRING) < 0) { + perror("keyctl_link() failed"); + return -1; + } + + if (keyctl_unlink(key, KEY_SPEC_SESSION_KEYRING) < 0) { + perror("keyctl_unlink() failed"); + return -1; + } + + return EXIT_SUCCESS; +} + +/*** answer_askpass ***/ +static int answer_askpass(const char * ask_file, const char * passphrase) { + int rc = EXIT_FAILURE, fd_askpass; + const char * ask_message, * ask_socket; + /* iniparser */ + dictionary * ini; + + if ((ini = iniparser_load(ask_file)) == NULL) { + 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 out2; + + if ((ask_socket = iniparser_getstring(ini, "Ask:Socket", NULL)) == NULL) { + perror("Could not get socket name"); + goto out2; + } + + if ((fd_askpass = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0)) < 0) { + perror("socket() failed"); + goto out2; + } + + if (send_on_socket(fd_askpass, ask_socket, passphrase, PASSPHRASELEN + 1) < 0) { + perror("send_on_socket() failed"); + goto out3; + } + + rc = EXIT_SUCCESS; + +out3: + close(fd_askpass); + +out2: + iniparser_freedict(ini); + +out1: + return rc; +} + +/*** walk_askpass ***/ +static int walk_askpass(const char * passphrase) { + int rc = EXIT_FAILURE; + DIR * dir; + struct dirent * ent; + + /* change to directory so we do not have to assemble complete/absolute path */ + if (chdir(ASK_PATH) != 0) { + perror("chdir() failed"); + return rc; + } + + /* 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 = answer_askpass(ent->d_name, passphrase)) == EXIT_SUCCESS) + goto out; + } + } + } else { + perror ("opendir() failed"); + return EXIT_FAILURE; + } + + rc = EXIT_SUCCESS; + +out: + closedir(dir); + + return rc; +} + +/*** main ***/ +int main(int argc, char **argv) { + int8_t rc = EXIT_FAILURE; + /* Yubikey */ + YK_KEY * yk; + uint8_t slot = SLOT_CHAL_HMAC2; + unsigned int serial = 0; + /* challenge and passphrase */ + char challenge[CHALLENGELEN + 1]; + char passphrase[PASSPHRASELEN + 2]; + +#ifdef DEBUG + /* reopening stderr to /dev/console may help debugging... */ + FILE * tmp = freopen("/dev/console", "w", stderr); + (void) tmp; +#endif + + /* initialize static memory */ + memset(challenge, 0, CHALLENGELEN + 1); + memset(passphrase, 0, PASSPHRASELEN + 2); + + *passphrase = '+'; + + /* init and open first Yubikey */ + if (yk_init() == 0) { + perror("yk_init() failed"); + goto out10; + } + + /* open Yubikey and get serial */ + if ((yk = yk_open_and_check(0, &serial)) == NULL) { + if (errno == EAGAIN) + rc = EXIT_SUCCESS; + goto out30; + } + + /* close Yubikey */ + if (yk_close_key(yk) == 0) { + perror("yk_close_key() failed"); + goto out30; + } + + if ((rc = read_challenge(serial, challenge)) < 0) + goto out30; + + if ((rc = get_response(serial, slot, challenge, passphrase + 1)) < 0) + goto out30; + + if ((rc = add_keyring(passphrase + 1)) < 0) + goto out30; + + if ((rc = walk_askpass(passphrase)) < 0) + goto out30; + +out30: + /* release Yubikey */ + if (yk_release() == 0) + perror("yk_release() failed"); + +out10: + /* wipe challenge from memory */ + memset(challenge, 0, CHALLENGELEN + 1); + memset(passphrase, 0, PASSPHRASELEN + 2); + + return rc; +} + +// vim: set syntax=c: |