From 8e74e1fdedbadd58e218d99f215ef91e6b84e3e7 Mon Sep 17 00:00:00 2001 From: Moritz Maxeiner Date: Mon, 3 Feb 2014 22:50:17 +0100 Subject: [PATCH 1/2] Replace the current Yubikey PBA implementation with the previous one. Rationale: * The main reason for choosing to implement the PBA in accordance with the Yubico documentation was to prevent a MITM-USB-attack successfully recovering the new LUKS key. * However, a MITM-USB-attacker can read user id and password when they were entered for PBA, which allows him to recover the new challenge after the PBA is complete, with which he can challenge the Yubikey, decrypt the new AES blob and recover the LUKS key. * Additionally, since the Yubikey shared secret is stored in the same AES blob, after such an attack not only is the LUKS device compromised, the Yubikey is as well, since the shared secret has also been recovered by the attacker. * Furthermore, with this method an attacker could also bruteforce the AES blob, if he has access to the unencrypted device, which would again compromise the Yubikey, should he be successful. * Finally, with this method, once the LUKS key has been recovered once, the encryption is permanently broken, while with the previous system, the LUKS key itself it changed at every successful boot, so recovering it once will not necessarily result in a permanent breakage and will also not compromise the Yubikey itself (since its secret is never stored anywhere but on the Yubikey itself). Summary: The current implementation opens up up vulnerability to brute-forcing the AES blob, while retaining the current MITM-USB attack, additionally making the consequences of this attack permanent and extending it to the Yubikey itself. --- nixos/modules/system/boot/luksroot.nix | 134 ++++++++----------------- 1 file changed, 42 insertions(+), 92 deletions(-) diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix index 8547682284f7..d70d13411669 100644 --- a/nixos/modules/system/boot/luksroot.nix +++ b/nixos/modules/system/boot/luksroot.nix @@ -39,27 +39,11 @@ let ${optionalString (luks.yubikeySupport && (yubikey != null)) '' rbtohex() { - od -An -vtx1 | tr -d ' \n' + ( od -An -vtx1 | tr -d ' \n' ) } hextorb() { - tr '[:lower:]' '[:upper:]' | sed -e 's|\([0-9A-F]\{2\}\)|\\\\\\x\1|gI' | xargs printf - } - - 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 + ( tr '[:lower:]' '[:upper:]' | sed -e 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/gI'| xargs printf ) } open_yubikey() { @@ -70,83 +54,41 @@ let local uuid_r local k_user local challenge - local k_blob - local aes_blob_decrypted - local checksum_correct - local checksum - local uuid_luks - local user_record + local opened - uuid_luks="$(cryptsetup luksUUID ${device} | take 36 | tr -d '-')" + sleep 1 - ${optionalString (!yubikey.multiUser) '' - user_record="$(cat ${yubikey.storage.mountPoint}${yubikey.storage.path})" - uuid_r="$(echo -n $user_record | take 32)" - ''} + uuid_r="$(cat ${yubikey.storage.mountPoint}${yubikey.storage.path})" for try in $(seq 3); do - ${optionalString yubikey.multiUser '' - local user_id - echo -n "Enter user id: " - read -s user_id - echo - ''} - ${optionalString yubikey.twoFactor '' echo -n "Enter two-factor passphrase: " read -s k_user echo ''} - ${optionalString yubikey.multiUser '' - local user_id_hash - user_id_hash="$(echo -n $user_id | openssl-wrap dgst -binary -sha512 | rbtohex)" + challenge="$(echo -n $k_user$uuid_r | openssl-wrap dgst -binary -sha512 | rbtohex)" - user_record="$(sed -n -e /^$user_id_hash[^$]*$/p ${yubikey.storage.mountPoint}${yubikey.storage.path} | tr -d '\n')" + k_luks="$(ykchalresp -${toString yubikey.slot} -x $challenge 2>/dev/null)" - if [ ! -z "$user_record" ]; then - user_record="$(echo -n $user_record | drop 128)" - uuid_r="$(echo -n $user_record | take 32)" - ''} + echo -n "$k_luks" | hextorb | cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} --key-file=- - 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="$(echo -n $user_record | drop 32 | hextorb | openssl-wrap enc -d -aes-256-ctr -K $k_blob -iv $uuid_r | rbtohex)" - - checksum="$(echo -n $aes_blob_decrypted | drop 168)" - 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 - - ${optionalString yubikey.multiUser '' + if [ $? == "0" ]; then + opened=true + break else - checksum_correct=0 + opened=false echo "Authentication failed!" fi - ''} done - if [ "$checksum_correct" != "1" ]; then + if [ "$opened" == false ]; then umount ${yubikey.storage.mountPoint} echo "Maximum authentication errors reached" exit 1 fi - local k_yubi - k_yubi="$(echo -n $aes_blob_decrypted | take 40)" - - local k_luks - k_luks="$(echo -n $aes_blob_decrypted | drop 40 | take 128)" - - echo -n "$k_luks" | hextorb | cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} --key-file=- - update_failed=false local new_uuid_r @@ -161,24 +103,32 @@ let fi if [ "$update_failed" == false ]; then - new_uuid_r="$(echo -n $new_uuid_r | take 36 | tr -d '-')" + new_uuid_r="$(echo -n $new_uuid_r | head -c 36 | tr -d '-')" local new_challenge - new_challenge="$(echo -n $k_user$new_uuid_r$uuid_luks | openssl-wrap dgst -binary -sha1 | rbtohex)" + new_challenge="$(echo -n $k_user$new_uuid_r | openssl-wrap dgst -binary -sha512 | rbtohex)" - local new_k_blob - new_k_blob="$(echo -n $new_challenge | hextorb | openssl-wrap dgst -binary -sha1 -mac HMAC -macopt hexkey:$k_yubi | rbtohex)" + local new_k_luks + new_k_luks="$(ykchalresp -${toString yubikey.slot} -x $new_challenge 2>/dev/null)" - local new_aes_blob - new_aes_blob=$(echo -n "$k_yubi$k_luks$checksum" | hextorb | openssl-wrap enc -e -aes-256-ctr -K "$new_k_blob" -iv "$new_uuid_r" | rbtohex) + 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} - ${optionalString yubikey.multiUser '' - sed -i -e "s|^$user_id_hash$user_record|$user_id_hash$new_uuid_r$new_aes_blob|1" - ''} + echo -n "$new_k_luks" | hextorb > ${yubikey.ramfsMountPoint}/new_key + echo -n "$k_luks" | cryptsetup luksChangeKey ${device} --key-file=- ${yubikey.ramfsMountPoint}/new_key - ${optionalString (!yubikey.multiUser) '' - echo -n "$new_uuid_r$new_aes_blob" > ${yubikey.storage.mountPoint}${yubikey.storage.path} - ''} + if [ $? == "0" ]; then + echo -n "$new_uuid_r" > ${yubikey.storage.mountPoint}${yubikey.storage.path} + else + echo "Warning: Could not update LUKS key, current challenge persists!" + fi + + rm -f ${yubikey.ramfsMountPoint}/new_key + umount ${yubikey.ramfsMountPoint} + rm -rf ${yubikey.ramfsMountPoint} else echo "Warning: Could not obtain new UUID, current challenge persists!" fi @@ -336,21 +286,21 @@ in description = "Whether to use a passphrase and a Yubikey (true), or only a Yubikey (false)"; }; - multiUser = mkOption { - default = false; - type = types.bool; - description = "Whether to allow multiple users to authenticate with a Yubikey"; - }; - slot = mkOption { default = 2; type = types.int; description = "Which slot on the Yubikey to challenge"; }; + ramfsMountPoint = mkOption { + default = "/crypt-ramfs"; + type = types.string; + description = "Path where the ramfs used to update the LUKS key will be mounted in stage-1"; + }; + storage = mkOption { type = types.optionSet; - description = "Options related to the authentication record"; + description = "Options related to the storing the random UUID"; options = { device = mkOption { @@ -358,7 +308,7 @@ in type = types.path; description = '' An unencrypted device that will temporarily be mounted in stage-1. - Must contain the authentication record for this LUKS device. + Must contain the current random UUID to create the challenge for this LUKS device. ''; }; @@ -378,7 +328,7 @@ in default = "/crypt-storage/default"; type = types.string; description = '' - Absolute path of the authentication record on the unencrypted device with + Absolute path of the random UUID on the unencrypted device with that device's root directory as "/". ''; }; From 09f9af17b46c8c6dad4df82214afa9f0b15af3b9 Mon Sep 17 00:00:00 2001 From: Moritz Maxeiner Date: Wed, 5 Feb 2014 17:10:59 +0100 Subject: [PATCH 2/2] Update to the Yubikey PBA Security-relevant changes: * No (salted) passphrase hash send to the yubikey, only hash of the salt (as it was in the original implementation). * Derive $k_luks with PBKDF2 from the yubikey $response (as the PBKDF2 salt) and the passphrase $k_user (as the PBKDF2 password), so that if two-factor authentication is enabled (a) a USB-MITM attack on the yubikey itself is not enough to break the system (b) the potentially low-entropy $k_user is better protected against brute-force attacks * Instead of using uuidgen, gather the salt (previously random uuid / uuid_r) directly from /dev/random. * Length of the new salt in byte added as the parameter "saltLength", defaults to 16 byte. Note: Length of the challenge is 64 byte, so saltLength > 64 may have no benefit over saltLengh = 64. * Length of $k_luks derived with PBKDF2 in byte added as the parameter "keyLength", defaults to 64 byte. Example: For a luks device with a 512-bit key, keyLength should be 64. * Increase of the PBKDF2 iteration count per successful authentication added as the parameter "iterationStep", defaults to 0. Other changes: * Add optional grace period before trying to find the yubikey, defaults to 2 seconds. Full overview of the yubikey authentication process: (1) Read $salt and $iterations from unencrypted device (UD). (2) Calculate the $challenge from the $salt with a hash function. Chosen instantiation: SHA-512($salt). (3) Challenge the yubikey with the $challenge and receive the $response. (4) Repeat three times: (a) Prompt for the passphrase $k_user. (b) Derive the key $k_luks for the luks device with a key derivation function from $k_user and $response. Chosen instantiation: PBKDF2(HMAC-SHA-512, $k_user, $response, $iterations, keyLength). (c) Try to open the luks device with $k_luks and escape loop (4) only on success. (5) Proceed only if luks device was opened successfully, fail otherwise. (6) Gather $new_salt from a cryptographically secure pseudorandom number generator Chosen instantiation: /dev/random (7) Calculate the $new_challenge from the $new_salt with the same hash function as (2). (8) Challenge the yubikey with the $new_challenge and receive the $new_response. (9) Derive the new key $new_k_luks for the luks device in the same manner as in (4) (b), but with more iterations as given by iterationStep. (10) Try to change the luks device's key $k_luks to $new_k_luks. (11) If (10) was successful, write the $new_salt and the $new_iterations to the UD. Note: $new_iterations = $iterations + iterationStep Known (software) attack vectors: * A MITM attack on the keyboard can recover $k_user. This, combined with a USB-MITM attack on the yubikey for the $response (1) or the $new_response (2) will result in (1) $k_luks being recovered, (2) $new_k_luks being recovered. * Any attacker with access to the RAM state of stage-1 at mid- or post-authentication can recover $k_user, $k_luks, and $new_k_luks * If an attacker has recovered $response or $new_response, he can perform a brute-force attack on $k_user with it without the Yubikey needing to be present (using cryptsetup's "luksOpen --verify-passphrase" oracle. He could even make a copy of the luks device's luks header and run the brute-force attack without further access to the system. * A USB-MITM attack on the yubikey will allow an attacker to attempt to brute-force the yubikey's internal key ("shared secret") without it needing to be present anymore. Credits: * Florian Klien, for the original concept and the reference implementation over at https://github.com/flowolf/initramfs_ykfde * Anthony Thysse, for the reference implementation of accessing OpenSSL's PBKDF2 over at http://www.ict.griffith.edu.au/anthony/software/pbkdf2.c --- nixos/modules/system/boot/luksroot.nix | 153 ++++++++++++++-------- nixos/modules/system/boot/pbkdf2-sha512.c | 38 ++++++ 2 files changed, 137 insertions(+), 54 deletions(-) create mode 100644 nixos/modules/system/boot/pbkdf2-sha512.c diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix index d70d13411669..117c526fcd38 100644 --- a/nixos/modules/system/boot/luksroot.nix +++ b/nixos/modules/system/boot/luksroot.nix @@ -43,22 +43,33 @@ let } hextorb() { - ( tr '[:lower:]' '[:upper:]' | sed -e 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/gI'| xargs printf ) + ( tr '[:lower:]' '[:upper:]' | sed -e 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/gI' | xargs printf ) } open_yubikey() { + # Make all of these local to this function + # to prevent their values being leaked + local salt + local iterations + local k_user + local challenge + local response + local k_luks + local opened + local new_salt + local new_iterations + local new_challenge + local new_response + local new_k_luks + mkdir -p ${yubikey.storage.mountPoint} mount -t ${yubikey.storage.fsType} ${toString yubikey.storage.device} ${yubikey.storage.mountPoint} - local uuid_r - local k_user - local challenge - local opened - - sleep 1 - - uuid_r="$(cat ${yubikey.storage.mountPoint}${yubikey.storage.path})" + salt="$(cat ${yubikey.storage.mountPoint}${yubikey.storage.path} | sed -n 1p | tr -d '\n')" + iterations="$(cat ${yubikey.storage.mountPoint}${yubikey.storage.path} | sed -n 2p | tr -d '\n')" + challenge="$(echo -n $salt | openssl-wrap dgst -binary -sha512 | rbtohex)" + response="$(ykchalresp -${toString yubikey.slot} -x $challenge 2>/dev/null)" for try in $(seq 3); do @@ -68,9 +79,11 @@ let echo ''} - challenge="$(echo -n $k_user$uuid_r | openssl-wrap dgst -binary -sha512 | rbtohex)" - - k_luks="$(ykchalresp -${toString yubikey.slot} -x $challenge 2>/dev/null)" + if [ ! -z "$k_user" ]; then + k_luks="$(echo -n $k_user | pbkdf2-sha512 ${toString yubikey.keyLength} $iterations $response | rbtohex)" + else + k_luks="$(echo | pbkdf2-sha512 ${toString yubikey.keyLength} $iterations $response | rbtohex)" + fi echo -n "$k_luks" | hextorb | cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} --key-file=- @@ -89,53 +102,60 @@ let exit 1 fi - update_failed=false + echo -n "Gathering entropy for new salt (please enter random keys to generate entropy if this blocks for long)..." + for i in $(seq ${toString yubikey.saltLength}); do + byte="$(dd if=/dev/random bs=1 count=1 2>/dev/null | rbtohex)"; + new_salt="$new_salt$byte"; + echo -n . + done; + echo "ok" - local new_uuid_r - new_uuid_r="$(uuidgen)" - if [ $? != "0" ]; then - for try in $(seq 10); do - sleep 1 - new_uuid_r="$(uuidgen)" - if [ $? == "0" ]; then break; fi - if [ $try -eq 10 ]; then update_failed=true; fi - done - fi + new_iterations="$iterations" + ${optionalString (yubikey.iterationStep > 0) '' + new_iterations="$(($new_iterations + ${toString yubikey.iterationStep}))" + ''} - if [ "$update_failed" == false ]; then - new_uuid_r="$(echo -n $new_uuid_r | head -c 36 | tr -d '-')" + new_challenge="$(echo -n $new_salt | openssl-wrap dgst -binary -sha512 | rbtohex)" - local new_challenge - new_challenge="$(echo -n $k_user$new_uuid_r | openssl-wrap dgst -binary -sha512 | rbtohex)" + new_response="$(ykchalresp -${toString yubikey.slot} -x $new_challenge 2>/dev/null)" - local new_k_luks - new_k_luks="$(ykchalresp -${toString yubikey.slot} -x $new_challenge 2>/dev/null)" - - 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} - - echo -n "$new_k_luks" | hextorb > ${yubikey.ramfsMountPoint}/new_key - echo -n "$k_luks" | cryptsetup luksChangeKey ${device} --key-file=- ${yubikey.ramfsMountPoint}/new_key - - if [ $? == "0" ]; then - echo -n "$new_uuid_r" > ${yubikey.storage.mountPoint}${yubikey.storage.path} - else - echo "Warning: Could not update LUKS key, current challenge persists!" - fi - - rm -f ${yubikey.ramfsMountPoint}/new_key - umount ${yubikey.ramfsMountPoint} - rm -rf ${yubikey.ramfsMountPoint} + if [ ! -z "$k_user" ]; then + new_k_luks="$(echo -n $k_user | pbkdf2-sha512 ${toString yubikey.keyLength} $new_iterations $new_response | rbtohex)" else - echo "Warning: Could not obtain new UUID, current challenge persists!" + new_k_luks="$(echo | pbkdf2-sha512 ${toString yubikey.keyLength} $new_iterations $new_response | rbtohex)" 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} + + echo -n "$new_k_luks" | hextorb > ${yubikey.ramfsMountPoint}/new_key + echo -n "$k_luks" | hextorb | cryptsetup luksChangeKey ${device} --key-file=- ${yubikey.ramfsMountPoint}/new_key + + if [ $? == "0" ]; then + echo -ne "$new_salt\n$new_iterations" > ${yubikey.storage.mountPoint}${yubikey.storage.path} + else + echo "Warning: Could not update LUKS key, current challenge persists!" + fi + + rm -f ${yubikey.ramfsMountPoint}/new_key + umount ${yubikey.ramfsMountPoint} + rm -rf ${yubikey.ramfsMountPoint} + umount ${yubikey.storage.mountPoint} } + ${optionalString (yubikey.gracePeriod > 0) '' + echo -n "Waiting ${toString yubikey.gracePeriod} seconds as grace..." + for i in $(seq ${toString yubikey.gracePeriod}); do + sleep 1 + echo -n . + done + echo "ok" + ''} + yubikey_missing=true ykinfo -v 1>/dev/null 2>&1 if [ $? != "0" ]; then @@ -292,6 +312,30 @@ in description = "Which slot on the Yubikey to challenge"; }; + saltLength = mkOption { + default = 16; + type = types.int; + description = "Length of the new salt in byte (64 is the effective maximum)"; + }; + + keyLength = mkOption { + default = 64; + type = types.int; + description = "Length of the LUKS slot key derived with PBKDF2 in byte"; + }; + + iterationStep = mkOption { + default = 0; + type = types.int; + description = "How much the iteration count for PBKDF2 is increased at each successful authentication"; + }; + + gracePeriod = mkOption { + default = 2; + type = types.int; + description = "Time in seconds to wait before attempting to find the Yubikey"; + }; + ramfsMountPoint = mkOption { default = "/crypt-ramfs"; type = types.string; @@ -300,7 +344,7 @@ in storage = mkOption { type = types.optionSet; - description = "Options related to the storing the random UUID"; + description = "Options related to the storing the salt"; options = { device = mkOption { @@ -308,7 +352,7 @@ in type = types.path; description = '' An unencrypted device that will temporarily be mounted in stage-1. - Must contain the current random UUID to create the challenge for this LUKS device. + Must contain the current salt to create the challenge for this LUKS device. ''; }; @@ -328,7 +372,7 @@ in default = "/crypt-storage/default"; type = types.string; description = '' - Absolute path of the random UUID on the unencrypted device with + Absolute path of the salt on the unencrypted device with that device's root directory as "/". ''; }; @@ -370,11 +414,13 @@ in cp -pdv ${pkgs.popt}/lib/libpopt*.so.* $out/lib ${optionalString luks.yubikeySupport '' - cp -pdv ${pkgs.utillinux}/bin/uuidgen $out/bin cp -pdv ${pkgs.ykpers}/bin/ykchalresp $out/bin cp -pdv ${pkgs.ykpers}/bin/ykinfo $out/bin cp -pdv ${pkgs.openssl}/bin/openssl $out/bin + cc -O3 -I${pkgs.openssl}/include -L${pkgs.openssl}/lib ${./pbkdf2-sha512.c} -o $out/bin/pbkdf2-sha512 -lcrypto + strip -s $out/bin/pbkdf2-sha512 + cp -pdv ${pkgs.libusb1}/lib/libusb*.so.* $out/lib cp -pdv ${pkgs.ykpers}/lib/libykpers*.so.* $out/lib cp -pdv ${pkgs.libyubikey}/lib/libyubikey*.so.* $out/lib @@ -394,7 +440,6 @@ EOF boot.initrd.extraUtilsCommandsTest = '' $out/bin/cryptsetup --version ${optionalString luks.yubikeySupport '' - $out/bin/uuidgen --version $out/bin/ykchalresp -V $out/bin/ykinfo -V cat > $out/bin/openssl-wrap < +#include +#include +#include + +void hextorb(uint8_t* hex, uint8_t* rb) +{ + while(sscanf(hex, "%2x", rb) == 1) + { + hex += 2; + rb += 1; + } + *rb = '\0'; +} + +int main(int argc, char** argv) +{ + uint8_t k_user[2048]; + uint8_t salt[2048]; + uint8_t key[4096]; + + uint32_t key_length = atoi(argv[1]); + uint32_t iteration_count = atoi(argv[2]); + + hextorb(argv[3], salt); + uint32_t salt_length = strlen(argv[3]) / 2; + + fgets(k_user, 2048, stdin); + uint32_t k_user_length = strlen(k_user); + if(k_user[k_user_length - 1] == '\n') { + k_user[k_user_length - 1] = '\0'; + } + + PKCS5_PBKDF2_HMAC(k_user, k_user_length, salt, salt_length, iteration_count, EVP_sha512(), key_length, key); + fwrite(key, 1, key_length, stdout); + + return 0; +} \ No newline at end of file