diff --git a/example/luks-tpm2.nix b/example/luks-tpm2.nix new file mode 100644 index 00000000..90492858 --- /dev/null +++ b/example/luks-tpm2.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; + enrollFinalTPM2PostInstall = 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 4b8b95a4..d5afb48b 100644 --- a/lib/tests.nix +++ b/lib/tests.nix @@ -301,6 +301,7 @@ let ) testConfigInstall.networking.hostId; virtualisation = { + tpm.enable = true; emptyDiskImages = builtins.genList (_: 4096) num-disks; qemu.options = lib.mkIf enableCanokey [ "-device pci-ohci,id=usb-bus" diff --git a/lib/types/luks.nix b/lib/types/luks.nix index 9fc0b026..26525152 100644 --- a/lib/types/luks.nix +++ b/lib/types/luks.nix @@ -9,7 +9,7 @@ }: let # These options will automatically generate a temporary password and remove it later on. - autogeneratedPassword = config.enrollFido2; + autogeneratedPassword = config.enrollFido2 || config.enrollTPM2; keyFile = if config.settings ? "keyFile" then @@ -122,7 +122,7 @@ in && config.passwordFile == null && (!config.settings ? "keyFile") && !autogeneratedPassword; - defaultText = "true if neither keyFile nor passwordFile nor enrollFido2 are set"; + defaultText = "true if neither keyFile nor passwordFile nor enrollFido2 nor enrollTPM2 are set"; description = "Whether to ask for a password for initial encryption"; }; enrollFido2 = lib.mkOption { @@ -138,10 +138,28 @@ in ]; description = "Extra arguments to pass to `systemd-cryptenroll` when enrolling the FIDO2 device"; }; + enrollTPM2 = lib.mkOption { + type = lib.types.bool; + default = config.enrollFinalTPM2PostInstall; + description = "Whether to enroll a TPM2 token. This enrollment is intended for bootstrapping, thus it omits PCR bindings. For stronger security with PCR-based policies, use `enrollFinalTPM2PostInstall`."; + }; + enrollFinalTPM2PostInstall = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Whether to generate a NixOS systemd service to enroll a final TPM2 token after first boot, this allows proper PCRs to be configured via `extraFinalTPM2EnrollArgs`"; + }; + extraFinalTPM2EnrollArgs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + example = [ + "--tpm2-pcrs=4+7" + ]; + description = "Extra arguments to pass to `systemd-cryptenroll` when enrolling the final TPM2 token"; + }; enrollRecovery = lib.mkOption { type = lib.types.bool; - default = config.enrollFido2; - defaultText = "true if fido2 is enabled"; + default = autogeneratedPassword; + defaultText = "true if fido2 or tpm2 enrollment 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 { @@ -272,6 +290,14 @@ in fi fi ''} + ${lib.optionalString config.enrollTPM2 '' + if ! systemd-cryptenroll "${config.device}" 2>/dev/null | grep -qw tpm2; then + systemd-cryptenroll \ + --tpm2-device=auto \ + --unlock-key-file=${formatKeyFile} \ + "${config.device}" + fi + ''} ${lib.optionalString config.enrollFido2 '' if ! systemd-cryptenroll "${config.device}" 2>/dev/null | grep -qw fido2; then wait_for_token() { @@ -339,19 +365,70 @@ in internal = true; readOnly = true; default = - [ ] + [ + { + assertions = [ + { + assertion = !config.enrollFinalTPM2PostInstall || config.enrollTPM2; + message = "`enrollFinalTPM2PostInstall` requires `enrollTPM2`."; + } + ]; + } + ] # If initrdUnlock is true, then add a device entry to the initrd.luks.devices config. ++ (lib.optional config.initrdUnlock [ { boot.initrd.luks.devices.${config.name} = { inherit (config) device; - crypttabExtraOpts = lib.mkIf config.enrollFido2 [ "fido2-device=auto" ]; + crypttabExtraOpts = lib.mkIf config.enrollFido2 [ "fido2-device=auto" ] + // lib.mkIf config.enrollTPM2 ["tpm2-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; + boot.initrd.systemd.enable = lib.mkIf (config.enrollFido2 || config.enrollTPM2) true; + + # Should we turn this into an assertion? + security.tpm2.enable = lib.mkIf config.enrollTPM2 true; + } + ]) + ++ (lib.optional config.enrollFinalTPM2PostInstall [ + { + systemd.services."disko-tpm2-enroll-${config.name}" = { + description = "Finalize TPM2 unlock key for ${config.name}"; + + wantedBy = ["multi-user.target"]; + after = ["tpm2.target"]; + wants = ["tpm2.target"]; + + unitConfig = { + ConditionPathExists = "!/var/lib/disko/tpm2-enrolled-${config.name}"; + }; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + + script = '' + set -euo pipefail + + echo "Removing temporary TPM2 token" + + systemd-cryptenroll \ + "${config.device}" \ + --unlock-tpm2-device=auto \ + --tpm2-device=auto \ + --wipe-slot=tpm2 \ + ${toString config.extraFinalTPM2EnrollArgs} + + mkdir -p /var/lib/disko + touch /var/lib/disko/tpm2-enrolled-${config.name} + + echo "TPM2 finalization complete" + ''; + }; } ]) ++ (lib.optional (config.content != null) config.content._config); diff --git a/module.nix b/module.nix index 1094a5c2..5c9f5834 100644 --- a/module.nix +++ b/module.nix @@ -243,15 +243,18 @@ in }; config = { - assertions = [ - { - assertion = config.disko.imageBuilder.qemu != null -> diskoLib.vmToolsSupportsCustomQemu lib; - message = '' - You have set config.disko.imageBuild.qemu, but vmTools in your nixpkgs version "${lib.version}" - does not support overriding the qemu package with the customQemu option yet. - Please upgrade nixpkgs so that `lib.version` is at least "24.11.20240709". - ''; - } + assertions = lib.mkMerge [ + cfg.devices._config.assertions + [ + { + assertion = config.disko.imageBuilder.qemu != null -> diskoLib.vmToolsSupportsCustomQemu lib; + message = '' + You have set config.disko.imageBuild.qemu, but vmTools in your nixpkgs version "${lib.version}" + does not support overriding the qemu package with the customQemu option yet. + Please upgrade nixpkgs so that `lib.version` is at least "24.11.20240709". + ''; + } + ] ]; _module.args.imagePkgs = pkgs; @@ -307,5 +310,7 @@ in fileSystems = lib.mkIf cfg.enableConfig cfg.devices._config.fileSystems or { }; boot = lib.mkIf cfg.enableConfig cfg.devices._config.boot or { }; swapDevices = lib.mkIf cfg.enableConfig cfg.devices._config.swapDevices or [ ]; + systemd = lib.mkIf cfg.enableConfig cfg.devices._config.systemd or { }; + security = lib.mkIf cfg.enableConfig cfg.devices._config.security or { }; }; } diff --git a/tests/luks-tpm2.nix b/tests/luks-tpm2.nix new file mode 100644 index 00000000..72836538 --- /dev/null +++ b/tests/luks-tpm2.nix @@ -0,0 +1,18 @@ +{ + pkgs ? import { }, + diskoLib ? pkgs.callPackage ../lib { }, +}: +diskoLib.testLib.makeDiskoTest { + inherit pkgs; + name = "luks-tpm2"; + disko-config = ../example/luks-tpm2.nix; + enableCanokey = true; + extraTestScript = '' + machine.succeed("cryptsetup isLuks /dev/vda2"); + machine.succeed("mountpoint /"); + + machine.succeed("systemd-cryptenroll /dev/vda2 | grep -qw tpm2") + # Recovery should be disabled + machine.fail("systemd-cryptenroll /dev/vda2 | grep -qw recovery") + ''; +}