feat: add FIDO2 & recovery support for LUKS
This introduces what has been described in https://0pointer.net/blog/unlocking-luks2-volumes-with-tpm2-fido2-pkcs11-security-hardware-on-systemd-248.html. That is: support for FIDO2 and recovery passphrases and their boot mechanism. Testing is hard right now because of canokey-qemu being broken and U2F is not a valid replacement for FIDO2… I tried to keep as much as possible the previous behavior and make it possible to mix FIDO2 and normal passphrases or key files without any problem. PIV support is out of scope for this change but can easily be added. Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
{
|
||||
disko.devices = {
|
||||
disk = {
|
||||
main = {
|
||||
type = "disk";
|
||||
device = "/dev/vdb";
|
||||
content = {
|
||||
type = "gpt";
|
||||
partitions = {
|
||||
ESP = {
|
||||
size = "500M";
|
||||
type = "EF00";
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "vfat";
|
||||
mountpoint = "/boot";
|
||||
mountOptions = [ "umask=0077" ];
|
||||
};
|
||||
};
|
||||
luks = {
|
||||
size = "100%";
|
||||
content = {
|
||||
type = "luks";
|
||||
name = "crypted";
|
||||
settings.allowDiscards = true;
|
||||
enrollFido2 = true;
|
||||
# Do not wait for recovery displaying and blocking formatting.
|
||||
enrollRecovery = false;
|
||||
content = {
|
||||
type = "filesystem";
|
||||
format = "ext4";
|
||||
mountpoint = "/";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
+13
-1
@@ -80,6 +80,7 @@ let
|
||||
extraInstallerConfig ? { },
|
||||
extraSystemConfig ? { },
|
||||
efi ? !pkgs.stdenv.hostPlatform.isRiscV64,
|
||||
enableCanokey ? false,
|
||||
postDisko ? "",
|
||||
testMode ? "module", # can be one of direct module cli
|
||||
testBoot ? true, # if we actually want to test booting or just create/mount
|
||||
@@ -288,7 +289,13 @@ let
|
||||
(testConfigInstall ? networking.hostId) && (testConfigInstall.networking.hostId != null)
|
||||
) testConfigInstall.networking.hostId;
|
||||
|
||||
virtualisation.emptyDiskImages = builtins.genList (_: 4096) num-disks;
|
||||
virtualisation = {
|
||||
emptyDiskImages = builtins.genList (_: 4096) num-disks;
|
||||
qemu.options = lib.mkIf enableCanokey [
|
||||
"-device pci-ohci,id=usb-bus"
|
||||
"-device canokey,bus=usb-bus.0,file=/tmp/canokey-file"
|
||||
];
|
||||
};
|
||||
|
||||
# useful for debugging via repl
|
||||
system.build.systemToInstall = installed-system-eval;
|
||||
@@ -329,6 +336,11 @@ let
|
||||
"if=pflash,format=raw,unit=1,readonly=on,file=${pkgs.OVMF.variables}"
|
||||
]
|
||||
''}
|
||||
${lib.optionalString enableCanokey ''
|
||||
start_command += ["-device", "pci-ohci,id=usb-bus",
|
||||
"-device", "canokey,bus=usb-bus.0,file=/tmp/canokey-file"
|
||||
]
|
||||
''}
|
||||
machine = create_machine(start_command=" ".join(start_command), **kwargs)
|
||||
driver.machines.append(machine)
|
||||
return machine
|
||||
|
||||
+129
-8
@@ -8,6 +8,9 @@
|
||||
...
|
||||
}:
|
||||
let
|
||||
# These options will automatically generate a temporary password and remove it later on.
|
||||
autogeneratedPassword = config.enrollFido2;
|
||||
|
||||
keyFile =
|
||||
if config.settings ? "keyFile" then
|
||||
config.settings.keyFile
|
||||
@@ -25,13 +28,30 @@ let
|
||||
) config.keyFile
|
||||
else
|
||||
null;
|
||||
keyFileArgs = ''
|
||||
|
||||
formatKeyFile =
|
||||
if autogeneratedPassword then ''<(set +x; echo -n "$password"; set -x)'' else keyFile;
|
||||
|
||||
generateKeyFileArgs = keyFile: ''
|
||||
${lib.optionalString (keyFile != null) "--key-file ${keyFile}"} \
|
||||
${lib.optionalString (lib.hasAttr "keyFileSize" config.settings) "--keyfile-size ${builtins.toString config.settings.keyFileSize}"} \
|
||||
${lib.optionalString (lib.hasAttr "keyFileOffset" config.settings) "--keyfile-offset ${builtins.toString config.settings.keyFileOffset}"} \
|
||||
'';
|
||||
cryptsetupOpen = ''
|
||||
|
||||
# This is the one used for standard one shot formatting and mounting.
|
||||
keyFileArgs = generateKeyFileArgs keyFile;
|
||||
# This is the one used for 2-staged formatting like FIDO2 and NEVER for mounting.
|
||||
formatKeyFileArgs = generateKeyFileArgs formatKeyFile;
|
||||
|
||||
# --token-only forces to try FIRST the token then passphrase.
|
||||
createOpenCommand =
|
||||
{
|
||||
keyFileArgs,
|
||||
tokenType ? null,
|
||||
}:
|
||||
''
|
||||
cryptsetup open "${config.device}" "${config.name}" \
|
||||
${lib.optionalString (tokenType != null) "--token-type ${tokenType}"} \
|
||||
${lib.optionalString (config.settings.allowDiscards or false) "--allow-discards"} \
|
||||
${
|
||||
lib.optionalString (config.settings.bypassWorkqueues or false
|
||||
@@ -40,6 +60,17 @@ let
|
||||
${toString config.extraOpenArgs} \
|
||||
${keyFileArgs} \
|
||||
'';
|
||||
|
||||
# Use this open command when you want to open it after full enrollment, e.g. at mount time or in standard enrollments.
|
||||
cryptsetupOpen = createOpenCommand {
|
||||
inherit keyFileArgs;
|
||||
tokenType = if config.enrollFido2 then "systemd-fido2" else null;
|
||||
};
|
||||
|
||||
# Use this open command when you want to open it immediately after the formatting and before the stage 2 process is finished (i.e. the wipe slot).
|
||||
formatCryptsetupOpen = createOpenCommand {
|
||||
keyFileArgs = formatKeyFileArgs;
|
||||
};
|
||||
in
|
||||
{
|
||||
options = {
|
||||
@@ -71,10 +102,33 @@ in
|
||||
};
|
||||
askPassword = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = config.keyFile == null && config.passwordFile == null && (!config.settings ? "keyFile");
|
||||
defaultText = "true if neither keyFile nor passwordFile are set";
|
||||
default =
|
||||
config.keyFile == null
|
||||
&& config.passwordFile == null
|
||||
&& (!config.settings ? "keyFile")
|
||||
&& !autogeneratedPassword;
|
||||
defaultText = "true if neither keyFile nor passwordFile nor enrollFido2 are set";
|
||||
description = "Whether to ask for a password for initial encryption";
|
||||
};
|
||||
enrollFido2 = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "Whether to enroll a FIDO2 token and use it";
|
||||
};
|
||||
extraFido2EnrollArgs = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [
|
||||
"--fido2-parameters-in-header=false"
|
||||
];
|
||||
description = "Extra arguments to pass to `systemd-cryptenroll` when enrolling the FIDO2 device";
|
||||
};
|
||||
enrollRecovery = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = config.enrollFido2;
|
||||
defaultText = "true if fido2 is enabled";
|
||||
description = "Whether to enroll an automatic (keyboard layout independent) recovery passphrase with high entropy and print a QR code on screen to take it";
|
||||
};
|
||||
settings = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.anything;
|
||||
default = { };
|
||||
@@ -159,18 +213,67 @@ in
|
||||
echo "Passwords did not match, please try again."
|
||||
done
|
||||
''}
|
||||
cryptsetup -q luksFormat "${config.device}" ${toString config.extraFormatArgs} ${keyFileArgs}
|
||||
${lib.optionalString autogeneratedPassword ''
|
||||
# Generate a random throwable key that will be removed later on.
|
||||
set +x
|
||||
password=$(openssl rand -hex 32)
|
||||
export password
|
||||
set -x
|
||||
|
||||
# We have the guarantee that slot 0 needs to be deleted later on.
|
||||
# If the user had set its own password, we wouldn't create this variable
|
||||
# and the script later will not wipe the slot zero. The user keep his password.
|
||||
export SLOT_ZERO_TO_DELETE=true
|
||||
''}
|
||||
cryptsetup -q luksFormat "${config.device}" ${toString config.extraFormatArgs} ${formatKeyFileArgs}
|
||||
fi
|
||||
|
||||
if ! cryptsetup status "${config.name}" >/dev/null; then
|
||||
${cryptsetupOpen} --persistent
|
||||
${formatCryptsetupOpen} \
|
||||
--persistent
|
||||
fi
|
||||
|
||||
${toString (
|
||||
lib.forEach config.additionalKeyFiles (keyFile: ''
|
||||
cryptsetup luksAddKey "${config.device}" ${keyFile} ${keyFileArgs}
|
||||
cryptsetup luksAddKey "${config.device}" ${keyFile} ${formatKeyFileArgs}
|
||||
'')
|
||||
)}
|
||||
|
||||
${lib.optionalString config.enrollRecovery ''
|
||||
systemd-cryptenroll \
|
||||
--recovery-key \
|
||||
--unlock-key-file=${formatKeyFile} \
|
||||
"${config.device}"
|
||||
if [ -z ''${IN_DISKO_TEST+x} ]; then
|
||||
set +x; read -p "Press Enter when you scanned the QR code offscreen or that the recovery key is stored securely."; set -x
|
||||
fi
|
||||
''}
|
||||
${lib.optionalString config.enrollFido2 ''
|
||||
wait_for_token() {
|
||||
set +f
|
||||
echo "Waiting for FIDO2 token insertion..."
|
||||
|
||||
# Check if any FIDO2 device is available via /dev/hidraw*
|
||||
while true; do
|
||||
if ls /dev/hidraw* &>/dev/null; then
|
||||
echo "FIDO2 device detected."
|
||||
break
|
||||
else
|
||||
echo "FIDO2 device not detected, waiting..."
|
||||
sleep 2
|
||||
fi
|
||||
done
|
||||
set -f
|
||||
}
|
||||
|
||||
wait_for_token
|
||||
systemd-cryptenroll \
|
||||
--fido2-device=auto \
|
||||
''${SLOT_ZERO_TO_DELETE:+--wipe-slot=0} \
|
||||
--unlock-key-file=${formatKeyFile} \
|
||||
${toString config.extraFido2EnrollArgs} \
|
||||
"${config.device}"
|
||||
''}
|
||||
${lib.optionalString (config.content != null) config.content._create}
|
||||
'';
|
||||
};
|
||||
@@ -227,8 +330,13 @@ in
|
||||
{
|
||||
boot.initrd.luks.devices.${config.name} = {
|
||||
inherit (config) device;
|
||||
crypttabExtraOpts = lib.mkIf config.enrollFido2 [ "fido2-device=auto" ];
|
||||
}
|
||||
// config.settings;
|
||||
|
||||
# If FIDO2 is used, systemd stage 1 is absolutely necessary.
|
||||
# Should we turn this into an assertion?
|
||||
boot.initrd.systemd.enable = lib.mkIf config.enrollFido2 true;
|
||||
}
|
||||
])
|
||||
++ (lib.optional (config.content != null) config.content._config);
|
||||
@@ -242,7 +350,20 @@ in
|
||||
pkgs:
|
||||
[
|
||||
pkgs.gnugrep
|
||||
pkgs.cryptsetup
|
||||
pkgs.openssl
|
||||
pkgs.systemd
|
||||
# We make cryptsetup aware of token libraries from systemd.
|
||||
# We do not have a lot of nice ways to do this...
|
||||
(pkgs.runCommandNoCC pkgs.cryptsetup.name
|
||||
{
|
||||
nativeBuildInputs = [ pkgs.makeWrapper ];
|
||||
}
|
||||
''
|
||||
mkdir -p $out/bin/
|
||||
makeWrapper ${pkgs.cryptsetup.bin}/bin/cryptsetup $out/bin/cryptsetup \
|
||||
--prefix LD_LIBRARY_PATH : ${pkgs.systemd}/lib/cryptsetup
|
||||
''
|
||||
)
|
||||
]
|
||||
++ (lib.optionals (config.content != null) (config.content._pkgs pkgs));
|
||||
description = "Packages";
|
||||
|
||||
Reference in New Issue
Block a user