Merge pull request #29441 from oxij/nixos/luks

nixos: initrd/luks: allow to reuse passphrases, cleanup
This commit is contained in:
Samuel Dionne-Riel 2018-08-08 13:16:57 -04:00 committed by GitHub
commit 27c6bf0ef3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 225 additions and 126 deletions

View File

@ -190,6 +190,16 @@ $ nix-instantiate -E '(import <nixpkgsunstable> {}).gitFull'
which indicates that the nix output hash will be used as tag. which indicates that the nix output hash will be used as tag.
</para> </para>
</listitem> </listitem>
<listitem>
<para>
Options
<literal>boot.initrd.luks.devices.<replaceable>name</replaceable>.yubikey.ramfsMountPoint</literal>
<literal>boot.initrd.luks.devices.<replaceable>name</replaceable>.yubikey.storage.mountPoint</literal>
were removed. <literal>luksroot.nix</literal> 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.
</para>
</listitem>
</itemizedlist> </itemizedlist>
</section> </section>

View File

@ -5,62 +5,171 @@ with lib;
let let
luks = config.boot.initrd.luks; 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() { wait_target() {
local name="$1" local name="$1"
local target="$2" local target="$2"
local secs="''${3:-10}"
local desc="''${4:-$name $target to appear}"
if [ ! -e $target ]; then if [ ! -e $target ]; then
echo -n "Waiting 10 seconds for $name $target to appear" echo -n "Waiting $secs seconds for $desc..."
local success=false; local success=false;
for try in $(seq 10); do for try in $(seq $secs); do
echo -n "." echo -n "."
sleep 1 sleep 1
if [ -e $target ]; then success=true break; fi if [ -e $target ]; then
success=true
break
fi
done done
if [ $success = true ]; then if [ $success == true ]; then
echo " - success"; echo " - success";
return 0
else else
echo " - failure"; echo " - failure";
return 1
fi fi
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. # Wait for luksRoot (and optionally keyFile and/or header) to appear, e.g.
# if on a USB drive. # 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) '' ${optionalString (header != null) ''
wait_target "header" ${header} wait_target "header" ${header} || die "${header} is unavailable"
''} ''}
open_normally() { do_open_passphrase() {
echo luksOpen ${device} ${name} ${optionalString allowDiscards "--allow-discards"} \ local passphrase
${optionalString (header != null) "--header=${header}"} \
> /.luksopen_args while true; do
${optionalString (keyFile != null) '' echo -n "Passphrase for ${device}: "
${optionalString fallbackToPassword "if [ -e ${keyFile} ]; then"} passphrase=
echo " --key-file=${keyFile} ${optionalString (keyFileSize != null) "--keyfile-size=${toString keyFileSize}"}" \ while true; do
"${optionalString (keyFileOffset != null) "--keyfile-offset=${toString keyFileOffset}"}" \ if [ -e /crypt-ramfs/passphrase ]; then
>> /.luksopen_args echo "reused"
${optionalString fallbackToPassword '' passphrase=$(cat /crypt-ramfs/passphrase)
else break
echo "keyfile ${keyFile} not found -- fallback to interactive unlocking" else
fi # ask cryptsetup-askpass
''} echo -n "${device}" > /crypt-ramfs/device
''}
cryptsetup-askpass # and try reading it from /dev/console with a timeout
rm /.luksopen_args 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
} }
${optionalString (luks.yubikeySupport && (yubikey != null)) '' # LUKS
open_normally() {
${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
${if fallbackToPassword then "echo" else "die"} "${keyFile} is unavailable"
echo " - failing back to interactive password prompt"
do_open_passphrase
fi
'' else ''
do_open_passphrase
''}
}
${if luks.yubikeySupport && (yubikey != null) then ''
# Yubikey
rbtohex() { rbtohex() {
( od -An -vtx1 | tr -d ' \n' ) ( od -An -vtx1 | tr -d ' \n' )
} }
@ -69,8 +178,7 @@ let
( 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() { do_open_yubikey() {
# Make all of these local to this function # Make all of these local to this function
# to prevent their values being leaked # to prevent their values being leaked
local salt local salt
@ -86,19 +194,18 @@ let
local new_response local new_response
local new_k_luks local new_k_luks
mkdir -p ${yubikey.storage.mountPoint} mount -t ${yubikey.storage.fsType} ${yubikey.storage.device} /crypt-storage || \
mount -t ${yubikey.storage.fsType} ${toString yubikey.storage.device} ${yubikey.storage.mountPoint} die "Failed to mount Yubikey salt storage device"
salt="$(cat ${yubikey.storage.mountPoint}${yubikey.storage.path} | sed -n 1p | tr -d '\n')" salt="$(cat /crypt-storage${yubikey.storage.path} | sed -n 1p | tr -d '\n')"
iterations="$(cat ${yubikey.storage.mountPoint}${yubikey.storage.path} | sed -n 2p | 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)" challenge="$(echo -n $salt | openssl-wrap dgst -binary -sha512 | rbtohex)"
response="$(ykchalresp -${toString yubikey.slot} -x $challenge 2>/dev/null)" response="$(ykchalresp -${toString yubikey.slot} -x $challenge 2>/dev/null)"
for try in $(seq 3); do for try in $(seq 3); do
${optionalString yubikey.twoFactor '' ${optionalString yubikey.twoFactor ''
echo -n "Enter two-factor passphrase: " echo -n "Enter two-factor passphrase: "
read -s k_user read -r k_user
echo echo
''} ''}
@ -108,9 +215,9 @@ let
k_luks="$(echo | pbkdf2-sha512 ${toString yubikey.keyLength} $iterations $response | rbtohex)" k_luks="$(echo | pbkdf2-sha512 ${toString yubikey.keyLength} $iterations $response | rbtohex)"
fi 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 opened=true
break break
else else
@ -119,11 +226,7 @@ let
fi fi
done done
if [ "$opened" == false ]; then [ "$opened" == false ] && die "Maximum authentication errors reached"
umount ${yubikey.storage.mountPoint}
echo "Maximum authentication errors reached"
exit 1
fi
echo -n "Gathering entropy for new salt (please enter random keys to generate entropy if this blocks for long)..." 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 for i in $(seq ${toString yubikey.saltLength}); do
@ -148,69 +251,52 @@ let
new_k_luks="$(echo | pbkdf2-sha512 ${toString yubikey.keyLength} $new_iterations $new_response | rbtohex)" new_k_luks="$(echo | pbkdf2-sha512 ${toString yubikey.keyLength} $new_iterations $new_response | rbtohex)"
fi fi
mkdir -p ${yubikey.ramfsMountPoint} echo -n "$new_k_luks" | hextorb > /crypt-ramfs/new_key
# A ramfs is used here to ensure that the file used to update echo -n "$k_luks" | hextorb | ${cschange} --key-file=- /crypt-ramfs/new_key
# 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 if [ $? == 0 ]; then
echo -n "$k_luks" | hextorb | cryptsetup luksChangeKey ${device} --key-file=- ${yubikey.ramfsMountPoint}/new_key echo -ne "$new_salt\n$new_iterations" > /crypt-storage${yubikey.storage.path}
if [ $? == "0" ]; then
echo -ne "$new_salt\n$new_iterations" > ${yubikey.storage.mountPoint}${yubikey.storage.path}
else else
echo "Warning: Could not update LUKS key, current challenge persists!" echo "Warning: Could not update LUKS key, current challenge persists!"
fi fi
rm -f ${yubikey.ramfsMountPoint}/new_key rm -f /crypt-ramfs/new_key
umount ${yubikey.ramfsMountPoint} umount /crypt-storage
rm -rf ${yubikey.ramfsMountPoint}
umount ${yubikey.storage.mountPoint}
} }
${optionalString (yubikey.gracePeriod > 0) '' open_yubikey() {
echo -n "Waiting ${toString yubikey.gracePeriod} seconds as grace..." if wait_yubikey ${toString yubikey.gracePeriod}; then
for i in $(seq ${toString yubikey.gracePeriod}); do do_open_yubikey
sleep 1 else
echo -n . echo "No yubikey found, falling back to non-yubikey open procedure"
done open_normally
echo "ok" fi
''} }
yubikey_missing=true open_yubikey
ykinfo -v 1>/dev/null 2>&1 '' else ''
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
''}
# open luksRoot and scan for logical volumes
${optionalString ((!luks.yubikeySupport) || (yubikey == null)) ''
open_normally 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; preLVM = filterAttrs (n: v: v.preLVM) luks.devices;
postLVM = filterAttrs (n: v: !v.preLVM) luks.devices; postLVM = filterAttrs (n: v: !v.preLVM) luks.devices;
@ -256,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 { boot.initrd.luks.devices = mkOption {
default = { }; default = { };
example = { "luksroot".device = "/dev/disk/by-uuid/430e9eff-d852-4f68-aa3b-2fa3599ebe08"; }; example = { "luksroot".device = "/dev/disk/by-uuid/430e9eff-d852-4f68-aa3b-2fa3599ebe08"; };
@ -397,15 +499,9 @@ in
}; };
gracePeriod = mkOption { gracePeriod = mkOption {
default = 2; default = 10;
type = types.int; 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 {
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: /* TODO: Add to the documentation of the current module:
@ -428,12 +524,6 @@ in
description = "The filesystem of the unencrypted device."; 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 { path = mkOption {
default = "/crypt-storage/default"; default = "/crypt-storage/default";
type = types.str; type = types.str;
@ -446,8 +536,8 @@ in
}; };
}); });
}; };
};
}; })); }));
}; };
boot.initrd.luks.yubikeySupport = mkOption { boot.initrd.luks.yubikeySupport = mkOption {
@ -477,18 +567,8 @@ in
# copy the cryptsetup binary and it's dependencies # copy the cryptsetup binary and it's dependencies
boot.initrd.extraUtilsCommands = '' boot.initrd.extraUtilsCommands = ''
copy_bin_and_libs ${pkgs.cryptsetup}/bin/cryptsetup copy_bin_and_libs ${pkgs.cryptsetup}/bin/cryptsetup
copy_bin_and_libs ${askPass}/bin/cryptsetup-askpass
cat > $out/bin/cryptsetup-askpass <<EOF sed -i s,/bin/sh,$out/bin/sh, $out/bin/cryptsetup-askpass
#!$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
${optionalString luks.yubikeySupport '' ${optionalString luks.yubikeySupport ''
copy_bin_and_libs ${pkgs.yubikey-personalization}/bin/ykchalresp copy_bin_and_libs ${pkgs.yubikey-personalization}/bin/ykchalresp
@ -520,8 +600,9 @@ in
''} ''}
''; '';
boot.initrd.preLVMCommands = concatStrings (mapAttrsToList openCommand preLVM); boot.initrd.preFailCommands = postCommands;
boot.initrd.postDeviceCommands = concatStrings (mapAttrsToList openCommand postLVM); boot.initrd.preLVMCommands = commonFunctions + preCommands + concatStrings (mapAttrsToList openCommand preLVM) + postCommands;
boot.initrd.postDeviceCommands = commonFunctions + preCommands + concatStrings (mapAttrsToList openCommand postLVM) + postCommands;
environment.systemPackages = [ pkgs.cryptsetup ]; environment.systemPackages = [ pkgs.cryptsetup ];
}; };

View File

@ -248,6 +248,14 @@ let
isExecutable = true; 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 udevRules extraUtils modulesClosure;
inherit (config.boot) resumeDevice; inherit (config.boot) resumeDevice;