diff --git a/man/systemd-measure.xml b/man/systemd-measure.xml
index 65cb2e503e7..0fc0d0e87d4 100644
--- a/man/systemd-measure.xml
+++ b/man/systemd-measure.xml
@@ -17,7 +17,7 @@
systemd-measure
- Pre-calculate expected TPM2 PCR values for booted unified kernel images
+ Pre-calculate and sign expected TPM2 PCR values for booted unified kernel images
@@ -32,15 +32,17 @@
Note: this command is experimental for now. While it is likely to become a regular component of
systemd, it might still change in behaviour and interface.
- systemd-measure is a tool that may be used to pre-calculate the expected TPM2
- PCR 11 values that should be seen when a unified Linux kernel image based on
+ systemd-measure is a tool that may be used to pre-calculate and sign the
+ expected TPM2 PCR 11 values that should be seen when a unified Linux kernel image based on
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.
+ 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.
@@ -61,11 +63,30 @@
calculate
- Pre-calculate the expected value seen in PCR register 11 after boot-up of a unified
+ 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.
+ mandatory. (Alternatively, specify to use the current values of PCR
+ register 11 instead.)
+
+
+
+ sign
+
+ As with the calculate command, pre-calculate the expected value
+ seen in TPM2 PCR register 11 after boot-up of a unified kernel image. Then, cryptographically sign
+ the resulting values with the private/public key pair (RSA) configured via
+ and . This will write a JSON object to
+ standard output that contains signatures for all specified PCR banks (see
+ ) below, which may be used to unlock encrypted credentials (see
+ systemd-creds1) or
+ LUKS volumes (see
+ systemd-cryptsetup@.service8). This
+ allows binding secrets to a set of kernels for which such PCR 11 signatures can be provided.
+
+ Note that a TPM2 device must be available for this signing to take place, even though the
+ result is not tied to any TPM2 device or its state.
@@ -84,29 +105,47 @@
- When used with the calculate verb, configures the files to read
- the unified kernel image components from. Each option corresponds with the equally named section in
- the unified kernel PE file. The switch expects the path to the ELF kernel
- file that the unified PE kernel will wrap. All switches except are
- optional. Each option may be used at most once.
+ When used with the calculate or sign verb,
+ configures the files to read the unified kernel image components from. Each option corresponds with
+ the equally named section in the unified kernel PE file. The switch expects
+ the path to the ELF kernel file that the unified PE kernel will wrap. All switches except
+ are optional. Each option may be used at most once.
- When used with the calculate verb, takes the PCR 11 values
- currently in effect for the system (which should typically reflect the hashes of the currently booted
- kernel). This can be used in place of and the other switches listed
- above.
+ When used with the calculate or sign verb,
+ takes the PCR 11 values currently in effect for the system (which should typically reflect the hashes
+ of the currently booted kernel). This can be used in place of and the other
+ switches listed above.Controls the PCR banks to pre-calculate the PCR values for – in case
- calculate is invoked –, or the banks to show in the status
- output. May be used more then once to specify multiple banks. If not specified, defaults to the four
- banks sha1, sha256, sha384,
- sha512.
+ calculate or sign is invoked –, or the banks to show in the
+ status output. May be used more then once to specify multiple banks. If not
+ specified, defaults to the four banks sha1, sha256,
+ sha384, sha512.
+
+
+
+
+
+
+ These switches take paths to a pair of PEM encoded RSA key files, for use with
+ the sign command.
+
+
+
+ PATH
+
+ Controls which TPM2 device to use. Expects a device node path referring to the TPM2
+ chip (e.g. /dev/tpmrm0). Alternatively the special value auto
+ may be specified, in order to automatically determine the device node of a suitable TPM2 device (of
+ which there must be exactly one). The special value list may be used to enumerate
+ all suitable TPM2 devices currently discovered.
@@ -133,7 +172,7 @@
foo.efi
# systemd-measure calculate \
--linux=vmlinux \
- --osrel=os-release \
+ --osrel=os-release.txt \
--cmdline=cmdline.txt \
--initrd=initrd.cpio \
--splash=splash.bmp \
@@ -144,6 +183,41 @@
11:sha512=8e79acd3ddbbc8282e98091849c3530f996303c8ac8e87a3b2378b71c8b3a6e86d5c4f41ecea9e1517090c3e8ec0c714821032038f525f744960bcd082d937da
+
+
+ Generate a private/public key pair, and a unified kernel image, and a TPM PCR 11 signature for it
+
+ # 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 \
+ --cmdline=cmdline.txt \
+ --initrd=initrd.cpio \
+ --splash=splash.bmp \
+ --dtb=devicetree.dtb \
+ --bank=sha1 \
+ --bank=sha256 \
+ --private-key=tpm2-pcr-private.pem \
+ --public-key=tpm2-pcr-public.pem > tpm2-pcr-signature.json
+
+ Later on, enroll the signed PCR policy on a LUKS volume:
+
+ # systemd-cryptenroll --tpm2-device=auto --tpm2-public-key=tpm2-pcr-public.pem --tpm2-signature=tpm2-pcr-signature.json /dev/sda5
+
+ 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
+
@@ -157,7 +231,9 @@
systemd1,
systemd-stub7,
- objcopy1
+ objcopy1,
+ systemd-creds1,
+ systemd-cryptsetup@.service8
diff --git a/src/boot/measure.c b/src/boot/measure.c
index 33117fe9396..33821688698 100644
--- a/src/boot/measure.c
+++ b/src/boot/measure.c
@@ -14,6 +14,7 @@
#include "parse-argument.h"
#include "parse-util.h"
#include "pretty-print.h"
+#include "sha256.h"
#include "terminal-util.h"
#include "tpm-pcr.h"
#include "tpm2-util.h"
@@ -24,11 +25,17 @@
static char *arg_sections[_UNIFIED_SECTION_MAX] = {};
static char **arg_banks = NULL;
-static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
+static char *arg_tpm2_device = NULL;
+static char *arg_private_key = NULL;
+static char *arg_public_key = NULL;
+static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO|JSON_FORMAT_OFF;
static PagerFlags arg_pager_flags = 0;
static bool arg_current = false;
STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep);
+STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_private_key, freep);
+STATIC_DESTRUCTOR_REGISTER(arg_public_key, freep);
static inline void free_sections(char*(*sections)[_UNIFIED_SECTION_MAX]) {
for (UnifiedSection c = 0; c < _UNIFIED_SECTION_MAX; c++)
@@ -46,10 +53,11 @@ static int help(int argc, char *argv[], void *userdata) {
return log_oom();
printf("%1$s [OPTIONS...] COMMAND ...\n"
- "\n%5$sPre-calculate PCR hash for kernel image.%6$s\n"
+ "\n%5$sPre-calculate and sign PCR hash for a unified kernel image.%6$s\n"
"\n%3$sCommands:%4$s\n"
- " status Show current PCR values\n"
- " calculate Calculate expected PCR values\n"
+ " status Show current PCR values\n"
+ " calculate Calculate expected PCR values\n"
+ " sign Calculate and sign expected PCR values\n"
"\n%3$sOptions:%4$s\n"
" -h --help Show this help\n"
" --version Print version\n"
@@ -62,6 +70,9 @@ static int help(int argc, char *argv[], void *userdata) {
" --dtb=PATH Path to Devicetree file\n"
" -c --current Use current PCR values\n"
" --bank=DIGEST Select TPM bank (SHA1, SHA256)\n"
+ " --tpm2-device=PATH Use specified TPM2 device\n"
+ " --private-key=KEY Private key (PEM) to sign with\n"
+ " --public-key=KEY Public key (PEM) to validate against\n"
" --json=MODE Output as JSON\n"
" -j Same as --json=pretty on tty, --json=short otherwise\n"
"\nSee the %2$s for details.\n",
@@ -88,6 +99,9 @@ static int parse_argv(int argc, char *argv[]) {
_ARG_SECTION_LAST,
ARG_DTB = _ARG_SECTION_LAST,
ARG_BANK,
+ ARG_PRIVATE_KEY,
+ ARG_PUBLIC_KEY,
+ ARG_TPM2_DEVICE,
ARG_JSON,
};
@@ -103,6 +117,9 @@ static int parse_argv(int argc, char *argv[]) {
{ "dtb", required_argument, NULL, ARG_DTB },
{ "current", no_argument, NULL, 'c' },
{ "bank", required_argument, NULL, ARG_BANK },
+ { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE },
+ { "private-key", required_argument, NULL, ARG_PRIVATE_KEY },
+ { "public-key", required_argument, NULL, ARG_PUBLIC_KEY },
{ "json", required_argument, NULL, ARG_JSON },
{}
};
@@ -155,6 +172,36 @@ static int parse_argv(int argc, char *argv[]) {
break;
}
+ case ARG_PRIVATE_KEY:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_private_key);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case ARG_PUBLIC_KEY:
+ r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_public_key);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case ARG_TPM2_DEVICE: {
+ _cleanup_free_ char *device = NULL;
+
+ if (streq(optarg, "list"))
+ return tpm2_list_devices();
+
+ if (!streq(optarg, "auto")) {
+ device = strdup(optarg);
+ if (!device)
+ return log_oom();
+ }
+
+ free_and_replace(arg_tpm2_device, device);
+ break;
+ }
+
case 'j':
arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO;
break;
@@ -377,14 +424,9 @@ static int measure_pcr(PcrState *pcr_states, size_t n) {
return 0;
}
-static int verb_calculate(int argc, char *argv[], void *userdata) {
+static int pcr_states_allocate(PcrState **ret) {
_cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL;
- _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
size_t n = 0;
- int r;
-
- if (!arg_sections[UNIFIED_SECTION_LINUX] && !arg_current)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either --linux= or --current must be specified, refusing.");
pcr_states = new0(PcrState, strv_length(arg_banks) + 1);
if (!pcr_states)
@@ -419,6 +461,25 @@ static int verb_calculate(int argc, char *argv[], void *userdata) {
};
}
+ *ret = TAKE_PTR(pcr_states);
+ return (int) n;
+}
+
+static int verb_calculate(int argc, char *argv[], void *userdata) {
+ _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
+ _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL;
+ size_t n;
+ int r;
+
+ if (!arg_sections[UNIFIED_SECTION_LINUX] && !arg_current)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either --linux= or --current must be specified, refusing.");
+
+ r = pcr_states_allocate(&pcr_states);
+ if (r < 0)
+ return r;
+
+ n = (size_t) r;
+
r = measure_pcr(pcr_states, n);
if (r < 0)
return r;
@@ -464,6 +525,235 @@ static int verb_calculate(int argc, char *argv[], void *userdata) {
return 0;
}
+static TPM2_ALG_ID convert_evp_md_name_to_tpm2_alg(const EVP_MD *md) {
+ const char *mdname;
+
+ mdname = EVP_MD_name(md);
+ if (strcaseeq(mdname, "sha1"))
+ return TPM2_ALG_SHA1;
+ if (strcaseeq(mdname, "sha256"))
+ return TPM2_ALG_SHA256;
+ if (strcaseeq(mdname, "sha384"))
+ return TPM2_ALG_SHA384;
+ if (strcaseeq(mdname, "sha512"))
+ return TPM2_ALG_SHA512;
+
+ return TPM2_ALG_ERROR;
+}
+
+static int verb_sign(int argc, char *argv[], void *userdata) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(pcr_state_free_all) PcrState *pcr_states = NULL;
+ _cleanup_(EVP_PKEY_freep) EVP_PKEY *privkey = NULL, *pubkey = NULL;
+ _cleanup_(tpm2_context_destroy) struct tpm2_context c = {};
+ _cleanup_fclose_ FILE *privkeyf = NULL , *pubkeyf = NULL;
+ ESYS_TR session_handle = ESYS_TR_NONE;
+ TSS2_RC rc;
+ size_t n;
+ int r;
+
+ if (!arg_sections[UNIFIED_SECTION_LINUX] && !arg_current)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Either --linux= or --current must be specified, refusing.");
+
+ if (!arg_private_key)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No private key specified, use --private-key=.");
+ if (!arg_public_key)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No public key specified, use --public-key=.");
+
+ /* When signing we only support JSON output */
+ arg_json_format_flags &= ~JSON_FORMAT_OFF;
+
+ privkeyf = fopen(arg_private_key, "re");
+ if (!privkeyf)
+ return log_error_errno(errno, "Failed to open private key file '%s': %m", arg_private_key);
+
+ pubkeyf = fopen(arg_public_key, "re");
+ if (!pubkeyf)
+ return log_error_errno(errno, "Failed to open public key file '%s': %m", arg_public_key);
+
+ privkey = PEM_read_PrivateKey(privkeyf, NULL, NULL, NULL);
+ if (!privkey)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse private key '%s'.", arg_private_key);
+
+ pubkey = PEM_read_PUBKEY(pubkeyf, NULL, NULL, NULL);
+ if (!pubkey)
+ return log_error_errno(SYNTHETIC_ERRNO(EIO), "Failed to parse public key '%s'.", arg_public_key);
+
+ r = pcr_states_allocate(&pcr_states);
+ if (r < 0)
+ return r;
+
+ n = (size_t) r;
+
+ r = measure_pcr(pcr_states, n);
+ if (r < 0)
+ return r;
+
+ r = dlopen_tpm2();
+ if (r < 0)
+ return r;
+
+ r = tpm2_context_init(arg_tpm2_device, &c);
+ if (r < 0)
+ return r;
+
+ for (size_t i = 0; i < n; i++) {
+ static const TPMT_SYM_DEF symmetric = {
+ .algorithm = TPM2_ALG_AES,
+ .keyBits.aes = 128,
+ .mode.aes = TPM2_ALG_CFB,
+ };
+ PcrState *p = pcr_states + i;
+
+ rc = sym_Esys_StartAuthSession(
+ c.esys_context,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ NULL,
+ TPM2_SE_TRIAL,
+ &symmetric,
+ TPM2_ALG_SHA256,
+ &session_handle);
+ if (rc != TSS2_RC_SUCCESS) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to open session in TPM: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
+
+ /* Generate a single hash value from the PCRs included in our policy. Given that that's
+ * exactly one, the calculation is trivial. */
+ TPM2B_DIGEST intermediate_digest = {
+ .size = SHA256_DIGEST_SIZE,
+ };
+ assert(sizeof(intermediate_digest.buffer) >= SHA256_DIGEST_SIZE);
+ sha256_direct(p->value, p->value_size, intermediate_digest.buffer);
+
+ TPM2_ALG_ID tpmalg = convert_evp_md_name_to_tpm2_alg(p->md);
+ if (tpmalg == TPM2_ALG_ERROR) {
+ r = log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unsupported PCR bank");
+ goto finish;
+ }
+
+ TPML_PCR_SELECTION pcr_selection;
+ tpm2_pcr_mask_to_selection(1 << TPM_PCR_INDEX_KERNEL_IMAGE, tpmalg, &pcr_selection);
+
+ rc = sym_Esys_PolicyPCR(
+ c.esys_context,
+ session_handle,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ &intermediate_digest,
+ &pcr_selection);
+ if (rc != TSS2_RC_SUCCESS) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to push PCR policy into TPM: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
+
+ _cleanup_(Esys_Freep) TPM2B_DIGEST *pcr_policy_digest = NULL;
+ rc = sym_Esys_PolicyGetDigest(
+ c.esys_context,
+ session_handle,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ ESYS_TR_NONE,
+ &pcr_policy_digest);
+ if (rc != TSS2_RC_SUCCESS) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to get policy digest from TPM: %s", sym_Tss2_RC_Decode(rc));
+ goto finish;
+ }
+
+ session_handle = tpm2_flush_context_verbose(c.esys_context, session_handle);
+
+ _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX* mdctx = NULL;
+ mdctx = EVP_MD_CTX_new();
+ if (!mdctx) {
+ r = log_oom();
+ goto finish;
+ }
+
+ if (EVP_DigestSignInit(mdctx, NULL, p->md, NULL, privkey) != 1) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to initialize signature context.");
+ goto finish;
+ }
+
+ if (EVP_DigestSignUpdate(mdctx, pcr_policy_digest->buffer, pcr_policy_digest->size) != 1) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to sign data.");
+ goto finish;
+ }
+
+ size_t ss;
+ if (EVP_DigestSignFinal(mdctx, NULL, &ss) != 1) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to finalize signature");
+ goto finish;
+ }
+
+ _cleanup_free_ void *sig = malloc(ss);
+ if (!ss) {
+ r = log_oom();
+ goto finish;
+ }
+
+ if (EVP_DigestSignFinal(mdctx, sig, &ss) != 1) {
+ r = log_error_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
+ "Failed to acquire signature data");
+ goto finish;
+ }
+
+ _cleanup_free_ void *pubkey_fp = NULL;
+ size_t pubkey_fp_size = 0;
+ r = pubkey_fingerprint(pubkey, EVP_sha256(), &pubkey_fp, &pubkey_fp_size);
+ if (r < 0)
+ goto finish;
+
+ _cleanup_(json_variant_unrefp) JsonVariant *bv = NULL, *a = NULL;
+
+ r = tpm2_make_pcr_json_array(UINT64_C(1) << TPM_PCR_INDEX_KERNEL_IMAGE, &a);
+ if (r < 0) {
+ log_error_errno(r, "Failed to build JSON PCR mask array: %m");
+ goto finish;
+ }
+
+ r = json_build(&bv, JSON_BUILD_ARRAY(
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("pcrs", JSON_BUILD_VARIANT(a)), /* PCR mask */
+ JSON_BUILD_PAIR("pkfp", JSON_BUILD_HEX(pubkey_fp, pubkey_fp_size)), /* SHA256 fingerprint of public key (DER) used for the signature */
+ JSON_BUILD_PAIR("pol", JSON_BUILD_HEX(pcr_policy_digest->buffer, pcr_policy_digest->size)), /* TPM2 policy hash that is signed */
+ JSON_BUILD_PAIR("sig", JSON_BUILD_BASE64(sig, ss))))); /* signature data */
+ if (r < 0) {
+ log_error_errno(r, "Failed to build JSON object: %m");
+ goto finish;
+ }
+
+ r = json_variant_set_field(&v, p->bank, bv);
+ if (r < 0) {
+ log_error_errno(r, "Failed to add JSON field: %m");
+ goto finish;
+ }
+ }
+
+ if (!v)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unable to find a single working PCR bank.");
+
+ if (arg_json_format_flags & (JSON_FORMAT_PRETTY|JSON_FORMAT_PRETTY_AUTO))
+ pager_open(arg_pager_flags);
+
+ json_variant_dump(v, arg_json_format_flags, stdout, NULL);
+ r = 0;
+
+finish:
+ session_handle = tpm2_flush_context_verbose(c.esys_context, session_handle);
+ return r;
+}
+
static int compare_reported_pcr_nr(uint32_t pcr, const char *varname, const char *description) {
_cleanup_free_ char *s = NULL;
uint32_t v;
@@ -641,6 +931,7 @@ static int measure_main(int argc, char *argv[]) {
{ "help", VERB_ANY, VERB_ANY, 0, help },
{ "status", VERB_ANY, 1, VERB_DEFAULT, verb_status },
{ "calculate", VERB_ANY, 1, 0, verb_calculate },
+ { "sign", VERB_ANY, 1, 0, verb_sign },
{}
};