|
| 1 | +From 136a73c9f9b64afcc1bc2a4610768be83d406d33 Mon Sep 17 00:00:00 2001 |
| 2 | +From: Nikita Dubrovskii < [email protected]> |
| 3 | +Date: Wed, 20 Mar 2024 10:03:49 +0100 |
| 4 | +Subject: [PATCH] stages: add stage for creating dm-verity partitions |
| 5 | + |
| 6 | +Co-authored-by: Michael Vogt < [email protected]> |
| 7 | +--- |
| 8 | + stages/org.osbuild.dmverity | 31 ++++++++++ |
| 9 | + stages/org.osbuild.dmverity.meta.json | 58 +++++++++++++++++++ |
| 10 | + stages/test/test_dmverity.py | 83 +++++++++++++++++++++++++++ |
| 11 | + 3 files changed, 172 insertions(+) |
| 12 | + create mode 100755 stages/org.osbuild.dmverity |
| 13 | + create mode 100644 stages/org.osbuild.dmverity.meta.json |
| 14 | + create mode 100644 stages/test/test_dmverity.py |
| 15 | + |
| 16 | +diff --git a/stages/org.osbuild.dmverity b/stages/org.osbuild.dmverity |
| 17 | +new file mode 100755 |
| 18 | +index 00000000..88a9cae7 |
| 19 | +--- /dev/null |
| 20 | ++++ b/stages/org.osbuild.dmverity |
| 21 | +@@ -0,0 +1,31 @@ |
| 22 | ++#!/usr/bin/python3 |
| 23 | ++import os |
| 24 | ++import subprocess |
| 25 | ++import sys |
| 26 | ++ |
| 27 | ++import osbuild.api |
| 28 | ++ |
| 29 | ++ |
| 30 | ++def main(tree, paths, devices, options): |
| 31 | ++ data_device = os.path.join(paths["devices"], devices["data_device"]["path"]) |
| 32 | ++ hash_device = os.path.join(paths["devices"], devices["hash_device"]["path"]) |
| 33 | ++ |
| 34 | ++ blocksize = options.get("blocksize", 512) |
| 35 | ++ root_hash_file = os.path.join(tree, options["root_hash_file"]) |
| 36 | ++ |
| 37 | ++ subprocess.run(["/usr/sbin/veritysetup", |
| 38 | ++ "format", data_device, hash_device, |
| 39 | ++ "--data-block-size", f"{blocksize}", |
| 40 | ++ "--root-hash-file", root_hash_file], |
| 41 | ++ check=True) |
| 42 | ++ |
| 43 | ++ subprocess.run(["/usr/sbin/veritysetup", |
| 44 | ++ "verify", data_device, hash_device, |
| 45 | ++ "--root-hash-file", root_hash_file], |
| 46 | ++ check=True) |
| 47 | ++ |
| 48 | ++ |
| 49 | ++if __name__ == '__main__': |
| 50 | ++ args = osbuild.api.arguments() |
| 51 | ++ r = main(args["tree"], args["paths"], args["devices"], args["options"]) |
| 52 | ++ sys.exit(r) |
| 53 | +diff --git a/stages/org.osbuild.dmverity.meta.json b/stages/org.osbuild.dmverity.meta.json |
| 54 | +new file mode 100644 |
| 55 | +index 00000000..213b5855 |
| 56 | +--- /dev/null |
| 57 | ++++ b/stages/org.osbuild.dmverity.meta.json |
| 58 | +@@ -0,0 +1,58 @@ |
| 59 | ++{ |
| 60 | ++ "summary": "Enables dm-verity protection", |
| 61 | ++ "description": [ |
| 62 | ++ "Sets up dm-verity for data_device and stores hash blockes on hash_device.", |
| 63 | ++ "Root hash gets written to `root_hash_file`" |
| 64 | ++ ], |
| 65 | ++ "schema_2": { |
| 66 | ++ "options": { |
| 67 | ++ "additionalProperties": false, |
| 68 | ++ "required": [ |
| 69 | ++ "root_hash_file" |
| 70 | ++ ], |
| 71 | ++ "properties": { |
| 72 | ++ "blocksize": { |
| 73 | ++ "type": "number", |
| 74 | ++ "default": 512 |
| 75 | ++ }, |
| 76 | ++ "root_hash_file": { |
| 77 | ++ "type": "string" |
| 78 | ++ } |
| 79 | ++ } |
| 80 | ++ }, |
| 81 | ++ "devices": { |
| 82 | ++ "type": "object", |
| 83 | ++ "additionalProperties": true, |
| 84 | ++ "required": [ |
| 85 | ++ "data_device", |
| 86 | ++ "hash_device" |
| 87 | ++ ], |
| 88 | ++ "properties": { |
| 89 | ++ "data_device": { |
| 90 | ++ "type": "object", |
| 91 | ++ "additionalProperties": false, |
| 92 | ++ "required": [ |
| 93 | ++ "path" |
| 94 | ++ ], |
| 95 | ++ "properties": { |
| 96 | ++ "path": { |
| 97 | ++ "type": "string" |
| 98 | ++ } |
| 99 | ++ } |
| 100 | ++ }, |
| 101 | ++ "hash_device": { |
| 102 | ++ "type": "object", |
| 103 | ++ "additionalProperties": false, |
| 104 | ++ "required": [ |
| 105 | ++ "path" |
| 106 | ++ ], |
| 107 | ++ "properties": { |
| 108 | ++ "path": { |
| 109 | ++ "type": "string" |
| 110 | ++ } |
| 111 | ++ } |
| 112 | ++ } |
| 113 | ++ } |
| 114 | ++ } |
| 115 | ++ } |
| 116 | ++} |
| 117 | +diff --git a/stages/test/test_dmverity.py b/stages/test/test_dmverity.py |
| 118 | +new file mode 100644 |
| 119 | +index 00000000..b2d3621b |
| 120 | +--- /dev/null |
| 121 | ++++ b/stages/test/test_dmverity.py |
| 122 | +@@ -0,0 +1,83 @@ |
| 123 | ++#!/usr/bin/python3 |
| 124 | ++ |
| 125 | ++import os |
| 126 | ++import subprocess |
| 127 | ++ |
| 128 | ++import pytest # type: ignore |
| 129 | ++ |
| 130 | ++from osbuild import testutil |
| 131 | ++from osbuild.testutil import has_executable |
| 132 | ++ |
| 133 | ++STAGE_NAME = "org.osbuild.dmverity" |
| 134 | ++ |
| 135 | ++ |
| 136 | ++@pytest.mark.parametrize("test_data,expected_err", [ |
| 137 | ++ # bad |
| 138 | ++ ({}, "'root_hash_file' is a required property"), |
| 139 | ++ ({"root_hash_file": 123}, "123 is not of type 'string'"), |
| 140 | ++ # good |
| 141 | ++ ({"root_hash_file": "abc"}, ""), |
| 142 | ++]) |
| 143 | ++def test_schema_validation_dmverity(stage_schema, test_data, expected_err): |
| 144 | ++ test_input = { |
| 145 | ++ "type": STAGE_NAME, |
| 146 | ++ "devices": { |
| 147 | ++ "data_device": { |
| 148 | ++ "path": "some-path", |
| 149 | ++ }, |
| 150 | ++ "hash_device": { |
| 151 | ++ "path": "some-path", |
| 152 | ++ }, |
| 153 | ++ }, |
| 154 | ++ "options": { |
| 155 | ++ } |
| 156 | ++ } |
| 157 | ++ test_input["options"].update(test_data) |
| 158 | ++ res = stage_schema.validate(test_input) |
| 159 | ++ |
| 160 | ++ if expected_err == "": |
| 161 | ++ assert res.valid is True, f"err: {[e.as_dict() for e in res.errors]}" |
| 162 | ++ else: |
| 163 | ++ assert res.valid is False |
| 164 | ++ testutil.assert_jsonschema_error_contains(res, expected_err, expected_num_errs=1) |
| 165 | ++ |
| 166 | ++ |
| 167 | ++@pytest.mark.skipif(not has_executable("mkfs.ext4"), reason="need mkfs.ext4") |
| 168 | ++@pytest.mark.skipif(not has_executable("veritysetup"), reason="need veritysetup") |
| 169 | ++def test_dmverity_integration(tmp_path, stage_module): |
| 170 | ++ fake_dev_path = tmp_path / "dev" |
| 171 | ++ fake_dev_path.mkdir() |
| 172 | ++ |
| 173 | ++ fake_data_disk = "xxd1" |
| 174 | ++ fake_hash_disk = "xxd2" |
| 175 | ++ for fname in [fake_data_disk, fake_hash_disk]: |
| 176 | ++ p = fake_dev_path / fname |
| 177 | ++ p.write_bytes(b"") |
| 178 | ++ os.truncate(p, 10 * 1024 * 1024) |
| 179 | ++ # format is not strictly needed as dmvertify is working on the block level but this makes the test more realistic |
| 180 | ++ subprocess.run( |
| 181 | ++ ["mkfs.ext4", os.fspath(fake_dev_path / fake_data_disk)], check=True) |
| 182 | ++ |
| 183 | ++ paths = { |
| 184 | ++ "devices": fake_dev_path, |
| 185 | ++ } |
| 186 | ++ devices = { |
| 187 | ++ "data_device": { |
| 188 | ++ "path": fake_data_disk, |
| 189 | ++ }, |
| 190 | ++ "hash_device": { |
| 191 | ++ "path": fake_hash_disk, |
| 192 | ++ }, |
| 193 | ++ } |
| 194 | ++ options = { |
| 195 | ++ "root_hash_file": "hashfile", |
| 196 | ++ } |
| 197 | ++ |
| 198 | ++ tree = tmp_path |
| 199 | ++ stage_module.main(tree, paths, devices, options) |
| 200 | ++ output = subprocess.check_output( |
| 201 | ++ ["veritysetup", "dump", os.fspath(fake_dev_path / fake_hash_disk)], |
| 202 | ++ universal_newlines=True) |
| 203 | ++ assert "UUID:" in output |
| 204 | ++ # hash file is created and has the expected size |
| 205 | ++ assert (tree / "hashfile").stat().st_size == 64 |
| 206 | +-- |
| 207 | +2.45.2 |
| 208 | + |
0 commit comments