diff options
Diffstat (limited to 'nixos/modules/system/boot')
-rw-r--r-- | nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py | 1 | ||||
-rw-r--r-- | nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix | 34 | ||||
-rw-r--r-- | nixos/modules/system/boot/luksroot.nix | 337 | ||||
-rw-r--r-- | nixos/modules/system/boot/stage-1.nix | 8 | ||||
-rw-r--r-- | nixos/modules/system/boot/systemd.nix | 5 |
5 files changed, 261 insertions, 124 deletions
diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py index 3333569c36be..1dc888c58227 100644 --- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py +++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py @@ -43,6 +43,7 @@ def write_loader_conf(profile, generation): f.write("default nixos-generation-%d\n" % (generation)) if not @editor@: f.write("editor 0"); + f.write("console-mode @consoleMode@\n"); os.rename("@efiSysMountPoint@/loader/loader.conf.tmp", "@efiSysMountPoint@/loader/loader.conf") def profile_path(profile, generation, name): diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix index a5a88a99be8f..feed863efd66 100644 --- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix +++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix @@ -22,6 +22,8 @@ let editor = if cfg.editor then "True" else "False"; + inherit (cfg) consoleMode; + inherit (efi) efiSysMountPoint canTouchEfiVariables; }; in { @@ -52,6 +54,38 @@ in { compatibility. ''; }; + + consoleMode = mkOption { + default = "keep"; + + type = types.enum [ "0" "1" "2" "auto" "max" "keep" ]; + + description = '' + The resolution of the console. The following values are valid: + </para> + <para> + <itemizedlist> + <listitem><para> + <literal>"0"</literal>: Standard UEFI 80x25 mode + </para></listitem> + <listitem><para> + <literal>"1"</literal>: 80x50 mode, not supported by all devices + </para></listitem> + <listitem><para> + <literal>"2"</literal>: The first non-standard mode provided by the device firmware, if any + </para></listitem> + <listitem><para> + <literal>"auto"</literal>: Pick a suitable mode automatically using heuristics + </para></listitem> + <listitem><para> + <literal>"max"</literal>: Pick the highest-numbered available mode + </para></listitem> + <listitem><para> + <literal>"keep"</literal>: Keep the mode selected by firmware (the default) + </para></listitem> + </itemizedlist> + ''; + }; }; config = mkIf cfg.enable { diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix index 7ebfdb134d7d..27c1f891f485 100644 --- a/nixos/modules/system/boot/luksroot.nix +++ b/nixos/modules/system/boot/luksroot.nix @@ -5,61 +5,171 @@ with lib; let luks = config.boot.initrd.luks; - openCommand = name': { name, device, header, keyFile, keyFileSize, 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 + } + ''; + + 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 + + # 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 + ''; + + 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} - - ${optionalString (keyFile != null) '' - wait_target "key file" ${keyFile} - ''} + wait_target "device" ${device} || die "${device} is unavailable" ${optionalString (header != null) '' - wait_target "header" ${header} + wait_target "header" ${header} || die "${header} 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 with a timeout + IFS= read -t 1 -r 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}"}" \ - >> /.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' ) } @@ -68,8 +178,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 @@ -85,19 +194,18 @@ 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)" for try in $(seq 3); do - ${optionalString yubikey.twoFactor '' echo -n "Enter two-factor passphrase: " - read -s k_user + read -r k_user echo ''} @@ -107,9 +215,9 @@ 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 + if [ $? == 0 ]; then opened=true break else @@ -118,11 +226,7 @@ let fi done - if [ "$opened" == false ]; then - umount ${yubikey.storage.mountPoint} - echo "Maximum authentication errors reached" - exit 1 - 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 @@ -147,67 +251,50 @@ 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 > /crypt-ramfs/new_key + echo -n "$k_luks" | hextorb | ${cschange} --key-file=- /crypt-ramfs/new_key - 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} + if [ $? == 0 ]; then + 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} + rm -f /crypt-ramfs/new_key + umount /crypt-storage + } - umount ${yubikey.storage.mountPoint} + 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 } - ${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" + open_yubikey + '' else '' + open_normally ''} + ''; - 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" - 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 - ''} + askPass = pkgs.writeScriptBin "cryptsetup-askpass" '' + #!/bin/sh - # open luksRoot and scan for logical volumes - ${optionalString ((!luks.yubikeySupport) || (yubikey == null)) '' - open_normally - ''} + ${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; @@ -255,6 +342,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 <command>cryptsetup + luksSuspend</command>. 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"; }; @@ -316,6 +419,19 @@ in ''; }; + keyFileOffset = mkOption { + default = null; + example = 4096; + type = types.nullOr types.int; + description = '' + The offset of the key file. Use this in combination with + <literal>keyFileSize</literal> to use part of a file as key file + (often the case if a raw device or partition is used as a key file). + If not specified, the key begins at the first byte of + <literal>keyFile</literal>. + ''; + }; + # FIXME: get rid of this option. preLVM = mkOption { default = true; @@ -383,15 +499,9 @@ in }; gracePeriod = mkOption { - default = 2; + default = 10; type = types.int; - description = "Time in seconds to wait before attempting to find 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."; + description = "Time in seconds to wait for the Yubikey."; }; /* TODO: Add to the documentation of the current module: @@ -414,12 +524,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; @@ -432,8 +536,8 @@ in }; }); }; - - }; })); + }; + })); }; boot.initrd.luks.yubikeySupport = mkOption { @@ -463,18 +567,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 <<EOF - #!$out/bin/sh -e - if [ -e /.luksopen_args ]; then - cryptsetup \$(cat /.luksopen_args) - killall -q cryptsetup - else - echo "Passphrase is not requested now" - exit 1 - fi - EOF - chmod +x $out/bin/cryptsetup-askpass + copy_bin_and_libs ${askPass}/bin/cryptsetup-askpass + sed -i s,/bin/sh,$out/bin/sh, $out/bin/cryptsetup-askpass ${optionalString luks.yubikeySupport '' copy_bin_and_libs ${pkgs.yubikey-personalization}/bin/ykchalresp @@ -506,8 +600,9 @@ in ''} ''; - boot.initrd.preLVMCommands = concatStrings (mapAttrsToList openCommand preLVM); - boot.initrd.postDeviceCommands = concatStrings (mapAttrsToList openCommand postLVM); + boot.initrd.preFailCommands = postCommands; + boot.initrd.preLVMCommands = commonFunctions + preCommands + concatStrings (mapAttrsToList openCommand preLVM) + postCommands; + boot.initrd.postDeviceCommands = commonFunctions + preCommands + concatStrings (mapAttrsToList openCommand postLVM) + postCommands; environment.systemPackages = [ pkgs.cryptsetup ]; }; diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix index 35062d891b8b..f58b68cb3353 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; diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix index 737d1c10db87..12e029ae57f8 100644 --- a/nixos/modules/system/boot/systemd.nix +++ b/nixos/modules/system/boot/systemd.nix @@ -190,9 +190,8 @@ let ]; makeJobScript = name: text: - let mkScriptName = s: (replaceChars [ "\\" ] [ "-" ] (shellEscape s) ); - x = pkgs.writeTextFile { name = "unit-script"; executable = true; destination = "/bin/${mkScriptName name}"; inherit text; }; - in "${x}/bin/${mkScriptName name}"; + let mkScriptName = s: "unit-script-" + (replaceChars [ "\\" "@" ] [ "-" "_" ] (shellEscape s) ); + in pkgs.writeTextFile { name = mkScriptName name; executable = true; inherit text; }; unitConfig = { config, ... }: { config = { |