mirror of
https://github.com/systemd/systemd.git
synced 2026-06-30 19:57:29 +00:00
shared/tpm2: support chunked reads of NV indexes
The TPM2_NV_Read commands returns the requested data in a TPM2B_MAX_NV_BUFFER type, the maximum size of which is TPM-specific and can be determined by querying the value of the TPM_PT_NV_BUFFER_MAX property. The value of this may be smaller than the payload size of some NV indexes, particularly when that payload is a X509 certificate with a RSA public key. Eg, the manufacturer supplied RSA EK certificate on my own machine has a size of 1035 bytes, and the value of TPM_PT_NV_BUFFER_MAX is 1024. To handle this case and make it possible to read any EK certificate from the TPM, make tpm2_read_nv_index support chunked reads when the payload size is larger than what the TPM can return in a single command.
This commit is contained in:
committed by
Lennart Poettering
parent
7a4436ea32
commit
93e9c2c974
@@ -389,6 +389,25 @@ static int tpm2_get_capability(
|
||||
return more == TPM2_YES;
|
||||
}
|
||||
|
||||
static int tpm2_get_capability_property(Tpm2Context *c, uint32_t property, uint32_t *ret_value) {
|
||||
int r;
|
||||
|
||||
assert(c);
|
||||
assert(ret_value);
|
||||
|
||||
TPMU_CAPABILITIES capabilities = {};
|
||||
r = tpm2_get_capability(c, TPM2_CAP_TPM_PROPERTIES, property, 1, &capabilities);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (capabilities.tpmProperties.count == 0 ||
|
||||
capabilities.tpmProperties.tpmProperty[0].property != property)
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(ENOENT), "TPM property 0x%04" PRIx32 " does not exist", property);
|
||||
|
||||
*ret_value = capabilities.tpmProperties.tpmProperty[0].value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tpm2_vendor_info_to_modalias(const Tpm2VendorInfo *info, char **ret) {
|
||||
_cleanup_free_ char *m = NULL;
|
||||
|
||||
@@ -668,6 +687,31 @@ static int tpm2_cache_capabilities(Tpm2Context *c) {
|
||||
log_debug("TPM bug: reported multiple PCR sets; using only first set.");
|
||||
c->capability_pcrs = capability.assignedPCR;
|
||||
|
||||
/* Cache the value of TPM_PT_NV_BUFFER_MAX, which defines the maximum size of TPM2B_MAX_NV_BUFFER and
|
||||
* which limits the amount of data that TPM2_NV_Read can return in a single command. This may be
|
||||
* smaller than the size of a NV index payload, particularly if that payload contains a X509
|
||||
* certificate with a RSA public key. */
|
||||
uint32_t max_nv_buffer_size = 0;
|
||||
|
||||
/* The TCG reference library spec (part 2) doesn't guarantee a minimum size for the
|
||||
* TPM2B_MAX_NV_BUFFER type. However, the PC-Client PTP spec does set a minimum value of 512,
|
||||
* so we'll just assume this if the TPM didn't report a value or reports an implausible value. */
|
||||
static const uint32_t fallback_max_nv_buffer_size = 512;
|
||||
|
||||
r = tpm2_get_capability_property(c, TPM2_PT_NV_BUFFER_MAX, &max_nv_buffer_size);
|
||||
if (r == -ENOENT) {
|
||||
log_debug("TPM bug: didn't report a value for TPM_PT_NV_BUFFER_MAX; using %" PRIu32 ".", fallback_max_nv_buffer_size);
|
||||
max_nv_buffer_size = fallback_max_nv_buffer_size;
|
||||
} else if (r < 0)
|
||||
return r;
|
||||
if (max_nv_buffer_size == 0 || max_nv_buffer_size > UINT16_MAX) {
|
||||
/* TPM2B types have a uint16 size field. If the TPM reported a maximum size that is larger
|
||||
* than this, or 0, then consider this as implausible and pick the default fallback. */
|
||||
log_debug("TPM bug: reported implausible value for TPM_PT_NV_BUFFER_MAX; using %" PRIu32 ".", fallback_max_nv_buffer_size);
|
||||
max_nv_buffer_size = fallback_max_nv_buffer_size;
|
||||
}
|
||||
c->max_nv_buffer_size = (uint16_t) max_nv_buffer_size;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -6325,6 +6369,116 @@ int tpm2_write_policy_nv_index(
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tpm2_define_data_nv_index(
|
||||
Tpm2Context *c,
|
||||
const Tpm2Handle *session,
|
||||
TPM2_HANDLE requested_nv_index,
|
||||
const struct iovec *data,
|
||||
TPM2_HANDLE *ret_nv_index,
|
||||
Tpm2Handle **ret_nv_handle) {
|
||||
|
||||
_cleanup_(tpm2_handle_freep) Tpm2Handle *new_handle = NULL;
|
||||
TPM2_HANDLE nv_index = 0;
|
||||
TSS2_RC rc;
|
||||
int r;
|
||||
|
||||
assert(c);
|
||||
assert(iovec_is_set(data));
|
||||
|
||||
/* Allocates an ordinary NV index sized to hold 'data' and writes 'data' into it. The index is created
|
||||
* with AUTHREAD/AUTHWRITE attributes so it can be read back with tpm2_read_nv_index(). */
|
||||
|
||||
if (data->iov_len == 0 || data->iov_len > UINT16_MAX)
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
|
||||
"Invalid NV index data size %zu.", data->iov_len);
|
||||
|
||||
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 */
|
||||
|
||||
for (unsigned try = 0;; try++) {
|
||||
if (requested_nv_index != 0)
|
||||
nv_index = requested_nv_index;
|
||||
else
|
||||
nv_index = generate_random_nv_index();
|
||||
|
||||
TPM2B_NV_PUBLIC public_info = {
|
||||
.size = sizeof_field(TPM2B_NV_PUBLIC, nvPublic),
|
||||
.nvPublic = {
|
||||
.nvIndex = nv_index,
|
||||
.nameAlg = TPM2_ALG_SHA256,
|
||||
.attributes = TPM2_NT_ORDINARY | TPMA_NV_AUTHWRITE | TPMA_NV_AUTHREAD | TPMA_NV_NO_DA,
|
||||
.dataSize = data->iov_len,
|
||||
},
|
||||
};
|
||||
|
||||
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 == TSS2_RC_SUCCESS)
|
||||
break;
|
||||
if (rc != TPM2_RC_NV_DEFINED)
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
|
||||
"Failed to allocate NV index: %s", sym_Tss2_RC_Decode(rc));
|
||||
if (requested_nv_index != 0) {
|
||||
assert(nv_index == requested_nv_index);
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(EEXIST),
|
||||
"Requested NV index 0x%" PRIx32 " already taken.", requested_nv_index);
|
||||
}
|
||||
if (try >= 24U)
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
|
||||
"Too many attempts trying to allocate NV index: %s", sym_Tss2_RC_Decode(rc));
|
||||
|
||||
log_debug("NV index 0x%" PRIx32 " already taken, trying another one (%u tries left)", nv_index, 24U - try);
|
||||
}
|
||||
|
||||
log_debug("NV index 0x%" PRIx32 " successfully allocated.", nv_index);
|
||||
|
||||
/* TPM2_NV_Write is bounded by TPM_PT_NV_BUFFER_MAX just like TPM2_NV_Read, so write in chunks no
|
||||
* larger than what the TPM accepts in a single command. */
|
||||
size_t max_buffer_size = MIN((size_t) c->max_nv_buffer_size, (size_t) TPM2_MAX_NV_BUFFER_SIZE);
|
||||
assert(max_buffer_size > 0);
|
||||
|
||||
for (size_t offset = 0; offset < data->iov_len;) {
|
||||
size_t chunk_size = MIN(data->iov_len - offset, max_buffer_size);
|
||||
|
||||
TPM2B_MAX_NV_BUFFER buffer = { .size = chunk_size };
|
||||
memcpy(buffer.buffer, (const uint8_t*) data->iov_base + offset, chunk_size);
|
||||
|
||||
rc = sym_Esys_NV_Write(
|
||||
c->esys_context,
|
||||
/* authHandle= */ new_handle->esys_handle,
|
||||
/* nvIndex= */ new_handle->esys_handle,
|
||||
/* shandle1= */ session ? session->esys_handle : ESYS_TR_PASSWORD,
|
||||
/* shandle2= */ ESYS_TR_NONE,
|
||||
/* shandle3= */ ESYS_TR_NONE,
|
||||
&buffer,
|
||||
/* offset= */ offset);
|
||||
if (rc != TSS2_RC_SUCCESS) {
|
||||
(void) tpm2_undefine_nv_index(c, session, nv_index, new_handle);
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
|
||||
"Failed to write NV index 0x%" PRIx32 ": %s", nv_index, sym_Tss2_RC_Decode(rc));
|
||||
}
|
||||
|
||||
offset += chunk_size;
|
||||
}
|
||||
|
||||
if (ret_nv_index)
|
||||
*ret_nv_index = nv_index;
|
||||
if (ret_nv_handle)
|
||||
*ret_nv_handle = TAKE_PTR(new_handle);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int tpm2_undefine_nv_index(
|
||||
Tpm2Context *c,
|
||||
const Tpm2Handle *session,
|
||||
@@ -6519,37 +6673,57 @@ int tpm2_read_nv_index(
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
log_debug("Read public info for nvindex 0x%x, value size is %zu", nv_index, (size_t) nv_public->nvPublic.dataSize);
|
||||
size_t data_size = nv_public->nvPublic.dataSize;
|
||||
log_debug("Read public info for nvindex 0x%x, value size is %zu", nv_index, data_size);
|
||||
|
||||
_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));
|
||||
/* TPM2_NV_Read returns the data in a TPM2B_MAX_NV_BUFFER, whose maximum size is bounded by the value
|
||||
* of the TPM_PT_NV_BUFFER_MAX property. This limits the amount of data that can be read in a single
|
||||
* command. As this limit can be smaller than the size of the NV index payload (particularly if the
|
||||
* payload contains a X509 certificate with a RSA public key), we read the contents in chunks no
|
||||
* larger than what the TPM reports it can return in a single command. */
|
||||
|
||||
if (ret_value) {
|
||||
assert(value);
|
||||
|
||||
struct iovec result = {
|
||||
.iov_base = memdup(value->buffer, value->size),
|
||||
.iov_len = value->size,
|
||||
};
|
||||
/* Never ask for more than the buffer the TPM library hands back can hold. */
|
||||
size_t max_buffer_size = MIN((size_t) c->max_nv_buffer_size, (size_t) TPM2_MAX_NV_BUFFER_SIZE);
|
||||
assert(max_buffer_size > 0); /* tpm2_cache_capabilities sets this to 512 if the TPM reported 0. */
|
||||
|
||||
_cleanup_(iovec_done) struct iovec result = {};
|
||||
if (data_size > 0) {
|
||||
result.iov_base = malloc(data_size);
|
||||
if (!result.iov_base)
|
||||
return log_oom_debug();
|
||||
|
||||
*ret_value = TAKE_STRUCT(result);
|
||||
}
|
||||
|
||||
while (result.iov_len < data_size) {
|
||||
size_t chunk_size = MIN(data_size - result.iov_len, max_buffer_size);
|
||||
|
||||
_cleanup_(Esys_Freep) TPM2B_MAX_NV_BUFFER *chunk = 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,
|
||||
chunk_size,
|
||||
/* offset= */ result.iov_len,
|
||||
&chunk);
|
||||
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));
|
||||
assert(chunk);
|
||||
if (chunk->size != chunk_size)
|
||||
/* On success, TPM2_NV_Read should return exactly what we asked for. */
|
||||
return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE),
|
||||
"TPM returned an unexpected amount of data (%" PRIu16 ") reading nvindex 0x%x.",
|
||||
chunk->size, nv_index);
|
||||
|
||||
memcpy((uint8_t*) result.iov_base + result.iov_len, chunk->buffer, chunk->size);
|
||||
result.iov_len += chunk->size;
|
||||
}
|
||||
|
||||
if (ret_value)
|
||||
*ret_value = TAKE_STRUCT(result);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ typedef struct Tpm2Context {
|
||||
TPM2_ECC_CURVE *capability_ecc_curves;
|
||||
size_t n_capability_ecc_curves;
|
||||
TPML_PCR_SELECTION capability_pcrs;
|
||||
uint16_t max_nv_buffer_size;
|
||||
} Tpm2Context;
|
||||
|
||||
int tpm2_context_new(const char *device, Tpm2Context **ret_context);
|
||||
@@ -361,6 +362,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_define_data_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE requested_nv_index, const struct iovec *data, TPM2_HANDLE *ret_nv_index, Tpm2Handle **ret_nv_handle);
|
||||
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);
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "crypto-util.h"
|
||||
#include "hexdecoct.h"
|
||||
#include "iovec-util.h"
|
||||
#include "random-util.h"
|
||||
#include "tests.h"
|
||||
#include "tpm2-util.h"
|
||||
#include "virt.h"
|
||||
@@ -1373,6 +1375,81 @@ static void check_seal_unseal(Tpm2Context *c) {
|
||||
}
|
||||
}
|
||||
|
||||
static void check_nv_index_read(Tpm2Context *c) {
|
||||
int r;
|
||||
uint8_t payload[1031];
|
||||
|
||||
assert(c);
|
||||
|
||||
TEST_LOG_FUNC();
|
||||
|
||||
random_bytes(payload, sizeof(payload));
|
||||
struct iovec data = IOVEC_MAKE(payload, sizeof(payload));
|
||||
|
||||
/* Test chunked reads first by mocking c->max_nv_buffer_size with several values that are less than
|
||||
* the payload size and the TPM's reported size for TPM2_PT_NV_BUFFER_MAX. */
|
||||
TPM2_HANDLE nv_index = 0;
|
||||
_cleanup_(tpm2_handle_freep) Tpm2Handle *nv_handle = NULL;
|
||||
r = tpm2_define_data_nv_index(c, /* session= */ NULL, /* requested_nv_index= */ 0, &data, &nv_index, &nv_handle);
|
||||
if (r < 0) {
|
||||
/* Could fail because the index size is greater than the value of TPM2_PT_NV_INDEX_MAX, or
|
||||
* there isn't enough space available. */
|
||||
log_notice_errno(r, "Could not allocate NV index, skipping NV index read test: %m");
|
||||
return;
|
||||
}
|
||||
ASSERT_NE(nv_index, 0U);
|
||||
ASSERT_NOT_NULL(nv_handle);
|
||||
|
||||
uint16_t saved_max_nv_buffer_size = c->max_nv_buffer_size;
|
||||
|
||||
static const uint16_t chunk_sizes[] = { 128, 256, 512, 1024 };
|
||||
FOREACH_ELEMENT(cs, chunk_sizes) {
|
||||
if (*cs >= saved_max_nv_buffer_size)
|
||||
continue;
|
||||
|
||||
c->max_nv_buffer_size = *cs;
|
||||
|
||||
_cleanup_(iovec_done) struct iovec value = {};
|
||||
ASSERT_OK_ZERO(tpm2_read_nv_index(c, /* session= */ NULL, nv_index, nv_handle, &value));
|
||||
ASSERT_TRUE(iovec_equal(&value, &data));
|
||||
}
|
||||
|
||||
c->max_nv_buffer_size = saved_max_nv_buffer_size;
|
||||
|
||||
ASSERT_OK_ZERO(tpm2_undefine_nv_index(c, /* session= */ NULL, nv_index, nv_handle));
|
||||
nv_index = 0;
|
||||
nv_handle = tpm2_handle_free(nv_handle);
|
||||
|
||||
/* Test reading of a payload with the size of the reported TPM2_PT_NV_BUFFER_MAX. */
|
||||
_cleanup_free_ void *payload2 = malloc(c->max_nv_buffer_size);
|
||||
ASSERT_NOT_NULL(payload2);
|
||||
random_bytes(payload2, c->max_nv_buffer_size);
|
||||
struct iovec data2 = IOVEC_MAKE(payload2, c->max_nv_buffer_size);
|
||||
ASSERT_OK_ZERO(tpm2_define_data_nv_index(c, /* session= */ NULL, /* requested_nv_index= */ 0, &data2, &nv_index, &nv_handle));
|
||||
ASSERT_NE(nv_index, 0U);
|
||||
ASSERT_NOT_NULL(nv_handle);
|
||||
|
||||
_cleanup_(iovec_done) struct iovec value = {};
|
||||
ASSERT_OK_ZERO(tpm2_read_nv_index(c, /* session= */ NULL, nv_index, nv_handle, &value));
|
||||
ASSERT_TRUE(iovec_equal(&value, &data2));
|
||||
|
||||
ASSERT_OK_ZERO(tpm2_undefine_nv_index(c, /* session= */ NULL, nv_index, nv_handle));
|
||||
nv_index = 0;
|
||||
nv_handle = tpm2_handle_free(nv_handle);
|
||||
iovec_done(&value);
|
||||
|
||||
/* Test reading of a payload which is smaller than the reported size of TPM2_PT_NV_BUFFER_MAX. */
|
||||
data.iov_len = 36;
|
||||
ASSERT_OK_ZERO(tpm2_define_data_nv_index(c, /* session= */ NULL, /* requested_nv_index= */ 0, &data, &nv_index, &nv_handle));
|
||||
ASSERT_NE(nv_index, 0U);
|
||||
ASSERT_NOT_NULL(nv_handle);
|
||||
|
||||
ASSERT_OK_ZERO(tpm2_read_nv_index(c, /* session= */ NULL, nv_index, nv_handle, &value));
|
||||
ASSERT_TRUE(iovec_equal(&value, &data));
|
||||
|
||||
ASSERT_OK_ZERO(tpm2_undefine_nv_index(c, /* session= */ NULL, nv_index, nv_handle));
|
||||
}
|
||||
|
||||
TEST_RET(tests_which_require_tpm) {
|
||||
_cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL;
|
||||
int r = 0;
|
||||
@@ -1386,6 +1463,7 @@ TEST_RET(tests_which_require_tpm) {
|
||||
check_best_srk_template(c);
|
||||
check_get_or_create_srk(c);
|
||||
check_seal_unseal(c);
|
||||
check_nv_index_read(c);
|
||||
|
||||
#if HAVE_OPENSSL
|
||||
r = check_calculate_seal(c);
|
||||
|
||||
Reference in New Issue
Block a user