From e172615f428cf56210da74b7edd6ff7e3e9842a1 Mon Sep 17 00:00:00 2001 From: Will Fancher Date: Fri, 14 Apr 2023 11:38:55 -0400 Subject: [PATCH] pcr phases --- Cargo.lock | 1 + flake.lock | 6 +- flake.nix | 2 + generator/Cargo.toml | 1 + generator/src/bootable/efi.rs | 102 ++++++++++++++----- generator/src/bootable/mod.rs | 2 + generator/src/bootable/pcr.rs | 11 +++ generator/src/main.rs | 16 ++- generator/src/systemd_boot/mod.rs | 12 ++- nixos-module.nix | 64 ++++++++++++ pcr-test.nix | 157 ++++++++++++++++++++++++++++++ 11 files changed, 344 insertions(+), 30 deletions(-) create mode 100644 generator/src/bootable/pcr.rs create mode 100644 pcr-test.nix diff --git a/Cargo.lock b/Cargo.lock index 5e8d669..85e7b9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -233,6 +233,7 @@ dependencies = [ "chrono", "lazy_static", "regex", + "serde", "serde_json", "structopt", "tempfile", diff --git a/flake.lock b/flake.lock index 2375087..255580c 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1659131907, - "narHash": "sha256-8bz4k18M/FuVC+EVcI4aREN2PsEKT7LGmU2orfjnpCg=", + "lastModified": 1681217261, + "narHash": "sha256-RbxCHWN3Vhyv/WEsXcJlDwF7bpvZ9NxDjfSouQxXEKo=", "owner": "nixos", "repo": "nixpkgs", - "rev": "8d435fca5c561da8168abb30270788d2da2a7951", + "rev": "3fb8eedc450286d5092e4953118212fa21091b3b", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 80257d0..b8de2c5 100644 --- a/flake.nix +++ b/flake.nix @@ -56,6 +56,8 @@ }; sbattach = import ./installer/patched-sbattach.nix { inherit pkgs; }; + + pcrTest = pkgs.callPackage ./pcr-test.nix { inherit inputs; }; }); defaultPackage = forAllSystems ({ system, ... }: self.packages.${system}.package); diff --git a/generator/Cargo.toml b/generator/Cargo.toml index d24a780..bd8bead 100644 --- a/generator/Cargo.toml +++ b/generator/Cargo.toml @@ -16,6 +16,7 @@ doctest = false chrono = { version = "0.4.23", default-features = false, features = [ "std", "clock" ] } lazy_static = "1.4.0" regex = { version = "1.7.1" } +serde = "1.0.137" serde_json = "1.0.94" tempfile = "3.3.0" structopt = { version = "0.3.26", default-features = false } diff --git a/generator/src/bootable/efi.rs b/generator/src/bootable/efi.rs index 99bd795..4b57c58 100644 --- a/generator/src/bootable/efi.rs +++ b/generator/src/bootable/efi.rs @@ -1,10 +1,11 @@ +use std::io::Seek; use std::io::Write; -use std::path::Path; -use std::process::Command; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; use tempfile::NamedTempFile; -use super::BootableToplevel; +use super::{BootableToplevel, PcrPhase}; use crate::Result; pub struct EfiProgram { @@ -16,9 +17,17 @@ impl EfiProgram { Self { source } } - pub fn write_unified_efi(&self, objcopy: &Path, outpath: &Path, stub: &Path) -> Result<()> { + pub fn write_unified_efi( + &self, + objcopy: &Path, + systemd_measure: &Option, + pcr_phases: &Option>, + outpath: &Path, + stub: &Path, + ) -> Result<()> { let generation_path = &self.source.toplevel.0; let mut kernel_params = NamedTempFile::new()?; + let mut pcr_sig = NamedTempFile::new()?; write!( kernel_params, @@ -27,30 +36,75 @@ impl EfiProgram { self.source.kernel_params.join(" ") )?; + write!(pcr_sig, "{{}}")?; + + if let Some(pcr_phases) = pcr_phases { + for phase in pcr_phases { + let mut cmd = Command::new(systemd_measure.as_ref().unwrap()); + for bank in &phase.banks { + cmd.args(["--bank", bank]); + } + cmd.args([ + "--osrel", + &format!("{}/etc/os-release", generation_path.display()), + "--cmdline", + &format!("{}", kernel_params.path().display()), + "--linux", + &format!("{}/kernel", generation_path.display()), + "--initrd", + &format!("{}/initrd", generation_path.display()), + "--phase", + &phase.phase_path.to_string(), + "--private-key", + &format!("{}", phase.private_key_file.display()), + "--public-key", + &format!("{}", phase.public_key_file.display()), + "--append", + &format!("{}", pcr_sig.path().display()), + "sign", + ]); + let output = cmd.stderr(Stdio::inherit()).output()?; + + if !output.status.success() { + return Err("failed to sign measurement".into()); + } + pcr_sig.rewind()?; + pcr_sig.as_file().set_len(0)?; + pcr_sig.write_all(&output.stdout)?; + } + } + // Offsets taken from one of systemd's EFI tests: // https://github.com/systemd/systemd/blob/01d0123f044d6c090b6ac2f6d304de2bdb19ae3b/test/test-efi-create-disk.sh#L32-L38 - let status = Command::new(objcopy) - .args(&[ - "--add-section", - &format!(".osrel={}/etc/os-release", generation_path.display()), - "--change-section-vma", - ".osrel=0x20000", + let mut cmd = Command::new(objcopy); + cmd.args([ + "--add-section", + &format!(".osrel={}/etc/os-release", generation_path.display()), + "--change-section-vma", + ".osrel=0x20000", + "--add-section", + &format!(".cmdline={}", kernel_params.path().display()), + "--change-section-vma", + ".cmdline=0x30000", + "--add-section", + &format!(".linux={}/kernel", generation_path.display()), + "--change-section-vma", + ".linux=0x2000000", + "--add-section", + &format!(".initrd={}/initrd", generation_path.display()), + "--change-section-vma", + ".initrd=0x3000000", + ]); + if pcr_phases.is_some() { + cmd.args([ "--add-section", - &format!(".cmdline={}", kernel_params.path().display()), + &format!(".pcrsig={}", pcr_sig.path().display()), "--change-section-vma", - ".cmdline=0x30000", - "--add-section", - &format!(".linux={}/kernel", generation_path.display()), - "--change-section-vma", - ".linux=0x2000000", - "--add-section", - &format!(".initrd={}/initrd", generation_path.display()), - "--change-section-vma", - ".initrd=0x3000000", - &stub.display().to_string(), - &outpath.display().to_string(), - ]) - .status()?; + ".pcrsig=0x40000", + ]); + } + cmd.args([&stub.display().to_string(), &outpath.display().to_string()]); + let status = cmd.status()?; if !status.success() { return Err("failed to write unified efi".into()); diff --git a/generator/src/bootable/mod.rs b/generator/src/bootable/mod.rs index 58e7c9a..059179d 100644 --- a/generator/src/bootable/mod.rs +++ b/generator/src/bootable/mod.rs @@ -5,9 +5,11 @@ use bootspec::SpecialisationName; use crate::{Generation, Result}; mod efi; +mod pcr; mod toplevel; pub use efi::EfiProgram; +pub use pcr::PcrPhase; pub use toplevel::BootableToplevel; pub enum Bootable { diff --git a/generator/src/bootable/pcr.rs b/generator/src/bootable/pcr.rs new file mode 100644 index 0000000..f9a34ac --- /dev/null +++ b/generator/src/bootable/pcr.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct PcrPhase { + pub phase_path: String, + pub banks: Vec, + pub private_key_file: PathBuf, + pub public_key_file: PathBuf, +} diff --git a/generator/src/main.rs b/generator/src/main.rs index 4b5608c..392835f 100644 --- a/generator/src/main.rs +++ b/generator/src/main.rs @@ -1,6 +1,7 @@ +use std::fs; use std::path::PathBuf; -use generator::bootable::{self, Bootable, EfiProgram}; +use generator::bootable::{self, Bootable, EfiProgram, PcrPhase}; use generator::{systemd_boot, Generation, Result}; use structopt::StructOpt; @@ -13,6 +14,12 @@ struct Args { /// The `objcopy` binary #[structopt(long, requires_all = &["systemd-efi-stub", "unified-efi"])] objcopy: Option, + /// The `systemd-measure` binary + #[structopt(long, requires_all = &["systemd-efi-stub", "unified-efi"])] + systemd_measure: Option, + /// The pcr phase spec json file + #[structopt(long, requires_all = &["systemd-efi-stub", "unified-efi"])] + pcr_phases: Option, /// Whether or not to combine the initrd and kernel into a unified EFI file #[structopt(long, requires_all = &["systemd-efi-stub", "objcopy"])] unified_efi: bool, @@ -58,9 +65,16 @@ fn main() -> Result<()> { toplevels.into_iter().map(Bootable::Linux).collect() }; + let pcr_phases: Option> = args.pcr_phases.map(|json_path| { + let cont = fs::read_to_string(json_path).unwrap(); + serde_json::from_str(&cont).unwrap() + }); + systemd_boot::generate( bootables, args.objcopy, + args.systemd_measure, + pcr_phases, args.systemd_efi_stub, args.systemd_machine_id_setup, )?; diff --git a/generator/src/systemd_boot/mod.rs b/generator/src/systemd_boot/mod.rs index ecd3053..c7ab189 100644 --- a/generator/src/systemd_boot/mod.rs +++ b/generator/src/systemd_boot/mod.rs @@ -6,7 +6,7 @@ use std::process::Command; use bootspec::SpecialisationName; -use crate::bootable::{Bootable, BootableToplevel, EfiProgram}; +use crate::bootable::{Bootable, BootableToplevel, EfiProgram, PcrPhase}; use crate::Result; // FIXME: placeholder dir @@ -38,6 +38,8 @@ pub struct Contents { pub fn generate( bootables: Vec, objcopy: Option, + systemd_measure: Option, + pcr_phases: Option>, systemd_efi_stub: Option, systemd_machine_id_setup: PathBuf, ) -> Result<()> { @@ -58,7 +60,13 @@ pub fn generate( let objcopy = objcopy.as_ref().unwrap(); let systemd_efi_stub = systemd_efi_stub.as_ref().unwrap(); - efi.write_unified_efi(objcopy, Path::new(&unified_dest), systemd_efi_stub)?; + efi.write_unified_efi( + objcopy, + &systemd_measure, + &pcr_phases, + Path::new(&unified_dest), + systemd_efi_stub, + )?; } Bootable::Linux(toplevel) => { let (path, contents) = self::linux_entry_impl(&toplevel, &machine_id)?; diff --git a/nixos-module.nix b/nixos-module.nix index c695ace..c0ebb7e 100644 --- a/nixos-module.nix +++ b/nixos-module.nix @@ -14,9 +14,68 @@ in type = types.nullOr types.str; default = null; }; + + pcrPhases = { + enable = lib.mkEnableOption "pcr phases"; + + signatures = lib.mkOption { + default = { }; + type = types.attrsOf (types.submodule ({ name, ... }: { + config.phasePath = lib.mkDefault name; + options = { + phasePath = lib.mkOption { + type = types.str; + }; + banks = lib.mkOption { + type = types.listOf types.str; + default = [ ]; + }; + privateKeyFile = lib.mkOption { + type = types.path // { apply = toString; }; + }; + publicKeyFile = lib.mkOption { + type = types.path // { apply = toString; }; + }; + }; + })); + }; + }; }; }; config = { + boot.kernelParams = lib.mkIf config.boot.loader.secureboot.pcrPhases.enable [ "systemd.gpt_auto=false" ]; + boot.initrd = lib.mkIf config.boot.loader.secureboot.pcrPhases.enable { + availableKernelModules = [ "efivarfs" ]; + systemd = { + package = config.systemd.package; + additionalUpstreamUnits = [ "systemd-pcrphase-initrd.service" ]; + services.systemd-pcrphase-initrd = { + wantedBy = [ "initrd.target" ]; + after = [ "systemd-modules-load.service" ]; + + # TODO: How should this be pulled in? + wants = [ "cryptsetup-pre.target" ]; + }; + + # TODO: This is sketchy, but works as long as no initrd FSes + # are ordered before local-fs.target (zfs currently needlessly + # does this in nixos) + targets.cryptsetup-pre.after = [ "systemd-tmpfiles-setup.service" ]; + + storePaths = [ "${config.boot.initrd.systemd.package}/lib/systemd/systemd-pcrphase" ]; + contents."/etc/tmpfiles.d/90-tpm-pcr-signature.conf".text = '' + C /run/systemd/tpm2-pcr-signature.json - - - - /.extra/tpm2-pcr-signature.json + ''; + }; + }; + systemd = lib.mkIf config.boot.loader.secureboot.pcrPhases.enable { + additionalUpstreamSystemUnits = [ + "systemd-pcrphase-sysinit.service" + "systemd-pcrphase.service" + ]; + services.systemd-pcrphase-sysinit.wantedBy = [ "basic.target" ]; + services.systemd-pcrphase.wantedBy = [ "multi-user.target" ]; + }; boot.loader.external = { enable = true; installHook = pkgs.writeShellScript "install-bootloader" @@ -34,6 +93,11 @@ in "--systemd-efi-stub" "${config.systemd.package}/lib/systemd/boot/efi/linuxx64.efi.stub" + ] ++ lib.optionals config.boot.loader.secureboot.pcrPhases.enable [ + "--systemd-measure" + "${config.systemd.package}/lib/systemd/systemd-measure" + "--pcr-phases" + (pkgs.writeText "pcr-phases" (builtins.toJSON (lib.mapAttrsToList (n: v: v) config.boot.loader.secureboot.pcrPhases.signatures))) ])); installerArgs = lib.escapeShellArgs diff --git a/pcr-test.nix b/pcr-test.nix new file mode 100644 index 0000000..1feae9c --- /dev/null +++ b/pcr-test.nix @@ -0,0 +1,157 @@ +{ inputs +, lib +, nixosTest +, cryptsetup +, sbctl +, swtpm +, OVMFFull +, e2fsprogs +, libressl +, systemd +}: + +nixosTest ( + let + baseSecureBoot = { + imports = [ inputs.self.nixosModules.bootspec-secureboot ]; + boot.loader.systemd-boot.enable = false; + boot.loader.secureboot = { + enable = true; + signingKeyPath = "/etc/secureboot/keys/db/db.key"; + signingCertPath = "/etc/secureboot/keys/db/db.pem"; + + pcrPhases = { + enable = true; + signatures."enter-initrd" = { + privateKeyFile = "/etc/pcrphase-keys/tpm2-pcr-private.pem"; + publicKeyFile = "/etc/pcrphase-keys/tpm2-pcr-public.pem"; + }; + }; + }; + + # Enable boot counting, because nixosTest's allow_reboot doesn't + # crash on panic. Will fallback to a working config if we mess + # with the /boot/loader/entries files a little bit. + systemd.additionalUpstreamSystemUnits = [ + "systemd-bless-boot.service" + "boot-complete.target" + "systemd-boot-check-no-failures.service" + ]; + }; + in + { + name = "pcr-test"; + + nodes.machine = { config, ... }: { + virtualisation = { + emptyDiskImages = [ 512 ]; + useBootLoader = true; + useEFIBoot = true; + efi = { + inherit (OVMFFull) firmware variables; + }; + qemu.options = [ "-chardev socket,id=chrtpm,path=/tmp/mytpm1/swtpm-sock -tpmdev emulator,id=tpm0,chardev=chrtpm -device tpm-tis,tpmdev=tpm0" ]; + }; + boot.loader.timeout = 0; + boot.loader.efi.canTouchEfiVariables = true; + boot.loader.systemd-boot.enable = lib.mkDefault true; + boot.initrd.kernelModules = [ "tpm_tis" "tpm_crb" ]; + environment.systemPackages = [ cryptsetup sbctl e2fsprogs libressl ]; + boot.initrd.systemd.enable = true; + + specialisation.secureboot.configuration = baseSecureBoot; + specialisation.unlock-cryptenroll.configuration = { + imports = [ baseSecureBoot ]; + boot.initrd.luks.devices = lib.mkVMOverride { + "foo".device = "/dev/vdc"; + "foo".crypttabExtraOpts = [ "tpm2-device=auto" "headless=true" ]; # tpm2-signature default should work + }; + virtualisation.fileSystems."/foo" = { + fsType = "ext4"; + autoFormat = true; + device = "/dev/mapper/foo"; + neededForBoot = true; + }; + }; + }; + + testScript = '' + import subprocess + import os + + os.mkdir("/tmp/mytpm1") + subprocess.Popen( + [ + "${swtpm}/bin/swtpm", + "socket", "--tpmstate", "dir=/tmp/mytpm1", "--ctrl", + "type=unixio,path=/tmp/mytpm1/swtpm-sock", + "--log", "level=20", "--tpm2" + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + + machine.start(allow_reboot=True) + machine.wait_for_unit("multi-user.target") + machine.fail("test -e /sys/firmware/efi/efivars/StubPcrKernelImage-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f") + machine.succeed( + "sbctl create-keys", + "sbctl enroll-keys --yes-this-might-brick-my-machine", + "mkdir /etc/pcrphase-keys", + "openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out /etc/pcrphase-keys/tpm2-pcr-private.pem", + "openssl rsa -pubout -in /etc/pcrphase-keys/tpm2-pcr-private.pem -out /etc/pcrphase-keys/tpm2-pcr-public.pem", + "ln -s system-1-link /nix/var/nix/profiles/system", + "ln -s $(readlink -f /run/current-system/specialisation/secureboot) /nix/var/nix/profiles/system-1-link", + "ln -s $(readlink -f /run/current-system) /nix/var/nix/profiles/orig-system", + "rm -vr /boot/*", + "NIXOS_INSTALL_BOOTLOADER=1 /nix/var/nix/profiles/system/bin/switch-to-configuration boot", + "sync", + ) + print(machine.succeed("sbctl verify")) + machine.reboot() + + machine.wait_for_unit("multi-user.target") + machine.succeed( + "test -e /sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f", + "test -e /sys/firmware/efi/efivars/StubPcrKernelImage-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f", + "test -e /run/systemd/tpm2-pcr-signature.json", + + "echo somepass | cryptsetup luksFormat --type=luks2 /dev/vdc", + "dd if=/dev/urandom of=/etc/keyfile bs=32 count=1", + "echo somepass | cryptsetup luksAddKey --new-keyfile=/etc/keyfile /dev/vdc", + "systemd-cryptenroll --unlock-key-file=/etc/keyfile --tpm2-device=auto --tpm2-public-key=/etc/pcrphase-keys/tpm2-pcr-public.pem --tpm2-public-key-pcrs=11 --tpm2-pcrs=0+2+7 /dev/vdc", + "rm /nix/var/nix/profiles/system", + "ln -s $(readlink -f /nix/var/nix/profiles/orig-system/specialisation/unlock-cryptenroll) /nix/var/nix/profiles/system-2-link", + "ln -s system-2-link /nix/var/nix/profiles/system", + "/nix/var/nix/profiles/system/bin/switch-to-configuration boot", + "sync", + ) + + machine.reboot() + machine.wait_for_unit("multi-user.target") + machine.succeed( + # Test that the LUKS device was unlocked. + "test -e /dev/mapper/foo", + "umount /foo", + "cryptsetup close foo", + ) + # TPM should not allow unlocking this outside initrd + machine.fail("${systemd}/lib/systemd/systemd-cryptsetup attach foo /dev/vdc - tpm2-device=auto,headless=true,tpm2-signature=/run/systemd/tpm2-pcr-signature.json") + machine.succeed( + # Reset keys to make sure it won't unlock. + "sbctl reset", + # Mess with the loader entries to enable boot counting. + "mv /boot/loader/entries/nixos-generation-2.conf /boot/loader/entries/nixos-generation-2+3.conf", + "mv /boot/loader/entries/nixos-generation-1.conf /boot/loader/entries/nixos-generation-1+3.conf", + ) + + # With keys reset, the LUKS disk should fail because of PCR 7, so + # wait for systemd-boot boot counting to fallback to the previous + # generation + machine.reboot() + machine.wait_for_unit("systemd-bless-boot.service") + machine.succeed("[ $(readlink -f /run/current-system) = $(readlink -f /nix/var/nix/profiles/system-1-link) ]") + machine.fail("test -e /dev/mapper/foo") + ''; + } +)