diff --git a/stages/org.osbuild.dmverity b/stages/org.osbuild.dmverity new file mode 100755 index 00000000..88a9cae7 --- /dev/null +++ b/stages/org.osbuild.dmverity @@ -0,0 +1,31 @@ +#!/usr/bin/python3 +import os +import subprocess +import sys + +import osbuild.api + + +def main(tree, paths, devices, options): + data_device = os.path.join(paths["devices"], devices["data_device"]["path"]) + hash_device = os.path.join(paths["devices"], devices["hash_device"]["path"]) + + blocksize = options.get("blocksize", 512) + root_hash_file = os.path.join(tree, options["root_hash_file"]) + + subprocess.run(["/usr/sbin/veritysetup", + "format", data_device, hash_device, + "--data-block-size", f"{blocksize}", + "--root-hash-file", root_hash_file], + check=True) + + subprocess.run(["/usr/sbin/veritysetup", + "verify", data_device, hash_device, + "--root-hash-file", root_hash_file], + check=True) + + +if __name__ == '__main__': + args = osbuild.api.arguments() + r = main(args["tree"], args["paths"], args["devices"], args["options"]) + sys.exit(r) diff --git a/stages/org.osbuild.dmverity.meta.json b/stages/org.osbuild.dmverity.meta.json new file mode 100644 index 00000000..213b5855 --- /dev/null +++ b/stages/org.osbuild.dmverity.meta.json @@ -0,0 +1,58 @@ +{ + "summary": "Enables dm-verity protection", + "description": [ + "Sets up dm-verity for data_device and stores hash blockes on hash_device.", + "Root hash gets written to `root_hash_file`" + ], + "schema_2": { + "options": { + "additionalProperties": false, + "required": [ + "root_hash_file" + ], + "properties": { + "blocksize": { + "type": "number", + "default": 512 + }, + "root_hash_file": { + "type": "string" + } + } + }, + "devices": { + "type": "object", + "additionalProperties": true, + "required": [ + "data_device", + "hash_device" + ], + "properties": { + "data_device": { + "type": "object", + "additionalProperties": false, + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + } + } + }, + "hash_device": { + "type": "object", + "additionalProperties": false, + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + } + } + } + } + } + } +} diff --git a/stages/test/test_dmverity.py b/stages/test/test_dmverity.py new file mode 100644 index 00000000..b2d3621b --- /dev/null +++ b/stages/test/test_dmverity.py @@ -0,0 +1,83 @@ +#!/usr/bin/python3 + +import os +import subprocess + +import pytest # type: ignore + +from osbuild import testutil +from osbuild.testutil import has_executable + +STAGE_NAME = "org.osbuild.dmverity" + + +@pytest.mark.parametrize("test_data,expected_err", [ + # bad + ({}, "'root_hash_file' is a required property"), + ({"root_hash_file": 123}, "123 is not of type 'string'"), + # good + ({"root_hash_file": "abc"}, ""), +]) +def test_schema_validation_dmverity(stage_schema, test_data, expected_err): + test_input = { + "type": STAGE_NAME, + "devices": { + "data_device": { + "path": "some-path", + }, + "hash_device": { + "path": "some-path", + }, + }, + "options": { + } + } + test_input["options"].update(test_data) + res = stage_schema.validate(test_input) + + if expected_err == "": + assert res.valid is True, f"err: {[e.as_dict() for e in res.errors]}" + else: + assert res.valid is False + testutil.assert_jsonschema_error_contains(res, expected_err, expected_num_errs=1) + + +@pytest.mark.skipif(not has_executable("mkfs.ext4"), reason="need mkfs.ext4") +@pytest.mark.skipif(not has_executable("veritysetup"), reason="need veritysetup") +def test_dmverity_integration(tmp_path, stage_module): + fake_dev_path = tmp_path / "dev" + fake_dev_path.mkdir() + + fake_data_disk = "xxd1" + fake_hash_disk = "xxd2" + for fname in [fake_data_disk, fake_hash_disk]: + p = fake_dev_path / fname + p.write_bytes(b"") + os.truncate(p, 10 * 1024 * 1024) + # format is not strictly needed as dmvertify is working on the block level but this makes the test more realistic + subprocess.run( + ["mkfs.ext4", os.fspath(fake_dev_path / fake_data_disk)], check=True) + + paths = { + "devices": fake_dev_path, + } + devices = { + "data_device": { + "path": fake_data_disk, + }, + "hash_device": { + "path": fake_hash_disk, + }, + } + options = { + "root_hash_file": "hashfile", + } + + tree = tmp_path + stage_module.main(tree, paths, devices, options) + output = subprocess.check_output( + ["veritysetup", "dump", os.fspath(fake_dev_path / fake_hash_disk)], + universal_newlines=True) + assert "UUID:" in output + # hash file is created and has the expected size + assert (tree / "hashfile").stat().st_size == 64