Skip to content

Commit

Permalink
Added LUKS reencryption support
Browse files Browse the repository at this point in the history
Added rd.kiwi.oem.luks.reencrypt boot option consumed by the
kiwi-repart dracut module. 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.
  • Loading branch information
schaefi committed Jan 9, 2025
1 parent 6a10983 commit 868408a
Show file tree
Hide file tree
Showing 13 changed files with 267 additions and 16 deletions.
49 changes: 47 additions & 2 deletions build-tests/x86/tumbleweed/test-image-luks/appliance.kiwi
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>

<!-- OBS-Profiles: @BUILD_FLAVOR@ -->
<image schemaversion="7.5" name="kiwi-test-image-luks">
<description type="system">
<author>Marcus Schäfer</author>
<contact>[email protected]</contact>
<specification>Disk full encryption disk test build</specification>
</description>
<profiles>
<profile name="Insecure" description="Encrypted no reencryption"/>
<profile name="ReEncryptExtraBootEmptyPass" description="Run reencryption with extra boot partition and empty passphrase"/>
<profile name="ReEncryptExtraBootWithPass" description="Run reencryption with extra boot partition and passphrase"/>
<profile name="ReEncryptFullDisk" description="Run full disk reencryption with passphrase"/>
</profiles>
<preferences>
<version>1.15.1</version>
<packagemanager>zypper</packagemanager>
Expand All @@ -16,13 +22,51 @@
<rpm-check-signatures>false</rpm-check-signatures>
<bootsplash-theme>breeze</bootsplash-theme>
<bootloader-theme>openSUSE</bootloader-theme>
</preferences>
<preferences profiles="Insecure">
<type image="oem" filesystem="ext4" kernelcmdline="console=ttyS0" firmware="uefi" luks="linux" luks_version="luks2" luks_pbkdf="pbkdf2" bootpartition="false">
<luksformat>
<option name="--cipher" value="aes-xts-plain64"/>
<option name="--key-size" value="256"/>
</luksformat>
<oemconfig>
<oem-resize>false</oem-resize>
<oem-resize>true</oem-resize>
</oemconfig>
<bootloader name="grub2" console="serial" timeout="10"/>
</type>
</preferences>
<preferences profiles="ReEncryptExtraBootEmptyPass">
<type image="oem" filesystem="ext4" kernelcmdline="console=ttyS0 rd.kiwi.oem.luks.reencrypt" firmware="uefi" luks="" luks_version="luks2" luks_pbkdf="pbkdf2" bootpartition="true">
<luksformat>
<option name="--cipher" value="aes-xts-plain64"/>
<option name="--key-size" value="256"/>
</luksformat>
<oemconfig>
<oem-resize>true</oem-resize>
</oemconfig>
<bootloader name="grub2" console="serial" timeout="10"/>
</type>
</preferences>
<preferences profiles="ReEncryptExtraBootWithPass">
<type image="oem" filesystem="ext4" kernelcmdline="console=ttyS0 rd.kiwi.oem.luks.reencrypt" firmware="uefi" luks="linux" luks_version="luks2" luks_pbkdf="pbkdf2" bootpartition="true">
<luksformat>
<option name="--cipher" value="aes-xts-plain64"/>
<option name="--key-size" value="256"/>
</luksformat>
<oemconfig>
<oem-resize>true</oem-resize>
</oemconfig>
<bootloader name="grub2" console="serial" timeout="10"/>
</type>
</preferences>
<preferences profiles="ReEncryptFullDisk">
<type image="oem" filesystem="ext4" kernelcmdline="console=ttyS0 rd.kiwi.oem.luks.reencrypt" firmware="uefi" luks="linux" luks_version="luks2" luks_pbkdf="pbkdf2" bootpartition="false">
<luksformat>
<option name="--cipher" value="aes-xts-plain64"/>
<option name="--key-size" value="256"/>
</luksformat>
<oemconfig>
<oem-resize>true</oem-resize>
</oemconfig>
<bootloader name="grub2" console="serial" timeout="10"/>
</type>
Expand Down Expand Up @@ -61,6 +105,7 @@
<package name="shim"/>
<package name="timezone"/>
<package name="cryptsetup"/>
<package name="dracut-kiwi-oem-repart"/>
</packages>
<packages type="bootstrap">
<package name="gawk"/>
Expand Down
34 changes: 33 additions & 1 deletion build-tests/x86/tumbleweed/test-image-luks/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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...
Expand All @@ -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
12 changes: 12 additions & 0 deletions doc/source/concept_and_workflow/customize_the_boot_process.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 18 additions & 3 deletions dracut/modules.d/90kiwi-repart/kiwi-repart-disk.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions dracut/modules.d/90kiwi-repart/module-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 5 additions & 0 deletions dracut/modules.d/99kiwi-lib/kiwi-dialog-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
56 changes: 56 additions & 0 deletions dracut/modules.d/99kiwi-lib/kiwi-luks-lib.sh
Original file line number Diff line number Diff line change
@@ -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}
Expand All @@ -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
}
Expand Down
10 changes: 9 additions & 1 deletion kiwi/boot/image/dracut.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
"""
Expand Down Expand Up @@ -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(
Expand Down
16 changes: 16 additions & 0 deletions kiwi/builder/disk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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]]
Expand Down
19 changes: 19 additions & 0 deletions kiwi/storage/luks_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 868408a

Please sign in to comment.