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 }, {} };