From 9892b7238b0fef076aaed3cf2cf55c886e71e4b8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 16 Apr 2024 13:43:07 +0200 Subject: [PATCH 01/11] tpm2-util: import two more symbols from tpm2-tss libraries We want to make use of TPM_PolicySigned soon, hence import the necessary symbols from tpm2-tss. --- src/shared/tpm2-util.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 42975cdb97..69f8ee7a7c 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -65,6 +65,7 @@ static DLSYM_FUNCTION(Esys_PolicyAuthorizeNV); static DLSYM_FUNCTION(Esys_PolicyGetDigest); static DLSYM_FUNCTION(Esys_PolicyOR); static DLSYM_FUNCTION(Esys_PolicyPCR); +static DLSYM_FUNCTION(Esys_PolicySigned); static DLSYM_FUNCTION(Esys_ReadPublic); static DLSYM_FUNCTION(Esys_StartAuthSession); static DLSYM_FUNCTION(Esys_Startup); @@ -77,6 +78,7 @@ static DLSYM_FUNCTION(Esys_TR_GetTpmHandle); static DLSYM_FUNCTION(Esys_TR_Serialize); static DLSYM_FUNCTION(Esys_TR_SetAuth); static DLSYM_FUNCTION(Esys_TRSess_GetAttributes); +static DLSYM_FUNCTION(Esys_TRSess_GetNonceTPM); static DLSYM_FUNCTION(Esys_TRSess_SetAttributes); static DLSYM_FUNCTION(Esys_Unseal); static DLSYM_FUNCTION(Esys_VerifySignature); @@ -132,6 +134,7 @@ int dlopen_tpm2(void) { DLSYM_ARG(Esys_PolicyGetDigest), DLSYM_ARG(Esys_PolicyOR), DLSYM_ARG(Esys_PolicyPCR), + DLSYM_ARG(Esys_PolicySigned), DLSYM_ARG(Esys_ReadPublic), DLSYM_ARG(Esys_StartAuthSession), DLSYM_ARG(Esys_Startup), @@ -143,6 +146,7 @@ int dlopen_tpm2(void) { DLSYM_ARG(Esys_TR_Serialize), DLSYM_ARG(Esys_TR_SetAuth), DLSYM_ARG(Esys_TRSess_GetAttributes), + DLSYM_ARG(Esys_TRSess_GetNonceTPM), DLSYM_ARG(Esys_TRSess_SetAttributes), DLSYM_ARG(Esys_Unseal), DLSYM_ARG(Esys_VerifySignature)); From 98ef5f8419ade95b7df0022ced6559cf47b80b83 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 16 Apr 2024 13:45:00 +0200 Subject: [PATCH 02/11] tpm2: export tpm2_get_name() We later want to use this from pcrlock.c, hence export it. --- src/shared/tpm2-util.c | 2 +- src/shared/tpm2-util.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 69f8ee7a7c..0c20024b98 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -3396,7 +3396,7 @@ int tpm2_calculate_pubkey_name(const TPMT_PUBLIC *public, TPM2B_NAME *ret_name) * * The handle must reference a key already present in the TPM. It may be either a public key only, or a * public/private keypair. */ -static int tpm2_get_name( +int tpm2_get_name( Tpm2Context *c, const Tpm2Handle *handle, TPM2B_NAME **ret_name) { diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index f9f29e310d..7249ad04ab 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -131,6 +131,7 @@ int tpm2_marshal_nv_public(const TPM2B_NV_PUBLIC *nv_public, void **ret, size_t int tpm2_unmarshal_nv_public(const void *data, size_t size, TPM2B_NV_PUBLIC *ret_nv_public); int tpm2_marshal_blob(const TPM2B_PUBLIC *public, const TPM2B_PRIVATE *private, const TPM2B_ENCRYPTED_SECRET *seed, void **ret_blob, size_t *ret_blob_size); int tpm2_unmarshal_blob(const void *blob, size_t blob_size, TPM2B_PUBLIC *ret_public, TPM2B_PRIVATE *ret_private, TPM2B_ENCRYPTED_SECRET *ret_seed); +int tpm2_get_name(Tpm2Context *c, const Tpm2Handle *handle, TPM2B_NAME **ret_name); bool tpm2_supports_alg(Tpm2Context *c, TPM2_ALG_ID alg); bool tpm2_supports_command(Tpm2Context *c, TPM2_CC command); From d0f8da9815744a0a7e1a79a5134b56334e12a197 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 16 Apr 2024 13:52:30 +0200 Subject: [PATCH 03/11] =?UTF-8?q?tpm2-util:=20rename=20tpm2=5Fget=5Fpin=5F?= =?UTF-8?q?auth()=20=E2=86=92=20tpm2=5Fauth=5Fvalue=5Ffrom=5Fpin()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Just some renaming. I found the old name a bit confusing since it sounds as if this would get the pin from somewhere, but it really doesn't. It just converts a PIN into an auth_value, and I think saying so explicitly makes things easier to grok. --- src/pcrlock/pcrlock.c | 2 +- src/shared/tpm2-util.c | 10 +++++----- src/shared/tpm2-util.h | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c index 1fb9d692a2..39b6810501 100644 --- a/src/pcrlock/pcrlock.c +++ b/src/pcrlock/pcrlock.c @@ -4500,7 +4500,7 @@ static int make_policy(bool force, bool recovery_pin) { CLEANUP_ERASE(auth); if (pin) { - r = tpm2_get_pin_auth(TPM2_ALG_SHA256, pin, &auth); + r = tpm2_auth_value_from_pin(TPM2_ALG_SHA256, pin, &auth); if (r < 0) return log_error_errno(r, "Failed to hash PIN: %m"); } else { diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 0c20024b98..e012dd2c5c 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -3062,7 +3062,7 @@ static void tpm2_trim_auth_value(TPM2B_AUTH *auth) { log_debug("authValue ends in 0, trimming as required by the TPM2 specification Part 1 section 'HMAC Computation' authValue Note 2."); } -int tpm2_get_pin_auth(TPMI_ALG_HASH hash, const char *pin, TPM2B_AUTH *ret_auth) { +int tpm2_auth_value_from_pin(TPMI_ALG_HASH hash, const char *pin, TPM2B_AUTH *ret_auth) { TPM2B_AUTH auth = {}; int r; @@ -3109,7 +3109,7 @@ int tpm2_set_auth(Tpm2Context *c, const Tpm2Handle *handle, const char *pin) { CLEANUP_ERASE(auth); - r = tpm2_get_pin_auth(TPM2_ALG_SHA256, pin, &auth); + r = tpm2_auth_value_from_pin(TPM2_ALG_SHA256, pin, &auth); if (r < 0) return r; @@ -4789,7 +4789,7 @@ static int tpm2_calculate_seal_private( TPM2B_AUTH auth = {}; if (pin) { - r = tpm2_get_pin_auth(parent->publicArea.nameAlg, pin, &auth); + r = tpm2_auth_value_from_pin(parent->publicArea.nameAlg, pin, &auth); if (r < 0) return r; } @@ -5254,7 +5254,7 @@ int tpm2_seal(Tpm2Context *c, CLEANUP_ERASE(hmac_sensitive); if (pin) { - r = tpm2_get_pin_auth(TPM2_ALG_SHA256, pin, &hmac_sensitive.userAuth); + r = tpm2_auth_value_from_pin(TPM2_ALG_SHA256, pin, &hmac_sensitive.userAuth); if (r < 0) return r; } @@ -5649,7 +5649,7 @@ int tpm2_define_policy_nv_index( CLEANUP_ERASE(_auth); if (!auth) { - r = tpm2_get_pin_auth(TPM2_ALG_SHA256, pin, &_auth); + r = tpm2_auth_value_from_pin(TPM2_ALG_SHA256, pin, &_auth); if (r < 0) return r; diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 7249ad04ab..343c2575aa 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -257,7 +257,7 @@ int tpm2_index_from_handle(Tpm2Context *c, const Tpm2Handle *handle, TPM2_HANDLE int tpm2_pcr_read(Tpm2Context *c, const TPML_PCR_SELECTION *pcr_selection, Tpm2PCRValue **ret_pcr_values, size_t *ret_n_pcr_values); int tpm2_pcr_read_missing_values(Tpm2Context *c, Tpm2PCRValue *pcr_values, size_t n_pcr_values); -int tpm2_get_pin_auth(TPMI_ALG_HASH hash, const char *pin, TPM2B_AUTH *ret_auth); +int tpm2_auth_value_from_pin(TPMI_ALG_HASH hash, const char *pin, TPM2B_AUTH *ret_auth); int tpm2_set_auth(Tpm2Context *c, const Tpm2Handle *handle, const char *pin); int tpm2_set_auth_binary(Tpm2Context *c, const Tpm2Handle *handle, const TPM2B_AUTH *auth); From 371b59441459e3bc33ceca4da619fec310dd7b37 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 16 Apr 2024 15:01:41 +0200 Subject: [PATCH 04/11] tpm2-util: load external key into NULL hierarchy if private key is provided If we load an external key into the TPM we must do so in the NULL hierarchy. An external key after all is one that is not wrapped by any hierarchy's seed. See TPM2 spec, Part 3, Section 12.3.1 --- src/shared/tpm2-util.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index e012dd2c5c..51c01b6c9c 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -2242,9 +2242,9 @@ static int tpm2_load_external( #if HAVE_TSS2_ESYS3 /* tpm2-tss >= 3.0.0 requires a ESYS_TR_RH_* constant specifying the requested * hierarchy, older versions need TPM2_RH_* instead. */ - ESYS_TR_RH_OWNER, + private ? ESYS_TR_RH_NULL : ESYS_TR_RH_OWNER, #else - TPM2_RH_OWNER, + private ? TPM2_RH_NULL : TPM2_RH_OWNER, #endif &handle->esys_handle); if (rc != TSS2_RC_SUCCESS) From 19d82e1beef5858a7d68692607317b630c187d84 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 17 Apr 2024 10:10:56 +0200 Subject: [PATCH 05/11] tpm2-util: add comment explaining what tpm2_define_policy_nv_index() actually does --- src/shared/tpm2-util.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 51c01b6c9c..051ff291dd 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -5639,6 +5639,10 @@ int tpm2_define_policy_nv_index( assert(c); assert(pin || auth); + /* Allocates an nvindex to store a policy for use in PolicyAuthorizeNV in. This is where pcrlock then + * stores its predicted PCR policies in. If 'requested_nv_index' will try to allocate the specified + * nvindex, otherwise will find a free one, and use that. */ + r = tpm2_handle_new(c, &new_handle); if (r < 0) return r; From cb835a2ed13604358d356514955b4a2b003dcee6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 16 Apr 2024 13:46:58 +0200 Subject: [PATCH 06/11] pcrlock: switch access policy for nvindex to store policy in from PolicyAuthValue to PolicySigned (with an HMAC-SHA256 key) So far the nvindex to store the pcrlock policy in was protected via a PolicyAuthValue policy (i.e. with a simple PIN set on the nvindex). That's a bad idea however, as it means an attacker can simply remove and re-create the nvindex and the "name" of the nvindex does not change, thus defeating the logic. (This is because the authValue is *not* part of the "name" of an nvindex!). Fix this by switching from PolicyAuthValue to PolicySigned with an HMAC-SHA256 key. Behaviour is very similar: however, the PIN is now part of of the access policy hash, which *is* part of the "name" of an nvindex. Thus, if an attacker removes and recreates the nvindex it has to provide the same PIN again or the "name" of the nvindex will change. Mission accomplished. I'd like to thank Chris Coulson for finding this issue (and helping me address it). Thank you! --- src/pcrlock/pcrlock.c | 29 ++++-- src/shared/tpm2-util.c | 214 +++++++++++++++++++++++++++++++++++++++++ src/shared/tpm2-util.h | 4 + 3 files changed, 238 insertions(+), 9 deletions(-) diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c index 39b6810501..f73724f1c6 100644 --- a/src/pcrlock/pcrlock.c +++ b/src/pcrlock/pcrlock.c @@ -4567,15 +4567,28 @@ static int make_policy(bool force, bool recovery_pin) { log_info("Retrieved PIN from TPM2 in %s.", FORMAT_TIMESPAN(usec_sub_unsigned(now(CLOCK_MONOTONIC), pin_start_usec), 1)); } - TPM2B_NV_PUBLIC nv_public = {}; + /* Now convert the PIN into an HMAC-SHA256 key that we can use in PolicySigned to protect access to the nvindex with */ + _cleanup_(tpm2_handle_freep) Tpm2Handle *pin_handle = NULL; + r = tpm2_hmac_key_from_pin(tc, encryption_session, &auth, &pin_handle); + if (r < 0) + return r; + TPM2B_NV_PUBLIC nv_public = {}; usec_t nv_index_start_usec = now(CLOCK_MONOTONIC); if (!iovec_is_set(&nv_blob)) { - TPM2B_DIGEST recovery_policy_digest = TPM2B_DIGEST_MAKE(NULL, TPM2_SHA256_DIGEST_SIZE); - r = tpm2_calculate_policy_auth_value(&recovery_policy_digest); + _cleanup_(Esys_Freep) TPM2B_NAME *pin_name = NULL; + r = tpm2_get_name( + tc, + pin_handle, + &pin_name); if (r < 0) - return log_error_errno(r, "Failed to calculate authentication value policy: %m"); + return log_error_errno(r, "Failed to get name of PIN from TPM2: %m"); + + TPM2B_DIGEST recovery_policy_digest = TPM2B_DIGEST_MAKE(NULL, TPM2_SHA256_DIGEST_SIZE); + r = tpm2_calculate_policy_signed(&recovery_policy_digest, pin_name); + if (r < 0) + return log_error_errno(r, "Failed to calculate PolicySigned policy: %m"); log_debug("Allocating NV index to write PCR policy to..."); r = tpm2_define_policy_nv_index( @@ -4594,10 +4607,6 @@ static int make_policy(bool force, bool recovery_pin) { return log_error_errno(r, "Failed to allocate NV index: %m"); } - r = tpm2_set_auth_binary(tc, nv_handle, &auth); - if (r < 0) - return log_error_errno(r, "Failed to set authentication value on NV index: %m"); - _cleanup_(tpm2_handle_freep) Tpm2Handle *policy_session = NULL; r = tpm2_make_policy_session( tc, @@ -4607,9 +4616,11 @@ static int make_policy(bool force, bool recovery_pin) { if (r < 0) return log_error_errno(r, "Failed to allocate policy session: %m"); - r = tpm2_policy_auth_value( + r = tpm2_policy_signed_hmac_sha256( tc, policy_session, + pin_handle, + &IOVEC_MAKE(auth.buffer, auth.size), /* ret_policy_digest= */ NULL); if (r < 0) return log_error_errno(r, "Failed to submit authentication value policy: %m"); diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 051ff291dd..dd2148fcda 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -29,6 +29,7 @@ #include "recurse-dir.h" #include "sha256.h" #include "sort-util.h" +#include "sparse-endian.h" #include "stat-util.h" #include "string-table.h" #include "sync-util.h" @@ -3534,6 +3535,150 @@ int tpm2_policy_auth_value( return tpm2_get_policy_digest(c, session, ret_policy_digest); } +/* Extend 'digest' with the PolicySigned calculated hash. */ +int tpm2_calculate_policy_signed(TPM2B_DIGEST *digest, const TPM2B_NAME *name) { + TPM2_CC command = TPM2_CC_PolicySigned; + TSS2_RC rc; + int r; + + assert(digest); + assert(digest->size == SHA256_DIGEST_SIZE); + assert(name); + + r = dlopen_tpm2(); + if (r < 0) + return log_debug_errno(r, "TPM2 support not installed: %m"); + + uint8_t buf[sizeof(command)]; + size_t offset = 0; + + rc = sym_Tss2_MU_TPM2_CC_Marshal(command, buf, sizeof(buf), &offset); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to marshal PolicySigned command: %s", sym_Tss2_RC_Decode(rc)); + + if (offset != sizeof(command)) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Offset 0x%zx wrong after marshalling PolicySigned command", offset); + + struct iovec data[] = { + IOVEC_MAKE(buf, offset), + IOVEC_MAKE(name->name, name->size), + }; + + r = tpm2_digest_many(TPM2_ALG_SHA256, digest, data, ELEMENTSOF(data), /* extend= */ true); + if (r < 0) + return r; + + const TPM2B_NONCE policyRef = {}; /* For now, we do not make use of the policyRef stuff */ + + r = tpm2_digest_buffer(TPM2_ALG_SHA256, digest, policyRef.buffer, policyRef.size, /* extend= */ true); + if (r < 0) + return r; + + tpm2_log_debug_digest(digest, "PolicySigned calculated digest"); + + return 0; +} + +int tpm2_policy_signed_hmac_sha256( + Tpm2Context *c, + const Tpm2Handle *session, + const Tpm2Handle *hmac_key_handle, + const struct iovec *hmac_key, + TPM2B_DIGEST **ret_policy_digest) { + +#if HAVE_OPENSSL + TSS2_RC rc; + int r; + + assert(c); + assert(session); + assert(hmac_key_handle); + assert(iovec_is_set(hmac_key)); + + /* This sends a TPM2_PolicySigned command to the tpm. As signature key we use an HMAC-SHA256 key + * specified in the hmac_key parameter. The secret key must be loaded into the TPM already and + * referenced in hmac_key_handle. */ + + log_debug("Submitting PolicySigned policy for HMAC-SHA256."); + + /* Acquire the nonce from the TPM that we shall sign */ + _cleanup_(Esys_Freep) TPM2B_NONCE *nonce = NULL; + rc = sym_Esys_TRSess_GetNonceTPM( + c->esys_context, + session->esys_handle, + &nonce); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to determine NoneTPM of auth session: %s", + sym_Tss2_RC_Decode(rc)); + + be32_t expiration = htobe64(0); + const TPM2B_DIGEST cpHashA = {}; /* For now, we do not make use of the cpHashA stuff */ + const TPM2B_NONCE policyRef = {}; /* ditto, we do not bother with policyRef */ + + /* Put together the data to sign, as per TPM2 Spec Part 3, 23.3.1 */ + struct iovec data_to_sign[] = { + IOVEC_MAKE(nonce->buffer, nonce->size), + IOVEC_MAKE(&expiration, sizeof(expiration)), + IOVEC_MAKE(cpHashA.buffer, cpHashA.size), + IOVEC_MAKE(policyRef.buffer, policyRef.size), + }; + + /* Now calculate the digest of the data we put together */ + TPM2B_DIGEST digest_to_sign; + r = tpm2_digest_many(TPM2_ALG_SHA256, &digest_to_sign, data_to_sign, ELEMENTSOF(data_to_sign), /* extend= */ false); + if (r < 0) + return r; + + unsigned char hmac_signature[SHA256_DIGEST_SIZE]; + unsigned hmac_signature_size = sizeof(hmac_signature); + + /* And sign this with our key */ + if (!HMAC(EVP_sha256(), + hmac_key->iov_base, + hmac_key->iov_len, + digest_to_sign.buffer, + digest_to_sign.size, + hmac_signature, + &hmac_signature_size)) + return -ENOTRECOVERABLE; + + /* Now bring the signature into a format that the TPM understands */ + TPMT_SIGNATURE sig = { + .sigAlg = TPM2_ALG_HMAC, + .signature.hmac.hashAlg = TPM2_ALG_SHA256, + }; + assert(hmac_signature_size == sizeof(sig.signature.hmac.digest.sha256)); + memcpy(sig.signature.hmac.digest.sha256, hmac_signature, hmac_signature_size); + + /* And submit the whole shebang to the TPM */ + rc = sym_Esys_PolicySigned( + c->esys_context, + hmac_key_handle->esys_handle, + session->esys_handle, + /* shandle1= */ ESYS_TR_NONE, + /* shandle2= */ ESYS_TR_NONE, + /* shandle3= */ ESYS_TR_NONE, + nonce, + &cpHashA, + &policyRef, + expiration, + &sig, + /* timeout= */ NULL, + /* policyTicket= */ NULL); + if (rc != TSS2_RC_SUCCESS) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to add PolicySigned policy to TPM: %s", + sym_Tss2_RC_Decode(rc)); + + return tpm2_get_policy_digest(c, session, ret_policy_digest); +#else /* HAVE_OPENSSL */ + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "OpenSSL support is disabled."); +#endif +} + int tpm2_calculate_policy_authorize_nv( const TPM2B_NV_PUBLIC *public_info, TPM2B_DIGEST *digest) { @@ -7039,6 +7184,75 @@ int tpm2_load_public_key_file(const char *path, TPM2B_PUBLIC *ret) { *ret = device_key_public; return 0; } + +int tpm2_hmac_key_from_pin(Tpm2Context *c, const Tpm2Handle *session, const TPM2B_AUTH *pin, Tpm2Handle **ret) { + int r; + + assert(c); + assert(pin); + assert(ret); + + log_debug("Converting PIN into TPM2 HMAC-SHA256 object."); + + /* Load the PIN (which we have stored in the "auth" TPM2B_AUTH) into the TPM as an HMAC key so that + * we can use it in a TPM2_PolicySigned() to write to the nvindex. For that we'll prep a pair of + * TPM2B_PUBLIC and TPM2B_SENSITIVE that defines an HMAC-SHA256 keyed hash function, and initialize + * it based on on the provided PIN data. */ + + TPM2B_PUBLIC auth_hmac_public = { + .publicArea = { + .type = TPM2_ALG_KEYEDHASH, + .nameAlg = TPM2_ALG_SHA256, + .objectAttributes = TPMA_OBJECT_SIGN_ENCRYPT, + .parameters.keyedHashDetail.scheme = { + .scheme = TPM2_ALG_HMAC, + .details.hmac.hashAlg = TPM2_ALG_SHA256, + }, + .unique.keyedHash.size = SHA256_DIGEST_SIZE, + }, + }; + + TPM2B_SENSITIVE auth_hmac_private = { + .sensitiveArea = { + .sensitiveType = TPM2_ALG_KEYEDHASH, + .sensitive.bits.size = pin->size, + .seedValue.size = SHA256_DIGEST_SIZE, + }, + }; + + /* Copy in the key data */ + memcpy_safe(auth_hmac_private.sensitiveArea.sensitive.bits.buffer, pin->buffer, pin->size); + + /* NB: We initialize the seed of the TPMT_SENSITIVE structure to all zeroes, since we want a stable + * "name" of the PIN object */ + + /* Now calculate the "unique" field for the public area, based on the sensitive data, according to + * the algorithm in the TPM2 spec, part 1, Section 27.5.3.2 */ + struct iovec sensitive_data[] = { + IOVEC_MAKE(auth_hmac_private.sensitiveArea.seedValue.buffer, auth_hmac_private.sensitiveArea.seedValue.size), + IOVEC_MAKE(auth_hmac_private.sensitiveArea.sensitive.bits.buffer, auth_hmac_private.sensitiveArea.sensitive.bits.size), + }; + r = tpm2_digest_many( + auth_hmac_public.publicArea.nameAlg, + &auth_hmac_public.publicArea.unique.keyedHash, + sensitive_data, + ELEMENTSOF(sensitive_data), + /* extend= */ false); + if (r < 0) + return r; + + /* And now load the public/private parts into the TPM and get a handle back */ + r = tpm2_load_external( + c, + session, + &auth_hmac_public, + &auth_hmac_private, + ret); + if (r < 0) + return log_error_errno(r, "Failed to load PIN into TPM2: %m"); + + return 0; +} #endif char *tpm2_pcr_mask_to_string(uint32_t mask) { diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 343c2575aa..adee36f8ed 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -268,6 +268,7 @@ int tpm2_policy_authorize_nv(Tpm2Context *c, const Tpm2Handle *session, const Tp int tpm2_policy_pcr(Tpm2Context *c, const Tpm2Handle *session, const TPML_PCR_SELECTION *pcr_selection, TPM2B_DIGEST **ret_policy_digest); int tpm2_policy_or(Tpm2Context *c, const Tpm2Handle *session, const TPM2B_DIGEST *branches, size_t n_branches, TPM2B_DIGEST **ret_policy_digest); int tpm2_policy_super_pcr(Tpm2Context *c, const Tpm2Handle *session, const Tpm2PCRPrediction *prediction, uint16_t algorithm); +int tpm2_policy_signed_hmac_sha256(Tpm2Context *c, const Tpm2Handle *session, const Tpm2Handle *hmac_key_handle, const struct iovec *hmac_key, TPM2B_DIGEST **ret_policy_digest); int tpm2_calculate_pubkey_name(const TPMT_PUBLIC *public, TPM2B_NAME *ret_name); int tpm2_calculate_nv_index_name(const TPMS_NV_PUBLIC *nvpublic, TPM2B_NAME *ret_name); @@ -278,6 +279,7 @@ int tpm2_calculate_policy_authorize_nv(const TPM2B_NV_PUBLIC *public, TPM2B_DIGE int tpm2_calculate_policy_pcr(const Tpm2PCRValue *pcr_values, size_t n_pcr_values, TPM2B_DIGEST *digest); int tpm2_calculate_policy_or(const TPM2B_DIGEST *branches, size_t n_branches, TPM2B_DIGEST *digest); int tpm2_calculate_policy_super_pcr(Tpm2PCRPrediction *prediction, uint16_t algorithm, TPM2B_DIGEST *pcr_policy); +int tpm2_calculate_policy_signed(TPM2B_DIGEST *digest, const TPM2B_NAME *name); int tpm2_calculate_serialize(TPM2_HANDLE handle, const TPM2B_NAME *name, const TPM2B_PUBLIC *public, void **ret_serialized, size_t *ret_serialized_size); int tpm2_calculate_sealing_policy(const Tpm2PCRValue *pcr_values, size_t n_pcr_values, const TPM2B_PUBLIC *public, bool use_pin, const Tpm2PCRLockPolicy *policy, TPM2B_DIGEST *digest); int tpm2_calculate_seal(TPM2_HANDLE parent_handle, const TPM2B_PUBLIC *parent_public, const TPMA_OBJECT *attributes, const struct iovec *secret, const TPM2B_DIGEST *policy, const char *pin, struct iovec *ret_secret, struct iovec *ret_blob, struct iovec *ret_serialized_parent); @@ -311,6 +313,8 @@ int tpm2_deserialize(Tpm2Context *c, const void *serialized, size_t serialized_s int tpm2_load_public_key_file(const char *path, TPM2B_PUBLIC *ret); +int tpm2_hmac_key_from_pin(Tpm2Context *c, const Tpm2Handle *session, const TPM2B_AUTH *pin, Tpm2Handle **ret); + /* The tpm2-tss library has many structs that are simply a combination of an array (or object) and * size. These macros allow easily initializing or assigning instances of such structs from an existing * buffer/object and size, while also checking the size for safety with the struct buffer/object size. If the From d10d4a37018375b5ff92d9cafead72026bf3418a Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 17 Apr 2024 10:17:20 +0200 Subject: [PATCH 07/11] tpm2-util: now that we don't use PolicyAuthValue anymore, let's not set an authValue anymore for the policy nvindex We have now switched from PolicyAuthValue to PolicySigned to control access to the policy nvindex to. This means there's no point in setting an authValue on the nvindex anymore, hence drop this. --- src/pcrlock/pcrlock.c | 2 -- src/shared/tpm2-util.c | 16 +--------------- src/shared/tpm2-util.h | 2 +- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c index f73724f1c6..fa382a22d8 100644 --- a/src/pcrlock/pcrlock.c +++ b/src/pcrlock/pcrlock.c @@ -4596,8 +4596,6 @@ static int make_policy(bool force, bool recovery_pin) { encryption_session, arg_nv_index, &recovery_policy_digest, - pin, - &auth, &nv_index, &nv_handle, &nv_public); diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index dd2148fcda..35b4957eef 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -5771,8 +5771,6 @@ int tpm2_define_policy_nv_index( const Tpm2Handle *session, TPM2_HANDLE requested_nv_index, const TPM2B_DIGEST *write_policy, - const char *pin, - const TPM2B_AUTH *auth, TPM2_HANDLE *ret_nv_index, Tpm2Handle **ret_nv_handle, TPM2B_NV_PUBLIC *ret_nv_public) { @@ -5782,7 +5780,6 @@ int tpm2_define_policy_nv_index( int r; assert(c); - assert(pin || auth); /* Allocates an nvindex to store a policy for use in PolicyAuthorizeNV in. This is where pcrlock then * stores its predicted PCR policies in. If 'requested_nv_index' will try to allocate the specified @@ -5794,17 +5791,6 @@ int tpm2_define_policy_nv_index( new_handle->flush = false; /* This is a persistent NV index, don't flush hence */ - TPM2B_AUTH _auth = {}; - CLEANUP_ERASE(_auth); - - if (!auth) { - r = tpm2_auth_value_from_pin(TPM2_ALG_SHA256, pin, &_auth); - if (r < 0) - return r; - - auth = &_auth; - } - for (unsigned try = 0; try < 25U; try++) { TPM2_HANDLE nv_index; @@ -5832,7 +5818,7 @@ int tpm2_define_policy_nv_index( /* shandle1= */ session ? session->esys_handle : ESYS_TR_PASSWORD, /* shandle2= */ ESYS_TR_NONE, /* shandle3= */ ESYS_TR_NONE, - auth, + /* auth= */ NULL, &public_info, &new_handle->esys_handle); diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index adee36f8ed..f09b71f84c 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -301,7 +301,7 @@ int tpm2_tpm2b_public_from_openssl_pkey(const EVP_PKEY *pkey, TPM2B_PUBLIC *ret) int tpm2_tpm2b_public_from_pem(const void *pem, size_t pem_size, TPM2B_PUBLIC *ret); int tpm2_tpm2b_public_to_fingerprint(const TPM2B_PUBLIC *public, void **ret_fingerprint, size_t *ret_fingerprint_size); -int tpm2_define_policy_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE requested_nv_index, const TPM2B_DIGEST *write_policy, const char *pin, const TPM2B_AUTH *auth, TPM2_HANDLE *ret_nv_index, Tpm2Handle **ret_nv_handle, TPM2B_NV_PUBLIC *ret_nv_public); +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); From 0ec4c098ddc03ce548af9a34e91063b84f292290 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 17 Apr 2024 19:02:18 +0200 Subject: [PATCH 08/11] pcrlock: generate recovery PINs via make_recovery_key() We already have infrastructure for generating nice recovery keys, for the usual cryptenroll recovery keys. Let's reuse them here, as they are nicer to read and type than the base64 encoded randomness we so far used. Previously valid recovery keys remain valid, in their original format. For future enrollments we'll however have nicer, easier recovery keys to deal with. --- src/pcrlock/pcrlock.c | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c index fa382a22d8..6651b1e3e1 100644 --- a/src/pcrlock/pcrlock.c +++ b/src/pcrlock/pcrlock.c @@ -4473,16 +4473,9 @@ static int make_policy(bool force, bool recovery_pin) { } } else if (!have_old_policy) { - char rnd[256]; - - r = crypto_random_bytes(rnd, sizeof(rnd)); + r = make_recovery_key(&pin); if (r < 0) return log_error_errno(r, "Failed to generate a randomized recovery PIN: %m"); - - (void) base64mem(rnd, sizeof(rnd), &pin); - explicit_bzero_safe(rnd, sizeof(rnd)); - if (!pin) - return log_oom(); } _cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL; From 43a59b8b86b0c5106d0ec90568b1b55dc6bc19a3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 17 Apr 2024 19:04:29 +0200 Subject: [PATCH 09/11] pcrlock: rework --recovery-pin= to take three different arguments This reworkds --recovery-pin= from a parameter that takes a boolean to an enum supporting one of "hide", "show", "query". If "hide" (default behaviour) we'll generate a recovery pin automatically, but never show it, and thus just seal it and good. If "show" we'll generate a recovery pin automatically, but display it in the output, so the user can write it down. If "query" we'll ask the user for a recovery pin, and not automatically generate any. For compatibility the old boolean behaviour is kept. With this you can now do "systemd-pcrlock make-policy --recovery-pin=show" to set up the first policy, write down the recovery PIN. Later, if the PCR prediction didn't work out one day you can then do "systemd-pcrlock make-policy --recovery-pin=query" and enter the recovery key and write a new policy. --- man/systemd-pcrlock.xml | 17 ++++++++++------- src/pcrlock/pcrlock.c | 38 +++++++++++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/man/systemd-pcrlock.xml b/man/systemd-pcrlock.xml index 2c674a34b4..e2e861b246 100644 --- a/man/systemd-pcrlock.xml +++ b/man/systemd-pcrlock.xml @@ -504,13 +504,16 @@ - Takes a boolean. Defaults to false. Honoured by make-policy. If - true, will query the user for a PIN to unlock the TPM2 NV index with. If no policy was created before - this PIN is used to protect the newly allocated NV index. If a policy has been created before the PIN - is used to unlock write access to the NV index. If this option is not used a PIN is automatically - generated. Regardless if user supplied or automatically generated, it is stored in encrypted form in - the policy metadata file. The recovery PIN may be used to regain write access to an NV index in case - the access policy became out of date. + Takes one of hide, show or + query. Defaults to hide. Honoured by + make-policy. If query, will query the user for a PIN to unlock + the TPM2 NV index with. If no policy was created before, this PIN is used to protect the newly + allocated NV index. If a policy has been created before, the PIN is used to unlock write access to + the NV index. If either hide or show is used, a PIN is + automatically generated, and — only in case of show — displayed on + screen. Regardless if user supplied or automatically generated, it is stored in encrypted form in the + policy metadata file. The recovery PIN may be used to regain write access to an NV index in case the + access policy became out of date. diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c index 6651b1e3e1..33cdafc06e 100644 --- a/src/pcrlock/pcrlock.c +++ b/src/pcrlock/pcrlock.c @@ -43,6 +43,7 @@ #include "random-util.h" #include "recovery-key.h" #include "sort-util.h" +#include "string-table.h" #include "terminal-util.h" #include "tpm2-util.h" #include "unaligned.h" @@ -52,6 +53,14 @@ #include "varlink-io.systemd.PCRLock.h" #include "verbs.h" +typedef enum RecoveryPinMode { + RECOVERY_PIN_HIDE, /* generate a recovery PIN automatically, but don't show it (alias: "no") */ + RECOVERY_PIN_SHOW, /* generate a recovery PIN automatically, and display it to the user */ + RECOVERY_PIN_QUERY, /* asks the user for a PIN to use interactively (alias: "yes") */ + _RECOVERY_PIN_MODE_MAX, + _RECOVERY_PIN_MODE_INVALID = -EINVAL, +} RecoveryPinMode; + static PagerFlags arg_pager_flags = 0; static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF|JSON_FORMAT_NEWLINE; static char **arg_components = NULL; @@ -62,7 +71,7 @@ static bool arg_raw_description = false; static char *arg_location_start = NULL; static char *arg_location_end = NULL; static TPM2_HANDLE arg_nv_index = 0; -static bool arg_recovery_pin = false; +static RecoveryPinMode arg_recovery_pin = RECOVERY_PIN_HIDE; static char *arg_policy_path = NULL; static bool arg_force = false; static BootEntryTokenType arg_entry_token_type = BOOT_ENTRY_TOKEN_AUTO; @@ -104,6 +113,14 @@ STATIC_DESTRUCTOR_REGISTER(arg_entry_token, freep); (UINT32_C(1) << TPM2_PCR_SHIM_POLICY) | \ (UINT32_C(1) << TPM2_PCR_SYSTEM_IDENTITY)) +static const char* recovery_pin_mode_table[_RECOVERY_PIN_MODE_MAX] = { + [RECOVERY_PIN_HIDE] = "hide", + [RECOVERY_PIN_SHOW] = "show", + [RECOVERY_PIN_QUERY] = "query", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING_WITH_BOOLEAN(recovery_pin_mode, RecoveryPinMode, RECOVERY_PIN_QUERY); + typedef struct EventLogRecordBank EventLogRecordBank; typedef struct EventLogRecord EventLogRecord; typedef struct EventLogRegisterBank EventLogRegisterBank; @@ -4320,7 +4337,7 @@ static int write_boot_policy_file(const char *json_text) { return 1; } -static int make_policy(bool force, bool recovery_pin) { +static int make_policy(bool force, RecoveryPinMode recovery_pin_mode) { int r; /* Here's how this all works: after predicting all possible PCR values for next boot (with @@ -4444,7 +4461,7 @@ static int make_policy(bool force, bool recovery_pin) { /* Acquire a recovery PIN, either from the user, or create a randomized one */ _cleanup_(erase_and_freep) char *pin = NULL; - if (recovery_pin) { + if (recovery_pin_mode == RECOVERY_PIN_QUERY) { r = getenv_steal_erase("PIN", &pin); if (r < 0) return log_error_errno(r, "Failed to acquire PIN from environment: %m"); @@ -4476,6 +4493,13 @@ static int make_policy(bool force, bool recovery_pin) { r = make_recovery_key(&pin); if (r < 0) return log_error_errno(r, "Failed to generate a randomized recovery PIN: %m"); + + if (recovery_pin_mode == RECOVERY_PIN_SHOW) + printf("%s Selected recovery PIN is: %s%s%s\n", + special_glyph(SPECIAL_GLYPH_LOCK_AND_KEY), + ansi_highlight_cyan(), + pin, + ansi_normal()); } _cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL; @@ -5046,9 +5070,9 @@ static int parse_argv(int argc, char *argv[]) { } case ARG_RECOVERY_PIN: - r = parse_boolean_argument("--recovery-pin", optarg, &arg_recovery_pin); - if (r < 0) - return r; + arg_recovery_pin = recovery_pin_mode_from_string(optarg); + if (arg_recovery_pin < 0) + return log_error_errno(arg_recovery_pin, "Failed to parse --recovery-pin= mode: %s", optarg); break; case ARG_PCRLOCK: @@ -5212,7 +5236,7 @@ static int vl_method_make_policy(Varlink *link, JsonVariant *parameters, Varlink if (r != 0) return r; - r = make_policy(p.force, /* recovery_key= */ false); + r = make_policy(p.force, /* recovery_key= */ RECOVERY_PIN_HIDE); if (r < 0) return r; if (r == 0) From bb4525c8d89b27ab95e7a8aac1590a202f02ba06 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 17 Apr 2024 10:48:42 +0200 Subject: [PATCH 10/11] update NEWS --- NEWS | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/NEWS b/NEWS index f91e8e8914..a454c0d1d4 100644 --- a/NEWS +++ b/NEWS @@ -461,20 +461,8 @@ CHANGES WITH 256-rc1: * confexts are loaded by systemd-stub from the ESP as well. - * The pcrlock policy is saved in an unencrypted credential file - "pcrlock..cred" under XBOOTLDR/ESP in the - /loader/credentials/ directory. It will be picked up at boot by - systemd-stub and passed to the initrd, where it can be used to unlock - the root file system. - * kernel-install gained support for --root= for the 'list' verb. - * systemd-pcrlock gained an --entry-token= option to configure the - entry-token. - - * systemd-pcrlock now provides a basic Varlink interface and can be run - as a daemon via a template unit. - * bootctl now provides a basic Varlink interface and can be run as a daemon via a template unit. @@ -498,6 +486,30 @@ CHANGES WITH 256-rc1: for enrolling "dbx" too (Previously, only db/KEK/PK enrollment was supported). It also now supports UEFI "Custom" mode. + * The pcrlock policy is saved in an unencrypted credential file + "pcrlock..cred" under XBOOTLDR/ESP in the + /loader/credentials/ directory. It will be picked up at boot by + systemd-stub and passed to the initrd, where it can be used to unlock + the root file system. + + * systemd-pcrlock gained an --entry-token= option to configure the + entry-token. + + * systemd-pcrlock now provides a basic Varlink interface and can be run + as a daemon via a template unit. + + * systemd-pcrlock's TPM nvindex access policy has been modified, this + means that previous pcrlock policies stored in nvindexes are + invalidated. They must be removed (systemd-pcrlock remove-policy) and + recreated (systemd-pcrlock make-policy). For the time being + systemd-pcrlock remains an experimental feature, but it is expected + to become stable in the next release, i.e. v257. + + * systemd-pcrlock's --recovery-pin= switch now takes three values: + "hide", "show", "query". If "show" is selected the automatically + generated recovery PIN is shown to the user. If "query" is selected + then the PIN is queried from the user. + systemd-run/run0: * systemd-run is now a multi-call binary. When invoked as 'run0', it From 36769db1b0f0ec1d897d51e55ea7179d65c88492 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 18 Apr 2024 18:12:12 +0200 Subject: [PATCH 11/11] ci: update tests to showcase new option a bit --- test/units/testsuite-70.pcrlock.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/units/testsuite-70.pcrlock.sh b/test/units/testsuite-70.pcrlock.sh index ecdd910c28..fbb93738b2 100755 --- a/test/units/testsuite-70.pcrlock.sh +++ b/test/units/testsuite-70.pcrlock.sh @@ -74,7 +74,7 @@ if [[ -n "$SD_STUB" ]]; then "$SD_PCRLOCK" lock-uki <"$SD_STUB" fi -PIN=huhu "$SD_PCRLOCK" make-policy --pcr="$PCRS" --recovery-pin=yes +PIN=huhu "$SD_PCRLOCK" make-policy --pcr="$PCRS" --recovery-pin=query # Repeat immediately (this call will have to reuse the nvindex, rather than create it) "$SD_PCRLOCK" make-policy --pcr="$PCRS" "$SD_PCRLOCK" make-policy --pcr="$PCRS" --force @@ -102,7 +102,7 @@ systemd-cryptsetup detach pcrlock # work. echo -n test70 | "$SD_PCRLOCK" lock-raw --pcrlock=/var/lib/pcrlock.d/910-test70.pcrlock --pcr=16 (! "$SD_PCRLOCK" make-policy --pcr="$PCRS") -PIN=huhu "$SD_PCRLOCK" make-policy --pcr="$PCRS" --recovery-pin=yes +PIN=huhu "$SD_PCRLOCK" make-policy --pcr="$PCRS" --recovery-pin=query systemd-cryptsetup attach pcrlock "$img" - tpm2-device=auto,tpm2-pcrlock=/var/lib/systemd/pcrlock.json,headless systemd-cryptsetup detach pcrlock @@ -110,6 +110,10 @@ systemd-cryptsetup detach pcrlock # And now let's do it the clean way, and generate the right policy ahead of time. echo -n test70-take-two | "$SD_PCRLOCK" lock-raw --pcrlock=/var/lib/pcrlock.d/920-test70.pcrlock --pcr=16 "$SD_PCRLOCK" make-policy --pcr="$PCRS" +# the next one should be skipped because redundant +"$SD_PCRLOCK" make-policy --pcr="$PCRS" +# but this one should not be skipped, even if redundant, because we force it +"$SD_PCRLOCK" make-policy --pcr="$PCRS" --force --recovery-pin=show "$SD_PCREXTEND" --pcr=16 test70-take-two