From 1acec1c89071e19508d9875c1ebd41fe8996215d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 10 Jun 2024 14:56:11 +0200 Subject: [PATCH 01/22] fileio: make filename/path argument optional in xfopenat_full(), read_full_file_full() --- src/basic/fileio.c | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/basic/fileio.c b/src/basic/fileio.c index ca763a315b..5d9ccbfb84 100644 --- a/src/basic/fileio.c +++ b/src/basic/fileio.c @@ -827,7 +827,6 @@ int read_full_file_full( XfopenFlags xflags = XFOPEN_UNLOCKED; int r; - assert(filename); assert(ret_contents); if (FLAGS_SET(flags, READ_FULL_FILE_CONNECT_SOCKET) && /* If this is enabled, let's try to connect to it */ @@ -1002,11 +1001,10 @@ static int xfopenat_regular(int dir_fd, const char *path, const char *mode, int /* A combination of fopen() with openat() */ assert(dir_fd >= 0 || dir_fd == AT_FDCWD); - assert(path); assert(mode); assert(ret); - if (dir_fd == AT_FDCWD && open_flags == 0) + if (dir_fd == AT_FDCWD && open_flags == 0 && path) f = fopen(path, mode); else { _cleanup_close_ int fd = -EBADF; @@ -1016,9 +1014,18 @@ static int xfopenat_regular(int dir_fd, const char *path, const char *mode, int if (mode_flags < 0) return mode_flags; - fd = openat(dir_fd, path, mode_flags | open_flags); - if (fd < 0) - return -errno; + if (path) { + fd = openat(dir_fd, path, mode_flags | open_flags); + if (fd < 0) + return -errno; + } else { + if (dir_fd == AT_FDCWD) + return -EBADF; + + fd = fd_reopen(dir_fd, mode_flags | open_flags); + if (fd < 0) + return fd; + } f = take_fdopen(&fd, mode); } @@ -1035,7 +1042,6 @@ static int xfopenat_unix_socket(int dir_fd, const char *path, const char *bind_n int r; assert(dir_fd >= 0 || dir_fd == AT_FDCWD); - assert(path); assert(ret); sk = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0); @@ -1084,7 +1090,6 @@ int xfopenat_full( int r; assert(dir_fd >= 0 || dir_fd == AT_FDCWD); - assert(path); assert(mode); assert(ret); From 7ecc69c33b749ec98eaba6e3c34fe42da79d3a8d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 10 Jun 2024 14:55:24 +0200 Subject: [PATCH 02/22] fileio: add new flag READ_FULL_FILE_VERIFY_REGULAR() that checks if file we operate on is regular --- src/basic/fileio.c | 9 ++++++++- src/basic/fileio.h | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/basic/fileio.c b/src/basic/fileio.c index 5d9ccbfb84..a493384c52 100644 --- a/src/basic/fileio.c +++ b/src/basic/fileio.c @@ -664,6 +664,12 @@ int read_full_stream_full( if (fstat(fd, &st) < 0) return -errno; + if (FLAGS_SET(flags, READ_FULL_FILE_VERIFY_REGULAR)) { + r = stat_verify_regular(&st); + if (r < 0) + return r; + } + if (S_ISREG(st.st_mode)) { /* Try to start with the right file size if we shall read the file in full. Note @@ -685,7 +691,8 @@ int read_full_stream_full( if (flags & READ_FULL_FILE_WARN_WORLD_READABLE) (void) warn_file_is_world_accessible(filename, &st, NULL, 0); } - } + } else if (FLAGS_SET(flags, READ_FULL_FILE_VERIFY_REGULAR)) + return -EBADFD; /* If we don't know how much to read, figure it out now. If we shall read a part of the file, then * allocate the requested size. If we shall load the full file start with LINE_MAX. Note that if diff --git a/src/basic/fileio.h b/src/basic/fileio.h index 96a8773620..578c16c0ee 100644 --- a/src/basic/fileio.h +++ b/src/basic/fileio.h @@ -30,6 +30,7 @@ typedef enum { READ_FULL_FILE_WARN_WORLD_READABLE = 1 << 3, /* if regular file, log at LOG_WARNING level if access mode above 0700 */ READ_FULL_FILE_CONNECT_SOCKET = 1 << 4, /* if socket inode, connect to it and read off it */ READ_FULL_FILE_FAIL_WHEN_LARGER = 1 << 5, /* fail loading if file is larger than specified size */ + READ_FULL_FILE_VERIFY_REGULAR = 1 << 6, /* before reading, verify this is a regular file */ } ReadFullFileFlags; int fdopen_unlocked(int fd, const char *options, FILE **ret); From 86279dc970bdab56ed5bfc7e363e6288e5e650b2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 4 Jun 2024 11:04:01 +0200 Subject: [PATCH 03/22] format-table: add field type that outputs hex values prefixed with 0x --- src/shared/format-table.c | 32 ++++++++++++++++++++++++++++++++ src/shared/format-table.h | 2 ++ 2 files changed, 34 insertions(+) diff --git a/src/shared/format-table.c b/src/shared/format-table.c index c129f260e8..1c728a3d2b 100644 --- a/src/shared/format-table.c +++ b/src/shared/format-table.c @@ -313,12 +313,14 @@ static size_t table_data_size(TableDataType type, const void *data) { case TABLE_INT64: case TABLE_UINT64: case TABLE_UINT64_HEX: + case TABLE_UINT64_HEX_0x: case TABLE_BPS: return sizeof(uint64_t); case TABLE_INT32: case TABLE_UINT32: case TABLE_UINT32_HEX: + case TABLE_UINT32_HEX_0x: return sizeof(uint32_t); case TABLE_INT16: @@ -1028,12 +1030,14 @@ int table_add_many_internal(Table *t, TableDataType first_type, ...) { case TABLE_UINT32: case TABLE_UINT32_HEX: + case TABLE_UINT32_HEX_0x: buffer.uint32 = va_arg(ap, uint32_t); data = &buffer.uint32; break; case TABLE_UINT64: case TABLE_UINT64_HEX: + case TABLE_UINT64_HEX_0x: buffer.uint64 = va_arg(ap, uint64_t); data = &buffer.uint64; break; @@ -1455,10 +1459,12 @@ static int cell_data_compare(TableData *a, size_t index_a, TableData *b, size_t case TABLE_UINT32: case TABLE_UINT32_HEX: + case TABLE_UINT32_HEX_0x: return CMP(a->uint32, b->uint32); case TABLE_UINT64: case TABLE_UINT64_HEX: + case TABLE_UINT64_HEX_0x: return CMP(a->uint64, b->uint64); case TABLE_PERCENT: @@ -1859,6 +1865,18 @@ static const char *table_data_format(Table *t, TableData *d, bool avoid_uppercas break; } + case TABLE_UINT32_HEX_0x: { + _cleanup_free_ char *p = NULL; + + p = new(char, 2 + 8 + 1); + if (!p) + return NULL; + + sprintf(p, "0x%" PRIx32, d->uint32); + d->formatted = TAKE_PTR(p); + break; + } + case TABLE_UINT64: { _cleanup_free_ char *p = NULL; @@ -1883,6 +1901,18 @@ static const char *table_data_format(Table *t, TableData *d, bool avoid_uppercas break; } + case TABLE_UINT64_HEX_0x: { + _cleanup_free_ char *p = NULL; + + p = new(char, 2 + 16 + 1); + if (!p) + return NULL; + + sprintf(p, "0x%" PRIx64, d->uint64); + d->formatted = TAKE_PTR(p); + break; + } + case TABLE_PERCENT: { _cleanup_free_ char *p = NULL; @@ -2822,10 +2852,12 @@ static int table_data_to_json(TableData *d, sd_json_variant **ret) { case TABLE_UINT32: case TABLE_UINT32_HEX: + case TABLE_UINT32_HEX_0x: return sd_json_variant_new_unsigned(ret, d->uint32); case TABLE_UINT64: case TABLE_UINT64_HEX: + case TABLE_UINT64_HEX_0x: return sd_json_variant_new_unsigned(ret, d->uint64); case TABLE_PERCENT: diff --git a/src/shared/format-table.h b/src/shared/format-table.h index 2222373eb3..b0b9b2060e 100644 --- a/src/shared/format-table.h +++ b/src/shared/format-table.h @@ -40,8 +40,10 @@ typedef enum TableDataType { TABLE_UINT16, TABLE_UINT32, TABLE_UINT32_HEX, + TABLE_UINT32_HEX_0x, TABLE_UINT64, TABLE_UINT64_HEX, + TABLE_UINT64_HEX_0x, TABLE_PERCENT, TABLE_IFINDEX, TABLE_IN_ADDR, /* Takes a union in_addr_union (or a struct in_addr) */ From d2d1fc59b4e7eae5eeb1e50c6ad42e8d5969daa2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 27 Oct 2025 12:22:02 +0100 Subject: [PATCH 04/22] conf-files: optionally truncate suffix from discovered files --- src/basic/conf-files.c | 47 +++++++++++++++++++++++++++++++++++------- src/basic/conf-files.h | 13 ++++++------ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/basic/conf-files.c b/src/basic/conf-files.c index c00325f2e4..77affe269c 100644 --- a/src/basic/conf-files.c +++ b/src/basic/conf-files.c @@ -416,7 +416,13 @@ static int dump_files(Hashmap *fh, const char *root, ConfFile ***ret_files, size return 0; } -static int copy_and_sort_files_from_hashmap(Hashmap *fh, const char *root, ConfFilesFlags flags, char ***ret) { +static int copy_and_sort_files_from_hashmap( + Hashmap *fh, + const char *suffix, + const char *root, + ConfFilesFlags flags, + char ***ret) { + _cleanup_strv_free_ char **results = NULL; _cleanup_free_ ConfFile **files = NULL; size_t n_files = 0, n_results = 0; @@ -432,19 +438,44 @@ static int copy_and_sort_files_from_hashmap(Hashmap *fh, const char *root, ConfF FOREACH_ARRAY(i, files, n_files) { ConfFile *c = *i; + const char *add = NULL; if (FLAGS_SET(flags, CONF_FILES_BASENAME)) - r = strv_extend_with_size(&results, &n_results, c->name); + add = c->name; else if (root) { - char *p; + _cleanup_free_ char *p = NULL; r = chaseat_prefix_root(c->result, root, &p); if (r < 0) return log_debug_errno(r, "Failed to prefix '%s' with root '%s': %m", c->result, root); - r = strv_consume_with_size(&results, &n_results, TAKE_PTR(p)); + if (FLAGS_SET(flags, CONF_FILES_TRUNCATE_SUFFIX) && suffix) { + char *e = endswith(p, suffix); + if (!e) + continue; + + *e = 0; + } + + if (strv_consume_with_size(&results, &n_results, TAKE_PTR(p)) < 0) + return log_oom_debug(); + + continue; } else - r = strv_extend_with_size(&results, &n_results, c->result); + add = c->result; + + if (FLAGS_SET(flags, CONF_FILES_TRUNCATE_SUFFIX)) { + const char *e = endswith(add, suffix); + if (!e) + continue; + + _cleanup_free_ char *n = strndup(add, e - add); + if (!n) + return log_oom_debug(); + + r = strv_consume_with_size(&results, &n_results, TAKE_PTR(n)); + } else + r = strv_extend_with_size(&results, &n_results, add); if (r < 0) return log_oom_debug(); } @@ -566,7 +597,7 @@ int conf_files_list_strv( if (r < 0) return r; - return copy_and_sort_files_from_hashmap(fh, empty_to_root(root_abs), flags, ret); + return copy_and_sort_files_from_hashmap(fh, suffix, empty_to_root(root_abs), flags, ret); } int conf_files_list_strv_full( @@ -619,7 +650,7 @@ int conf_files_list_strv_at( if (r < 0) return r; - return copy_and_sort_files_from_hashmap(fh, /* root = */ NULL, flags, ret); + return copy_and_sort_files_from_hashmap(fh, suffix, /* root = */ NULL, flags, ret); } int conf_files_list_strv_at_full( @@ -747,7 +778,7 @@ int conf_files_list_with_replacement( return log_debug_errno(r, "Failed to prefix '%s' with root '%s': %m", c->result, empty_to_root(root_abs)); } - r = copy_and_sort_files_from_hashmap(fh, empty_to_root(root_abs), flags, ret_files); + r = copy_and_sort_files_from_hashmap(fh, ".conf", empty_to_root(root_abs), flags, ret_files); if (r < 0) return r; diff --git a/src/basic/conf-files.h b/src/basic/conf-files.h index f6e63120e5..05a2966e21 100644 --- a/src/basic/conf-files.h +++ b/src/basic/conf-files.h @@ -6,13 +6,14 @@ #include "basic-forward.h" typedef enum ConfFilesFlags { - CONF_FILES_EXECUTABLE = 1 << 0, - CONF_FILES_REGULAR = 1 << 1, - CONF_FILES_DIRECTORY = 1 << 2, - CONF_FILES_BASENAME = 1 << 3, - CONF_FILES_FILTER_MASKED_BY_SYMLINK = 1 << 4, - CONF_FILES_FILTER_MASKED_BY_EMPTY = 1 << 5, + CONF_FILES_EXECUTABLE = 1 << 0, /* inode must be marked executable */ + CONF_FILES_REGULAR = 1 << 1, /* inode must be regular file */ + CONF_FILES_DIRECTORY = 1 << 2, /* inode must be directory */ + CONF_FILES_BASENAME = 1 << 3, /* only return basename of file, not full path */ + CONF_FILES_FILTER_MASKED_BY_SYMLINK = 1 << 4, /* implement /dev/null symlink based masking */ + CONF_FILES_FILTER_MASKED_BY_EMPTY = 1 << 5, /* implement masking by empty file */ CONF_FILES_FILTER_MASKED = CONF_FILES_FILTER_MASKED_BY_SYMLINK | CONF_FILES_FILTER_MASKED_BY_EMPTY, + CONF_FILES_TRUNCATE_SUFFIX = 1 << 6, /* truncate specified suffix from return filename or path */ } ConfFilesFlags; typedef struct ConfFile { From c30e3d729000b06a5709ea79eaf0ea8dd42f4819 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 6 Jun 2024 11:25:50 +0200 Subject: [PATCH 05/22] creds-util: add automatic mode for tpm2 based creds This reworkds TPM2 based creds a bit. Instead of mapping the key type "tpm2" directly to a TPM2 key without PK, let's map it to an "automatic" key type that either picks PK or doesn't, depending on what's available. That should make things easier to grok for people, as the nitty gritty details of PK or not PK are made autmatic. Moreover it gives us more leverage to change the TPM2 enrollment types later (for example, we definitely want to start pinning SRK, and hook up pcrlock too, for creds, which we currently don't). This hence adds a new _CRED_AUTO_TPM2 pseudo-type we automatically maps to CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK or CRED_AES256_GCM_BY_TPM2_HMAC depending if PK as available. Similar, _CRED_AUTO_HOST_AND_TPM2 is added, which does the same for the host/nonhost cred type. This does not introduce any new type on the wire, it just changes how we select the right key type. To make the code more readable this also adds some categorization macros for the keys, instead of repeating the list of key types at multiple places. --- man/systemd-creds.xml | 6 +-- src/creds/creds.c | 3 +- src/shared/creds-util.c | 95 +++++++++++------------------------------ src/shared/creds-util.h | 85 +++++++++++++++++++++++++++++++++--- 4 files changed, 110 insertions(+), 79 deletions(-) diff --git a/man/systemd-creds.xml b/man/systemd-creds.xml index 7b9ce14e5f..60ded42193 100644 --- a/man/systemd-creds.xml +++ b/man/systemd-creds.xml @@ -327,15 +327,15 @@ When specified with the encrypt command controls the encryption/signature key to use. Takes one of host, tpm2, host+tpm2, null, auto, - auto-initrd. See above for details on the three key types. If set to + auto-initrd. See above for details on the key types. If set to auto (which is the default) the TPM2 key is used if a TPM2 device is found and not running in a container. The host key is used if /var/lib/systemd/ is on persistent media. This means on typical systems the encryption is by default bound to both the TPM2 chip and the OS installation, and both need to be available to decrypt the credential again. If auto is selected but neither TPM2 is available (or running in container) nor /var/lib/systemd/ is on persistent media, encryption will fail. If set to - null a fixed zero length key is used (thus, in this mode no confidentiality - nor authenticity are provided!). This logic is useful to cover for systems that lack a TPM2 chip but + null a fixed zero length key is used (thus, in this mode no confidentiality nor + authenticity are provided!). This logic is useful to cover for systems that lack a TPM2 chip but where credentials shall be generated. Note that decryption of such credentials is refused on systems that have a TPM2 chip and where UEFI SecureBoot is enabled (this is done so that such a locked down system cannot be tricked into loading a credential generated this way that lacks authentication diff --git a/src/creds/creds.c b/src/creds/creds.c index 92b527270d..a4a90dc883 100644 --- a/src/creds/creds.c +++ b/src/creds/creds.c @@ -958,7 +958,6 @@ static int parse_argv(int argc, char *argv[]) { if (streq(optarg, "help")) { if (arg_legend) puts("Supported key types:"); - return DUMP_STRING_TABLE(cred_key_type, CredKeyType, _CRED_KEY_TYPE_MAX); } @@ -978,7 +977,7 @@ static int parse_argv(int argc, char *argv[]) { break; case 'T': - arg_with_key = CRED_AES256_GCM_BY_TPM2_HMAC; + arg_with_key = _CRED_AUTO_TPM2; break; case ARG_TPM2_DEVICE: diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index fe3fce3576..a190e3720c 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -815,19 +815,7 @@ int encrypt_credential_and_warn( /* Only one of these two flags may be set at the same time */ assert(!FLAGS_SET(flags, CREDENTIAL_ALLOW_NULL) || !FLAGS_SET(flags, CREDENTIAL_REFUSE_NULL)); - if (!sd_id128_in_set(with_key, - _CRED_AUTO, - _CRED_AUTO_INITRD, - _CRED_AUTO_SCOPED, - CRED_AES256_GCM_BY_HOST, - CRED_AES256_GCM_BY_HOST_SCOPED, - CRED_AES256_GCM_BY_TPM2_HMAC, - CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK, - CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC, - CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED, - CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK, - CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED, - CRED_AES256_GCM_BY_NULL)) + if (!CRED_KEY_IS_VALID(with_key) && !CRED_KEY_IS_AUTO(with_key)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid key type: " SD_ID128_FORMAT_STR, SD_ID128_FORMAT_VAL(with_key)); if (name && !credential_name_valid(name)) @@ -847,33 +835,21 @@ int encrypt_credential_and_warn( log_debug("Including not-after timestamp '%s' in encrypted credential.", format_timestamp(buf, sizeof(buf), not_after)); } - if (sd_id128_in_set(with_key, - _CRED_AUTO_SCOPED, - CRED_AES256_GCM_BY_HOST_SCOPED, - CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED, - CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED)) { + if (CRED_KEY_IS_SCOPED(with_key)) { if (!uid_is_valid(uid)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Scoped credential key type "SD_ID128_FORMAT_STR" selected, but no UID specified.", SD_ID128_FORMAT_VAL(with_key)); } else uid = UID_INVALID; - if (sd_id128_in_set(with_key, - _CRED_AUTO, - _CRED_AUTO_SCOPED, - CRED_AES256_GCM_BY_HOST, - CRED_AES256_GCM_BY_HOST_SCOPED, - CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC, - CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED, - CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK, - CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED)) { + if (CRED_KEY_WANTS_HOST(with_key) || CRED_KEY_REQUIRES_HOST(with_key)) { r = get_credential_host_secret( CREDENTIAL_SECRET_GENERATE| CREDENTIAL_SECRET_WARN_NOT_ENCRYPTED| - (sd_id128_in_set(with_key, _CRED_AUTO, _CRED_AUTO_SCOPED) ? CREDENTIAL_SECRET_FAIL_ON_TEMPORARY_FS : 0), + (CRED_KEY_WANTS_HOST(with_key) ? CREDENTIAL_SECRET_FAIL_ON_TEMPORARY_FS : 0), &host_key); - if (r == -ENOMEDIUM && sd_id128_in_set(with_key, _CRED_AUTO, _CRED_AUTO_SCOPED)) + if (r == -ENOMEDIUM && CRED_KEY_WANTS_HOST(with_key)) log_debug_errno(r, "Credential host secret location on temporary file system, not using."); else if (r < 0) return log_error_errno(r, "Failed to determine local credential host secret: %m"); @@ -881,7 +857,7 @@ int encrypt_credential_and_warn( #if HAVE_TPM2 bool try_tpm2; - if (sd_id128_in_set(with_key, _CRED_AUTO, _CRED_AUTO_INITRD, _CRED_AUTO_SCOPED)) { + if (CRED_KEY_WANTS_TPM2(with_key)) { /* If automatic mode is selected lets see if a TPM2 it is present. If we are running in a * container tpm2_support will detect this, and will return a different flag combination of * TPM2_SUPPORT_FULL, effectively skipping the use of TPM2 when inside one. */ @@ -890,28 +866,16 @@ int encrypt_credential_and_warn( if (!try_tpm2) log_debug("System lacks TPM2 support or running in a container, not attempting to use TPM2."); } else - try_tpm2 = sd_id128_in_set(with_key, - CRED_AES256_GCM_BY_TPM2_HMAC, - CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK, - CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC, - CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED, - CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK, - CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED); + try_tpm2 = CRED_KEY_REQUIRES_TPM2(with_key); if (try_tpm2) { - if (sd_id128_in_set(with_key, - _CRED_AUTO, - _CRED_AUTO_INITRD, - _CRED_AUTO_SCOPED, - CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK, - CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK, - CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED)) { + if (CRED_KEY_WANTS_TPM2_PK(with_key) || CRED_KEY_REQUIRES_TPM2_PK(with_key)) { /* Load public key for PCR policies, if one is specified, or explicitly requested */ r = tpm2_load_pcr_public_key(tpm2_pubkey_path, &pubkey.iov_base, &pubkey.iov_len); if (r < 0) { - if (tpm2_pubkey_path || r != -ENOENT || !sd_id128_in_set(with_key, _CRED_AUTO, _CRED_AUTO_INITRD, _CRED_AUTO_SCOPED)) + if (r != -ENOENT || tpm2_pubkey_path || CRED_KEY_REQUIRES_TPM2_PK(with_key)) return log_error_errno(r, "Failed to read TPM PCR public key: %m"); log_debug_errno(r, "Failed to read TPM2 PCR public key, proceeding without: %m"); @@ -991,7 +955,7 @@ int encrypt_credential_and_warn( } #endif - if (sd_id128_in_set(with_key, _CRED_AUTO, _CRED_AUTO_INITRD, _CRED_AUTO_SCOPED)) { + if (CRED_KEY_IS_AUTO(with_key)) { /* Let's settle the key type in auto mode now. */ if (iovec_is_set(&host_key) && iovec_is_set(&tpm2_key)) @@ -1200,7 +1164,6 @@ int decrypt_credential_and_warn( struct encrypted_credential_header *h; struct metadata_credential_header *m; uint8_t md[SHA256_DIGEST_LENGTH]; - bool with_tpm2, with_tpm2_pk, with_host_key, with_null, with_scope; const EVP_CIPHER *cc; size_t p, hs; int r, added; @@ -1230,16 +1193,10 @@ int decrypt_credential_and_warn( if (input->iov_len < sizeof(h->id)) return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Encrypted file too short."); - with_host_key = sd_id128_in_set(h->id, CRED_AES256_GCM_BY_HOST, CRED_AES256_GCM_BY_HOST_SCOPED, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED); - with_tpm2_pk = sd_id128_in_set(h->id, CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED); - with_tpm2 = sd_id128_in_set(h->id, CRED_AES256_GCM_BY_TPM2_HMAC, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED) || with_tpm2_pk; - with_null = sd_id128_equal(h->id, CRED_AES256_GCM_BY_NULL); - with_scope = sd_id128_in_set(h->id, CRED_AES256_GCM_BY_HOST_SCOPED, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED, CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED); - - if (!with_host_key && !with_tpm2 && !with_null) + if (!CRED_KEY_IS_VALID(h->id)) return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unknown encryption format, or corrupted data."); - if (with_tpm2_pk) { + if (CRED_KEY_REQUIRES_TPM2_PK(h->id)) { r = tpm2_load_pcr_signature(tpm2_signature_path, &signature_json); if (r == -ENOENT) return log_error_errno(SYNTHETIC_ERRNO(EHOSTDOWN), "Couldn't find PCR signature file: %m"); @@ -1247,7 +1204,7 @@ int decrypt_credential_and_warn( return log_error_errno(r, "Failed to load PCR signature: %m"); } - if (with_null) { + if (sd_id128_equal(h->id, CRED_AES256_GCM_BY_NULL)) { if (FLAGS_SET(flags, CREDENTIAL_REFUSE_NULL)) return log_error_errno(SYNTHETIC_ERRNO(EHWPOISON), "Credential uses null key, but that's not allowed, refusing."); @@ -1273,7 +1230,7 @@ int decrypt_credential_and_warn( } } - if (with_scope) { + if (CRED_KEY_IS_SCOPED(h->id)) { if (!uid_is_valid(uid)) return log_error_errno(SYNTHETIC_ERRNO(EMEDIUMTYPE), "Encrypted file is scoped to a user, but no user selected."); } else { @@ -1302,16 +1259,16 @@ int decrypt_credential_and_warn( * lower limit only) */ if (input->iov_len < ALIGN8(offsetof(struct encrypted_credential_header, iv) + le32toh(h->iv_size)) + - ALIGN8(with_tpm2 ? offsetof(struct tpm2_credential_header, policy_hash_and_blob) : 0) + - ALIGN8(with_tpm2_pk ? offsetof(struct tpm2_public_key_credential_header, data) : 0) + - ALIGN8(with_scope ? sizeof(struct scoped_credential_header) : 0) + + ALIGN8(CRED_KEY_REQUIRES_TPM2(h->id) ? offsetof(struct tpm2_credential_header, policy_hash_and_blob) : 0) + + ALIGN8(CRED_KEY_REQUIRES_TPM2_PK(h->id) ? offsetof(struct tpm2_public_key_credential_header, data) : 0) + + ALIGN8(CRED_KEY_IS_SCOPED(h->id) ? sizeof(struct scoped_credential_header) : 0) + ALIGN8(offsetof(struct metadata_credential_header, name)) + le32toh(h->tag_size)) return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Encrypted file too short."); p = ALIGN8(offsetof(struct encrypted_credential_header, iv) + le32toh(h->iv_size)); - if (with_tpm2) { + if (CRED_KEY_REQUIRES_TPM2(h->id)) { #if HAVE_TPM2 struct tpm2_credential_header* t = (struct tpm2_credential_header*) ((uint8_t*) input->iov_base + p); struct tpm2_public_key_credential_header *z = NULL; @@ -1332,8 +1289,8 @@ int decrypt_credential_and_warn( if (input->iov_len < p + ALIGN8(offsetof(struct tpm2_credential_header, policy_hash_and_blob) + le32toh(t->blob_size) + le32toh(t->policy_hash_size)) + - ALIGN8(with_tpm2_pk ? offsetof(struct tpm2_public_key_credential_header, data) : 0) + - ALIGN8(with_scope ? sizeof(struct scoped_credential_header) : 0) + + ALIGN8(CRED_KEY_REQUIRES_TPM2_PK(h->id) ? offsetof(struct tpm2_public_key_credential_header, data) : 0) + + ALIGN8(CRED_KEY_IS_SCOPED(h->id) ? sizeof(struct scoped_credential_header) : 0) + ALIGN8(offsetof(struct metadata_credential_header, name)) + le32toh(h->tag_size)) return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Encrypted file too short."); @@ -1342,7 +1299,7 @@ int decrypt_credential_and_warn( le32toh(t->blob_size) + le32toh(t->policy_hash_size)); - if (with_tpm2_pk) { + if (CRED_KEY_REQUIRES_TPM2_PK(h->id)) { z = (struct tpm2_public_key_credential_header*) ((uint8_t*) input->iov_base + p); if (!TPM2_PCR_MASK_VALID(le64toh(z->pcr_mask)) || le64toh(z->pcr_mask) == 0) @@ -1353,7 +1310,7 @@ int decrypt_credential_and_warn( if (input->iov_len < p + ALIGN8(offsetof(struct tpm2_public_key_credential_header, data) + le32toh(z->size)) + - ALIGN8(with_scope ? sizeof(struct scoped_credential_header) : 0) + + ALIGN8(CRED_KEY_IS_SCOPED(h->id) ? sizeof(struct scoped_credential_header) : 0) + ALIGN8(offsetof(struct metadata_credential_header, name)) + le32toh(h->tag_size)) return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Encrypted file too short."); @@ -1395,7 +1352,7 @@ int decrypt_credential_and_warn( #endif } - if (with_scope) { + if (CRED_KEY_IS_SCOPED(h->id)) { struct scoped_credential_header* sh = (struct scoped_credential_header*) ((uint8_t*) input->iov_base + p); if (le64toh(sh->flags) != SCOPE_HASH_DATA_BASE_FLAGS) @@ -1411,18 +1368,18 @@ int decrypt_credential_and_warn( p += sizeof(struct scoped_credential_header); } - if (with_host_key) { + if (CRED_KEY_REQUIRES_HOST(h->id)) { r = get_credential_host_secret(/* flags= */ 0, &host_key); if (r < 0) return log_error_errno(r, "Failed to determine local credential key: %m"); } - if (with_null && !FLAGS_SET(flags, CREDENTIAL_ALLOW_NULL)) + if (sd_id128_equal(h->id, CRED_AES256_GCM_BY_NULL) && !FLAGS_SET(flags, CREDENTIAL_ALLOW_NULL)) log_warning("Warning: using a null key for decryption and authentication. Confidentiality or authenticity are not provided."); sha256_hash_host_and_tpm2_key(&host_key, &tpm2_key, md); - if (with_scope) { + if (CRED_KEY_IS_SCOPED(h->id)) { r = mangle_uid_into_key(uid, md); if (r < 0) return r; diff --git a/src/shared/creds-util.h b/src/shared/creds-util.h index d23afa6c13..e5194c7f07 100644 --- a/src/shared/creds-util.h +++ b/src/shared/creds-util.h @@ -79,15 +79,90 @@ typedef enum CredentialFlags { SD_ID128_MAKE(ad,bc,4c,a3,ef,b6,42,01,ba,88,1b,6f,2e,40,95,ea) #define CRED_AES256_GCM_BY_NULL SD_ID128_MAKE(05,84,69,da,f6,f5,43,24,80,05,49,da,0f,8e,a2,fb) -/* Two special IDs to pick a general automatic mode (i.e. tpm2+host if TPM2 exists, only host otherwise) or - * an initrd-specific automatic mode (i.e. tpm2 if firmware can do it, otherwise fixed zero-length key, and - * never involve host keys). These IDs will never be stored on disk, but are useful only internally while - * figuring out what precisely to write to disk. To mark that these aren't a "real" type, we'll prefix them - * with an underscore. */ +/* Five special IDs to pick a general automatic mode. These IDs will never be stored on disk, but are useful + * only internally while figuring out what precisely to write to disk. To mark that these aren't a "real" + * type, we'll prefix them with an underscore. */ + +/* Use TPM2 if available + host if available and on physical media. If neither are available, fail. */ #define _CRED_AUTO SD_ID128_MAKE(a2,19,cb,07,85,b2,4c,04,b1,6d,18,ca,b9,d2,ee,01) + +/* Use best TPM2, and do not use host, and fail if no TPM */ +#define _CRED_AUTO_TPM2 SD_ID128_MAKE(45,f3,a6,7e,0c,12,42,56,a4,ee,75,eb,44,c6,5a,6f) + +/* Use TPM2 *and* host, and fail if one of the two isn't available. */ +#define _CRED_AUTO_HOST_AND_TPM2 SD_ID128_MAKE(da,f6,7a,60,d3,eb,47,b3,a9,be,2f,d5,fe,c2,15,22) + +/* Like _CRED_AUTO_TPM2, but uses "null" if not TPM is around */ #define _CRED_AUTO_INITRD SD_ID128_MAKE(02,dc,8e,de,3a,02,43,ab,a9,ec,54,9c,05,e6,a0,71) + +/* Like _CRED_AUTO, but with per-UID scoping */ #define _CRED_AUTO_SCOPED SD_ID128_MAKE(23,88,96,85,6f,74,48,8a,9c,78,6f,6a,b0,e7,3b,6a) +#define CRED_KEY_IS_VALID(key) \ + sd_id128_in_set((key), \ + CRED_AES256_GCM_BY_HOST, \ + CRED_AES256_GCM_BY_HOST_SCOPED, \ + CRED_AES256_GCM_BY_TPM2_HMAC, \ + CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK, \ + CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC, \ + CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED, \ + CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK, \ + CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED, \ + CRED_AES256_GCM_BY_NULL) +#define CRED_KEY_IS_AUTO(key) \ + sd_id128_in_set((key), \ + _CRED_AUTO, \ + _CRED_AUTO_TPM2, \ + _CRED_AUTO_HOST_AND_TPM2, \ + _CRED_AUTO_INITRD, \ + _CRED_AUTO_SCOPED) +#define CRED_KEY_IS_SCOPED(key) \ + sd_id128_in_set((key), \ + _CRED_AUTO_SCOPED, \ + CRED_AES256_GCM_BY_HOST_SCOPED, \ + CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED, \ + CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED) +#define CRED_KEY_REQUIRES_HOST(key) \ + sd_id128_in_set((key), \ + _CRED_AUTO_HOST_AND_TPM2, \ + CRED_AES256_GCM_BY_HOST, \ + CRED_AES256_GCM_BY_HOST_SCOPED, \ + CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC, \ + CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED, \ + CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK, \ + CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED) +#define CRED_KEY_WANTS_HOST(key) \ + sd_id128_in_set((key), \ + _CRED_AUTO, \ + _CRED_AUTO_SCOPED) +#define CRED_KEY_REQUIRES_TPM2(key) \ + sd_id128_in_set((key), \ + _CRED_AUTO_TPM2, \ + _CRED_AUTO_HOST_AND_TPM2, \ + CRED_AES256_GCM_BY_TPM2_HMAC, \ + CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK, \ + CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC, \ + CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_SCOPED, \ + CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK, \ + CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED) +#define CRED_KEY_WANTS_TPM2(key) \ + sd_id128_in_set((key), \ + _CRED_AUTO, \ + _CRED_AUTO_INITRD, \ + _CRED_AUTO_SCOPED) +#define CRED_KEY_REQUIRES_TPM2_PK(key) \ + sd_id128_in_set((key), \ + CRED_AES256_GCM_BY_TPM2_HMAC_WITH_PK, \ + CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK, \ + CRED_AES256_GCM_BY_HOST_AND_TPM2_HMAC_WITH_PK_SCOPED) +#define CRED_KEY_WANTS_TPM2_PK(key) \ + sd_id128_in_set((key), \ + _CRED_AUTO, \ + _CRED_AUTO_TPM2, \ + _CRED_AUTO_HOST_AND_TPM2, \ + _CRED_AUTO_INITRD, \ + _CRED_AUTO_SCOPED) + int encrypt_credential_and_warn(sd_id128_t with_key, const char *name, usec_t timestamp, usec_t not_after, const char *tpm2_device, uint32_t tpm2_hash_pcr_mask, const char *tpm2_pubkey_path, uint32_t tpm2_pubkey_pcr_mask, uid_t uid, const struct iovec *input, CredentialFlags flags, struct iovec *ret); int decrypt_credential_and_warn(const char *validate_name, usec_t validate_timestamp, const char *tpm2_device, const char *tpm2_signature_path, uid_t uid, const struct iovec *input, CredentialFlags flags, struct iovec *ret); From ffe958b98ffb9bceceac63db6381bb51329c1abf Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 10 Jun 2024 14:53:38 +0200 Subject: [PATCH 06/22] creds-util: add helper for querying system credential dirs The dirs are constant string, but let's make them overridable via env vars for debugging purposes. --- src/creds/creds.c | 15 ++++++--------- src/shared/creds-util.c | 24 ++++++++++++++++++++++++ src/shared/creds-util.h | 4 ++++ src/shared/tpm2-util.c | 9 ++++++--- 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/creds/creds.c b/src/creds/creds.c index a4a90dc883..383ef268b8 100644 --- a/src/creds/creds.c +++ b/src/creds/creds.c @@ -148,17 +148,14 @@ static int open_credential_directory( if (arg_system) /* PID 1 ensures that system credentials are always accessible under the same fixed path. It * will create symlinks if necessary to guarantee that. */ - p = encrypted ? - ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY : - SYSTEM_CREDENTIALS_DIRECTORY; - else { + r = (encrypted ? get_encrypted_system_credentials_dir : get_system_credentials_dir)(&p); + else /* Otherwise take the dirs from the env vars we got passed */ r = (encrypted ? get_encrypted_credentials_dir : get_credentials_dir)(&p); - if (r == -ENXIO) /* No environment variable? */ - goto not_found; - if (r < 0) - return log_error_errno(r, "Failed to get credentials directory: %m"); - } + if (r == -ENXIO) /* No environment variable? */ + goto not_found; + if (r < 0) + return log_error_errno(r, "Failed to get credentials directory: %m"); d = opendir(p); if (!d) { diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index a190e3720c..c035dd671d 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -125,6 +125,30 @@ int open_credentials_dir(void) { return RET_NERRNO(open(d, O_CLOEXEC|O_DIRECTORY)); } +int get_system_credentials_dir(const char **ret) { + int r; + + /* Note that for system credentials the environment variable we honour is just for debugging purpose + * (unlike for the per-service credential path env var where it's key part of the protocol). */ + r = get_credentials_dir_internal("SYSTEMD_SYSTEM_CREDENTIALS_DIRECTORY", ret); + if (r >= 0 || r != -ENXIO) + return r; + + *ret = SYSTEM_CREDENTIALS_DIRECTORY; + return 0; +} + +int get_encrypted_system_credentials_dir(const char **ret) { + int r; + + r = get_credentials_dir_internal("SYSTEMD_ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY", ret); + if (r >= 0 || r != -ENXIO) + return r; + + *ret = ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY; + return 0; +} + int read_credential(const char *name, void **ret, size_t *ret_size) { _cleanup_free_ char *fn = NULL; const char *d; diff --git a/src/shared/creds-util.h b/src/shared/creds-util.h index e5194c7f07..7b2fc76eb4 100644 --- a/src/shared/creds-util.h +++ b/src/shared/creds-util.h @@ -33,6 +33,10 @@ int open_credentials_dir(void); #define SYSTEM_CREDENTIALS_DIRECTORY "/run/credentials/@system" #define ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY "/run/credentials/@encrypted" +/* Where system creds have been passed */ +int get_system_credentials_dir(const char **ret); +int get_encrypted_system_credentials_dir(const char **ret); + int read_credential(const char *name, void **ret, size_t *ret_size); /* use in services! */ int read_credential_with_decryption(const char *name, void **ret, size_t *ret_size); /* use in generators + pid1! */ diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index de1c56a84e..ba6171f15f 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -7065,9 +7065,12 @@ int tpm2_pcrlock_policy_from_credentials( * multi-boot), hence we use the SRK and NV data from the LUKS2 header as search key, and parse all * such JSON policies until we find a matching one. */ - const char *cp = secure_getenv("SYSTEMD_ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY") ?: ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY; + const char *dp; + r = get_encrypted_system_credentials_dir(&dp); + if (r < 0) + return log_error_errno(r, "Failed to get encrypted system credentials directory: %m"); - dfd = open(cp, O_CLOEXEC|O_DIRECTORY); + dfd = open(dp, O_CLOEXEC|O_DIRECTORY); if (dfd < 0) { if (errno == ENOENT) { log_debug("No encrypted system credentials passed."); @@ -7100,7 +7103,7 @@ int tpm2_pcrlock_policy_from_credentials( if (r == -ENOENT) continue; if (r < 0) { - log_warning_errno(r, "Failed to read credentials file %s/%s, skipping: %m", ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY, d->d_name); + log_warning_errno(r, "Failed to read credentials file %s/%s, skipping: %m", dp, d->d_name); continue; } From 3e9ff7c0d8912b0a8ea01bb7e3355e4026ffa266 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 10 Jun 2024 14:58:52 +0200 Subject: [PATCH 07/22] creds-util: initialize default PCR mask in encrypt_credential_and_warn() If UINT32_MAX is passed in the PCR masks pick some reasonable defaults in encrypt_credential_and_warn(). These defaults copy what "systemd-creds encrypt" uses. By adding these defaults to the internal functions any user of them can take benefit of them. --- src/shared/creds-util.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index c035dd671d..ae448cbd59 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -41,6 +41,7 @@ #include "stat-util.h" #include "string-util.h" #include "tmpfile-util.h" +#include "tpm2-pcr.h" #include "tpm2-util.h" #include "user-util.h" @@ -879,6 +880,11 @@ int encrypt_credential_and_warn( return log_error_errno(r, "Failed to determine local credential host secret: %m"); } + if (tpm2_hash_pcr_mask == UINT32_MAX) + tpm2_hash_pcr_mask = 0; + if (tpm2_pubkey_pcr_mask == UINT32_MAX) + tpm2_pubkey_pcr_mask = UINT32_C(1) << TPM2_PCR_KERNEL_BOOT; + #if HAVE_TPM2 bool try_tpm2; if (CRED_KEY_WANTS_TPM2(with_key)) { From b95912446e82dfa303ac79f7f3f031905b38df7b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 5 Jun 2024 21:45:50 +0200 Subject: [PATCH 08/22] pcrlock: make sure we can parse nv_index measurement records in TCG CEL Make the systemd-pcrlock tool compatible with TCG CEL records that encode measurements into nvindexes rather than PCRs. This doesn't add code for actually predicting them, but just makes sure we can parse them correctly and display them reasonably. --- src/pcrlock/pcrlock.c | 94 +++++++++++++++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 21 deletions(-) diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c index 3a294a9171..276f6a61d7 100644 --- a/src/pcrlock/pcrlock.c +++ b/src/pcrlock/pcrlock.c @@ -155,7 +155,9 @@ typedef enum EventPayloadValid { struct EventLogRecord { EventLog *event_log; - uint32_t pcr; + + uint32_t pcr; /* Either 'pcr' or 'nv_index' are set, but not both. Other one is UINT32_MAX. */ + uint32_t nv_index; const char *source; char *description; @@ -184,6 +186,8 @@ struct EventLogRecord { #define EVENT_LOG_RECORD_IS_FIRMWARE(record) ((record)->firmware_event_type != UINT32_MAX) #define EVENT_LOG_RECORD_IS_USERSPACE(record) ((record)->userspace_event_type >= 0) +#define EVENT_LOG_RECORD_IS_PCR(record) ((record)->pcr != UINT32_MAX) +#define EVENT_LOG_RECORD_IS_NV_INDEX(record) ((record)->nv_index != UINT32_MAX) struct EventLogRegisterBank { TPM2B_DIGEST observed; @@ -339,6 +343,8 @@ static EventLogRecord* event_log_record_new(EventLog *el) { *record = (EventLogRecord) { .event_log = el, + .pcr = UINT32_MAX, + .nv_index = UINT32_MAX, .firmware_event_type = UINT32_MAX, .userspace_event_type = _TPM2_USERSPACE_EVENT_TYPE_INVALID, .event_payload_valid = _EVENT_PAYLOAD_VALID_INVALID, @@ -1003,9 +1009,6 @@ static int event_log_load_firmware(EventLog *el) { } static int event_log_record_parse_json(EventLogRecord *record, sd_json_variant *j) { - const char *rectype = NULL; - sd_json_variant *x, *k; - uint64_t u; int r; assert(record); @@ -1014,23 +1017,44 @@ static int event_log_record_parse_json(EventLogRecord *record, sd_json_variant * if (!sd_json_variant_is_object(j)) return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "record object is not an object."); - x = sd_json_variant_by_key(j, "pcr"); - if (!x) - return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'pcr' field missing from TPM measurement log file entry."); - if (!sd_json_variant_is_unsigned(x)) - return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'pcr' field is not an integer."); + sd_json_variant *jp = sd_json_variant_by_key(j, "pcr"); + sd_json_variant *jn = sd_json_variant_by_key(j, "nv_index"); + if (jp) { + uint64_t u; + if (jn) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Both 'pcr' and 'nv_index' field found in TPM measurement log file entry."); + if (!sd_json_variant_is_unsigned(jp)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'pcr' field is not an integer."); - u = sd_json_variant_unsigned(x); - if (u >= TPM2_PCRS_MAX) - return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'pcr' field is out of range."); - record->pcr = sd_json_variant_unsigned(x); + u = sd_json_variant_unsigned(jp); + if (u >= TPM2_PCRS_MAX) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'pcr' field is out of range."); - x = sd_json_variant_by_key(j, "digests"); + record->pcr = u; + record->nv_index = UINT32_MAX; + } else { + uint64_t u; + + if (!jn) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Neither 'pcr' nor 'nv_index' field found in TPM measurement log file entry."); + if (!sd_json_variant_is_unsigned(jn)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'nv_index' field is not an integer."); + + u = sd_json_variant_unsigned(jn); + if (u >= UINT32_MAX) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'nv_index' field is out of range."); + + record->nv_index = u; + record->pcr = UINT32_MAX; + } + + sd_json_variant *x = sd_json_variant_by_key(j, "digests"); if (!x) return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'digests' field missing from TPM measurement log file entry."); if (!sd_json_variant_is_array(x)) return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "'digests' field is not an array."); + sd_json_variant *k; JSON_VARIANT_ARRAY_FOREACH(k, x) { _cleanup_free_ void *hash = NULL; size_t hash_size; @@ -1067,6 +1091,7 @@ static int event_log_record_parse_json(EventLogRecord *record, sd_json_variant * return log_error_errno(r, "Failed to add bank to event log record: %m"); } + const char *rectype = NULL; x = sd_json_variant_by_key(j, "content_type"); if (!x) log_debug("'content_type' missing from TPM measurement log file entry, ignoring."); @@ -1324,6 +1349,10 @@ static int event_log_calculate_pcrs(EventLog *el) { } FOREACH_ARRAY(rr, el->records, el->n_records) { + + if (!EVENT_LOG_RECORD_IS_PCR(*rr)) + continue; + EventLogRegister *reg = el->registers + (*rr)->pcr; for (size_t i = 0; i < el->n_algorithms; i++) { @@ -1626,6 +1655,9 @@ static int event_log_record_equal(const EventLogRecord *a, const EventLogRecord if (a->pcr != b->pcr) return false; + if (a->nv_index != b->nv_index) + return false; + x = event_log_record_find_bank(a, a->event_log->primary_algorithm); y = event_log_record_find_bank(b, b->event_log->primary_algorithm); if (!x || !y) @@ -1848,6 +1880,9 @@ static int event_log_validate_fully_recognized(EventLog *el) { FOREACH_ARRAY(rr, el->records, el->n_records) { EventLogRecord *rec = *rr; + if (!EVENT_LOG_RECORD_IS_PCR(rec)) + continue; + if (rec->pcr != pcr) continue; @@ -1916,8 +1951,13 @@ static uint32_t event_log_component_variant_pcrs(EventLogComponentVariant *i) { /* returns mask of PCRs touched by this variant */ - FOREACH_ARRAY(rr, i->records, i->n_records) + FOREACH_ARRAY(rr, i->records, i->n_records) { + + if (!EVENT_LOG_RECORD_IS_PCR(*rr)) + continue; + mask |= UINT32_C(1) << (*rr)->pcr; + } return mask; } @@ -2123,11 +2163,19 @@ static int show_log_table(EventLog *el, sd_json_variant **ret_variant) { FOREACH_ARRAY(rr, el->records, el->n_records) { EventLogRecord *record = *rr; - r = table_add_many(table, - TABLE_UINT32, record->pcr, - TABLE_STRING, glyph(GLYPH_FULL_BLOCK), - TABLE_SET_COLOR, color_for_pcr(el, record->pcr), - TABLE_STRING, tpm2_pcr_index_to_string(record->pcr)); + if (EVENT_LOG_RECORD_IS_PCR(record)) + r = table_add_many(table, + TABLE_UINT32, record->pcr, + TABLE_STRING, glyph(GLYPH_FULL_BLOCK), + TABLE_SET_COLOR, color_for_pcr(el, record->pcr), + TABLE_STRING, tpm2_pcr_index_to_string(record->pcr)); + else if EVENT_LOG_RECORD_IS_NV_INDEX(record) + r = table_add_many(table, + TABLE_UINT32_HEX_0x, record->nv_index, + TABLE_EMPTY, + TABLE_EMPTY); + else + assert_not_reached(); if (r < 0) return table_log_add_error(r); @@ -2538,7 +2586,8 @@ static int event_log_record_to_cel(EventLogRecord *record, uint64_t *recnum, sd_ r = sd_json_buildo( ret, - SD_JSON_BUILD_PAIR_UNSIGNED("pcr", record->pcr), + SD_JSON_BUILD_PAIR_CONDITION(EVENT_LOG_RECORD_IS_PCR(record), "pcr", SD_JSON_BUILD_UNSIGNED(record->pcr)), + SD_JSON_BUILD_PAIR_CONDITION(EVENT_LOG_RECORD_IS_NV_INDEX(record), "nv_index", SD_JSON_BUILD_UNSIGNED(record->nv_index)), SD_JSON_BUILD_PAIR_UNSIGNED("recnum", ++(*recnum)), SD_JSON_BUILD_PAIR_VARIANT("digests", ja), SD_JSON_BUILD_PAIR_CONDITION(!!ct, "content_type", SD_JSON_BUILD_STRING(ct)), @@ -4004,6 +4053,9 @@ static int event_log_component_variant_calculate( FOREACH_ARRAY(rr, variant->records, variant->n_records) { EventLogRecord *rec = *rr; + if (!EVENT_LOG_RECORD_IS_PCR(rec)) + continue; + if (rec->pcr != pcr) continue; From d412b1629c54a6cc40dff225394661e09d20c461 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 3 Jun 2024 17:44:15 +0200 Subject: [PATCH 09/22] =?UTF-8?q?tpm2-util:=20rename=20tpm2=5Fextend=5Fbyt?= =?UTF-8?q?es()=E2=86=92tpm2=5Fpcr=5Fextend=5Fbytes()=20and=20make=20it=20?= =?UTF-8?q?take=20struct=20iovec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We soon want to add the ability to extend into nvindexes in addition to PCRs, hence rename the function to make clear it is about pcr extension. While we are at it, switch things over to "struct iovec" as we generally try to do it now in tpm2-util.[ch] these days. --- src/cryptsetup/cryptsetup.c | 2 +- src/pcrextend/pcrextend.c | 2 +- src/shared/tpm2-util.c | 26 +++++++++++--------------- src/shared/tpm2-util.h | 2 +- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index 7351e5dc2a..09939bfa48 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -1047,7 +1047,7 @@ static int measure_volume_key( if (!s) return log_oom(); - r = tpm2_extend_bytes(c, l ?: arg_tpm2_measure_banks, arg_tpm2_measure_pcr, s, SIZE_MAX, volume_key, volume_key_size, TPM2_EVENT_VOLUME_KEY, s); + r = tpm2_pcr_extend_bytes(c, l ?: arg_tpm2_measure_banks, arg_tpm2_measure_pcr, &IOVEC_MAKE_STRING(s), &IOVEC_MAKE(volume_key, volume_key_size), TPM2_EVENT_VOLUME_KEY, s); if (r < 0) return log_error_errno(r, "Could not extend PCR: %m"); diff --git a/src/pcrextend/pcrextend.c b/src/pcrextend/pcrextend.c index 75b561da90..cc50ad3ff7 100644 --- a/src/pcrextend/pcrextend.c +++ b/src/pcrextend/pcrextend.c @@ -233,7 +233,7 @@ static int extend_now(unsigned pcr, const void *data, size_t size, Tpm2Userspace log_debug("Measuring '%s' into PCR index %u, banks %s.", safe, pcr, joined_banks); - r = tpm2_extend_bytes(c, arg_banks, pcr, data, size, /* secret= */ NULL, /* secret_size= */ 0, event, safe); + r = tpm2_pcr_extend_bytes(c, arg_banks, pcr, &IOVEC_MAKE(data, size), /* secret= */ NULL, event, safe); if (r < 0) return log_error_errno(r, "Could not extend PCR: %m"); diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index ba6171f15f..38c1d73dc7 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -6321,14 +6321,12 @@ static int tpm2_userspace_log( } #endif -int tpm2_extend_bytes( +int tpm2_pcr_extend_bytes( Tpm2Context *c, char **banks, unsigned pcr_index, - const void *data, - size_t data_size, - const void *secret, - size_t secret_size, + const struct iovec *data, + const struct iovec *secret, Tpm2UserspaceEventType event_type, const char *description) { @@ -6338,17 +6336,15 @@ int tpm2_extend_bytes( TSS2_RC rc; assert(c); - assert(data || data_size == 0); - assert(secret || secret_size == 0); - - if (data_size == SIZE_MAX) - data_size = strlen(data); - if (secret_size == SIZE_MAX) - secret_size = strlen(secret); + assert(iovec_is_valid(data)); + assert(iovec_is_valid(secret)); if (pcr_index >= TPM2_PCRS_MAX) return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Can't measure into unsupported PCR %u, refusing.", pcr_index); + if (!iovec_is_set(data)) + data = &iovec_empty; + if (strv_isempty(banks)) return 0; @@ -6376,10 +6372,10 @@ int tpm2_extend_bytes( * secret for other purposes, maybe because it needs a shorter secret derived from it for * some unrelated purpose, who knows). Hence we instead measure an HMAC signature of a * private non-secret string instead. */ - if (secret_size > 0) { - if (!HMAC(implementation, secret, secret_size, data, data_size, (unsigned char*) &values.digests[values.count].digest, NULL)) + if (iovec_is_set(secret) > 0) { + if (!HMAC(implementation, secret->iov_base, secret->iov_len, data->iov_base, data->iov_len, (unsigned char*) &values.digests[values.count].digest, NULL)) return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to calculate HMAC of data to measure."); - } else if (EVP_Digest(data, data_size, (unsigned char*) &values.digests[values.count].digest, NULL, implementation, NULL) != 1) + } else if (EVP_Digest(data->iov_base, data->iov_len, (unsigned char*) &values.digests[values.count].digest, NULL, implementation, NULL) != 1) return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash data to measure."); values.count++; diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index b051c8435e..0fc3b55050 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -149,7 +149,7 @@ typedef enum Tpm2UserspaceEventType { const char* tpm2_userspace_event_type_to_string(Tpm2UserspaceEventType type) _const_; Tpm2UserspaceEventType tpm2_userspace_event_type_from_string(const char *s) _pure_; -int tpm2_extend_bytes(Tpm2Context *c, char **banks, unsigned pcr_index, const void *data, size_t data_size, const void *secret, size_t secret_size, Tpm2UserspaceEventType event, const char *description); +int tpm2_pcr_extend_bytes(Tpm2Context *c, char **banks, unsigned pcr_index, const struct iovec *data, const struct iovec *secret, Tpm2UserspaceEventType event, const char *description); uint32_t tpm2_tpms_pcr_selection_to_mask(const TPMS_PCR_SELECTION *s); void tpm2_tpms_pcr_selection_from_mask(uint32_t mask, TPMI_ALG_HASH hash, TPMS_PCR_SELECTION *ret); From 260b9e848923a1c0cf8f06a327ca6f864c02df52 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 3 Jun 2024 17:56:35 +0200 Subject: [PATCH 10/22] tpm2-util: make tpm2_undefine_policy_nv_index() generic We can use this to remove any kind of nvindex, hence give it a generic name. Also instead of passing "NONE" as session if none is specified, pass PASSWORD instead, so that the function actually becomes useful if no session is specified (the only user so far, pcrlock always provides a session, hence this is no change in behaviour). --- src/pcrlock/pcrlock.c | 2 +- src/shared/tpm2-util.c | 6 +++--- src/shared/tpm2-util.h | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c index 276f6a61d7..8be8c9ac3d 100644 --- a/src/pcrlock/pcrlock.c +++ b/src/pcrlock/pcrlock.c @@ -4952,7 +4952,7 @@ static int undefine_policy_nv_index( if (r < 0) return r; - r = tpm2_undefine_policy_nv_index( + r = tpm2_undefine_nv_index( tc, encryption_session, nv_index, diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 38c1d73dc7..0a5f9d4673 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -5889,7 +5889,7 @@ int tpm2_write_policy_nv_index( return 0; } -int tpm2_undefine_policy_nv_index( +int tpm2_undefine_nv_index( Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE nv_index, @@ -5904,14 +5904,14 @@ int tpm2_undefine_policy_nv_index( c->esys_context, /* authHandle= */ ESYS_TR_RH_OWNER, /* nvIndex= */ nv_handle->esys_handle, - /* shandle1= */ session ? session->esys_handle : ESYS_TR_NONE, + /* shandle1= */ session ? session->esys_handle : ESYS_TR_PASSWORD, /* shandle2= */ ESYS_TR_NONE, /* shandle3= */ ESYS_TR_NONE); if (rc != TSS2_RC_SUCCESS) return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to undefine NV index: %s", sym_Tss2_RC_Decode(rc)); - log_debug("Undefined NV index 0x%x", nv_index); + log_debug("Successfully undefined NV index 0x%x.", nv_index); return 0; } diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 0fc3b55050..67b035b442 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -296,7 +296,7 @@ int tpm2_tpm2b_public_to_fingerprint(const TPM2B_PUBLIC *public, void **ret_fing int tpm2_define_policy_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE requested_nv_index, const TPM2B_DIGEST *write_policy, TPM2_HANDLE *ret_nv_index, Tpm2Handle **ret_nv_handle, TPM2B_NV_PUBLIC *ret_nv_public); int tpm2_write_policy_nv_index(Tpm2Context *c, const Tpm2Handle *policy_session, TPM2_HANDLE nv_index, const Tpm2Handle *nv_handle, const TPM2B_DIGEST *policy_digest); -int tpm2_undefine_policy_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE nv_index, const Tpm2Handle *nv_handle); +int tpm2_undefine_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE nv_index, const Tpm2Handle *nv_handle); int tpm2_seal_data(Tpm2Context *c, const struct iovec *data, const Tpm2Handle *primary_handle, const Tpm2Handle *encryption_session, const TPM2B_DIGEST *policy, struct iovec *ret_public, struct iovec *ret_private); int tpm2_unseal_data(Tpm2Context *c, const struct iovec *public, const struct iovec *private, const Tpm2Handle *primary_handle, const Tpm2Handle *policy_session, const Tpm2Handle *encryption_session, struct iovec *ret_data); From b7e072f3fac5e0cb8f3c2189ee12cbbad8678f41 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 3 Jun 2024 21:40:52 +0200 Subject: [PATCH 11/22] tpm2-util: move parse for tpm2 hash algorithms up --- src/shared/tpm2-util.c | 60 +++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 0a5f9d4673..86fe383956 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -6160,6 +6160,36 @@ int tpm2_find_device_auto(char **ret) { #endif } +const uint16_t tpm2_hash_algorithms[] = { + TPM2_ALG_SHA1, + TPM2_ALG_SHA256, + TPM2_ALG_SHA384, + TPM2_ALG_SHA512, + 0, +}; + +assert_cc(ELEMENTSOF(tpm2_hash_algorithms) == TPM2_N_HASH_ALGORITHMS + 1); + +static size_t tpm2_hash_algorithm_index(uint16_t algorithm) { + for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) + if (tpm2_hash_algorithms[i] == algorithm) + return i; + + return SIZE_MAX; +} + +static int json_dispatch_tpm2_algorithm(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + uint16_t *algorithm = ASSERT_PTR(userdata); + int r; + + r = tpm2_hash_alg_from_string(sd_json_variant_string(variant)); + if (r < 0 || tpm2_hash_algorithm_index(r) == SIZE_MAX) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Invalid hash algorithm: %s", sd_json_variant_string(variant)); + + *algorithm = r; + return 0; +} + #if HAVE_TPM2 static const char* tpm2_userspace_event_type_table[_TPM2_USERSPACE_EVENT_TYPE_MAX] = { [TPM2_EVENT_PHASE] = "phase", @@ -6408,24 +6438,6 @@ int tpm2_pcr_extend_bytes( #endif } -const uint16_t tpm2_hash_algorithms[] = { - TPM2_ALG_SHA1, - TPM2_ALG_SHA256, - TPM2_ALG_SHA384, - TPM2_ALG_SHA512, - 0, -}; - -assert_cc(ELEMENTSOF(tpm2_hash_algorithms) == TPM2_N_HASH_ALGORITHMS + 1); - -static size_t tpm2_hash_algorithm_index(uint16_t algorithm) { - for (size_t i = 0; i < TPM2_N_HASH_ALGORITHMS; i++) - if (tpm2_hash_algorithms[i] == algorithm) - return i; - - return SIZE_MAX; -} - TPM2B_DIGEST *tpm2_pcr_prediction_result_get_hash(Tpm2PCRPredictionResult *result, uint16_t alg) { size_t alg_idx; @@ -6903,18 +6915,6 @@ void tpm2_pcrlock_policy_done(Tpm2PCRLockPolicy *data) { iovec_done(&data->pin_private); } -static int json_dispatch_tpm2_algorithm(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { - uint16_t *algorithm = ASSERT_PTR(userdata); - int r; - - r = tpm2_hash_alg_from_string(sd_json_variant_string(variant)); - if (r < 0 || tpm2_hash_algorithm_index(r) == SIZE_MAX) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid hash algorithm: %s", sd_json_variant_string(variant)); - - *algorithm = r; - return 0; -} - int tpm2_pcrlock_search_file(const char *path, FILE **ret_file, char **ret_path) { static const char search[] = "/run/systemd\0" From b0c5c6aad8d5834fda9d04a64265a35d1a984a26 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 10 Jun 2024 15:55:54 +0200 Subject: [PATCH 12/22] tpm2-util: add infra for allocating nvindex-based PCRs (aka "NvPCRs") We'd like to measure various additional things into PCRs, but all available ones to the OS are already used for various purposes. Hence, let's introduce a new concept of "NV Index based PCRs", i.e. let's use TPM2 nv indexes of type TPM2_NT_EXTEND that mostly behave like real PCRs, but which we can allocate relatively freely from the nv index space. Let's call these "fake" PCRs "NvPCRs". My original intention was to get a fixed NV index range assigned from the TCG, either for Linux or for systemd as a project, but this stalled with no further updates from the TCG for more than a year and a half now. I was told an NV index range to use though, even if it never was officially assigned, hence this PR uses this by default. But the range is configurable at build time, on purpose, so that downstreams have some flexibility to change this if they want. To abstract the actual nvindex number away we introduce a naming concept, so that nvindexes are referenced by name string rather than number. NvPCRs are defined in little JSON snippets in /usr/lib/nvpcr/*.nvpcr, that match up index number and name, as well as pick a hash algorithm. There's one complication: these nvindex (like any nvindex) can be deleted by anyone with access to the TPM, and then be recreated. This could be used to reset the NvPCRs to zero during runtime, which defeats the whole point of them. Our way out: we measure a secret as first thing after creation into the NvPCRs. (Or actually, we measure a per-NvPCR secret we derive from a system secret via an HMAC of the NvPCR name) and the nvindex handle). This "anchoring" secret is stored in /run/ + /var/lib/ + ESP/XBOOTLDR (the latter encrypted as credential, locked to the TPM), to make it available at the whole runtime of the OS. --- meson.build | 2 + meson_options.txt | 2 + src/shared/tpm2-util.c | 1033 +++++++++++++++++++++++++++++++++++- src/shared/tpm2-util.h | 10 + src/tpm2-setup/meson.build | 17 + 5 files changed, 1044 insertions(+), 20 deletions(-) diff --git a/meson.build b/meson.build index 24efc9f7c3..6c4b67d28d 100644 --- a/meson.build +++ b/meson.build @@ -1343,6 +1343,7 @@ tpm2 = dependency('tss2-esys tss2-rc tss2-mu tss2-tcti-device', tpm2_cflags = tpm2.partial_dependency(includes: true, compile_args: true) conf.set10('HAVE_TPM2', tpm2.found()) conf.set10('HAVE_TSS2_ESYS3', tpm2.found() and tpm2.version().version_compare('>= 3.0.0')) +conf.set('TPM2_NVPCR_BASE', get_option('tpm2-nvpcr-base')) libdw = dependency('libdw', required : get_option('elfutils')) @@ -3028,6 +3029,7 @@ summary({ 'default user $PATH' : default_user_path != '' ? default_user_path : '(same as system services)', 'systemd service watchdog' : service_watchdog == '' ? 'disabled' : service_watchdog, 'time epoch' : f'@time_epoch@ (@alt_time_epoch@)', + 'TPM2 nvpcr base' : run_command(sh, '-c', 'printf 0x%x @0@'.format(get_option('tpm2-nvpcr-base')), check : true).stdout() }) # TODO: diff --git a/meson_options.txt b/meson_options.txt index f7ec39322c..d44030ef8b 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -449,6 +449,8 @@ option('libfido2', type : 'feature', deprecated : { 'true' : 'enabled', 'false' description : 'FIDO2 support') option('tpm2', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : 'disabled' }, description : 'TPM2 support') +option('tpm2-nvpcr-base', type : 'integer', value: 0x01d10200, + description : 'Base for TPM2 nvindex based PCRs') option('elfutils', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : 'disabled' }, description : 'elfutils support') option('zlib', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : 'disabled' }, diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 86fe383956..49da185271 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -6,12 +6,15 @@ #include "alloc-util.h" #include "ansi-color.h" #include "bitfield.h" +#include "bootspec.h" +#include "boot-entry.h" #include "constants.h" #include "creds-util.h" #include "cryptsetup-util.h" #include "dirent-util.h" #include "dlfcn-util.h" #include "efi-api.h" +#include "errno-util.h" #include "extract-word.h" #include "fd-util.h" #include "fileio.h" @@ -39,6 +42,7 @@ #include "sync-util.h" #include "time-util.h" #include "tpm2-pcr.h" +#include "tmpfile-util.h" #include "tpm2-util.h" #include "virt.h" @@ -65,6 +69,9 @@ static DLSYM_PROTOTYPE(Esys_Initialize) = NULL; static DLSYM_PROTOTYPE(Esys_Load) = NULL; static DLSYM_PROTOTYPE(Esys_LoadExternal) = NULL; static DLSYM_PROTOTYPE(Esys_NV_DefineSpace) = NULL; +static DLSYM_PROTOTYPE(Esys_NV_Extend) = NULL; +static DLSYM_PROTOTYPE(Esys_NV_Read) = NULL; +static DLSYM_PROTOTYPE(Esys_NV_ReadPublic) = NULL; static DLSYM_PROTOTYPE(Esys_NV_UndefineSpace) = NULL; static DLSYM_PROTOTYPE(Esys_NV_Write) = NULL; static DLSYM_PROTOTYPE(Esys_PCR_Extend) = NULL; @@ -139,6 +146,9 @@ static int dlopen_tpm2_esys(void) { DLSYM_ARG(Esys_Load), DLSYM_ARG(Esys_LoadExternal), DLSYM_ARG(Esys_NV_DefineSpace), + DLSYM_ARG(Esys_NV_Extend), + DLSYM_ARG(Esys_NV_Read), + DLSYM_ARG(Esys_NV_ReadPublic), DLSYM_ARG(Esys_NV_UndefineSpace), DLSYM_ARG(Esys_NV_Write), DLSYM_ARG(Esys_PCR_Extend), @@ -5915,6 +5925,224 @@ int tpm2_undefine_nv_index( return 0; } +int tpm2_define_nvpcr_nv_index( + Tpm2Context *c, + const Tpm2Handle *session, + TPM2_HANDLE nv_index, + TPMI_ALG_HASH algorithm, + Tpm2Handle **ret_nv_handle) { + + _cleanup_(tpm2_handle_freep) Tpm2Handle *new_handle = NULL; + TSS2_RC rc; + int r; + + assert(c); + + /* Allocates an nvindex to use as a "fake" PCR. We call these "NvPCR" in our codebase */ + + if (algorithm == 0) + algorithm = TPM2_ALG_SHA256; + + int digest_size = tpm2_hash_alg_to_size(algorithm); + if (digest_size < 0) + return digest_size; + + if ((size_t) digest_size > sizeof_field(TPM2B_MAX_NV_BUFFER, buffer)) + return log_debug_errno(SYNTHETIC_ERRNO(E2BIG), "Digest too large for extension."); + + r = tpm2_handle_new(c, &new_handle); + if (r < 0) + return r; + + new_handle->flush = false; /* This is a persistent NV index, don't flush hence */ + + TPM2B_NV_PUBLIC public_info = { + .size = sizeof_field(TPM2B_NV_PUBLIC, nvPublic), + .nvPublic = { + .nvIndex = nv_index, + .nameAlg = algorithm, + .attributes = TPMA_NV_CLEAR_STCLEAR | + TPMA_NV_ORDERLY | + TPMA_NV_OWNERWRITE | + TPMA_NV_AUTHWRITE | + TPMA_NV_OWNERREAD | + TPMA_NV_AUTHREAD | + (TPM2_NT_EXTEND << TPMA_NV_TPM2_NT_SHIFT), + .dataSize = digest_size, + }, + }; + + rc = sym_Esys_NV_DefineSpace( + c->esys_context, + /* authHandle= */ ESYS_TR_RH_OWNER, + /* shandle1= */ session ? session->esys_handle : ESYS_TR_PASSWORD, + /* shandle2= */ ESYS_TR_NONE, + /* shandle3= */ ESYS_TR_NONE, + /* auth= */ NULL, + &public_info, + &new_handle->esys_handle); + if (rc == TPM2_RC_NV_DEFINED) { + log_debug("NV index 0x%" PRIu32 " already registered.", nv_index); + + new_handle = tpm2_handle_free(new_handle); + + r = tpm2_index_to_handle( + c, + nv_index, + session, + /* ret_public= */ NULL, + /* ret_name= */ NULL, + /* ret_qname= */ NULL, + &new_handle); + if (r < 0) + return log_debug_errno(r, "Failed to acquire handle to existing NV index 0x%" PRIu32 ".", nv_index); + + log_debug("Successfully acquired handle to existing NV index 0x%" PRIx32 ".", nv_index); + + _cleanup_(Esys_Freep) TPM2B_NV_PUBLIC *nv_public_real = NULL; + rc = sym_Esys_NV_ReadPublic( + c->esys_context, + /* nvIndex= */ new_handle->esys_handle, + /* shandle1= */ ESYS_TR_NONE, + /* shandle2= */ ESYS_TR_NONE, + /* shandle3= */ ESYS_TR_NONE, + &nv_public_real, + /* ret_nv_name= */ NULL); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed read public data of nvindex 0x%x: %s", nv_index, sym_Tss2_RC_Decode(rc)); + + log_debug("Read public info for nvindex 0x%x.", nv_index); + + if (nv_public_real->size < endoffsetof_field(TPMS_NV_PUBLIC, attributes) + sizeof_field(TPMS_NV_PUBLIC, dataSize) || + nv_public_real->nvPublic.nvIndex != public_info.nvPublic.nvIndex || + nv_public_real->nvPublic.nameAlg != public_info.nvPublic.nameAlg || + ((nv_public_real->nvPublic.attributes ^ public_info.nvPublic.attributes) & ~TPMA_NV_WRITTEN) != 0 || + nv_public_real->nvPublic.dataSize != public_info.nvPublic.dataSize) + return log_debug_errno(SYNTHETIC_ERRNO(EEXIST), + "Public data of nvindex 0x%x does not match our expectations.", nv_index); + + log_debug("Public info for nvindex 0x%x checks out, using.", nv_index); + + if (ret_nv_handle) + *ret_nv_handle = TAKE_PTR(new_handle); + + return 0; + } + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to allocate NV index: %s", sym_Tss2_RC_Decode(rc)); + + log_debug("NV Index 0x%" PRIx32 " successfully allocated.", nv_index); + + if (ret_nv_handle) + *ret_nv_handle = TAKE_PTR(new_handle); + + return 1; +} + +int tpm2_extend_nvpcr_nv_index( + Tpm2Context *c, + TPM2_HANDLE nv_index, + const Tpm2Handle *nv_handle, + const struct iovec *digest) { + + TPM2_RC rc; + + assert(c); + assert(nv_index); + assert(nv_handle); + assert(iovec_is_set(digest)); + + if (digest->iov_len > sizeof_field(TPM2B_MAX_NV_BUFFER, buffer)) + return log_debug_errno(SYNTHETIC_ERRNO(E2BIG), "Hash value to extend too long."); + + TPM2B_MAX_NV_BUFFER buf = { + .size = digest->iov_len, + }; + memcpy(buf.buffer, digest->iov_base, digest->iov_len); + + rc = sym_Esys_NV_Extend( + c->esys_context, + /* authHandle= */ nv_handle->esys_handle, + /* nvIndex= */ nv_handle->esys_handle, + /* shandle1= */ ESYS_TR_PASSWORD, + /* shandle2= */ ESYS_TR_NONE, + /* shandle3= */ ESYS_TR_NONE, + &buf); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to extend NV index: %s", sym_Tss2_RC_Decode(rc)); + + if (DEBUG_LOGGING) { + _cleanup_free_ char *h = NULL; + h = hexmem(digest->iov_base, digest->iov_len); + log_debug("Written hash %s to NV index 0x%x", strnull(h), nv_index); + } + + return 0; +} + +int tpm2_read_nv_index( + Tpm2Context *c, + const Tpm2Handle *session, + TPM2_HANDLE nv_index, + const Tpm2Handle *nv_handle, + struct iovec *ret_value) { + + TPM2_RC rc; + + assert(c); + assert(nv_index); + assert(nv_handle); + + _cleanup_(Esys_Freep) TPM2B_NV_PUBLIC *nv_public = NULL; + rc = sym_Esys_NV_ReadPublic( + c->esys_context, + /* nvIndex= */ nv_handle->esys_handle, + /* shandle1= */ ESYS_TR_NONE, + /* shandle2= */ ESYS_TR_NONE, + /* shandle3= */ ESYS_TR_NONE, + &nv_public, + /* ret_nv_name= */ NULL); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed read public data of nvindex 0x%x: %s", nv_index, sym_Tss2_RC_Decode(rc)); + + log_debug("Read public info for nvindex 0x%x, value size is %zu", nv_index, (size_t) nv_public->nvPublic.dataSize); + + _cleanup_(Esys_Freep) TPM2B_MAX_NV_BUFFER *value = NULL; + rc = sym_Esys_NV_Read( + c->esys_context, + /* authHandle= */ nv_handle->esys_handle, + /* nvIndex= */ nv_handle->esys_handle, + /* shandle1= */ session ? session->esys_handle : ESYS_TR_PASSWORD, + /* shandle2= */ ESYS_TR_NONE, + /* shandle3= */ ESYS_TR_NONE, + nv_public->nvPublic.dataSize, + /* offset= */ 0, + &value); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed read contents of nvindex 0x%x: %s", nv_index, sym_Tss2_RC_Decode(rc)); + + if (ret_value) { + assert(value); + + struct iovec result = { + .iov_base = memdup(value->buffer, value->size), + .iov_len = value->size, + }; + + if (!result.iov_base) + return log_oom_debug(); + + *ret_value = TAKE_STRUCT(result); + } + + return 0; +} + int tpm2_seal_data( Tpm2Context *c, const struct iovec *data, @@ -6160,6 +6388,7 @@ int tpm2_find_device_auto(char **ret) { #endif } +#if HAVE_TPM2 const uint16_t tpm2_hash_algorithms[] = { TPM2_ALG_SHA1, TPM2_ALG_SHA256, @@ -6190,7 +6419,6 @@ static int json_dispatch_tpm2_algorithm(const char *name, sd_json_variant *varia return 0; } -#if HAVE_TPM2 static const char* tpm2_userspace_event_type_table[_TPM2_USERSPACE_EVENT_TYPE_MAX] = { [TPM2_EVENT_PHASE] = "phase", [TPM2_EVENT_FILESYSTEM] = "filesystem", @@ -6211,7 +6439,6 @@ const char* tpm2_firmware_log_path(void) { #if HAVE_OPENSSL static int tpm2_userspace_log_open(void) { _cleanup_close_ int fd = -EBADF; - struct stat st; const char *e; int r; @@ -6228,29 +6455,61 @@ static int tpm2_userspace_log_open(void) { if (flock(fd, LOCK_EX) < 0) return log_debug_errno(errno, "Failed to lock TPM log file '%s', ignoring: %m", e); - if (fstat(fd, &st) < 0) - return log_debug_errno(errno, "Failed to fstat TPM log file '%s', ignoring: %m", e); - - r = stat_verify_regular(&st); + r = fd_verify_regular(fd); if (r < 0) return log_debug_errno(r, "TPM log file '%s' is not regular, ignoring: %m", e); + return TAKE_FD(fd); +} + +static int tpm2_userspace_log_dirty(int fd) { + struct stat st; + + if (fd < 0) /* Apparently tpm2_local_log_open() failed earlier, let's not complain again */ + return 0; + /* We set the sticky bit when we are about to append to the log file. We'll unset it afterwards * again. If we manage to take a lock on a file that has it set we know we didn't write it fully and * it is corrupted. Ideally we'd like to use user xattrs for this, but unfortunately tmpfs (which is * our assumed backend fs) doesn't know user xattrs. */ + + if (fstat(fd, &st) < 0) + return log_debug_errno(errno, "Failed to fstat TPM log file, ignoring: %m"); + if (st.st_mode & S_ISVTX) - return log_debug_errno(SYNTHETIC_ERRNO(ESTALE), "TPM log file '%s' aborted, ignoring.", e); + return log_debug_errno(SYNTHETIC_ERRNO(ESTALE), "TPM log file aborted, ignoring."); if (fchmod(fd, 0600 | S_ISVTX) < 0) - return log_debug_errno(errno, "Failed to chmod() TPM log file '%s', ignoring: %m", e); + return log_debug_errno(errno, "Failed to chmod() TPM log file, ignoring: %m"); - return TAKE_FD(fd); + return 0; +} + +static int tpm2_userspace_log_clean(int fd) { + int r; + + if (fd < 0) /* Apparently tpm2_local_log_open() failed earlier, let's not complain again */ + return 0; + + if (fsync(fd) < 0) + return log_debug_errno(errno, "Failed to sync JSON data: %m"); + + /* Unset S_ISVTX again */ + if (fchmod(fd, 0600) < 0) + return log_debug_errno(errno, "Failed to chmod() TPM log file, ignoring: %m"); + + r = fsync_full(fd); + if (r < 0) + return log_debug_errno(r, "Failed to sync JSON log: %m"); + + return 0; } static int tpm2_userspace_log( int fd, unsigned pcr_index, + uint32_t nv_index, + const char *nv_index_name, const TPML_DIGEST_VALUES *values, Tpm2UserspaceEventType event_type, const char *description) { @@ -6314,10 +6573,12 @@ static int tpm2_userspace_log( r = sd_json_buildo( &v, - SD_JSON_BUILD_PAIR("pcr", SD_JSON_BUILD_UNSIGNED(pcr_index)), + SD_JSON_BUILD_PAIR_CONDITION(pcr_index != UINT_MAX, "pcr", SD_JSON_BUILD_UNSIGNED(pcr_index)), + SD_JSON_BUILD_PAIR_CONDITION(nv_index != UINT32_MAX, "nv_index", SD_JSON_BUILD_UNSIGNED(nv_index)), SD_JSON_BUILD_PAIR("digests", SD_JSON_BUILD_VARIANT(array)), SD_JSON_BUILD_PAIR("content_type", SD_JSON_BUILD_STRING("systemd")), SD_JSON_BUILD_PAIR("content", SD_JSON_BUILD_OBJECT( + SD_JSON_BUILD_PAIR_CONDITION(!!nv_index_name, "nvIndexName", SD_JSON_BUILD_STRING(nv_index_name)), SD_JSON_BUILD_PAIR_CONDITION(!!description, "string", SD_JSON_BUILD_STRING(description)), SD_JSON_BUILD_PAIR("bootId", SD_JSON_BUILD_ID128(boot_id)), SD_JSON_BUILD_PAIR("timestamp", SD_JSON_BUILD_UNSIGNED(now(CLOCK_BOOTTIME))), @@ -6336,16 +6597,9 @@ static int tpm2_userspace_log( if (r < 0) return log_debug_errno(r, "Failed to write JSON data to log: %m"); - if (fsync(fd) < 0) - return log_debug_errno(errno, "Failed to sync JSON data: %m"); - - /* Unset S_ISVTX again */ - if (fchmod(fd, 0600) < 0) - return log_debug_errno(errno, "Failed to chmod() TPM log file, ignoring: %m"); - - r = fsync_full(fd); + r = tpm2_userspace_log_clean(fd); if (r < 0) - return log_debug_errno(r, "Failed to sync JSON log: %m"); + return r; return 1; } @@ -6415,6 +6669,7 @@ int tpm2_pcr_extend_bytes( * and our measurement and change either */ log_fd = tpm2_userspace_log_open(); + (void) tpm2_userspace_log_dirty(log_fd); rc = sym_Esys_PCR_Extend( c->esys_context, ESYS_TR_PCR0 + pcr_index, @@ -6430,7 +6685,739 @@ int tpm2_pcr_extend_bytes( sym_Tss2_RC_Decode(rc)); /* Now, write what we just extended to the log, too. */ - (void) tpm2_userspace_log(log_fd, pcr_index, &values, event_type, description); + (void) tpm2_userspace_log( + log_fd, + pcr_index, + /* nv_index= */ UINT32_MAX, + /* nv_index_name= */ NULL, + &values, + event_type, + description); + + return 0; +#else /* HAVE_OPENSSL */ + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL support is disabled."); +#endif +} + +typedef struct NvPCRData { + char *name; + uint16_t algorithm; + uint32_t nv_index; +} NvPCRData; + +static void nvpcr_data_done(NvPCRData *d) { + assert(d); + + free(d->name); +} + +static int nvpcr_data_load(const char *name, NvPCRData *ret) { + int r; + + assert(ret); + + if (!tpm2_nvpcr_name_is_valid(name)) + return -EINVAL; + + const char *fname = strjoina(name, ".nvpcr"); + + _cleanup_free_ char *path = NULL; + _cleanup_fclose_ FILE *f = NULL; + r = search_and_fopen_nulstr(fname, "re", /* root= */ NULL, CONF_PATHS_NULSTR("nvpcr"), &f, &path); + if (r < 0) + return r; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + r = sd_json_parse_file( + f, + path, + /* flags= */ 0, + &v, + /* reterr_line= */ NULL, + /* reterr_column= */ NULL); + if (r < 0) + return log_debug_errno(r, "Failed to load '%s': %m", path); + + static const sd_json_dispatch_field dispatch_table[] = { + { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(NvPCRData, name), SD_JSON_MANDATORY }, + { "algorithm", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_tpm2_algorithm, offsetof(NvPCRData, algorithm), 0 }, + { "nvIndex", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint32, offsetof(NvPCRData, nv_index), SD_JSON_MANDATORY }, + {}, + }; + + _cleanup_(nvpcr_data_done) NvPCRData p = { + .algorithm = TPM2_ALG_SHA256, + }; + r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); + if (r < 0) + return r; + + if (!streq_ptr(p.name, name)) + return log_debug_errno(SYNTHETIC_ERRNO(ESTALE), "NvPCR name doesn't match filename, refusing."); + + *ret = TAKE_STRUCT(p); + return 0; +} + +int tpm2_nvpcr_get_index(const char *name, uint32_t *ret) { + int r; + + _cleanup_(nvpcr_data_done) NvPCRData p = {}; + r = nvpcr_data_load(name, &p); + if (r < 0) + return r; + + if (ret) + *ret = p.nv_index; + + return 0; +} + +int tpm2_nvpcr_extend_bytes( + Tpm2Context *c, + const Tpm2Handle *session, + const char *name, + const struct iovec *data, + const struct iovec *secret, + Tpm2UserspaceEventType event_type, + const char *description) { + +#if HAVE_OPENSSL + _cleanup_close_ int log_fd = -EBADF; + int r; + + assert(c); + assert(name); + assert(iovec_is_valid(data)); + assert(iovec_is_valid(secret)); + + _cleanup_(nvpcr_data_done) NvPCRData p = {}; + r = nvpcr_data_load(name, &p); + if (r < 0) + return r; + + /* Open + lock the log file *before* we start measuring, so that no one else can come between our log + * and our measurement and change either */ + log_fd = tpm2_userspace_log_open(); + + /* Check if this NvPCR is already anchored */ + const char *anchor_fname = strjoina("/run/systemd/nvpcr/", name, ".anchor"); + if (faccessat(AT_FDCWD, anchor_fname, F_OK, AT_SYMLINK_NOFOLLOW) < 0) { + if (errno != ENOENT) + return log_debug_errno(errno, "Failed to check if '%s' exists: %m", anchor_fname); + + return log_debug_errno(SYNTHETIC_ERRNO(ENETDOWN), "NvPCR '%s' not anchored yet, refusing.", name); + } + + const char *an = tpm2_hash_alg_to_string(p.algorithm); + if (!an) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unsupported algorithm for NvPCR, refusing."); + + const EVP_MD *implementation; + assert_se(implementation = EVP_get_digestbyname(an)); + + _cleanup_(iovec_done) struct iovec digest = { + .iov_len = EVP_MD_size(implementation), + }; + + digest.iov_base = malloc(digest.iov_len); + if (!digest.iov_base) + return log_oom_debug(); + + if (!iovec_is_set(data)) + data = &iovec_empty; + + if (iovec_is_set(secret)) { + if (!HMAC(implementation, secret->iov_base, secret->iov_len, data->iov_base, data->iov_len, digest.iov_base, NULL)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to calculate HMAC of data to measure."); + } else if (EVP_Digest(data->iov_base, data->iov_len, digest.iov_base, NULL, implementation, NULL) != 1) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to hash data to measure."); + + _cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL; + r = tpm2_index_to_handle( + c, + p.nv_index, + session, + /* ret_public= */ NULL, + /* ret_name= */ NULL, + /* ret_qname= */ NULL, + &nv_handle); + if (r < 0) + return log_debug_errno(r, "Failed to acquire handle to NV index 0x%" PRIu32 ".", p.nv_index); + + log_debug("Successfully acquired handle to existing NV index 0x%" PRIx32 ".", p.nv_index); + + (void) tpm2_userspace_log_dirty(log_fd); + + r = tpm2_extend_nvpcr_nv_index( + c, + p.nv_index, + nv_handle, + &digest); + if (r < 0) + return r; + + TPML_DIGEST_VALUES digest_values = { + .count = 1, + .digests[0].hashAlg = p.algorithm, + }; + memcpy(&digest_values.digests[0].digest, digest.iov_base, digest.iov_len); + + /* Now, write what we just extended to the log, too. */ + (void) tpm2_userspace_log( + log_fd, + /* pcr_index= */ UINT_MAX, + p.nv_index, + name, + &digest_values, + event_type, + description); + + return 0; +#else /* HAVE_OPENSSL */ + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL support is disabled."); +#endif +} + +#if HAVE_OPENSSL +static int tpm2_nvpcr_write_anchor_secret( + const char *dir, + const char *fname, + const struct iovec *credential) { + + int r; + + assert(dir); + assert(fname); + assert(iovec_is_set(credential)); + + /* Writes the encrypted credential of the anchor secret to directory 'dir' and file 'fname' */ + + _cleanup_close_ int dfd = open_mkdir(dir, O_CLOEXEC, 0755); + if (dfd < 0) + return log_error_errno(dfd, "Failed to create '%s' directory: %m", dir); + + _cleanup_free_ char *joined = path_join(dir, fname); + if (!joined) + return log_oom(); + + _cleanup_(iovec_done) struct iovec existing = {}; + r = read_full_file_full( + dfd, + fname, + /* offset= */ UINT64_MAX, + CREDENTIAL_ENCRYPTED_SIZE_MAX, + READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER, + /* bind_name= */ NULL, + (char**) &existing.iov_base, + &existing.iov_len); + if (r < 0) { + if (r != -ENOENT) + return log_error_errno(r, "Failed to read '%s' file: %m", joined); + } else if (iovec_memcmp(&existing, credential) == 0) { + log_debug("Anchor secret file '%s' already matches expectations, not updating.", joined); + return 0; + } else + log_notice("Anchor secret file '%s' different from current anchor secret, updating.", joined); + + r = write_base64_file_at( + dfd, + fname, + credential, + WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_SYNC); + if (r < 0) + return log_error_errno(r, "Failed to write anchor secret file to '%s': %m", joined); + + log_info("Successfully written anchor secret to '%s'.", joined); + return 1; +} + +static int tpm2_nvpcr_write_anchor_secret_to_var(const struct iovec *credential) { + return tpm2_nvpcr_write_anchor_secret("/var/lib/systemd/nvpcr", "nvpcr-anchor.cred", credential); +} + +static int tpm2_nvpcr_write_anchor_secret_to_boot(const struct iovec *credential) { + int r; + + assert(iovec_is_set(credential)); + + _cleanup_free_ char *dir = NULL; + r = get_global_boot_credentials_path(&dir); + if (r < 0) + return r; + if (r == 0) { + log_debug("No XBOOTLDR/ESP partition found, not writing boot anchor secret file."); + return 0; + } + + sd_id128_t machine_id; + r = sd_id128_get_machine(&machine_id); + if (r < 0) + return log_error_errno(r, "Failed to read machine ID: %m"); + + BootEntryTokenType entry_token_type = BOOT_ENTRY_TOKEN_AUTO; + _cleanup_free_ char *entry_token = NULL; + r = boot_entry_token_ensure( + /* root= */ NULL, + /* conf_root= */ NULL, + machine_id, + /* machine_id_is_random = */ false, + &entry_token_type, + &entry_token); + if (r < 0) + return r; + + _cleanup_free_ char *fname = strjoin("nvpcr-anchor.", entry_token, ".cred"); + if (!fname) + return log_oom(); + + if (!filename_is_valid(fname)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Credential name '%s' would not be a valid file name, refusing.", fname); + + return tpm2_nvpcr_write_anchor_secret(dir, fname, credential); +} + +static int tpm2_nvpcr_acquire_anchor_secret_from_var(struct iovec *ret_credential) { + int r; + + assert(ret_credential); + + r = read_full_file_full( + AT_FDCWD, + "/var/lib/systemd/nvpcr/nvpcr-anchor.cred", + /* offset= */ UINT64_MAX, + CREDENTIAL_ENCRYPTED_SIZE_MAX, + READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER|READ_FULL_FILE_VERIFY_REGULAR, + /* bind_name= */ NULL, + (char**) &ret_credential->iov_base, + &ret_credential->iov_len); + if (r == -ENOENT) { + log_debug_errno(r, "No '/var/lib/systemd/nvpcr/nvpcr-anchor.cred' file."); + *ret_credential = (struct iovec) {}; + return 0; + } + if (r < 0) + return log_error_errno(r, "Failed to read '/var/lib/systemd/nvpcr/nvpcr-anchor.cred': %m"); + + return 1; +} + +static int tpm2_nvpcr_acquire_anchor_secret_from_credential(struct iovec *ret_credential, struct iovec *ret_secret) { + int r; + + assert(ret_credential); + assert(ret_secret); + + /* We need the anchor secret before the first measurement into an NvPCR. That means very early. Hence + * we'll try to pass it into the system via the system credentials logic. Because we must expect a + * multi-boot scenario it's hard to know which secret to use for which system. Hence we'll just try + * to unlock all of the available ones, until we can decrypt one of them, and then we'll use that. */ + + const char *dp; + r = get_encrypted_system_credentials_dir(&dp); + if (r < 0) + return log_error_errno(r, "Failed to get encrypted system credentials directory: %m"); + + /* Define early, so that it is definitely initialized, even if we take "goto not_found" branch below. */ + _cleanup_free_ DirectoryEntries *de = NULL; + + _cleanup_close_ int dfd = open(dp, O_CLOEXEC|O_DIRECTORY); + if (dfd < 0) { + if (errno == ENOENT) { + log_debug("No encrypted system credentials passed."); + goto not_found; + } + + return log_error_errno(errno, "Failed to open system credentials directory."); + } + + r = readdir_all(dfd, RECURSE_DIR_IGNORE_DOT, &de); + if (r < 0) + return log_error_errno(r, "Failed to enumerate system credentials: %m"); + + FOREACH_ARRAY(i, de->entries, de->n_entries) { + _cleanup_(iovec_done) struct iovec credential = {}; + struct dirent *d = *i; + + if (!startswith_no_case(d->d_name, "nvpcr-anchor.")) /* VFAT is case-insensitive, hence don't be too strict here */ + continue; + + r = read_full_file_full( + dfd, + d->d_name, + /* offset= */ UINT64_MAX, + CREDENTIAL_ENCRYPTED_SIZE_MAX, + READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER, + /* bind_name= */ NULL, + (char**) &credential.iov_base, + &credential.iov_len); + if (r == -ENOENT) + continue; + if (r < 0) { + log_warning_errno(r, "Failed to read anchor secret file '%s/%s', skipping: %m", dp, d->d_name); + continue; + } + + r = decrypt_credential_and_warn( + "nvpcr-anchor.cred", + now(CLOCK_REALTIME), + /* tpm2_device= */ NULL, + /* tpm2_signature_path= */ NULL, + /* uid= */ UID_INVALID, + &credential, + /* flags= */ 0, + ret_secret); + if (r < 0) + log_debug_errno(r, "Failed to decrypt anchor secret file '%s' passed in as system credential, skipping: %m", d->d_name); + else { + *ret_credential = TAKE_STRUCT(credential); + return 1; + } + } + + log_debug("No suitable anchor secret passed as system credential."); + +not_found: + *ret_credential = (struct iovec) {}; + *ret_secret = (struct iovec) {}; + return 0; +} +#endif + +#define ANCHOR_SECRET_SIZE 4096U + +int tpm2_nvpcr_acquire_anchor_secret(struct iovec *ret, bool sync_secondary) { +#if HAVE_OPENSSL + _cleanup_close_ int fd = -EBADF; + int r; + + /* Acquires the anchor secret. We store it in a credential. The primary location (and primary truth) + * for it is /run/systemd/nvpcr/ (i.e. volatile) [this file also doubles as lock file for the whole + * logic]. But something has to place it there once. We do keep two copies of it: one in + * /var/lib/systemd/nvpcr/, which is the persistent place for it, but which is only available at late + * boot, potentially. And one in the ESP/XBOOTLDR which will make it available in the initrd + * already via system credentials. */ + + _cleanup_close_ int dfd = open_mkdir("/run/systemd/nvpcr", O_CLOEXEC, 0755); + if (dfd < 0) + return log_error_errno(dfd, "Failed to open directory '/run/systemd/nvpcr': %m"); + + /* Use restrictive access mode of 0600. Not because the data inside needs to be kept inaccessible + * (it's encrypted, hence that'd be fine), but because we need to lock it, and unprivileged clients + * shouldn't be permitted to lock it. */ + fd = openat(dfd, "nvpcr-anchor.cred", O_RDWR|O_CLOEXEC|O_CREAT|O_NOCTTY, 0644); + if (fd < 0) + return log_error_errno(errno, "Failed to open anchor secret: %m"); + + r = lock_generic(fd, LOCK_BSD, LOCK_SH); + if (r < 0) + return log_error_errno(r, "Failed to lock anchor secret file: %m"); + + struct stat st; + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to stat() anchor secret: %m"); + + r = stat_verify_regular(&st); + if (r < 0) + return log_error_errno(r, "Anchor secret file is not a regular file: %m"); + + if (st.st_size == 0) { + /* If this is not initialized yet, then let's update the lock to an exclusive lock */ + r = lock_generic(fd, LOCK_BSD, LOCK_EX); + if (r < 0) + return log_error_errno(r, "Failed to upgrade lock on anchor secret file: %m"); + + /* Refresh size info, in case someone else has initialized it by now */ + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to stat() anchor secret: %m"); + } + + bool copy_to_var = true, copy_to_boot = true; + + _cleanup_(iovec_done) struct iovec credential = {}; + _cleanup_(iovec_done_erase) struct iovec secret = {}; + if (st.st_size == 0) { /* No initialized yet? */ + + /* Check if we have a secret in /var/lib/systemd/nvpcr/. If so, import the secret from there */ + if (!sync_secondary) { + r = tpm2_nvpcr_acquire_anchor_secret_from_var(&credential); + if (r < 0) + return r; + if (r > 0) + copy_to_var = false; /* We read the secret from /var/, hence we don't have to copy it there. */ + } + + /* Did the copy_source logic work? If not, let's search for the secret among passed system credentials. */ + if (!iovec_is_set(&credential)) { + r = tpm2_nvpcr_acquire_anchor_secret_from_credential(&credential, &secret); + if (r < 0) + return r; + if (r > 0) + copy_to_boot = false; /* We read the secret from the boot partition, hence we don't have to copy it there. */ + } + + /* Did the copy_source or system credential logic work? If not, let's generate a new random one */ + if (!iovec_is_set(&credential)) { + r = crypto_random_bytes_allocate_iovec(ANCHOR_SECRET_SIZE, &secret); + if (r < 0) + return log_error_errno(r, "Failed to acquire entropy for anchor secret: %m"); + + r = encrypt_credential_and_warn( + _CRED_AUTO_TPM2, + "nvpcr-anchor.cred", + now(CLOCK_REALTIME), + /* not_after= */ USEC_INFINITY, + /* tpm2_device= */ NULL, + /* tpm2_hash_pcr_mask= */ 0, + /* tpm2_pubkey_path= */ NULL, + /* tpm2_pubkey_pcrs= */ UINT32_MAX, + /* uid= */ UID_INVALID, + &secret, + /* flags= */ 0, + &credential); + if (r < 0) + return r; + } + + _cleanup_free_ char *encoded = NULL; + ssize_t n = base64mem_full(credential.iov_base, credential.iov_len, 79, &encoded); + if (n < 0) + return log_error_errno(n, "Failed to base64 encode credential: %m"); + + if (!strextend(&encoded, "\n")) + return log_oom(); + + n++; + + r = loop_write(fd, encoded, n); + if (r < 0) + return log_error_errno(r, "Failed to write anchor secret to disk: %m"); + } else { + /* The file was already initialized? Then just read it. */ + r = read_full_file_full( + fd, + /* filename= */ NULL, + /* offset= */ UINT64_MAX, + CREDENTIAL_ENCRYPTED_SIZE_MAX, + READ_FULL_FILE_UNBASE64|READ_FULL_FILE_FAIL_WHEN_LARGER, + /* bind_name= */ NULL, + (char**) &credential.iov_base, + &credential.iov_len); + if (r < 0) + return log_error_errno(r, "Failed to read anchor secret file: %m"); + } + + /* if we don't have the plaintext secret yet, then decrypt it now. */ + if (!iovec_is_set(&secret)) { + assert(iovec_is_set(&credential)); + + r = decrypt_credential_and_warn( + "nvpcr-anchor.cred", + now(CLOCK_REALTIME), + /* tpm2_device= */ NULL, + /* tpm2_signature_path= */ NULL, + /* uid= */ UID_INVALID, + &credential, + /* flags= */ 0, + &secret); + if (r < 0) + return r; + } + + if (sync_secondary) { + if (copy_to_var) { + r = tpm2_nvpcr_write_anchor_secret_to_var(&credential); + if (r < 0) + return r; + } + + if (copy_to_boot) { + r = tpm2_nvpcr_write_anchor_secret_to_boot(&credential); + if (r < 0) + return r; + } + } + + if (ret) + *ret = TAKE_STRUCT(secret); + return 0; +#else /* HAVE_OPENSSL */ + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL support is disabled."); +#endif +} + +int tpm2_nvpcr_initialize( + Tpm2Context *c, + const Tpm2Handle *session, + const char *name, + const struct iovec *anchor_secret) { + +#if HAVE_OPENSSL + TPM2_RC rc; + int r; + + assert(c); + assert(name); + + _cleanup_(nvpcr_data_done) NvPCRData p = {}; + r = nvpcr_data_load(name, &p); + if (r < 0) + return r; + + /* Open + lock the log file *before* we check for the *.anchor flag file. */ + _cleanup_close_ int log_fd = tpm2_userspace_log_open(); + + _cleanup_close_ int dfd = open_mkdir("/run/systemd/nvpcr", O_CLOEXEC, 0755); + if (dfd < 0) + return log_error_errno(dfd, "Failed to open directory '/run/systemd/nvpcr': %m"); + + const char *anchor_fname = strjoina(name, ".anchor"); + if (faccessat(dfd, anchor_fname, F_OK, AT_SYMLINK_NOFOLLOW) < 0) { + if (errno != ENOENT) + return log_debug_errno(errno, "Failed to check if /run/systemd/nvpcr/%s exists: %m", anchor_fname); + } else { + log_debug("NvPCR '%s' is already anchored.", name); + return 0; + } + + if (!iovec_is_set(anchor_secret)) + return log_debug_errno(SYNTHETIC_ERRNO(EUNATCH), "Need anchor secret."); + + const char *an = tpm2_hash_alg_to_string(p.algorithm); + if (!an) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Unsupported algorithm for NvPCR, refusing."); + + const EVP_MD *implementation; + assert_se(implementation = EVP_get_digestbyname(an)); + + int digest_size = EVP_MD_get_size(implementation); + assert_se(digest_size > 0); + + if ((size_t) digest_size > sizeof_field(TPM2B_MAX_NV_BUFFER, buffer)) + return log_debug_errno(SYNTHETIC_ERRNO(E2BIG), "Hash function result too large for TPM, refusing."); + + /* Put together a buffer consisting if the nvindex number and the NvPCR name, that we can calculate an HMAC() off, see below */ + size_t hmac_buffer_size = sizeof(le32_t) + strlen(p.name); + _cleanup_free_ void* hmac_buffer = malloc(hmac_buffer_size); + if (!hmac_buffer) + return log_oom_debug(); + + *(le32_t*) hmac_buffer = htole32(p.nv_index); + memcpy((uint8_t*) hmac_buffer + sizeof(le32_t), name, strlen(name)); + + TPM2B_MAX_NV_BUFFER buf = { + .size = digest_size, + }; + CLEANUP_ERASE(buf); + + /* We measure HMAC(anchor_secret, name) into the NvPCR to anchor it on our secret. */ + if (!HMAC(implementation, anchor_secret->iov_base, anchor_secret->iov_len, hmac_buffer, hmac_buffer_size, buf.buffer, NULL)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to calculate HMAC of data to measure."); + + _cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL; + r = tpm2_define_nvpcr_nv_index( + c, + session, + p.nv_index, + p.algorithm, + &nv_handle); + if (r < 0) + return r; + + log_debug("Successfully acquired handle to NV index 0x%" PRIx32 ".", p.nv_index); + + tpm2_userspace_log_dirty(log_fd); + rc = sym_Esys_NV_Extend( + c->esys_context, + /* authHandle= */ nv_handle->esys_handle, + /* nvIndex= */ nv_handle->esys_handle, + /* shandle1= */ ESYS_TR_PASSWORD, + /* shandle2= */ ESYS_TR_NONE, + /* shandle3= */ ESYS_TR_NONE, + &buf); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to extend NV index: %s", sym_Tss2_RC_Decode(rc)); + + log_debug("Successfully extended NvPCR '%s' with anchor secret.", name); + + /* Now pre-calculate the initial measurement of an "anchor" secret. This makes sure that others + * cannot delete and reproduce the same fake PCR, unless they also know the "anchor" secret. */ + TPM2B_DIGEST start = { /* initialize to zero */ + .size = digest_size, + }; + r = tpm2_digest_buffer( + p.algorithm, + &start, + buf.buffer, + buf.size, + /* extend= */ true); + if (r < 0) + return log_debug_errno(r, "Failed to calculate initial value: %m"); + + /* Now create the anchor flag file */ + _cleanup_free_ char *h = hexmem(start.buffer, start.size); + if (!h) + return log_oom_debug(); + + r = write_string_file_at(dfd, anchor_fname, h, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC); + if (r < 0) + return log_debug_errno(r, "Failed to write anchor file: %m"); + + tpm2_userspace_log_clean(log_fd); + return 1; +#else /* HAVE_OPENSSL */ + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL support is disabled."); +#endif +} + +int tpm2_nvpcr_read( + Tpm2Context *c, + const Tpm2Handle *session, + const char *name, + struct iovec *ret_value, + uint32_t *ret_nv_index) { + +#if HAVE_OPENSSL + int r; + + assert(c); + assert(name); + + if (!tpm2_nvpcr_name_is_valid(name)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Attempt to read from NvPCR with invalid name, refusing: %s", name); + + _cleanup_(nvpcr_data_done) NvPCRData p = {}; + r = nvpcr_data_load(name, &p); + if (r < 0) + return r; + + _cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL; + r = tpm2_index_to_handle( + c, + p.nv_index, + session, + /* ret_public= */ NULL, + /* ret_name= */ NULL, + /* ret_qname= */ NULL, + &nv_handle); + if (r < 0) + return log_debug_errno(r, "Failed to acquire handle to NV index 0x%" PRIu32 ".", p.nv_index); + + log_debug("Successfully acquired handle to NV index 0x%" PRIx32 ".", p.nv_index); + + r = tpm2_read_nv_index( + c, + /* session= */ NULL, + p.nv_index, + nv_handle, + ret_value); + if (r < 0) + return r; + + if (ret_nv_index) + *ret_nv_index = p.nv_index; return 0; #else /* HAVE_OPENSSL */ @@ -8140,3 +9127,9 @@ static const char* const tpm2_pcr_index_table[_TPM2_PCR_INDEX_MAX_DEFINED] = { DEFINE_STRING_TABLE_LOOKUP_FROM_STRING_WITH_FALLBACK(tpm2_pcr_index, int, TPM2_PCRS_MAX - 1); DEFINE_STRING_TABLE_LOOKUP_TO_STRING(tpm2_pcr_index, int); + +bool tpm2_nvpcr_name_is_valid(const char *name) { + return filename_is_valid(name) && + string_is_safe(name) && + tpm2_pcr_index_from_string(name) < 0; /* don't allow nvpcrs to be name like pcrs */ +} diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 67b035b442..564a3e46ad 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -150,6 +150,11 @@ const char* tpm2_userspace_event_type_to_string(Tpm2UserspaceEventType type) _co Tpm2UserspaceEventType tpm2_userspace_event_type_from_string(const char *s) _pure_; int tpm2_pcr_extend_bytes(Tpm2Context *c, char **banks, unsigned pcr_index, const struct iovec *data, const struct iovec *secret, Tpm2UserspaceEventType event, const char *description); +int tpm2_nvpcr_get_index(const char *name, uint32_t *ret); +int tpm2_nvpcr_extend_bytes(Tpm2Context *c, const Tpm2Handle *session, const char *name, const struct iovec *data, const struct iovec *secret, Tpm2UserspaceEventType event_type, const char *description); +int tpm2_nvpcr_acquire_anchor_secret(struct iovec *ret, bool sync_secondary); +int tpm2_nvpcr_initialize(Tpm2Context *c, const Tpm2Handle *session, const char *name, const struct iovec *anchor_secret); +int tpm2_nvpcr_read(Tpm2Context *c, const Tpm2Handle *session, const char *name, struct iovec *ret, uint32_t *ret_nv_index); uint32_t tpm2_tpms_pcr_selection_to_mask(const TPMS_PCR_SELECTION *s); void tpm2_tpms_pcr_selection_from_mask(uint32_t mask, TPMI_ALG_HASH hash, TPMS_PCR_SELECTION *ret); @@ -296,7 +301,10 @@ int tpm2_tpm2b_public_to_fingerprint(const TPM2B_PUBLIC *public, void **ret_fing int tpm2_define_policy_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE requested_nv_index, const TPM2B_DIGEST *write_policy, TPM2_HANDLE *ret_nv_index, Tpm2Handle **ret_nv_handle, TPM2B_NV_PUBLIC *ret_nv_public); int tpm2_write_policy_nv_index(Tpm2Context *c, const Tpm2Handle *policy_session, TPM2_HANDLE nv_index, const Tpm2Handle *nv_handle, const TPM2B_DIGEST *policy_digest); +int tpm2_define_nvpcr_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE nv_index, TPMI_ALG_HASH algorithm, Tpm2Handle **ret_nv_handle); +int tpm2_extend_nvpcr_nv_index(Tpm2Context *c, TPM2_HANDLE nv_index, const Tpm2Handle *nv_handle, const struct iovec *digest); int tpm2_undefine_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE nv_index, const Tpm2Handle *nv_handle); +int tpm2_read_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE nv_index, const Tpm2Handle *nv_handle, struct iovec *ret_value); int tpm2_seal_data(Tpm2Context *c, const struct iovec *data, const Tpm2Handle *primary_handle, const Tpm2Handle *encryption_session, const TPM2B_DIGEST *policy, struct iovec *ret_public, struct iovec *ret_private); int tpm2_unseal_data(Tpm2Context *c, const struct iovec *public, const struct iovec *private, const Tpm2Handle *primary_handle, const Tpm2Handle *policy_session, const Tpm2Handle *encryption_session, struct iovec *ret_data); @@ -510,3 +518,5 @@ const char* tpm2_pcr_index_to_string(int pcr) _const_; assert_cc(TPM2_NV_INDEX_UNASSIGNED_FIRST >= TPM2_NV_INDEX_FIRST); assert_cc(TPM2_NV_INDEX_UNASSIGNED_LAST <= TPM2_NV_INDEX_LAST); #endif + +bool tpm2_nvpcr_name_is_valid(const char *name); diff --git a/src/tpm2-setup/meson.build b/src/tpm2-setup/meson.build index 39fc97b91e..36082486d3 100644 --- a/src/tpm2-setup/meson.build +++ b/src/tpm2-setup/meson.build @@ -25,6 +25,23 @@ executables += [ generator_template + { 'name' : 'systemd-tpm2-generator', 'sources' : files('tpm2-generator.c'), + 'conditions' : [ + 'ENABLE_BOOTLOADER', + 'HAVE_OPENSSL', + 'HAVE_TPM2', + ], }, ] + +if conf.get('ENABLE_BOOTLOADER') == 1 and conf.get('HAVE_OPENSSL') == 1 and conf.get('HAVE_TPM2') == 1 + nvpcrs = [] + foreach n : nvpcrs + custom_target( + input : 'nvpcr/' + n + '.nvpcr.in', + output : n + '.nvpcr', + command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'], + install : true, + install_dir : prefixdir / 'lib/nvpcr') + endforeach +endif From f5f26332fa5a8502f6c1305c5a63174449bf5887 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Fri, 31 Oct 2025 17:56:41 +0100 Subject: [PATCH 13/22] catalog: improve PCR extended catalog entry --- catalog/systemd.catalog.in | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/catalog/systemd.catalog.in b/catalog/systemd.catalog.in index f7ef980962..cb1142b194 100644 --- a/catalog/systemd.catalog.in +++ b/catalog/systemd.catalog.in @@ -734,10 +734,14 @@ Support: %SUPPORT_URL% The Trusted Platform Module's (TPM) Platform Configuration Register (PCR) @PCR@, on banks @BANKS@, has been extended with the string '@MEASURING@'. -Whenever the system transitions to a new runtime phase, the specified PCR is -extended with a different string, to ensure that security policies for -TPM-bound secrets and other resources are limited to specific phases of the -runtime. +System state, configuration and properties are cryptographically measured into +the security chip in an irreversible way to provide local and remote +attestation of system state and identity. + +For example, whenever the system transitions to a new runtime phase, the +PCRs ares extended with a different string, to ensure that security +policies for TPM-bound secrets and other resources are limited to specific +phases of the runtime. -- f9b0be465ad540d0850ad32172d57c21 Subject: Memory Trimmed From 2b90bf173081758e75bdb2b8ff376731b0268ede Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 3 Jun 2024 21:44:50 +0200 Subject: [PATCH 14/22] pcrextend: make use new nvindex-based PCRs --- catalog/systemd.catalog.in | 13 ++ man/systemd-pcrphase.service.xml | 34 ++++- src/pcrextend/pcrextend.c | 161 ++++++++++++++++++---- src/shared/varlink-io.systemd.PCRExtend.c | 6 +- src/systemd/sd-messages.h | 2 + 5 files changed, 188 insertions(+), 28 deletions(-) diff --git a/catalog/systemd.catalog.in b/catalog/systemd.catalog.in index cb1142b194..4152112078 100644 --- a/catalog/systemd.catalog.in +++ b/catalog/systemd.catalog.in @@ -743,6 +743,19 @@ PCRs ares extended with a different string, to ensure that security policies for TPM-bound secrets and other resources are limited to specific phases of the runtime. +-- 4c2e46d266a747c6ac1460aa54484fa7 +Subject: TPM NvPCR Extended +Defined-By: systemd +Support: %SUPPORT_URL% + +The Trusted Platform Module's (TPM) additional Platform Configuration Register +(PCR) stored in non-volatile indexes (NV Indexes) @NVPCR@, has been extended +with the string '@MEASURING@'. + +System state, configuration and properties are cryptographically measured into +the security chip in an irreversible way to provide local and remote +attestation of system state and identity. + -- f9b0be465ad540d0850ad32172d57c21 Subject: Memory Trimmed Defined-By: systemd diff --git a/man/systemd-pcrphase.service.xml b/man/systemd-pcrphase.service.xml index e301435f8e..a00eda08bb 100644 --- a/man/systemd-pcrphase.service.xml +++ b/man/systemd-pcrphase.service.xml @@ -138,7 +138,8 @@ Options The /usr/lib/systemd/system-pcrextend executable may also be invoked from the - command line, where it expects the word to extend into PCR 11, as well as the following switches: + command line, where it expects the word to extend into a PCR or NvPCR, as well as the following + switches: @@ -155,11 +156,22 @@ Takes the index of the PCR to extend. If or - are specified defaults to 15, otherwise defaults to 11. + are specified defaults to 15, otherwise defaults to 11. May not be + combined with . + + + + Takes a name of an NvPCR to extend. NvPCRs are additional PCRs implemented via TPM NV + indexes. The name should be a short string such as hardware or + disk-encryption. May not be combined with . + + + + @@ -182,6 +194,17 @@ + + + + Selects early-boot mode. Specifically this means that the NvPCR anchor secret is not + attempted to be written into /var/lib/ and the boot loader partition in addition + to /run/. (Unlike the latter the former are generally not mounted and writable + during early boot or in the initrd.) + + + + @@ -230,6 +253,13 @@ + + + /usr/lib/nvpcr/*.nvpcr + + Definition files for NvPCRs. + + diff --git a/src/pcrextend/pcrextend.c b/src/pcrextend/pcrextend.c index cc50ad3ff7..68d90d4e4a 100644 --- a/src/pcrextend/pcrextend.c +++ b/src/pcrextend/pcrextend.c @@ -28,11 +28,14 @@ static char **arg_banks = NULL; static char *arg_file_system = NULL; static bool arg_machine_id = false; static unsigned arg_pcr_index = UINT_MAX; +static char *arg_nvpcr_name = NULL; static bool arg_varlink = false; +static bool arg_early = false; STATIC_DESTRUCTOR_REGISTER(arg_banks, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); STATIC_DESTRUCTOR_REGISTER(arg_file_system, freep); +STATIC_DESTRUCTOR_REGISTER(arg_nvpcr_name, freep); #define EXTENSION_STRING_SAFE_LIMIT 1024 @@ -53,10 +56,12 @@ static int help(int argc, char *argv[], void *userdata) { " --version Print version\n" " --bank=DIGEST Select TPM PCR bank (SHA1, SHA256)\n" " --pcr=INDEX Select TPM PCR index (0…23)\n" + " --nvpcr=NAME Select TPM PCR mode nvindex name\n" " --tpm2-device=PATH Use specified TPM2 device\n" " --graceful Exit gracefully if no TPM2 device is found\n" " --file-system=PATH Measure UUID/labels of file system into PCR 15\n" " --machine-id Measure machine ID into PCR 15\n" + " --early Run in early boot mode, without access to /var/\n" "\nSee the %2$s for details.\n", program_invocation_short_name, link, @@ -73,10 +78,12 @@ static int parse_argv(int argc, char *argv[]) { ARG_VERSION = 0x100, ARG_BANK, ARG_PCR, + ARG_NVPCR, ARG_TPM2_DEVICE, ARG_GRACEFUL, ARG_FILE_SYSTEM, ARG_MACHINE_ID, + ARG_EARLY, }; static const struct option options[] = { @@ -84,10 +91,12 @@ static int parse_argv(int argc, char *argv[]) { { "version", no_argument, NULL, ARG_VERSION }, { "bank", required_argument, NULL, ARG_BANK }, { "pcr", required_argument, NULL, ARG_PCR }, + { "nvpcr", required_argument, NULL, ARG_NVPCR }, { "tpm2-device", required_argument, NULL, ARG_TPM2_DEVICE }, { "graceful", no_argument, NULL, ARG_GRACEFUL }, { "file-system", required_argument, NULL, ARG_FILE_SYSTEM }, { "machine-id", no_argument, NULL, ARG_MACHINE_ID }, + { "early", no_argument, NULL, ARG_EARLY }, {} }; @@ -127,6 +136,15 @@ static int parse_argv(int argc, char *argv[]) { arg_pcr_index = r; break; + case ARG_NVPCR: + if (!tpm2_nvpcr_name_is_valid(optarg)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "NvPCR name is not valid: %s", optarg); + + r = free_and_strdup_warn(&arg_nvpcr_name, optarg); + if (r < 0) + return r; + break; + case ARG_TPM2_DEVICE: { _cleanup_free_ char *device = NULL; @@ -158,6 +176,10 @@ static int parse_argv(int argc, char *argv[]) { arg_machine_id = true; break; + case ARG_EARLY: + arg_early = true; + break; + case '?': return -EINVAL; @@ -165,15 +187,18 @@ static int parse_argv(int argc, char *argv[]) { assert_not_reached(); } - if (arg_file_system && arg_machine_id) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--file-system= and --machine-id may not be combined."); + if (!!arg_file_system + arg_machine_id > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--file-system=, --machine-id may not be combined."); + + if (arg_pcr_index != UINT_MAX && arg_nvpcr_name) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--pcr= and --nvpcr= may not be combined."); r = sd_varlink_invocation(SD_VARLINK_ALLOW_ACCEPT); if (r < 0) return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m"); if (r > 0) arg_varlink = true; - else if (arg_pcr_index == UINT_MAX) + else if (arg_pcr_index == UINT_MAX && !arg_nvpcr_name) arg_pcr_index = (arg_file_system || arg_machine_id) ? TPM2_PCR_SYSTEM_IDENTITY : /* → PCR 15 */ TPM2_PCR_KERNEL_BOOT; /* → PCR 11 */ @@ -198,7 +223,34 @@ static int determine_banks(Tpm2Context *c, unsigned target_pcr_nr) { return 0; } -static int extend_now(unsigned pcr, const void *data, size_t size, Tpm2UserspaceEventType event) { +static int escape_and_truncate_data(const void *data, size_t size, char **ret) { + _cleanup_free_ char *safe = NULL; + + assert(data || size == 0); + + if (size > EXTENSION_STRING_SAFE_LIMIT) { + safe = cescape_length(data, EXTENSION_STRING_SAFE_LIMIT); + if (!safe) + return -ENOMEM; + + if (!strextend(&safe, "...")) + return -ENOMEM; + } else { + safe = cescape_length(data, size); + if (!safe) + return -ENOMEM; + } + + *ret = TAKE_PTR(safe); + return 0; +} + +static int extend_pcr_now( + unsigned pcr, + const void *data, + size_t size, + Tpm2UserspaceEventType event) { + _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL; int r; @@ -218,18 +270,8 @@ static int extend_now(unsigned pcr, const void *data, size_t size, Tpm2Userspace return log_oom(); _cleanup_free_ char *safe = NULL; - if (size > EXTENSION_STRING_SAFE_LIMIT) { - safe = cescape_length(data, EXTENSION_STRING_SAFE_LIMIT); - if (!safe) - return log_oom(); - - if (!strextend(&safe, "...")) - return log_oom(); - } else { - safe = cescape_length(data, size); - if (!safe) - return log_oom(); - } + if (escape_and_truncate_data(data, size, &safe) < 0) + return log_oom(); log_debug("Measuring '%s' into PCR index %u, banks %s.", safe, pcr, joined_banks); @@ -247,8 +289,55 @@ static int extend_now(unsigned pcr, const void *data, size_t size, Tpm2Userspace return 0; } +static int extend_nvpcr_now( + const char *name, + const void *data, + size_t size, + Tpm2UserspaceEventType event) { + + _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL; + int r; + + r = tpm2_context_new_or_warn(arg_tpm2_device, &c); + if (r < 0) + return r; + + _cleanup_free_ char *safe = NULL; + if (escape_and_truncate_data(data, size, &safe) < 0) + return log_oom(); + + log_debug("Measuring '%s' into NvPCR index '%s'.", safe, name); + + r = tpm2_nvpcr_extend_bytes(c, /* session= */ NULL, name, &IOVEC_MAKE(data, size), /* secret= */ NULL, event, safe); + if (r == -ENETDOWN) { + /* NvPCR is not initialized yet. Let's do this now. */ + + _cleanup_(iovec_done_erase) struct iovec anchor_secret = {}; + r = tpm2_nvpcr_acquire_anchor_secret(&anchor_secret, /* sync_secondary= */ !arg_early); + if (r < 0) + return r; + + r = tpm2_nvpcr_initialize(c, /* session= */ NULL, name, &anchor_secret); + if (r < 0) + return log_error_errno(r, "Failed to extend NvPCR index '%s' with anchor secret: %m", name); + + r = tpm2_nvpcr_extend_bytes(c, /* session= */ NULL, name, &IOVEC_MAKE(data, size), /* secret= */ NULL, event, safe); + } + if (r < 0) + return log_error_errno(r, "Could not extend NvPCR: %m"); + + log_struct(LOG_INFO, + "MESSAGE_ID=" SD_MESSAGE_TPM_NVPCR_EXTEND_STR, + LOG_MESSAGE("Extended NvPCR index '%s' with '%s'.", name, safe), + "MEASURING=%s", safe, + "NVPCR=%u", name); + + return 0; +} + typedef struct MethodExtendParameters { unsigned pcr; + const char *nvpcr; const char *text; struct iovec data; } MethodExtendParameters; @@ -262,9 +351,10 @@ static void method_extend_parameters_done(MethodExtendParameters *p) { static int vl_method_extend(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) { static const sd_json_dispatch_field dispatch_table[] = { - { "pcr", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(MethodExtendParameters, pcr), SD_JSON_MANDATORY }, - { "text", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(MethodExtendParameters, text), 0 }, - { "data", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(MethodExtendParameters, data), 0 }, + { "pcr", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, offsetof(MethodExtendParameters, pcr), 0 }, + { "nvpcr", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(MethodExtendParameters, nvpcr), 0 }, + { "text", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(MethodExtendParameters, text), 0 }, + { "data", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, offsetof(MethodExtendParameters, data), 0 }, {} }; _cleanup_(method_extend_parameters_done) MethodExtendParameters p = { @@ -278,19 +368,38 @@ static int vl_method_extend(sd_varlink *link, sd_json_variant *parameters, sd_va if (r != 0) return r; - if (!TPM2_PCR_INDEX_VALID(p.pcr)) + if (p.nvpcr) { + /* Specifying both nvpcr name and pcr doesn't make sense */ + if (p.pcr != UINT_MAX) + return sd_varlink_error_invalid_parameter_name(link, "nvpcr"); + + if (!tpm2_nvpcr_name_is_valid(p.nvpcr)) + return sd_varlink_error_invalid_parameter_name(link, "nvpcr"); + + } else if (!TPM2_PCR_INDEX_VALID(p.pcr)) return sd_varlink_error_invalid_parameter_name(link, "pcr"); + struct iovec *extend_iovec, text_iovec; + if (p.text) { /* Specifying both the text string and the binary data is not allowed */ if (p.data.iov_base) return sd_varlink_error_invalid_parameter_name(link, "data"); - r = extend_now(p.pcr, p.text, strlen(p.text), _TPM2_USERSPACE_EVENT_TYPE_INVALID); + text_iovec = IOVEC_MAKE_STRING(p.text); + extend_iovec = &text_iovec; + } else if (p.data.iov_base) - r = extend_now(p.pcr, p.data.iov_base, p.data.iov_len, _TPM2_USERSPACE_EVENT_TYPE_INVALID); + extend_iovec = &p.data; else return sd_varlink_error_invalid_parameter_name(link, "text"); + + if (p.nvpcr) { + r = extend_nvpcr_now(p.nvpcr, extend_iovec->iov_base, extend_iovec->iov_len, _TPM2_USERSPACE_EVENT_TYPE_INVALID); + if (r == -ENOENT) + return sd_varlink_error(link, "io.systemd.PCRExtend.NoSuchNvPCR", NULL); + } else + r = extend_pcr_now(p.pcr, extend_iovec->iov_base, extend_iovec->iov_len, _TPM2_USERSPACE_EVENT_TYPE_INVALID); if (r < 0) return r; @@ -354,7 +463,6 @@ static int run(int argc, char *argv[]) { return r; event = TPM2_EVENT_MACHINE_ID; - } else { if (optind+1 != argc) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected a single argument."); @@ -386,9 +494,12 @@ static int run(int argc, char *argv[]) { return EXIT_SUCCESS; } - r = extend_now(arg_pcr_index, word, strlen(word), event); + if (arg_nvpcr_name) + r = extend_nvpcr_now(arg_nvpcr_name, word, strlen(word), event); + else + r = extend_pcr_now(arg_pcr_index, word, strlen(word), event); if (r < 0) - return log_error_errno(r, "Failed to create TPM2 context: %m"); + return r; return EXIT_SUCCESS; } diff --git a/src/shared/varlink-io.systemd.PCRExtend.c b/src/shared/varlink-io.systemd.PCRExtend.c index d104fc0853..84ee2e731b 100644 --- a/src/shared/varlink-io.systemd.PCRExtend.c +++ b/src/shared/varlink-io.systemd.PCRExtend.c @@ -6,14 +6,18 @@ static SD_VARLINK_DEFINE_METHOD( Extend, SD_VARLINK_FIELD_COMMENT("PCR number to extend, in range of 0…23"), SD_VARLINK_DEFINE_INPUT(pcr, SD_VARLINK_INT, 0), + SD_VARLINK_DEFINE_INPUT(nvpcr, SD_VARLINK_INT, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("Text string to measure. (Specify either this, or the 'data' field below, not both)"), SD_VARLINK_DEFINE_INPUT(text, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("Binary data to measure, encoded in Base64. (Specify either this, or the 'text' field above, not both)"), SD_VARLINK_DEFINE_INPUT(data, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); +static SD_VARLINK_DEFINE_ERROR(NoSuchNvPCR); + SD_VARLINK_DEFINE_INTERFACE( io_systemd_PCRExtend, "io.systemd.PCRExtend", SD_VARLINK_INTERFACE_COMMENT("TPM PCR Extension APIs"), SD_VARLINK_SYMBOL_COMMENT("Measure some text or binary data into a PCR"), - &vl_method_Extend); + &vl_method_Extend, + &vl_error_NoSuchNvPCR); diff --git a/src/systemd/sd-messages.h b/src/systemd/sd-messages.h index 02edf9facf..6dc7dd527a 100644 --- a/src/systemd/sd-messages.h +++ b/src/systemd/sd-messages.h @@ -272,6 +272,8 @@ _SD_BEGIN_DECLARATIONS; #define SD_MESSAGE_TPM_PCR_EXTEND SD_ID128_MAKE(3f,7d,5e,f3,e5,4f,43,02,b4,f0,b1,43,bb,27,0c,ab) #define SD_MESSAGE_TPM_PCR_EXTEND_STR SD_ID128_MAKE_STR(3f,7d,5e,f3,e5,4f,43,02,b4,f0,b1,43,bb,27,0c,ab) +#define SD_MESSAGE_TPM_NVPCR_EXTEND SD_ID128_MAKE(4c,2e,46,d2,66,a7,47,c6,ac,14,60,aa,54,48,4f,a7) +#define SD_MESSAGE_TPM_NVPCR_EXTEND_STR SD_ID128_MAKE_STR(4c,2e,46,d2,66,a7,47,c6,ac,14,60,aa,54,48,4f,a7) #define SD_MESSAGE_MEMORY_TRIM SD_ID128_MAKE(f9,b0,be,46,5a,d5,40,d0,85,0a,d3,21,72,d5,7c,21) #define SD_MESSAGE_MEMORY_TRIM_STR SD_ID128_MAKE_STR(f9,b0,be,46,5a,d5,40,d0,85,0a,d3,21,72,d5,7c,21) From 0196abbd10331f89bd5ca7abc39225852dff7406 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 4 Jun 2024 12:01:10 +0200 Subject: [PATCH 15/22] pcrextend: automatically measure SMBIOS product ID at boot Now that PCRs are not that expensive anymore, let's use them to measure the SMBIOS product ID to one. --- man/systemd-pcrphase.service.xml | 24 +++++++++++++--- src/pcrextend/pcrextend.c | 38 ++++++++++++++++++++++---- src/shared/pcrextend-util.c | 22 +++++++++++++++ src/shared/pcrextend-util.h | 1 + src/shared/tpm2-util.c | 1 + src/shared/tpm2-util.h | 1 + src/tpm2-setup/meson.build | 2 +- src/tpm2-setup/nvpcr/hardware.nvpcr.in | 5 ++++ units/meson.build | 5 ++++ units/systemd-pcrproduct.service.in | 23 ++++++++++++++++ 10 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 src/tpm2-setup/nvpcr/hardware.nvpcr.in create mode 100644 units/systemd-pcrproduct.service.in diff --git a/man/systemd-pcrphase.service.xml b/man/systemd-pcrphase.service.xml index a00eda08bb..b95007dfcb 100644 --- a/man/systemd-pcrphase.service.xml +++ b/man/systemd-pcrphase.service.xml @@ -21,10 +21,11 @@ systemd-pcrphase-sysinit.service systemd-pcrphase-initrd.service systemd-pcrmachine.service + systemd-pcrproduct.service systemd-pcrfs-root.service systemd-pcrfs@.service systemd-pcrextend - Measure boot phase into TPM2 PCR 11, machine ID and file system identity into PCR 15 + Measure boot phases, machine ID, product UUID and file system identity into TPM PCRs and NvPCRs @@ -49,6 +50,10 @@ (see machine-id5) into PCR 15. + systemd-pcrproduct.service is a system service that measures the firmware + product UUID (as provided by one of SMBIOS, Devicetree, …) into a NvPCR named + hardware. + systemd-pcrfs-root.service and systemd-pcrfs@.service are services that measure file system identity information (i.e. mount point, file system type, label and UUID, partition label and UUID) into PCR 15. systemd-pcrfs-root.service does so for @@ -156,8 +161,9 @@ Takes the index of the PCR to extend. If or - are specified defaults to 15, otherwise defaults to 11. May not be - combined with . + are specified defaults to 15, otherwise (and unless + is specified) defaults to 11. May not be combined with + . @@ -167,7 +173,8 @@ Takes a name of an NvPCR to extend. NvPCRs are additional PCRs implemented via TPM NV indexes. The name should be a short string such as hardware or - disk-encryption. May not be combined with . + disk-encryption. If is specified defaults + hardware. May not be combined with . @@ -214,6 +221,15 @@ + + + + Instead of measuring a word specified on the command line into PCR 11, measure the + firmware's product UUID into an NvPCR named hardware. + + + + diff --git a/src/pcrextend/pcrextend.c b/src/pcrextend/pcrextend.c index 68d90d4e4a..641b1e8cbe 100644 --- a/src/pcrextend/pcrextend.c +++ b/src/pcrextend/pcrextend.c @@ -27,6 +27,7 @@ static char *arg_tpm2_device = NULL; static char **arg_banks = NULL; static char *arg_file_system = NULL; static bool arg_machine_id = false; +static bool arg_product_id = false; static unsigned arg_pcr_index = UINT_MAX; static char *arg_nvpcr_name = NULL; static bool arg_varlink = false; @@ -50,6 +51,7 @@ static int help(int argc, char *argv[], void *userdata) { printf("%1$s [OPTIONS...] WORD\n" "%1$s [OPTIONS...] --file-system=PATH\n" "%1$s [OPTIONS...] --machine-id\n" + "%1$s [OPTIONS...] --product-id\n" "\n%5$sExtend a TPM2 PCR with boot phase, machine ID, or file system ID.%6$s\n" "\n%3$sOptions:%4$s\n" " -h --help Show this help\n" @@ -61,6 +63,7 @@ static int help(int argc, char *argv[], void *userdata) { " --graceful Exit gracefully if no TPM2 device is found\n" " --file-system=PATH Measure UUID/labels of file system into PCR 15\n" " --machine-id Measure machine ID into PCR 15\n" + " --product-id Measure SMBIOS product ID into NvPCR 'hardware'\n" " --early Run in early boot mode, without access to /var/\n" "\nSee the %2$s for details.\n", program_invocation_short_name, @@ -83,6 +86,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_GRACEFUL, ARG_FILE_SYSTEM, ARG_MACHINE_ID, + ARG_PRODUCT_ID, ARG_EARLY, }; @@ -96,6 +100,7 @@ static int parse_argv(int argc, char *argv[]) { { "graceful", no_argument, NULL, ARG_GRACEFUL }, { "file-system", required_argument, NULL, ARG_FILE_SYSTEM }, { "machine-id", no_argument, NULL, ARG_MACHINE_ID }, + { "product-id", no_argument, NULL, ARG_PRODUCT_ID }, { "early", no_argument, NULL, ARG_EARLY }, {} }; @@ -176,6 +181,10 @@ static int parse_argv(int argc, char *argv[]) { arg_machine_id = true; break; + case ARG_PRODUCT_ID: + arg_product_id = true; + break; + case ARG_EARLY: arg_early = true; break; @@ -187,8 +196,8 @@ static int parse_argv(int argc, char *argv[]) { assert_not_reached(); } - if (!!arg_file_system + arg_machine_id > 1) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--file-system=, --machine-id may not be combined."); + if (!!arg_file_system + arg_machine_id + arg_product_id > 1) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--file-system=, --machine-id, --product-id may not be combined."); if (arg_pcr_index != UINT_MAX && arg_nvpcr_name) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--pcr= and --nvpcr= may not be combined."); @@ -198,10 +207,16 @@ static int parse_argv(int argc, char *argv[]) { return log_error_errno(r, "Failed to check if invoked in Varlink mode: %m"); if (r > 0) arg_varlink = true; - else if (arg_pcr_index == UINT_MAX && !arg_nvpcr_name) - arg_pcr_index = (arg_file_system || arg_machine_id) ? - TPM2_PCR_SYSTEM_IDENTITY : /* → PCR 15 */ - TPM2_PCR_KERNEL_BOOT; /* → PCR 11 */ + else if (arg_pcr_index == UINT_MAX && !arg_nvpcr_name) { + arg_pcr_index = + (arg_file_system || arg_machine_id) ? TPM2_PCR_SYSTEM_IDENTITY : /* → PCR 15 */ + !arg_product_id ? TPM2_PCR_KERNEL_BOOT : /* → PCR 11 */ + UINT_MAX; + + r = free_and_strdup_warn(&arg_nvpcr_name, arg_product_id ? "hardware" : NULL); + if (r < 0) + return r; + } return 1; } @@ -463,6 +478,17 @@ static int run(int argc, char *argv[]) { return r; event = TPM2_EVENT_MACHINE_ID; + + } else if (arg_product_id) { + + if (optind != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected no argument."); + + r = pcrextend_product_id_word(&word); + if (r < 0) + return r; + + event = TPM2_EVENT_PRODUCT_ID; } else { if (optind+1 != argc) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected a single argument."); diff --git a/src/shared/pcrextend-util.c b/src/shared/pcrextend-util.c index e1e80a93ee..15e0da822a 100644 --- a/src/shared/pcrextend-util.c +++ b/src/shared/pcrextend-util.c @@ -10,6 +10,7 @@ #include "errno-util.h" #include "escape.h" #include "fd-util.h" +#include "id128-util.h" #include "log.h" #include "mountpoint-util.h" #include "pcrextend-util.h" @@ -158,3 +159,24 @@ int pcrextend_machine_id_word(char **ret) { *ret = TAKE_PTR(word); return 0; } + +int pcrextend_product_id_word(char **ret) { + _cleanup_free_ char *word = NULL; + sd_id128_t pid; + int r; + + assert(ret); + + r = id128_get_product(&pid); + if (IN_SET(r, -ENOENT, -EADDRNOTAVAIL)) /* No product UUID field, or an all-zero or all-0xFF UUID */ + word = strdup("product-id:missing"); + else if (r < 0) + return log_error_errno(r, "Failed to acquire product ID: %m"); + else + word = strjoin("product-id:", SD_ID128_TO_STRING(pid)); + if (!word) + return log_oom(); + + *ret = TAKE_PTR(word); + return 0; +} diff --git a/src/shared/pcrextend-util.h b/src/shared/pcrextend-util.h index 7dd612b8fb..88bd23c10d 100644 --- a/src/shared/pcrextend-util.h +++ b/src/shared/pcrextend-util.h @@ -3,3 +3,4 @@ int pcrextend_file_system_word(const char *path, char **ret, char **ret_normalized_path); int pcrextend_machine_id_word(char **ret); +int pcrextend_product_id_word(char **ret); diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 49da185271..da74ca06a5 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -6424,6 +6424,7 @@ static const char* tpm2_userspace_event_type_table[_TPM2_USERSPACE_EVENT_TYPE_MA [TPM2_EVENT_FILESYSTEM] = "filesystem", [TPM2_EVENT_VOLUME_KEY] = "volume-key", [TPM2_EVENT_MACHINE_ID] = "machine-id", + [TPM2_EVENT_PRODUCT_ID] = "product-id", }; DEFINE_STRING_TABLE_LOOKUP(tpm2_userspace_event_type, Tpm2UserspaceEventType); diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 564a3e46ad..667d783cea 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -142,6 +142,7 @@ typedef enum Tpm2UserspaceEventType { TPM2_EVENT_FILESYSTEM, TPM2_EVENT_VOLUME_KEY, TPM2_EVENT_MACHINE_ID, + TPM2_EVENT_PRODUCT_ID, _TPM2_USERSPACE_EVENT_TYPE_MAX, _TPM2_USERSPACE_EVENT_TYPE_INVALID = -EINVAL, } Tpm2UserspaceEventType; diff --git a/src/tpm2-setup/meson.build b/src/tpm2-setup/meson.build index 36082486d3..0221a5fba1 100644 --- a/src/tpm2-setup/meson.build +++ b/src/tpm2-setup/meson.build @@ -35,7 +35,7 @@ executables += [ ] if conf.get('ENABLE_BOOTLOADER') == 1 and conf.get('HAVE_OPENSSL') == 1 and conf.get('HAVE_TPM2') == 1 - nvpcrs = [] + nvpcrs = [ 'hardware' ] foreach n : nvpcrs custom_target( input : 'nvpcr/' + n + '.nvpcr.in', diff --git a/src/tpm2-setup/nvpcr/hardware.nvpcr.in b/src/tpm2-setup/nvpcr/hardware.nvpcr.in new file mode 100644 index 0000000000..ddeb7f99e5 --- /dev/null +++ b/src/tpm2-setup/nvpcr/hardware.nvpcr.in @@ -0,0 +1,5 @@ +{ + "name" : "hardware", + "algorithm" : "sha256", + "nvIndex" : {{TPM2_NVPCR_BASE + 0}} +} diff --git a/units/meson.build b/units/meson.build index ba2dfcab06..bd788f6d0b 100644 --- a/units/meson.build +++ b/units/meson.build @@ -556,6 +556,11 @@ units = [ 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], 'symlinks' : ['factory-reset.target.wants/'], }, + { + 'file' : 'systemd-pcrproduct.service.in', + 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2'], + 'symlinks' : ['sysinit.target.wants/'], + }, { 'file' : 'systemd-pcrphase-initrd.service.in', 'conditions' : ['ENABLE_BOOTLOADER', 'HAVE_OPENSSL', 'HAVE_TPM2', 'ENABLE_INITRD'], diff --git a/units/systemd-pcrproduct.service.in b/units/systemd-pcrproduct.service.in new file mode 100644 index 0000000000..a70fff19df --- /dev/null +++ b/units/systemd-pcrproduct.service.in @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=TPM PCR Product ID Measurement +Documentation=man:systemd-pcrproduct.service(8) +DefaultDependencies=no +Conflicts=shutdown.target +After=tpm2.target +Before=sysinit.target shutdown.target +ConditionPathExists=!/etc/initrd-release +ConditionSecurity=measured-uki + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart={{LIBEXECDIR}}/systemd-pcrextend --graceful --product-id From 2da86d62ffc8969511a6b87ee06af9f17d3ad86b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 4 Jun 2024 16:36:39 +0200 Subject: [PATCH 16/22] tpm2-setup: split out SRK setup into a function of its own --- src/tpm2-setup/tpm2-setup.c | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/tpm2-setup/tpm2-setup.c b/src/tpm2-setup/tpm2-setup.c index 032b1e3381..6338b7a75b 100644 --- a/src/tpm2-setup/tpm2-setup.c +++ b/src/tpm2-setup/tpm2-setup.c @@ -252,22 +252,9 @@ static int load_public_key_tpm2(struct public_key_data *ret) { return 0; } -static int run(int argc, char *argv[]) { +static int setup_srk(void) { int r; - log_setup(); - - r = parse_argv(argc, argv); - if (r <= 0) - return r; - - if (arg_graceful && !tpm2_is_fully_supported()) { - log_notice("No complete TPM2 support detected, exiting gracefully."); - return EXIT_SUCCESS; - } - - umask(0022); - _cleanup_(public_key_data_done) struct public_key_data runtime_key = {}, persistent_key = {}, tpm2_key = {}; r = load_public_key_disk(TPM2_SRK_PEM_RUNTIME_PATH, &runtime_key); @@ -390,4 +377,23 @@ static int run(int argc, char *argv[]) { return 0; } +static int run(int argc, char *argv[]) { + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + if (arg_graceful && !tpm2_is_fully_supported()) { + log_notice("No complete TPM2 support detected, exiting gracefully."); + return EXIT_SUCCESS; + } + + umask(0022); + + return setup_srk(); +} + DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); From 8a6e77f1a8088bb6c7877e256bcc1f1e71552819 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 4 Jun 2024 18:16:03 +0200 Subject: [PATCH 17/22] tpm2-setup: measure "anchor" extension early at boot into nvpcrs --- man/systemd-tpm2-setup.service.xml | 27 ++++++- src/tpm2-setup/tpm2-setup.c | 118 +++++++++++++++++++++++++++- units/systemd-tpm2-setup.service.in | 2 +- 3 files changed, 142 insertions(+), 5 deletions(-) diff --git a/man/systemd-tpm2-setup.service.xml b/man/systemd-tpm2-setup.service.xml index 52ed6acf92..95d8da4644 100644 --- a/man/systemd-tpm2-setup.service.xml +++ b/man/systemd-tpm2-setup.service.xml @@ -20,7 +20,7 @@ systemd-tpm2-setup.service systemd-tpm2-setup-early.service systemd-tpm2-setup - Set up the TPM2 Storage Root Key (SRK) at boot + Set up the TPM2 Storage Root Key (SRK) and initialize NvPCRs at boot @@ -33,7 +33,8 @@ systemd-tpm2-setup.service and systemd-tpm2-setup-early.service are services that generate the Storage Root Key - (SRK) if it has not been generated yet, and stores it in the TPM. + (SRK) if it has not been generated yet, and stores it in the TPM. If NvPCRs (additional PCR registers in + TPM NV Indexes) are defined, these are initialized with the anchoring secret. The services will store the public key of the SRK key pair in a PEM file in /run/systemd/tpm2-srk-public-key.pem and @@ -70,6 +71,28 @@ + + + /usr/lib/nvpcr/*.nvpcr + + Definition files for NvPCRs. + + + + + /run/credentials/@encrypted/nvpcr-anchor.* + + Encrypted NvPCR anchor secret, received into the system via system credentials. + + + + + $BOOT/loader/credentials/nvpcr-anchor.*.cred + + Encrypted NvPCR anchor secret, to be picked up by the kernel loader stub, + i.e. systemd-stub7. + + diff --git a/src/tpm2-setup/tpm2-setup.c b/src/tpm2-setup/tpm2-setup.c index 6338b7a75b..baa485debe 100644 --- a/src/tpm2-setup/tpm2-setup.c +++ b/src/tpm2-setup/tpm2-setup.c @@ -7,6 +7,10 @@ #include "alloc-util.h" #include "build.h" +#include "conf-files.h" +#include "constants.h" +#include "creds-util.h" +#include "errno-util.h" #include "fd-util.h" #include "fileio.h" #include "fs-util.h" @@ -16,7 +20,12 @@ #include "mkdir.h" #include "parse-util.h" #include "pretty-print.h" +#include "recurse-dir.h" +#include "set.h" #include "string-util.h" +#include "strv.h" +#include "terminal-util.h" +#include "time-util.h" #include "tmpfile-util.h" #include "tpm2-util.h" @@ -41,7 +50,7 @@ static int help(int argc, char *argv[], void *userdata) { return log_oom(); printf("%1$s [OPTIONS...]\n" - "\n%5$sSet up the TPM2 Storage Root Key (SRK).%6$s\n" + "\n%5$sSet up the TPM2 Storage Root Key (SRK), and initialize NvPCRs.%6$s\n" "\n%3$sOptions:%4$s\n" " -h --help Show this help\n" " --version Show package version\n" @@ -377,6 +386,108 @@ static int setup_srk(void) { return 0; } +typedef struct SetupNvPCRContext { + Tpm2Context *tpm2_context; + struct iovec anchor_secret; + size_t n_already, n_anchored; + Set *done; +} SetupNvPCRContext; + +static void setup_nvpcr_context_done(SetupNvPCRContext *c) { + assert(c); + + iovec_done_erase(&c->anchor_secret); + c->tpm2_context = tpm2_context_unref(c->tpm2_context); + c->done = set_free(c->done); +} + +static int setup_nvpcr_one( + SetupNvPCRContext *c, + const char *name) { + int r; + + assert(c); + assert(name); + + if (set_contains(c->done, name)) + return 0; + + if (!c->tpm2_context) { + r = tpm2_context_new_or_warn(arg_tpm2_device, &c->tpm2_context); + if (r < 0) + return r; + } + + r = tpm2_nvpcr_initialize(c->tpm2_context, /* session= */ NULL, name, &c->anchor_secret); + if (r == -EUNATCH) { + assert(!iovec_is_set(&c->anchor_secret)); + + /* If we get EUNATCH this means we actually need to initialize this NvPCR + * now, and haven't provided the anchor secret yet. Hence acquire it now. */ + + r = tpm2_nvpcr_acquire_anchor_secret(&c->anchor_secret, /* sync_secondary= */ !arg_early); + if (r < 0) + return log_error_errno(r, "Failed to acquire anchor secret: %m"); + + r = tpm2_nvpcr_initialize(c->tpm2_context, /* session= */ NULL, name, &c->anchor_secret); + } + if (r < 0) + return log_error_errno(r, "Failed to extend NvPCR index with anchor secret: %m"); + + if (r > 0) + c->n_anchored++; + else + c->n_already++; + + if (set_put_strdup(&c->done, name) < 0) + return log_oom(); + + return 0; +} + +static int setup_nvpcr(void) { + _cleanup_(setup_nvpcr_context_done) SetupNvPCRContext c = {}; + int r = 0; + + _cleanup_strv_free_ char **l = NULL; + r = conf_files_list_nulstr( + &l, + ".nvpcr", + /* root= */ NULL, + CONF_FILES_REGULAR|CONF_FILES_BASENAME|CONF_FILES_FILTER_MASKED|CONF_FILES_TRUNCATE_SUFFIX, + CONF_PATHS_NULSTR("nvpcr")); + if (r < 0) + return log_error_errno(r, "Failed to find .nvpcr files: %m"); + + STRV_FOREACH(i, l) { + r = setup_nvpcr_one(&c, *i); + if (r < 0) + return r; + } + + if (c.n_already > 0 && c.n_anchored == 0 && !arg_early) { + /* If we didn't anchor anything right now, but we anchored something earlier, then it might + * have happened in the initrd, and thus the anchor ID was not commited to /var/ or the ESP + * yet. Hence, let's explicitly do so now, to catch up. */ + + r = tpm2_nvpcr_acquire_anchor_secret(/* ret= */ NULL, /* sync_secondary= */ true); + if (r < 0) + return r; + } + + if (c.n_anchored > 0) { + if (c.n_already == 0) + log_info("%zu NvPCRs initialized.", c.n_anchored); + else + log_info("%zu NvPCRs initialized. (%zu NvPCRs were already initialized.)", c.n_anchored, c.n_already); + } else if (c.n_already > 0) + log_info("%zu NvPCRs already initialized.", c.n_already); + else + log_debug("No NvPCRs defined, nothing initialized."); + + return r; +} + static int run(int argc, char *argv[]) { int r; @@ -393,7 +504,10 @@ static int run(int argc, char *argv[]) { umask(0022); - return setup_srk(); + r = setup_srk(); + RET_GATHER(r, setup_nvpcr()); + + return r; } DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); diff --git a/units/systemd-tpm2-setup.service.in b/units/systemd-tpm2-setup.service.in index ac29a76966..34404a24cb 100644 --- a/units/systemd-tpm2-setup.service.in +++ b/units/systemd-tpm2-setup.service.in @@ -14,7 +14,7 @@ DefaultDependencies=no Conflicts=shutdown.target After=tpm2.target systemd-tpm2-setup-early.service systemd-remount-fs.service Before=sysinit.target shutdown.target -RequiresMountsFor=/var/lib/systemd/tpm2-srk-public-key.pem +RequiresMountsFor=/var/lib/systemd ConditionSecurity=measured-uki ConditionPathExists=!/etc/initrd-release From e5a2e7866572614e66cbf6c1d3969128562d9552 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 10 Jun 2024 18:24:58 +0200 Subject: [PATCH 18/22] cryptsetup: automatically measure used keyslot and mechanism (i.e. fido2, tpm2, pkcs11) to an NvPCR Fixes: #29877 --- man/crypttab.xml | 14 ++ src/cryptsetup/cryptsetup.c | 202 +++++++++++++++++--- src/gpt-auto-generator/gpt-auto-generator.c | 2 +- src/shared/tpm2-util.h | 1 + src/tpm2-setup/meson.build | 3 +- src/tpm2-setup/nvpcr/cryptsetup.nvpcr.in | 5 + 6 files changed, 198 insertions(+), 29 deletions(-) create mode 100644 src/tpm2-setup/nvpcr/cryptsetup.nvpcr.in diff --git a/man/crypttab.xml b/man/crypttab.xml index 1f77939589..a8a8242a62 100644 --- a/man/crypttab.xml +++ b/man/crypttab.xml @@ -941,6 +941,20 @@ + + + + Controls whether to measure information about the used LUKS unlock keyslot to a TPM2 + non-volatile index (nvindex in PCR mode). If set to to an empty string (which is the default) no TPM2 + nvindex extension is done, otherwise keyslot information is measured to an nvindex of the specified + name, which is allocated if needed. It is recommended to set this to cryptsetup to + enable this logic. The slot index and the used unlock mechanism (i.e. tpm2, + fido2, pkcs11) is measured along with the activated volume name + and its UUID. + + + + diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index 09939bfa48..508ca9750d 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -123,6 +123,7 @@ static char *arg_tpm2_pcrlock = NULL; static usec_t arg_token_timeout_usec = 30*USEC_PER_SEC; static unsigned arg_tpm2_measure_pcr = UINT_MAX; /* This and the following field is about measuring the unlocked volume key to the local TPM */ static char **arg_tpm2_measure_banks = NULL; +static char *arg_tpm2_measure_keyslot_nvpcr = NULL; static char *arg_link_keyring = NULL; static char *arg_link_key_type = NULL; static char *arg_link_key_description = NULL; @@ -138,6 +139,7 @@ STATIC_DESTRUCTOR_REGISTER(arg_fido2_rp_id, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_signature, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_measure_banks, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_tpm2_measure_keyslot_nvpcr, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_pcrlock, freep); STATIC_DESTRUCTOR_REGISTER(arg_link_keyring, freep); STATIC_DESTRUCTOR_REGISTER(arg_link_key_type, freep); @@ -555,6 +557,21 @@ static int parse_one_option(const char *option) { log_error("Build lacks OpenSSL support, cannot measure to PCR banks, ignoring: %s", option); #endif + } else if ((val = startswith(option, "tpm2-measure-keyslot-nvpcr="))) { + + if (isempty(val)) { + arg_tpm2_measure_keyslot_nvpcr = mfree(arg_tpm2_measure_keyslot_nvpcr); + return 0; + } + + if (!tpm2_nvpcr_name_is_valid(val)) { + log_warning("Invalid NvPCR name, ignoring: %s", option); + return 0; + } + + if (free_and_strdup(&arg_tpm2_measure_keyslot_nvpcr, val) < 0) + return log_oom(); + } else if ((val = startswith(option, "try-empty-password="))) { r = parse_boolean(val); @@ -1012,7 +1029,7 @@ static int measure_volume_key( if (r < 0) return r; if (r == 0) { - log_debug("Kernel stub did not measure kernel image into the expected PCR, skipping userspace measurement, too."); + log_debug("Kernel stub did not measure kernel image into the expected PCR, skipping userspace volume key measurement, too."); return 0; } @@ -1060,7 +1077,80 @@ static int measure_volume_key( return 0; #else - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support disabled, not measuring."); + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support disabled, not measuring volume key."); +#endif +} + +static int measure_keyslot( + struct crypt_device *cd, + const char *name, + const char *mechanism, + int keyslot) { + + int r; + + assert(cd); + assert(name); + + if (!arg_tpm2_measure_keyslot_nvpcr) { + log_debug("Not measuring unlock keyslot, deactivated."); + return 0; + } + + r = efi_measured_uki(LOG_WARNING); + if (r < 0) + return r; + if (r == 0) { + log_debug("Kernel stub did not measure kernel image into the expected PCR, skipping userspace key slot measurement, too."); + return 0; + } + +#if HAVE_TPM2 + _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL; + r = tpm2_context_new_or_warn(arg_tpm2_device, &c); + if (r < 0) + return r; + + _cleanup_free_ char *escaped = NULL; + escaped = xescape(name, ":"); /* avoid ambiguity around ":" once we join things below */ + if (!escaped) + return log_oom(); + + _cleanup_free_ char *k = NULL; + if (keyslot >= 0 && asprintf(&k, "%i", keyslot) < 0) + return log_oom(); + + _cleanup_free_ char *s = NULL; + s = strjoin("cryptsetup-keyslot:", escaped, ":", strempty(crypt_get_uuid(cd)), ":", strempty(mechanism), ":", strempty(k)); + if (!s) + return log_oom(); + + r = tpm2_nvpcr_extend_bytes(c, /* session= */ NULL, arg_tpm2_measure_keyslot_nvpcr, &IOVEC_MAKE_STRING(s), /* secret= */ NULL, TPM2_EVENT_KEYSLOT, s); + if (r == -ENETDOWN) { + /* NvPCR is not initialized yet. Do so now. */ + _cleanup_(iovec_done_erase) struct iovec anchor_secret = {}; + r = tpm2_nvpcr_acquire_anchor_secret(&anchor_secret, /* sync_secondary= */ false); + if (r < 0) + return r; + + r = tpm2_nvpcr_initialize(c, /* session= */ NULL, arg_tpm2_measure_keyslot_nvpcr, &anchor_secret); + if (r < 0) + return log_error_errno(r, "Failed to extend NvPCR index '%s' with anchor secret: %m", name); + + r = tpm2_nvpcr_extend_bytes(c, /* session= */ NULL, arg_tpm2_measure_keyslot_nvpcr, &IOVEC_MAKE_STRING(s), /* secret= */ NULL, TPM2_EVENT_KEYSLOT, s); + } + if (r < 0) + return log_error_errno(r, "Could not extend NvPCR: %m"); + + log_struct(LOG_INFO, + "MESSAGE_ID=" SD_MESSAGE_TPM_NVPCR_EXTEND_STR, + LOG_MESSAGE("Successfully extended NvPCR index '%s' with '%s'.", arg_tpm2_measure_keyslot_nvpcr, s), + "MEASURING=%s", s, + "NVPCR=%s", arg_tpm2_measure_keyslot_nvpcr); + + return 0; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support disabled, not measuring keyslot."); #endif } @@ -1074,6 +1164,8 @@ static int log_external_activation(int r, const char *volume) { static int measured_crypt_activate_by_volume_key( struct crypt_device *cd, const char *name, + const char *mechanism, + int keyslot, const void *volume_key, size_t volume_key_size, uint32_t flags) { @@ -1091,18 +1183,19 @@ static int measured_crypt_activate_by_volume_key( if (r < 0) return r; - if (volume_key_size == 0) { + if (volume_key_size > 0) + (void) measure_volume_key(cd, name, volume_key, volume_key_size); /* OK if fails */ + else log_debug("Not measuring volume key, none specified."); - return r; - } - (void) measure_volume_key(cd, name, volume_key, volume_key_size); /* OK if fails */ + (void) measure_keyslot(cd, name, mechanism, keyslot); /* ditto */ return r; } static int measured_crypt_activate_by_passphrase( struct crypt_device *cd, const char *name, + const char *mechanism, int keyslot, const char *passphrase, size_t passphrase_size, @@ -1136,17 +1229,21 @@ static int measured_crypt_activate_by_passphrase( if (!vk) return -ENOMEM; - r = crypt_volume_key_get(cd, keyslot, vk, &vks, passphrase, passphrase_size); - if (r < 0) - return r; + keyslot = crypt_volume_key_get(cd, keyslot, vk, &vks, passphrase, passphrase_size); + if (keyslot < 0) + return keyslot; - return measured_crypt_activate_by_volume_key(cd, name, vk, vks, flags); + return measured_crypt_activate_by_volume_key(cd, mechanism, name, keyslot, vk, vks, flags); shortcut: - r = crypt_activate_by_passphrase(cd, name, keyslot, passphrase, passphrase_size, flags); - if (r == -EEXIST) /* volume is already active */ - return log_external_activation(r, name); - return r; + keyslot = crypt_activate_by_passphrase(cd, name, keyslot, passphrase, passphrase_size, flags); + if (keyslot == -EEXIST) /* volume is already active */ + return log_external_activation(keyslot, name); + if (keyslot < 0) + return keyslot; + + (void) measure_keyslot(cd, name, mechanism, keyslot); + return keyslot; } static int attach_tcrypt( @@ -1226,7 +1323,14 @@ static int attach_tcrypt( return log_error_errno(r, "Failed to load tcrypt superblock on device %s: %m", crypt_get_device_name(cd)); } - r = measured_crypt_activate_by_volume_key(cd, name, NULL, 0, flags); + r = measured_crypt_activate_by_volume_key( + cd, + name, + /* mechanism= */ NULL, + /* keyslot= */ -1, + /* volume_key= */ NULL, + /* volume_key_size= */ 0, + flags); if (r < 0) return log_error_errno(r, "Failed to activate tcrypt device %s: %m", crypt_get_device_name(cd)); @@ -1351,6 +1455,8 @@ static bool use_token_plugins(void) { * plugins, if measurement has been requested. */ if (arg_tpm2_measure_pcr != UINT_MAX) return false; + if (arg_tpm2_measure_keyslot_nvpcr) + return false; #endif /* Disable tokens if we're in FIDO2 mode with manual parameters. */ @@ -1586,7 +1692,14 @@ static int attach_luks_or_plain_or_bitlk_by_fido2( } if (pass_volume_key) - r = measured_crypt_activate_by_volume_key(cd, name, decrypted_key, decrypted_key_size, flags); + r = measured_crypt_activate_by_volume_key( + cd, + name, + "fido2", + /* keyslot= */ -1, + decrypted_key, + decrypted_key_size, + flags); else { _cleanup_(erase_and_freep) char *base64_encoded = NULL; ssize_t base64_encoded_size; @@ -1597,7 +1710,14 @@ static int attach_luks_or_plain_or_bitlk_by_fido2( if (base64_encoded_size < 0) return log_oom(); - r = measured_crypt_activate_by_passphrase(cd, name, keyslot, base64_encoded, base64_encoded_size, flags); + r = measured_crypt_activate_by_passphrase( + cd, + name, + "fido2", + keyslot, + base64_encoded, + base64_encoded_size, + flags); } if (r == -EPERM) { log_error_errno(r, "Failed to activate with FIDO2 decrypted key. (Key incorrect?)"); @@ -1743,7 +1863,14 @@ static int attach_luks_or_plain_or_bitlk_by_pkcs11( assert(decrypted_key); if (pass_volume_key) - r = measured_crypt_activate_by_volume_key(cd, name, decrypted_key, decrypted_key_size, flags); + r = measured_crypt_activate_by_volume_key( + cd, + name, + "pkcs11", + /* keyslot= */ -1, + decrypted_key, + decrypted_key_size, + flags); else { _cleanup_(erase_and_freep) char *base64_encoded = NULL; ssize_t base64_encoded_size; @@ -1760,7 +1887,14 @@ static int attach_luks_or_plain_or_bitlk_by_pkcs11( if (base64_encoded_size < 0) return log_oom(); - r = measured_crypt_activate_by_passphrase(cd, name, keyslot, base64_encoded, base64_encoded_size, flags); + r = measured_crypt_activate_by_passphrase( + cd, + name, + "pkcs11", + keyslot, + base64_encoded, + base64_encoded_size, + flags); } if (r == -EPERM) { log_error_errno(r, "Failed to activate with PKCS#11 decrypted key. (Key incorrect?)"); @@ -2048,7 +2182,14 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2( } if (pass_volume_key) - r = measured_crypt_activate_by_volume_key(cd, name, decrypted_key.iov_base, decrypted_key.iov_len, flags); + r = measured_crypt_activate_by_volume_key( + cd, + name, + "tpm2", + /* keyslot= */ -1, + decrypted_key.iov_base, + decrypted_key.iov_len, + flags); else { _cleanup_(erase_and_freep) char *base64_encoded = NULL; ssize_t base64_encoded_size; @@ -2059,7 +2200,14 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2( if (base64_encoded_size < 0) return log_oom(); - r = measured_crypt_activate_by_passphrase(cd, name, keyslot, base64_encoded, base64_encoded_size, flags); + r = measured_crypt_activate_by_passphrase( + cd, + name, + "tpm2", + keyslot, + base64_encoded, + base64_encoded_size, + flags); } if (r == -EPERM) { log_error_errno(r, "Failed to activate with TPM2 decrypted key. (Key incorrect?)"); @@ -2085,9 +2233,9 @@ static int attach_luks_or_plain_or_bitlk_by_key_data( assert(key_data); if (pass_volume_key) - r = measured_crypt_activate_by_volume_key(cd, name, key_data->iov_base, key_data->iov_len, flags); + r = measured_crypt_activate_by_volume_key(cd, name, /* mechanism= */ NULL, /* keyslot= */ -1, key_data->iov_base, key_data->iov_len, flags); else - r = measured_crypt_activate_by_passphrase(cd, name, arg_key_slot, key_data->iov_base, key_data->iov_len, flags); + r = measured_crypt_activate_by_passphrase(cd, name, /* mechanism= */ NULL, arg_key_slot, key_data->iov_base, key_data->iov_len, flags); if (r == -EPERM) { log_error_errno(r, "Failed to activate. (Key incorrect?)"); return -EAGAIN; /* Log actual error, but return EAGAIN */ @@ -2138,9 +2286,9 @@ static int attach_luks_or_plain_or_bitlk_by_key_file( return log_error_errno(r, "Failed to read key file '%s': %m", key_file); if (pass_volume_key) - r = measured_crypt_activate_by_volume_key(cd, name, kfdata, kfsize, flags); + r = measured_crypt_activate_by_volume_key(cd, name, /* mechanism= */ NULL, /* keyslot= */ -1, kfdata, kfsize, flags); else - r = measured_crypt_activate_by_passphrase(cd, name, arg_key_slot, kfdata, kfsize, flags); + r = measured_crypt_activate_by_passphrase(cd, name, /* mechanism= */ NULL, arg_key_slot, kfdata, kfsize, flags); if (r == -EPERM) { log_error_errno(r, "Failed to activate with key file '%s'. (Key data incorrect?)", key_file); return -EAGAIN; /* Log actual error, but return EAGAIN */ @@ -2166,9 +2314,9 @@ static int attach_luks_or_plain_or_bitlk_by_passphrase( r = -EINVAL; STRV_FOREACH(p, passwords) { if (pass_volume_key) - r = measured_crypt_activate_by_volume_key(cd, name, *p, arg_key_size, flags); + r = measured_crypt_activate_by_volume_key(cd, name, /* mechanism= */ NULL, /* keyslot= */ -1, *p, arg_key_size, flags); else - r = measured_crypt_activate_by_passphrase(cd, name, arg_key_slot, *p, strlen(*p), flags); + r = measured_crypt_activate_by_passphrase(cd, name, /* mechanism= */ NULL, arg_key_slot, *p, strlen(*p), flags); if (r >= 0) break; } diff --git a/src/gpt-auto-generator/gpt-auto-generator.c b/src/gpt-auto-generator/gpt-auto-generator.c index 8ed8f15bbd..6b5ca7ed6a 100644 --- a/src/gpt-auto-generator/gpt-auto-generator.c +++ b/src/gpt-auto-generator/gpt-auto-generator.c @@ -127,7 +127,7 @@ static int add_cryptsetup( * assignment, under the assumption that people who are fine to use sd-stub with its PCR * assignments are also OK with our PCR 15 use here. */ if (r > 0) - if (!strextend_with_separator(&options, ",", "tpm2-measure-pcr=yes")) + if (!strextend_with_separator(&options, ",", "tpm2-measure-pcr=yes,tpm2-measure-slot-nvpcr=cryptsetup")) return log_oom(); if (r == 0) log_debug("Will not measure volume key of volume '%s', not booted via systemd-stub with measurements enabled.", id); diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 667d783cea..abf8adfdaf 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -143,6 +143,7 @@ typedef enum Tpm2UserspaceEventType { TPM2_EVENT_VOLUME_KEY, TPM2_EVENT_MACHINE_ID, TPM2_EVENT_PRODUCT_ID, + TPM2_EVENT_KEYSLOT, _TPM2_USERSPACE_EVENT_TYPE_MAX, _TPM2_USERSPACE_EVENT_TYPE_INVALID = -EINVAL, } Tpm2UserspaceEventType; diff --git a/src/tpm2-setup/meson.build b/src/tpm2-setup/meson.build index 0221a5fba1..9814fe1005 100644 --- a/src/tpm2-setup/meson.build +++ b/src/tpm2-setup/meson.build @@ -35,7 +35,8 @@ executables += [ ] if conf.get('ENABLE_BOOTLOADER') == 1 and conf.get('HAVE_OPENSSL') == 1 and conf.get('HAVE_TPM2') == 1 - nvpcrs = [ 'hardware' ] + nvpcrs = [ 'cryptsetup', + 'hardware' ] foreach n : nvpcrs custom_target( input : 'nvpcr/' + n + '.nvpcr.in', diff --git a/src/tpm2-setup/nvpcr/cryptsetup.nvpcr.in b/src/tpm2-setup/nvpcr/cryptsetup.nvpcr.in new file mode 100644 index 0000000000..acec95206f --- /dev/null +++ b/src/tpm2-setup/nvpcr/cryptsetup.nvpcr.in @@ -0,0 +1,5 @@ +{ + "name" : "cryptsetup", + "algorithm" : "sha256", + "nvIndex" : {{TPM2_NVPCR_BASE + 1}} +} From d73d369133dde8b1d29c9e32e6d21cc2fbf3a876 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 4 Jun 2024 11:03:39 +0200 Subject: [PATCH 19/22] analyze: add new verb for determining NvPCR values --- man/systemd-analyze.xml | 24 ++++++ shell-completion/bash/systemd-analyze | 2 +- src/analyze/analyze-nvpcrs.c | 110 ++++++++++++++++++++++++++ src/analyze/analyze-nvpcrs.h | 4 + src/analyze/analyze.c | 7 ++ src/analyze/meson.build | 1 + 6 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 src/analyze/analyze-nvpcrs.c create mode 100644 src/analyze/analyze-nvpcrs.h diff --git a/man/systemd-analyze.xml b/man/systemd-analyze.xml index 04b8d8d41f..fb95a3ec53 100644 --- a/man/systemd-analyze.xml +++ b/man/systemd-analyze.xml @@ -201,6 +201,12 @@ pcrs PCR + + systemd-analyze + OPTIONS + nvpcrs + NVPCR + systemd-analyze OPTIONS @@ -1059,6 +1065,24 @@ NR NAME SHA256 + + <command>systemd-analyze nvpcrs <optional><replaceable>NVPCR</replaceable>…</optional></command> + + This command shows the known TPM2 NvPCRs (additional PCRs stored in TPM2 NV indexes) along with + their identifying names and current values. + + + Example Output + + $ systemd-analyze nvpcrs +NAME NVINDEX VALUE +cryptsetup 0x1d10201 f400543943cc7215557ce672872ace5382e6d53177cc459078ba9277e41588d9 +hardware 0x1d10200 e155474936d7d74c893e6ece1099a2311d572cf23becea159dabf282db754284 + + + + + <command>systemd-analyze srk <optional>><replaceable>FILE</replaceable></optional></command> diff --git a/shell-completion/bash/systemd-analyze b/shell-completion/bash/systemd-analyze index 832da33021..0ed8191c0a 100644 --- a/shell-completion/bash/systemd-analyze +++ b/shell-completion/bash/systemd-analyze @@ -68,7 +68,7 @@ _systemd_analyze() { ) local -A VERBS=( - [STANDALONE]='time blame unit-files unit-paths exit-status compare-versions calendar timestamp timespan pcrs srk has-tpm2 smbios11 chid' + [STANDALONE]='time blame unit-files unit-paths exit-status compare-versions calendar timestamp timespan pcrs nvpcrs srk has-tpm2 smbios11 chid' [CRITICAL_CHAIN]='critical-chain' [DOT]='dot' [DUMP]='dump' diff --git a/src/analyze/analyze-nvpcrs.c b/src/analyze/analyze-nvpcrs.c new file mode 100644 index 0000000000..0fc7b163cf --- /dev/null +++ b/src/analyze/analyze-nvpcrs.c @@ -0,0 +1,110 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "analyze-nvpcrs.h" +#include "analyze.h" +#include "conf-files.h" +#include "constants.h" +#include "format-table.h" +#include "hexdecoct.h" +#include "string-util.h" +#include "strv.h" +#include "tpm2-util.h" + +#if HAVE_TPM2 +static int add_nvpcr_to_table(Tpm2Context **c, Table *t, const char *name) { + int r; + + _cleanup_free_ char *h = NULL; + uint32_t nv_index = 0; + if (c) { + if (!*c) { + r = tpm2_context_new_or_warn(/* device= */ NULL, c); + if (r < 0) + return r; + } + + _cleanup_(iovec_done) struct iovec digest = {}; + r = tpm2_nvpcr_read(*c, /* session= */ NULL, name, &digest, &nv_index); + if (r < 0) + return log_error_errno(r, "Failed to read NvPCR '%s': %m", name); + + h = hexmem(digest.iov_base, digest.iov_len); + if (!h) + return log_oom(); + } else { + r = tpm2_nvpcr_get_index(name, &nv_index); + if (r < 0) + return log_error_errno(r, "Failed to get NV index of NvPCR '%s': %m", name); + } + + r = table_add_many( + t, + TABLE_STRING, name, + TABLE_UINT32_HEX_0x, nv_index, + TABLE_STRING, h); + if (r < 0) + return table_log_add_error(r); + + return 0; +} +#endif + +int verb_nvpcrs(int argc, char *argv[], void *userdata) { +#if HAVE_TPM2 + _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL; + _cleanup_(table_unrefp) Table *table = NULL; + int r; + + bool have_tpm2 = tpm2_is_fully_supported(); + + if (!have_tpm2) + log_notice("System lacks full TPM2 support, not showing NvPCR state."); + + table = table_new("name", "nvindex", "value"); + if (!table) + return log_oom(); + + (void) table_set_align_percent(table, table_get_cell(table, 0, 1), 100); + (void) table_set_ersatz_string(table, TABLE_ERSATZ_DASH); + (void) table_set_sort(table, (size_t) 0); + + if (!have_tpm2) + (void) table_hide_column_from_display(table, (size_t) 2); + + if (strv_isempty(strv_skip(argv, 1))) { + _cleanup_strv_free_ char **l = NULL; + r = conf_files_list_nulstr( + &l, + ".nvpcr", + /* root= */ NULL, + CONF_FILES_REGULAR|CONF_FILES_BASENAME|CONF_FILES_FILTER_MASKED|CONF_FILES_TRUNCATE_SUFFIX, + CONF_PATHS_NULSTR("nvpcr")); + if (r < 0) + return log_error_errno(r, "Failed to find .nvpcr files: %m"); + + STRV_FOREACH(i, l) { + r = add_nvpcr_to_table(have_tpm2 ? &c : NULL, table, *i); + if (r < 0) + return r; + } + } else + for (int i = 1; i < argc; i++) { + r = add_nvpcr_to_table(have_tpm2 ? &c : NULL, table, argv[i]); + if (r < 0) + return r; + } + + if (table_isempty(table) && FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) + log_notice("No NvPCRs defined."); + else { + r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, /* show_header= */ true); + if (r < 0) + return log_error_errno(r, "Failed to output table: %m"); + } + + return EXIT_SUCCESS; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support not enabled at build time."); +#endif +} diff --git a/src/analyze/analyze-nvpcrs.h b/src/analyze/analyze-nvpcrs.h new file mode 100644 index 0000000000..258005617e --- /dev/null +++ b/src/analyze/analyze-nvpcrs.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int verb_nvpcrs(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c index 9b611daade..d3040d03e0 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -33,6 +33,7 @@ #include "analyze-inspect-elf.h" #include "analyze-log-control.h" #include "analyze-malloc.h" +#include "analyze-nvpcrs.h" #include "analyze-pcrs.h" #include "analyze-plot.h" #include "analyze-security.h" @@ -258,6 +259,7 @@ static int help(int argc, char *argv[], void *userdata) { "\n%3$sTPM Operations:%4$s\n" " has-tpm2 Report whether TPM2 support is available\n" " pcrs [PCR...] Show TPM2 PCRs and their names\n" + " nvpcrs [NVPCR...] Show additional TPM2 PCRs stored in NV indexes\n" " srk [>FILE] Write TPM2 SRK (to FILE)\n" "\n%3$sOptions:%4$s\n" " --recursive-errors=MODE Control which units are verified\n" @@ -713,6 +715,10 @@ done: return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option --offline= requires one or more units to perform a security review."); + if (arg_json_format_flags != SD_JSON_FORMAT_OFF && !STRPTR_IN_SET(argv[optind], "security", "inspect-elf", "dlopen-metadata", "plot", "fdstore", "pcrs", "nvpcrs", "architectures", "capability", "exit-status")) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Option --json= is only supported for security, inspect-elf, dlopen-metadata, plot, fdstore, pcrs, nvpcrs, architectures, capability, exit-status right now."); + if (arg_threshold != 100 && !streq_ptr(argv[optind], "security")) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Option --threshold= is only supported for security right now."); @@ -806,6 +812,7 @@ static int run(int argc, char *argv[]) { { "image-policy", 2, 2, 0, verb_image_policy }, { "has-tpm2", VERB_ANY, 1, 0, verb_has_tpm2 }, { "pcrs", VERB_ANY, VERB_ANY, 0, verb_pcrs }, + { "nvpcrs", VERB_ANY, VERB_ANY, 0, verb_nvpcrs }, { "srk", VERB_ANY, 1, 0, verb_srk }, { "architectures", VERB_ANY, VERB_ANY, 0, verb_architectures }, { "smbios11", VERB_ANY, 1, 0, verb_smbios11 }, diff --git a/src/analyze/meson.build b/src/analyze/meson.build index e26426fc6c..03fade3f4b 100644 --- a/src/analyze/meson.build +++ b/src/analyze/meson.build @@ -21,6 +21,7 @@ systemd_analyze_sources = files( 'analyze-inspect-elf.c', 'analyze-log-control.c', 'analyze-malloc.c', + 'analyze-nvpcrs.c', 'analyze-pcrs.c', 'analyze-plot.c', 'analyze-security.c', From a9d02df0c7b25e11fce74756009014185bbc3377 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 27 Oct 2025 16:15:41 +0100 Subject: [PATCH 20/22] ci: never go to raw tpm device, always go via resource manager --- test/units/TEST-70-TPM2.tpm2-setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/units/TEST-70-TPM2.tpm2-setup.sh b/test/units/TEST-70-TPM2.tpm2-setup.sh index faf6fe7847..72f7841858 100755 --- a/test/units/TEST-70-TPM2.tpm2-setup.sh +++ b/test/units/TEST-70-TPM2.tpm2-setup.sh @@ -15,7 +15,7 @@ fi "$SD_TPM2SETUP" --version "$SD_TPM2SETUP" --tpm2-device=list "$SD_TPM2SETUP" --tpm2-device=auto -"$SD_TPM2SETUP" --tpm2-device=/dev/tpm0 +"$SD_TPM2SETUP" --tpm2-device=/dev/tpmrm0 "$SD_TPM2SETUP" --early=yes "$SD_TPM2SETUP" --early=yes "$SD_TPM2SETUP" --early=no From 34c687f2b3885fbe053d6a908fc7ce0b43d1db19 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 27 Oct 2025 12:55:02 +0100 Subject: [PATCH 21/22] ci: add some test for the new nvpcr infra --- test/units/TEST-65-ANALYZE.sh | 5 +++++ test/units/TEST-70-TPM2.pcrextend.sh | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/test/units/TEST-65-ANALYZE.sh b/test/units/TEST-65-ANALYZE.sh index d71b76e92a..f368d21653 100755 --- a/test/units/TEST-65-ANALYZE.sh +++ b/test/units/TEST-65-ANALYZE.sh @@ -1093,6 +1093,11 @@ systemd-analyze image-policy 'home=encrypted:usr=verity' 2>&1 | grep -q -e '^usr systemd-analyze pcrs systemd-analyze pcrs --json=pretty systemd-analyze pcrs 14 7 0 ima +if systemd-analyze has-tpm2 -q ; then + systemd-analyze nvpcrs + systemd-analyze nvpcrs --json=pretty + systemd-analyze nvpcrs hardware cryptsetup +fi systemd-analyze architectures systemd-analyze architectures --json=pretty diff --git a/test/units/TEST-70-TPM2.pcrextend.sh b/test/units/TEST-70-TPM2.pcrextend.sh index 318fce02a1..e5cd4843d6 100755 --- a/test/units/TEST-70-TPM2.pcrextend.sh +++ b/test/units/TEST-70-TPM2.pcrextend.sh @@ -30,6 +30,7 @@ export SYSTEMD_FORCE_MEASURE=1 "$SD_PCREXTEND" --version "$SD_PCREXTEND" foo "$SD_PCREXTEND" --machine-id +"$SD_PCREXTEND" --product-id "$SD_PCREXTEND" --tpm2-device=list "$SD_PCREXTEND" --tpm2-device=auto foo "$SD_PCREXTEND" --tpm2-device=/dev/tpm0 foo @@ -40,6 +41,7 @@ export SYSTEMD_FORCE_MEASURE=1 "$SD_PCREXTEND" --file-system=/ "$SD_PCREXTEND" --file-system=/tmp --file-system=/ "$SD_PCREXTEND" --file-system=/tmp --file-system=/ --pcr=15 --pcr=11 +"$SD_PCREXTEND" --nvpcr=hardware foo if tpm_has_pcr sha1 11; then "$SD_PCREXTEND" --bank=sha1 --pcr=11 foo @@ -55,6 +57,7 @@ fi (! "$SD_PCREXTEND" --pcr=-1 foo) (! "$SD_PCREXTEND" --pcr=1024 foo) (! "$SD_PCREXTEND" --foo=bar) +(! "$SD_PCREXTEND" --nvpcr=idontexist foo) unset SYSTEMD_FORCE_MEASURE @@ -122,3 +125,28 @@ diff /tmp/newpcr15 \ <(cat /tmp/oldpcr15 <(echo -n "file-system:$FS_WORD" | openssl dgst -binary -sha256) | openssl dgst -binary -sha256) rm -f /tmp/oldpcr{11,15} /tmp/newpcr{11,15} + +mkdir -p /run/nvpcr + +cat >/run/nvpcr/test.nvpcr < Date: Fri, 31 Oct 2025 18:16:07 +0100 Subject: [PATCH 22/22] doc: document NvPCRs briefly --- docs/TPM2_PCR_MEASUREMENTS.md | 58 +++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/docs/TPM2_PCR_MEASUREMENTS.md b/docs/TPM2_PCR_MEASUREMENTS.md index c6164b76d2..abd280c004 100644 --- a/docs/TPM2_PCR_MEASUREMENTS.md +++ b/docs/TPM2_PCR_MEASUREMENTS.md @@ -43,6 +43,37 @@ recognizable. Measurements currently recorded as `EV_IPL` will continue to be recorded as `EV_IPL`, for compatibility reasons. However, `EV_IPL` will not be used for new, additional measurements. +## NvPCR Measurements + +Since the PCR number space is very small, systemd userspace supports additional +PCRs implemented via TPM2 NV Indexes (here called *NvPCRs*, even though they +are no less volatile than classic PCRs), using the `TPM2_NT_EXTEND` type. These +mostly behave like real PCRs, but we can allocate them relatively freely from +the NV index handle space. + +The NV index range to use for this is configurable at build time, so that +downstreams have some flexibility to change this if they want. This uses the +0x01d10200 NV index as base by default. To abstract the actual nvindex number +away there's a naming concept, so that nvindexes are referenced by name string +rather than number. + +NvPCRs are defined in little JSON snippets in `/usr/lib/nvpcr/*.nvpcr`, that +match up index number and name, as well as pick a hash algorithm. + +There's one complication: these NV indexes (like any NV indexes) can be deleted +by anyone with access to the TPM, and then be recreated. This could be used to +reset the NvPCRs to zero during runtime, which defeats the whole point of +them. Our way out: we measure a secret as first thing after creation into the +NvPCRs. (Or actually, we measure a per-NvPCR secret we derive from a system +secret via an HMAC of the NvPCR name and the NV index handle). This "anchoring" +secret is stored in `/run/` + `/var/lib/` + ESP/XBOOTLDR (the latter encrypted +as credential, locked to the TPM), to make it available at the whole runtime of +the OS. It's only accessible to privileged processes with access to the +TPM. Due to this, any process with access to the TPM and read access to any of +the storage locations of the anchor secret is considered part of the TCB, as +they are able to replay the NvPCR with their own content at will, so due care +must be employed when designing a system that uses this feature. + ## PCR Measurements Made by `systemd-boot` (UEFI) ### PCR 5, `EV_EVENT_TAG`, `loader.conf` @@ -168,7 +199,7 @@ initrd" in UTF-16. → **Measured hash** covers the per-UKI sysext cpio archive (which is generated on-the-fly by `systemd-stub`). -## PCR Measurements Made by `systemd-pcrextend` (Userspace) +## PCR/NvPCR Measurements Made by `systemd-pcrextend` (Userspace) ### PCR 11, boot phases @@ -191,6 +222,17 @@ from `/etc/machine-id`) during boot. formatted in hexadecimal lowercase characters (in UTF-8, without trailing NUL bytes). +### NvPCR `hardware` (base+0), product UUID + +The `systemd-pcrproduct.service` service will measure the product UUID (as +available from SMBIOS or Devicetree) of the host system, once at boot. + +→ **Measured hash** covers the string "product-id:" suffixed by the product +UUID formatted in hexadecimal lowercase characters, without separators. If no +product UUID of the local system could be determined the string +"product-id:missing" is measured instead. Example string: +`product-id:4691595be6a345f1833cc75fab63e475`. + ### PCR 15, file system The `systemd-pcrfs-root.service` and `systemd-pcrfs@.service` services will @@ -202,7 +244,7 @@ colon-separated strings, identifying the file system type, UUID, label as well as the GPT partition entry UUID, entry type UUID and entry label (in UTF-8, without trailing NUL bytes). -## PCR Measurements Made by `systemd-cryptsetup` (Userspace) +## PCR/NvPCR Measurements Made by `systemd-cryptsetup` (Userspace) ### PCR 15, volume key @@ -214,3 +256,15 @@ system). → **Measured hash** covers the (binary) result of the HMAC(V,S) calculation where V is the LUKS volume key, and S is the string "cryptsetup:" followed by the LUKS volume name and the UUID of the LUKS superblock. + +### NvPCR `cryptsetup` (base+1), LUKS unlock mechanism/key slot + +The `systemd-cryptsetup@.service` service will measure information about the +used LUKS keyslot, and in particular include the used unlock mechanism (pkcs11, +tpm2, fido2, …) in it. + +→ **Measured hash** covers the string "cryptsetup-keyslot:", suffixed by the DM +volume name, a ":" separator, the UUID of the LUKS superblock, a ":" separator, +a brief string identifying the unlock mechanism, a ":" separator, and finally +the LUKS slot number used. Example string: +`cryptsetup-keyslot:root:1e023a55-60f9-4b6b-9b80-67438dc5f065:tpm2:1`