diff --git a/build-tests/x86/tumbleweed/test-image-luks/appliance.kiwi b/build-tests/x86/tumbleweed/test-image-luks/appliance.kiwi index 81b74f2e06c..fea320b66d7 100644 --- a/build-tests/x86/tumbleweed/test-image-luks/appliance.kiwi +++ b/build-tests/x86/tumbleweed/test-image-luks/appliance.kiwi @@ -1,11 +1,17 @@ - + Marcus Schäfer ms@suse.com Disk full encryption disk test build + + + + + + 1.15.1 zypper @@ -16,13 +22,51 @@ false breeze openSUSE + + - false + true + + + + + + + + + + true + + + + + + + + + + true + + + + + + + + + + true @@ -61,6 +105,7 @@ + diff --git a/build-tests/x86/tumbleweed/test-image-luks/config.sh b/build-tests/x86/tumbleweed/test-image-luks/config.sh index 314e0fbb35d..60a0466beb7 100644 --- a/build-tests/x86/tumbleweed/test-image-luks/config.sh +++ b/build-tests/x86/tumbleweed/test-image-luks/config.sh @@ -15,11 +15,14 @@ # : # STATUS : BETA #---------------- +declare kiwi_iname=${kiwi_iname} +declare kiwi_profiles=${kiwi_profiles} + #====================================== # Functions... #-------------------------------------- +# shellcheck disable=SC1091 test -f /.kconfig && . /.kconfig -test -f /.profile && . /.profile #====================================== # Greeting... @@ -46,3 +49,32 @@ baseSetRunlevel 3 #------------------------------------------ rm -rf /usr/share/doc/packages/* rm -rf /usr/share/doc/manual/* + + +# For image tests with an extra boot partition the +# kernel must not be a symlink to another area of +# the filesystem. Latest changes on SUSE changed the +# layout of the kernel which breaks every image with +# an extra boot partition +# +# All of the following is more than a hack and I +# don't like it all +# +# Complains and discussions about this please with +# the SUSE kernel team as we in kiwi can just live +# with the consequences of this change +# +for profile in ${kiwi_profiles//,/ }; do + if [ "${profile}" = "ReEncryptExtraBootEmptyPass" ] || [ "${profile}" = "ReEncryptExtraBootWithPass" ]; then + pushd / + + for file in /boot/* /boot/.*; do + if [ -L "${file}" ];then + link_target=$(readlink "${file}") + if [[ "${link_target}" =~ usr/lib/modules ]];then + mv "${link_target}" "${file}" + fi + fi + done + fi +done diff --git a/doc/source/concept_and_workflow/customize_the_boot_process.rst b/doc/source/concept_and_workflow/customize_the_boot_process.rst index 1c838c70582..1633b8f5f37 100644 --- a/doc/source/concept_and_workflow/customize_the_boot_process.rst +++ b/doc/source/concept_and_workflow/customize_the_boot_process.rst @@ -209,6 +209,18 @@ the available kernel boot parameters for these modules: Note that options starting with `rd.kiwi` are not passed to avoid side effects. +``rd.kiwi.oem.luks.reencrypt`` + For OEM LUKS2 encrypted disk images. If set, reencrypts the disk + prior an eventual resize and therefore creates a new key pool and + master key. The reencryption is advisable if the image binary is + not protected. With access to the image binary it's possible to + extract the luks header which then allows to decrypt the data + unless it was reencrypted. The reencryption process only runs if + the checksum of the luks header still matches the one from the + original disk image. Be aware that the reencryption will ask + for the passphrase if the image has been built with an initial + luks passphrase. + ``rd.kiwi.oem.maxdisk=size[KMGT]`` Specifies the maximum disk size an unattended OEM installation uses for image deployment. Unattended OEM deployments default to deploying on `/dev/sda` (or diff --git a/dracut/modules.d/90kiwi-repart/kiwi-repart-disk.sh b/dracut/modules.d/90kiwi-repart/kiwi-repart-disk.sh index d573f981016..93ff56fbba9 100755 --- a/dracut/modules.d/90kiwi-repart/kiwi-repart-disk.sh +++ b/dracut/modules.d/90kiwi-repart/kiwi-repart-disk.sh @@ -24,6 +24,14 @@ function initialize { test -f ${profile} && import_file ${profile} import_file ${partition_ids} + # Optional env TERM setup + term=$(getarg "rd.kiwi.term=") + [ -n "${term}" ] && export TERM="${term}" + + # Create dialog profile from current env + # Used in the systemd dialog unit + env >/dialog_profile + disk=$(lookup_disk_device_from_root) export disk @@ -248,6 +256,16 @@ mask_fsck_root_service # initialize for disk repartition initialize +# reencrypt luks device +if luks_system "${disk}";then + if getargbool 0 rd.kiwi.oem.luks.reencrypt; then + reencrypt_luks "${disk}" + fi +fi + +# wait for the root device to appear +wait_for_storage_device "${root_device}" + # check if repart/resize is wanted if ! resize_wanted "${root_device}" "${disk}"; then return @@ -258,9 +276,6 @@ if [ "$(get_partition_table_type "${disk}")" = 'gpt' ];then relocate_gpt_at_end_of_disk "${disk}" fi -# wait for the root device to appear -wait_for_storage_device "${root_device}" - # resize disk partition table if lvm_system;then repart_lvm_disk || return diff --git a/dracut/modules.d/90kiwi-repart/module-setup.sh b/dracut/modules.d/90kiwi-repart/module-setup.sh index 5707ee03729..91284edff39 100755 --- a/dracut/modules.d/90kiwi-repart/module-setup.sh +++ b/dracut/modules.d/90kiwi-repart/module-setup.sh @@ -19,6 +19,8 @@ installkernel() { # called by dracut install() { declare moddir=${moddir} + inst_multiple \ + sha256sum sed if inst_simple /config.partids; then test -f /.profile && inst_simple /.profile inst_hook pre-mount 20 "${moddir}/kiwi-repart-disk.sh" diff --git a/dracut/modules.d/99kiwi-lib/kiwi-dialog-lib.sh b/dracut/modules.d/99kiwi-lib/kiwi-dialog-lib.sh index 617855a8e1e..f343e68094a 100644 --- a/dracut/modules.d/99kiwi-lib/kiwi-dialog-lib.sh +++ b/dracut/modules.d/99kiwi-lib/kiwi-dialog-lib.sh @@ -165,3 +165,8 @@ function ask_and_shutdown { systemctl halt fi } + +function ask_for_credentials { + local text_message="$1" + run_dialog --insecure --passwordbox "\"${text_message}\"" 7 60 +} diff --git a/dracut/modules.d/99kiwi-lib/kiwi-luks-lib.sh b/dracut/modules.d/99kiwi-lib/kiwi-luks-lib.sh index 79801c30b4c..9ac1cf24bd5 100644 --- a/dracut/modules.d/99kiwi-lib/kiwi-luks-lib.sh +++ b/dracut/modules.d/99kiwi-lib/kiwi-luks-lib.sh @@ -1,9 +1,11 @@ #!/bin/bash +# shellcheck disable=SC1091 type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh type set_root_map >/dev/null 2>&1 || . /lib/kiwi-lib.sh type wait_for_storage_device >/dev/null 2>&1 || . /lib/kiwi-partitions-lib.sh type ask_for_password >/dev/null 2>&1 || . /lib/dracut-crypt-lib.sh +type run_progress_dialog >/dev/null 2>&1 || . /lib/kiwi-dialog-lib.sh function luks_system { declare kiwi_RootPart=${kiwi_RootPart} @@ -20,6 +22,60 @@ function deactivate_luks { /usr/lib/systemd/systemd-cryptsetup detach luks } +function reencrypt_luks { + declare kiwi_RootPart=${kiwi_RootPart} + local disk=$1 + local header_checksum_origin=/root/.luks.header + local header_checksum_cur=/root/.luks.header.cur + local keyfile=/root/.root.keyfile + local passphrase_file=/root/.slot0 + local progress=/dev/install_progress + local load_text="Reencrypting..." + local title_text="LUKS" + local device + device=$(get_partition_node_name "${disk}" "${kiwi_RootPart}") + read -r header_checksum_origin < "${header_checksum_origin}" + if [ "${kiwi_luks_empty_passphrase}" = "true" ];then + cryptsetup \ + --key-file /dev/zero \ + --keyfile-size 32 \ + luksHeaderBackup "${device}" \ + --header-backup-file "${header_checksum_cur}" + else + cryptsetup \ + --key-file "${keyfile}" \ + luksHeaderBackup "${device}" \ + --header-backup-file "${header_checksum_cur}" + fi + header_checksum_cur=$( + sha256sum "${header_checksum_cur}" |\ + cut -f1 -d" "; rm -f "${header_checksum_cur}" + ) + if [ "${header_checksum_origin}" == "${header_checksum_cur}" ];then + if [ "${kiwi_luks_empty_passphrase}" = "true" ];then + echo -n > "${passphrase_file}" + else + ask_for_credentials "Enter Credentials for Key Slot(0)" + get_dialog_result > "${passphrase_file}" + fi + setup_progress_fifo ${progress} + ( + # reencrypt slot0, this will wipe all key slots + cryptsetup reencrypt \ + --progress-frequency 1 \ + --key-file "${passphrase_file}" \ + --key-slot 0 \ + "${device}" 2>&1 | sed -u 's/.* \([0-9]*\)[0-9.]*%.*/\1/' + ) >"${progress}" & + run_progress_dialog "${load_text}" "${title_text}" + if [ -e "${keyfile}" ];then + # re-add keyfile if present + cryptsetup --key-file "${passphrase_file}" luksAddKey \ + "${device}" "${keyfile}" + fi + fi +} + function resize_luks { cryptsetup resize luks } diff --git a/kiwi/boot/image/dracut.py b/kiwi/boot/image/dracut.py index 9aa4f436459..32bc28c5be2 100644 --- a/kiwi/boot/image/dracut.py +++ b/kiwi/boot/image/dracut.py @@ -61,12 +61,16 @@ def post_init(self) -> None: # Initialize empty list of dracut caller options self.dracut_options: List[str] = [] self.included_files: List[str] = [] + self.delete_after_include_files: List[str] = [] self.modules: List[str] = [] self.add_modules: List[str] = [] self.omit_modules: List[str] = [] self.available_modules = self._get_modules() - def include_file(self, filename: str, install_media: bool = False) -> None: + def include_file( + self, filename: str, install_media: bool = False, + delete_after_include: bool = False + ) -> None: """ Include file to dracut boot image @@ -75,6 +79,8 @@ def include_file(self, filename: str, install_media: bool = False) -> None: """ self.included_files.append('--install') self.included_files.append(filename) + if delete_after_include: + self.delete_after_include_files.append(filename) def include_module(self, module: str, install_media: bool = False) -> None: """ @@ -238,6 +244,8 @@ def create_initrd( Command.run( ['chmod', '644', self.initrd_filename] ) + for filename in self.delete_after_include_files: + os.unlink(f'{self.boot_root_directory}/{filename}') def _get_modules(self) -> List[str]: cmd = Command.run( diff --git a/kiwi/builder/disk.py b/kiwi/builder/disk.py index e2ce950fe5b..914c6544059 100644 --- a/kiwi/builder/disk.py +++ b/kiwi/builder/disk.py @@ -782,6 +782,8 @@ def _build_main_system( self._write_crypttab_to_system_image(luks_root) + self._write_luks_header_checksum_to_boot_image(luks_root) + self._write_integritytab_to_system_image(integrity_root) self._write_generic_fstab_to_system_image(device_map, system) @@ -1271,6 +1273,20 @@ def _write_crypttab_to_system_image( os.sep + os.sep.join(['etc', os.path.basename(filename)]) ) + def _write_luks_header_checksum_to_boot_image( + self, luks_root: Optional[LuksDevice] + ) -> None: + if luks_root is not None: + log.info('Including origin LUKS header checksum') + filename = ''.join( + [self.root_dir, '/root/.luks.header'] + ) + self.boot_image.include_file( + filename=os.sep + os.sep.join( + ['root', os.path.basename(filename)] + ), delete_after_include=True + ) + def _write_generic_fstab_to_system_image( self, device_map: Dict, system: Optional[Union[FileSystemBase, VolumeManagerBase]] diff --git a/kiwi/storage/luks_device.py b/kiwi/storage/luks_device.py index ab72b7d9a87..44a2adec697 100644 --- a/kiwi/storage/luks_device.py +++ b/kiwi/storage/luks_device.py @@ -21,6 +21,8 @@ # project from kiwi.utils.temporary import Temporary +from kiwi.utils.checksum import Checksum +from kiwi.path import Path from kiwi.command import Command from kiwi.defaults import Defaults from kiwi.storage.device_provider import DeviceProvider @@ -172,6 +174,23 @@ def create_crypto_luks( 'luksAddKey', storage_device, keyfile_path ] ) + + # Create backup header checksum as reencryption reference + master_checksum = f'{root_dir}/root/.luks.header' + Path.wipe(master_checksum) + Command.run( + [ + 'cryptsetup', '--key-file', passphrase_file + ] + extra_options + [ + 'luksHeaderBackup', storage_device, + '--header-backup-file', master_checksum + ] + ) + checksum = Checksum(master_checksum).sha256() + with open(master_checksum, 'w') as shasum: + shasum.write(checksum) + + # open the pool Command.run( [ 'cryptsetup', '--key-file', passphrase_file diff --git a/test/unit/boot/image/dracut_test.py b/test/unit/boot/image/dracut_test.py index 096b2ce7779..5113974c817 100644 --- a/test/unit/boot/image/dracut_test.py +++ b/test/unit/boot/image/dracut_test.py @@ -47,10 +47,13 @@ def test_prepare(self, mock_setup): ] def test_include_file(self): - self.boot_image.include_file('foo') + self.boot_image.include_file(filename='foo', delete_after_include=True) assert self.boot_image.included_files == [ '--install', 'foo' ] + assert self.boot_image.delete_after_include_files == [ + 'foo' + ] def test_include_module(self): self.boot_image.include_module('foobar') @@ -112,9 +115,10 @@ def test_include_file_install(self): @patch('kiwi.boot.image.base.BootImageBase.is_prepared') @patch('kiwi.boot.image.dracut.Profile') @patch('kiwi.boot.image.dracut.MountManager') + @patch('os.unlink') def test_create_initrd( - self, mock_MountManager, mock_Profile, mock_prepared, - mock_command, mock_kernel + self, mock_os_unlink, mock_MountManager, mock_Profile, + mock_prepared, mock_command, mock_kernel ): profile = Mock() profile.dot_profile = dict() @@ -124,7 +128,9 @@ def test_create_initrd( kernel_details.version = '1.2.3' kernel.get_kernel = Mock(return_value=kernel_details) mock_kernel.return_value = kernel - self.boot_image.include_file('system-directory/etc/foo') + self.boot_image.include_file( + filename='system-directory/etc/foo', delete_after_include=True + ) self.boot_image.include_module('foo') self.boot_image.omit_module('bar') self.boot_image.create_initrd() @@ -157,6 +163,9 @@ def test_create_initrd( ['chmod', '644', 'some-target-dir/LimeJeOS.x86_64-1.13.2.initrd'] ) ] + mock_os_unlink.assert_called_once_with( + 'system-directory/system-directory/etc/foo' + ) mock_command.reset_mock() self.boot_image.create_initrd(basename='foo') assert mock_command.call_args_list == [ diff --git a/test/unit/builder/disk_test.py b/test/unit/builder/disk_test.py index ff27723599e..174de7c861d 100644 --- a/test/unit/builder/disk_test.py +++ b/test/unit/builder/disk_test.py @@ -1195,7 +1195,8 @@ def test_create_disk_luks_root( assert self.boot_image_task.include_file.call_args_list == [ call('/root/.root.keyfile'), call('/config.partids'), - call('/etc/crypttab') + call('/etc/crypttab'), + call(filename='/root/.luks.header', delete_after_include=True) ] self.boot_image_task.write_system_config_file.assert_called_once_with( config={'install_items': ['/root/.root.keyfile']}, @@ -1249,7 +1250,8 @@ def test_create_disk_luks_root_with_disk_password( assert self.boot_image_task.include_file.call_args_list == [ call('/root/.root.keyfile'), call('/config.partids'), - call('/etc/crypttab') + call('/etc/crypttab'), + call(filename='/root/.luks.header', delete_after_include=True) ] self.boot_image_task.write_system_config_file.assert_called_once_with( config={'install_items': ['/root/.root.keyfile']}, diff --git a/test/unit/storage/luks_device_test.py b/test/unit/storage/luks_device_test.py index 69c41c6f423..e2f7dd529ff 100644 --- a/test/unit/storage/luks_device_test.py +++ b/test/unit/storage/luks_device_test.py @@ -53,9 +53,10 @@ def test_get_device_none(self, mock_path): assert self.luks.get_device() is None @patch('kiwi.storage.luks_device.Command.run') + @patch('kiwi.storage.luks_device.Checksum') @patch('os.chmod') def test_create_crypto_luks_empty_passphrase( - self, mock_os_chmod, mock_command + self, mock_os_chmod, mock_Checksum, mock_command ): with patch('builtins.open', create=True): self.luks.create_crypto_luks( @@ -85,6 +86,14 @@ def test_create_crypto_luks_empty_passphrase( 'luksAddKey', '/dev/some-device', 'root/some-keyfile' ] ), + call( + [ + 'cryptsetup', '--key-file', '/dev/zero', + '--keyfile-size', '32', + 'luksHeaderBackup', '/dev/some-device', + '--header-backup-file', 'root/root/.luks.header' + ] + ), call( [ 'cryptsetup', '--key-file', '/dev/zero', @@ -93,14 +102,17 @@ def test_create_crypto_luks_empty_passphrase( ] ) ] + mock_Checksum.return_value.sha256.assert_called_once_with() self.luks.luks_device = None @patch('kiwi.storage.luks_device.LuksDevice') @patch('kiwi.storage.luks_device.Command.run') @patch('kiwi.storage.luks_device.Temporary.new_file') + @patch('kiwi.storage.luks_device.Checksum') @patch('os.chmod') def test_create_crypto_luks_random_passphrase( - self, mock_os_chmod, mock_tmpfile, mock_command, mock_LuksDevice + self, mock_os_chmod, mock_Checksum, mock_tmpfile, + mock_command, mock_LuksDevice ): tmpfile = Mock() tmpfile.name = 'tmpfile' @@ -125,6 +137,13 @@ def test_create_crypto_luks_random_passphrase( 'luksFormat', '/dev/some-device' ] ), + call( + [ + 'cryptsetup', '--key-file', 'root/some-keyfile', + 'luksHeaderBackup', '/dev/some-device', + '--header-backup-file', 'root/root/.luks.header' + ] + ), call( [ 'cryptsetup', '--key-file', 'root/some-keyfile', 'luksOpen', @@ -132,6 +151,7 @@ def test_create_crypto_luks_random_passphrase( ] ) ] + mock_Checksum.return_value.sha256.assert_called_once_with() mock_LuksDevice.create_random_keyfile.assert_called_once_with( 'root/some-keyfile' ) @@ -141,9 +161,11 @@ def test_create_crypto_luks_random_passphrase( @patch('kiwi.storage.luks_device.LuksDevice') @patch('kiwi.storage.luks_device.Command.run') @patch('kiwi.storage.luks_device.Temporary.new_file') + @patch('kiwi.storage.luks_device.Checksum') @patch('os.chmod') def test_create_crypto_luks( - self, mock_os_chmod, mock_tmpfile, mock_command, mock_LuksDevice + self, mock_os_chmod, mock_Checksum, mock_tmpfile, + mock_command, mock_LuksDevice ): tmpfile = Mock() tmpfile.name = 'tmpfile' @@ -174,6 +196,13 @@ def test_create_crypto_luks( '/dev/some-device', 'root/some-keyfile' ] ), + call( + [ + 'cryptsetup', '--key-file', 'tmpfile', + 'luksHeaderBackup', '/dev/some-device', + '--header-backup-file', 'root/root/.luks.header' + ] + ), call( [ 'cryptsetup', '--key-file', 'tmpfile', 'luksOpen', @@ -181,6 +210,7 @@ def test_create_crypto_luks( ] ) ] + mock_Checksum.return_value.sha256.assert_called_once_with() mock_LuksDevice.create_random_keyfile.assert_called_once_with( 'root/some-keyfile' )