Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions example/luks-fido2.nix
Original file line number Diff line number Diff line change
@@ -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 = "/";
};
};
};
};
};
};
};
};
}
14 changes: 13 additions & 1 deletion lib/tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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"
]
''}
Comment on lines +329 to +333
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Ensure host-side Canokey backing file exists before QEMU starts.

file=/tmp/canokey-file is consumed by the host QEMU process, not the guest. If it’s missing, start-up may fail.

Add a lightweight pre-step in the Python testScript to create it:

                 ${lib.optionalString enableCanokey ''
+                  import pathlib
+                  pathlib.Path("/tmp/canokey-file").touch()
                   start_command += ["-device", "pci-ohci,id=usb-bus",
                     "-device", "canokey,bus=usb-bus.0,file=/tmp/canokey-file"
                   ]
                 ''}
🤖 Prompt for AI Agents
In lib/tests.nix around lines 329 to 333, the QEMU device uses a host backing
file (/tmp/canokey-file) which must exist before QEMU starts; modify the Python
testScript to create the file as a lightweight pre-step (e.g., open(path,
'a').close()) and set safe permissions (e.g., 0600) before launching QEMU, and
optionally remove the file in teardown to avoid leftover artifacts.

machine = create_machine(start_command=" ".join(start_command), **kwargs)
driver.machines.append(machine)
return machine
Expand Down
124 changes: 116 additions & 8 deletions lib/types/luks.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 = {
Expand Down Expand Up @@ -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 = { };
Expand Down Expand Up @@ -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}
'';
};
Expand Down Expand Up @@ -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);
Expand All @@ -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";
Expand Down
14 changes: 14 additions & 0 deletions tests/luks-fido2.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
pkgs ? import <nixpkgs> { },
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");
'';
}