Skip to content

Commit 253aef8

Browse files
authored
Merge pull request #350 from desultory/dev
Add basic dm-integrity support
2 parents f350a11 + a3f106b commit 253aef8

File tree

5 files changed

+92
-15
lines changed

5 files changed

+92
-15
lines changed

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "ugrd"
7-
version = "2.0.2"
7+
version = "2.1.0"
88
authors = [
99
{ name="Desultory", email="[email protected]" },
1010
]
@@ -19,7 +19,7 @@ classifiers = [
1919

2020
dependencies = [
2121
"zenlib >= 3.0.2",
22-
"pycpio >= 1.5.2"
22+
"pycpio >= 1.5.5"
2323
]
2424

2525
[project.optional-dependencies]

src/ugrd/crypto/cryptsetup.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
__author__ = "desultory"
2-
__version__ = "4.1.3"
2+
__version__ = "4.2.0"
33

44
from json import loads
55
from pathlib import Path
@@ -28,6 +28,7 @@
2828
"include_header",
2929
"validate_key",
3030
"validate_header",
31+
"_dm-integrity", # Internal parameter for when dm-integrity was detected. mostly used for test automation
3132
]
3233

3334

@@ -147,6 +148,9 @@ def _get_dm_info(self, mapped_name: str) -> dict:
147148
def _get_dm_slave_info(self, device_info: dict) -> (str, dict):
148149
"""Gets the device mapper slave information for a particular device."""
149150
slave_source = device_info["slaves"][0]
151+
# For integrity backed devices, get the slave's slave
152+
if self["_vblk_info"].get(slave_source, {}).get("uuid", "").startswith("CRYPT-SUBDEV"):
153+
slave_source = self["_vblk_info"][slave_source]["slaves"][0]
150154
slave_name = self["_vblk_info"].get(slave_source, {}).get("name")
151155
search_paths = ["/dev/", "/dev/mapper/"]
152156

@@ -207,7 +211,7 @@ def _detect_luks_aes_module(self, luks_cipher_name: str) -> None:
207211
self["_kmod_auto"] = crypto_config["module"]
208212

209213

210-
def _detect_luks_header_aes(self, luks_info: dict) -> dict:
214+
def _detect_luks_header_aes(self, luks_info: dict) -> None:
211215
"""Checks the cipher type in the LUKS header, reads /proc/crypto to find the
212216
corresponding driver. If it's not builtin, adds the module to the kernel modules."""
213217
for keyslot in luks_info.get("keyslots", {}).values():
@@ -218,7 +222,7 @@ def _detect_luks_header_aes(self, luks_info: dict) -> dict:
218222
_detect_luks_aes_module(self, segment["encryption"])
219223

220224

221-
def _detect_luks_header_sha(self, luks_info: dict) -> dict:
225+
def _detect_luks_header_sha(self, luks_info: dict) -> None:
222226
"""Reads the hash algorithm from the LUKS header,
223227
enables the corresponding kernel module using _crypto_ciphers"""
224228
for keyslot in luks_info.get("keyslots", {}).values():
@@ -229,6 +233,21 @@ def _detect_luks_header_sha(self, luks_info: dict) -> dict:
229233
self["kernel_modules"] = self._crypto_ciphers[digest["hash"]]["driver"]
230234

231235

236+
def _detect_luks_header_integrity(self, luks_info: dict, mapped_name: str) -> None:
237+
"""Reads the integrity algorithm from the LUKS header,
238+
Enables the dm-integrity module, and returns the integrity type."""
239+
for segment in luks_info.get("segments", {}).values():
240+
if integrity_type := segment.get("integrity", {}).get("type"):
241+
integrity_kmods = ["dm_integrity", "authenc"]
242+
if integrity_type.startswith("hmac"):
243+
integrity_kmods.append("hmac")
244+
self.logger.info(
245+
f"[{c_(mapped_name, 'blue')}]({c_(integrity_type, 'cyan')}) Enabling kernel modules for dm-integrity: {c_(', '.join(integrity_kmods), 'magenta', bright=True)}"
246+
)
247+
self["cryptsetup"][mapped_name]["_dm-integrity"] = integrity_type
248+
return
249+
250+
232251
@contains("cryptsetup_header_validation", "Skipping cryptsetup header validation.", log_level=30)
233252
def _validate_cryptsetup_header(self, mapped_name: str) -> None:
234253
"""Validates configured cryptsetup volumes against the LUKS header."""
@@ -265,6 +284,7 @@ def _validate_cryptsetup_header(self, mapped_name: str) -> None:
265284

266285
_detect_luks_header_aes(self, luks_info)
267286
_detect_luks_header_sha(self, luks_info)
287+
_detect_luks_header_integrity(self, luks_info, mapped_name)
268288

269289
if not self["argon2"]: # if argon support was not detected, check if the header wants it
270290
for keyslot in luks_info.get("keyslots", {}).values():

src/ugrd/fs/mounts.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
__author__ = "desultory"
2-
__version__ = "7.1.4"
2+
__version__ = "7.2.1"
33

44
from pathlib import Path
55
from re import search
@@ -504,20 +504,26 @@ def _autodetect_dm(self, mountpoint, device=None) -> None:
504504
Ensures it's a device mapper mount, then autodetects the mount type.
505505
Adds kmods to the autodetect list based on the mount source.
506506
"""
507+
# If a device is explicity passed, set it as the source device
507508
if device:
508509
self.logger.debug("[%s] Using provided device for mount autodetection: %s" % (mountpoint, device))
509510
source_device = device
511+
# If it's a mountpoint, try to resolve underlying devices
510512
elif mountpoint:
511513
source_device = _resolve_overlay_lower_device(self, mountpoint)
512514
else:
513515
raise AutodetectError("Mountpoint not found in host mounts: %s" % mountpoint)
514516

517+
# Get the name of the device from the path
515518
device_name = source_device.split("/")[-1]
519+
# Check that it's a device mapper mount (by prefix or path)
520+
# If it's not, skip this autodetection by returning early
516521
if not any(device_name.startswith(prefix) for prefix in ["dm-", "md"]):
517522
if not source_device.startswith("/dev/mapper/"):
518523
self.logger.debug("Mount is not a device mapper mount: %s" % source_device)
519524
return
520525

526+
# Get the source device using blkid info/virtual block device info
521527
if source_device not in self["_blkid_info"]:
522528
if device_name in self["_vblk_info"]:
523529
source_name = self["_vblk_info"][device_name]["name"]
@@ -535,6 +541,7 @@ def _autodetect_dm(self, mountpoint, device=None) -> None:
535541
major, minor = _get_device_id(source_device)
536542
self.logger.debug("[%s] Major: %s, Minor: %s" % (source_device, major, minor))
537543

544+
# Get the virtual block device name using the major/minor
538545
for name, info in self["_vblk_info"].items():
539546
if info["major"] == str(major) and info["minor"] == str(minor):
540547
dev_name = name
@@ -544,27 +551,43 @@ def _autodetect_dm(self, mountpoint, device=None) -> None:
544551
"[%s] Unable to find device mapper device with maj: %s min: %s" % (source_device, major, minor)
545552
)
546553

554+
# Check that the virtual block device has slaves defined
547555
if len(self["_vblk_info"][dev_name]["slaves"]) == 0:
548556
raise AutodetectError("No slaves found for device mapper device, unknown type: %s" % source_device.name)
557+
# Treat the first slave as the source for autodetection
549558
slave_source = self["_vblk_info"][dev_name]["slaves"][0]
559+
# If the slave source is a CRYPT-SUBDEV device, use its slave instead
560+
if self["_vblk_info"].get(slave_source, {}).get("uuid", "").startswith("CRYPT-SUBDEV"):
561+
slave_source = self["_vblk_info"][slave_source]["slaves"][0]
562+
self.logger.info(f"[{c_(dev_name, 'blue')}] Slave is a CRYPT-SUBDEV, using its slave instead: {c_(slave_source, 'cyan')}")
563+
# Add the kmod for it
564+
self.logger.info(f"[{c_(dev_name, 'blue')}] Adding kmod for CRYPT-SUBDEV: {c_('dm-crypt', 'magenta')}")
565+
self["_kmod_auto"] = ["dm_integrity", "authenc"]
566+
550567
autodetect_mount_kmods(self, slave_source)
551568

569+
# Check that the source device name matches the devie mapper name
570+
if source_device.name != self["_vblk_info"][dev_name]["name"] and source_device.name != dev_name:
571+
raise ValidationError(
572+
"Device mapper device name mismatch: %s != %s" % (source_device.name, self["_vblk_info"][dev_name]["name"])
573+
)
574+
575+
# Get block info using the slave source device
552576
try:
553577
blkid_info = self["_blkid_info"][f"/dev/{slave_source}"]
554578
except KeyError:
555579
if slave_source in self["_vblk_info"]:
580+
# If the slave source isn't used in blkid, use the /dev/mapper path with the slave name
556581
blkid_info = self["_blkid_info"][f"/dev/mapper/{self['_vblk_info'][slave_source]['name']}"]
557582
else:
558583
return self.logger.warning(f"No blkid info found for device mapper slave: {c_(slave_source, 'yellow')}")
559-
if source_device.name != self["_vblk_info"][dev_name]["name"] and source_device.name != dev_name:
560-
raise ValidationError(
561-
"Device mapper device name mismatch: %s != %s" % (source_device.name, self["_vblk_info"][dev_name]["name"])
562-
)
563584

564585
self.logger.debug(
565586
"[%s] Device mapper info: %s\nDevice config: %s"
566587
% (source_device.name, self["_vblk_info"][dev_name], blkid_info)
567588
)
589+
590+
# With the blkid info, run the appropriate autodetect function based on the type
568591
if blkid_info.get("type") == "crypto_LUKS" or source_device.name in self.get("cryptsetup", {}):
569592
autodetect_luks(self, source_device, dev_name, blkid_info)
570593
elif blkid_info.get("type") == "LVM2_member":
@@ -579,8 +602,19 @@ def _autodetect_dm(self, mountpoint, device=None) -> None:
579602
raise AutodetectError("[%s] No type found for device mapper device: %s" % (dev_name, source_device))
580603
raise ValidationError("Unknown device mapper device type: %s" % blkid_info.get("type"))
581604

605+
# Run autodetect on all slaves, in case of nested device mapper devices
582606
for slave in self["_vblk_info"][dev_name]["slaves"]:
583607
try:
608+
# If the slave is a CRYPT-SUBDEV, iterate over its slaves instead
609+
if self["_vblk_info"][slave]["uuid"].startswith("CRYPT-SUBDEV"):
610+
for crypt_slave in self["_vblk_info"][slave]["slaves"]:
611+
_autodetect_dm(self, mountpoint, crypt_slave)
612+
self.logger.info(
613+
"[%s] Autodetected device mapper container: %s"
614+
% (c_(source_device.name, "blue", bright=True), c_(crypt_slave, "cyan"))
615+
)
616+
continue
617+
# Otherwise, just autodetect the slave device
584618
_autodetect_dm(self, mountpoint, slave) # Just pass the slave device name, as it will be re-detected
585619
self.logger.info(
586620
"[%s] Autodetected device mapper container: %s"

src/ugrd/fs/test_image.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
__version__ = "2.0.0"
1+
__version__ = "2.1.0"
22

3+
from re import match
34
from tempfile import TemporaryDirectory
45

56
from zenlib.util import colorize as c_
67
from zenlib.util import contains
78

8-
99
MIN_FS_SIZES = {"btrfs": 110, "f2fs": 50}
1010

11+
1112
@contains("test_flag", "A test flag must be set to create a test image", raise_exception=True)
1213
def init_banner(self):
1314
"""Initialize the test image banner, set a random flag if not set."""
@@ -33,12 +34,13 @@ def _allocate_image(self, image_path, padding=0):
3334

3435
with open(image_path, "wb") as f:
3536
total_size = (self.test_image_size + padding) * (2**20) # Convert MB to bytes
36-
self.logger.info(f"Allocating {self.test_image_size + padding}MB test image file: { c_(f.name, 'green')}")
37-
self.logger.debug(f"[{f.name}] Total bytes: { c_(total_size, 'green')}")
37+
self.logger.info(f"Allocating {self.test_image_size + padding}MB test image file: {c_(f.name, 'green')}")
38+
self.logger.debug(f"[{f.name}] Total bytes: {c_(total_size, 'green')}")
3839
f.write(b"\0" * total_size)
3940

41+
4042
def _copy_fs_contents(self, image_path, build_dir):
41-
""" Mount and copy the filesystem contents into the image,
43+
"""Mount and copy the filesystem contents into the image,
4244
for filesystems which cannot be created directly from a directory"""
4345
try:
4446
with TemporaryDirectory() as tmp_dir:
@@ -84,6 +86,14 @@ def make_test_luks_image(self, image_path):
8486
keyfile_path = _get_luks_keyfile(self)
8587
self.logger.info("Using LUKS keyfile: %s" % c_(keyfile_path, "green"))
8688
self.logger.info("Creating LUKS image: %s" % c_(image_path, "green"))
89+
extra_args = []
90+
if integrity_type := _get_luks_config(self).get("_dm-integrity"):
91+
# If it's the type reported in the header like <type>(<algo>), turn it into <type>-<algo> for arg usage
92+
if m := match(r"^(?P<type>\w+)\((?P<algo>[\w-]+)\)$", integrity_type):
93+
integrity_type = f"{m['type']}-{m['algo']}"
94+
self.logger.info(f"[{c_(image_path, 'green')}] LUKS integrity type: {c_(integrity_type, 'cyan')}")
95+
extra_args.extend(["--integrity", integrity_type])
96+
8797
self._run(
8898
[
8999
"cryptsetup",
@@ -94,6 +104,9 @@ def make_test_luks_image(self, image_path):
94104
"--batch-mode",
95105
"--key-file",
96106
keyfile_path,
107+
"--pbkdf-memory",
108+
"8192", # Only use 8MB of memory for PBKDF to speed up test image creation and avoid high memory usage
109+
*extra_args,
97110
]
98111
)
99112
self.logger.info("Opening LUKS image: %s" % c_(image_path, "magenta"))

tests/test_cryptsetup.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ def test_cryptsetup_included_key(self):
3131
generator = InitramfsGenerator(logger=self.logger, config="tests/cryptsetup_included_key.toml")
3232
generator.build()
3333

34+
def test_cryptsetup_integrity(self):
35+
"""Tests LUKS based roots using a keyfile included in the initramfs with integrity protection"""
36+
generator = InitramfsGenerator(
37+
logger=self.logger,
38+
config="tests/cryptsetup_included_key.toml",
39+
_kmod_auto=["dm-integrity", "authenc"], # Specify this because its usually auto-detected during header validation
40+
cryptsetup={"root": {"_dm-integrity": "hmac(sha256)"}}, # Use the type like defined in the header, not the proper args to test processing
41+
)
42+
generator.build()
43+
3444

3545
if __name__ == "__main__":
3646
main()

0 commit comments

Comments
 (0)