From e139565b01429ccd0373366d1cfd96274ccf7e31 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 25 Aug 2022 16:54:03 +0200 Subject: [PATCH 1/8] cpio: add helper for packing cpios of literally specified data blobs Let's add simple helpers for passing data blobs from the stub into the booted kernel as initrds that are generated on-the-fly. (Note used yet, a later commit will make use of this) --- src/boot/efi/cpio.c | 57 +++++++++++++++++++++++++++++++++++++++++++++ src/boot/efi/cpio.h | 14 +++++++++++ 2 files changed, 71 insertions(+) diff --git a/src/boot/efi/cpio.c b/src/boot/efi/cpio.c index b21cf53b46..c71c218e4f 100644 --- a/src/boot/efi/cpio.c +++ b/src/boot/efi/cpio.c @@ -487,3 +487,60 @@ nothing: return EFI_SUCCESS; } + +EFI_STATUS pack_cpio_literal( + const void *data, + size_t data_size, + const char *target_dir_prefix, + const char16_t *target_filename, + uint32_t dir_mode, + uint32_t access_mode, + const uint32_t tpm_pcr[], + UINTN n_tpm_pcr, + const char16_t *tpm_description, + void **ret_buffer, + UINTN *ret_buffer_size, + bool *ret_measured) { + + uint32_t inode = 1; /* inode counter, so that each item gets a new inode */ + _cleanup_free_ void *buffer = NULL; + UINTN buffer_size; + EFI_STATUS err; + + assert(data || data_size == 0); + assert(target_dir_prefix); + assert(target_filename); + assert(tpm_pcr || n_tpm_pcr == 0); + assert(ret_buffer); + assert(ret_buffer_size); + + /* Generate the leading directory inodes right before adding the first files, to the + * archive. Otherwise the cpio archive cannot be unpacked, since the leading dirs won't exist. */ + + err = pack_cpio_prefix(target_dir_prefix, dir_mode, &inode, &buffer, &buffer_size); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to pack cpio prefix: %r", err); + + err = pack_cpio_one( + target_filename, + data, data_size, + target_dir_prefix, + access_mode, + &inode, + &buffer, &buffer_size); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to pack cpio file %s: %r", target_filename, err); + + err = pack_cpio_trailer(&buffer, &buffer_size); + if (err != EFI_SUCCESS) + return log_error_status_stall(err, L"Failed to pack cpio trailer: %r"); + + err = measure_cpio(buffer, buffer_size, tpm_pcr, n_tpm_pcr, tpm_description, ret_measured); + if (err != EFI_SUCCESS) + return err; + + *ret_buffer = TAKE_PTR(buffer); + *ret_buffer_size = buffer_size; + + return EFI_SUCCESS; +} diff --git a/src/boot/efi/cpio.h b/src/boot/efi/cpio.h index 672a751825..beebef3d8b 100644 --- a/src/boot/efi/cpio.h +++ b/src/boot/efi/cpio.h @@ -18,3 +18,17 @@ EFI_STATUS pack_cpio( void **ret_buffer, UINTN *ret_buffer_size, bool *ret_measured); + +EFI_STATUS pack_cpio_literal( + const void *data, + size_t data_size, + const char *target_dir_prefix, + const char16_t *target_filename, + uint32_t dir_mode, + uint32_t access_mode, + const uint32_t tpm_pcr[], + UINTN n_tpm_pcr, + const char16_t *tpm_description, + void **ret_buffer, + UINTN *ret_buffer_size, + bool *ret_measured); From df7ee6f8b0c73ecd4d9f042482b894ebb5b71353 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 25 Aug 2022 16:55:01 +0200 Subject: [PATCH 2/8] stub: add new special PE sections ".pcrsig" and ".pcrpkey" in unified kernels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These aren't wired up yet to do anything useful. For now we just define them. This sections are supposed to carry a signature for expected measurements on PCR 11 if this kernel is booted, in the JSON format "systemd-measure sign" generates, and the public key used for the signature. The idea is to embedd the signature and the public key in unified kernels and making them available to userspace, so that userspace can easily access them and enroll (for which the public key is needed) or unlock (for which the PCR signature is needed) LUKS2 volumes and credentials that are bound to the currently used kernel version stream. Why put these files in PE sections rather than just into simple files in the initrd or into the host fs? The signature cannot be in the initrd, since it is after all covering the initrd, and thus the initrd as input for the calculation cannot carry the result of the calculation. Putting the signature onto the root fs sucks too, since we typically want to unlock the root fs with it, hence it would be inaccessible for it's primary purpose then. The public key could be in the initrd or in the root fs, there's no technical restriction for that. However, I still think it's a good idea to put it in a PE section as well, because this means the piece of code that attaches the signature can also attach the public key easily in one step, which is nice since it allows separating the roles of the kernel/initrd/root fs builder, and the role of the signer, and the former doesn't have to have knowledge about what the latter is going to add to the image. Note that the signature section is excluded from the TPM measurements sd-stub does about its resource sections, since – as mentioned – it's the expected output of the signing operation whose input are the measurements, hence it cannot also be input to them. The public key section is included in the measurements however. --- src/boot/efi/stub.c | 6 +++++- src/boot/measure.c | 6 +++++- src/fundamental/tpm-pcr.c | 2 ++ src/fundamental/tpm-pcr.h | 10 ++++++++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/boot/efi/stub.c b/src/boot/efi/stub.c index 3f6832e0c2..a00892718e 100644 --- a/src/boot/efi/stub.c +++ b/src/boot/efi/stub.c @@ -190,11 +190,15 @@ EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *sys_table) { * into so far), so that we have one PCR that we can nicely write policies against because it * contains all static data of this image, and thus can be easily be pre-calculated. */ for (UnifiedSection section = 0; section < _UNIFIED_SECTION_MAX; section++) { - m = false; + + if (!unified_section_measure(section)) /* shall not measure? */ + continue; if (szs[section] == 0) /* not found */ continue; + m = false; + /* First measure the name of the section */ (void) tpm_log_event_ascii( TPM_PCR_INDEX_KERNEL_IMAGE, diff --git a/src/boot/measure.c b/src/boot/measure.c index e8404026cb..bc8f720514 100644 --- a/src/boot/measure.c +++ b/src/boot/measure.c @@ -68,6 +68,7 @@ static int help(int argc, char *argv[], void *userdata) { " --initrd=PATH Path to initrd image\n" " --splash=PATH Path to splash bitmap\n" " --dtb=PATH Path to Devicetree file\n" + " --pcrpkey=PATH Path to public key for PCR signatures in DER format\n" " -c --current Use current PCR values\n" " --bank=DIGEST Select TPM bank (SHA1, SHA256)\n" " --tpm2-device=PATH Use specified TPM2 device\n" @@ -96,8 +97,10 @@ static int parse_argv(int argc, char *argv[]) { ARG_CMDLINE, ARG_INITRD, ARG_SPLASH, + ARG_DTB, + _ARG_PCRSIG, /* the .pcrsig section is not input for signing, hence not actually an argument here */ _ARG_SECTION_LAST, - ARG_DTB = _ARG_SECTION_LAST, + ARG_PCRPKEY = _ARG_SECTION_LAST, ARG_BANK, ARG_PRIVATE_KEY, ARG_PUBLIC_KEY, @@ -115,6 +118,7 @@ static int parse_argv(int argc, char *argv[]) { { "initrd", required_argument, NULL, ARG_INITRD }, { "splash", required_argument, NULL, ARG_SPLASH }, { "dtb", required_argument, NULL, ARG_DTB }, + { "pcrpkey", required_argument, NULL, ARG_PCRPKEY }, { "current", no_argument, NULL, 'c' }, { "bank", required_argument, NULL, ARG_BANK }, { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE }, diff --git a/src/fundamental/tpm-pcr.c b/src/fundamental/tpm-pcr.c index 97b3c7b9d2..7609d83c2e 100644 --- a/src/fundamental/tpm-pcr.c +++ b/src/fundamental/tpm-pcr.c @@ -11,5 +11,7 @@ const char* const unified_sections[_UNIFIED_SECTION_MAX + 1] = { [UNIFIED_SECTION_INITRD] = ".initrd", [UNIFIED_SECTION_SPLASH] = ".splash", [UNIFIED_SECTION_DTB] = ".dtb", + [UNIFIED_SECTION_PCRSIG] = ".pcrsig", + [UNIFIED_SECTION_PCRPKEY] = ".pcrpkey", NULL, }; diff --git a/src/fundamental/tpm-pcr.h b/src/fundamental/tpm-pcr.h index fb0774f70d..235d4841b0 100644 --- a/src/fundamental/tpm-pcr.h +++ b/src/fundamental/tpm-pcr.h @@ -1,6 +1,8 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once +#include "macro-fundamental.h" + /* The various TPM PCRs we measure into from sd-stub and sd-boot. */ /* This TPM PCR is where we extend the sd-stub "payloads" into, before using them. i.e. the kernel ELF image, @@ -32,7 +34,15 @@ typedef enum UnifiedSection { UNIFIED_SECTION_INITRD, UNIFIED_SECTION_SPLASH, UNIFIED_SECTION_DTB, + UNIFIED_SECTION_PCRSIG, + UNIFIED_SECTION_PCRPKEY, _UNIFIED_SECTION_MAX, } UnifiedSection; extern const char* const unified_sections[_UNIFIED_SECTION_MAX + 1]; + +static inline bool unified_section_measure(UnifiedSection section) { + /* Don't include the PCR signature in the PCR measurements, since they sign the expected result of + * the measurement, and hence shouldn't be input to it. */ + return section >= 0 && section < _UNIFIED_SECTION_MAX && section != UNIFIED_SECTION_PCRSIG; +} From 23002b45b121740cfe9237489778ecc5d97f520a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 25 Aug 2022 16:55:49 +0200 Subject: [PATCH 3/8] stub: pass .pcrsig and .pcrpkey PE sections as cpio into invoked kernel Pick up the two new sections in sd-stub and pass them as initrds into the booted kernels, where they'll show up as /.extra/tpm2-pcr-signature.json and /.extra/tpm2-pcr-public-key.pem in the initrd file system. The initrd is then supposed to pick these files up from there and save them at a place that will survive into the host OS. --- src/boot/efi/stub.c | 102 +++++++++++++++++++++++++++++++------------- 1 file changed, 72 insertions(+), 30 deletions(-) diff --git a/src/boot/efi/stub.c b/src/boot/efi/stub.c index a00892718e..494972fa38 100644 --- a/src/boot/efi/stub.c +++ b/src/boot/efi/stub.c @@ -20,9 +20,7 @@ _used_ _section_(".sdmagic") static const char magic[] = "#### LoaderInfo: syste static EFI_STATUS combine_initrd( EFI_PHYSICAL_ADDRESS initrd_base, UINTN initrd_size, - const void *credential_initrd, UINTN credential_initrd_size, - const void *global_credential_initrd, UINTN global_credential_initrd_size, - const void *sysext_initrd, UINTN sysext_initrd_size, + const void * const extra_initrds[], const size_t extra_initrd_sizes[], size_t n_extra_initrds, EFI_PHYSICAL_ADDRESS *ret_initrd_base, UINTN *ret_initrd_size) { EFI_PHYSICAL_ADDRESS base = UINT32_MAX; /* allocate an area below the 32bit boundary for this */ @@ -36,23 +34,15 @@ static EFI_STATUS combine_initrd( /* Combines four initrds into one, by simple concatenation in memory */ n = ALIGN4(initrd_size); /* main initrd might not be padded yet */ - if (credential_initrd) { - if (n > UINTN_MAX - credential_initrd_size) + + for (size_t i = 0; i < n_extra_initrds; i++) { + if (!extra_initrds[i]) + continue; + + if (n > UINTN_MAX - extra_initrd_sizes[i]) return EFI_OUT_OF_RESOURCES; - n += credential_initrd_size; - } - if (global_credential_initrd) { - if (n > UINTN_MAX - global_credential_initrd_size) - return EFI_OUT_OF_RESOURCES; - - n += global_credential_initrd_size; - } - if (sysext_initrd) { - if (n > UINTN_MAX - sysext_initrd_size) - return EFI_OUT_OF_RESOURCES; - - n += sysext_initrd_size; + n += extra_initrd_sizes[i]; } err = BS->AllocatePages( @@ -78,12 +68,12 @@ static EFI_STATUS combine_initrd( } } - if (credential_initrd) - p = mempcpy(p, credential_initrd, credential_initrd_size); - if (global_credential_initrd) - p = mempcpy(p, global_credential_initrd, global_credential_initrd_size); - if (sysext_initrd) - p = mempcpy(p, sysext_initrd, sysext_initrd_size); + for (size_t i = 0; i < n_extra_initrds; i++) { + if (!extra_initrds[i]) + continue; + + p = mempcpy(p, extra_initrds[i], extra_initrd_sizes[i]); + } assert((uint8_t*) PHYSICAL_ADDRESS_TO_POINTER(base) + n == p); @@ -150,10 +140,9 @@ static void export_variables(EFI_LOADED_IMAGE_PROTOCOL *loaded_image) { } EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *sys_table) { + _cleanup_free_ void *credential_initrd = NULL, *global_credential_initrd = NULL, *sysext_initrd = NULL, *pcrsig_initrd = NULL, *pcrpkey_initrd = NULL; + UINTN credential_initrd_size = 0, global_credential_initrd_size = 0, sysext_initrd_size = 0, pcrsig_initrd_size = 0, pcrpkey_initrd_size = 0; UINTN cmdline_len = 0, linux_size, initrd_size, dt_size; - UINTN credential_initrd_size = 0, global_credential_initrd_size = 0, sysext_initrd_size = 0; - _cleanup_free_ void *credential_initrd = NULL, *global_credential_initrd = NULL; - _cleanup_free_ void *sysext_initrd = NULL; EFI_PHYSICAL_ADDRESS linux_base, initrd_base, dt_base; _cleanup_(devicetree_cleanup) struct devicetree_state dt_state = {}; EFI_LOADED_IMAGE_PROTOCOL *loaded_image; @@ -305,6 +294,45 @@ EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *sys_table) { if (sysext_measured) (void) efivar_set_uint_string(LOADER_GUID, L"StubPcrInitRDSysExts", TPM_PCR_INDEX_INITRD_SYSEXTS, 0); + /* If the PCR signature was embedded in the PE image, then let's wrap it in a cpio and also pass it + * to the kernel, so that it can be read from /.extra/tpm2-pcr-signature.json. Note that this section + * is not measured, neither as raw section (see above), nor as cpio (here), because it is the + * signature of expected PCR values, i.e. it's input are PCR measurement, and hence it shouldn't + * itself be input for PCR measurements. */ + if (szs[UNIFIED_SECTION_PCRSIG] > 0) + (void) pack_cpio_literal( + (uint8_t*) loaded_image->ImageBase + addrs[UNIFIED_SECTION_PCRSIG], + szs[UNIFIED_SECTION_PCRSIG], + ".extra", + L"tpm2-pcr-signature.json", + /* dir_mode= */ 0555, + /* access_mode= */ 0444, + /* tpm_pcr= */ NULL, + /* n_tpm_pcr= */ 0, + /* tpm_description= */ NULL, + &pcrsig_initrd, + &pcrsig_initrd_size, + /* ret_measured= */ NULL); + + /* If the public key used for the PCR signatures was embedded in the PE image, then let's wrap it in + * a cpio and also pass it to the kernel, so that it can be read from + * /.extra/tpm2-pcr-public-key.pem. This section is already measure above, hence we won't measure the + * cpio. */ + if (szs[UNIFIED_SECTION_PCRPKEY] > 0) + (void) pack_cpio_literal( + (uint8_t*) loaded_image->ImageBase + addrs[UNIFIED_SECTION_PCRPKEY], + szs[UNIFIED_SECTION_PCRPKEY], + ".extra", + L"tpm2-pcr-public-key.pem", + /* dir_mode= */ 0555, + /* access_mode= */ 0444, + /* tpm_pcr= */ NULL, + /* n_tpm_pcr= */ 0, + /* tpm_description= */ NULL, + &pcrpkey_initrd, + &pcrpkey_initrd_size, + /* ret_measured= */ NULL); + linux_size = szs[UNIFIED_SECTION_LINUX]; linux_base = POINTER_TO_PHYSICAL_ADDRESS(loaded_image->ImageBase) + addrs[UNIFIED_SECTION_LINUX]; @@ -318,9 +346,21 @@ EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *sys_table) { /* If we have generated initrds dynamically, let's combine them with the built-in initrd. */ err = combine_initrd( initrd_base, initrd_size, - credential_initrd, credential_initrd_size, - global_credential_initrd, global_credential_initrd_size, - sysext_initrd, sysext_initrd_size, + (const void*const[]) { + credential_initrd, + global_credential_initrd, + sysext_initrd, + pcrsig_initrd, + pcrpkey_initrd, + }, + (const size_t[]) { + credential_initrd_size, + global_credential_initrd_size, + sysext_initrd_size, + pcrsig_initrd_size, + pcrpkey_initrd_size, + }, + 5, &initrd_base, &initrd_size); if (err != EFI_SUCCESS) return err; @@ -329,6 +369,8 @@ EFI_STATUS efi_main(EFI_HANDLE image, EFI_SYSTEM_TABLE *sys_table) { credential_initrd = mfree(credential_initrd); global_credential_initrd = mfree(global_credential_initrd); sysext_initrd = mfree(sysext_initrd); + pcrsig_initrd = mfree(pcrsig_initrd); + pcrpkey_initrd = mfree(pcrpkey_initrd); } if (dt_size > 0) { From f51b49c6758f897c58498cd06c851ef17dd760c2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 25 Aug 2022 17:16:52 +0200 Subject: [PATCH 4/8] tmpfiles: copy PCR sig/pkey from initrd /.extra/ into /run/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that sd-stub will place the PCR signature and its public key in the initrd's /.extra/ directory, let's copy it from there into /run/ from userspace. This is done because /.extra/ is on the initrd's tmpfs which will be emptied during the initrd → host transition. Since we want these two files to survive we'll copy them – if they exist – into /run/ where they will survive the transition. Thus, with this last change the files will have safely propagated from their PE sections into files in /run/ where userspace can find them The paths in /run/ happen to be the exact ones that systemd-cryptenroll/systemd-cryptsetup/systemd-creds look for them. --- tmpfiles.d/systemd.conf.in | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tmpfiles.d/systemd.conf.in b/tmpfiles.d/systemd.conf.in index e23e102782..d267a6b2e6 100644 --- a/tmpfiles.d/systemd.conf.in +++ b/tmpfiles.d/systemd.conf.in @@ -64,3 +64,9 @@ d /var/lib/systemd/coredump 0755 root root 3d d /var/lib/private 0700 root root - d /var/log/private 0700 root root - d /var/cache/private 0700 root root - + +{% if ENABLE_EFI %} +# Copy sd-stub provided PCR signature and and public key file from initrd into /run/, so that it will survive the initrd stage +C /run/systemd/tpm2-pcr-signature.json 0444 root root - /.extra/tpm2-pcr-signature.json +C /run/systemd/tpm2-pcr-public-key.pem 0444 root root - /.extra/tpm2-pcr-public-key.pem +{% endif %} From 2deca517f6da196ebcf9510d6fce8d1ea3193f6e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 9 Sep 2022 11:08:35 +0200 Subject: [PATCH 5/8] man: document the new .pcrsig/.pcrpkey sections for unified kernel images --- man/systemd-measure.xml | 59 +++++++++++++-------- man/systemd-stub.xml | 115 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 148 insertions(+), 26 deletions(-) diff --git a/man/systemd-measure.xml b/man/systemd-measure.xml index 0fc0d0e87d..69ac348184 100644 --- a/man/systemd-measure.xml +++ b/man/systemd-measure.xml @@ -37,12 +37,12 @@ systemd-stub7 is booted up. It accepts paths to the ELF kernel image file, initial ram disk image file, devicetree file, kernel command line file, - os-release5 file, and - boot splash file that make up the unified kernel image, and determines the PCR values expected to be in - place after booting the image. Calculation starts with a zero-initialized PCR 11, and is executed in a - fashion compatible with what systemd-stub does at boot. The result may optionally be - signed cryptographically, to allow TPM2 policies that can only be unlocked if a certain set of kernels is - booted, for which such a PCR signature can be provided. + os-release5 file, boot + splash file, and TPM2 PCR PEM public key file that make up the unified kernel image, and determines the + PCR values expected to be in place after booting the image. Calculation starts with a zero-initialized + PCR 11, and is executed in a fashion compatible with what systemd-stub does at + boot. The result may optionally be signed cryptographically, to allow TPM2 policies that can only be + unlocked if a certain set of kernels is booted, for which such a PCR signature can be provided. @@ -66,9 +66,9 @@ Pre-calculate the expected values seen in PCR register 11 after boot-up of a unified kernel image consisting of the components specified with , , , , - , , see below. Only is - mandatory. (Alternatively, specify to use the current values of PCR - register 11 instead.) + , , see below. Only + is mandatory. (Alternatively, specify to use the + current values of PCR register 11 instead.) @@ -104,6 +104,7 @@ + When used with the calculate or sign verb, configures the files to read the unified kernel image components from. Each option corresponds with @@ -135,7 +136,14 @@ These switches take paths to a pair of PEM encoded RSA key files, for use with - the sign command. + the sign command. + + Note the difference between the and + switches. The former selects the data to include in the .pcrpkey PE section of the + unified kernel image, the latter picks the public key of the key pair used to sign the resulting PCR + 11 values. The former is the key that the booted system will likely use to lock disk and credential + encryption to, the latter is the key used for unlocking such resources again. Hence, typically the + same PEM key should be supplied in both cases. @@ -185,19 +193,11 @@ - Generate a private/public key pair, and a unified kernel image, and a TPM PCR 11 signature for it + Generate a private/public key pair, and a unified kernel image, and a TPM PCR 11 signature for + it, and embed the signature and the public key in the image # openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out tpm2-pcr-private.pem # openssl rsa -pubout -in tpm2-pcr-private.pem -out tpm2-pcr-public.pem -# objcopy \ - --add-section .linux=vmlinux --change-section-vma .linux=0x2000000 \ - --add-section .osrel=os-release.txt --change-section-vma .osrel=0x20000 \ - --add-section .cmdline=cmdline.txt --change-section-vma .cmdline=0x30000 \ - --add-section .initrd=initrd.cpio --change-section-vma .initrd=0x3000000 \ - --add-section .splash=splash.bmp --change-section-vma .splash=0x100000 \ - --add-section .dtb=devicetree.dtb --change-section-vma .dtb=0x40000 \ - /usr/lib/systemd/boot/efi/linuxx64.efi.stub \ - foo.efi # systemd-measure sign \ --linux=vmlinux \ --osrel=os-release.txt \ @@ -205,10 +205,22 @@ --initrd=initrd.cpio \ --splash=splash.bmp \ --dtb=devicetree.dtb \ + --pcrpkey=tpm2-pcr-public.pem \ --bank=sha1 \ --bank=sha256 \ --private-key=tpm2-pcr-private.pem \ - --public-key=tpm2-pcr-public.pem > tpm2-pcr-signature.json + --public-key=tpm2-pcr-public.pem > tpm2-pcr-signature.json +# objcopy \ + --add-section .linux=vmlinux --change-section-vma .linux=0x2000000 \ + --add-section .osrel=os-release.txt --change-section-vma .osrel=0x20000 \ + --add-section .cmdline=cmdline.txt --change-section-vma .cmdline=0x30000 \ + --add-section .initrd=initrd.cpio --change-section-vma .initrd=0x3000000 \ + --add-section .splash=splash.bmp --change-section-vma .splash=0x100000 \ + --add-section .dtb=devicetree.dtb --change-section-vma .dtb=0x40000 \ + --add-section .pcrsig=tpm2-pcr-signature.json --change-section-vma .splash=0x80000 \ + --add-section .pcrpkey=tpm2-pcr-public.pem --change-section-vma .splash=0x90000 \ + /usr/lib/systemd/boot/efi/linuxx64.efi.stub \ + foo.efi Later on, enroll the signed PCR policy on a LUKS volume: @@ -217,6 +229,11 @@ And then unlock the device with the signature: # /usr/lib/systemd/systemd-cryptsetup attach myvolume /dev/sda5 - tpm2-device=auto,tpm2-signature=/path/to/tpm2-pcr-signature.json + + Note that when the generated unified kernel image foo.efi is booted the + signature and public key files will be placed at locations systemd-cryptenroll and + systemd-cryptsetup will look for anyway, and thus these paths do not actually need to + be specified. diff --git a/man/systemd-stub.xml b/man/systemd-stub.xml index 1e9bb5d631..2479d9f5fa 100644 --- a/man/systemd-stub.xml +++ b/man/systemd-stub.xml @@ -68,6 +68,14 @@ A boot splash (in Windows .BMP format) to show on screen before invoking the kernel will be looked for in the .splash PE section. + + A set of cryptographic signatures for expected TPM2 PCR values when this kernel is + booted, in JSON format, in the .pcrsig section. This is useful for implementing TPM2 + policies that bind disk encryption and similar to kernels that are signed by a specific + key. + + A public key in PEM format matching this TPM2 PCR signature data in the + .pcrpkey section. If UEFI SecureBoot is enabled and the .cmdline section is present in the executed @@ -81,8 +89,25 @@ DeviceTree in the corresponding EFI configuration table. systemd-stub will ask the firmware via the EFI_DT_FIXUP_PROTOCOL for hardware specific fixups to the DeviceTree. - The contents of these six PE sections are measured into TPM PCR 11, that is otherwise not - used. Thus, it can be pre-calculated without too much effort. + The contents of seven of these eight PE sections are measured into TPM PCR 11, that is otherwise + not used. Thus, it can be pre-calculated without too much effort. The .pcrsig section + is not included in this PCR measurement, since it's supposed to contain signatures for the expected + results for these measurements, i.e. of the outputs of the measurement operation, and thus cannot also be + input to it. + + When .pcrsig and/or .pcrpkey are present in a unified kernel + image their contents are passed to the booted kernel in an synthetic initrd cpio archive that places them in the + /.extra/tpm2-pcr-signature.json and + /.extra/tpm2-pcr-public-key.pem files. Typically, a + tmpfiles.d5 line then + ensures they are copied into /run/systemd/tpm2-pcr-signature.json and + /run/systemd/tpm2-pcr-public-key.pem where they remain accessible even after the + system transitions out of the initrd environment into the host file system. Tools such + systemd-cryptsetup@.service8, + systemd-cryptenroll1 + and systemd-creds1 + will automatically use files present under these paths to unlock protected resources (encrypted storage + or credentials) or bind encryption to booted kernels. @@ -166,12 +191,12 @@ - Boot splash (embedded in the unified PE binary) + Core kernel code (embedded in unified PE binary) 4 + 11 - Core kernel code (embedded in unified PE binary) + OS release information (embedded in the unified PE binary) 4 + 11 @@ -190,6 +215,21 @@ 12 + + Boot splash (embedded in the unified PE binary) + 4 + 11 + + + + TPM2 PCR signature JSON (embedded in unified PE binary, synthesized into initrd) + 4 + 9 + + + + TPM2 PCR PEM public key (embedded in unified PE binary, synthesized into initrd) + 4 + 9 + 11 + + Credentials (synthesized initrd from companion files) 9 + 12 @@ -279,6 +319,66 @@ url="https://systemd.io/BOOT_LOADER_INTERFACE">Boot Loader Interface. + + initrd Resources + + The following resources are passed as initrd cpio archives to the booted kernel, and thus make up + the initial file system hierarchy in the initrd execution environment: + + + + / + + The main initrd from the .initrd PE section of the unified kernel image. + + + + /.extra/credentials/*.cred + Credential files (suffix .cred) that are placed next to the + unified kernel image (as described above) are copied into the + /.extra/credentials/ directory in the initrd execution + environment. + + + + /.extra/global_credentials/*.cred + Similar, credential files in the /loader/credentials/ directory + in the file system the unified kernel image is placed in are copied into the + /.extra/global_credentials/ directory in the initrd execution + environment. + + + + /.extra/sysext/*.raw + System extension image files (suffix .raw) that are placed next to + the unified kernel image (as described above) are copied into the + /.extra/sysext/ directory in the initrd execution environment. + + + + /.extra/tpm2-pcr-signature.json + The TPM2 PCR signature JSON object included in the .pcrsig PE + section of the unified kernel image is copied into the + /.extra/tpm2-pcr-signature.json file in the initrd execution + environment. + + + + /.extra/tpm2-pcr-pkey.pem + The PEM public key included in the .pcrpkey PE section of the + unified kernel image is copied into the /.extra/tpm2-pcr-public-key.pem file in + the initrd execution environment. + + + + Note that all these files are located in the tmpfs file system the kernel sets + up for the initrd file hierarchy and are thus lost when the system transitions from the initrd execution + environment into the host file system. If these resources shall be kept around over this transition they + need to be copied to a place that survives the transition first, for example via a suitable + tmpfiles.d5 line. By + default, this is done for the TPM2 PCR signature and public key files. + + Assembling Kernel Images @@ -313,6 +413,10 @@ This expects a pair of X.509 private key and certificate as parameters and then signs the UEFI PE executable we generated above for UEFI SecureBoot and generates a signed UEFI PE executable as result. + + See + systemd-measure1 for + an example involving the .pcrsig and .pcrpkey sections. @@ -325,7 +429,8 @@ Boot Loader Specification, Boot Loader Interface, objcopy1, - sbsign1 + sbsign1, + systemd-measure1 From 4889e809beb5134a8baad04ab8ef26afd9e233d9 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 9 Sep 2022 11:09:30 +0200 Subject: [PATCH 6/8] man: be more careful regarding TPM vs. TPM2 We support PCR measurements for both classic TPM1.2 and TPM2, hence just say "TPM" generically in that context. But the signed policies are exclusive to TPM2, hence always say TPM2 there. We mostly got that right, except at one place. Fix that. --- man/systemd-stub.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/systemd-stub.xml b/man/systemd-stub.xml index 2479d9f5fa..50fe571fe7 100644 --- a/man/systemd-stub.xml +++ b/man/systemd-stub.xml @@ -158,7 +158,7 @@ - TPM2 PCR Notes + TPM PCR Notes Note that when a unified kernel using systemd-stub is invoked the firmware will measure it as a whole to TPM PCR 4, covering all embedded resources, such as the stub code itself, the From 6d856e26a71c7badb7bcc9b19546efb0be08c291 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 9 Sep 2022 11:24:12 +0200 Subject: [PATCH 7/8] man: say early what a unified kernel image/UKI is --- man/systemd-stub.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/man/systemd-stub.xml b/man/systemd-stub.xml index 50fe571fe7..f8c3eee393 100644 --- a/man/systemd-stub.xml +++ b/man/systemd-stub.xml @@ -45,9 +45,9 @@ system into the Linux world. The UEFI boot stub looks for various resources for the kernel invocation inside the UEFI PE binary - itself. This allows combining various resources inside a single PE binary image, which may then be signed - via UEFI SecureBoot as a whole, covering all individual resources at once. Specifically it may - include: + itself. This allows combining various resources inside a single PE binary image (usually called "Unified + Kernel Image", or "UKI" for short), which may then be signed via UEFI SecureBoot as a whole, covering all + individual resources at once. Specifically it may include: The ELF Linux kernel images will be looked for in the .linux PE From 1d5f14ef3dd24f75fdb5dbd63cb665d9e2f58ec2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 26 Aug 2022 11:11:07 +0200 Subject: [PATCH 8/8] update TODO --- TODO | 65 ++++++++++-------------------------------------------------- 1 file changed, 11 insertions(+), 54 deletions(-) diff --git a/TODO b/TODO index 84ed31bba9..3231d0848a 100644 --- a/TODO +++ b/TODO @@ -117,6 +117,11 @@ Deprecations and removals: Features: +* during the initrd → host transition measure a fixed value into TPM PCR 11 + (where we already measure the UKI into), so that unlock policies for disk + enryption/credential encryption can be put together that only work in the + initrd or only on the host (or both). + * Add support for extra verity configuration options to systemd-reart (FEC, hash type, etc) * chase_symlinks(): take inspiraton from path_extract_filename() and return @@ -160,11 +165,6 @@ Features: * systemd-measure tool: - pre-calculate PCR 12 (command line) + PCR 13 (sysext) the same way we can precalculate PCR 11 - - sign pre-calculated hashes in a way compatible with TPM2 PCR hash signature - policies, in a way they can be included in unified PE kernel images, and - made available to userspace. There, this should be consumed by - systemd-cryptsetup to implement PCR signature based TPM volume unlock - policies. * in sd-boot: load EFI drivers from a new PE section. That way, one can have a "supercharged" sd-boot binary, that could carry ext4 drivers built-in. @@ -249,8 +249,7 @@ Features: * repart: allow defining additional partitions via credential -* tmpfiles: add snippet that provisions /etc/hosts, /etc/motd, - /root/.ssh/authorized_keys from credential +* tmpfiles: add snippet that provisions /root/.ssh/authorized_keys from credential * timesyncd: pick NTP server info from credential @@ -343,50 +342,11 @@ Features: * given that /etc/ssh/ssh_config.d/ is a thing now, ship a drop-in for that that hooks up userbdctl ssh-key stuff. -* allow embedding a signature blob for PCR hashes into separate section in - unified kernel binaries. This section should be picked up by sd-stub, and - passed in a file to the booted kernel (via initrd cpio, as usual). Usecase: - this way we can implement disk encryption policies that bind to specific - kernel PCR state, without breaking things on every kernel update. As long as - the kernel includes the PCR signature blob we should be good, as disk - encryption can then pass the signature to the TPM to unlock their secrets. - Why do this via a separate PE section? That's because the PCR state depends - on the measured kernel/initrd of course, thus we cannot put the signature - into the kernel/initrd itself, because that would require a time machine. - Hence we have to find a separate place. A simple solution is a PE section - of its own, because then it is next to the kernel and initrd which after all - are stored in PE sections of their own too. Building a unified kernel would - thus mean, calculating PCR values for the raw kernel image, and raw initrd - image, then signing those PCR values with a vendor key, and then combining - sd-stub, raw kernel image, raw initrd, and PCR signature into a unified - kernel image. - -* a new tool "systemd-trust" or so, that can calculate PCR hashes offline, and - optionally sign them. for that we should extend our syntax for specifying pcr - policies (e.g. the string like "4+7+9") so that it can also include explicit - hash values, i.e. - 4=sha256:0ef149998289474e4bb31813edda6ad7f3c991b2d8dec6e8fe4db7a1f039f2d1+7=sha256:87428fc522803d31065e7bce3cf03fe475096631e5e07bbd7a0fde60c4cf25c7+9=sha256:0263829989b6fd954f72baaf2fc64bc2e2f01d692d4de72986ea808f6e99813f - and file names to calculate hashes from, i.e. - 4=file:/boot/vmlinuz+7=file:/boot/initrd/+9=file:/etc/fstab" - The systemd-trust tool should then be able to resolve any "underspecifed" - form into the form with explicit hash values. - * maybe add support for binding and connecting AF_UNIX sockets in the file system outside of the 108ch limit. When connecting, open O_PATH fd to socket inode first, then connect to /proc/self/fd/XYZ. When binding, create symlink to target dir in /tmp, and bind through it. -* tmpfiles: for f/F/w lines, if the argument columns is left unspecified, look - for a service credential named after the file path to write to, and load - contents to write from there. Usecase: provision arbitrary files from - credentials. Example use: with a line like "f /root/.ssh/authorized-keys - 0644 root root" in a tmpfiles.d/ snippet add - LoadCredential=root.ssh.authorized-keys via drop-in to - systemd-tmpfiles.service, and then provision an SSH access key through - nspawn's --load-credential=, through qemu's fw_cfg, or via systemd-stub's - credntial pick-up. The latter is particularly interesting to implement SSH - access to an initrd. - * systemd-homed: when initializing, look for a credential sysemd.homed.register or so with JSON user records to automatically register if not registered yet. Usecase: deploy a system, and add an account one can directly log into. @@ -406,14 +366,11 @@ Features: set up the directory so that it can only be accessed if host and app are in order. -* TPM2: add auth policy for signed PCR values to make updates easy. i.e. do - what tpm2_policyauthorize tool does. To be truly useful scheme needs to be a - bit more elaborate though: policy probably must take some nvram based - generation counter into account that can only monotonically increase and can - be used to invalidate old PCR signatures. Otherwise people could downgrade to - old signed PCR sets whenever they want. Usecase: encrypt the rootfs with LUKS - with a key that can only be unlocked via a pristine pre-built Fedora - kernel+initrd. +* TPM2: extend unlock policy to protect against version downgrades in signed + policies: policy probably must take some nvram based generation counter into + account that can only monotonically increase and can be used to invalidate + old PCR signatures. Otherwise people could downgrade to old signed PCR sets + whenever they want. * update HACKING.md to suggest developing systemd with the ideas from: https://0pointer.net/blog/testing-my-system-code-in-usr-without-modifying-usr.html