Unverified Commit fed418aa authored by Ryan Lahfa's avatar Ryan Lahfa Committed by GitHub
Browse files

add support for building fully dm-verity protected images with systemd-repart (#343252)

parents 9fc6ead6 56d038e1
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -46,6 +46,9 @@
  If you experience any issues, please report them.
  The original Perl script can still be used for now by setting `system.switch.enableNg` to `false`.

- Support for mounting filesystems from block devices protected with [dm-verity](https://docs.kernel.org/admin-guide/device-mapper/verity.html)
  was added through the `boot.initrd.systemd.dmVerity` option.

- The [Xen Hypervisor](https://xenproject.org) is once again available as a virtualisation option under [`virtualisation.xen`](#opt-virtualisation.xen.enable).
  - This release includes Xen [4.17.5](https://wiki.xenproject.org/wiki/Xen_Project_4.17_Release_Notes), [4.18.3](https://wiki.xenproject.org/wiki/Xen_Project_4.18_Release_Notes) and [4.19.0](https://wiki.xenproject.org/wiki/Xen_Project_4.19_Release_Notes), as well as support for booting the hypervisor on EFI systems.
  ::: {.warning}
+78 −0
Original line number Diff line number Diff line
import json
import sys

store_verity_type = "@NIX_STORE_VERITY@"  # replaced at import by Nix


def extract_uki_cmdline_params(ukify_json: dict) -> dict[str, str]:
    """
    Return a dict of the parameters in the .cmdline section of the UKI
    Exits early if "usrhash" is not included.
    """
    cmdline = ukify_json.get(".cmdline", {}).get("text")
    if cmdline is None:
        print("Failed to get cmdline from ukify output")

    params = {}
    for param in cmdline.split():
        key, val = param.partition("=")[::2]
        params[key] = val

    if "usrhash" not in params:
        print(
            f"UKI cmdline does not contain a usrhash:\n{cmdline}"
        )
        exit(1)

    return params


def hashes_match(partition: dict[str, str], expected: str) -> bool:
    """
    Checks if the value of the "roothash" key in the passed partition object matches `expected`.
    """
    if partition.get("roothash") != expected:
        pretty_part = json.dumps(partition, indent=2)
        print(
            f"hash mismatch, expected to find roothash {expected} in:\n{pretty_part}"
        )
        return False
    else:
        return True


def check_partitions(
    partitions: list[dict], uki_params: dict[str, str]
) -> bool:
    """
    Checks if the usrhash from `uki_params` has a matching roothash
    for the corresponding partition in `partitions`.
    """
    for part in partitions:
        if part.get("type") == store_verity_type:
            expected = uki_params["usrhash"]
            return hashes_match(part, expected)

    return False


def main() -> None:
    ukify_json = json.load(sys.stdin)
    repart_json_output = sys.argv[1]

    with open(repart_json_output, "r") as r:
        repart_json = json.load(r)

    uki_params = extract_uki_cmdline_params(ukify_json)

    if check_partitions(repart_json, uki_params):
        print("UKI and repart verity hashes match")
    else:
        print("Compatibility check for UKI and image failed!")
        print(f"UKI cmdline parameters:\n{uki_params}")
        print(f"repart config: {repart_json_output}")
        exit(1)


if __name__ == "__main__":
    main()
+209 −0
Original line number Diff line number Diff line
# opinionated module that can be used to build nixos images with
# a dm-verity protected nix store
{
  config,
  pkgs,
  lib,
  ...
}:
let
  cfg = config.image.repart.verityStore;

  verityMatchKey = "store";

  # TODO: make these and other arch mappings available from systemd-lib for example
  partitionTypes = {
    usr =
      {
        "x86_64" = "usr-x86-64";
        "arm64" = "usr-arm64";
      }
      ."${pkgs.stdenv.hostPlatform.linuxArch}";

    usr-verity =
      {
        "x86_64" = "usr-x86-64-verity";
        "arm64" = "usr-arm64-verity";
      }
      ."${pkgs.stdenv.hostPlatform.linuxArch}";
  };

  verityHashCheck =
    pkgs.buildPackages.writers.writePython3Bin "assert_uki_repart_match.py"
      {
        flakeIgnore = [ "E501" ]; # ignores PEP8's line length limit of 79 (black defaults to 88 characters)
      }
      (
        builtins.replaceStrings [ "@NIX_STORE_VERITY@" ] [
          partitionTypes.usr-verity
        ] (builtins.readFile ./assert_uki_repart_match.py)
      );
in
{
  options.image.repart.verityStore = {
    enable = lib.mkEnableOption "building images with a dm-verity protected nix store";

    ukiPath = lib.mkOption {
      type = lib.types.str;
      default = "/EFI/Linux/${config.system.boot.loader.ukiFile}";
      defaultText = "/EFI/Linux/\${config.system.boot.loader.ukiFile}";
      description = ''
        Specify the location on the ESP where the UKI is placed.
      '';
    };

    partitionIds = {
      esp = lib.mkOption {
        type = lib.types.str;
        default = "00-esp";
        description = ''
          Specify the attribute name of the ESP.
        '';
      };
      store-verity = lib.mkOption {
        type = lib.types.str;
        default = "10-store-verity";
        description = ''
          Specify the attribute name of the store's dm-verity hash partition.
        '';
      };
      store = lib.mkOption {
        type = lib.types.str;
        default = "20-store";
        description = ''
          Specify the attribute name of the store partition.
        '';
      };
    };
  };

  config = lib.mkIf cfg.enable {
    boot.initrd.systemd.dmVerity.enable = true;

    image.repart.partitions = {
      # dm-verity hash partition
      ${cfg.partitionIds.store-verity}.repartConfig = {
        Type = partitionTypes.usr-verity;
        Verity = "hash";
        VerityMatchKey = lib.mkDefault verityMatchKey;
        Label = lib.mkDefault "store-verity";
      };
      # dm-verity data partition that contains the nix store
      ${cfg.partitionIds.store} = {
        storePaths = [ config.system.build.toplevel ];
        repartConfig = {
          Type = partitionTypes.usr;
          Verity = "data";
          Format = lib.mkDefault "erofs";
          VerityMatchKey = lib.mkDefault verityMatchKey;
          Label = lib.mkDefault "store";
        };
      };

    };

    system.build = {

      # intermediate system image without ESP
      intermediateImage =
        (config.system.build.image.override {
          # always disable compression for the intermediate image
          compression.enable = false;
        }).overrideAttrs
          (
            _: previousAttrs: {
              # make it easier to identify the intermediate image in build logs
              pname = "${previousAttrs.pname}-intermediate";

              # do not prepare the ESP, this is done in the final image
              systemdRepartFlags = previousAttrs.systemdRepartFlags ++ [ "--defer-partitions=esp" ];

              # the image will be self-contained so we can drop references
              # to the closure that was used to build it
              unsafeDiscardReferences.out = true;
            }
          );

      # UKI with embedded usrhash from intermediateImage
      uki =
        let
          inherit (config.system.boot.loader) ukiFile;
          cmdline = "init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}";
        in
        # override the default UKI
        lib.mkOverride 99 (
          pkgs.runCommand ukiFile
            {
              nativeBuildInputs = [
                pkgs.jq
                pkgs.systemdUkify
              ];
            }
            ''
              mkdir -p $out

              # Extract the usrhash from the output of the systemd-repart invocation for the intermediate image.
              usrhash=$(jq -r \
                '.[] | select(.type=="${partitionTypes.usr-verity}") | .roothash' \
                ${config.system.build.intermediateImage}/repart-output.json
              )

              # Build UKI with the embedded usrhash.
              ukify build \
                  --config=${config.boot.uki.configFile} \
                  --cmdline="${cmdline} usrhash=$usrhash" \
                  --output="$out/${ukiFile}"
            ''
        );

      # final system image that is created from the intermediate image by injecting the UKI from above
      finalImage =
        (config.system.build.image.override {
          # continue building with existing intermediate image
          createEmpty = false;
        }).overrideAttrs
          (
            finalAttrs: previousAttrs:
            let
              copyUki = "CopyFiles=${config.system.build.uki}/${config.system.boot.loader.ukiFile}:${cfg.ukiPath}";
            in
            {
              nativeBuildInputs = previousAttrs.nativeBuildInputs ++ [
                pkgs.systemdUkify
                verityHashCheck
              ];

              postPatch = ''
                # add entry to inject UKI into ESP
                echo '${copyUki}' >> $finalRepartDefinitions/${cfg.partitionIds.esp}.conf
              '';

              preBuild = ''
                # check that we build the final image with the same intermediate image for
                # which the injected UKI was built by comparing the UKI cmdline with the repart output
                # of the intermediate image
                #
                # This is necessary to notice incompatible substitutions of
                # non-reproducible store paths, for example when working with distributed
                # builds, or when offline-signing the UKI.
                ukify --json=short inspect ${config.system.build.uki}/${config.system.boot.loader.ukiFile} \
                  | assert_uki_repart_match.py "${config.system.build.intermediateImage}/repart-output.json"

                # copy the uncompressed intermediate image, so that systemd-repart picks it up
                cp -v ${config.system.build.intermediateImage}/${config.image.repart.imageFileBasename}.raw .
                chmod +w ${config.image.repart.imageFileBasename}.raw
              '';

              # the image will be self-contained so we can drop references
              # to the closure that was used to build it
              unsafeDiscardReferences.out = true;
            }
          );
    };
  };

  meta.maintainers = with lib.maintainers; [
    nikstur
    willibutz
  ];
}
+4 −0
Original line number Diff line number Diff line
@@ -69,6 +69,10 @@ let
  }) opts;
in
{
  imports = [
    ./repart-verity-store.nix
  ];

  options.image.repart = {

    name = lib.mkOption {
+1 −0
Original line number Diff line number Diff line
@@ -1625,6 +1625,7 @@
  ./system/boot/stage-2.nix
  ./system/boot/systemd.nix
  ./system/boot/systemd/coredump.nix
  ./system/boot/systemd/dm-verity.nix
  ./system/boot/systemd/initrd-secrets.nix
  ./system/boot/systemd/initrd.nix
  ./system/boot/systemd/journald.nix
Loading