← Back to team overview

curtin-dev team mailing list archive

[Merge] ~dbungert/curtin:cryptoswap into curtin:master

 

Dan Bungert has proposed merging ~dbungert/curtin:cryptoswap into curtin:master.

Commit message:
Add features to the dm_crypt action to support cryptoswap

Requested reviews:
  curtin developers (curtin-dev)

For more details, see:
https://code.launchpad.net/~dbungert/curtin/+git/curtin/+merge/458460
-- 
Your team curtin developers is requested to review the proposed merge of ~dbungert/curtin:cryptoswap into curtin:master.
diff --git a/curtin/commands/block_meta.py b/curtin/commands/block_meta.py
index ebae27c..8ba7a55 100644
--- a/curtin/commands/block_meta.py
+++ b/curtin/commands/block_meta.py
@@ -1618,6 +1618,8 @@ def dm_crypt_handler(info, storage_config, context):
     keysize = info.get('keysize')
     cipher = info.get('cipher')
     dm_name = info.get('dm_name')
+    options = ','.join(info.get('options', ['luks']))
+    crypttab_keyfile = 'none'
     if not dm_name:
         dm_name = info.get('id')
     dmcrypt_dev = os.path.join("/dev", "mapper", dm_name)
@@ -1634,8 +1636,13 @@ def dm_crypt_handler(info, storage_config, context):
     if 'keyfile' in info:
         if 'key' in info:
             raise ValueError("cannot specify both key and keyfile")
-        keyfile_is_tmp = False
         keyfile = info['keyfile']
+        if keyfile in ("/dev/random", "/dev/urandom"):
+            crypttab_keyfile = keyfile
+            keyfile = tempfile.mkstemp()[1]
+            keyfile_is_tmp = True
+        else:
+            keyfile_is_tmp = False
     elif 'key' in info:
         # TODO: this is insecure, find better way to do this
         key = info.get('key')
@@ -1727,8 +1734,9 @@ def dm_crypt_handler(info, storage_config, context):
         state_dir = os.path.dirname(state['fstab'])
         crypt_tab_location = os.path.join(state_dir, "crypttab")
         uuid = block.get_volume_uuid(volume_path)
-        util.write_file(crypt_tab_location,
-                        "%s UUID=%s none luks\n" % (dm_name, uuid), omode="a")
+        util.write_file(
+            crypt_tab_location,
+            f"{dm_name} UUID={uuid} {crypttab_keyfile} {options}\n", omode="a")
     else:
         LOG.info("fstab configuration is not present in environment, so \
             cannot locate an appropriate directory to write crypttab in \
diff --git a/doc/topics/storage.rst b/doc/topics/storage.rst
index 45b9bbe..30e52aa 100644
--- a/doc/topics/storage.rst
+++ b/doc/topics/storage.rst
@@ -886,6 +886,15 @@ system will prompt for this password in order to mount the disk.
 The ``keyfile`` contains the password of the encryption key.  The target
 system will prompt for this password in order to mount the disk.
 
+A special case of ``keyfile`` are the values ``/dev/urandom`` and
+``/dev/random``, which indicate that this ``keyfile`` value will be used in
+enterity to decrypt the device.  In this case, the keyfile is passed along to
+the crypttab, as the third field.
+
+.. note::
+  ``/dev/urandom`` and ``/dev/random`` are functionally equivalent on modern
+  systems.
+
 Exactly one of **key** and **keyfile** must be supplied.
 
 **preserve**: *true, false*
@@ -893,13 +902,16 @@ Exactly one of **key** and **keyfile** must be supplied.
 If the ``preserve`` option is True, curtin will verify the dm-crypt device
 specified is composed of the device specified in ``volume``.
 
-
 **wipe**: *superblock, superblock-recursive, pvremove, zero, random*
 
 If ``wipe`` option is set, and ``preserve`` is False, curtin will wipe the
 contents of the dm-crypt device.  Curtin skips wipe settings if it creates
 the dm-crypt volume.
 
+**options**: *<list of man 5 options strings>*
+
+Options to pass to crypttab, as the fourth field.  See man 5 crypttab for
+details. The default is ``luks``.
 
 .. note::
 
diff --git a/tests/integration/test_block_meta.py b/tests/integration/test_block_meta.py
index acd12e1..d7dcc87 100644
--- a/tests/integration/test_block_meta.py
+++ b/tests/integration/test_block_meta.py
@@ -6,6 +6,7 @@ import contextlib
 import json
 import os
 from parameterized import parameterized
+from pathlib import Path
 import re
 import sys
 from typing import Optional
@@ -192,6 +193,15 @@ class StorageConfigBuilder:
         for action in self.config:
             action['preserve'] = True
 
+    def add_dmcrypt(self, *, volume, dm_name=None, **kw):
+        if dm_name is None:
+            dm_name = CiTestCase.random_string()
+        return self._add(
+            type='dm_crypt',
+            volume=volume['id'],
+            dm_name=dm_name,
+            **kw)
+
 
 class TestBlockMeta(IntegrationTestCase):
     def setUp(self):
@@ -239,15 +249,17 @@ class TestBlockMeta(IntegrationTestCase):
         with open(config_path, 'w') as fp:
             yaml.dump(config, fp)
 
+        self.fstab_dir = self.tmp_dir()
         cmd_env = kwargs.pop('env', {})
         cmd_env.update({
             'PATH': os.environ['PATH'],
             'CONFIG': config_path,
             'WORKING_DIR': '/tmp',
-            'OUTPUT_FSTAB': self.tmp_path('fstab'),
+            'OUTPUT_FSTAB': self.tmp_path('fstab', _dir=self.fstab_dir),
             'OUTPUT_INTERFACES': '',
             'OUTPUT_NETWORK_STATE': '',
             'OUTPUT_NETWORK_CONFIG': '',
+            'TARGET_MOUNT_POINT': self.tmp_dir(),
         })
 
         cmd = [
@@ -1285,6 +1297,38 @@ table-length: 256'''.encode()
                      partition_type='82'))
 
     @parameterized.expand(((1,), (2,)))
+    def test_cryptoswap(self, sv=2):
+        self.img = self.tmp_path('image.img')
+        config = StorageConfigBuilder(version=sv)
+        config.add_image(path=self.img, create=True, size='200M',
+                         ptable='msdos')
+        p1 = config.add_part(
+            number=1, offset=1 << 20, size=19 << 20, flag='swap'
+        )
+        cryptoswap = f"cryptoswap-{self.random_string(length=6)}"
+        dmc1 = config.add_dmcrypt(
+            volume=p1,
+            dm_name=cryptoswap,
+            keyfile="/dev/urandom",
+            options=["swap", "initramfs"],
+        )
+        config.add_format(part=dmc1, fstype="swap")
+        self.run_bm(config.render())
+
+        self.assertPartitions(
+            PartData(number=1, offset=1 << 20, size=19 << 20, boot=False,
+                     partition_type='82'))
+
+        crypttab_path = Path(self.fstab_dir) / "crypttab"
+        with open(crypttab_path) as fp:
+            crypttab = fp.read()
+        tokens = re.split(r'\s', crypttab)
+        self.assertEqual(cryptoswap, tokens[0])
+        self.assertTrue(tokens[1].startswith("UUID="))
+        self.assertEqual("/dev/urandom", tokens[2])
+        self.assertEqual("swap,initramfs", tokens[3])
+
+    @parameterized.expand(((1,), (2,)))
     def test_msftres(self, sv):
         self.img = self.tmp_path('image.img')
         config = StorageConfigBuilder(version=sv)
diff --git a/tests/unittests/test_commands_block_meta.py b/tests/unittests/test_commands_block_meta.py
index 9d7d0f3..5adb46c 100644
--- a/tests/unittests/test_commands_block_meta.py
+++ b/tests/unittests/test_commands_block_meta.py
@@ -2030,10 +2030,10 @@ class TestLvmPartitionHandler(CiTestCase):
         self.assertEqual(0, self.m_subp.call_count)
 
 
-class TestDmCryptHandler(CiTestCase):
+class DmCryptCommon(CiTestCase):
 
     def setUp(self):
-        super(TestDmCryptHandler, self).setUp()
+        super().setUp()
 
         basepath = 'curtin.commands.block_meta.'
         self.add_patch(basepath + 'get_path_to_storage_volume', 'm_getpath')
@@ -2047,7 +2047,26 @@ class TestDmCryptHandler(CiTestCase):
         self.keyfile = self.random_string()
         self.cipher = self.random_string()
         self.keysize = self.random_string()
-        self.config = {
+        self.m_block.zkey_supported.return_value = False
+        self.block_uuids = [random_uuid() for unused in range(2)]
+        self.m_block.get_volume_uuid.side_effect = self.block_uuids
+
+        self.m_which.return_value = False
+        self.fstab = self.tmp_path('fstab')
+        self.crypttab = os.path.join(os.path.dirname(self.fstab), 'crypttab')
+        self.m_load_env.return_value = {'fstab': self.fstab,
+                                        'target': self.target}
+
+    def setUpStorageConfig(self, config):
+        self.config = config
+        self.storage_config = block_meta.extract_storage_ordered_dict(config)
+
+
+class TestDmCryptHandler(DmCryptCommon):
+
+    def setUp(self):
+        super().setUp()
+        self.setUpStorageConfig({
             'storage': {
                 'version': 1,
                 'config': [
@@ -2073,15 +2092,7 @@ class TestDmCryptHandler(CiTestCase):
                      'keyfile': self.keyfile},
                 ],
             }
-        }
-        self.storage_config = (
-            block_meta.extract_storage_ordered_dict(self.config))
-        self.m_block.zkey_supported.return_value = False
-        self.m_which.return_value = False
-        self.fstab = self.tmp_path('fstab')
-        self.crypttab = os.path.join(os.path.dirname(self.fstab), 'crypttab')
-        self.m_load_env.return_value = {'fstab': self.fstab,
-                                        'target': self.target}
+        })
 
     def test_dm_crypt_calls_cryptsetup(self):
         """ verify dm_crypt calls (format, open) w/ correct params"""
@@ -2402,6 +2413,109 @@ class TestDmCryptHandler(CiTestCase):
                 info, self.storage_config, empty_context)
 
 
+class TestCrypttab(DmCryptCommon):
+
+    def test_multi_dm_crypt(self):
+        """ verify that multiple dm_crypt calls result in the data for both
+        being present in crypttab"""
+
+        self.setUpStorageConfig({
+            'storage': {
+                'version': 1,
+                'config': [
+                    {'grub_device': True,
+                     'id': 'sda',
+                     'name': 'sda',
+                     'path': '/wark/xxx',
+                     'ptable': 'msdos',
+                     'type': 'disk',
+                     'wipe': 'superblock'},
+                    {'device': 'sda',
+                     'id': 'sda-part1',
+                     'name': 'sda-part1',
+                     'number': 1,
+                     'size': '511705088B',
+                     'type': 'partition'},
+                    {'id': 'dmcrypt0',
+                     'type': 'dm_crypt',
+                     'dm_name': 'cryptroot',
+                     'volume': 'sda-part1',
+                     'cipher': self.cipher,
+                     'keysize': self.keysize,
+                     'keyfile': self.keyfile},
+                    {'device': 'sda',
+                     'id': 'sda-part2',
+                     'name': 'sda-part2',
+                     'number': 2,
+                     'size': '511705088B',
+                     'type': 'partition'},
+                    {'id': 'dmcrypt1',
+                     'type': 'dm_crypt',
+                     'dm_name': 'cryptfoo',
+                     'volume': 'sda-part2',
+                     'cipher': self.cipher,
+                     'keysize': self.keysize,
+                     'keyfile': self.keyfile},
+                ],
+            }
+        })
+
+        for i in range(2):
+            self.m_getpath.return_value = self.random_string()
+            info = self.storage_config['dmcrypt' + str(i)]
+            block_meta.dm_crypt_handler(
+                info, self.storage_config, empty_context
+            )
+
+        data = util.load_file(self.crypttab)
+        self.assertEqual(
+            f"cryptroot UUID={self.block_uuids[0]} none luks\n"
+            f"cryptfoo UUID={self.block_uuids[1]} none luks\n",
+            data
+        )
+
+    def test_cryptoswap(self):
+        """ Check the keyfile and options features needed for cryptoswap """
+
+        self.setUpStorageConfig({
+            'storage': {
+                'version': 1,
+                'config': [
+                    {'grub_device': True,
+                     'id': 'sda',
+                     'name': 'sda',
+                     'path': '/wark/xxx',
+                     'ptable': 'msdos',
+                     'type': 'disk',
+                     'wipe': 'superblock'},
+                    {'device': 'sda',
+                     'id': 'sda-part1',
+                     'name': 'sda-part1',
+                     'number': 1,
+                     'size': '511705088B',
+                     'type': 'partition'},
+                    {'id': 'dmcrypt0',
+                     'type': 'dm_crypt',
+                     'dm_name': 'cryptswap',
+                     'volume': 'sda-part1',
+                     'options': ["swap", "initramfs"],
+                     'keyfile': "/dev/urandom"},
+                ],
+            }
+        })
+
+        self.m_getpath.return_value = self.random_string()
+        block_meta.dm_crypt_handler(
+            self.storage_config['dmcrypt0'], self.storage_config, empty_context
+        )
+
+        uuid = self.block_uuids[0]
+        self.assertEqual(
+            f"cryptswap UUID={uuid} /dev/urandom swap,initramfs\n",
+            util.load_file(self.crypttab),
+        )
+
+
 class TestRaidHandler(CiTestCase):
 
     def setUp(self):
diff --git a/tools/vmtest-system-setup b/tools/vmtest-system-setup
index 5172741..2f82464 100755
--- a/tools/vmtest-system-setup
+++ b/tools/vmtest-system-setup
@@ -14,6 +14,7 @@ esac
 DEPS=(
   build-essential
   cloud-image-utils
+  cryptsetup
   git
   make
   net-tools

Follow ups