From 407a77016162958c743bbf354465e0396c32ecc0 Mon Sep 17 00:00:00 2001 From: Moritz Maxeiner Date: Tue, 28 Jan 2014 04:02:51 +0100 Subject: [PATCH] Rewrite as a pre-boot authentication module (mostly) comforming to the design specification of 'YubiKey Integration for Full Disk Encryption Pre-Boot Authentication (Copyright) Yubico, 2011 Version: 1.1'. Used binaries: * uuidgen - for generation of random sequence numbers * ykchalresp - for challenging a Yubikey * ykinfo - to check if a Yubikey is plugged in at boot (fallback to passphrase authentication otherwise) * openssl - for calculation of SHA-1, HMAC-SHA-1, as well as AES-256-CTR (de/en)cryption Main differences to the specification mentioned above: * No user management (yet), only one password+yubikey per LUKS device * SHA-512 instead of CRC-16 for checksum Main differences to the previous implementation: * Instead of changing the key slot of the LUKS device each boot, the actual key for the LUKS device will be encrypted itself * Since the response for the new challenge is now calculated locally with openssl, the MITM-USB-attack with which previously an attacker could obtain the new response (that was used as the new encryption key for the LUKS device) by listening to the Yubikey has ideally become useless (as long as uuidgen can successfuly generate new random sequence numbers). Remarks: * This is not downwards compatible to the previous implementation --- nixos/modules/system/boot/luksroot.nix | 272 ++++++++++++++----------- 1 file changed, 148 insertions(+), 124 deletions(-) diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix index ca4a8c480f80..52c91a0e20ce 100644 --- a/nixos/modules/system/boot/luksroot.nix +++ b/nixos/modules/system/boot/luksroot.nix @@ -31,140 +31,148 @@ let fi ''} - ${optionalString (luks.yubikeySupport && (yubikey != null)) '' - mkdir -p ${yubikey.challenge.mountPoint} - mount -t ${yubikey.challenge.fsType} ${toString yubikey.challenge.device} ${yubikey.challenge.mountPoint} - response="$(ykchalresp -${toString yubikey.yubikeySlot} "`cat ${yubikey.challenge.mountPoint}${yubikey.challenge.file}`" 2>/dev/null || true)" - if [ -z "$response" ]; then - echo -n "waiting 10 seconds for yubikey to appear..." - for try in $(seq 10); do - sleep 1 - response="$(ykchalresp -${toString yubikey.yubikeySlot} "`cat ${yubikey.challenge.mountPoint}${yubikey.challenge.file}`" 2>/dev/null || true)" - if [ ! -z "$response" ]; then break; fi - echo -n . - done - echo "ok" - fi - - ${optionalString yubikey.twoFactor '' - if [ ! -z "$response" ]; then - echo -n "Enter two-factor passphrase: " - read -s passphrase - current_key="$passphrase$response" - fi - ''} - - ${optionalString (!yubikey.twoFactor) '' - if [ ! -z "$response" ]; then - current_key="$response" - fi - ''} - ''} - - # open luksRoot and scan for logical volumes - ${optionalString ((!luks.yubikeySupport) || (yubikey == null)) '' - cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} \ - ${optionalString (keyFile != null) "--key-file=${keyFile} ${optionalString (keyFileSize != null) "--keyfile-size=${toString keyFileSize}"}"} - ''} - - ${optionalString (luks.yubikeySupport && (yubikey != null)) '' - if [ -z "$response" ]; then + open_normally() { cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} \ ${optionalString (keyFile != null) "--key-file=${keyFile} ${optionalString (keyFileSize != null) "--keyfile-size=${toString keyFileSize}"}"} - else - echo $current_key | cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} --key-file=- + } - if [ $? != "0" ]; then - for try in $(seq 3); do + ${optionalString (luks.yubikeySupport && (yubikey != null)) '' - ${optionalString (!yubikey.twoFactor) '' - sleep 1 - ''} + rbtohex() { + od -An -vtx1 | tr -d ' \n' + } - ${optionalString yubikey.twoFactor '' - echo -n "Enter two-factor passphrase: " - read -s passphrase - current_key="$passphrase$response" - ''} + hextorb() { + tr '[:lower:]' '[:upper:]' | sed -e 's|\([0-9A-F]\{2\}\)|\\\\\\x\1|gI' | xargs printf + } - echo $current_key | cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} --key-file=- - if [ $? == "0" ]; then break; fi - echo -n . - done + take() { + local c="$1" + shift + head -c $c "$@" + } + + drop() { + local c=$1 + shift + if [ -e "$1" ]; then + cat "$1" | ( dd of=/dev/null bs="$c" count=1 2>/dev/null ; dd 2>/dev/null ) + else + ( dd of=/dev/null bs="$c" count=1 2>/dev/null ; dd 2>/dev/null ) + fi + } + + open_yubikey() { + + mkdir -p ${yubikey.storage.mountPoint} + mount -t ${yubikey.storage.fsType} ${toString yubikey.storage.device} ${yubikey.storage.mountPoint} + + local uuid_r + uuid_r="$(take 16 ${yubikey.storage.mountPoint}${yubikey.storage.path} | rbtohex)" + + local uuid_luks + uuid_luks="$(cryptsetup luksUUID ${device} | take 36 | tr -d '-')" + + local k_user + local challenge + local k_blob + local aes_blob_decrypted + local checksum_correct + local checksum + + for try in $(seq 3); do + + ${optionalString yubikey.twoFactor '' + echo -n "Enter two-factor passphrase: " + read -s k_user + ''} + + challenge="$(echo -n $k_user$uuid_r$uuid_luks | openssl-wrap dgst -binary -sha1 | rbtohex)" + + k_blob="$(ykchalresp -${toString yubikey.slot} -x $challenge 2>/dev/null)" + + aes_blob_decrypted="$(drop 16 ${yubikey.storage.mountPoint}${yubikey.storage.path} | openssl-wrap enc -d -aes-256-ctr -K $k_blob -iv $uuid_r | rbtohex)" + + checksum="$(echo -n $aes_blob_decrypted | hextorb | drop 84 | rbtohex)" + if [ "$(echo -n $aes_blob_decrypted | hextorb | take 84 | openssl-wrap dgst -binary -sha512 | rbtohex)" == "$checksum" ]; then + checksum_correct=1 + break + else + checksum_correct=0 + echo "Authentication failed!" + fi + done + + if [ "$checksum_correct" != "1" ]; then + umount ${yubikey.storage.mountPoint} + echo "Maximum authentication errors reached" + exit 1 fi - mkdir -p ${yubikey.ramfsMountPoint} - # A ramfs is used here to ensure that the file used to update - # the key slot with cryptsetup will never get swapped out. - # Warning: Do NOT replace with tmpfs! - mount -t ramfs none ${yubikey.ramfsMountPoint} + local k_yubi + k_yubi="$(echo -n $aes_blob_decrypted | hextorb | take 20 | rbtohex)" + + local k_luks + k_luks="$(echo -n $aes_blob_decrypted | hextorb | drop 20 | take 64 | rbtohex)" + + echo -n "$k_luks" | hextorb | cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} --key-file=- update_failed=false - old_challenge=$(cat ${yubikey.challenge.mountPoint}${yubikey.challenge.file}) - new_challenge=$(uuidgen) + local new_uuid_r + new_uuid_r="$(uuidgen)" if [ $? != "0" ]; then for try in $(seq 10); do sleep 1 - new_challenge=$(uuidgen) + new_uuid_r="$(uuidgen)" if [ $? == "0" ]; then break; fi if [ $try -eq 10 ]; then update_failed=true; fi done fi if [ "$update_failed" == false ]; then - echo $new_challenge > ${yubikey.challenge.mountPoint}${yubikey.challenge.file} - response="$(ykchalresp -${toString yubikey.yubikeySlot} "`cat ${yubikey.challenge.mountPoint}${yubikey.challenge.file}`" 2>/dev/null || true)" - if [ -z "$response" ]; then - echo -n "waiting 10 seconds for yubikey to appear..." - for try in $(seq 10); do - sleep 1 - response="$(ykchalresp -${toString yubikey.yubikeySlot} "`cat ${yubikey.challenge.mountPoint}${yubikey.challenge.file}`" 2>/dev/null || true)" - if [ ! -z "$response" ]; then break; fi - echo -n . - done - echo "ok"; - fi + new_uuid_r="$(echo -n $new_uuid_r | take 36 | tr -d '-')" - if [ ! -z "$response" ]; then - ${optionalString yubikey.twoFactor '' - new_key="$passphrase$response" - ''} + local new_challenge + new_challenge="$(echo -n $k_user$new_uuid_r$uuid_luks | openssl-wrap dgst -binary -sha1 | rbtohex)" - ${optionalString (!yubikey.twoFactor) '' - new_key="$response" - ''} + local new_k_blob + new_k_blob="$(echo -n $new_challenge | hextorb | openssl-wrap dgst -binary -sha1 -mac HMAC -macopt hexkey:$k_yubi | rbtohex)" - echo $new_key > ${yubikey.ramfsMountPoint}/new_key - - echo $current_key | cryptsetup luksChangeKey ${device} --key-file=- --key-slot ${toString yubikey.luksKeySlot} ${yubikey.ramfsMountPoint}/new_key - if [ $? != "0" ]; then - for try in $(seq 10); do - sleep 1 - echo $current_key | cryptsetup luksChangeKey ${device} --key-file=- --key-slot ${toString yubikey.luksKeySlot} ${yubikey.ramfsMountPoint}/new_key - if [ $? == "0" ]; then break; fi - if [ $try -eq 10 ]; then update_failed=true; fi - done - - fi - - rm -f ${yubikey.ramfsMountPoint}/new_key - - if [ "$update_failed" == true ]; then - echo $old_challenge > ${yubikey.challenge.mountPoint}${yubikey.challenge.file} - echo "Warning: Could not update luks header with new key for ${device}, old challenge restored!" - fi - else - echo $old_challenge > ${yubikey.challenge.mountPoint}${yubikey.challenge.file} - echo "Warning: No yubikey present to challenge for ${device}, old challenge restored!" - fi + echo -n "$new_uuid_r" | hextorb > ${yubikey.storage.mountPoint}${yubikey.storage.path} + echo -n "$k_yubi$k_luks$checksum" | hextorb | openssl-wrap enc -e -aes-256-ctr -K "$new_k_blob" -iv "$new_uuid_r" >> ${yubikey.storage.mountPoint}${yubikey.storage.path} else - echo "Warning: New challenge could not be obtained for ${device}, old challenge persists!" + echo "Warning: Could not obtain new UUID, current challenge persists!" fi - umount ${yubikey.ramfsMountPoint} - umount ${yubikey.challenge.mountPoint} + umount ${yubikey.storage.mountPoint} + } + + ykinfo -v + yubikey_missing="$(ykinfo -v 1>/dev/null 2>&1)$?" + if [ "$yubikey_missing" != "0" ]; then + echo -n "waiting 10 seconds for yubikey to appear..." + for try in $(seq 10); do + sleep 1 + ykinfo -v + yubikey_missing="$(ykinfo -v 1>/dev/null 2>&1)$?" + if [ "$yubikey_missing" == "0" ]; then break; fi + echo -n . + done + echo "ok" fi + + if [ "$yubikey_missing" != "0" ]; then + echo "no yubikey found, falling back to non-yubikey open procedure" + open_normally + else + open_yubikey + fi + ''} + + # open luksRoot and scan for logical volumes + ${optionalString ((!luks.yubikeySupport) || (yubikey == null)) '' + open_normally ''} ''; @@ -283,19 +291,13 @@ in description = "TODO"; }; - yubikeySlot = mkOption { + slot = mkOption { default = 2; type = types.int; description = "TODO"; }; - luksKeySlot = mkOption { - default = 1; - type = types.int; - description = "TODO"; - }; - - challenge = mkOption { + storage = mkOption { type = types.optionSet; description = "TODO"; @@ -313,24 +315,18 @@ in }; mountPoint = mkOption { - default = "/crypt-challenge"; + default = "/crypt-storage"; type = types.string; description = "TODO"; }; - file = mkOption { - default = "/crypt-challenge"; + path = mkOption { + default = "/crypt-storage/default"; type = types.string; description = "TODO"; }; }; }; - - ramfsMountPoint = mkOption { - default = "/crypt-update"; - type = types.string; - description = "TODO"; - }; }; }; @@ -361,6 +357,7 @@ in cp -pdvn $lib $out/lib cp -pvn $(readlink -f $lib) $out/lib done + ${optionalString luks.yubikeySupport '' cp -pdv ${pkgs.utillinux}/bin/uuidgen $out/bin for lib in $(ldd $out/bin/uuidgen |grep '=>' |grep /nix/store/ |cut -d' ' -f3); do @@ -373,6 +370,26 @@ in cp -pdvn $lib $out/lib cp -pvn $(readlink -f $lib) $out/lib done + + cp -pdv ${pkgs.ykpers}/bin/ykinfo $out/bin + for lib in $(ldd $out/bin/ykinfo |grep '=>' |grep /nix/store/ |cut -d' ' -f3); do + cp -pdvn $lib $out/lib + cp -pvn $(readlink -f $lib) $out/lib + done + + cp -pdv ${pkgs.openssl}/bin/openssl $out/bin + for lib in $(ldd $out/bin/openssl |grep '=>' |grep /nix/store/ |cut -d' ' -f3); do + cp -pdvn $lib $out/lib + cp -pvn $(readlink -f $lib) $out/lib + done + + mkdir -p $out/etc/ssl + cp -pdv ${pkgs.openssl}/etc/ssl/openssl.cnf $out/etc/ssl + + cat > $out/bin/openssl-wrap < $out/bin/openssl-wrap <