Skip to content

Commit

Permalink
Add support for creating standalone integrity devices
Browse files Browse the repository at this point in the history
Advanced options for the integrity format are not support, users
can choose only the integrity algorithm (HMAC is not supported)
and sector size.
  • Loading branch information
vojtechtrefny committed Sep 30, 2021
1 parent 3146758 commit 04276d5
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 9 deletions.
36 changes: 36 additions & 0 deletions blivet/devicelibs/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
# Martin Sivak <msivak@redhat.com>
#

import hashlib

import gi
gi.require_version("BlockDev", "2.0")

Expand All @@ -42,6 +44,11 @@
"luks2": BlockDev.CryptoLUKSVersion.LUKS2}
DEFAULT_LUKS_VERSION = "luks2"

DEFAULT_INTEGRITY_ALGORITHM = "crc32c"

# from linux/drivers/md/dm-integrity.c
MAX_JOURNAL_SIZE = 131072 * SECTOR_SIZE


def calculate_luks2_max_memory():
""" Calculates maximum RAM that will be used during LUKS format.
Expand All @@ -64,3 +71,32 @@ def calculate_luks2_max_memory():
# free rounded to multiple of 128 MiB
else:
return free_mem.round_to_nearest(Size("128 MiB"), ROUND_DOWN)


def _integrity_tag_size(hash_alg):
if hash_alg.startswith("crc32"):
return 4

try:
h = hashlib.new(hash_alg)
except ValueError:
log.debug("unknown/unsupported hash '%s' for integrity", hash_alg)
return 4
else:
return h.digest_size


def calculate_integrity_metadata_size(device_size, algorithm=DEFAULT_INTEGRITY_ALGORITHM):
tag_size = _integrity_tag_size(algorithm)
# metadata (checksums) size
msize = Size(device_size * tag_size / (SECTOR_SIZE + tag_size))
msize = (msize / SECTOR_SIZE + 1) * SECTOR_SIZE # round up to sector

# superblock and journal metadata
msize += Size("1 MiB")

# journal size, based on linux/drivers/md/dm-integrity.c
jsize = min(MAX_JOURNAL_SIZE, Size(int(device_size) >> 7))
jsize = (jsize / SECTOR_SIZE + 1) * SECTOR_SIZE # round up to sector

return msize + jsize
35 changes: 33 additions & 2 deletions blivet/devices/luks.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ class IntegrityDevice(DMIntegrityDevice):
""" A mapped integrity device. """
_type = "integrity/dm-crypt"
_resizable = False
_packages = ["cryptsetup"]
_external_dependencies = [availability.BLOCKDEV_CRYPTO_PLUGIN]
_packages = ["integritysetup"]
_external_dependencies = [availability.BLOCKDEV_CRYPTO_PLUGIN_INTEGRITY]

def __init__(self, name, fmt=None, size=None, uuid=None,
exists=False, sysfs_path='', parents=None):
Expand All @@ -200,10 +200,41 @@ def __init__(self, name, fmt=None, size=None, uuid=None,
parents=parents, sysfs_path=sysfs_path,
uuid=None, exists=exists)

@property
def raw_device(self):
return self.parents[0]

@property
def metadata_size(self):
return crypto.calculate_integrity_metadata_size(self.raw_device.size,
self.raw_device.format.algorithm)

def _get_size(self):
if not self.exists:
size = self.raw_device.size - self.metadata_size
else:
size = self._size
return size

def _set_size(self, newsize):
if not self.exists and not self.raw_device.exists:
self.raw_device.size = newsize + self.metadata_size

# just run the StorageDevice._set_size to make sure we are in the format limits
super(IntegrityDevice, self)._set_size(newsize - self.metadata_size)
else:
raise DeviceError("Cannot set size for an existing integrity device")

size = property(_get_size, _set_size)

def _post_teardown(self, recursive=False):
if not recursive:
# we need to propagate the teardown "down" to the parent that
# actually has the LUKS format to close the LUKS device
self.teardown_parents(recursive=recursive)

StorageDevice._post_teardown(self, recursive=recursive)

def _post_create(self):
self.name = self.raw_device.format.map_name
StorageDevice._post_create(self)
4 changes: 4 additions & 0 deletions blivet/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ class LUKSError(DeviceFormatError):
pass


class IntegrityError(LUKSError):
pass


class MDMemberError(DeviceFormatError):
pass

Expand Down
53 changes: 49 additions & 4 deletions blivet/formats/luks.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import os

from ..storage_log import log_method_call
from ..errors import LUKSError
from ..errors import LUKSError, IntegrityError
from ..devicelibs import crypto
from . import DeviceFormat, register_device_format
from ..flags import flags
Expand Down Expand Up @@ -402,19 +402,21 @@ class Integrity(DeviceFormat):
_name = N_("DM Integrity")
_udev_types = ["DM_integrity"]
_supported = False # is supported
_formattable = False # can be formatted
_formattable = True # can be formatted
_linux_native = True # for clearpart
_resizable = False # can be resized
_packages = ["cryptsetup"] # required packages
_plugin = availability.BLOCKDEV_CRYPTO_PLUGIN
_plugin = availability.BLOCKDEV_CRYPTO_PLUGIN_INTEGRITY

def __init__(self, **kwargs):
"""
:keyword device: the path to the underlying device
:keyword uuid: the LUKS UUID
:keyword exists: indicates whether this is an existing format
:type exists: bool
:keyword name: the name of the mapped device
:keyword algorithm: integrity algorithm (HMAC is not supported)
:keyword sector_size: integrity sector size
:type sector_size: int
.. note::
Expand All @@ -428,13 +430,56 @@ def __init__(self, **kwargs):
DeviceFormat.__init__(self, **kwargs)

self.map_name = kwargs.get("name")
self.algorithm = kwargs.get("algorithm", crypto.DEFAULT_INTEGRITY_ALGORITHM)
self.sector_size = kwargs.get("sector_size", 0)

if not self.map_name and self.device:
self.map_name = "integrity-%s" % os.path.basename(self.device)

@property
def formattable(self):
return super(Integrity, self).formattable and self._plugin.available

@property
def status(self):
if not self.exists or not self.map_name:
return False
return os.path.exists("/dev/mapper/%s" % self.map_name)

def _pre_setup(self, **kwargs):
if not self._plugin.available:
raise IntegrityError("Integrity devices not fully supported: %s" % ",".join(self._plugin.availability_errors))

return super(Integrity, self)._pre_setup(**kwargs)

def _setup(self, **kwargs):
log_method_call(self, device=self.device, map_name=self.map_name,
type=self.type, status=self.status)
try:
blockdev.crypto.integrity_open(self.device, self.map_name, self.algorithm)
except blockdev.CryptoError as e:
raise IntegrityError(e)

def _pre_create(self, **kwargs):
if not self.formattable:
raise IntegrityError("Integrity devices not fully supported: %s" % ",".join(self._plugin.availability_errors))

return super(Integrity, self)._pre_create(**kwargs)

def _create(self, **kwargs):
log_method_call(self, device=self.device,
type=self.type, status=self.status)
super(Integrity, self)._create(**kwargs) # set up the event sync

if self.sector_size:
extra = blockdev.CryptoIntegrityExtra(sector_size=self.sector_size) # pylint: disable=c-extension-no-member
else:
extra = None

blockdev.crypto.integrity_format(self.device,
self.algorithm,
extra=extra)

def _teardown(self, **kwargs):
""" Close, or tear down, the format. """
log_method_call(self, device=self.device,
Expand Down
12 changes: 11 additions & 1 deletion blivet/populator/helpers/luks.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,17 @@ def match(cls, data):

def run(self):
parents = self._devicetree._add_parent_devices(self.data)
device = IntegrityDevice(udev.device_get_name(self.data),
name = udev.device_get_name(self.data)

try:
info = blockdev.crypto.integrity_info(name)
# integrity algorithm is not part of the on-disk metadata but for active
# device we can get it from the device mapper device here
parents[0].format.algorithm = info.algorithm
except blockdev.BlockDevError:
log.info("failed to get information about integrity device %s", name)

device = IntegrityDevice(name,
sysfs_path=udev.device_get_sysfs_path(self.data),
parents=parents,
exists=True)
Expand Down
9 changes: 9 additions & 0 deletions blivet/tasks/availability.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,13 @@ def available_resource(name):
blockdev.CryptoTech.ESCROW: blockdev.CryptoTechMode.CREATE})
BLOCKDEV_CRYPTO_TECH = BlockDevMethod(BLOCKDEV_CRYPTO)

BLOCKDEV_CRYPTO_INTEGRITY = BlockDevTechInfo(plugin_name="crypto",
check_fn=blockdev.crypto_is_tech_avail,
technologies={blockdev.CryptoTech.INTEGRITY: (blockdev.CryptoTechMode.CREATE |
blockdev.CryptoTechMode.OPEN_CLOSE |
blockdev.CryptoTechMode.QUERY)})
BLOCKDEV_CRYPTO_TECH_INTEGRITY = BlockDevMethod(BLOCKDEV_CRYPTO_INTEGRITY)

# libblockdev dm plugin required technologies and modes
BLOCKDEV_DM_ALL_MODES = (blockdev.DMTechMode.CREATE_ACTIVATE |
blockdev.DMTechMode.REMOVE_DEACTIVATE |
Expand Down Expand Up @@ -419,6 +426,8 @@ def available_resource(name):
# due to missing dependencies)
BLOCKDEV_BTRFS_PLUGIN = blockdev_plugin("libblockdev btrfs plugin", BLOCKDEV_BTRFS_TECH)
BLOCKDEV_CRYPTO_PLUGIN = blockdev_plugin("libblockdev crypto plugin", BLOCKDEV_CRYPTO_TECH)
BLOCKDEV_CRYPTO_PLUGIN_INTEGRITY = blockdev_plugin("libblockdev crypto plugin (integrity technology)",
BLOCKDEV_CRYPTO_TECH_INTEGRITY)
BLOCKDEV_DM_PLUGIN = blockdev_plugin("libblockdev dm plugin", BLOCKDEV_DM_TECH)
BLOCKDEV_DM_PLUGIN_RAID = blockdev_plugin("libblockdev dm plugin (raid technology)", BLOCKDEV_DM_TECH_RAID)
BLOCKDEV_LOOP_PLUGIN = blockdev_plugin("libblockdev loop plugin", BLOCKDEV_LOOP_TECH)
Expand Down
26 changes: 24 additions & 2 deletions tests/formats_test/luks_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
except ImportError:
from mock import patch

import os
import unittest

from blivet.formats.luks import LUKS

from blivet.formats.luks import LUKS, Integrity
from blivet.devicelibs import crypto
from blivet.size import Size

from . import loopbackedtestcase
Expand Down Expand Up @@ -125,3 +126,24 @@ def test_sector_size(self):

fmt = LUKS(luks_version="luks2", luks_sector_size=4096)
self.assertEqual(fmt.luks_sector_size, 4096)


@unittest.skipUnless(Integrity._plugin.available, "Integrity support not available")
class IntegrityTestCase(loopbackedtestcase.LoopBackedTestCase):

def __init__(self, methodName='run_test'):
super(IntegrityTestCase, self).__init__(methodName=methodName, device_spec=[Size("100 MiB")])

def test_integrity(self):
fmt = Integrity(device=self.loop_devices[0])
self.assertEqual(fmt.algorithm, crypto.DEFAULT_INTEGRITY_ALGORITHM)

# create and open the integrity format
fmt.create()
fmt.setup()
self.assertTrue(fmt.status)
self.assertTrue(os.path.exists("/dev/mapper/%s" % fmt.map_name))

fmt.teardown()
self.assertFalse(fmt.status)
self.assertFalse(os.path.exists("/dev/mapper/%s" % fmt.map_name))

0 comments on commit 04276d5

Please sign in to comment.