942588c686
This module provides some abstraction for a multi-stage build to create a dm-verity protected NixOS repart image. The opinionated approach realized by this module is to first create an immutable, verity-protected nix store partition, then embed the root hash of the corresponding verity hash partition in a UKI, that is then injected into the ESP of the resulting image. The UKI can then precisely identify the corresponding data from which the entire system is bootstrapped. The module comes with a script that checks the UKI used in the final image corresponds to the intermediate image created in the first step. 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.
79 lines
2.1 KiB
Python
79 lines
2.1 KiB
Python
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()
|