Repository: osbuild/osbuild
Pull request #1806 (2024-06-20 17:12:54)

stages: add stage for creating dm-verity partitions
This adds new stage for creating `dm-verity` protected partitions and would be useful for CoreOS with "IBM Secure Execution" (`secex`) images.

elkoniu: @nikita-dubrovskii thanks for the PR. If there is a chance you could also add unit test for this stage in the same PR?
elkoniu: @nikita-dubrovskii thanks for the update, I will try to review it today :)
nikita-dubrovskii: > @nikita-dubrovskii thanks for the PR. If there is a chance you could also add unit test for this stage in the same PR?

Hey, i've added, but not sure if it's enough. IMO it would be nice to have a test capable of real `dmverity` creation, but hadn't succeeded with it yet
mvo5: > > @nikita-dubrovskii thanks for the PR. If there is a chance you could also add unit test for this stage in the same PR?
> 
> Hey, i've added, but not sure if it's enough. IMO it would be nice to have a test capable of real `dmverity` creation, but hadn't succeeded with it yet

Thank you! If you don't mind I will have a look.
mvo5: I got a bit carried away (sorry!) and here is an updated git comiit that tweaks the code to avoid the open and improves the test and also fixes some  pylint issues 
[0001-stages-dmverity-tweak-and-add-integration-test.patch.txt](https://github.com/user-attachments/files/15969754/0001-stages-dmverity-tweak-and-add-integration-test.patch.txt)

If this looks reasonable to you and works in your setup I can push directly here.
jlebon: Sorry, I should've added my review here but I added some review comments on an older version of this as part of https://github.com/coreos/coreos-assembler/pull/3764. I think some of the comments still apply.
jlebon: Hmmm, the "Generate / Test Data" is breaking I think on stale f38 content?

```
Errors during downloading metadata for repository 'default':
  - Status code: 404 for https://rpmrepo.osbuild.org/v2/mirror/public/f38/f38-x86_64-fedora-20230413/repodata/repomd.xml (IP: 3.225.231.174)
```

Patch:
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"
+            }
+          }
+        }
+      }
+    }
+  }
+}


Test patch:
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