/* * (C) 2014-2024 by Christian Hesse <mail@eworm.de> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ #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 <systemd/sd-daemon.h> #include <iniparser/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, }; memcpy(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_USER_KEYRING)) < 0) { perror("add_key() failed"); return -1; } if (keyctl_set_timeout(key, 150) < 0) { perror("keyctl_set_timeout() 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 /* check that we are running from systemd */ if (sd_notify(0, "READY=0\nSTATUS=Work in progress...") <= 0) { fprintf(stderr, "This is expected to run from a systemd service.\n"); goto out10; } /* 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); /* notify systemd that we are ready This does not indicate whether or not we are successful, but prevents systemd from reporting: Failed with result 'protocol'. */ sd_notify(0, "READY=1\nSTATUS=All done."); return rc; } // vim: set syntax=c: