From dc653449c541312a120b2dd25fad118e04828b62 Mon Sep 17 00:00:00 2001 From: Jan Malakhovski Date: Sun, 10 Jun 2018 20:18:55 +0000 Subject: [PATCH 1/6] nixos: boot/stage-1: check syntax of the generated script --- nixos/modules/system/boot/stage-1.nix | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix index 71b806a0b4e1a8..2caab69cbb95ce 100644 --- a/nixos/modules/system/boot/stage-1.nix +++ b/nixos/modules/system/boot/stage-1.nix @@ -248,6 +248,14 @@ let isExecutable = true; + postInstall = '' + echo checking syntax + # check both with bash + ${pkgs.bash}/bin/sh -n $target + # and with ash shell, just in case + ${extraUtils}/bin/ash -n $target + ''; + inherit udevRules extraUtils modulesClosure; inherit (config.boot) resumeDevice; From 12e6907f33b79bb74d2b1502f4d8ff09c5ad56d8 Mon Sep 17 00:00:00 2001 From: Jan Malakhovski Date: Fri, 15 Sep 2017 21:47:36 +0000 Subject: [PATCH 2/6] nixos: initrd/luks: cleanup and generalize common shell expressions Also fix Yubikey timeout handling mess. --- nixos/modules/system/boot/luksroot.nix | 112 ++++++++++++++----------- 1 file changed, 61 insertions(+), 51 deletions(-) diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix index 401c3cc69919f9..8cdb3981d53450 100644 --- a/nixos/modules/system/boot/luksroot.nix +++ b/nixos/modules/system/boot/luksroot.nix @@ -5,39 +5,79 @@ with lib; let luks = config.boot.initrd.luks; - openCommand = name': { name, device, header, keyFile, keyFileSize, keyFileOffset, allowDiscards, yubikey, fallbackToPassword, ... }: assert name' == name; '' + commonFunctions = '' + die() { + echo "$@" >&2 + exit 1 + } - # Wait for a target (e.g. device, keyFile, header, ...) to appear. wait_target() { local name="$1" local target="$2" + local secs="''${3:-10}" + local desc="''${4:-$name $target to appear}" if [ ! -e $target ]; then - echo -n "Waiting 10 seconds for $name $target to appear" + echo -n "Waiting $secs seconds for $desc..." local success=false; - for try in $(seq 10); do + for try in $(seq $secs); do echo -n "." sleep 1 - if [ -e $target ]; then success=true break; fi + if [ -e $target ]; then + success=true + break + fi done - if [ $success = true ]; then + if [ $success == true ]; then echo " - success"; + return 0 else echo " - failure"; + return 1 fi fi + return 0 } + wait_yubikey() { + local secs="''${1:-10}" + + ykinfo -v 1>/dev/null 2>&1 + if [ $? != 0 ]; then + echo -n "Waiting $secs seconds for Yubikey to appear..." + local success=false + for try in $(seq $secs); do + echo -n . + sleep 1 + ykinfo -v 1>/dev/null 2>&1 + if [ $? == 0 ]; then + success=true + break + fi + done + if [ $success == true ]; then + echo " - success"; + return 0 + else + echo " - failure"; + return 1 + fi + fi + return 0 + } + ''; + + openCommand = name': { name, device, header, keyFile, keyFileSize, keyFileOffset, allowDiscards, yubikey, fallbackToPassword, ... }: assert name' == name; '' # Wait for luksRoot (and optionally keyFile and/or header) to appear, e.g. # if on a USB drive. - wait_target "device" ${device} + wait_target "device" ${device} || die "${device} is unavailable" - ${optionalString (keyFile != null) '' - wait_target "key file" ${keyFile} + ${optionalString (header != null) '' + wait_target "header" ${header} || die "${header} is unavailable" ''} - ${optionalString (header != null) '' - wait_target "header" ${header} + ${optionalString (keyFile != null) '' + wait_target "key file" ${keyFile} || die "${keyFile} is unavailable" ''} open_normally() { @@ -70,7 +110,6 @@ let } open_yubikey() { - # Make all of these local to this function # to prevent their values being leaked local salt @@ -95,10 +134,9 @@ let response="$(ykchalresp -${toString yubikey.slot} -x $challenge 2>/dev/null)" for try in $(seq 3); do - ${optionalString yubikey.twoFactor '' echo -n "Enter two-factor passphrase: " - read -s k_user + read -rs k_user echo ''} @@ -110,7 +148,7 @@ let echo -n "$k_luks" | hextorb | cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} --key-file=- - if [ $? == "0" ]; then + if [ $? == 0 ]; then opened=true break else @@ -121,8 +159,7 @@ let if [ "$opened" == false ]; then umount ${yubikey.storage.mountPoint} - echo "Maximum authentication errors reached" - exit 1 + die "Maximum authentication errors reached" fi echo -n "Gathering entropy for new salt (please enter random keys to generate entropy if this blocks for long)..." @@ -157,7 +194,7 @@ let 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 + 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!" @@ -170,38 +207,11 @@ let 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 - echo -n "waiting 10 seconds for yubikey to appear..." - for try in $(seq 10); do - sleep 1 - ykinfo -v 1>/dev/null 2>&1 - if [ $? == "0" ]; then - yubikey_missing=false - break - fi - echo -n . - done - echo "ok" + if wait_yubikey ${toString yubikey.gracePeriod}; then + open_yubikey else - yubikey_missing=false - fi - - if [ "$yubikey_missing" == true ]; then echo "no yubikey found, falling back to non-yubikey open procedure" open_normally - else - open_yubikey fi ''} @@ -397,9 +407,9 @@ in }; gracePeriod = mkOption { - default = 2; + default = 10; type = types.int; - description = "Time in seconds to wait before attempting to find the Yubikey."; + description = "Time in seconds to wait for the Yubikey."; }; ramfsMountPoint = mkOption { @@ -520,8 +530,8 @@ in ''} ''; - boot.initrd.preLVMCommands = concatStrings (mapAttrsToList openCommand preLVM); - boot.initrd.postDeviceCommands = concatStrings (mapAttrsToList openCommand postLVM); + boot.initrd.preLVMCommands = commonFunctions + concatStrings (mapAttrsToList openCommand preLVM); + boot.initrd.postDeviceCommands = commonFunctions + concatStrings (mapAttrsToList openCommand postLVM); environment.systemPackages = [ pkgs.cryptsetup ]; }; From a9d69a74d6edb6bcca29b1189d4bc3b203ecaf25 Mon Sep 17 00:00:00 2001 From: Jan Malakhovski Date: Fri, 15 Sep 2017 21:48:51 +0000 Subject: [PATCH 3/6] nixos: initrd/luks: change passphrases handling Also reuse common cryptsetup invocation subexpressions. - Passphrase reading is done via the shell now, not by cryptsetup. This way the same passphrase can be reused between cryptsetup invocations, which this module now tries to do by default (can be disabled). - Number of retries is now infinity, it makes no sense to make users reboot when they fail to type in their passphrase. --- nixos/modules/system/boot/luksroot.nix | 175 ++++++++++++++++++------- 1 file changed, 130 insertions(+), 45 deletions(-) diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix index 8cdb3981d53450..ea6d189d9907c0 100644 --- a/nixos/modules/system/boot/luksroot.nix +++ b/nixos/modules/system/boot/luksroot.nix @@ -67,7 +67,25 @@ let } ''; - openCommand = name': { name, device, header, keyFile, keyFileSize, keyFileOffset, allowDiscards, yubikey, fallbackToPassword, ... }: assert name' == name; '' + preCommands = '' + # A place to store crypto things + + # 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! + mkdir -p /crypt-ramfs + mount -t ramfs none /crypt-ramfs + ''; + + postCommands = '' + umount /crypt-ramfs 2>/dev/null + ''; + + openCommand = name': { name, device, header, keyFile, keyFileSize, keyFileOffset, allowDiscards, yubikey, fallbackToPassword, ... }: assert name' == name; + let + csopen = "cryptsetup luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} ${optionalString (header != null) "--header=${header}"}"; + cschange = "cryptsetup luksChangeKey ${device} ${optionalString (header != null) "--header=${header}"}"; + in '' # Wait for luksRoot (and optionally keyFile and/or header) to appear, e.g. # if on a USB drive. wait_target "device" ${device} || die "${device} is unavailable" @@ -76,31 +94,72 @@ let wait_target "header" ${header} || die "${header} is unavailable" ''} - ${optionalString (keyFile != null) '' - wait_target "key file" ${keyFile} || die "${keyFile} is unavailable" - ''} + do_open_passphrase() { + local passphrase + + while true; do + echo -n "Passphrase for ${device}: " + passphrase= + while true; do + if [ -e /crypt-ramfs/passphrase ]; then + echo "reused" + passphrase=$(cat /crypt-ramfs/passphrase) + break + else + # ask cryptsetup-askpass + echo -n "${device}" > /crypt-ramfs/device + + # and try reading it from /dev/console + IFS= read -t 1 -rs passphrase + if [ -n "$passphrase" ]; then + ${if luks.reusePassphrases then '' + # remember it for the next device + echo -n "$passphrase" > /crypt-ramfs/passphrase + '' else '' + # Don't save it to ramfs. We are very paranoid + ''} + echo + break + fi + fi + done + echo -n "Verifiying passphrase for ${device}..." + echo -n "$passphrase" | ${csopen} --key-file=- + if [ $? == 0 ]; then + echo " - success" + ${if luks.reusePassphrases then '' + # we don't rm here because we might reuse it for the next device + '' else '' + rm -f /crypt-ramfs/passphrase + ''} + break + else + echo " - failure" + # ask for a different one + rm -f /crypt-ramfs/passphrase + fi + done + } + # LUKS open_normally() { - echo luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} \ - ${optionalString (header != null) "--header=${header}"} \ - > /.luksopen_args - ${optionalString (keyFile != null) '' - ${optionalString fallbackToPassword "if [ -e ${keyFile} ]; then"} - echo " --key-file=${keyFile} ${optionalString (keyFileSize != null) "--keyfile-size=${toString keyFileSize}"}" \ - "${optionalString (keyFileOffset != null) "--keyfile-offset=${toString keyFileOffset}"}" \ - >> /.luksopen_args - ${optionalString fallbackToPassword '' + ${if (keyFile != null) then '' + if wait_target "key file" ${keyFile}; then + ${csopen} --key-file=${keyFile} \ + ${optionalString (keyFileSize != null) "--keyfile-size=${toString keyFileSize}"} \ + ${optionalString (keyFileOffset != null) "--keyfile-offset=${toString keyFileOffset}"} else - echo "keyfile ${keyFile} not found -- fallback to interactive unlocking" + ${if fallbackToPassword then "echo" else "die"} "${keyFile} is unavailable" + echo " - failing back to interactive password prompt" + do_open_passphrase fi + '' else '' + do_open_passphrase ''} - ''} - cryptsetup-askpass - rm /.luksopen_args } - ${optionalString (luks.yubikeySupport && (yubikey != null)) '' - + ${if luks.yubikeySupport && (yubikey != null) then '' + # Yubikey rbtohex() { ( od -An -vtx1 | tr -d ' \n' ) } @@ -109,7 +168,7 @@ let ( tr '[:lower:]' '[:upper:]' | sed -e 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/gI' | xargs printf ) } - open_yubikey() { + do_open_yubikey() { # Make all of these local to this function # to prevent their values being leaked local salt @@ -146,7 +205,7 @@ let 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=- + echo -n "$k_luks" | hextorb | ${csopen} --key-file=- if [ $? == 0 ]; then opened=true @@ -192,7 +251,7 @@ let 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 + echo -n "$k_luks" | hextorb | ${cschange} --key-file=- ${yubikey.ramfsMountPoint}/new_key if [ $? == 0 ]; then echo -ne "$new_salt\n$new_iterations" > ${yubikey.storage.mountPoint}${yubikey.storage.path} @@ -207,20 +266,39 @@ let umount ${yubikey.storage.mountPoint} } - if wait_yubikey ${toString yubikey.gracePeriod}; then - open_yubikey - else - echo "no yubikey found, falling back to non-yubikey open procedure" - open_normally - fi - ''} + open_yubikey() { + if wait_yubikey ${toString yubikey.gracePeriod}; then + do_open_yubikey + else + echo "No yubikey found, falling back to non-yubikey open procedure" + open_normally + fi + } - # open luksRoot and scan for logical volumes - ${optionalString ((!luks.yubikeySupport) || (yubikey == null)) '' + open_yubikey + '' else '' open_normally ''} ''; + askPass = pkgs.writeScriptBin "cryptsetup-askpass" '' + #!/bin/sh + + ${commonFunctions} + + while true; do + wait_target "luks" /crypt-ramfs/device 10 "LUKS to request a passphrase" || die "Passphrase is not requested now" + device=$(cat /crypt-ramfs/device) + + echo -n "Passphrase for $device: " + IFS= read -rs passphrase + echo + + rm /crypt-ramfs/device + echo -n "$passphrase" > /crypt-ramfs/passphrase + done + ''; + preLVM = filterAttrs (n: v: v.preLVM) luks.devices; postLVM = filterAttrs (n: v: !v.preLVM) luks.devices; @@ -266,6 +344,22 @@ in ''; }; + boot.initrd.luks.reusePassphrases = mkOption { + type = types.bool; + default = true; + description = '' + When opening a new LUKS device try reusing last successful + passphrase. + + Useful for mounting a number of devices that use the same + passphrase without retyping it several times. + + Such setup can be useful if you use cryptsetup + luksSuspend. Different LUKS devices will still have + different master keys even when using the same passphrase. + ''; + }; + boot.initrd.luks.devices = mkOption { default = { }; example = { "luksroot".device = "/dev/disk/by-uuid/430e9eff-d852-4f68-aa3b-2fa3599ebe08"; }; @@ -487,18 +581,8 @@ in # copy the cryptsetup binary and it's dependencies boot.initrd.extraUtilsCommands = '' copy_bin_and_libs ${pkgs.cryptsetup}/bin/cryptsetup - - cat > $out/bin/cryptsetup-askpass < Date: Sun, 10 Jun 2018 20:18:21 +0000 Subject: [PATCH 4/6] nixos: initrd/luks: simplify Yubikey handling code From reading the source I'm pretty sure it doesn't support multiple Yubikeys, hence those options are useless. Also, I'm pretty sure nobody actually uses this feature, because enabling it causes extra utils' checks to fail (even before applying any patches of this branch). As I don't have the hardware to test this, I'm too lazy to fix the utils, but I did test that with extra utils checks commented out and Yubikey enabled the resulting script still passes the syntax check. --- nixos/modules/system/boot/luksroot.nix | 52 ++++++++------------------ 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix index ea6d189d9907c0..5f42c76d5d7f4a 100644 --- a/nixos/modules/system/boot/luksroot.nix +++ b/nixos/modules/system/boot/luksroot.nix @@ -75,9 +75,13 @@ let # Warning: Do NOT replace with tmpfs! mkdir -p /crypt-ramfs mount -t ramfs none /crypt-ramfs + + # For Yubikey salt storage + mkdir -p /crypt-storage ''; postCommands = '' + umount /crypt-storage 2>/dev/null umount /crypt-ramfs 2>/dev/null ''; @@ -184,11 +188,11 @@ let local new_response local new_k_luks - mkdir -p ${yubikey.storage.mountPoint} - mount -t ${yubikey.storage.fsType} ${toString yubikey.storage.device} ${yubikey.storage.mountPoint} + mount -t ${yubikey.storage.fsType} ${yubikey.storage.device} /crypt-storage || \ + die "Failed to mount Yubikey salt storage device" - 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')" + salt="$(cat /crypt-storage${yubikey.storage.path} | sed -n 1p | tr -d '\n')" + iterations="$(cat /crypt-storage${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)" @@ -216,10 +220,7 @@ let fi done - if [ "$opened" == false ]; then - umount ${yubikey.storage.mountPoint} - die "Maximum authentication errors reached" - fi + [ "$opened" == false ] && die "Maximum authentication errors reached" 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 @@ -244,26 +245,17 @@ let 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 | ${cschange} --key-file=- ${yubikey.ramfsMountPoint}/new_key + echo -n "$new_k_luks" | hextorb > /crypt-ramfs/new_key + echo -n "$k_luks" | hextorb | ${cschange} --key-file=- /crypt-ramfs/new_key if [ $? == 0 ]; then - echo -ne "$new_salt\n$new_iterations" > ${yubikey.storage.mountPoint}${yubikey.storage.path} + echo -ne "$new_salt\n$new_iterations" > /crypt-storage${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} + rm -f /crypt-ramfs/new_key + umount /crypt-storage } open_yubikey() { @@ -506,12 +498,6 @@ in description = "Time in seconds to wait for the Yubikey."; }; - ramfsMountPoint = mkOption { - default = "/crypt-ramfs"; - type = types.str; - description = "Path where the ramfs used to update the LUKS key will be mounted during early boot."; - }; - /* TODO: Add to the documentation of the current module: Options related to the storing the salt. @@ -532,12 +518,6 @@ in description = "The filesystem of the unencrypted device."; }; - mountPoint = mkOption { - default = "/crypt-storage"; - type = types.str; - description = "Path where the unencrypted device will be mounted during early boot."; - }; - path = mkOption { default = "/crypt-storage/default"; type = types.str; @@ -550,8 +530,8 @@ in }; }); }; - - }; })); + }; + })); }; boot.initrd.luks.yubikeySupport = mkOption { From 8c83ba03867e2aef97d331c902e745dc9cafba9d Mon Sep 17 00:00:00 2001 From: Jan Malakhovski Date: Sun, 10 Jun 2018 20:18:27 +0000 Subject: [PATCH 5/6] nixos: initrd/luks: disable input echo for the whole stage --- nixos/modules/system/boot/luksroot.nix | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix index 5f42c76d5d7f4a..27c1f891f485a2 100644 --- a/nixos/modules/system/boot/luksroot.nix +++ b/nixos/modules/system/boot/luksroot.nix @@ -78,9 +78,15 @@ let # For Yubikey salt storage mkdir -p /crypt-storage + + # Disable all input echo for the whole stage. We could use read -s + # instead but that would ocasionally leak characters between read + # invocations. + stty -echo ''; postCommands = '' + stty echo umount /crypt-storage 2>/dev/null umount /crypt-ramfs 2>/dev/null ''; @@ -113,8 +119,8 @@ let # ask cryptsetup-askpass echo -n "${device}" > /crypt-ramfs/device - # and try reading it from /dev/console - IFS= read -t 1 -rs passphrase + # and try reading it from /dev/console with a timeout + IFS= read -t 1 -r passphrase if [ -n "$passphrase" ]; then ${if luks.reusePassphrases then '' # remember it for the next device @@ -199,7 +205,7 @@ let for try in $(seq 3); do ${optionalString yubikey.twoFactor '' echo -n "Enter two-factor passphrase: " - read -rs k_user + read -r k_user echo ''} From 456f97f2e699511f4ff7a81b4e4b61cc66e62863 Mon Sep 17 00:00:00 2001 From: Jan Malakhovski Date: Mon, 6 Aug 2018 16:55:39 +0000 Subject: [PATCH 6/6] doc: document luksroot.nix changes in release notes --- nixos/doc/manual/release-notes/rl-1809.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/nixos/doc/manual/release-notes/rl-1809.xml b/nixos/doc/manual/release-notes/rl-1809.xml index a70fb1aedbb86b..d527984f5ef142 100644 --- a/nixos/doc/manual/release-notes/rl-1809.xml +++ b/nixos/doc/manual/release-notes/rl-1809.xml @@ -190,6 +190,16 @@ $ nix-instantiate -E '(import <nixpkgsunstable> {}).gitFull' which indicates that the nix output hash will be used as tag. + + + Options + boot.initrd.luks.devices.name.yubikey.ramfsMountPoint + boot.initrd.luks.devices.name.yubikey.storage.mountPoint + were removed. luksroot.nix module never supported more than one YubiKey at + a time anyway, hence those options never had any effect. You should be able to remove them + from your config without any issues. + +