diff --git a/example/luks-fido2.nix b/example/luks-fido2.nix new file mode 100644 index 00000000..a37438d4 --- /dev/null +++ b/example/luks-fido2.nix @@ -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 = "/"; + }; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/lib/tests.nix b/lib/tests.nix index d0cba72e..11338f08 100644 --- a/lib/tests.nix +++ b/lib/tests.nix @@ -79,6 +79,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 @@ -279,7 +280,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; @@ -319,6 +326,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 diff --git a/lib/types/luks.nix b/lib/types/luks.nix index 421d12fe..8436c04f 100644 --- a/lib/types/luks.nix +++ b/lib/types/luks.nix @@ -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,25 @@ 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 +55,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 +97,29 @@ 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 +204,66 @@ 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}" + + set +x; read -p "Press Enter when you scanned the QR code offscreen or that the recovery key is stored securely."; set -x + ''} + ${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} ''; }; @@ -226,7 +319,12 @@ 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 = config.enrollFido2; } ]) ++ (lib.optional (config.content != null) config.content._config); @@ -240,7 +338,17 @@ 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"; diff --git a/tests/luks-fido2.nix b/tests/luks-fido2.nix new file mode 100644 index 00000000..61ff9f45 --- /dev/null +++ b/tests/luks-fido2.nix @@ -0,0 +1,14 @@ +{ + pkgs ? import { }, + diskoLib ? pkgs.callPackage ../lib { }, +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "luks-fido2"; + disko-config = ../example/luks-fido2.nix; + # This simulates a FIDO2 stick. + enableCanokey = true; + extraTestScript = '' + machine.succeed("cryptsetup isLuks /dev/vda2"); + ''; +}