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.
</para>
</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>
</section>

View File

@ -5,62 +5,171 @@ 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
}
'';
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"
''}
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 ''
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
echo "keyfile ${keyFile} not found -- fallback to interactive unlocking"
# 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
''}
''}
cryptsetup-askpass
rm /.luksopen_args
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() {
( 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 )
}
open_yubikey() {
do_open_yubikey() {
# Make all of these local to this function
# to prevent their values being leaked
local salt
@ -86,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
''}
@ -108,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
@ -119,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
@ -148,69 +251,52 @@ 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}
umount ${yubikey.storage.mountPoint}
rm -f /crypt-ramfs/new_key
umount /crypt-storage
}
${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"
open_yubikey() {
if wait_yubikey ${toString yubikey.gracePeriod}; then
do_open_yubikey
else
yubikey_missing=false
fi
if [ "$yubikey_missing" == true ]; then
echo "no yubikey found, falling back to non-yubikey open procedure"
echo "No yubikey found, falling back to non-yubikey open procedure"
open_normally
else
fi
}
open_yubikey
fi
''}
# open luksRoot and scan for logical volumes
${optionalString ((!luks.yubikeySupport) || (yubikey == null)) ''
'' 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;
@ -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 {
default = { };
example = { "luksroot".device = "/dev/disk/by-uuid/430e9eff-d852-4f68-aa3b-2fa3599ebe08"; };
@ -397,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:
@ -428,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;
@ -446,8 +536,8 @@ in
};
});
};
}; }));
};
}));
};
boot.initrd.luks.yubikeySupport = mkOption {
@ -477,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
@ -520,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 ];
};

View File

@ -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;