Merge pull request #189676 from zhaofengli/cryptenroll

systemd: Fix systemd-{cryptenroll,cryptsetup} TPM2 and FIDO2 support (attempt #3)

authored by Florian Klink and committed by GitHub 3ff0a8f8 1a0c8a44

+203 -4
+4 -2
nixos/modules/system/boot/luksroot.nix
··· 905 905 { assertion = config.boot.initrd.systemd.enable -> !luks.gpgSupport; 906 906 message = "systemd stage 1 does not support GPG smartcards yet."; 907 907 } 908 - # TODO 909 908 { assertion = config.boot.initrd.systemd.enable -> !luks.fido2Support; 910 - message = "systemd stage 1 does not support FIDO2 yet."; 909 + message = '' 910 + systemd stage 1 does not support configuring FIDO2 unlocking through `boot.initrd.luks.devices.<name>.fido2`. 911 + Use systemd-cryptenroll(1) to configure FIDO2 support. 912 + ''; 911 913 } 912 914 # TODO 913 915 { assertion = config.boot.initrd.systemd.enable -> !luks.yubikeySupport;
+15 -1
nixos/modules/system/boot/systemd/initrd.nix
··· 332 332 config = mkIf (config.boot.initrd.enable && cfg.enable) { 333 333 system.build = { inherit initialRamdisk; }; 334 334 335 - boot.initrd.availableKernelModules = [ "autofs4" ]; # systemd needs this for some features 335 + boot.initrd.availableKernelModules = [ 336 + "autofs4" # systemd needs this for some features 337 + "tpm-tis" "tpm-crb" # systemd-cryptenroll 338 + ]; 336 339 337 340 boot.initrd.systemd = { 338 341 initrdBin = [pkgs.bash pkgs.coreutils cfg.package.kmod cfg.package] ++ config.system.fsPackages; ··· 403 406 404 407 # so NSS can look up usernames 405 408 "${pkgs.glibc}/lib/libnss_files.so.2" 409 + ] ++ optionals cfg.package.withCryptsetup [ 410 + # tpm2 support 411 + "${cfg.package}/lib/cryptsetup/libcryptsetup-token-systemd-tpm2.so" 412 + pkgs.tpm2-tss 413 + 414 + # fido2 support 415 + "${cfg.package}/lib/cryptsetup/libcryptsetup-token-systemd-fido2.so" 416 + "${pkgs.libfido2}/lib/libfido2.so.1" 417 + 418 + # the unwrapped systemd-cryptsetup executable 419 + "${cfg.package}/lib/systemd/.systemd-cryptsetup-wrapped" 406 420 ] ++ jobScripts; 407 421 408 422 targets.initrd.aliases = ["default.target"];
+2
nixos/tests/all-tests.nix
··· 596 596 systemd-cryptenroll = handleTest ./systemd-cryptenroll.nix {}; 597 597 systemd-escaping = handleTest ./systemd-escaping.nix {}; 598 598 systemd-initrd-btrfs-raid = handleTest ./systemd-initrd-btrfs-raid.nix {}; 599 + systemd-initrd-luks-fido2 = handleTest ./systemd-initrd-luks-fido2.nix {}; 599 600 systemd-initrd-luks-keyfile = handleTest ./systemd-initrd-luks-keyfile.nix {}; 600 601 systemd-initrd-luks-password = handleTest ./systemd-initrd-luks-password.nix {}; 602 + systemd-initrd-luks-tpm2 = handleTest ./systemd-initrd-luks-tpm2.nix {}; 601 603 systemd-initrd-modprobe = handleTest ./systemd-initrd-modprobe.nix {}; 602 604 systemd-initrd-shutdown = handleTest ./systemd-shutdown.nix { systemdStage1 = true; }; 603 605 systemd-initrd-simple = handleTest ./systemd-initrd-simple.nix {};
+45
nixos/tests/systemd-initrd-luks-fido2.nix
··· 1 + import ./make-test-python.nix ({ lib, pkgs, ... }: { 2 + name = "systemd-initrd-luks-fido2"; 3 + 4 + nodes.machine = { pkgs, config, ... }: { 5 + # Use systemd-boot 6 + virtualisation = { 7 + emptyDiskImages = [ 512 ]; 8 + useBootLoader = true; 9 + useEFIBoot = true; 10 + qemu.package = lib.mkForce (pkgs.qemu_test.override { canokeySupport = true; }); 11 + qemu.options = [ "-device canokey,file=/tmp/canokey-file" ]; 12 + }; 13 + boot.loader.systemd-boot.enable = true; 14 + 15 + boot.initrd.systemd.enable = true; 16 + 17 + environment.systemPackages = with pkgs; [ cryptsetup ]; 18 + 19 + specialisation.boot-luks.configuration = { 20 + boot.initrd.luks.devices = lib.mkVMOverride { 21 + cryptroot = { 22 + device = "/dev/vdc"; 23 + crypttabExtraOpts = [ "fido2-device=auto" ]; 24 + }; 25 + }; 26 + virtualisation.bootDevice = "/dev/mapper/cryptroot"; 27 + }; 28 + }; 29 + 30 + testScript = '' 31 + # Create encrypted volume 32 + machine.wait_for_unit("multi-user.target") 33 + machine.succeed("echo -n supersecret | cryptsetup luksFormat -q --iter-time=1 /dev/vdc -") 34 + machine.succeed("PASSWORD=supersecret SYSTEMD_LOG_LEVEL=debug systemd-cryptenroll --fido2-device=auto /dev/vdc |& systemd-cat") 35 + 36 + # Boot from the encrypted disk 37 + machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-luks.conf") 38 + machine.succeed("sync") 39 + machine.crash() 40 + 41 + # Boot and decrypt the disk 42 + machine.wait_for_unit("multi-user.target") 43 + assert "/dev/mapper/cryptroot on / type ext4" in machine.succeed("mount") 44 + ''; 45 + })
+72
nixos/tests/systemd-initrd-luks-tpm2.nix
··· 1 + import ./make-test-python.nix ({ lib, pkgs, ... }: { 2 + name = "systemd-initrd-luks-tpm2"; 3 + 4 + nodes.machine = { pkgs, ... }: { 5 + # Use systemd-boot 6 + virtualisation = { 7 + emptyDiskImages = [ 512 ]; 8 + useBootLoader = true; 9 + useEFIBoot = true; 10 + qemu.options = ["-chardev socket,id=chrtpm,path=/tmp/mytpm1/swtpm-sock -tpmdev emulator,id=tpm0,chardev=chrtpm -device tpm-tis,tpmdev=tpm0"]; 11 + }; 12 + boot.loader.systemd-boot.enable = true; 13 + 14 + boot.initrd.availableKernelModules = [ "tpm_tis" ]; 15 + 16 + environment.systemPackages = with pkgs; [ cryptsetup ]; 17 + boot.initrd.systemd = { 18 + enable = true; 19 + }; 20 + 21 + specialisation.boot-luks.configuration = { 22 + boot.initrd.luks.devices = lib.mkVMOverride { 23 + cryptroot = { 24 + device = "/dev/vdc"; 25 + crypttabExtraOpts = [ "tpm2-device=auto" ]; 26 + }; 27 + }; 28 + virtualisation.bootDevice = "/dev/mapper/cryptroot"; 29 + }; 30 + }; 31 + 32 + testScript = '' 33 + import subprocess 34 + import os 35 + import time 36 + 37 + 38 + class Tpm: 39 + def __init__(self): 40 + os.mkdir("/tmp/mytpm1") 41 + self.start() 42 + 43 + def start(self): 44 + 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"]) 45 + 46 + def wait_for_death_then_restart(self): 47 + while self.proc.poll() is None: 48 + print("waiting for tpm to die") 49 + time.sleep(1) 50 + assert self.proc.returncode == 0 51 + self.start() 52 + 53 + tpm = Tpm() 54 + 55 + 56 + # Create encrypted volume 57 + machine.wait_for_unit("multi-user.target") 58 + machine.succeed("echo -n supersecret | cryptsetup luksFormat -q --iter-time=1 /dev/vdc -") 59 + machine.succeed("PASSWORD=supersecret SYSTEMD_LOG_LEVEL=debug systemd-cryptenroll --tpm2-pcrs= --tpm2-device=auto /dev/vdc |& systemd-cat") 60 + 61 + # Boot from the encrypted disk 62 + machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-luks.conf") 63 + machine.succeed("sync") 64 + machine.crash() 65 + 66 + tpm.wait_for_death_then_restart() 67 + 68 + # Boot and decrypt the disk 69 + machine.wait_for_unit("multi-user.target") 70 + assert "/dev/mapper/cryptroot on / type ext4" in machine.succeed("mount") 71 + ''; 72 + })
+5
pkgs/os-specific/linux/cryptsetup/default.nix
··· 20 20 sha256 = "sha256-kYSm672c5+shEVLn90GmyC8tHMDiSoTsnFKTnu4PBUI="; 21 21 }; 22 22 23 + patches = [ 24 + # Allow reading tokens from a relative path, see #167994 25 + ./relative-token-path.patch 26 + ]; 27 + 23 28 postPatch = '' 24 29 patchShebangs tests 25 30
+50
pkgs/os-specific/linux/cryptsetup/relative-token-path.patch
··· 1 + From 4f95ab1f8110a8ab9d7b0e192731ce467f6e5c26 Mon Sep 17 00:00:00 2001 2 + From: =?UTF-8?q?Janne=20He=C3=9F?= <janne@hess.ooo> 3 + Date: Sun, 4 Sep 2022 11:15:02 -0600 4 + Subject: [PATCH] Allow loading token handlers from the default search path 5 + 6 + Since [1] landed in cryptsetup, token handlers (libcryptsetup-token-*.so) 7 + are loaded from a fixed path defined at compile-time. This is 8 + problematic with NixOS since it introduces a dependency cycle 9 + between cryptsetup and systemd. 10 + 11 + This downstream patch [2] allows loading token plugins from the 12 + default library search path. This approach is not accepted upstream [3] 13 + due to security concerns, but the potential attack vectors require 14 + root access and they are sufficiently addressed: 15 + 16 + * cryptsetup could be used as a setuid binary (not used in NixOS). 17 + In this case, LD_LIBRARY_PATH is ignored because of secure-execution 18 + mode. 19 + * cryptsetup running as root could lead to a malicious token handler 20 + being loaded through LD_LIBRARY_PATH. However, fixing the path 21 + doesn't prevent the same malicious .so being loaded through LD_PRELOAD. 22 + 23 + [1] https://gitlab.com/cryptsetup/cryptsetup/-/commit/5b9e98f94178d3cd179d9f6e2a0a68c7d9eb6507 24 + [2] https://github.com/NixOS/nixpkgs/issues/167994#issuecomment-1094249369 25 + [3] https://gitlab.com/cryptsetup/cryptsetup/-/issues/733 26 + --- 27 + lib/luks2/luks2_token.c | 4 +--- 28 + 1 file changed, 1 insertion(+), 3 deletions(-) 29 + 30 + diff --git a/lib/luks2/luks2_token.c b/lib/luks2/luks2_token.c 31 + index 26467253..6f8329f0 100644 32 + --- a/lib/luks2/luks2_token.c 33 + +++ b/lib/luks2/luks2_token.c 34 + @@ -151,12 +151,10 @@ crypt_token_load_external(struct crypt_device *cd, const char *name, struct cryp 35 + 36 + token = &ret->u.v2; 37 + 38 + - r = snprintf(buf, sizeof(buf), "%s/libcryptsetup-token-%s.so", crypt_token_external_path(), name); 39 + + r = snprintf(buf, sizeof(buf), "libcryptsetup-token-%s.so", name); 40 + if (r < 0 || (size_t)r >= sizeof(buf)) 41 + return -EINVAL; 42 + 43 + - assert(*buf == '/'); 44 + - 45 + log_dbg(cd, "Trying to load %s.", buf); 46 + 47 + h = dlopen(buf, RTLD_LAZY); 48 + -- 49 + 2.37.2 50 +
+10 -1
pkgs/os-specific/linux/systemd/default.nix
··· 7 7 , fetchpatch 8 8 , fetchzip 9 9 , buildPackages 10 + , makeBinaryWrapper 10 11 , ninja 11 12 , meson 12 13 , m4 ··· 333 334 nativeBuildInputs = 334 335 [ 335 336 pkg-config 337 + makeBinaryWrapper 336 338 gperf 337 339 ninja 338 340 meson ··· 668 670 preFixup = lib.optionalString withEfi '' 669 671 mv $out/lib/systemd/boot/efi $out/dont-strip-me 670 672 ''; 671 - postFixup = lib.optionalString withEfi '' 673 + 674 + # Wrap in the correct path for LUKS2 tokens. 675 + postFixup = lib.optionalString withCryptsetup '' 676 + for f in lib/systemd/systemd-cryptsetup bin/systemd-cryptenroll; do 677 + # 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` 678 + wrapProgram $out/$f --prefix LD_LIBRARY_PATH : ${placeholder "out"}/lib/cryptsetup 679 + done 680 + '' + lib.optionalString withEfi '' 672 681 mv $out/dont-strip-me $out/lib/systemd/boot/efi 673 682 ''; 674 683