Skip to content
Merged
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
6 changes: 4 additions & 2 deletions nixos/modules/system/boot/luksroot.nix
Original file line number Diff line number Diff line change
Expand Up @@ -905,9 +905,11 @@ in
{ assertion = config.boot.initrd.systemd.enable -> !luks.gpgSupport;
message = "systemd stage 1 does not support GPG smartcards yet.";
}
# TODO
{ assertion = config.boot.initrd.systemd.enable -> !luks.fido2Support;
message = "systemd stage 1 does not support FIDO2 yet.";
message = ''
systemd stage 1 does not support configuring FIDO2 unlocking through `boot.initrd.luks.devices.<name>.fido2`.
Use systemd-cryptenroll(1) to configure FIDO2 support.
'';
}
# TODO
{ assertion = config.boot.initrd.systemd.enable -> !luks.yubikeySupport;
Expand Down
16 changes: 15 additions & 1 deletion nixos/modules/system/boot/systemd/initrd.nix
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,10 @@ in {
config = mkIf (config.boot.initrd.enable && cfg.enable) {
system.build = { inherit initialRamdisk; };

boot.initrd.availableKernelModules = [ "autofs4" ]; # systemd needs this for some features
boot.initrd.availableKernelModules = [
"autofs4" # systemd needs this for some features
"tpm-tis" "tpm-crb" # systemd-cryptenroll
Copy link
Member

Choose a reason for hiding this comment

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

Can we maybe make this optional? Or conditional on if the kernel has this option set?

I have some custom kernel configs that really don't need this module and also use the new initrd and now the initrd build fails with modprobe: FATAL: Module tpm-tis not found in directory […].

Copy link
Member Author

Choose a reason for hiding this comment

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

I guess we can have a new option under boot.initrd.luks.devices.<name> that indicates whether a LUKS makes use of cryptenroll-based TPM2/FIDO2 unlocking, and only insert the modules and extra files when a volume with those options enabled exists. However, this can be confusing for non-systemd-stage1 users, especially when boot.initrd.luks.devices.<name>.fido2 already exists.

Copy link
Member

Choose a reason for hiding this comment

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

I'd prefer if disabling these kernel modules would an opt-in config option.

We can't determine at evaluation time if there's a fido luks slot configured.

Copy link
Member

Choose a reason for hiding this comment

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

fair enough. iirc we can determine at eval time if the kernel even has these modules enabled. maybe using system.requiredKernelConfig?

Copy link
Member

Choose a reason for hiding this comment

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

looking at the kernel config, these options/modules don't even seem to be available on non-x86 systems or systems without ACPI, so I'm really not sure they should be enabled by default

Copy link
Member

Choose a reason for hiding this comment

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

Hmmh, that'd be a problem. Can we check if the kernel has these modules configured, and only include them in that case?

Copy link
Member

Choose a reason for hiding this comment

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

I think something like this might work:

--- a/nixos/modules/system/boot/systemd/initrd.nix                                                                                                                                    
+++ b/nixos/modules/system/boot/systemd/initrd.nix
@@ -334,8 +334,11 @@ in {
               
     boot.initrd.availableKernelModules = [
       "autofs4"           # systemd needs this for some features
-      "tpm-tis" "tpm-crb" # systemd-cryptenroll              
-    ];                                                                                    
+    ]                                                                                     
+      # systemd-cryptenroll                                                                                                                                                          
+      ++ lib.optional (config.boot.kernelPackages.kernel.config.isEnabled "TCG_TIS") "tpm-tis"
+      ++ lib.optional (config.boot.kernelPackages.kernel.config.isEnabled "TCG_CRB") "tpm-crb"                                                                     
+    ;                                                                                     
                                
     boot.initrd.systemd = {                                                               
       initrdBin = [pkgs.bash pkgs.coreutils cfg.package.kmod cfg.package] ++ config.system.fsPackages;                                                                              

but only if we actually set these options and don't rely on kernel defaults. Maybe.

For some reason nixosTests.systemd-initrd-luks-fido2 and nixosTests.systemd-initrd-luks-tpm2 build even with:

     boot.initrd.availableKernelModules = [
       "autofs4"           # systemd needs this for some features
     ];

so I'm not sure how to test.

Copy link
Member Author

@zhaofengli zhaofengli Nov 11, 2022

Choose a reason for hiding this comment

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

The VM test passes without adding any extra modules but on (some?) actual hardware they are required.

];

boot.initrd.systemd = {
initrdBin = [pkgs.bash pkgs.coreutils cfg.package.kmod cfg.package] ++ config.system.fsPackages;
Expand Down Expand Up @@ -403,6 +406,17 @@ in {

# so NSS can look up usernames
"${pkgs.glibc}/lib/libnss_files.so.2"
] ++ optionals cfg.package.withCryptsetup [
# tpm2 support
"${cfg.package}/lib/cryptsetup/libcryptsetup-token-systemd-tpm2.so"
pkgs.tpm2-tss

# fido2 support
"${cfg.package}/lib/cryptsetup/libcryptsetup-token-systemd-fido2.so"
"${pkgs.libfido2}/lib/libfido2.so.1"

# the unwrapped systemd-cryptsetup executable
"${cfg.package}/lib/systemd/.systemd-cryptsetup-wrapped"
] ++ jobScripts;

targets.initrd.aliases = ["default.target"];
Expand Down
2 changes: 2 additions & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -595,8 +595,10 @@ in {
systemd-cryptenroll = handleTest ./systemd-cryptenroll.nix {};
systemd-escaping = handleTest ./systemd-escaping.nix {};
systemd-initrd-btrfs-raid = handleTest ./systemd-initrd-btrfs-raid.nix {};
systemd-initrd-luks-fido2 = handleTest ./systemd-initrd-luks-fido2.nix {};
systemd-initrd-luks-keyfile = handleTest ./systemd-initrd-luks-keyfile.nix {};
systemd-initrd-luks-password = handleTest ./systemd-initrd-luks-password.nix {};
systemd-initrd-luks-tpm2 = handleTest ./systemd-initrd-luks-tpm2.nix {};
systemd-initrd-modprobe = handleTest ./systemd-initrd-modprobe.nix {};
systemd-initrd-shutdown = handleTest ./systemd-shutdown.nix { systemdStage1 = true; };
systemd-initrd-simple = handleTest ./systemd-initrd-simple.nix {};
Expand Down
45 changes: 45 additions & 0 deletions nixos/tests/systemd-initrd-luks-fido2.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import ./make-test-python.nix ({ lib, pkgs, ... }: {
name = "systemd-initrd-luks-fido2";

nodes.machine = { pkgs, config, ... }: {
# Use systemd-boot
virtualisation = {
emptyDiskImages = [ 512 ];
useBootLoader = true;
useEFIBoot = true;
qemu.package = lib.mkForce (pkgs.qemu_test.override { canokeySupport = true; });
qemu.options = [ "-device canokey,file=/tmp/canokey-file" ];
};
boot.loader.systemd-boot.enable = true;

boot.initrd.systemd.enable = true;

environment.systemPackages = with pkgs; [ cryptsetup ];

specialisation.boot-luks.configuration = {
boot.initrd.luks.devices = lib.mkVMOverride {
cryptroot = {
device = "/dev/vdc";
crypttabExtraOpts = [ "fido2-device=auto" ];
};
};
virtualisation.bootDevice = "/dev/mapper/cryptroot";
};
};

testScript = ''
# Create encrypted volume
machine.wait_for_unit("multi-user.target")
machine.succeed("echo -n supersecret | cryptsetup luksFormat -q --iter-time=1 /dev/vdc -")
machine.succeed("PASSWORD=supersecret SYSTEMD_LOG_LEVEL=debug systemd-cryptenroll --fido2-device=auto /dev/vdc |& systemd-cat")

# Boot from the encrypted disk
machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-luks.conf")
machine.succeed("sync")
machine.crash()

# Boot and decrypt the disk
machine.wait_for_unit("multi-user.target")
assert "/dev/mapper/cryptroot on / type ext4" in machine.succeed("mount")
'';
})
72 changes: 72 additions & 0 deletions nixos/tests/systemd-initrd-luks-tpm2.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import ./make-test-python.nix ({ lib, pkgs, ... }: {
name = "systemd-initrd-luks-tpm2";

nodes.machine = { pkgs, ... }: {
# Use systemd-boot
virtualisation = {
emptyDiskImages = [ 512 ];
useBootLoader = true;
useEFIBoot = true;
qemu.options = ["-chardev socket,id=chrtpm,path=/tmp/mytpm1/swtpm-sock -tpmdev emulator,id=tpm0,chardev=chrtpm -device tpm-tis,tpmdev=tpm0"];
};
boot.loader.systemd-boot.enable = true;

boot.initrd.availableKernelModules = [ "tpm_tis" ];

environment.systemPackages = with pkgs; [ cryptsetup ];
boot.initrd.systemd = {
enable = true;
};

specialisation.boot-luks.configuration = {
boot.initrd.luks.devices = lib.mkVMOverride {
cryptroot = {
device = "/dev/vdc";
crypttabExtraOpts = [ "tpm2-device=auto" ];
};
};
virtualisation.bootDevice = "/dev/mapper/cryptroot";
};
};

testScript = ''
import subprocess
import os
import time


class Tpm:
def __init__(self):
os.mkdir("/tmp/mytpm1")
self.start()

def start(self):
self.proc = subprocess.Popen(["${pkgs.swtpm}/bin/swtpm", "socket", "--tpmstate", "dir=/tmp/mytpm1", "--ctrl", "type=unixio,path=/tmp/mytpm1/swtpm-sock", "--log", "level=20", "--tpm2"])

def wait_for_death_then_restart(self):
while self.proc.poll() is None:
print("waiting for tpm to die")
time.sleep(1)
assert self.proc.returncode == 0
self.start()

tpm = Tpm()


# Create encrypted volume
machine.wait_for_unit("multi-user.target")
machine.succeed("echo -n supersecret | cryptsetup luksFormat -q --iter-time=1 /dev/vdc -")
machine.succeed("PASSWORD=supersecret SYSTEMD_LOG_LEVEL=debug systemd-cryptenroll --tpm2-pcrs= --tpm2-device=auto /dev/vdc |& systemd-cat")

# Boot from the encrypted disk
machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-luks.conf")
machine.succeed("sync")
machine.crash()

tpm.wait_for_death_then_restart()

# Boot and decrypt the disk
machine.wait_for_unit("multi-user.target")
assert "/dev/mapper/cryptroot on / type ext4" in machine.succeed("mount")
'';
})
5 changes: 5 additions & 0 deletions pkgs/os-specific/linux/cryptsetup/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ stdenv.mkDerivation rec {
sha256 = "sha256-kYSm672c5+shEVLn90GmyC8tHMDiSoTsnFKTnu4PBUI=";
};

patches = [
# Allow reading tokens from a relative path, see #167994
./relative-token-path.patch
];

postPatch = ''
patchShebangs tests

Expand Down
50 changes: 50 additions & 0 deletions pkgs/os-specific/linux/cryptsetup/relative-token-path.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
From 4f95ab1f8110a8ab9d7b0e192731ce467f6e5c26 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Janne=20He=C3=9F?= <janne@hess.ooo>
Date: Sun, 4 Sep 2022 11:15:02 -0600
Subject: [PATCH] Allow loading token handlers from the default search path

Since [1] landed in cryptsetup, token handlers (libcryptsetup-token-*.so)
are loaded from a fixed path defined at compile-time. This is
problematic with NixOS since it introduces a dependency cycle
between cryptsetup and systemd.

This downstream patch [2] allows loading token plugins from the
default library search path. This approach is not accepted upstream [3]
due to security concerns, but the potential attack vectors require
root access and they are sufficiently addressed:

* cryptsetup could be used as a setuid binary (not used in NixOS).
In this case, LD_LIBRARY_PATH is ignored because of secure-execution
mode.
* cryptsetup running as root could lead to a malicious token handler
being loaded through LD_LIBRARY_PATH. However, fixing the path
doesn't prevent the same malicious .so being loaded through LD_PRELOAD.

[1] https://gitlab.com/cryptsetup/cryptsetup/-/commit/5b9e98f94178d3cd179d9f6e2a0a68c7d9eb6507
[2] https://github.com/NixOS/nixpkgs/issues/167994#issuecomment-1094249369
[3] https://gitlab.com/cryptsetup/cryptsetup/-/issues/733
---
lib/luks2/luks2_token.c | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/lib/luks2/luks2_token.c b/lib/luks2/luks2_token.c
index 26467253..6f8329f0 100644
--- a/lib/luks2/luks2_token.c
+++ b/lib/luks2/luks2_token.c
@@ -151,12 +151,10 @@ crypt_token_load_external(struct crypt_device *cd, const char *name, struct cryp

token = &ret->u.v2;

- r = snprintf(buf, sizeof(buf), "%s/libcryptsetup-token-%s.so", crypt_token_external_path(), name);
+ r = snprintf(buf, sizeof(buf), "libcryptsetup-token-%s.so", name);
if (r < 0 || (size_t)r >= sizeof(buf))
return -EINVAL;

- assert(*buf == '/');
-
log_dbg(cd, "Trying to load %s.", buf);

h = dlopen(buf, RTLD_LAZY);
--
2.37.2

11 changes: 10 additions & 1 deletion pkgs/os-specific/linux/systemd/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
, fetchpatch
, fetchzip
, buildPackages
, makeBinaryWrapper
, ninja
, meson
, m4
Expand Down Expand Up @@ -332,6 +333,7 @@ stdenv.mkDerivation {
nativeBuildInputs =
[
pkg-config
makeBinaryWrapper
gperf
ninja
meson
Expand Down Expand Up @@ -666,7 +668,14 @@ stdenv.mkDerivation {
preFixup = lib.optionalString withEfi ''
mv $out/lib/systemd/boot/efi $out/dont-strip-me
'';
postFixup = lib.optionalString withEfi ''

# Wrap in the correct path for LUKS2 tokens.
postFixup = lib.optionalString withCryptsetup ''
for f in lib/systemd/systemd-cryptsetup bin/systemd-cryptenroll; do
# This needs to be in LD_LIBRARY_PATH because rpath on a binary is not propagated to libraries using dlopen, in this case `libcryptsetup.so`
wrapProgram $out/$f --prefix LD_LIBRARY_PATH : ${placeholder "out"}/lib/cryptsetup
done
'' + lib.optionalString withEfi ''
mv $out/dont-strip-me $out/lib/systemd/boot/efi
'';

Expand Down