diff --git a/doc/api_ref/tpm.rst b/doc/api_ref/tpm.rst index c278cbba12..113273549c 100644 --- a/doc/api_ref/tpm.rst +++ b/doc/api_ref/tpm.rst @@ -144,8 +144,9 @@ Asymmetric Keys hosted by a TPM 2.0 The TPM v2.0 supports RSA and ECC keys. Botan provides the classed ``PrivateKey`` and ``PublicKey`` in the ``TPM2`` namespace, to manage and use -asymmetric keys on the TPM. Additionally there are derived classes for RSA. ECC -is not supported at this time, but could be added in the future. +asymmetric keys on the TPM. Additionally there are derived classes for RSA and ECC. +Currently, RSA keys can be used for signing and encryption, while ECC keys can only +be used for ECDSA signing (i.e., ECDH, ECSCHNORR, and SM2 are not supported). Objects of these classes can be used throughout the Botan library to perform cryptographic operations with TPM keys wherever an abstract @@ -216,6 +217,23 @@ and manage RSA keys on the TPM. stored in the TPM's NVRAM and must be loaded from their public and private blobs after a reboot. +Similarly, Botan provides a set of derived classes for ECC keys. + +.. cpp:class:: Botan::TPM2::EC_PrivateKey + + .. cpp:function:: static std::unique_ptr create_unrestricted_transient(const std::shared_ptr& ctx, const SessionBundle& sessions, std::span auth_value, const TPM2::PrivateKey& parent, const EC_Group& group); + + Creates a new ECC key pair on the TPM with the given ``group``. The + group must be one of the supported curves by the TPM and currently + must be one of the NIST curves (secp192r1, secp224r1, secp256r1, + secp384r1, secp521r1). + + Keys generated with this function are not restricted in their usage. + They may only be used for signing: Currently, Botan only supports creating + ECDSA keys. Furthermore, they are transient, i.e. they are not stored in + the TPM's NVRAM and must be loaded from their public and private blobs after + a reboot. + Once a transient key pair was created on the TPM, it can be persisted into the TPM's NVRAM to make it available across reboots independently of the "private blob". This is done by passing the key pair to the ``Context::persist`` method. diff --git a/src/lib/prov/tpm2/info.txt b/src/lib/prov/tpm2/info.txt index 1538ceaf19..c37ea1fe7f 100644 --- a/src/lib/prov/tpm2/info.txt +++ b/src/lib/prov/tpm2/info.txt @@ -22,6 +22,7 @@ pubkey tpm2_algo_mappings.h tpm2_hash.h tpm2_util.h +tpm2_pkops.h diff --git a/src/lib/prov/tpm2/tpm2_algo_mappings.h b/src/lib/prov/tpm2/tpm2_algo_mappings.h index 569ad93381..fcf1b40dce 100644 --- a/src/lib/prov/tpm2/tpm2_algo_mappings.h +++ b/src/lib/prov/tpm2/tpm2_algo_mappings.h @@ -1,7 +1,7 @@ /* * TPM 2 algorithm mappings * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ @@ -9,6 +9,7 @@ #ifndef BOTAN_TPM2_ALGORITHM_MAPPINGS_H_ #define BOTAN_TPM2_ALGORITHM_MAPPINGS_H_ +#include #include #include @@ -29,7 +30,7 @@ namespace Botan::TPM2 { } else if(algo_name == "ECC") { return TPM2_ALG_ECC; } else if(algo_name == "ECDSA") { - return TPM2_ALG_ECC; + return TPM2_ALG_ECDSA; } else if(algo_name == "ECDH") { return TPM2_ALG_ECDH; } else if(algo_name == "ECDAA") { @@ -195,6 +196,69 @@ namespace Botan::TPM2 { } } +[[nodiscard]] inline std::optional curve_id_tss2_to_botan(TPMI_ECC_CURVE mode_id) { + // Currently, tpm2-tss does not include support for Brainpool curves or 25519/448. + // Once the corresponding PR (https://github.com/tpm2-software/tpm2-tss/pull/2897) is merged and released, + // this function should be updated. + switch(mode_id) { + case TPM2_ECC_NIST_P192: + return "secp192r1"; + case TPM2_ECC_NIST_P224: + return "secp224r1"; + case TPM2_ECC_NIST_P256: + return "secp256r1"; + case TPM2_ECC_NIST_P384: + return "secp384r1"; + case TPM2_ECC_NIST_P521: + return "secp521r1"; + case TPM2_ECC_SM2_P256: + return "sm2p256v1"; + default: + return std::nullopt; + } +} + +[[nodiscard]] inline std::optional curve_id_order_byte_size(TPMI_ECC_CURVE curve_id) { + switch(curve_id) { + case TPM2_ECC_NIST_P192: + return 24; + case TPM2_ECC_NIST_P224: + return 28; + case TPM2_ECC_NIST_P256: + return 32; + case TPM2_ECC_NIST_P384: + return 48; + case TPM2_ECC_NIST_P521: + return 66; // Rounded up to the next full byte + case TPM2_ECC_SM2_P256: + return 32; + default: + return std::nullopt; + } +} + +[[nodiscard]] inline std::optional get_tpm2_curve_id(const OID& curve_oid) { + // Currently, tpm2-tss does not include support for Brainpool curves or 25519/448. + // Once the corresponding PR (https://github.com/tpm2-software/tpm2-tss/pull/2897) is merged and released, + // this function should be updated. + const std::string curve_name = curve_oid.to_formatted_string(); + if(curve_name == "secp192r1") { + return TPM2_ECC_NIST_P192; + } else if(curve_name == "secp224r1") { + return TPM2_ECC_NIST_P224; + } else if(curve_name == "secp256r1") { + return TPM2_ECC_NIST_P256; + } else if(curve_name == "secp384r1") { + return TPM2_ECC_NIST_P384; + } else if(curve_name == "secp521r1") { + return TPM2_ECC_NIST_P521; + } else if(curve_name == "sm2p256v1") { + return TPM2_ECC_SM2_P256; + } else { + return std::nullopt; + } +} + [[nodiscard]] inline std::optional cipher_mode_botan_to_tss2(std::string_view mode_name) noexcept { if(mode_name == "CFB") { return TPM2_ALG_CFB; diff --git a/src/lib/prov/tpm2/tpm2_context.cpp b/src/lib/prov/tpm2/tpm2_context.cpp index 28521bed20..1c3ca4e6d7 100644 --- a/src/lib/prov/tpm2/tpm2_context.cpp +++ b/src/lib/prov/tpm2/tpm2_context.cpp @@ -1,7 +1,7 @@ /* * TPM 2 interface * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ diff --git a/src/lib/prov/tpm2/tpm2_context.h b/src/lib/prov/tpm2/tpm2_context.h index 522640e18b..8670652788 100644 --- a/src/lib/prov/tpm2/tpm2_context.h +++ b/src/lib/prov/tpm2/tpm2_context.h @@ -1,7 +1,7 @@ /* * TPM 2 interface * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ diff --git a/src/lib/prov/tpm2/tpm2_crypto_backend/tpm2_crypto_backend.cpp b/src/lib/prov/tpm2/tpm2_crypto_backend/tpm2_crypto_backend.cpp index 7f2f84f7fb..d6a15c2c05 100644 --- a/src/lib/prov/tpm2/tpm2_crypto_backend/tpm2_crypto_backend.cpp +++ b/src/lib/prov/tpm2/tpm2_crypto_backend/tpm2_crypto_backend.cpp @@ -1,36 +1,42 @@ /* * TPM 2 TSS crypto callbacks backend * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ #include -#include -#include -#include -#include - -#if defined(BOTAN_HAS_EME_OAEP) - #include -#endif - #include #include #include #include #include #include +#include -#if defined(BOTAN_HAS_TPM2_RSA_ADAPTER) - #include +#if defined(BOTAN_HAS_RSA) + #include #endif -#include +#if defined(BOTAN_HAS_ECDH) + #include +#endif + +#include +#include +#include +#include + +#if defined(BOTAN_HAS_EME_OAEP) + #include +#endif #include +#include + +#include #if defined(BOTAN_TSS2_SUPPORTS_CRYPTO_CALLBACKS) @@ -125,18 +131,24 @@ template F> * The bytes in @p data are encrypted/decrypted in-place. */ [[nodiscard]] TSS2_RC symmetric_algo(Botan::Cipher_Dir direction, + const uint8_t* key, TPM2_ALG_ID tpm_sym_alg, TPMI_AES_KEY_BITS key_bits, TPM2_ALG_ID tpm_mode, - const uint8_t* key, - const uint8_t* iv, - std::span data) noexcept { + uint8_t* buffer, + size_t buffer_size, + const uint8_t* iv) noexcept { return thunk([&] { if(!key) { return (direction == Botan::Cipher_Dir::Encryption) ? TSS2_ESYS_RC_NO_ENCRYPT_PARAM : TSS2_ESYS_RC_NO_DECRYPT_PARAM; } + // nullptr buffer with size 0 is alright + if(!buffer && buffer_size != 0) { + return TSS2_ESYS_RC_BAD_VALUE; + } + const auto cipher_name = Botan::TPM2::cipher_tss2_to_botan({ .algorithm = tpm_sym_alg, .keyBits = {.sym = key_bits}, @@ -163,6 +175,7 @@ template F> return TSS2_ESYS_RC_BAD_VALUE; } + const auto s_data = std::span{buffer, buffer_size}; const auto s_key = std::span{key, keylength}; const auto s_iv = [&]() -> std::span { if(iv) { @@ -174,7 +187,7 @@ template F> cipher->set_key(s_key); cipher->start(s_iv); - cipher->process(data); + cipher->process(s_data); return TSS2_RC_SUCCESS; }); } @@ -193,6 +206,10 @@ extern "C" { TSS2_RC hash_start(ESYS_CRYPTO_CONTEXT_BLOB** context, TPM2_ALG_ID hash_alg, void* userdata) { BOTAN_UNUSED(userdata); return thunk([&] { + if(!context) { + return TSS2_ESYS_RC_BAD_REFERENCE; + } + const auto hash_name = Botan::TPM2::hash_algo_tss2_to_botan(hash_alg); if(!hash_name) { return TSS2_ESYS_RC_NOT_SUPPORTED; @@ -228,6 +245,11 @@ TSS2_RC hash_update(ESYS_CRYPTO_CONTEXT_BLOB* context, const uint8_t* buffer, si return TSS2_ESYS_RC_BAD_REFERENCE; } + // nullptr buffer with size 0 is alright + if(!buffer && size != 0) { + return TSS2_ESYS_RC_BAD_VALUE; + } + hash->get().update(std::span{buffer, size}); return TSS2_RC_SUCCESS; }); @@ -246,14 +268,21 @@ TSS2_RC hash_update(ESYS_CRYPTO_CONTEXT_BLOB* context, const uint8_t* buffer, si */ TSS2_RC hash_finish(ESYS_CRYPTO_CONTEXT_BLOB** context, uint8_t* buffer, size_t* size, void* userdata) { BOTAN_UNUSED(userdata); + if(size != nullptr) { + *size = 0; + } + return thunk([&] { auto hash = get(context); - if(!hash) { + if(!hash || !buffer) { return TSS2_ESYS_RC_BAD_REFERENCE; } - *size = hash->get().output_length(); - hash->get().final(std::span{buffer, *size}); + const auto digest_size = hash->get().output_length(); + hash->get().final(std::span{buffer, digest_size}); + if(size != nullptr) { + *size = digest_size; + } delete *context; // allocated in hash_start() *context = nullptr; @@ -269,8 +298,10 @@ TSS2_RC hash_finish(ESYS_CRYPTO_CONTEXT_BLOB** context, uint8_t* buffer, size_t* */ void hash_abort(ESYS_CRYPTO_CONTEXT_BLOB** context, void* userdata) { BOTAN_UNUSED(userdata); - delete *context; // allocated in hash_start() - *context = nullptr; + if(context) { + delete *context; // allocated in hash_start() + *context = nullptr; + } } /** Provide the context an HMAC digest object from a byte buffer key. @@ -289,6 +320,10 @@ TSS2_RC hmac_start( ESYS_CRYPTO_CONTEXT_BLOB** context, TPM2_ALG_ID hash_alg, const uint8_t* key, size_t size, void* userdata) { BOTAN_UNUSED(userdata); return thunk([&] { + if(!context || !key) { + return TSS2_ESYS_RC_BAD_REFERENCE; + } + const auto hash_name = Botan::TPM2::hash_algo_tss2_to_botan(hash_alg); if(!hash_name) { return TSS2_ESYS_RC_NOT_SUPPORTED; @@ -326,6 +361,11 @@ TSS2_RC hmac_update(ESYS_CRYPTO_CONTEXT_BLOB* context, const uint8_t* buffer, si return TSS2_ESYS_RC_BAD_REFERENCE; } + // nullptr buffer with size 0 is alright + if(!buffer && size != 0) { + return TSS2_ESYS_RC_BAD_VALUE; + } + hmac->get().update(std::span{buffer, size}); return TSS2_RC_SUCCESS; }); @@ -344,14 +384,21 @@ TSS2_RC hmac_update(ESYS_CRYPTO_CONTEXT_BLOB* context, const uint8_t* buffer, si */ TSS2_RC hmac_finish(ESYS_CRYPTO_CONTEXT_BLOB** context, uint8_t* buffer, size_t* size, void* userdata) { BOTAN_UNUSED(userdata); + if(size != nullptr) { + *size = 0; + } + return thunk([&] { auto hmac = get(context); - if(!hmac) { + if(!hmac || !buffer) { return TSS2_ESYS_RC_BAD_REFERENCE; } - *size = hmac->get().output_length(); - hmac->get().final(std::span{buffer, *size}); + const auto digest_size = hmac->get().output_length(); + hmac->get().final(std::span{buffer, digest_size}); + if(size != nullptr) { + *size = digest_size; + } delete *context; // allocated in hmac_start() *context = nullptr; @@ -367,8 +414,10 @@ TSS2_RC hmac_finish(ESYS_CRYPTO_CONTEXT_BLOB** context, uint8_t* buffer, size_t* */ void hmac_abort(ESYS_CRYPTO_CONTEXT_BLOB** context, void* userdata) { BOTAN_UNUSED(userdata); - delete *context; // allocated in hmac_start() - *context = nullptr; + if(context) { + delete *context; // allocated in hmac_start() + *context = nullptr; + } } /** Compute random TPM2B data. @@ -384,12 +433,11 @@ void hmac_abort(ESYS_CRYPTO_CONTEXT_BLOB** context, void* userdata) { TSS2_RC get_random2b(TPM2B_NONCE* nonce, size_t num_bytes, void* userdata) { return thunk([&] { auto ccs = get(userdata); - if(!ccs) { + if(!ccs || !ccs->get().rng || !nonce) { return TSS2_ESYS_RC_BAD_REFERENCE; } - nonce->size = num_bytes; - ccs->get().rng->randomize(Botan::TPM2::as_span(*nonce)); + ccs->get().rng->randomize(Botan::TPM2::as_span(*nonce, num_bytes)); return TSS2_RC_SUCCESS; }); } @@ -417,6 +465,10 @@ TSS2_RC rsa_pk_encrypt(TPM2B_PUBLIC* pub_tpm_key, size_t* out_size, const char* label, void* userdata) { + if(out_size != nullptr) { + *out_size = 0; + } + // TODO: This is currently a dumpster fire of code duplication and // YOLO manual padding. // @@ -424,8 +476,7 @@ TSS2_RC rsa_pk_encrypt(TPM2B_PUBLIC* pub_tpm_key, // this up. See the extensive discussions in: // // https://github.com/randombit/botan/pull/4318#issuecomment-2297682058 - - #if defined(BOTAN_HAS_TPM2_RSA_ADAPTER) + #if defined(BOTAN_HAS_RSA) auto create_eme = [&]( const TPMT_RSA_SCHEME& scheme, [[maybe_unused]] TPM2_ALG_ID name_algo, @@ -487,7 +538,13 @@ TSS2_RC rsa_pk_encrypt(TPM2B_PUBLIC* pub_tpm_key, }; return thunk([&] { - BOTAN_ASSERT_NONNULL(pub_tpm_key); + auto ccs = get(userdata); + if(!ccs || !pub_tpm_key || !in_buffer || !out_buffer || !ccs->get().rng) { + return TSS2_ESYS_RC_BAD_REFERENCE; + } + + Botan::RandomNumberGenerator& rng = *ccs->get().rng; + BOTAN_ASSERT_NOMSG(pub_tpm_key->publicArea.type == TPM2_ALG_RSA); const auto maybe_eme = create_eme(pub_tpm_key->publicArea.parameters.rsaDetail.scheme, @@ -524,13 +581,6 @@ TSS2_RC rsa_pk_encrypt(TPM2B_PUBLIC* pub_tpm_key, return TSS2_ESYS_RC_INSUFFICIENT_BUFFER; } - auto ccs = get(userdata); - if(!ccs) { - return TSS2_ESYS_RC_BAD_REFERENCE; - } - - Botan::RandomNumberGenerator& rng = *ccs->get().rng; - const auto max_raw_bits = keybits - 1; const auto max_raw_bytes = (max_raw_bits + 7) / 8; const auto padded_bytes = eme->pad({out_buffer, max_raw_bytes}, {in_buffer, in_size}, max_raw_bits, rng); @@ -539,16 +589,19 @@ TSS2_RC rsa_pk_encrypt(TPM2B_PUBLIC* pub_tpm_key, // TODO: provide an `.encrypt()` overload that accepts an output buffer. Botan::PK_Encryptor_EME encryptor(pubkey, rng, "Raw"); const auto encrypted = encryptor.encrypt({out_buffer, padded_bytes}, rng); + BOTAN_DEBUG_ASSERT(encrypted.size() == output_size); // We abused the `out_buffer` to hold the result of the padding. Hence, we // now have to copy the encrypted data over the padded plaintext data. - *out_size = encrypted.size(); - Botan::copy_mem(std::span{out_buffer, *out_size}, encrypted); + Botan::copy_mem(std::span{out_buffer, encrypted.size()}, encrypted); + if(out_size != nullptr) { + *out_size = encrypted.size(); + } return TSS2_RC_SUCCESS; }); #else - BOTAN_UNUSED(pub_tpm_key, in_size, in_buffer, max_out_size, out_buffer, out_size, label, userdata); + BOTAN_UNUSED(pub_tpm_key, in_size, in_buffer, max_out_size, out_buffer, label, userdata); return TSS2_ESYS_RC_NOT_IMPLEMENTED; #endif } @@ -577,16 +630,49 @@ TSS2_RC get_ecdh_point(TPM2B_PUBLIC* key, BYTE* out_buffer, size_t* out_size, void* userdata) { - BOTAN_UNUSED(key, max_out_size, Z, Q, out_buffer, out_size, userdata); - // This is currently not required for the exposed functionality. - // TODO: Implement this function if required. - // - // Note that "if required" does not mean it can wait until we actually - // implement support for ECC keys. As soon as one wants to work with a - // TPM that contains ECC keys (from another source) and they would want - // to use such a key as the basis of a session key, this function would - // be required. + if(out_size != nullptr) { + *out_size = 0; + } + + #if defined(BOTAN_HAS_ECDH) + return thunk([&] { + auto ccs = get(userdata); + if(!ccs || !key || !Z || !Q || !out_buffer | !ccs->get().rng) { + return TSS2_ESYS_RC_BAD_REFERENCE; + } + + Botan::RandomNumberGenerator& rng = *ccs->get().rng; + + // 1: Get TPM public key + const auto [tpm_ec_group, tpm_ec_point] = Botan::TPM2::ecc_pubkey_from_tss2_public(key); + const auto tpm_sw_pubkey = Botan::ECDH_PublicKey(tpm_ec_group, tpm_ec_point.to_legacy_point()); + + const auto curve_order_byte_size = tpm_sw_pubkey.domain().get_p_bytes(); + + // 2: Generate ephemeral key + const auto eph_key = Botan::ECDH_PrivateKey(rng, tpm_sw_pubkey.domain()); + + // Serialize public key coordinates into TPM2B_ECC_PARAMETER with Big Endian encoding. + // This ensures bn_{x,y}.bytes() <= curve_order_byte_size. + const Botan::PointGFp& eph_pub_point = eph_key.public_point(); + eph_pub_point.get_affine_x().serialize_to(Botan::TPM2::as_span(Q->x, curve_order_byte_size)); + eph_pub_point.get_affine_y().serialize_to(Botan::TPM2::as_span(Q->y, curve_order_byte_size)); + + // 3: ECDH Key Agreement + Botan::PK_Key_Agreement ecdh(eph_key, rng, "Raw" /*No KDF used here*/); + const auto shared_secret = ecdh.derive_key(0 /*Ignored for raw KDF*/, tpm_sw_pubkey.public_value()).bits_of(); + + Botan::TPM2::copy_into(*Z, shared_secret); + + Botan::TPM2::check_rc("Tss2_MU_TPMS_ECC_POINT_Marshal", + Tss2_MU_TPMS_ECC_POINT_Marshal(Q, out_buffer, max_out_size, out_size)); + + return TSS2_RC_SUCCESS; + }); + #else + BOTAN_UNUSED(key, max_out_size, Z, Q, out_buffer, userdata); return TSS2_ESYS_RC_NOT_IMPLEMENTED; + #endif } /** Encrypt data with AES. @@ -617,8 +703,7 @@ TSS2_RC aes_encrypt(uint8_t* key, return TSS2_ESYS_RC_BAD_VALUE; } - return symmetric_algo( - Botan::Cipher_Dir::Encryption, tpm_sym_alg, key_bits, tpm_mode, key, iv, std::span{buffer, buffer_size}); + return symmetric_algo(Botan::Cipher_Dir::Encryption, key, tpm_sym_alg, key_bits, tpm_mode, buffer, buffer_size, iv); } /** Decrypt data with AES. @@ -649,8 +734,7 @@ TSS2_RC aes_decrypt(uint8_t* key, return TSS2_ESYS_RC_BAD_VALUE; } - return symmetric_algo( - Botan::Cipher_Dir::Decryption, tpm_sym_alg, key_bits, tpm_mode, key, iv, std::span{buffer, buffer_size}); + return symmetric_algo(Botan::Cipher_Dir::Decryption, key, tpm_sym_alg, key_bits, tpm_mode, buffer, buffer_size, iv); } #if defined(BOTAN_TSS2_SUPPORTS_SM4_IN_CRYPTO_CALLBACKS) @@ -683,8 +767,7 @@ TSS2_RC sm4_encrypt(uint8_t* key, return TSS2_ESYS_RC_BAD_VALUE; } - return symmetric_algo( - Botan::Cipher_Dir::Encryption, tpm_sym_alg, key_bits, tpm_mode, key, iv, std::span{buffer, buffer_size}); + return symmetric_algo(Botan::Cipher_Dir::Encryption, key, tpm_sym_alg, key_bits, tpm_mode, buffer, buffer_size, iv); } /** Decrypt data with SM4. @@ -715,8 +798,7 @@ TSS2_RC sm4_decrypt(uint8_t* key, return TSS2_ESYS_RC_BAD_VALUE; } - return symmetric_algo( - Botan::Cipher_Dir::Decryption, tpm_sym_alg, key_bits, tpm_mode, key, iv, std::span{buffer, buffer_size}); + return symmetric_algo(Botan::Cipher_Dir::Decryption, key, tpm_sym_alg, key_bits, tpm_mode, buffer, buffer_size, iv); } #endif /* TPM2_ALG_SM4 */ @@ -769,9 +851,16 @@ namespace Botan::TPM2 { * The runtime crypto backend is available since TSS2 4.0.0 and later. Explicit * support for SM4 was added in TSS2 4.1.0. * + * Note that the callback implementations should be defensive in regard to the + * input parameters. All pointers should be checked for nullptr before being + * dereferenced. Some output parameters (e.g. out-buffer lengths) may be + * regarded as optional, and should be checked for nullptr before being written + * to. + * * Error code conventions: * * * TSS2_ESYS_RC_BAD_REFERENCE: reference (typically userdata) invalid + * * TSS2_ESYS_RC_BAD_VALUE: invalid input (e.g. size != 0 w/ nullptr buffer) * * TSS2_ESYS_RC_NOT_SUPPORTED: algorithm identifier not mapped to Botan * * TSS2_ESYS_RC_NOT_IMPLEMENTED: algorithm not available (e.g. disabled) */ diff --git a/src/lib/prov/tpm2/tpm2_crypto_backend/tpm2_crypto_backend.h b/src/lib/prov/tpm2/tpm2_crypto_backend/tpm2_crypto_backend.h index 952368cb9b..b5161eaf2f 100644 --- a/src/lib/prov/tpm2/tpm2_crypto_backend/tpm2_crypto_backend.h +++ b/src/lib/prov/tpm2/tpm2_crypto_backend/tpm2_crypto_backend.h @@ -1,7 +1,7 @@ /* * TPM 2 TSS crypto callbacks backend * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ diff --git a/src/lib/prov/tpm2/tpm2_ecc/info.txt b/src/lib/prov/tpm2/tpm2_ecc/info.txt new file mode 100644 index 0000000000..3696e06bfc --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_ecc/info.txt @@ -0,0 +1,16 @@ + +TPM2_ECC_ADAPTER -> 20240925 + + + +name -> "TPM2 ECC Adapter" +brief -> "Support for ECC pairs hosted on TPM 2.0" + + + +ecdsa + + + +tpm2_ecc.h + diff --git a/src/lib/prov/tpm2/tpm2_ecc/tpm2_ecc.cpp b/src/lib/prov/tpm2/tpm2_ecc/tpm2_ecc.cpp new file mode 100644 index 0000000000..04552a6525 --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_ecc/tpm2_ecc.cpp @@ -0,0 +1,255 @@ +/* +* TPM 2.0 ECC Key Wrappres +* (C) 2024 Jack Lloyd +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include + +#include +#include +#include +#include +#include + +#include + +namespace Botan::TPM2 { + +EC_PublicKey::EC_PublicKey(Object handle, SessionBundle sessions, const TPM2B_PUBLIC* public_blob) : + EC_PublicKey(std::move(handle), std::move(sessions), ecc_pubkey_from_tss2_public(public_blob)) {} + +EC_PublicKey::EC_PublicKey(Object handle, SessionBundle sessions, std::pair public_key) : + Botan::TPM2::PublicKey(std::move(handle), std::move(sessions)), + Botan::EC_PublicKey(std::move(public_key.first), public_key.second) {} + +EC_PrivateKey::EC_PrivateKey(Object handle, + SessionBundle sessions, + const TPM2B_PUBLIC* public_blob, + std::span private_blob) : + EC_PrivateKey(std::move(handle), std::move(sessions), ecc_pubkey_from_tss2_public(public_blob), private_blob) {} + +EC_PrivateKey::EC_PrivateKey(Object handle, + SessionBundle sessions, + std::pair public_key, + std::span private_blob) : + Botan::TPM2::PrivateKey(std::move(handle), std::move(sessions), private_blob), + Botan::EC_PublicKey(std::move(public_key.first), public_key.second) {} + +std::unique_ptr EC_PrivateKey::public_key() const { + return std::make_unique(domain(), public_point()); +} + +std::vector EC_PublicKey::public_key_bits() const { + return Botan::EC_PublicKey::raw_public_key_bits(); +} + +std::vector EC_PublicKey::raw_public_key_bits() const { + return TPM2::PublicKey::raw_public_key_bits(); +} + +std::vector EC_PrivateKey::public_key_bits() const { + return Botan::EC_PublicKey::raw_public_key_bits(); +} + +std::vector EC_PrivateKey::raw_public_key_bits() const { + return TPM2::PrivateKey::raw_public_key_bits(); +} + +std::unique_ptr EC_PrivateKey::create_unrestricted_transient(const std::shared_ptr& ctx, + const SessionBundle& sessions, + std::span auth_value, + const TPM2::PrivateKey& parent, + const EC_Group& group) { + // TODO: Code duplication from RSA_PrivateKey::create_unrestricted_transient + BOTAN_ARG_CHECK(parent.is_parent(), "The passed key cannot be used as a parent key"); + + const auto curve_id = get_tpm2_curve_id(group.get_curve_oid()); + if(!curve_id) { + throw Invalid_Argument("Unsupported ECC curve"); + } + + TPM2B_SENSITIVE_CREATE sensitive_data = { + .size = 0, // ignored + .sensitive = + { + .userAuth = copy_into(auth_value), + + // Architecture Document, Section 25.2.3 + // When an asymmetric key is created, the caller is not allowed to + // provide the sensitive data of the key. + .data = init_empty(), + }, + }; + + TPMT_PUBLIC key_template = { + .type = TPM2_ALG_ECC, + + // This is the algorithm for fingerprinting the newly created public key. + // For best compatibility we always use SHA-256. + .nameAlg = TPM2_ALG_SHA256, + + // This sets up the key to be both a decryption and a signing key, forbids + // its duplication (fixed_tpm, fixed_parent) and ensures that the key's + // private portion can be used only by a user with an HMAC or password + // session. + .objectAttributes = ObjectAttributes::render({ + .fixed_tpm = true, + .fixed_parent = true, + .sensitive_data_origin = true, + .user_with_auth = true, + .decrypt = true, // TODO: Shall we set this? + .sign_encrypt = true, + }), + + // We currently do not support policy-based authorization + .authPolicy = init_empty(), + .parameters = + { + .eccDetail = + { + // Structures Document (Part 2), Section 12.2.3.5 + // If the key is not a restricted decryption key, this field + // shall be set to TPM_ALG_NULL. + // + // TODO: Once we stop supporting TSS < 4.0, we could use + // `.keyBits = {.null = {}}, .mode = {.null = {}}` + // which better reflects our intention here. + .symmetric = + { + .algorithm = TPM2_ALG_NULL, + .keyBits = {.sym = 0}, + .mode = {.sym = TPM2_ALG_NULL}, + }, + + // Structures Document (Part 2), Section 12.2.3.6 + // If the decrypt attribute of the key is SET, then this shall be a + // valid key exchange scheme or TPM_ALG_NULL + // + // TODO: Once we stop supporting TSS < 4.0, we could use + // `.details = {.null = {}}` + // which better reflects our intention here. + .scheme = + { + .scheme = TPM2_ALG_NULL, + .details = {.anySig = {.hashAlg = TPM2_ALG_NULL}}, + }, + .curveID = curve_id.value(), + + // Structures Document (Part 2), Section 12.2.3.6 + // If the kdf parameter associated with curveID is not + // TPM_ALG_NULL then this is required to be NULL. + // NOTE There are currently no commands where this parameter + // has effect and, in the reference code, this field needs to + // be set to TPM_ALG_NULL + // TODO: Easier initialization? + .kdf = {.scheme = TPM2_ALG_NULL, .details = {.kdf2 = {.hashAlg = TPM2_ALG_NULL}}}, + }, + }, + + // For creating an asymmetric key this value is not used. + .unique = {.ecc = {}}, + }; + + return create_transient_from_template( + ctx, sessions, parent.handles().transient_handle(), key_template, sensitive_data); +} + +namespace { + +SignatureAlgorithmSelection make_signature_scheme(std::string_view hash_name) { + return { + .signature_scheme = + TPMT_SIG_SCHEME{ + .scheme = TPM2_ALG_ECDSA, // Only support ECDSA + .details = {.any = {.hashAlg = get_tpm2_hash_type(hash_name)}}, + }, + .hash_name = std::string(hash_name), + .padding = std::nullopt, + }; +} + +size_t signature_length_for_key_handle(const SessionBundle& sessions, const Object& object) { + const auto curve_id = object._public_info(sessions, TPM2_ALG_ECDSA).pub->publicArea.parameters.eccDetail.curveID; + + const auto order_bytes = curve_id_order_byte_size(curve_id); + if(!order_bytes) { + throw Invalid_Argument(Botan::fmt("Unsupported ECC curve: {}", curve_id)); + }; + return 2 * order_bytes.value(); +} + +class EC_Signature_Operation final : public Signature_Operation { + public: + EC_Signature_Operation(const Object& object, const SessionBundle& sessions, std::string_view hash) : + Signature_Operation(object, sessions, make_signature_scheme(hash)) {} + + size_t signature_length() const override { return signature_length_for_key_handle(sessions(), key_handle()); } + + AlgorithmIdentifier algorithm_identifier() const override { + // Copied from ECDSA + const std::string full_name = "ECDSA/" + hash_function(); + const OID oid = OID::from_string(full_name); + return AlgorithmIdentifier(oid, AlgorithmIdentifier::USE_EMPTY_PARAM); + } + + private: + std::vector marshal_signature(const TPMT_SIGNATURE& signature) const override { + BOTAN_STATE_CHECK(signature.sigAlg == TPM2_ALG_ECDSA); + + const auto r = as_span(signature.signature.ecdsa.signatureR); + const auto s = as_span(signature.signature.ecdsa.signatureS); + const auto sig_len = signature_length_for_key_handle(sessions(), key_handle()); + BOTAN_ASSERT_NOMSG(sig_len % 2 == 0); + BOTAN_ASSERT_NOMSG(r.size() == sig_len / 2 && s.size() == sig_len / 2); + + return concat>(r, s); + } +}; + +class EC_Verification_Operation final : public Verification_Operation { + public: + EC_Verification_Operation(const Object& object, const SessionBundle& sessions, std::string_view hash) : + Verification_Operation(object, sessions, make_signature_scheme(hash)) {} + + private: + TPMT_SIGNATURE unmarshal_signature(std::span sig_data) const override { + BOTAN_STATE_CHECK(scheme().scheme == TPM2_ALG_ECDSA); + + const auto sig_len = signature_length_for_key_handle(sessions(), key_handle()); + BOTAN_ARG_CHECK(sig_data.size() == sig_len, "Invalid signature length"); + BOTAN_ASSERT_NOMSG(sig_len % 2 == 0); + + return { + .sigAlg = TPM2_ALG_ECDSA, + .signature = + { + .ecdsa = + { + .hash = scheme().details.any.hashAlg, + .signatureR = copy_into(sig_data.first(sig_len / 2)), + .signatureS = copy_into(sig_data.last(sig_len / 2)), + }, + }, + }; + } +}; + +} // namespace + +std::unique_ptr EC_PublicKey::create_verification_op(std::string_view params, + std::string_view provider) const { + BOTAN_UNUSED(provider); + return std::make_unique(handles(), sessions(), params); +} + +std::unique_ptr EC_PrivateKey::create_signature_op(Botan::RandomNumberGenerator& rng, + std::string_view params, + std::string_view provider) const { + BOTAN_UNUSED(rng, provider); + return std::make_unique(handles(), sessions(), params); +} + +} // namespace Botan::TPM2 diff --git a/src/lib/prov/tpm2/tpm2_ecc/tpm2_ecc.h b/src/lib/prov/tpm2/tpm2_ecc/tpm2_ecc.h new file mode 100644 index 0000000000..8f0ce6d6c3 --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_ecc/tpm2_ecc.h @@ -0,0 +1,121 @@ +/* +* TPM 2.0 ECC Wrappers +* (C) 2024 Jack Lloyd +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ +#ifndef BOTAN_TPM2_ECC_H_ +#define BOTAN_TPM2_ECC_H_ + +#include +#include + +namespace Botan::TPM2 { + +class BOTAN_PUBLIC_API(3, 6) EC_PublicKey final : public virtual Botan::TPM2::PublicKey, + public virtual Botan::EC_PublicKey { + public: + std::string algo_name() const override { return "ECDSA"; } + + /** + * @returns the public key encoding in ordinary point encoding + * @sa EC_PublicKey::set_point_encoding() + */ + std::vector public_key_bits() const override; + + /** + * @returns the public key encoding in TPM2B_PUBLIC format + */ + std::vector raw_public_key_bits() const override; + + bool supports_operation(PublicKeyOperation op) const override { + // TODO: ECDH/Key Agreement + return op == PublicKeyOperation::Signature; + } + + std::unique_ptr create_verification_op(std::string_view params, + std::string_view provider) const override; + + protected: + friend class TPM2::PublicKey; + + EC_PublicKey(Object handle, SessionBundle sessions, const TPM2B_PUBLIC* public_blob); + EC_PublicKey(Object handle, SessionBundle sessions, std::pair public_key); +}; + +BOTAN_DIAGNOSTIC_PUSH +BOTAN_DIAGNOSTIC_IGNORE_INHERITED_VIA_DOMINANCE + +class BOTAN_PUBLIC_API(3, 6) EC_PrivateKey final : public virtual Botan::TPM2::PrivateKey, + public virtual Botan::EC_PublicKey { + public: + std::string algo_name() const override { + // TODO: Different types of ECC + // TPM ECC keys may be used for different algorithms, so we do not always know the exact algorithm + // because it may be used for ECDH, ECDSA, ECDAA, etc. + // However, at least for signatures, we can say it is ECDSA since EdDSA is not supported by tpm2-tss and + // ECDAA and ECSCHNORR are not supported by Botan. + return "ECDSA"; + } + + std::unique_ptr generate_another(Botan::RandomNumberGenerator&) const override { + throw Not_Implemented("Cannot generate a new TPM-based keypair from this asymmetric key"); + } + + /** + * Create a transient EC key with the given @p group EC Group, + * under the given @p parent key, with the given @p auth_value. + * This key may only be used for ECDSA signatures. + * + * @param ctx The TPM context to use + * @param sessions The session bundle to use in the creation of the key + * @param auth_value The auth value to use for the key + * @param parent The parent key to create the new key under + * @param group The desired EC Group + */ + static std::unique_ptr create_unrestricted_transient(const std::shared_ptr& ctx, + const SessionBundle& sessions, + std::span auth_value, + const TPM2::PrivateKey& parent, + const EC_Group& group); + + public: + std::unique_ptr public_key() const override; + + /** + * @returns the public key encoding in ordinary point encoding + * @sa EC_PublicKey::set_point_encoding() + */ + std::vector public_key_bits() const override; + + /** + * @returns the public key encoding in TPM2B_PUBLIC format + */ + std::vector raw_public_key_bits() const override; + + bool supports_operation(PublicKeyOperation op) const override { return op == PublicKeyOperation::Signature; } + + std::unique_ptr create_signature_op(Botan::RandomNumberGenerator& rng, + std::string_view params, + std::string_view provider) const override; + + protected: + friend class TPM2::PrivateKey; + + EC_PrivateKey(Object handle, + SessionBundle sessions, + const TPM2B_PUBLIC* public_blob, + std::span private_blob = {}); + + EC_PrivateKey(Object handle, + SessionBundle sessions, + std::pair public_key, + std::span private_blob = {}); +}; + +BOTAN_DIAGNOSTIC_POP + +} // namespace Botan::TPM2 + +#endif diff --git a/src/lib/prov/tpm2/tpm2_error.cpp b/src/lib/prov/tpm2/tpm2_error.cpp index e6f6f408f2..286a92c0d4 100644 --- a/src/lib/prov/tpm2/tpm2_error.cpp +++ b/src/lib/prov/tpm2/tpm2_error.cpp @@ -1,7 +1,7 @@ /* * TPM 2 error handling * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ @@ -33,7 +33,7 @@ TSS2_RC get_raw_rc(TSS2_RC rc) { return rc & 0xFFFF; } #endif -}; +} namespace { diff --git a/src/lib/prov/tpm2/tpm2_error.h b/src/lib/prov/tpm2/tpm2_error.h index b1798b7f7c..c861e64a2b 100644 --- a/src/lib/prov/tpm2/tpm2_error.h +++ b/src/lib/prov/tpm2/tpm2_error.h @@ -1,7 +1,7 @@ /* * TPM 2 error handling * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ diff --git a/src/lib/prov/tpm2/tpm2_hash.cpp b/src/lib/prov/tpm2/tpm2_hash.cpp index dd0da8c9b5..f8ff82f5f8 100644 --- a/src/lib/prov/tpm2/tpm2_hash.cpp +++ b/src/lib/prov/tpm2/tpm2_hash.cpp @@ -1,7 +1,7 @@ /* * TPM 2.0 Hash Function Wrappers * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ diff --git a/src/lib/prov/tpm2/tpm2_hash.h b/src/lib/prov/tpm2/tpm2_hash.h index 0008c28d9b..fe36a6b022 100644 --- a/src/lib/prov/tpm2/tpm2_hash.h +++ b/src/lib/prov/tpm2/tpm2_hash.h @@ -1,7 +1,7 @@ /* * TPM 2.0 Hash Function Wrappers * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ diff --git a/src/lib/prov/tpm2/tpm2_key.cpp b/src/lib/prov/tpm2/tpm2_key.cpp index f405cd8be6..42251b1ad7 100644 --- a/src/lib/prov/tpm2/tpm2_key.cpp +++ b/src/lib/prov/tpm2/tpm2_key.cpp @@ -1,7 +1,7 @@ /* * TPM 2.0 Key Wrappers' Base Class * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ @@ -11,6 +11,9 @@ #if defined(BOTAN_HAS_TPM2_RSA_ADAPTER) #include #endif +#if defined(BOTAN_HAS_TPM2_ECC_ADAPTER) + #include +#endif #include #include @@ -23,6 +26,44 @@ namespace Botan::TPM2 { +#if defined(BOTAN_HAS_RSA) +Botan::RSA_PublicKey rsa_pubkey_from_tss2_public(const TPM2B_PUBLIC* public_area) { + BOTAN_ASSERT_NONNULL(public_area); + const auto& pub = public_area->publicArea; + BOTAN_ARG_CHECK(pub.type == TPM2_ALG_RSA, "Public key is not an RSA key"); + + // TPM2 may report 0 when the exponent is 'the default' (2^16 + 1) + const auto exponent = (pub.parameters.rsaDetail.exponent == 0) ? 65537 : pub.parameters.rsaDetail.exponent; + + return Botan::RSA_PublicKey(BigInt(as_span(pub.unique.rsa)), exponent); +} +#endif + +#if defined(BOTAN_HAS_ECC_GROUP) +std::pair ecc_pubkey_from_tss2_public(const TPM2B_PUBLIC* public_blob) { + BOTAN_ASSERT_NONNULL(public_blob); + BOTAN_ARG_CHECK(public_blob->publicArea.type == TPM2_ALG_ECC, "Public blob is not an ECC key"); + + const auto curve_id = public_blob->publicArea.parameters.eccDetail.curveID; + const auto curve_name = curve_id_tss2_to_botan(curve_id); + if(!curve_name) { + throw Invalid_Argument(Botan::fmt("Unsupported ECC curve: {}", curve_id)); + } + + auto curve = Botan::EC_Group::from_name(curve_name.value()); + // Create an EC_AffinePoint from the x and y coordinates as SEC1 uncompressed point. + // According to the TPM2.0 specification Part 1 C.8, each coordinate is already padded to the curve's size. + auto point = EC_AffinePoint::deserialize(curve, + concat(std::vector({0x04}), + as_span(public_blob->publicArea.unique.ecc.x), + as_span(public_blob->publicArea.unique.ecc.y))); + if(!point) { + throw Invalid_Argument("Invalid ECC Point"); + } + return {std::move(curve), std::move(point.value())}; +} +#endif + namespace { Object load_persistent_object(const std::shared_ptr& ctx, @@ -124,6 +165,11 @@ std::unique_ptr PublicKey::create(Object handles, const SessionBundle return std::unique_ptr(new RSA_PublicKey(std::move(handles), sessions, pubinfo)); } #endif +#if defined(BOTAN_HAS_TPM2_ECC_ADAPTER) + if(pubinfo->publicArea.type == TPM2_ALG_ECC) { + return std::unique_ptr(new EC_PublicKey(std::move(handles), sessions, pubinfo)); + } +#endif throw Not_Implemented(Botan::fmt("Loaded a {} public key of an unsupported type", handles.has_persistent_handle() ? "persistent" : "transient")); @@ -185,8 +231,9 @@ std::unique_ptr PrivateKey::create_transient_from_template(const std #endif break; case TPM2_ALG_ECC: - // TODO: support ECC keys - throw Not_Implemented("TPM2-based ECC keys are not yet supported"); +#if not defined(BOTAN_HAS_TPM2_ECC_ADAPTER) + throw Not_Implemented("TPM2-based ECC keys are not supported in this build"); +#endif break; default: throw Invalid_Argument("Unsupported key type"); @@ -256,7 +303,11 @@ std::unique_ptr PrivateKey::create(Object handles, } #endif - // TODO: Support ECC keys +#if defined(BOTAN_HAS_TPM2_ECC_ADAPTER) + if(public_info->publicArea.type == TPM2_ALG_ECC) { + return std::unique_ptr(new EC_PrivateKey(std::move(handles), sessions, public_info, private_blob)); + } +#endif throw Not_Implemented(Botan::fmt("Loaded a {} private key of an unsupported type", handles.has_persistent_handle() ? "persistent" : "transient")); diff --git a/src/lib/prov/tpm2/tpm2_key.h b/src/lib/prov/tpm2/tpm2_key.h index a8f96af551..291c8f2c4b 100644 --- a/src/lib/prov/tpm2/tpm2_key.h +++ b/src/lib/prov/tpm2/tpm2_key.h @@ -1,7 +1,7 @@ /* * TPM 2.0 Key Wrappers' Base Class * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ @@ -12,6 +12,13 @@ #include #include #include +#if defined(BOTAN_HAS_RSA) + #include +#endif +#if defined(BOTAN_HAS_ECC_GROUP) + #include + #include +#endif struct TPM2B_SENSITIVE_CREATE; struct TPMT_PUBLIC; @@ -19,12 +26,36 @@ struct TPM2B_PUBLIC; namespace Botan::TPM2 { +#if defined(BOTAN_HAS_RSA) +/** + * This helper function transforms a @p public_blob in a TPM2B_PUBLIC* format + * into an ordinary Botan::RSA_PublicKey. Note that the resulting key is not + * bound to a TPM and can be used as any other RSA key. + * + * @param public_blob The public blob to load as an ordinary RSA key + */ +BOTAN_PUBLIC_API(3, 6) Botan::RSA_PublicKey rsa_pubkey_from_tss2_public(const TPM2B_PUBLIC* public_blob); +#endif + +#if defined(BOTAN_HAS_ECC_GROUP) +/** + * This helper function transforms a @p public_blob in a TPM2B_PUBLIC* format + * into an ordinary Botan::EC_PublicKey in the form of a Botan::EC_Group and + * a Botan::EC_AffinePoint. Note that the resulting key is not bound to a TPM + * and can be used as any other ECC key. + * + * @param public_blob The public blob to load as an ordinary EC_Group and EC_AffinePoint + */ + +std::pair ecc_pubkey_from_tss2_public(const TPM2B_PUBLIC* public_blob); +#endif + /** * This wraps a public key that is hosted in a TPM 2.0 device. This class allows * performing public-key operations on the TPM. Namely verifying signatures and * encrypting data. * - * The class does not provied public constructors, but instead provides static + * The class does not provide public constructors, but instead provides static * methods to obtain a public key handle from a TPM. */ class BOTAN_PUBLIC_API(3, 6) PublicKey : public virtual Botan::Public_Key { diff --git a/src/lib/prov/tpm2/tpm2_object.cpp b/src/lib/prov/tpm2/tpm2_object.cpp index fbc070bc8c..355ce1760b 100644 --- a/src/lib/prov/tpm2/tpm2_object.cpp +++ b/src/lib/prov/tpm2/tpm2_object.cpp @@ -1,7 +1,7 @@ /* * TPM 2.0 Base Object handling * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ diff --git a/src/lib/prov/tpm2/tpm2_object.h b/src/lib/prov/tpm2/tpm2_object.h index ba8ec83cf0..ff43ed22cd 100644 --- a/src/lib/prov/tpm2/tpm2_object.h +++ b/src/lib/prov/tpm2/tpm2_object.h @@ -1,7 +1,7 @@ /* * TPM 2.0 Base Object handling * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ diff --git a/src/lib/prov/tpm2/tpm2_pkops.cpp b/src/lib/prov/tpm2/tpm2_pkops.cpp new file mode 100644 index 0000000000..bf1b9c48f5 --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_pkops.cpp @@ -0,0 +1,123 @@ +/* +* TPM 2.0 Public Key Operations +* (C) 2024 Jack Lloyd +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include + +#include +#include +#include + +namespace Botan::TPM2 { + +namespace { + +/** +* Signing with a restricted key requires a validation ticket that is provided +* when hashing the data to sign on the TPM. Otherwise, it is fine to hash the +* data in software. +* +* @param key_handle the key to create the signature with +* @param sessions the sessions to use for the TPM operations +* @param hash_name the name of the hash function to use +* +* @return a HashFunction that hashes in hardware if the key is restricted +*/ +std::unique_ptr create_hash_function(const Object& key_handle, + const SessionBundle& sessions, + std::string_view hash_name) { + if(key_handle.attributes(sessions).restricted) { + // TODO: this could also be ENDORSEMENT or PLATFORM, and we're not 100% sure + // that OWNER is always the right choice here. + const TPMI_RH_HIERARCHY hierarchy = ESYS_TR_RH_OWNER; + return std::make_unique(key_handle.context(), hash_name, hierarchy, sessions); + } else { + return Botan::HashFunction::create_or_throw(hash_name); + } +} + +} // namespace + +Signature_Operation::Signature_Operation(const Object& object, + const SessionBundle& sessions, + const SignatureAlgorithmSelection& algorithms) : + Botan::TPM2::Signature_Operation_Base( + object, sessions, algorithms, create_hash_function(object, sessions, algorithms.hash_name)) {} + +std::vector Signature_Operation::sign(Botan::RandomNumberGenerator& rng) { + BOTAN_UNUSED(rng); + + auto do_sign = [this](const TPM2B_DIGEST& digest, const TPMT_TK_HASHCHECK& validation) { + unique_esys_ptr signature; + check_rc("Esys_Sign", + Esys_Sign(*key_handle().context(), + key_handle().transient_handle(), + sessions()[0], + sessions()[1], + sessions()[2], + &digest, + &scheme(), + &validation, + out_ptr(signature))); + BOTAN_ASSERT_NONNULL(signature); + BOTAN_ASSERT_NOMSG(signature->sigAlg == scheme().scheme); + BOTAN_ASSERT_NOMSG(signature->signature.any.hashAlg == scheme().details.any.hashAlg); + return signature; + }; + + auto signature = [&] { + if(auto h = dynamic_cast(hash())) { + // This is a TPM2-based hash object that calculated the digest on + // the TPM. We can use the validation ticket to create the signature. + auto [digest, validation] = h->final_with_ticket(); + BOTAN_ASSERT_NONNULL(digest); + BOTAN_ASSERT_NONNULL(validation); + return do_sign(*digest, *validation); + } else { + // This is a software hash, so we have to stub the validation ticket + // and create the signature without it. + TPM2B_DIGEST digest; + hash()->final(as_span(digest, hash()->output_length())); + return do_sign(digest, + TPMT_TK_HASHCHECK{ + .tag = TPM2_ST_HASHCHECK, + .hierarchy = TPM2_RH_NULL, + .digest = init_empty(), + }); + } + }(); + + return marshal_signature(*signature); +} + +Verification_Operation::Verification_Operation(const Object& object, + const SessionBundle& sessions, + const SignatureAlgorithmSelection& algorithms) : + Signature_Operation_Base( + object, sessions, algorithms, Botan::HashFunction::create_or_throw(algorithms.hash_name)) {} + +bool Verification_Operation::is_valid_signature(std::span sig_data) { + TPM2B_DIGEST digest; + hash()->final(as_span(digest, hash()->output_length())); + + const auto signature = unmarshal_signature(sig_data); + + // If the signature is not valid, this returns TPM2_RC_SIGNATURE. + const auto rc = check_rc_expecting("Esys_VerifySignature", + Esys_VerifySignature(*key_handle().context(), + key_handle().transient_handle(), + sessions()[0], + sessions()[1], + sessions()[2], + &digest, + &signature, + nullptr /* validation */)); + + return rc == TPM2_RC_SUCCESS; +} + +} // namespace Botan::TPM2 diff --git a/src/lib/prov/tpm2/tpm2_pkops.h b/src/lib/prov/tpm2/tpm2_pkops.h new file mode 100644 index 0000000000..52ca9bb361 --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_pkops.h @@ -0,0 +1,104 @@ +/* +* TPM 2.0 Public Key Operations +* (C) 2024 Jack Lloyd +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_TPM2_PKOPS_H_ +#define BOTAN_TPM2_PKOPS_H_ + +#include + +#include +#include + +namespace Botan::TPM2 { + +struct SignatureAlgorithmSelection { + TPMT_SIG_SCHEME signature_scheme; + std::string hash_name; + std::optional padding; +}; + +template +class Signature_Operation_Base : public PKOpT { + public: + Signature_Operation_Base(const Object& object, + const SessionBundle& sessions, + const SignatureAlgorithmSelection& algorithms, + std::unique_ptr hash) : + m_key_handle(object), + m_sessions(sessions), + m_scheme(algorithms.signature_scheme), + m_hash(std::move(hash)), + m_padding(algorithms.padding) { + BOTAN_ASSERT_NONNULL(m_hash); + } + + public: + void update(std::span msg) override { m_hash->update(msg); } + + std::string hash_function() const override { return m_hash->name(); } + + protected: + Botan::HashFunction* hash() { return m_hash.get(); } + + const Object& key_handle() const { return m_key_handle; } + + const SessionBundle& sessions() const { return m_sessions; } + + const TPMT_SIG_SCHEME& scheme() const { return m_scheme; } + + std::optional padding() const { return m_padding; } + + private: + const Object& m_key_handle; + const SessionBundle& m_sessions; + TPMT_SIG_SCHEME m_scheme; + std::unique_ptr m_hash; + std::optional m_padding; +}; + +/** + * If the key is restricted, this will transparently use the TPM to hash the + * data to obtain a validation ticket. + * + * TPM Library, Part 1: Architecture", Section 11.4.6.3 (4) + * This ticket is used to indicate that a digest of external data is safe to + * sign using a restricted signing key. A restricted signing key may only + * sign a digest that was produced by the TPM. [...] This prevents forgeries + * of attestation data. + */ +class Signature_Operation : public Signature_Operation_Base { + public: + Signature_Operation(const Object& object, + const SessionBundle& sessions, + const SignatureAlgorithmSelection& algorithms); + + std::vector sign(Botan::RandomNumberGenerator& rng) override; + + protected: + virtual std::vector marshal_signature(const TPMT_SIGNATURE& signature) const = 0; +}; + +/** + * Signature verification on the TPM. This does not require a validation ticket, + * therefore the hash is always calculated in software. + */ +class Verification_Operation : public Signature_Operation_Base { + public: + Verification_Operation(const Object& object, + const SessionBundle& sessions, + const SignatureAlgorithmSelection& algorithms); + + bool is_valid_signature(std::span sig_data) override; + + protected: + virtual TPMT_SIGNATURE unmarshal_signature(std::span sig_data) const = 0; +}; + +} // namespace Botan::TPM2 + +#endif diff --git a/src/lib/prov/tpm2/tpm2_rng.cpp b/src/lib/prov/tpm2/tpm2_rng.cpp index a7f6d25dd6..04f6309663 100644 --- a/src/lib/prov/tpm2/tpm2_rng.cpp +++ b/src/lib/prov/tpm2/tpm2_rng.cpp @@ -1,7 +1,7 @@ /* * TPM 2 RNG interface * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ diff --git a/src/lib/prov/tpm2/tpm2_rng.h b/src/lib/prov/tpm2/tpm2_rng.h index 2180aff098..bd24423505 100644 --- a/src/lib/prov/tpm2/tpm2_rng.h +++ b/src/lib/prov/tpm2/tpm2_rng.h @@ -1,7 +1,7 @@ /* * TPM 2 RNG interface * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ diff --git a/src/lib/prov/tpm2/tpm2_rsa/tpm2_rsa.cpp b/src/lib/prov/tpm2/tpm2_rsa/tpm2_rsa.cpp index 08b47714e0..c70ed988c5 100644 --- a/src/lib/prov/tpm2/tpm2_rsa/tpm2_rsa.cpp +++ b/src/lib/prov/tpm2/tpm2_rsa/tpm2_rsa.cpp @@ -1,7 +1,7 @@ /* * TPM 2.0 RSA Key Wrappres * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ @@ -9,7 +9,6 @@ #include #include -#include #include #include @@ -20,23 +19,13 @@ #include #include #include +#include #include #include namespace Botan::TPM2 { -Botan::RSA_PublicKey rsa_pubkey_from_tss2_public(const TPM2B_PUBLIC* public_area) { - BOTAN_ASSERT_NONNULL(public_area); - const auto& pub = public_area->publicArea; - BOTAN_ARG_CHECK(pub.type == TPM2_ALG_RSA, "Public key is not an RSA key"); - - // TPM2 may report 0 when the exponent is 'the default' (2^16 + 1) - const auto exponent = (pub.parameters.rsaDetail.exponent == 0) ? 65537 : pub.parameters.rsaDetail.exponent; - - return Botan::RSA_PublicKey(BigInt(as_span(pub.unique.rsa)), exponent); -} - RSA_PublicKey::RSA_PublicKey(Object handle, SessionBundle session_bundle, const TPM2B_PUBLIC* public_blob) : Botan::TPM2::PublicKey(std::move(handle), std::move(session_bundle)), Botan::RSA_PublicKey(rsa_pubkey_from_tss2_public(public_blob)) {} @@ -136,12 +125,6 @@ std::unique_ptr RSA_PrivateKey::create_unrestricted_transient( namespace { -struct SignatureAlgorithmSelection { - TPMT_SIG_SCHEME signature_scheme; - std::string hash_name; - std::string padding; -}; - SignatureAlgorithmSelection select_signature_algorithms(std::string_view padding) { const SCAN_Name req(padding); if(req.arg_count() == 0) { @@ -160,85 +143,16 @@ SignatureAlgorithmSelection select_signature_algorithms(std::string_view padding }; } -/** - * Signing with a restricted key requires a validation ticket that is provided - * when hashing the data to sign on the TPM. Otherwise, it is fine to hash the - * data in software. - * - * @param key_handle the key to create the signature with - * @param sessions the sessions to use for the TPM operations - * @param hash_name the name of the hash function to use - * - * @return a HashFunction that hashes in hardware if the key is restricted - */ -std::unique_ptr create_hash_function(const Object& key_handle, - const SessionBundle& sessions, - std::string_view hash_name) { - if(key_handle.attributes(sessions).restricted) { - // TODO: this could also be ENDORSEMENT or PLATFORM, and we're not 100% sure - // that OWNER is always the right choice here. - const TPMI_RH_HIERARCHY hierarchy = ESYS_TR_RH_OWNER; - return std::make_unique(key_handle.context(), hash_name, hierarchy, sessions); - } else { - return Botan::HashFunction::create_or_throw(hash_name); - } +size_t signature_length_for_key_handle(const SessionBundle& sessions, const Object& key_handle) { + return key_handle._public_info(sessions, TPM2_ALG_RSA).pub->publicArea.parameters.rsaDetail.keyBits / 8; } -/** - * If the key is restricted, this will transparently use the TPM to hash the - * data to obtain a validation ticket. - * - * TPM Library, Part 1: Architecture", Section 11.4.6.3 (4) - * This ticket is used to indicate that a digest of external data is safe to - * sign using a restricted signing key. A restricted signing key may only - * sign a digest that was produced by the TPM. [...] This prevents forgeries - * of attestation data. - */ -class RSA_Signature_Operation final : public PK_Ops::Signature { - private: - RSA_Signature_Operation(const Object& object, - const SessionBundle& sessions, - SignatureAlgorithmSelection algorithms) : - m_key_handle(object), - m_sessions(sessions), - m_scheme(algorithms.signature_scheme), - m_hash(create_hash_function(m_key_handle, m_sessions, algorithms.hash_name)), - m_padding(std::move(algorithms.padding)) { - BOTAN_ASSERT_NONNULL(m_hash); - } - +class RSA_Signature_Operation final : public Signature_Operation { public: RSA_Signature_Operation(const Object& object, const SessionBundle& sessions, std::string_view padding) : - RSA_Signature_Operation(object, sessions, select_signature_algorithms(padding)) {} - - void update(std::span msg) override { m_hash->update(msg); } - - std::vector sign(Botan::RandomNumberGenerator& /* rng */) override { - if(auto hash = dynamic_cast(m_hash.get())) { - // This is a TPM2-based hash object that calculated the digest on - // the TPM. We can use the validation ticket to create the signature. - auto [digest, validation] = hash->final_with_ticket(); - return create_signature(digest.get(), validation.get()); - } else { - // This is a software hash, so we have to stub the validation ticket - // and create the signature without it. - TPMT_TK_HASHCHECK dummy_validation = { - .tag = TPM2_ST_HASHCHECK, - .hierarchy = TPM2_RH_NULL, - .digest = init_empty(), - }; - - auto digest = init_with_size(m_hash->output_length()); - m_hash->final(as_span(digest)); - return create_signature(&digest, &dummy_validation); - } - } - - size_t signature_length() const override { - return m_key_handle._public_info(m_sessions, TPM2_ALG_RSA).pub->publicArea.parameters.rsaDetail.keyBits / 8; - } + Signature_Operation(object, sessions, select_signature_algorithms(padding)) {} - std::string hash_function() const override { return m_hash->name(); } + size_t signature_length() const override { return signature_length_for_key_handle(sessions(), key_handle()); } AlgorithmIdentifier algorithm_identifier() const override { // TODO: This is essentially a copy of the ::algorithm_identifier() @@ -249,7 +163,8 @@ class RSA_Signature_Operation final : public PK_Ops::Signature { // conveniently figure out the algorithm identifier. // // TODO: This is a hack, and we should clean this up. - const auto emsa = EMSA::create_or_throw(m_padding); + BOTAN_STATE_CHECK(padding().has_value()); + const auto emsa = EMSA::create_or_throw(padding().value()); const std::string emsa_name = emsa->name(); try { @@ -267,104 +182,47 @@ class RSA_Signature_Operation final : public PK_Ops::Signature { } private: - std::vector create_signature(const TPM2B_DIGEST* digest, const TPMT_TK_HASHCHECK* validation) { - unique_esys_ptr signature; - check_rc("Esys_Sign", - Esys_Sign(*m_key_handle.context(), - m_key_handle.transient_handle(), - m_sessions[0], - m_sessions[1], - m_sessions[2], - digest, - &m_scheme, - validation, - out_ptr(signature))); - - BOTAN_ASSERT_NONNULL(signature); - const auto& sig = [&]() -> TPMS_SIGNATURE_RSA& { - if(signature->sigAlg == TPM2_ALG_RSASSA) { - return signature->signature.rsassa; - } else if(signature->sigAlg == TPM2_ALG_RSAPSS) { - return signature->signature.rsapss; + std::vector marshal_signature(const TPMT_SIGNATURE& signature) const override { + const auto& sig = [&] { + if(signature.sigAlg == TPM2_ALG_RSASSA) { + return signature.signature.rsassa; + } else if(signature.sigAlg == TPM2_ALG_RSAPSS) { + return signature.signature.rsapss; } - - throw Invalid_State(fmt("TPM2 returned an unexpected signature scheme {}", signature->sigAlg)); + throw Invalid_State(fmt("TPM2 returned an unexpected signature scheme {}", signature.sigAlg)); }(); - BOTAN_ASSERT_NOMSG(sig.hash == m_scheme.details.any.hashAlg); - + BOTAN_ASSERT_NOMSG(sig.sig.size == signature_length()); return copy_into>(sig.sig); } - - private: - const Object& m_key_handle; - const SessionBundle& m_sessions; - TPMT_SIG_SCHEME m_scheme; - std::unique_ptr m_hash; - std::string m_padding; }; -/** - * Signature verification on the TPM. This does not require a validation ticket, - * therefore the hash is always calculated in software. - */ -class RSA_Verification_Operation final : public PK_Ops::Verification { - private: - RSA_Verification_Operation(const Object& object, - const SessionBundle& sessions, - const SignatureAlgorithmSelection& algorithms) : - m_key_handle(object), - m_sessions(sessions), - m_scheme(algorithms.signature_scheme), - m_hash(Botan::HashFunction::create_or_throw(algorithms.hash_name)) {} - +class RSA_Verification_Operation final : public Verification_Operation { public: RSA_Verification_Operation(const Object& object, const SessionBundle& sessions, std::string_view padding) : - RSA_Verification_Operation(object, sessions, select_signature_algorithms(padding)) {} + Verification_Operation(object, sessions, select_signature_algorithms(padding)) {} - void update(std::span msg) override { m_hash->update(msg); } - - bool is_valid_signature(std::span sig_data) override { - auto digest = init_with_size(m_hash->output_length()); - m_hash->final(as_span(digest)); + private: + TPMT_SIGNATURE unmarshal_signature(std::span signature) const override { + BOTAN_ARG_CHECK(signature.size() == signature_length_for_key_handle(sessions(), key_handle()), + "Unexpected signature byte length"); - const auto signature = [&]() -> TPMT_SIGNATURE { - TPMT_SIGNATURE sig; - sig.sigAlg = m_scheme.scheme; - sig.signature.any.hashAlg = m_scheme.details.any.hashAlg; + TPMT_SIGNATURE sig; + sig.sigAlg = scheme().scheme; + auto& sig_data = [&]() -> TPMS_SIGNATURE_RSA& { if(sig.sigAlg == TPM2_ALG_RSASSA) { - copy_into(sig.signature.rsassa.sig, sig_data); + return sig.signature.rsassa; } else if(sig.sigAlg == TPM2_ALG_RSAPSS) { - copy_into(sig.signature.rsapss.sig, sig_data); - } else { - throw Invalid_State(fmt("Requested an unexpected signature scheme {}", sig.sigAlg)); + return sig.signature.rsapss; } - - return sig; + throw Invalid_State(fmt("Requested an unexpected signature scheme {}", sig.sigAlg)); }(); - // If the signature is not valid, this returns TPM2_RC_SIGNATURE. - const auto rc = check_rc_expecting("Esys_VerifySignature", - Esys_VerifySignature(*m_key_handle.context(), - m_key_handle.transient_handle(), - m_sessions[0], - m_sessions[1], - m_sessions[2], - &digest, - &signature, - nullptr /* validation */)); - - return rc == TPM2_RC_SUCCESS; + sig_data.hash = scheme().details.any.hashAlg; + copy_into(sig_data.sig, signature); + return sig; } - - std::string hash_function() const override { return m_hash->name(); } - - private: - const Object& m_key_handle; - const SessionBundle& m_sessions; - TPMT_SIG_SCHEME m_scheme; - std::unique_ptr m_hash; }; TPMT_RSA_DECRYPT select_encryption_algorithms(std::string_view padding) { diff --git a/src/lib/prov/tpm2/tpm2_rsa/tpm2_rsa.h b/src/lib/prov/tpm2/tpm2_rsa/tpm2_rsa.h index 10c0d48638..48a0b511e5 100644 --- a/src/lib/prov/tpm2/tpm2_rsa/tpm2_rsa.h +++ b/src/lib/prov/tpm2/tpm2_rsa/tpm2_rsa.h @@ -1,7 +1,7 @@ /* * TPM 2.0 RSA Key Wrappers * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ @@ -12,16 +12,6 @@ #include namespace Botan::TPM2 { - -/** - * This helper function transforms a @p public_blob in a TPM2B_PUBLIC* format - * into an ordinary Botan::RSA_PublicKey. Note that the resulting key is not - * bound to a TPM and can be used as any other RSA key. - * - * @param public_blob The public blob to load as an ordinary RSA key - */ -BOTAN_PUBLIC_API(3, 6) Botan::RSA_PublicKey rsa_pubkey_from_tss2_public(const TPM2B_PUBLIC* public_blob); - class BOTAN_PUBLIC_API(3, 6) RSA_PublicKey final : public virtual Botan::TPM2::PublicKey, public virtual Botan::RSA_PublicKey { public: diff --git a/src/lib/prov/tpm2/tpm2_session.cpp b/src/lib/prov/tpm2/tpm2_session.cpp index 4a5a3fdb03..4ccb8a6802 100644 --- a/src/lib/prov/tpm2/tpm2_session.cpp +++ b/src/lib/prov/tpm2/tpm2_session.cpp @@ -1,7 +1,7 @@ /* * TPM 2 Auth Session Wrapper * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ diff --git a/src/lib/prov/tpm2/tpm2_session.h b/src/lib/prov/tpm2/tpm2_session.h index 9d9f562f4c..c6b6496551 100644 --- a/src/lib/prov/tpm2/tpm2_session.h +++ b/src/lib/prov/tpm2/tpm2_session.h @@ -1,7 +1,7 @@ /* * TPM 2 Auth Session Wrapper * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ diff --git a/src/lib/prov/tpm2/tpm2_util.h b/src/lib/prov/tpm2/tpm2_util.h index 76459ac11d..47f2c7a2f5 100644 --- a/src/lib/prov/tpm2/tpm2_util.h +++ b/src/lib/prov/tpm2/tpm2_util.h @@ -1,7 +1,7 @@ /* * TPM 2 internal utilities * (C) 2024 Jack Lloyd -* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH, financed by LANCOM Systems GmbH * * Botan is released under the Simplified BSD License (see license.txt) */ @@ -103,13 +103,19 @@ constexpr auto as_span(tpm2_buffer auto& data) { return std::span{data.buffer, data.size}; } +/// Set the size of @p data to @p length and construct a std::span +/// as a view into @p data +constexpr auto as_span(tpm2_buffer auto& data, size_t length) { + BOTAN_ASSERT_NOMSG(length <= sizeof(data.buffer)); + data.size = static_cast(length); + return as_span(data); +} + /// Copy the @p data into the TPM2 buffer @p dest, assuming that the /// provided @p data is not larger than the capacity of the buffer. template constexpr void copy_into(T& dest, std::span data) { - BOTAN_ASSERT_NOMSG(data.size() <= sizeof(dest.buffer)); - dest.size = static_cast(data.size()); - copy_mem(as_span(dest), data); + copy_mem(as_span(dest, data.size()), data); } /// Create a TPM2 buffer from the provided @p data, assuming that the @@ -131,21 +137,12 @@ constexpr OutT copy_into(const tpm2_buffer auto& data) { return result; } -/// Create a TPM2 buffer setting it's size field to the given @p length, -/// assuming that the provided @p length is not larger than the capacity of the -/// buffer type. No data is copied into the new buffer. -template -constexpr T init_with_size(size_t length) { - T result; - BOTAN_ARG_CHECK(length <= sizeof(result.buffer), "Not enough capacity in TPM2 buffer type"); - result.size = static_cast(length); - return result; -} - /// Create an empty TPM2 buffer of the given type. template constexpr T init_empty() { - return init_with_size(0); + T result; + result.size = 0; + return result; } struct esys_liberator { diff --git a/src/lib/pubkey/ec_group/ec_group.h b/src/lib/pubkey/ec_group/ec_group.h index 87ffd9dcf9..188285e2cc 100644 --- a/src/lib/pubkey/ec_group/ec_group.h +++ b/src/lib/pubkey/ec_group/ec_group.h @@ -229,7 +229,7 @@ class BOTAN_PUBLIC_API(2, 0) EC_Group final { size_t get_p_bits() const; /** - * Return the size of p in bits (same as get_p().bytes()) + * Return the size of p in bytes (same as get_p().bytes()) */ size_t get_p_bytes() const; diff --git a/src/scripts/ci/start_tpm2_simulator.sh b/src/scripts/ci/start_tpm2_simulator.sh index 7649ea58e1..7dc24f8ee4 100755 --- a/src/scripts/ci/start_tpm2_simulator.sh +++ b/src/scripts/ci/start_tpm2_simulator.sh @@ -23,6 +23,7 @@ tcti_conf="bus_name=${dbus_name},bus_type=session" tcti="${tcti_name}:${tcti_conf}" test_pwd="password" persistent_rsa_key_handle="0x81000008" +persistent_ecc_key_handle="0x81000010" if ! systemctl is-active --quiet dbus; then echo "DBus is not running. Starting it..." @@ -74,6 +75,7 @@ tpm2_createprimary --tcti="$tcti" \ --key-algorithm rsa \ --key-context $tmp_dir/primary.ctx + # Use default key template of tpm2_create for rsa. # This means that the key will NOT be "restricted". tpm2_create --tcti="$tcti" \ @@ -92,6 +94,23 @@ tpm2_evictcontrol --tcti="$tcti" \ --object-context $tmp_dir/rsa.ctx \ $persistent_rsa_key_handle +# Do the same for ecc +tpm2_create --tcti="$tcti" \ + --parent-context $tmp_dir/primary.ctx \ + --key-algorithm ecc \ + --public $tmp_dir/ecc.pub \ + --private $tmp_dir/ecc.priv \ + --key-auth $test_pwd +tpm2_load --tcti="$tcti" \ + --parent-context $tmp_dir/primary.ctx \ + --public $tmp_dir/ecc.pub \ + --private $tmp_dir/ecc.priv \ + --key-context $tmp_dir/ecc.ctx +tpm2_evictcontrol --tcti="$tcti" \ + --hierarchy o \ + --object-context $tmp_dir/ecc.ctx \ + $persistent_ecc_key_handle + echo "Effectively disable dictionary attack lockout..." tpm2_dictionarylockout --tcti="$tcti" \ --setup-parameters \ @@ -107,4 +126,5 @@ if [ -n "$GITHUB_ACTIONS" ]; then echo "BOTAN_TPM2_TCTI_CONF=$tcti_conf" >> $GITHUB_ENV echo "BOTAN_TPM2_PERSISTENT_KEY_AUTH_VALUE=$test_pwd" >> $GITHUB_ENV echo "BOTAN_TPM2_PERSISTENT_RSA_KEY_HANDLE=$persistent_rsa_key_handle" >> $GITHUB_ENV + echo "BOTAN_TPM2_PERSISTENT_ECC_KEY_HANDLE=$persistent_ecc_key_handle" >> $GITHUB_ENV fi diff --git a/src/scripts/ci_build.py b/src/scripts/ci_build.py index b363c47840..7aa61eed8e 100755 --- a/src/scripts/ci_build.py +++ b/src/scripts/ci_build.py @@ -460,6 +460,7 @@ def add_boost_support(target, target_os): test_cmd += ["--tpm2-tcti-name=%s" % os.getenv('BOTAN_TPM2_TCTI_NAME'), "--tpm2-tcti-conf=%s" % os.getenv('BOTAN_TPM2_TCTI_CONF'), "--tpm2-persistent-rsa-handle=%s" % os.getenv('BOTAN_TPM2_PERSISTENT_RSA_KEY_HANDLE'), + "--tpm2-persistent-ecc-handle=%s" % os.getenv('BOTAN_TPM2_PERSISTENT_ECC_KEY_HANDLE'), "--tpm2-persistent-auth-value=%s" % os.getenv('BOTAN_TPM2_PERSISTENT_KEY_AUTH_VALUE')] elif os.environ.get('BOTAN_TPM2_ENABLED') == 'build': # build the TPM2 module but don't run the tests diff --git a/src/tests/test_tpm2.cpp b/src/tests/test_tpm2.cpp index 02f1ced36b..8584de6bdb 100644 --- a/src/tests/test_tpm2.cpp +++ b/src/tests/test_tpm2.cpp @@ -23,7 +23,10 @@ #include #endif - #include + #if defined(BOTAN_HAS_TPM2_ECC_ADAPTER) + #include + #include + #endif #endif namespace Botan_Tests { @@ -163,9 +166,9 @@ std::vector test_tpm2_context() { } }), - // TODO: once ECC support is added, add an ifdef and test for ECC keys #if defined(BOTAN_HAS_TPM2_RSA_ADAPTER) - CHECK("Fetch Storage Root Key", [&](Test::Result& result) { + // TODO: Since SRK is always RSA in start_tpm2_simulator.sh, the test always requires the RSA adapter? + CHECK("Fetch Storage Root Key RSA", [&](Test::Result& result) { auto srk = ctx->storage_root_key({}, {}); result.require("SRK is not null", srk != nullptr); result.test_eq("Algo", srk->algo_name(), "RSA"); @@ -199,7 +202,9 @@ std::vector test_tpm2_sessions() { ok(result, "CFB(AES-128),SHA-1", Session::unauthenticated_session(ctx, "CFB(AES-128)", "SHA-1")); }), - CHECK("Authenticated sessions", + #if defined(BOTAN_HAS_TPM2_RSA_ADAPTER) + CHECK( + "Authenticated sessions SRK", [&](Test::Result& result) { using Session = Botan::TPM2::Session; @@ -209,6 +214,26 @@ std::vector test_tpm2_sessions() { ok(result, "CFB(AES-128),SHA-384", Session::authenticated_session(ctx, *srk, "CFB(AES-128)", "SHA-384")); ok(result, "CFB(AES-128),SHA-1", Session::authenticated_session(ctx, *srk, "CFB(AES-128)", "SHA-1")); }), + #endif + + #if defined(BOTAN_HAS_TPM2_ECC_ADAPTER) + CHECK("Authenticated sessions ECC", [&](Test::Result& result) { + using Session = Botan::TPM2::Session; + const auto persistent_key_id = Test::options().tpm2_persistent_ecc_handle(); + + auto ecc_key = Botan::TPM2::EC_PrivateKey::load_persistent(ctx, persistent_key_id, {}, {}); + result.require("EK is not null", ecc_key != nullptr); + result.test_eq("Algo", ecc_key->algo_name(), "ECDSA"); + result.confirm("Has persistent handle", ecc_key->handles().has_persistent_handle()); + + ok(result, "default", Session::authenticated_session(ctx, *ecc_key)); + ok(result, "CFB(AES-128)", Session::authenticated_session(ctx, *ecc_key, "CFB(AES-128)")); + ok(result, + "CFB(AES-128),SHA-384", + Session::authenticated_session(ctx, *ecc_key, "CFB(AES-128)", "SHA-384")); + ok(result, "CFB(AES-128),SHA-1", Session::authenticated_session(ctx, *ecc_key, "CFB(AES-128)", "SHA-1")); + }), + #endif }; } @@ -649,6 +674,341 @@ std::vector test_tpm2_rsa() { #endif + #if defined(BOTAN_HAS_TPM2_ECC_ADAPTER) +template +auto load_persistent_ecc(Test::Result& result, + const std::shared_ptr& ctx, + uint32_t persistent_key_id, + std::span auth_value, + const std::shared_ptr& session) { + // TODO: Merge with RSA + const auto persistent_handles = ctx->persistent_handles(); + result.confirm( + "Persistent key available", + std::find(persistent_handles.begin(), persistent_handles.end(), persistent_key_id) != persistent_handles.end()); + + auto key = [&] { + if constexpr(std::same_as) { + return KeyT::load_persistent(ctx, persistent_key_id, session); + } else { + return KeyT::load_persistent(ctx, persistent_key_id, auth_value, session); + } + }(); + + result.test_eq("Algo", key->algo_name(), "ECDSA"); + result.test_is_eq("Handle", key->handles().persistent_handle(), persistent_key_id); + return key; +} + +std::vector test_tpm2_ecc() { + //TODO: Merge with RSA? + auto ctx = get_tpm2_context(__func__); + if(!ctx) { + return {bail_out()}; + } + + auto session = Botan::TPM2::Session::unauthenticated_session(ctx); + + const auto persistent_key_id = Test::options().tpm2_persistent_ecc_handle(); + const auto password = Test::options().tpm2_persistent_auth_value(); + + return { + CHECK("ECC and its helpers are supported", + [&](Test::Result& result) { + result.confirm("ECC is supported", ctx->supports_algorithm("ECC")); + result.confirm("ECDSA is supported", ctx->supports_algorithm("ECDSA")); + }), + CHECK("Load the private key multiple times", + [&](Test::Result& result) { + for(size_t i = 0; i < 20; ++i) { + auto key = load_persistent_ecc( + result, ctx, persistent_key_id, password, session); + result.test_eq(Botan::fmt("Key loaded successfully ({})", i), key->algo_name(), "ECDSA"); + } + }), + CHECK("Sign a message ECDSA", + [&](Test::Result& result) { + auto key = + load_persistent_ecc(result, ctx, persistent_key_id, password, session); + + Botan::Null_RNG null_rng; + Botan::PK_Signer signer(*key, null_rng /* TPM takes care of this */, "SHA-256"); + + // create a message that is larger than the TPM2 max buffer size + const auto message = [] { + std::vector msg(TPM2_MAX_DIGEST_BUFFER + 5); + for(size_t i = 0; i < msg.size(); ++i) { + msg[i] = static_cast(i); + } + return msg; + }(); + const auto signature = signer.sign_message(message, null_rng); + result.require("signature is not empty", !signature.empty()); + + auto public_key = key->public_key(); + Botan::PK_Verifier verifier(*public_key, "SHA-256"); + result.confirm("Signature is valid", verifier.verify_message(message, signature)); + }), + CHECK("verify signature ECDSA", + [&](Test::Result& result) { + auto sign = [&](std::span message) { + auto key = load_persistent_ecc( + result, ctx, persistent_key_id, password, session); + Botan::Null_RNG null_rng; + Botan::PK_Signer signer(*key, null_rng /* TPM takes care of this */, "SHA-256"); + return signer.sign_message(message, null_rng); + }; + + auto verify = [&](std::span msg, std::span sig) { + auto key = load_persistent_ecc( + result, ctx, persistent_key_id, password, session); + Botan::PK_Verifier verifier(*key, "SHA-256"); + return verifier.verify_message(msg, sig); + }; + + const auto message = Botan::hex_decode("baadcafe"); + const auto signature = sign(message); + + result.confirm("verification successful", verify(message, signature)); + + // change the message + auto rng = Test::new_rng(__func__); + auto mutated_message = Test::mutate_vec(message, *rng); + result.confirm("verification failed", !verify(mutated_message, signature)); + + // ESAPI manipulates the session attributes internally and does + // not reset them when an error occurs. A failure to validate a + // signature is an error, and hence behaves surprisingly by + // leaving the session attributes in an unexpected state. + // The Botan wrapper has a workaround for this... + const auto attrs = session->attributes(); + result.confirm("encrypt flag was not cleared by ESAPI", attrs.encrypt); + + // orignal message again + result.confirm("verification still successful", verify(message, signature)); + }), + + CHECK("sign and verify multiple messages with the same Signer/Verifier objects", + [&](Test::Result& result) { + const std::vector> messages = { + Botan::hex_decode("BAADF00D"), + Botan::hex_decode("DEADBEEF"), + Botan::hex_decode("CAFEBABE"), + }; + + // Generate a few signatures, then deallocate the private key. + auto signatures = [&] { + auto sk = load_persistent_ecc( + result, ctx, persistent_key_id, password, session); + Botan::Null_RNG null_rng; + Botan::PK_Signer signer(*sk, null_rng /* TPM takes care of this */, "SHA-256"); + std::vector> sigs; + sigs.reserve(messages.size()); + for(const auto& message : messages) { + sigs.emplace_back(signer.sign_message(message, null_rng)); + } + return sigs; + }(); + + // verify via TPM 2.0 + auto pk = + load_persistent_ecc(result, ctx, persistent_key_id, password, session); + Botan::PK_Verifier verifier(*pk, "SHA-256"); + for(size_t i = 0; i < messages.size(); ++i) { + result.confirm(Botan::fmt("verification successful ({})", i), + verifier.verify_message(messages[i], signatures[i])); + } + + // verify via software + auto soft_pk = + load_persistent_ecc(result, ctx, persistent_key_id, password, session) + ->public_key(); + Botan::PK_Verifier soft_verifier(*soft_pk, "SHA-256"); + for(size_t i = 0; i < messages.size(); ++i) { + result.confirm(Botan::fmt("software verification successful ({})", i), + soft_verifier.verify_message(messages[i], signatures[i])); + } + }), + + CHECK("Wrong password is not accepted during ECDSA signing", + [&](Test::Result& result) { + auto key = load_persistent_ecc( + result, ctx, persistent_key_id, Botan::hex_decode("deadbeef"), session); + + Botan::Null_RNG null_rng; + Botan::PK_Signer signer(*key, null_rng /* TPM takes care of this */, "SHA-256"); + + const auto message = Botan::hex_decode("baadcafe"); + result.test_throws("Fail with wrong password", + [&] { signer.sign_message(message, null_rng); }); + }), + + // SRK is an RSA key, so we can only test with the RSA adapter + #if defined(BOTAN_HAS_TPM2_RSA_ADAPTER) + CHECK("Create a transient ECDSA key and sign/verify a message", + [&](Test::Result& result) { + auto srk = ctx->storage_root_key({}, {}); + auto ecc_session_key = + Botan::TPM2::EC_PrivateKey::load_persistent(ctx, persistent_key_id, password, {}); + auto authed_session = Botan::TPM2::Session::authenticated_session(ctx, *ecc_session_key); + + const std::array secret = {'s', 'e', 'c', 'r', 'e', 't'}; + auto sk = Botan::TPM2::EC_PrivateKey::create_unrestricted_transient( + ctx, authed_session, secret, *srk, Botan::EC_Group::from_name("secp521r1")); + auto pk = sk->public_key(); + + const auto plaintext = Botan::hex_decode("feedc0debaadcafe"); + + Botan::Null_RNG null_rng; + Botan::PK_Signer signer(*sk, null_rng /* TPM takes care of this */, "SHA-256"); + + // create a message that is larger than the TPM2 max buffer size + const auto message = [] { + std::vector msg(TPM2_MAX_DIGEST_BUFFER + 5); + for(size_t i = 0; i < msg.size(); ++i) { + msg[i] = static_cast(i); + } + return msg; + }(); + const auto signature = signer.sign_message(message, null_rng); + result.require("signature is not empty", !signature.empty()); + + auto public_key = sk->public_key(); + Botan::PK_Verifier verifier(*public_key, "SHA-256"); + result.confirm("Signature is valid", verifier.verify_message(message, signature)); + }), + + CHECK("Create a new transient ECDSA key", + [&](Test::Result& result) { + auto srk = ctx->storage_root_key({}, {}); + auto ecc_session_key = + Botan::TPM2::EC_PrivateKey::load_persistent(ctx, persistent_key_id, password, {}); + + auto authed_session = Botan::TPM2::Session::authenticated_session(ctx, *ecc_session_key); + + const std::array secret = {'s', 'e', 'c', 'r', 'e', 't'}; + + auto sk = Botan::TPM2::EC_PrivateKey::create_unrestricted_transient( + ctx, authed_session, secret, *srk, Botan::EC_Group::from_name("secp384r1")); + + result.require("key was created", sk != nullptr); + result.confirm("is transient", sk->handles().has_transient_handle()); + result.confirm("is not persistent", !sk->handles().has_persistent_handle()); + + const auto sk_blob = sk->raw_private_key_bits(); + const auto pk_blob = sk->raw_public_key_bits(); + const auto pk = sk->public_key(); + + result.confirm("secret blob is not empty", !sk_blob.empty()); + result.confirm("public blob is not empty", !pk_blob.empty()); + + // Perform a round-trip sign/verify test with the new key pair + std::vector message = {'h', 'e', 'l', 'l', 'o'}; + Botan::Null_RNG null_rng; + Botan::PK_Signer signer(*sk, null_rng /* TPM takes care of this */, "SHA-256"); + const auto signature = signer.sign_message(message, null_rng); + result.require("signature is not empty", !signature.empty()); + + Botan::PK_Verifier verifier(*pk, "SHA-256"); + result.confirm("Signature is valid", verifier.verify_message(message, signature)); + + // Destruct the key and load it again from the encrypted blob + sk.reset(); + auto sk_loaded = + Botan::TPM2::PrivateKey::load_transient(ctx, secret, *srk, pk_blob, sk_blob, authed_session); + result.require("key was loaded", sk_loaded != nullptr); + result.test_eq("loaded key is ECDSA", sk_loaded->algo_name(), "ECDSA"); + + const auto sk_blob_loaded = sk_loaded->raw_private_key_bits(); + const auto pk_blob_loaded = sk_loaded->raw_public_key_bits(); + + result.test_is_eq("secret blob did not change", sk_blob, sk_blob_loaded); + result.test_is_eq("public blob did not change", pk_blob, pk_blob_loaded); + + // Perform a round-trip sign/verify test with the new key pair + std::vector message_loaded = {'g', 'u', 't', 'e', 'n', ' ', 't', 'a', 'g'}; + Botan::PK_Signer signer_loaded(*sk_loaded, null_rng /* TPM takes care of this */, "SHA-256"); + const auto signature_loaded = signer_loaded.sign_message(message_loaded, null_rng); + result.require("Next signature is not empty", !signature_loaded.empty()); + result.confirm("Existing verifier can validate signature", + verifier.verify_message(message_loaded, signature_loaded)); + + // Load the public portion of the key + auto pk_loaded = Botan::TPM2::PublicKey::load_transient(ctx, pk_blob, {}); + result.require("public key was loaded", pk_loaded != nullptr); + + Botan::PK_Verifier verifier_loaded(*pk_loaded, "SHA-256"); + result.confirm("TPM-verified signature is valid", + verifier_loaded.verify_message(message_loaded, signature_loaded)); + }), + + CHECK( + "Make a transient ECDSA key persistent then remove it again", + [&](Test::Result& result) { + auto srk = ctx->storage_root_key({}, {}); + auto ecc_session_key = Botan::TPM2::EC_PrivateKey::load_persistent(ctx, persistent_key_id, password, {}); + + auto sign_verify_roundtrip = [&](const Botan::TPM2::PrivateKey& key) { + std::vector message = {'h', 'e', 'l', 'l', 'o'}; + Botan::Null_RNG null_rng; + Botan::PK_Signer signer(key, null_rng /* TPM takes care of this */, "SHA-256"); + const auto signature = signer.sign_message(message, null_rng); + result.require("signature is not empty", !signature.empty()); + + auto pk = key.public_key(); + Botan::PK_Verifier verifier(*pk, "SHA-256"); + result.confirm("Signature is valid", verifier.verify_message(message, signature)); + }; + + // Create Key + auto authed_session = Botan::TPM2::Session::authenticated_session(ctx, *ecc_session_key); + + const std::array secret = {'s', 'e', 'c', 'r', 'e', 't'}; + auto sk = Botan::TPM2::EC_PrivateKey::create_unrestricted_transient( + ctx, authed_session, secret, *srk, Botan::EC_Group::from_name("secp192r1")); + result.require("key was created", sk != nullptr); + result.confirm("is transient", sk->handles().has_transient_handle()); + result.confirm("is not persistent", !sk->handles().has_persistent_handle()); + result.test_no_throw("use key after creation", [&] { sign_verify_roundtrip(*sk); }); + + // Make it persistent + const auto handles = ctx->persistent_handles().size(); + const auto new_location = ctx->persist(*sk, authed_session, secret); + result.test_eq("One more handle", ctx->persistent_handles().size(), handles + 1); + result.confirm("New location occupied", Botan::value_exists(ctx->persistent_handles(), new_location)); + result.confirm("is persistent", sk->handles().has_persistent_handle()); + result.test_is_eq( + "Persistent handle is the new handle", sk->handles().persistent_handle(), new_location); + result.test_throws( + "Cannot persist to the same location", [&] { ctx->persist(*sk, authed_session, {}, new_location); }); + result.test_throws("Cannot persist and already persistent key", + [&] { ctx->persist(*sk, authed_session); }); + result.test_no_throw("use key after persisting", [&] { sign_verify_roundtrip(*sk); }); + + // Evict it + ctx->evict(std::move(sk), authed_session); + result.test_eq("One less handle", ctx->persistent_handles().size(), handles); + result.confirm("New location no longer occupied", + !Botan::value_exists(ctx->persistent_handles(), new_location)); + }), + #endif + + CHECK("Read a software public key from a TPM serialization", [&](Test::Result& result) { + auto pk = load_persistent_ecc(result, ctx, persistent_key_id, password, session); + result.test_no_throw("Botan can read serialized ECC public key", [&] { + auto pk_sw = Botan::ECDSA_PublicKey(pk->algorithm_identifier(), pk->public_key_bits()); + }); + + auto sk = + load_persistent_ecc(result, ctx, persistent_key_id, password, session); + result.test_no_throw("Botan can read serialized public key from ECC private key", [&] { + auto sk_sw = Botan::ECDSA_PublicKey(sk->algorithm_identifier(), sk->public_key_bits()); + }); + }), + }; +} + #endif + std::vector test_tpm2_hash() { auto ctx = get_tpm2_context(__func__); if(!ctx) { @@ -787,6 +1147,9 @@ BOTAN_REGISTER_TEST_FN("tpm2", "tpm2_rng", test_tpm2_rng); #if defined(BOTAN_HAS_TPM2_RSA_ADAPTER) BOTAN_REGISTER_TEST_FN("tpm2", "tpm2_rsa", test_tpm2_rsa); #endif + #if defined(BOTAN_HAS_TPM2_ECC_ADAPTER) +BOTAN_REGISTER_TEST_FN("tpm2", "tpm2_ecc", test_tpm2_ecc); + #endif BOTAN_REGISTER_TEST_FN("tpm2", "tpm2_hash", test_tpm2_hash); #endif