1 # opinionated module that can be used to build nixos images with
2 # a dm-verity protected nix store
10 cfg = config.image.repart.verityStore;
12 verityMatchKey = "store";
14 # TODO: make these and other arch mappings available from systemd-lib for example
18 "x86_64" = "usr-x86-64";
19 "arm64" = "usr-arm64";
21 ."${pkgs.stdenv.hostPlatform.linuxArch}";
25 "x86_64" = "usr-x86-64-verity";
26 "arm64" = "usr-arm64-verity";
28 ."${pkgs.stdenv.hostPlatform.linuxArch}";
32 pkgs.buildPackages.writers.writePython3Bin "assert_uki_repart_match.py"
34 flakeIgnore = [ "E501" ]; # ignores PEP8's line length limit of 79 (black defaults to 88 characters)
37 builtins.replaceStrings [ "@NIX_STORE_VERITY@" ] [
38 partitionTypes.usr-verity
39 ] (builtins.readFile ./assert_uki_repart_match.py)
43 options.image.repart.verityStore = {
44 enable = lib.mkEnableOption "building images with a dm-verity protected nix store";
46 ukiPath = lib.mkOption {
48 default = "/EFI/Linux/${config.system.boot.loader.ukiFile}";
49 defaultText = "/EFI/Linux/\${config.system.boot.loader.ukiFile}";
51 Specify the location on the ESP where the UKI is placed.
60 Specify the attribute name of the ESP.
63 store-verity = lib.mkOption {
65 default = "10-store-verity";
67 Specify the attribute name of the store's dm-verity hash partition.
70 store = lib.mkOption {
74 Specify the attribute name of the store partition.
80 config = lib.mkIf cfg.enable {
81 boot.initrd.systemd.dmVerity.enable = true;
83 image.repart.partitions = {
84 # dm-verity hash partition
85 ${cfg.partitionIds.store-verity}.repartConfig = {
86 Type = partitionTypes.usr-verity;
88 VerityMatchKey = lib.mkDefault verityMatchKey;
89 Label = lib.mkDefault "store-verity";
91 # dm-verity data partition that contains the nix store
92 ${cfg.partitionIds.store} = {
93 storePaths = [ config.system.build.toplevel ];
95 Type = partitionTypes.usr;
97 Format = lib.mkDefault "erofs";
98 VerityMatchKey = lib.mkDefault verityMatchKey;
99 Label = lib.mkDefault "store";
107 # intermediate system image without ESP
109 (config.system.build.image.override {
110 # always disable compression for the intermediate image
111 compression.enable = false;
115 # make it easier to identify the intermediate image in build logs
116 pname = "${previousAttrs.pname}-intermediate";
118 # do not prepare the ESP, this is done in the final image
119 systemdRepartFlags = previousAttrs.systemdRepartFlags ++ [ "--defer-partitions=esp" ];
121 # the image will be self-contained so we can drop references
122 # to the closure that was used to build it
123 unsafeDiscardReferences.out = true;
127 # UKI with embedded usrhash from intermediateImage
130 inherit (config.system.boot.loader) ukiFile;
131 cmdline = "init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}";
133 # override the default UKI
135 pkgs.runCommand ukiFile
137 nativeBuildInputs = [
145 # Extract the usrhash from the output of the systemd-repart invocation for the intermediate image.
147 '.[] | select(.type=="${partitionTypes.usr-verity}") | .roothash' \
148 ${config.system.build.intermediateImage}/repart-output.json
151 # Build UKI with the embedded usrhash.
153 --config=${config.boot.uki.configFile} \
154 --cmdline="${cmdline} usrhash=$usrhash" \
155 --output="$out/${ukiFile}"
159 # final system image that is created from the intermediate image by injecting the UKI from above
161 (config.system.build.image.override {
162 # continue building with existing intermediate image
166 finalAttrs: previousAttrs: {
167 # add entry to inject UKI into ESP
168 finalPartitions = lib.recursiveUpdate previousAttrs.finalPartitions {
169 ${cfg.partitionIds.esp}.contents = {
170 "${cfg.ukiPath}".source = "${config.system.build.uki}/${config.system.boot.loader.ukiFile}";
174 nativeBuildInputs = previousAttrs.nativeBuildInputs ++ [
181 # check that we build the final image with the same intermediate image for
182 # which the injected UKI was built by comparing the UKI cmdline with the repart output
183 # of the intermediate image
185 # This is necessary to notice incompatible substitutions of
186 # non-reproducible store paths, for example when working with distributed
187 # builds, or when offline-signing the UKI.
188 ukify --json=short inspect ${config.system.build.uki}/${config.system.boot.loader.ukiFile} \
189 | assert_uki_repart_match.py "${config.system.build.intermediateImage}/repart-output.json"
191 # copy the uncompressed intermediate image, so that systemd-repart picks it up
192 cp -v ${config.system.build.intermediateImage}/${config.image.repart.imageFileBasename}.raw .
193 chmod +w ${config.image.repart.imageFileBasename}.raw
196 # replace "TBD" with the original roothash values
198 mv -v repart-output{.json,_orig.json}
200 jq --slurp --indent -1 \
201 '.[0] as $intermediate | .[1] as $final
202 | $intermediate | map(select(.roothash != null) | { "uuid":.uuid,"roothash":.roothash }) as $uuids
206 | sort_by(.offset)' \
207 ${config.system.build.intermediateImage}/repart-output.json \
208 repart-output_orig.json \
211 rm -v repart-output_orig.json
214 # the image will be self-contained so we can drop references
215 # to the closure that was used to build it
216 unsafeDiscardReferences.out = true;
222 meta.maintainers = with lib.maintainers; [