Skip to content

Commit

Permalink
add support for building fully dm-verity protected images with system…
Browse files Browse the repository at this point in the history
…d-repart (NixOS#343252)
  • Loading branch information
RaitoBezarius authored Sep 20, 2024
2 parents 9fc6ead + 56d038e commit fed418a
Show file tree
Hide file tree
Showing 8 changed files with 487 additions and 0 deletions.
3 changes: 3 additions & 0 deletions nixos/doc/manual/release-notes/rl-2411.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
78 changes: 78 additions & 0 deletions nixos/modules/image/assert_uki_repart_match.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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 changes: 209 additions & 0 deletions nixos/modules/image/repart-verity-store.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# 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 changes: 4 additions & 0 deletions nixos/modules/image/repart.nix
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ let
}) opts;
in
{
imports = [
./repart-verity-store.nix
];

options.image.repart = {

name = lib.mkOption {
Expand Down
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions nixos/modules/system/boot/systemd/dm-verity.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{ config, lib, ... }:

let
cfg = config.boot.initrd.systemd.dmVerity;
in
{
options = {
boot.initrd.systemd.dmVerity = {
enable = lib.mkEnableOption "dm-verity" // {
description = ''
Mount verity-protected block devices in the initrd.
Enabling this option allows to use `systemd-veritysetup` and
`systemd-veritysetup-generator` in the initrd.
'';
};
};
};

config = lib.mkIf cfg.enable {
assertions = [
{
assertion = config.boot.initrd.systemd.enable;
message = ''
'boot.initrd.systemd.dmVerity.enable' requires 'boot.initrd.systemd.enable' to be enabled.
'';
}
];

boot.initrd = {
availableKernelModules = [
"dm_mod"
"dm_verity"
];

# dm-verity needs additional udev rules from LVM to work.
services.lvm.enable = true;

# The additional targets and store paths allow users to integrate verity-protected devices
# through the systemd tooling.
systemd = {
additionalUpstreamUnits = [
"veritysetup-pre.target"
"veritysetup.target"
"remote-veritysetup.target"
];

storePaths = [
"${config.boot.initrd.systemd.package}/lib/systemd/systemd-veritysetup"
"${config.boot.initrd.systemd.package}/lib/systemd/system-generators/systemd-veritysetup-generator"
];
};
};
};

meta.maintainers = with lib.maintainers; [
msanft
nikstur
willibutz
];
}
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ in {
apcupsd = handleTest ./apcupsd.nix {};
apfs = runTest ./apfs.nix;
appliance-repart-image = runTest ./appliance-repart-image.nix;
appliance-repart-image-verity-store = runTest ./appliance-repart-image-verity-store.nix;
apparmor = handleTest ./apparmor.nix {};
archi = handleTest ./archi.nix {};
aria2 = handleTest ./aria2.nix {};
Expand Down
Loading

0 comments on commit fed418a

Please sign in to comment.