diff --git a/flake.nix b/flake.nix index e4b2e6db..baf79eb1 100644 --- a/flake.nix +++ b/flake.nix @@ -57,6 +57,17 @@ } ); + flake.nixosModules.lanzasignd = moduleWithSystem ( + perSystem@{ config }: + { ... }: { + imports = [ + ./nix/modules/lanzasignd.nix + ]; + + services.lanzasignd.package = perSystem.config.packages.lanzasignd; + } + ); + flake.nixosModules.uki = moduleWithSystem ( perSystem@{ config }: { lib, ... }: { @@ -96,6 +107,8 @@ , src , target ? null , doCheck ? true + # By default, it builds the default members of the workspace. + , packages ? null , extraArgs ? { } }: let @@ -123,7 +136,9 @@ #[cfg_attr(any(target_os = "none", target_os = "uefi"), export_name = "efi_main")] fn main() {} ''; - } // extraArgs; + + cargoExtraArgs = (extraArgs.cargoExtraArgs or "") + (if packages != null then (lib.concatStringsSep " " (map (p: "--package ${p}") packages)) else ""); + } // builtins.removeAttrs extraArgs [ "cargoExtraArgs" ]; cargoArtifacts = craneLib.buildDepsOnly commonArgs; in @@ -154,6 +169,14 @@ }; }; + lanzasigndCrane = buildRustApp { + pname = "lanzasignd"; + src = craneLib.cleanCargoSource ./rust/tool; + doCheck = false; + packages = [ "lanzasignd" ]; + }; + + lanzasignd = lanzasigndCrane.package; stub = stubCrane.package; fatStub = fatStubCrane.package; @@ -188,18 +211,22 @@ in { packages = { - inherit stub fatStub; + inherit stub fatStub lanzasignd; tool = wrappedTool; lzbt = wrappedTool; }; overlayAttrs = { - inherit (config.packages) tool; + inherit (config.packages) tool lanzasignd; }; checks = let nixosLib = import (pkgs.path + "/nixos/lib") { }; + lanzaLib = import ./nix/tests/lib.nix { + inherit pkgs; + lanzabooteModule = self.nixosModules.lanzaboote; + }; runTest = module: nixosLib.runTest { imports = [ module ]; hostPkgs = pkgs; @@ -212,11 +239,14 @@ toolFmt = toolCrane.rustfmt; stubFmt = stubCrane.rustfmt; } // (import ./nix/tests/lanzaboote.nix { - inherit pkgs; + inherit pkgs lanzaLib; lanzabooteModule = self.nixosModules.lanzaboote; }) // (import ./nix/tests/stub.nix { inherit pkgs runTest; ukiModule = self.nixosModules.uki; + }) // (import ./nix/tests/remote-signing.nix { + inherit pkgs lanzaLib; + lanzasigndModule = self.nixosModules.lanzasignd; }); devShells.default = pkgs.mkShell { diff --git a/nix/modules/lanzaboote.nix b/nix/modules/lanzaboote.nix index d1dd8876..8c8ecd81 100644 --- a/nix/modules/lanzaboote.nix +++ b/nix/modules/lanzaboote.nix @@ -36,23 +36,33 @@ in ''; }; - pkiBundle = mkOption { - type = types.nullOr types.path; - description = "PKI bundle containing db, PK, KEK"; + localSigning = { + enable = mkEnableOption "local signing" // { default = cfg.pkiBundle != null; defaultText = lib.literalExpression "cfg.pkiBundle != null"; }; + publicKeyFile = mkOption { + type = types.path; + default = "${cfg.pkiBundle}/keys/db/db.pem"; + description = "Public key to sign your boot files"; + }; + + privateKeyFile = mkOption { + type = types.path; + default = "${cfg.pkiBundle}/keys/db/db.key"; + description = "Private key to sign your boot files"; + }; }; - publicKeyFile = mkOption { - type = types.path; - default = "${cfg.pkiBundle}/keys/db/db.pem"; - defaultText = "\${cfg.pkiBundle}/keys/db/db.pem"; - description = "Public key to sign your boot files"; + remoteSigning = { + enable = mkEnableOption "remote signing"; + serverUrl = mkOption { + type = types.nullOr types.str; + default = null; + description = "Remote signing server to contact to ask for signatures"; + }; }; - privateKeyFile = mkOption { - type = types.path; - default = "${cfg.pkiBundle}/keys/db/db.key"; - defaultText = "\${cfg.pkiBundle}/keys/db/db.key"; - description = "Private key to sign your boot files"; + pkiBundle = mkOption { + type = types.nullOr types.path; + description = "PKI bundle containing db, PK, KEK"; }; package = mkOption { @@ -103,31 +113,56 @@ in }; config = mkIf cfg.enable { + assertions = [ + { + assertion = !(cfg.localSigning.enable && cfg.remoteSigning.enable); + message = '' + You cannot enable local and remote signing at the same time, pick either of the strategy. + + Did you set `pkiBundle` and forgot to set `localSigning.enable` to false? + ''; + } + ]; boot.bootspec = { enable = true; }; boot.loader.supportsInitrdSecrets = true; boot.loader.external = { enable = true; - installHook = pkgs.writeShellScript "bootinstall" '' - ${optionalString cfg.enrollKeys '' - mkdir -p /tmp/pki - cp -r ${cfg.pkiBundle}/* /tmp/pki - ${sbctlWithPki}/bin/sbctl enroll-keys --yes-this-might-brick-my-machine - ''} - - # Use the system from the kernel's hostPlatform because this should - # always, even in the cross compilation case, be the right system. - ${cfg.package}/bin/lzbt install \ - --system ${config.boot.kernelPackages.stdenv.hostPlatform.system} \ - --systemd ${config.systemd.package} \ - --systemd-boot-loader-config ${loaderConfigFile} \ - --public-key ${cfg.publicKeyFile} \ - --private-key ${cfg.privateKeyFile} \ - --configuration-limit ${toString configurationLimit} \ - ${config.boot.loader.efi.efiSysMountPoint} \ - /nix/var/nix/profiles/system-*-link - ''; + installHook = + let + lzbtArgs = [ + "install" + "--system" + config.boot.kernelPackages.stdenv.hostPlatform.system + "--systemd" + config.systemd.package + "--systemd-boot-loader-config" + loaderConfigFile + ] ++ lib.optionals cfg.localSigning.enable [ + "--public-key" + cfg.localSigning.publicKeyFile + "--private-key" + cfg.localSigning.privateKeyFile + ] ++ lib.optionals cfg.remoteSigning.enable [ + "--remote-signing-server-url" + cfg.remoteSigning.serverUrl + ] ++ [ + "--configuration-limit" + (toString configurationLimit) + config.boot.loader.efi.efiSysMountPoint + "/nix/var/nix/profiles/system-*-link" + ]; + in + pkgs.writeShellScript "bootinstall" '' + ${optionalString cfg.enrollKeys '' + mkdir -p /tmp/pki + cp -r ${cfg.pkiBundle}/* /tmp/pki + ${sbctlWithPki}/bin/sbctl enroll-keys --yes-this-might-brick-my-machine + ''} + + ${cfg.package}/bin/lzbt ${concatStringsSep " " lzbtArgs} + ''; }; systemd.services.fwupd = lib.mkIf config.services.fwupd.enable { diff --git a/nix/modules/lanzasignd.nix b/nix/modules/lanzasignd.nix new file mode 100644 index 00000000..7dc71b6f --- /dev/null +++ b/nix/modules/lanzasignd.nix @@ -0,0 +1,76 @@ +{ lib, config, pkgs, ... }: +let + inherit (lib) mkOption mkEnableOption mkPackageOptionMD types mkIf; + cfg = config.services.lanzasignd; + policyFile = (pkgs.formats.json { }).generate "lanzasignd-policy.json" { + allowedKernelCmdlineItems = cfg.policy.allowedCommandLineItems; + }; +in +{ + options.services.lanzasignd = { + enable = mkEnableOption "lanzasignd, a Secure Boot remote signing server for NixOS"; + + package = mkPackageOptionMD pkgs "lanzasignd" { }; + + port = mkOption { + type = types.port; + default = 9999; + description = "Port to run lanzasignd on"; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Open the firewall for the port lanzasignd is running on"; + }; + + policy = { + allowedCommandLineItems = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + example = [ "quiet" "init=some init script" ]; + }; + }; + + pkiBundle = mkOption { + type = types.nullOr types.path; + description = "PKI bundle containing db, PK, KEK"; + }; + + publicKeyFile = mkOption { + type = types.path; + default = "${cfg.pkiBundle}/keys/db/db.pem"; + description = "Public key to sign your boot files"; + }; + + privateKeyFile = mkOption { + type = types.path; + default = "${cfg.pkiBundle}/keys/db/db.key"; + description = "Private key to sign your boot files"; + }; + + }; + + config = mkIf cfg.enable { + systemd.services.lanzasignd = { + description = "Sign on demand bootables files compatible with Lanzaboote scheme"; + wants = [ "network.target" ]; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig.Type = "simple"; + path = [ + pkgs.binutils + pkgs.sbsigntool + ]; + script = '' + ${cfg.package}/bin/lanzasignd -vvv serve \ + --policy-file ${policyFile} \ + --public-key ${cfg.publicKeyFile} \ + --private-key ${cfg.privateKeyFile} \ + --port ${toString cfg.port} + ''; + }; + + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ]; + }; +} diff --git a/nix/tests/lanzaboote.nix b/nix/tests/lanzaboote.nix index 29896066..163aa1a0 100644 --- a/nix/tests/lanzaboote.nix +++ b/nix/tests/lanzaboote.nix @@ -1,157 +1,14 @@ { pkgs , lanzabooteModule +, lanzaLib }: let - inherit (pkgs) lib system; - defaultTimeout = 5 * 60; # = 5 minutes - inherit (pkgs.stdenv.hostPlatform) efiArch; efiArchUppercased = lib.toUpper efiArch; - mkSecureBootTest = { name, machine ? { }, useSecureBoot ? true, useTPM2 ? false, readEfiVariables ? false, testScript }: - let - tpmSocketPath = "/tmp/swtpm-sock"; - tpmDeviceModels = { - x86_64-linux = "tpm-tis"; - aarch64-linux = "tpm-tis-device"; - }; - # Should go to nixpkgs. - efiVariablesHelpers = '' - import struct - - SD_LOADER_GUID = "4a67b082-0a4c-41cf-b6c7-440b29bb8c4f" - def read_raw_variable(var: str) -> bytes: - attr_var = machine.succeed(f"cat /sys/firmware/efi/efivars/{var}-{SD_LOADER_GUID}").encode('raw_unicode_escape') - _ = attr_var[:4] # First 4 bytes are attributes according to https://www.kernel.org/doc/html/latest/filesystems/efivarfs.html - value = attr_var[4:] - return value - def read_string_variable(var: str, encoding='utf-16-le') -> str: - return read_raw_variable(var).decode(encoding).rstrip('\x00') - # By default, it will read a 4 byte value, read `struct` docs to change the format. - def assert_variable_uint(var: str, expected: int, format: str = 'I'): - with subtest(f"Is `{var}` set to {expected} (uint)"): - value, = struct.unpack(f'<{format}', read_raw_variable(var)) - assert value == expected, f"Unexpected variable value in `{var}`, expected: `{expected}`, actual: `{value}`" - def assert_variable_string(var: str, expected: str, encoding='utf-16-le'): - with subtest(f"Is `{var}` correctly set"): - value = read_string_variable(var, encoding) - assert value == expected, f"Unexpected variable value in `{var}`, expected: `{expected.encode(encoding)!r}`, actual: `{value.encode(encoding)!r}`" - def assert_variable_string_contains(var: str, expected_substring: str): - with subtest(f"Do `{var}` contain expected substrings"): - value = read_string_variable(var).strip() - assert expected_substring in value, f"Did not find expected substring in `{var}`, expected substring: `{expected_substring}`, actual value: `{value}`" - ''; - tpm2Initialization = '' - import subprocess - from tempfile import TemporaryDirectory - - # From systemd-initrd-luks-tpm2.nix - class Tpm: - def __init__(self): - self.state_dir = TemporaryDirectory() - self.start() - - def start(self): - self.proc = subprocess.Popen(["${pkgs.swtpm}/bin/swtpm", - "socket", - "--tpmstate", f"dir={self.state_dir.name}", - "--ctrl", "type=unixio,path=${tpmSocketPath}", - "--tpm2", - ]) - - # Check whether starting swtpm failed - try: - exit_code = self.proc.wait(timeout=0.2) - if exit_code is not None and exit_code != 0: - raise Exception("failed to start swtpm") - except subprocess.TimeoutExpired: - pass - - """Check whether the swtpm process exited due to an error""" - def check(self): - exit_code = self.proc.poll() - if exit_code is not None and exit_code != 0: - raise Exception("swtpm process died") - - tpm = Tpm() - - @polling_condition - def swtpm_running(): - tpm.check() - ''; - in - pkgs.nixosTest { - inherit name; - globalTimeout = defaultTimeout; - - testScript = '' - ${lib.optionalString useTPM2 tpm2Initialization} - ${lib.optionalString readEfiVariables efiVariablesHelpers} - ${testScript} - ''; - - nodes.machine = { pkgs, lib, ... }: { - imports = [ - lanzabooteModule - machine - ]; - - virtualisation = { - useBootLoader = true; - useEFIBoot = true; - - # We actually only want to enable features in OVMF, but at - # the moment edk2 202308 is also broken. So we downgrade it - # here as well. How painful! - # - # See #240. - efi.OVMF = - let - edk2Version = "202305"; - edk2Src = pkgs.fetchFromGitHub { - owner = "tianocore"; - repo = "edk2"; - rev = "edk2-stable${edk2Version}"; - fetchSubmodules = true; - hash = "sha256-htOvV43Hw5K05g0SF3po69HncLyma3BtgpqYSdzRG4s="; - }; - - edk2 = pkgs.edk2.overrideAttrs (old: rec { - version = edk2Version; - src = edk2Src; - }); - in - (pkgs.OVMF.override { - secureBoot = useSecureBoot; - tpmSupport = useTPM2; # This is needed otherwise OVMF won't initialize the TPM2 protocol. - - edk2 = edk2; - }).overrideAttrs (old: { - src = edk2Src; - }); - - qemu.options = lib.mkIf useTPM2 [ - "-chardev socket,id=chrtpm,path=${tpmSocketPath}" - "-tpmdev emulator,id=tpm_dev_0,chardev=chrtpm" - "-device ${tpmDeviceModels.${system}},tpmdev=tpm_dev_0" - ]; - - inherit useSecureBoot; - }; - - boot.initrd.availableKernelModules = lib.mkIf useTPM2 [ "tpm_tis" ]; - - boot.loader.efi = { - canTouchEfiVariables = true; - }; - boot.lanzaboote = { - enable = true; - enrollKeys = lib.mkDefault true; - pkiBundle = ./fixtures/uefi-keys; - }; - }; - }; + inherit (pkgs) lib; + inherit (lanzaLib) mkSecureBootTest; # Execute a boot test that has an intentionally broken secure boot # chain. This test is expected to fail with Secure Boot and should diff --git a/nix/tests/lib.nix b/nix/tests/lib.nix new file mode 100644 index 00000000..29974810 --- /dev/null +++ b/nix/tests/lib.nix @@ -0,0 +1,158 @@ +{ pkgs, lanzabooteModule }: +let + inherit (pkgs) lib system; + defaultTimeout = 5 * 60; # = 5 minutes +in +{ + mkSecureBootTest = { name, machine ? { }, useSecureBoot ? true, useTPM2 ? false, readEfiVariables ? false, testScript, extraNodes ? { } }: + let + tpmSocketPath = "/tmp/swtpm-sock"; + tpmDeviceModels = { + x86_64-linux = "tpm-tis"; + aarch64-linux = "tpm-tis-device"; + }; + # Should go to nixpkgs. + efiVariablesHelpers = '' + import struct + + SD_LOADER_GUID = "4a67b082-0a4c-41cf-b6c7-440b29bb8c4f" + def read_raw_variable(var: str) -> bytes: + attr_var = machine.succeed(f"cat /sys/firmware/efi/efivars/{var}-{SD_LOADER_GUID}").encode('raw_unicode_escape') + _ = attr_var[:4] # First 4 bytes are attributes according to https://www.kernel.org/doc/html/latest/filesystems/efivarfs.html + value = attr_var[4:] + return value + def read_string_variable(var: str, encoding='utf-16-le') -> str: + return read_raw_variable(var).decode(encoding).rstrip('\x00') + # By default, it will read a 4 byte value, read `struct` docs to change the format. + def assert_variable_uint(var: str, expected: int, format: str = 'I'): + with subtest(f"Is `{var}` set to {expected} (uint)"): + value, = struct.unpack(f'<{format}', read_raw_variable(var)) + assert value == expected, f"Unexpected variable value in `{var}`, expected: `{expected}`, actual: `{value}`" + def assert_variable_string(var: str, expected: str, encoding='utf-16-le'): + with subtest(f"Is `{var}` correctly set"): + value = read_string_variable(var, encoding) + assert value == expected, f"Unexpected variable value in `{var}`, expected: `{expected.encode(encoding)!r}`, actual: `{value.encode(encoding)!r}`" + def assert_variable_string_contains(var: str, expected_substring: str): + with subtest(f"Do `{var}` contain expected substrings"): + value = read_string_variable(var).strip() + assert expected_substring in value, f"Did not find expected substring in `{var}`, expected substring: `{expected_substring}`, actual value: `{value}`" + ''; + tpm2Initialization = '' + import subprocess + from tempfile import TemporaryDirectory + + # From systemd-initrd-luks-tpm2.nix + class Tpm: + def __init__(self): + self.state_dir = TemporaryDirectory() + self.start() + + def start(self): + self.proc = subprocess.Popen(["${pkgs.swtpm}/bin/swtpm", + "socket", + "--tpmstate", f"dir={self.state_dir.name}", + "--ctrl", "type=unixio,path=${tpmSocketPath}", + "--tpm2", + ]) + + # Check whether starting swtpm failed + try: + exit_code = self.proc.wait(timeout=0.2) + if exit_code is not None and exit_code != 0: + raise Exception("failed to start swtpm") + except subprocess.TimeoutExpired: + pass + + """Check whether the swtpm process exited due to an error""" + def check(self): + exit_code = self.proc.poll() + if exit_code is not None and exit_code != 0: + raise Exception("swtpm process died") + + tpm = Tpm() + + @polling_condition + def swtpm_running(): + tpm.check() + ''; + in + pkgs.nixosTest { + inherit name; + globalTimeout = defaultTimeout; + + testScript = { ... }@args: + let + testScript' = if lib.isFunction testScript then testScript args else testScript; + in + '' + ${lib.optionalString useTPM2 tpm2Initialization} + ${lib.optionalString readEfiVariables efiVariablesHelpers} + ${testScript'} + ''; + + + nodes = extraNodes // { + machine = { lib, ... }: { + imports = [ + lanzabooteModule + machine + ]; + + virtualisation = { + useBootLoader = true; + useEFIBoot = true; + + # We actually only want to enable features in OVMF, but at + # the moment edk2 202308 is also broken. So we downgrade it + # here as well. How painful! + # + # See #240. + efi.OVMF = + let + edk2Version = "202305"; + edk2Src = pkgs.fetchFromGitHub { + owner = "tianocore"; + repo = "edk2"; + rev = "edk2-stable${edk2Version}"; + fetchSubmodules = true; + hash = "sha256-htOvV43Hw5K05g0SF3po69HncLyma3BtgpqYSdzRG4s="; + }; + + edk2 = pkgs.edk2.overrideAttrs (old: rec { + version = edk2Version; + src = edk2Src; + }); + in + (pkgs.OVMF.override { + secureBoot = useSecureBoot; + tpmSupport = useTPM2; # This is needed otherwise OVMF won't initialize the TPM2 protocol. + + edk2 = edk2; + }).overrideAttrs (old: { + src = edk2Src; + }); + + qemu.options = lib.mkIf useTPM2 [ + "-chardev socket,id=chrtpm,path=${tpmSocketPath}" + "-tpmdev emulator,id=tpm_dev_0,chardev=chrtpm" + "-device ${tpmDeviceModels.${system}},tpmdev=tpm_dev_0" + ]; + + inherit useSecureBoot; + }; + + boot.initrd.availableKernelModules = lib.mkIf useTPM2 [ "tpm_tis" ]; + + boot.loader.efi = { + canTouchEfiVariables = true; + }; + boot.lanzaboote = { + enable = true; + enrollKeys = lib.mkDefault true; + pkiBundle = ./fixtures/uefi-keys; + }; + }; + }; + }; + +} diff --git a/nix/tests/remote-signing.nix b/nix/tests/remote-signing.nix new file mode 100644 index 00000000..df765590 --- /dev/null +++ b/nix/tests/remote-signing.nix @@ -0,0 +1,80 @@ +{ pkgs, lanzasigndModule, lanzaLib }: +let + inherit (lanzaLib) mkSecureBootTest; + inherit (pkgs) lib; + mkRemoteSigningTest = { name, machine ? { }, useSecureBoot ? true, useTPM2 ? false, testScript }: + mkSecureBootTest { + inherit name useSecureBoot useTPM2; + testScript = { nodes, ... }: + let + remoteClientSystem = "${nodes.machine.system.build.toplevel}/specialisation/remote"; + in + '' + server.start() + machine.start(allow_reboot=True) + server.wait_for_unit("lanzasignd.service") + server.wait_for_open_port(9999) + # Perform a switch to the remote configuration + # and contact the server to get the right bootables. + with subtest("Activation will request for remote signing"): + machine.fail("hello") + machine.succeed( + "${remoteClientSystem}/bin/switch-to-configuration boot >&2" + ) + with subtest("Reboot into remote signed generation is successful"): + machine.succeed("bootctl set-default nixos-generation-1-specialisation-remote-\*.efi") + machine.reboot() + machine.wait_for_unit("multi-user.target") + machine.succeed("hello") + ${testScript} + ''; + machine = { + imports = [ + machine + ]; + + specialisation.remote.configuration = { + boot.lanzaboote = { + # We disable explicitly local signing because `mkSecureBootTest` will set + # `pkiBundle` which will set local signing to true by default. + localSigning.enable = lib.mkForce false; + # Keys were already enrolled by the local setup. + enrollKeys = lib.mkForce false; + remoteSigning = { + enable = true; + serverUrl = "http://server:9999"; + }; + }; + environment.systemPackages = [ pkgs.hello ]; + }; + }; + extraNodes.server = { nodes, ... }: { + imports = [ + lanzasigndModule + ]; + + services.lanzasignd = { + enable = true; + pkiBundle = ./fixtures/uefi-keys; + openFirewall = true; + }; + + system.extraDependencies = [ + # Trust `machine` store paths! + nodes.machine.system.build.toplevel + ]; + }; + }; +in +{ + remote-signing-basic = mkRemoteSigningTest { + name = "remote-signing-basic"; + testScript = '' + assert "Secure Boot: enabled (user)" in machine.succeed("bootctl status") + ''; + }; + + # TODO: attack the signing server + # send a fake store path + # send ... +} diff --git a/rust/signd/Cargo.lock b/rust/signd/Cargo.lock new file mode 100644 index 00000000..b4177cee --- /dev/null +++ b/rust/signd/Cargo.lock @@ -0,0 +1,1417 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" + +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bootspec" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa369be5fcb7fb0b6f86427ae4446f724238bdcd8ec6db9e0d9ac041cde94fae" +dependencies = [ + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "brotli" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "buf_redux" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +dependencies = [ + "memchr", + "safemem", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.0", +] + +[[package]] +name = "chunked_transfer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" + +[[package]] +name = "clap" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deflate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" +dependencies = [ + "adler32", + "gzip-header", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.52.0", +] + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "goblin" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27c1b4369c2cd341b5de549380158b105a04c331be5db9110eef7b6d2742134" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "gzip-header" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2" +dependencies = [ + "crc32fast", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "iana-time-zone" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indoc" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "js-sys" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lanzaboote_signd" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "indoc", + "lanzaboote_tool", + "log", + "nix", + "rouille", + "serde", + "serde_json", + "stderrlog", + "tempfile", + "thiserror", + "ureq", +] + +[[package]] +name = "lanzaboote_tool" +version = "0.3.0" +dependencies = [ + "anyhow", + "bootspec", + "fastrand", + "goblin", + "indoc", + "log", + "serde", + "serde_json", + "sha2", + "stderrlog", + "tempfile", + "time", + "ureq", + "url", + "walkdir", +] + +[[package]] +name = "libc" +version = "0.2.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "multipart" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182" +dependencies = [ + "buf_redux", + "httparse", + "log", + "mime", + "mime_guess", + "quick-error", + "rand", + "safemem", + "tempfile", + "twoway", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.4", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.48.0", +] + +[[package]] +name = "rouille" +version = "3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3716fbf57fc1084d7a706adf4e445298d123e4a44294c4e8213caf1b85fcc921" +dependencies = [ + "base64 0.13.1", + "brotli", + "chrono", + "deflate", + "filetime", + "multipart", + "percent-encoding", + "rand", + "serde", + "serde_derive", + "serde_json", + "sha1_smol", + "threadpool", + "time", + "tiny_http", + "url", +] + +[[package]] +name = "rustix" +version = "0.38.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scroll" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "serde" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stderrlog" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69a26bbf6de627d389164afa9783739b56746c6c72c4ed16539f4ff54170327b" +dependencies = [ + "atty", + "chrono", + "log", + "termcolor", + "thread_local", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +dependencies = [ + "deranged", + "libc", + "num_threads", + "powerfmt", + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "tiny_http" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" +dependencies = [ + "ascii", + "chunked_transfer", + "httpdate", + "log", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "twoway" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" +dependencies = [ + "memchr", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" +dependencies = [ + "base64 0.21.7", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-webpki", + "serde", + "serde_json", + "url", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" + +[[package]] +name = "webpki-roots" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/rust/signd/Cargo.toml b/rust/signd/Cargo.toml new file mode 100644 index 00000000..2fa64bbf --- /dev/null +++ b/rust/signd/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "lanzaboote_signd" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.71" +stderrlog = "0.5.4" +log = { version = "0.4.18", features = ["std"] } +clap = { version = "4.3.1", features = ["derive"] } +lanzaboote_tool = { path = "../tool/shared" } +indoc = "2.0.1" +tempfile = "3.5.0" +nix = { version = "0.26.2", default-features = false, features = [ "fs" ] } +rouille = "3.6.2" +serde = { version = "1.0.163", features = ["derive"] } +serde_json = "1.0.107" +thiserror = "1.0.49" + +[dev-dependencies] +ureq = "2.9.1" diff --git a/rust/signd/README.md b/rust/signd/README.md new file mode 100644 index 00000000..25b5ae86 --- /dev/null +++ b/rust/signd/README.md @@ -0,0 +1,43 @@ +# Lanzaboote Remote Signature Server (`lanzasignd`) + +`lanzasignd` is a model of how to offer a remote signature server which +will serve the only two things of importance: + +- the lanzaboote stub +- a potential first stage bootloader, e.g. systemd-boot + +Instead of sending any binary on the wire which is wasteful, we can exploit the Nix store model here +and send store paths that are expected to be realizable on the signing server. + +Furthermore, this serves as a good enough way to protect the user against sending tampered stubs. + +## Theory of operations + +`lanzasignd` is expected to run as a hardened daemon with a potential access to the private key material +or an already authenticated socket to perform signing operations. + +No authentication or authorization is built-in as this is out of scope, it is recommended to run the daemon +behind a reverse proxy with authentication or authorization or in a trusted network via a VPN. + +No rate-limit is applied to protect against denial of service, PRs are welcome to figure out a reasonable solution on that, +otherwise rate-limits can be applied at the system level. + +## Endpoints + +- `POST /sign/stub`: assembles a signed stub based on the stub parameters sent, 200 OK with a signed binary as body, 400 with a plaintext error if failed. +- `POST /sign/store-path`: assembles a signed binary based on the store path sent, 200 OK with a signed binary as body, 400 with a plaintext error if failed. +- `GET /verify`: verify that the binary sent is signed according to the current keyring, returns a JSON `{ signed: bool, valid_according_to_secureboot_policy: bool }`, a signed binary can be invalid for the current Secure Boot policy, the two attributes represents this fact. + +## Operating it + +`lanzasignd` has hard requirements on possessing a Nix store. + +```nix + services.lanzasignd = { + enable = true; + port = 9999; + settings = { + kernel-cmdline-allowed = [ "..." ]; + }; + }; +``` diff --git a/rust/signd/src/handlers.rs b/rust/signd/src/handlers.rs new file mode 100644 index 00000000..ebc71378 --- /dev/null +++ b/rust/signd/src/handlers.rs @@ -0,0 +1,84 @@ +use std::{io::Read, path::PathBuf}; + +use lanzaboote_tool::{ + pe::{lanzaboote_image, StubParameters}, + signature::{remote::VerificationResponse, LanzabooteSigner}, +}; +use log::{debug, trace, warn}; +use rouille::{try_or_400, Request, Response}; +use thiserror::Error; + +use crate::policy::Policy; + +#[derive(Error, Debug)] +pub enum ErrorKind { + #[error("body was already opened in request")] + BodyAlreadyOpened, +} + +pub struct Handlers { + policy: P, + signer: S, +} + +impl Handlers { + pub fn new(signer: S, policy: P) -> Self { + Self { signer, policy } + } + + pub fn sign_stub(&self, req: &Request) -> Response { + debug!("Signing stub request"); + let stub_parameters: StubParameters = try_or_400!(rouille::input::json_input(req)); + trace!("Stub parameters: {:#?}", stub_parameters); + + // Validate the stub according to the policy + if !self.policy.trusted_stub_parameters(&stub_parameters) { + warn!("Untrusted stub parameters"); + return Response::empty_400(); + } + + let working_tree = tempfile::tempdir().expect("Failed to create a directory"); + + // Assemble the stub + let image = + lanzaboote_image(&working_tree, &stub_parameters).expect("Failed to build the stub"); + + // Sign the stub now + let image_to = image.with_extension(".signed"); + self.signer.sign_and_copy(&image, &image_to).unwrap(); + + Response::from_data( + "application/octet-stream", + std::fs::read(image_to).expect("Failed to read the stub"), + ) + } + + pub fn sign_store_path(&self, req: &Request) -> Response { + debug!("Signing store path request"); + let store_path: PathBuf = PathBuf::from(try_or_400!(rouille::input::plain_text_body(req))); + debug!("Request for {}", store_path.display()); + + if !self.policy.trusted_store_path(&store_path) { + warn!("Untrusted store path: {}", store_path.display()); + Response::empty_400() + } else { + Response::from_data( + "application/octet-stream", + self.signer.sign_store_path(&store_path).unwrap(), + ) + } + } + + pub fn verify(&self, req: &Request) -> Response { + let mut data = try_or_400!(req.data().ok_or(ErrorKind::BodyAlreadyOpened)); + let mut buf = Vec::new(); + try_or_400!(data.read_to_end(&mut buf)); + + let signed_according_to_signer = self.signer.verify(buf.as_slice()).unwrap(); + + Response::json(&VerificationResponse { + signed: signed_according_to_signer, + valid_according_secureboot_policy: signed_according_to_signer, + }) + } +} diff --git a/rust/signd/src/lib.rs b/rust/signd/src/lib.rs new file mode 100644 index 00000000..db3fd381 --- /dev/null +++ b/rust/signd/src/lib.rs @@ -0,0 +1,29 @@ +use lanzaboote_tool::signature::LanzabooteSigner; +use log::trace; +use policy::Policy; +use rouille::{router, Request, Response}; + +pub mod handlers; +pub mod policy; + +pub fn route( + handlers: handlers::Handlers, +) -> impl Fn(&Request) -> Response { + move |request| { + trace!("Receiving {:#?}", request); + router!(request, + (POST) (/sign/stub) => { + handlers.sign_stub(request) + }, + (POST) (/sign/store-path) => { + handlers.sign_store_path(request) + }, + (POST) (/verify) => { + handlers.verify(request) + }, + _ => { + Response::text("lanzasignd signature endpoint") + } + ) + } +} diff --git a/rust/signd/src/main.rs b/rust/signd/src/main.rs new file mode 100644 index 00000000..f07a56b1 --- /dev/null +++ b/rust/signd/src/main.rs @@ -0,0 +1,83 @@ +use std::path::PathBuf; + +use anyhow::Result; +use clap::{Parser, Subcommand}; +use lanzaboote_signd::handlers::Handlers; +use lanzaboote_signd::policy::TrivialPolicy; +use lanzaboote_signd::route; +use lanzaboote_tool::signature::local::LocalKeyPair; +use log::info; + +#[derive(Parser)] +struct Cli { + /// Verbose mode (-v, -vv, etc.) + #[arg(short, long, action = clap::ArgAction::Count)] + verbose: u8, + #[clap(subcommand)] + commands: Commands, +} + +#[derive(Subcommand)] +enum Commands { + Serve(ServeCommand), +} + +#[derive(Parser)] +struct ServeCommand { + /// Port for the service + #[arg(long)] + port: u16, + + /// Policy file settings + #[arg(long)] + policy_file: PathBuf, + + /// sbsign Public Key + #[arg(long)] + public_key: PathBuf, + + /// sbsign Private Key + #[arg(long)] + private_key: PathBuf, +} + +/// The default log level. +/// +/// 2 corresponds to the level INFO. +const DEFAULT_LOG_LEVEL: usize = 2; + +impl Cli { + pub fn call(self, module: &str) { + stderrlog::new() + .module(module) + .show_level(false) + .verbosity(DEFAULT_LOG_LEVEL + usize::from(self.verbose)) + .init() + .expect("Failed to setup logger."); + + if let Err(e) = self.commands.call() { + log::error!("{e:#}"); + std::process::exit(1); + }; + } +} + +impl Commands { + pub fn call(self) -> Result<()> { + match self { + Commands::Serve(args) => serve(args), + } + } +} + +fn serve(args: ServeCommand) -> Result<()> { + let keypair = LocalKeyPair::new(&args.public_key, &args.private_key); + let policy: TrivialPolicy = serde_json::from_slice(&std::fs::read(args.policy_file)?)?; + let handlers = Handlers::new(keypair, policy); + info!("Listening on 0.0.0.0:{}", args.port); + rouille::start_server(format!("0.0.0.0:{}", args.port), route(handlers)); +} + +fn main() { + Cli::parse().call(module_path!()) +} diff --git a/rust/signd/src/policy.rs b/rust/signd/src/policy.rs new file mode 100644 index 00000000..71ab4364 --- /dev/null +++ b/rust/signd/src/policy.rs @@ -0,0 +1,114 @@ +use std::{ + collections::HashSet, + path::{Path, PathBuf}, +}; + +use lanzaboote_tool::pe::StubParameters; +use log::trace; +use serde::{Deserialize, Serialize}; + +pub trait Policy { + /// Validate if this store path is trusted for signature. + fn trusted_store_path(&self, store_path: &Path) -> bool; + /// Validate if these stub parameters are trusted for signature. + fn trusted_stub_parameters(&self, parameters: &StubParameters) -> bool; +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TrivialPolicy { + pub allowed_kernel_cmdline_items: Option>, + pub store_location: PathBuf, +} + +impl Policy for TrivialPolicy { + /// For now, we will only assume it does exist in our local store. + /// This scenario makes sense if you deploy all your closures via this local machine's store, + /// e.g. a big builder, NFS nix store, etc. + fn trusted_store_path(&self, store_path: &Path) -> bool { + trace!( + "trusted store path {} → {}", + store_path.display(), + store_path.exists() + ); + store_path.starts_with(&self.store_location) && store_path.exists() + } + + fn trusted_stub_parameters(&self, parameters: &StubParameters) -> bool { + if !self.trusted_store_path(¶meters.lanzaboote_store_path) + || !self.trusted_store_path(¶meters.kernel_store_path) + || !self.trusted_store_path(¶meters.initrd_store_path) + { + return false; + } + + if let Some(allowed_cmdline_items) = &self.allowed_kernel_cmdline_items { + for item in ¶meters.kernel_cmdline { + if !allowed_cmdline_items.contains(item) { + trace!("untrusted command line item: {item}"); + return false; + } + } + } + + // XXX: validate os_release_contents + // parse then check if it contains allowed stuff? + + // kernel/initrd paths doesn't need to be validated per se. + // let's assume they are manipulated, let be K the kernel path in ESP. + // if the stub loads K, we will validate that hash(K) = hash in the stub. + // because of how the stub works, if hash(K) = hash in the stub and the hash function + // is strong enough, we know that K's contents = the kernel's contents we expected. + // Therefore, integrity is ensured. + // The only concern is that user could overwrite his bootables with the wrong K. + // Is that a concern for this signing server? Not really. + + true + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use lanzaboote_tool::pe::StubParameters; + + use super::{Policy, TrivialPolicy}; + + #[test] + fn test_reject_non_store_path() { + let policy = TrivialPolicy { + allowed_kernel_cmdline_items: None, + }; + + assert!(!policy.trusted_stub_parameters( + &StubParameters::new( + Path::new("/not/a/store/stub"), + Path::new("/not/a/store/kernel"), + Path::new("/not/a/store/initrd"), + Path::new("/efi/kernel"), + Path::new("/efi/initrd"), + Path::new("/efi"), + ) + .expect("Failed to obtain fake stub parameters"), + )); + } + + #[test] + fn test_reject_non_existent_store_path() { + let policy = TrivialPolicy { + allowed_kernel_cmdline_items: None, + }; + + assert!(!policy.trusted_stub_parameters( + &StubParameters::new( + Path::new("/nix/store/stub"), + Path::new("/nix/store/kernel"), + Path::new("/nix/store/initrd"), + Path::new("/efi/kernel"), + Path::new("/efi/initrd"), + Path::new("/efi"), + ) + .expect("Failed to obtain fake stub parameters"), + )); + } +} diff --git a/rust/signd/tests/basic_integration.rs b/rust/signd/tests/basic_integration.rs new file mode 100644 index 00000000..f25103f3 --- /dev/null +++ b/rust/signd/tests/basic_integration.rs @@ -0,0 +1,10 @@ +use lanzaboote_tool::pe::StubParameters; + +mod common; + +#[test] +fn test_sign_and_verify() { + let (server, remote_signer) = common::setup(); + let stub_parameters = common::setup_toplevel(tmpdir()); + remote_signer.build_and_sign_stub(stub_parameters); +} diff --git a/rust/signd/tests/common/mod.rs b/rust/signd/tests/common/mod.rs new file mode 100644 index 00000000..af44f153 --- /dev/null +++ b/rust/signd/tests/common/mod.rs @@ -0,0 +1,97 @@ +use std::{fs, path::Path}; + +use lanzaboote_signd::{handlers::Handlers, policy::Policy, route}; +use lanzaboote_tool::{ + architecture::Architecture, + pe::StubParameters, + signature::{local::LocalKeyPair, remote::RemoteSigningServer}, +}; +use rouille::{Request, Response}; + +/// Returns the host platform system +/// in the system double format for +/// our usual targets. +#[cfg(target_arch = "aarch64")] +pub static SYSTEM: &str = "aarch64-linux"; + +// We do not actually care much about 32 bit. However we can use this to easily test that lzbt +// works with another architecture. +#[cfg(target_arch = "x86")] +pub static SYSTEM: &str = "i686-linux"; + +#[cfg(target_arch = "x86_64")] +pub static SYSTEM: &str = "x86_64-linux"; + +/// An policy that should never ever be used in production. +pub struct AbsolutelyInsecurePolicy; + +impl Policy for AbsolutelyInsecurePolicy { + fn trusted_store_path(&self, store_path: &std::path::Path) -> bool { + true + } + + fn trusted_stub_parameters(&self, parameters: &lanzaboote_tool::pe::StubParameters) -> bool { + true + } +} + +pub fn setup_keypair() -> LocalKeyPair { + LocalKeyPair::new( + Path::new("../../tool/tests/fixtures/uefi-keys/db.pem"), + Path::new("../../tool/tests/fixtures/uefi-keys/db.key"), + ) +} + +pub fn setup() -> ( + rouille::Server Response>, + RemoteSigningServer, +) { + let keypair = setup_keypair(); + + let handlers = Handlers::new(keypair, AbsolutelyInsecurePolicy); + let server = rouille::Server::new("localhost:0", route(handlers)) + .expect("Failed to start the HTTP server"); + let server_url = format!("http://localhost:{}", server.server_addr().port()); + let remote_signer = RemoteSigningServer::new(&server_url, "rustc/integration testing") + .expect("Failed to build the remote signer"); + + (server, remote_signer) +} + +/// Stolen from `tool` setup. +/// Setup a mock toplevel inside a temporary directory. +/// +/// Accepts the temporary directory as a parameter so that the invoking function retains control of +/// it (and when it goes out of scope). +pub fn setup_toplevel(tmpdir: &Path) -> std::io::Result { + let system = Architecture::from_nixos_system(SYSTEM)?; + + // Generate a random toplevel name so that multiple toplevel paths can live alongside each + // other in the same directory. + let toplevel = tmpdir.join(format!("toplevel-{}", random_string(8))); + let fake_store_path = toplevel.join("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee-6.1.1"); + fs::create_dir_all(&fake_store_path)?; + + let test_systemd = systemd_location_from_env()?; + let systemd_stub_filename = system.systemd_stub_filename(); + let test_systemd_stub = format!( + "{test_systemd}/lib/systemd/boot/efi/{systemd_stub_filename}", + systemd_stub_filename = systemd_stub_filename.display() + ); + + let initrd_path = fake_store_path.join("initrd"); + let kernel_path = fake_store_path.join("kernel"); + let nixos_version_path = toplevel.join("nixos-version"); + let kernel_modules_path = toplevel.join("kernel-modules/lib/modules/6.1.1"); + + // To simplify the test setup, we use the systemd stub for all PE binaries used by lanzatool. + // Lanzatool doesn't care whether its actually a kernel or initrd but only whether it can + // manipulate the PE binary with objcopy and/or sign it with sbsigntool. For testing lanzatool + // in isolation this should suffice. + fs::copy(&test_systemd_stub, initrd_path)?; + fs::copy(&test_systemd_stub, kernel_path)?; + fs::write(nixos_version_path, b"23.05")?; + fs::create_dir_all(kernel_modules_path)?; + + Ok(toplevel) +} diff --git a/rust/tool/Cargo.lock b/rust/tool/Cargo.lock index b880f5dc..86e57e87 100644 --- a/rust/tool/Cargo.lock +++ b/rust/tool/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -52,7 +58,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -62,7 +68,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -109,6 +115,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396664016f30ad5ab761000391a5c0b436f7bfac738858263eb25897658b98c9" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "bitflags" version = "1.3.2" @@ -248,6 +260,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -302,7 +323,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -330,7 +351,26 @@ dependencies = [ "cfg-if", "libc", "redox_syscall", - "windows-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", ] [[package]] @@ -403,6 +443,16 @@ dependencies = [ "cc", ] +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indoc" version = "2.0.4" @@ -429,14 +479,23 @@ name = "lanzaboote_tool" version = "0.3.0" dependencies = [ "anyhow", + "assert_cmd", "bootspec", + "expect-test", "fastrand", + "filetime", "goblin", + "indoc", "log", + "rand", + "serde", "serde_json", "sha2", + "stderrlog", "tempfile", "time", + "ureq", + "url", "walkdir", ] @@ -487,6 +546,15 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + [[package]] name = "nix" version = "0.27.1" @@ -513,6 +581,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + [[package]] name = "plain" version = "0.2.3" @@ -621,6 +695,20 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.48.0", +] + [[package]] name = "rustix" version = "0.38.30" @@ -631,7 +719,29 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", ] [[package]] @@ -669,6 +779,16 @@ dependencies = [ "syn", ] +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "serde" version = "1.0.195" @@ -711,6 +831,12 @@ dependencies = [ "digest", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "stderrlog" version = "0.5.4" @@ -751,7 +877,7 @@ dependencies = [ "fastrand", "redox_syscall", "rustix", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -817,18 +943,83 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-webpki", + "serde", + "serde_json", + "url", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "utf8parse" version = "0.2.1" @@ -920,6 +1111,12 @@ version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" +[[package]] +name = "webpki-roots" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" + [[package]] name = "winapi" version = "0.3.9" @@ -960,6 +1157,15 @@ dependencies = [ "windows-targets 0.52.0", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/rust/tool/shared/Cargo.toml b/rust/tool/shared/Cargo.toml index 9d25e381..ac0aa287 100644 --- a/rust/tool/shared/Cargo.toml +++ b/rust/tool/shared/Cargo.toml @@ -18,3 +18,18 @@ sha2 = "0.10" # different versions. fastrand = "2.0.1" log = { version = "0.4", features = ["std"] } +serde = { version = "1.0.194", features = ["derive"] } +stderrlog = "0.5" +indoc = "2" +ureq = { version = "2.7.1", features = [ "json" ], optional = true } +url = { version = "2.4.1", optional = true } + +[features] +default = [ "remote_signature" ] +remote_signature = [ "ureq", "url" ] + +[dev-dependencies] +assert_cmd = "2.0.12" +expect-test = "1.4.1" +filetime = "0.2.21" +rand = "0.8.5" diff --git a/rust/tool/shared/src/pe.rs b/rust/tool/shared/src/pe.rs index 3333c61f..560c33e6 100644 --- a/rust/tool/shared/src/pe.rs +++ b/rust/tool/shared/src/pe.rs @@ -6,39 +6,114 @@ use std::process::Command; use anyhow::{Context, Result}; use goblin::pe::PE; +use serde::{Deserialize, Serialize}; use tempfile::TempDir; use crate::utils::{file_hash, tmpname, SecureTempDirExt}; +#[derive(Debug, Serialize, Deserialize)] +pub struct StubParameters { + pub lanzaboote_store_path: PathBuf, + pub kernel_cmdline: Vec, + pub os_release_contents: Vec, + pub kernel_store_path: PathBuf, + pub initrd_store_path: PathBuf, + /// Kernel path rooted at the ESP + /// i.e. if you refer to /boot/efi/EFI/NixOS/kernel.efi + /// this gets turned into \\EFI\\NixOS\\kernel.efi as a UTF-16 string + /// at assembling time. + pub kernel_path_at_esp: String, + /// Same as kernel. + pub initrd_path_at_esp: String, +} + +impl StubParameters { + pub fn new( + lanzaboote_stub: &Path, + kernel_path: &Path, + initrd_path: &Path, + kernel_target: &Path, + initrd_target: &Path, + esp: &Path, + ) -> Result { + // Resolve maximally those paths + // We won't verify they are store paths, otherwise the mocking strategy will fail for our + // unit tests. + + Ok(Self { + lanzaboote_store_path: lanzaboote_stub.to_path_buf(), + kernel_store_path: kernel_path.to_path_buf(), + initrd_store_path: initrd_path.to_path_buf(), + kernel_path_at_esp: esp_relative_uefi_path(esp, kernel_target)?, + initrd_path_at_esp: esp_relative_uefi_path(esp, initrd_target)?, + kernel_cmdline: Vec::new(), + os_release_contents: Vec::new(), + }) + } + + pub fn with_os_release_contents(mut self, os_release_contents: &[u8]) -> Self { + self.os_release_contents = os_release_contents.to_vec(); + self + } + + pub fn with_cmdline(mut self, cmdline: &[String]) -> Self { + self.kernel_cmdline = cmdline.to_vec(); + self + } + + pub fn all_signables_in_store(&self) -> bool { + self.lanzaboote_store_path.starts_with("/nix/store") + && self.kernel_store_path.starts_with("/nix/store") + && self.initrd_store_path.starts_with("/nix/store") + } +} + +/// Performs the evil operation +/// of calling the appender script to append +/// initrd "secrets" (not really) to the initrd. +pub fn append_initrd_secrets( + append_initrd_secrets_path: &Path, + initrd_path: &PathBuf, + generation_version: u64, +) -> Result<()> { + let status = Command::new(append_initrd_secrets_path) + .args(vec![initrd_path]) + .status() + .context("Failed to append initrd secrets")?; + if !status.success() { + return Err(anyhow::anyhow!( + "Failed to append initrd secrets for generation {} with args `{:?}`", + generation_version, + vec![append_initrd_secrets_path, initrd_path] + )); + } + + Ok(()) +} + /// Assemble a lanzaboote image. -#[allow(clippy::too_many_arguments)] pub fn lanzaboote_image( // Because the returned path of this function is inside the tempdir as well, the tempdir must // live longer than the function. This is why it cannot be created inside the function. tempdir: &TempDir, - lanzaboote_stub: &Path, - os_release: &Path, - kernel_cmdline: &[String], - kernel_source: &Path, - kernel_target: &Path, - initrd_source: &Path, - initrd_target: &Path, - esp: &Path, + stub_parameters: &StubParameters, ) -> Result { // objcopy can only copy files into the PE binary. That's why we // have to write the contents of some bootspec properties to disk. - let kernel_cmdline_file = tempdir.write_secure_file(kernel_cmdline.join(" "))?; + let kernel_cmdline_file = + tempdir.write_secure_file(stub_parameters.kernel_cmdline.join(" "))?; - let kernel_path_file = - tempdir.write_secure_file(esp_relative_uefi_path(esp, kernel_target)?)?; - let kernel_hash_file = tempdir.write_secure_file(file_hash(kernel_source)?.as_slice())?; + let kernel_path_file = tempdir.write_secure_file(&stub_parameters.kernel_path_at_esp)?; + let kernel_hash_file = + tempdir.write_secure_file(file_hash(&stub_parameters.kernel_store_path)?.as_slice())?; - let initrd_path_file = - tempdir.write_secure_file(esp_relative_uefi_path(esp, initrd_target)?)?; - let initrd_hash_file = tempdir.write_secure_file(file_hash(initrd_source)?.as_slice())?; + let initrd_path_file = tempdir.write_secure_file(&stub_parameters.initrd_path_at_esp)?; + let initrd_hash_file = + tempdir.write_secure_file(file_hash(&stub_parameters.initrd_store_path)?.as_slice())?; - let os_release_offs = stub_offset(lanzaboote_stub)?; - let kernel_cmdline_offs = os_release_offs + file_size(os_release)?; + let os_release = tempdir.write_secure_file(&stub_parameters.os_release_contents)?; + let os_release_offs = stub_offset(&stub_parameters.lanzaboote_store_path)?; + let kernel_cmdline_offs = os_release_offs + file_size(&os_release)?; let initrd_path_offs = kernel_cmdline_offs + file_size(&kernel_cmdline_file)?; let kernel_path_offs = initrd_path_offs + file_size(&initrd_path_file)?; let initrd_hash_offs = kernel_path_offs + file_size(&kernel_path_file)?; @@ -54,7 +129,11 @@ pub fn lanzaboote_image( ]; let image_path = tempdir.path().join(tmpname()); - wrap_in_pe(lanzaboote_stub, sections, &image_path)?; + wrap_in_pe( + &stub_parameters.lanzaboote_store_path, + sections, + &image_path, + )?; Ok(image_path) } diff --git a/rust/tool/shared/src/signature.rs b/rust/tool/shared/src/signature.rs deleted file mode 100644 index b78dfbac..00000000 --- a/rust/tool/shared/src/signature.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::ffi::OsString; -use std::io::Write; -use std::path::{Path, PathBuf}; -use std::process::Command; - -use anyhow::{Context, Result}; - -pub struct KeyPair { - pub private_key: PathBuf, - pub public_key: PathBuf, -} - -impl KeyPair { - pub fn new(public_key: &Path, private_key: &Path) -> Self { - Self { - public_key: public_key.into(), - private_key: private_key.into(), - } - } - - pub fn sign_and_copy(&self, from: &Path, to: &Path) -> Result<()> { - let args: Vec = vec![ - OsString::from("--key"), - self.private_key.clone().into(), - OsString::from("--cert"), - self.public_key.clone().into(), - from.as_os_str().to_owned(), - OsString::from("--output"), - to.as_os_str().to_owned(), - ]; - - let output = Command::new("sbsign") - .args(&args) - .output() - .context("Failed to run sbsign. Most likely, the binary is not on PATH.")?; - - if !output.status.success() { - std::io::stderr() - .write_all(&output.stderr) - .context("Failed to write output of sbsign to stderr.")?; - log::debug!("sbsign failed with args: `{args:?}`."); - return Err(anyhow::anyhow!("Failed to sign {to:?}.")); - } - - Ok(()) - } - - /// Verify the signature of a PE binary. Return true if the signature was verified. - pub fn verify(&self, path: &Path) -> bool { - let args: Vec = vec![ - OsString::from("--cert"), - self.public_key.clone().into(), - path.as_os_str().to_owned(), - ]; - - let output = Command::new("sbverify") - .args(&args) - .output() - .expect("Failed to run sbverify. Most likely, the binary is not on PATH."); - - if !output.status.success() { - if std::io::stderr().write_all(&output.stderr).is_err() { - return false; - }; - log::debug!("sbverify failed with args: `{args:?}`."); - return false; - } - true - } -} diff --git a/rust/tool/shared/src/signature/README.md b/rust/tool/shared/src/signature/README.md new file mode 100644 index 00000000..84067c65 --- /dev/null +++ b/rust/tool/shared/src/signature/README.md @@ -0,0 +1,12 @@ +# Signatures capabilities of Lanzaboote + +Currently, lanzaboote can perform signatures of PE binaries based on local keypairs present on disk. + +## Local keypairs + +Storing your signature keys in the disk in some location we can read is the most trivial signature capability +we can offer. + +You are responsible for securing them and ensuring they are not accessible by an attacker. + +Signature happens via `sbsign` which will copy your input inside a temporary directory, sign it, read it and offers it to you again. diff --git a/rust/tool/shared/src/signature/local.rs b/rust/tool/shared/src/signature/local.rs new file mode 100644 index 00000000..bdf2a067 --- /dev/null +++ b/rust/tool/shared/src/signature/local.rs @@ -0,0 +1,113 @@ +use crate::pe::lanzaboote_image; +use crate::utils::SecureTempDirExt; +use std::ffi::OsString; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::{Context, Result}; +use tempfile::tempdir; + +use super::LanzabooteSigner; + +#[derive(Debug, Clone)] +pub struct LocalKeyPair { + pub private_key: PathBuf, + pub public_key: PathBuf, +} + +impl LocalKeyPair { + pub fn new(public_key: &Path, private_key: &Path) -> Self { + Self { + public_key: public_key.into(), + private_key: private_key.into(), + } + } +} + +impl LanzabooteSigner for LocalKeyPair { + fn get_public_key(&self) -> Result> { + Ok(std::fs::read(&self.public_key)?) + } + + fn can_sign_stub(&self, _stub: &crate::pe::StubParameters) -> bool { + true + } + + fn sign_and_copy(&self, from: &Path, to: &Path) -> Result<()> { + let args: Vec = vec![ + OsString::from("--key"), + self.private_key.clone().into(), + OsString::from("--cert"), + self.public_key.clone().into(), + from.as_os_str().to_owned(), + OsString::from("--output"), + to.as_os_str().to_owned(), + ]; + + let output = Command::new("sbsign") + .args(&args) + .output() + .context("Failed to run sbsign. Most likely, the binary is not on PATH.")?; + + if !output.status.success() { + std::io::stderr() + .write_all(&output.stderr) + .context("Failed to write output of sbsign to stderr.")?; + log::debug!("sbsign failed with args: `{args:?}`."); + return Err(anyhow::anyhow!("Failed to sign {to:?}.")); + } + + Ok(()) + } + + fn sign_store_path(&self, store_path: &Path) -> Result> { + let working_tree = tempdir()?; + let to = &working_tree.path().join("signed.efi"); + self.sign_and_copy(store_path, to)?; + + Ok(std::fs::read(to)?) + } + + fn build_and_sign_stub(&self, stub: &crate::pe::StubParameters) -> Result> { + let working_tree = tempdir()?; + let lzbt_image_path = + lanzaboote_image(&working_tree, stub).context("Failed to build a lanzaboote image")?; + let to = working_tree.path().join("signed-stub.efi"); + self.sign_and_copy(&lzbt_image_path, &to)?; + + std::fs::read(&to).context("Failed to read a lanzaboote image") + } + + fn verify(&self, pe_binary: &[u8]) -> Result { + let working_tree = tempdir().context("Failed to get a temporary working tree")?; + let from = working_tree + .write_secure_file(pe_binary) + .context("Failed to write the PE binary in a secure file for verification")?; + + self.verify_path(&from) + } + + fn verify_path(&self, path: &Path) -> Result { + let args: Vec = vec![ + OsString::from("--cert"), + self.public_key.clone().into(), + path.as_os_str().to_owned(), + ]; + + let output = Command::new("sbverify") + .args(&args) + .output() + .context("Failed to run sbverify. Most likely, the binary is not on PATH.")?; + + if !output.status.success() { + if std::io::stderr().write_all(&output.stderr).is_err() { + return Ok(false); + }; + // XXX(Raito): do we want to bubble up this type of errors? :/ + log::debug!("sbverify failed with args: `{args:?}`."); + return Ok(false); + } + Ok(true) + } +} diff --git a/rust/tool/shared/src/signature/mod.rs b/rust/tool/shared/src/signature/mod.rs new file mode 100644 index 00000000..f39b8614 --- /dev/null +++ b/rust/tool/shared/src/signature/mod.rs @@ -0,0 +1,28 @@ +use anyhow::Result; +use std::path::Path; + +use crate::pe::StubParameters; + +pub mod local; +#[cfg(feature = "remote_signature")] +pub mod remote; + +pub trait LanzabooteSigner { + fn sign_store_path(&self, store_path: &Path) -> Result>; + fn can_sign_stub(&self, stub: &StubParameters) -> bool; + fn build_and_sign_stub(&self, stub: &StubParameters) -> Result>; + fn get_public_key(&self) -> Result>; + + fn sign_and_copy(&self, from: &Path, to: &Path) -> Result<()> { + Ok(std::fs::write(to, self.sign_store_path(from)?)?) + } + + /// Verify the signature of a PE binary, provided as bytes. + /// Return true if the signature was verified. + fn verify(&self, pe_binary: &[u8]) -> Result; + /// Verify the signature of a PE binary, provided by its path. + /// Return true if the signature was verified. + fn verify_path(&self, from: &Path) -> Result { + self.verify(&std::fs::read(from).expect("Failed to read the path to verify")) + } +} diff --git a/rust/tool/shared/src/signature/remote.rs b/rust/tool/shared/src/signature/remote.rs new file mode 100644 index 00000000..bc243ddd --- /dev/null +++ b/rust/tool/shared/src/signature/remote.rs @@ -0,0 +1,200 @@ +use std::time::Duration; + +use crate::pe::StubParameters; + +use super::LanzabooteSigner; +use anyhow::{bail, Context, Result}; +use serde::{Deserialize, Serialize}; +use ureq::{Agent, AgentBuilder}; +use url::Url; + +/// Remote signing server +/// +/// It will perform classical signature operations over HTTP +/// using the "Lanzaboote Remote Signing server" API. +/// +/// This API relies on the server exposing three endpoints: +/// +/// - `/sign/stub`: takes a StubParameter as input and reply with a signed stub +/// - `/sign/store-path`: takes a string store path as input and reply with the signed data +/// - `/verify`: takes PE binary as input and reply a `VerificationResponse` +/// +/// lanzasignd is an example of implementation. +pub struct RemoteSigningServer { + server_url: Url, + user_agent: String, + client: Agent, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct VerificationResponse { + /// If the binary has any signature attached + pub signed: bool, + /// If the binary is valid according to the Secure Boot policy + /// attached to this machine + /// This is not always a reliable piece of information + /// TODO: rework me. + pub valid_according_secureboot_policy: bool, +} + +impl RemoteSigningServer { + pub fn new(server_url: &str, user_agent: &str) -> Result { + let client = AgentBuilder::new() + .timeout_read(Duration::from_secs(5)) + .timeout_write(Duration::from_secs(5)) + .build(); + Ok(Self { + server_url: Url::parse(server_url) + .with_context(|| format!("Failed to parse {} as an URL", server_url))?, + user_agent: user_agent.to_string(), + client, + }) + } + + /// Asks for the remote server to send back a stub + /// assembled with the parameters provided. + /// + /// If the remote server agrees on providing that stub + /// It will return it signed. + fn request_signature(&self, stub_parameters: &StubParameters) -> Result> { + if !stub_parameters.all_signables_in_store() { + bail!("Signable stub parameters contains non-Nix store paths, the remote server cannot sign that!"); + } + + let response = self + .client + .post(self.server_url.join("/sign/stub")?.as_str()) + .set("User-Agent", &self.user_agent) + .send_json(stub_parameters) + .context("Failed to request signature")?; + + let len: Option = if response.has("Transfer-Encoding") + && response.header("Transfer-Encoding").unwrap() == "chunked" + { + None + } else { + Some( + response + .header("Content-Length") + .ok_or(anyhow::anyhow!( + "No content length in server response for stub signature" + ))? + .parse()?, + ) + }; + + let mut reader = response.into_reader(); + + let mut binary = match len { + Some(len) => Vec::with_capacity(len), + None => Vec::new(), + }; + + reader.read_to_end(&mut binary)?; + + Ok(binary) + } + + /// Asks for the remote server to sign an arbitrary + /// store path. + fn request_store_path_signature(&self, store_path: &str) -> Result> { + let response = self + .client + .post(self.server_url.join("/sign/store-path")?.as_str()) + .set("User-Agent", &self.user_agent) + .set("Content-Type", "text/plain; charset=utf8") + .send_string(store_path) + .context("Failed to request signature")?; + + let len: Option = if response.has("Transfer-Encoding") + && response.header("Transfer-Encoding").unwrap() == "chunked" + { + None + } else { + Some( + response + .header("Content-Length") + .ok_or(anyhow::anyhow!( + "No content length in server response for stub signature" + ))? + .parse()?, + ) + }; + + let mut reader = response.into_reader(); + + let mut binary = match len { + Some(len) => Vec::with_capacity(len), + None => Vec::new(), + }; + + reader.read_to_end(&mut binary)?; + + Ok(binary) + } +} + +impl LanzabooteSigner for RemoteSigningServer { + fn get_public_key(&self) -> Result> { + let response = self + .client + .get(self.server_url.join("/publickey")?.as_str()) + .set("User-Agent", &self.user_agent) + .set("Content-Type", "application/octet-stream") + .call() + .context("Failed to request public key")?; + + let len: Option = if response.has("Transfer-Encoding") + && response.header("Transfer-Encoding").unwrap() == "chunked" + { + None + } else { + Some( + response + .header("Content-Length") + .ok_or(anyhow::anyhow!( + "No content length in server response for stub signature" + ))? + .parse()?, + ) + }; + + let mut reader = response.into_reader(); + + let mut binary = match len { + Some(len) => Vec::with_capacity(len), + None => Vec::new(), + }; + + reader.read_to_end(&mut binary)?; + Ok(binary) + } + + fn can_sign_stub(&self, stub: &StubParameters) -> bool { + stub.all_signables_in_store() + } + + fn build_and_sign_stub(&self, stub: &StubParameters) -> Result> { + self.request_signature(stub) + } + fn sign_store_path(&self, store_path: &std::path::Path) -> Result> { + self.request_store_path_signature( + store_path.to_str().ok_or_else(|| { + anyhow::anyhow!("Failed to transform store path into valid UTF-8") + })?, + ) + } + + fn verify(&self, pe_binary: &[u8]) -> Result { + let resp: VerificationResponse = self + .client + .post(self.server_url.join("/verify")?.as_str()) + .set("User-Agent", &self.user_agent) + .set("Content-Type", "application/octet-stream") + .send_bytes(pe_binary) + .context("Failed to request verification")? + .into_json()?; + + Ok(resp.signed) + } +} diff --git a/rust/tool/systemd/src/cli.rs b/rust/tool/systemd/src/cli.rs index 90d91c03..be9ce2bb 100644 --- a/rust/tool/systemd/src/cli.rs +++ b/rust/tool/systemd/src/cli.rs @@ -1,16 +1,24 @@ use std::path::PathBuf; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use clap::{Parser, Subcommand}; use crate::install; -use lanzaboote_tool::architecture::Architecture; -use lanzaboote_tool::signature::KeyPair; +use lanzaboote_tool::{ + architecture::Architecture, + signature::{local::LocalKeyPair, remote::RemoteSigningServer, LanzabooteSigner}, +}; /// The default log level. /// /// 2 corresponds to the level INFO. const DEFAULT_LOG_LEVEL: usize = 2; +/// Lanzaboote user agent +pub static USER_AGENT: &str = concat!( + "lanzaboote tool (backend: systemd, version: ", + env!("CARGO_PKG_VERSION"), + ")" +); #[derive(Parser)] pub struct Cli { @@ -30,6 +38,7 @@ enum Commands { } #[derive(Parser)] +#[command(group = clap::ArgGroup::new("local-keys").multiple(true).requires_all(["private_key", "public_key"]).conflicts_with("remote_signing_server_url"))] struct InstallCommand { /// System for lanzaboote binaries, e.g. defines the EFI fallback path #[arg(long)] @@ -44,12 +53,16 @@ struct InstallCommand { systemd_boot_loader_config: PathBuf, /// sbsign Public Key - #[arg(long)] - public_key: PathBuf, + #[arg(long, group = "local-keys")] + public_key: Option, /// sbsign Private Key + #[arg(long, group = "local-keys")] + private_key: Option, + + /// Remote signing server #[arg(long)] - private_key: PathBuf, + remote_signing_server_url: Option, /// Configuration limit #[arg(long, default_value_t = 1)] @@ -87,21 +100,40 @@ impl Commands { } } -fn install(args: InstallCommand) -> Result<()> { +fn install_with_signer(args: InstallCommand, signer: S) -> Result<()> { let lanzaboote_stub = std::env::var("LANZABOOTE_STUB").context("Failed to read LANZABOOTE_STUB env variable")?; - let key_pair = KeyPair::new(&args.public_key, &args.private_key); - install::Installer::new( PathBuf::from(lanzaboote_stub), Architecture::from_nixos_system(&args.system)?, args.systemd, args.systemd_boot_loader_config, - key_pair, + signer, args.configuration_limit, args.esp, args.generations, ) .install() } + +fn install(args: InstallCommand) -> Result<()> { + // Many bail are impossible because of Clap ensuring they don't happen. + // For completeness, we provide them. + if let Some(public_key) = &args.public_key { + if let Some(private_key) = &args.private_key { + let signer = LocalKeyPair::new(public_key, private_key); + install_with_signer(args, signer) + } else { + bail!("Missing private key for local signature scheme!"); + } + } else if let Some(_private_key) = &args.private_key { + bail!("Missing public key for local signature scheme!"); + } else if let Some(remote_signing_server_url) = &args.remote_signing_server_url { + let signer = RemoteSigningServer::new(remote_signing_server_url, USER_AGENT) + .expect("Failed to create a remote signing server"); + install_with_signer(args, signer) + } else { + bail!("No mechanism for signature was provided, pass either a local pair of keys or a remote signing server"); + } +} diff --git a/rust/tool/systemd/src/install.rs b/rust/tool/systemd/src/install.rs index a40dca44..d58117af 100644 --- a/rust/tool/systemd/src/install.rs +++ b/rust/tool/systemd/src/install.rs @@ -4,7 +4,6 @@ use std::fs::{self, File}; use std::os::fd::AsRawFd; use std::os::unix::prelude::{OsStrExt, PermissionsExt}; use std::path::{Path, PathBuf}; -use std::process::Command; use std::string::ToString; use anyhow::{anyhow, Context, Result}; @@ -21,31 +20,31 @@ use lanzaboote_tool::esp::EspPaths; use lanzaboote_tool::gc::Roots; use lanzaboote_tool::generation::{Generation, GenerationLink}; use lanzaboote_tool::os_release::OsRelease; -use lanzaboote_tool::pe; -use lanzaboote_tool::signature::KeyPair; +use lanzaboote_tool::pe::{self, append_initrd_secrets}; +use lanzaboote_tool::signature::LanzabooteSigner; use lanzaboote_tool::utils::{file_hash, SecureTempDirExt}; -pub struct Installer { +pub struct Installer { broken_gens: BTreeSet, gc_roots: Roots, lanzaboote_stub: PathBuf, systemd: PathBuf, systemd_boot_loader_config: PathBuf, - key_pair: KeyPair, + signer: S, configuration_limit: usize, esp_paths: SystemdEspPaths, generation_links: Vec, arch: Architecture, } -impl Installer { - #[allow(clippy::too_many_arguments)] +#[allow(clippy::too_many_arguments)] +impl Installer { pub fn new( lanzaboote_stub: PathBuf, arch: Architecture, systemd: PathBuf, systemd_boot_loader_config: PathBuf, - key_pair: KeyPair, + signer: S, configuration_limit: usize, esp: PathBuf, generation_links: Vec, @@ -60,7 +59,7 @@ impl Installer { lanzaboote_stub, systemd, systemd_boot_loader_config, - key_pair, + signer, configuration_limit, esp_paths, generation_links, @@ -211,17 +210,27 @@ impl Installer { .context("Failed to install the kernel.")?; // Assemble and install the initrd, and record its path on the ESP. - let initrd_location = tempdir - .write_secure_file( - fs::read( - bootspec - .initrd - .as_ref() - .context("Lanzaboote does not support missing initrd yet.")?, + // It is not needed to write the initrd in a temporary directory + // if we do not have any initrd secret. + let initrd_location = if bootspec.initrd_secrets.is_some() { + tempdir + .write_secure_file( + fs::read( + bootspec + .initrd + .as_ref() + .context("Lanzaboote does not support missing initrd yet.")?, + ) + .context("Failed to read the initrd.")?, ) - .context("Failed to read the initrd.")?, - ) - .context("Failed to copy the initrd to the temporary directory.")?; + .context("Failed to copy the initrd to the temporary directory.")? + } else { + bootspec + .initrd + .clone() + .expect("Lanzaboote does not support missing initrd yet.") + }; + if let Some(initrd_secrets_script) = &bootspec.initrd_secrets { append_initrd_secrets(initrd_secrets_script, &initrd_location, generation.version)?; } @@ -232,29 +241,45 @@ impl Installer { // Assemble, sign and install the Lanzaboote stub. let os_release = OsRelease::from_generation(generation) .context("Failed to build OsRelease from generation.")?; - let os_release_path = tempdir - .write_secure_file(os_release.to_string().as_bytes()) - .context("Failed to write os-release file.")?; + + let os_release_contents = os_release.to_string(); + let kernel_cmdline = assemble_kernel_cmdline(&bootspec.init, bootspec.kernel_params.clone()); - let lanzaboote_image = pe::lanzaboote_image( - &tempdir, + + let parameters = pe::StubParameters::new( &self.lanzaboote_stub, - &os_release_path, - &kernel_cmdline, &bootspec.kernel, - &kernel_target, &initrd_location, + &kernel_target, &initrd_target, &self.esp_paths.esp, - ) - .context("Failed to assemble lanzaboote image.")?; + )? + .with_cmdline(&kernel_cmdline) + .with_os_release_contents(os_release_contents.as_bytes()); + + // TODO: how should we handle those cases? + if !self.signer.can_sign_stub(¶meters) { + log::warn!("Signer is not able to sign this stub, skipping..."); + return Ok(()); + } + + let lanzaboote_image = self + .signer + .build_and_sign_stub(¶meters) + .context("Failed to build and sign lanzaboote stub image.")?; + let lanzaboote_image_path = tempdir + .write_secure_file(lanzaboote_image) + .context("Failed to write securely the signed lanzaboote stub image.")?; + let stub_target = self .esp_paths .linux - .join(stub_name(generation, &self.key_pair.public_key)?); + .join(stub_name(generation, &self.signer)?); + self.gc_roots.extend([&stub_target]); - install_signed(&self.key_pair, &lanzaboote_image, &stub_target) + + install(&lanzaboote_image_path, &stub_target) .context("Failed to install the Lanzaboote stub.")?; Ok(()) @@ -267,7 +292,7 @@ impl Installer { let stub_target = self .esp_paths .linux - .join(stub_name(generation, &self.key_pair.public_key)?); + .join(stub_name(generation, &self.signer)?); let stub = fs::read(&stub_target)?; let kernel_path = resolve_efi_path( &self.esp_paths.esp, @@ -327,13 +352,13 @@ impl Installer { if newer_systemd_boot_available { log::info!("Updating {to:?}...") }; - let systemd_boot_is_signed = &self.key_pair.verify(to); + let systemd_boot_is_signed = &self.signer.verify_path(to)?; if !systemd_boot_is_signed { log::warn!("${to:?} is not signed. Replacing it with a signed binary...") }; if newer_systemd_boot_available || !systemd_boot_is_signed { - install_signed(&self.key_pair, from, to) + install_signed(&self.signer, from, to) .with_context(|| format!("Failed to install systemd-boot binary to: {to:?}"))?; } } @@ -361,15 +386,16 @@ fn resolve_efi_path(esp: &Path, efi_path: &[u8]) -> Result { /// Compute the file name to be used for the stub of a certain generation, signed with the given key. /// /// The generated name is input-addressed by the toplevel corresponding to the generation and the public part of the signing key. -fn stub_name(generation: &Generation, public_key: &Path) -> Result { +fn stub_name(generation: &Generation, signer: &S) -> Result { let bootspec = &generation.spec.bootspec.bootspec; + let public_key = signer.get_public_key()?; let stub_inputs = [ // Generation numbers can be reused if the latest generation was deleted. // To detect this, the stub path depends on the actual toplevel used. ("toplevel", bootspec.toplevel.0.as_os_str().as_bytes()), // If the key is rotated, the signed stubs must be re-generated. // So we make their path depend on the public key used for signature. - ("public_key", &fs::read(public_key)?), + ("public_key", &public_key), ]; let stub_input_hash = Base32Unpadded::encode_string(&Sha256::digest( serde_json::to_string(&stub_inputs).unwrap(), @@ -394,11 +420,11 @@ fn stub_name(generation: &Generation, public_key: &Path) -> Result { /// This is implemented as an atomic write. The file is first written to the destination with a /// `.tmp` suffix and then renamed to its final name. This is atomic, because a rename is an atomic /// operation on POSIX platforms. -fn install_signed(key_pair: &KeyPair, from: &Path, to: &Path) -> Result<()> { +fn install_signed(signer: &impl LanzabooteSigner, from: &Path, to: &Path) -> Result<()> { log::debug!("Signing and installing {to:?}..."); let to_tmp = to.with_extension(".tmp"); ensure_parent_dir(&to_tmp); - key_pair + signer .sign_and_copy(from, &to_tmp) .with_context(|| format!("Failed to copy and sign file from {from:?} to {to:?}"))?; fs::rename(&to_tmp, to).with_context(|| { @@ -435,26 +461,6 @@ fn force_install(from: &Path, to: &Path) -> Result<()> { Ok(()) } -pub fn append_initrd_secrets( - append_initrd_secrets_path: &Path, - initrd_path: &PathBuf, - generation_version: u64, -) -> Result<()> { - let status = Command::new(append_initrd_secrets_path) - .args(vec![initrd_path]) - .status() - .context("Failed to append initrd secrets")?; - if !status.success() { - return Err(anyhow::anyhow!( - "Failed to append initrd secrets for generation {} with args `{:?}`", - generation_version, - vec![append_initrd_secrets_path, initrd_path], - )); - } - - Ok(()) -} - fn assemble_kernel_cmdline(init: &Path, kernel_params: Vec) -> Vec { let init_string = String::from( init.to_str() diff --git a/rust/tool/systemd/tests/install.rs b/rust/tool/systemd/tests/install.rs index 90931147..46f5170e 100644 --- a/rust/tool/systemd/tests/install.rs +++ b/rust/tool/systemd/tests/install.rs @@ -52,6 +52,7 @@ fn do_not_overwrite_images() -> Result<()> { let output1 = common::lanzaboote_install(0, esp.path(), generation_links.clone())?; assert!(output1.status.success()); + assert!(verify_signature(&image1)?); remove_signature(&image1)?; assert!(!verify_signature(&image1)?); assert!(verify_signature(&image2)?);