From 6f1d96b6e0ec19d215fe800eb449d591362b3298 Mon Sep 17 00:00:00 2001 From: Amos Treiber Date: Thu, 5 Sep 2024 15:41:37 +0200 Subject: [PATCH] Allow using Botan for the TPM2 TSS' crypto needs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds an implementation of the tpm2-tss crypto callbacks. If enabled, Botan will be used for the client-side crypto functions to communicate with the TPM. This lets applications shed a transitive dependency on another crypto library (like OpenSSL or mbedTLS). The crypto callbacks are available in tpm2-tss 4.0 and later. Before that, calling TPM2::Context::use_botan_crypto_backend() will result in an exception. Co-Authored-By: René Meusel --- src/lib/prov/tpm2/tpm2_context.cpp | 42 + src/lib/prov/tpm2/tpm2_context.h | 33 + .../prov/tpm2/tpm2_crypto_backend/info.txt | 20 + .../tpm2_crypto_backend.cpp | 802 ++++++++++++++++++ .../tpm2_crypto_backend/tpm2_crypto_backend.h | 40 + src/tests/test_tpm2.cpp | 32 +- 6 files changed, 968 insertions(+), 1 deletion(-) create mode 100644 src/lib/prov/tpm2/tpm2_crypto_backend/info.txt create mode 100644 src/lib/prov/tpm2/tpm2_crypto_backend/tpm2_crypto_backend.cpp create mode 100644 src/lib/prov/tpm2/tpm2_crypto_backend/tpm2_crypto_backend.h diff --git a/src/lib/prov/tpm2/tpm2_context.cpp b/src/lib/prov/tpm2/tpm2_context.cpp index 92c1f1f06a..28521bed20 100644 --- a/src/lib/prov/tpm2/tpm2_context.cpp +++ b/src/lib/prov/tpm2/tpm2_context.cpp @@ -21,6 +21,10 @@ #include #include +#if defined(BOTAN_HAS_TPM2_CRYPTO_BACKEND) + #include +#endif + namespace Botan::TPM2 { namespace { @@ -32,8 +36,20 @@ constexpr TPM2_HANDLE storage_root_key_handle = TPM2_HR_PERSISTENT + 1; struct Context::Impl { TSS2_TCTI_CONTEXT* m_tcti_ctx; ESYS_CONTEXT* m_ctx; + +#if defined(BOTAN_HAS_TPM2_CRYPTO_BACKEND) + std::unique_ptr m_crypto_callback_state; +#endif }; +bool Context::supports_botan_crypto_backend() noexcept { +#if defined(BOTAN_TSS2_SUPPORTS_CRYPTO_CALLBACKS) and defined(BOTAN_HAS_TPM2_CRYPTO_BACKEND) + return true; +#else + return false; +#endif +} + std::shared_ptr Context::create(const std::string& tcti_nameconf) { // We cannot std::make_shared as the constructor is private return std::shared_ptr(new Context(tcti_nameconf.c_str())); @@ -61,6 +77,32 @@ Context::Context(const char* tcti_name, const char* tcti_conf) : m_impl(std::mak BOTAN_ASSERT_NONNULL(m_impl->m_ctx); } +void Context::use_botan_crypto_backend(const std::shared_ptr& rng) { +#if defined(BOTAN_HAS_TPM2_CRYPTO_BACKEND) + BOTAN_STATE_CHECK(!uses_botan_crypto_backend()); + m_impl->m_crypto_callback_state = std::make_unique(rng); + enable_crypto_callbacks(shared_from_this()); +#else + BOTAN_UNUSED(rng); + throw Not_Implemented("This build of botan does not provide the TPM2 crypto backend"); +#endif +} + +bool Context::uses_botan_crypto_backend() const noexcept { +#if defined(BOTAN_HAS_TPM2_CRYPTO_BACKEND) + return m_impl->m_crypto_callback_state != nullptr; +#else + return false; +#endif +} + +#if defined(BOTAN_HAS_TPM2_CRYPTO_BACKEND) +CryptoCallbackState& Context::crypto_callback_state() { + BOTAN_ASSERT_NONNULL(m_impl->m_crypto_callback_state); + return *m_impl->m_crypto_callback_state; +} +#endif + ESYS_CONTEXT* Context::esys_context() noexcept { return m_impl->m_ctx; } diff --git a/src/lib/prov/tpm2/tpm2_context.h b/src/lib/prov/tpm2/tpm2_context.h index 879903fe8d..522640e18b 100644 --- a/src/lib/prov/tpm2/tpm2_context.h +++ b/src/lib/prov/tpm2/tpm2_context.h @@ -26,6 +26,8 @@ struct ESYS_CONTEXT; namespace Botan::TPM2 { +struct CryptoCallbackState; + class PrivateKey; class SessionBundle; @@ -57,6 +59,32 @@ class BOTAN_PUBLIC_API(3, 6) Context final : public std::enable_shared_from_this Context& operator=(const Context&) = delete; Context& operator=(Context&& ctx) noexcept = default; + /** + * Overrides the TSS2's crypto callbacks with Botan's functionality. + * + * This replaces all cryptographic functionality required for the + * communication with the TPM by botan's implementations. The TSS2 + * would otherwise use OpenSSL or mbedTLS. + * + * Note that the provided @p rng should not be dependent on the TPM. + * + * @param rng the RNG to use for the crypto operations + * @throws Not_Implemented if the TPM2-TSS does not support crypto callbacks + * @sa supports_botan_crypto_backend() + */ + void use_botan_crypto_backend(const std::shared_ptr& rng); + + /** + * Checks if the TSS2 supports registering Botan's crypto backend at runtime. + * Older versions of the TSS2 do not support this feature ( 4.0.0), also + * Botan may be compiled without support for TSS' crypto backend. + * @return true if the TSS2 supports Botan's crypto backend + */ + static bool supports_botan_crypto_backend() noexcept; + + /// @returns true if botan is used for the TSS' crypto functions + bool uses_botan_crypto_backend() const noexcept; + /// @return an ESYS_CONTEXT* for use in other TPM2 functions. ESYS_CONTEXT* esys_context() noexcept; @@ -105,6 +133,11 @@ class BOTAN_PUBLIC_API(3, 6) Context final : public std::enable_shared_from_this Context(const char* tcti_nameconf); Context(const char* tcti_name, const char* tcti_conf); +#if defined(BOTAN_HAS_TPM2_CRYPTO_BACKEND) + friend void enable_crypto_callbacks(const std::shared_ptr&); + CryptoCallbackState& crypto_callback_state(); +#endif + private: struct Impl; // PImpl to avoid TPM2-TSS includes in this header std::unique_ptr m_impl; diff --git a/src/lib/prov/tpm2/tpm2_crypto_backend/info.txt b/src/lib/prov/tpm2/tpm2_crypto_backend/info.txt new file mode 100644 index 0000000000..065269fab1 --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_crypto_backend/info.txt @@ -0,0 +1,20 @@ + +TPM2_CRYPTO_BACKEND -> 20240806 + + + +name -> "TPM2 Crypto Backend" +brief -> "Implementation of the TPM2-TSS crypto callbacks" + + + +hash +hmac +modes +pk_pad +eme_raw + + + +tpm2_crypto_backend.h + 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 new file mode 100644 index 0000000000..102594858b --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_crypto_backend/tpm2_crypto_backend.cpp @@ -0,0 +1,802 @@ +/* +* TPM 2 TSS crypto callbacks backend +* (C) 2024 Jack Lloyd +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity 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 + +#if defined(BOTAN_HAS_TPM2_RSA_ADAPTER) + #include +#endif + +#include + +#include + +#if defined(BOTAN_TSS2_SUPPORTS_CRYPTO_CALLBACKS) + +namespace { + +/// Holds the hash state between update callback invocations +using DigestObject = + std::variant, std::unique_ptr>; + +} // namespace + +extern "C" { + +/** + * Some ESYS crypto callbacks require to hold state between calls. + * This struct is forward-declared in tss2_esys.h and we're implementing it here. + */ +typedef struct ESYS_CRYPTO_CONTEXT_BLOB { + DigestObject ctx; +} DigestCallbackState; + +} // extern "C" + +namespace { + +/// Safely converts the @p blob to a Botan crypto object of type @p T. +template + requires std::constructible_from> +[[nodiscard]] std::optional> get(DigestCallbackState* blob) noexcept { + if(!blob) { + return std::nullopt; + } + + if(!std::holds_alternative>(blob->ctx)) { + return std::nullopt; + } + + return {std::ref(*std::get>(blob->ctx))}; +} + +/// Safely converts the @p userdata to the Botan crypto context object. +[[nodiscard]] std::optional> get(void* userdata) noexcept { + if(!userdata) { + return std::nullopt; + } + + auto ccs = reinterpret_cast(userdata); + if(!ccs) { + return std::nullopt; + } + + return *ccs; +} + +/** + * Wraps the Botan-specific implementations of the TSS crypto callbacks into a + * try-catch block and converts encountered exceptions to TSS2_RC error codes as + * needed. + */ +template F> + requires std::same_as, TSS2_RC> +[[nodiscard]] TSS2_RC thunk(F&& f) noexcept { + try { + return f(); + } catch(const Botan::Invalid_Argument&) { + return TSS2_ESYS_RC_BAD_VALUE; + } catch(const Botan::Invalid_State&) { + return TSS2_ESYS_RC_BAD_SEQUENCE; + } catch(const Botan::Lookup_Error&) { + return TSS2_ESYS_RC_NOT_IMPLEMENTED; + } catch(const Botan::Invalid_Authentication_Tag&) { + return TSS2_ESYS_RC_MALFORMED_RESPONSE; + } catch(const Botan::Exception&) { + return TSS2_ESYS_RC_GENERAL_FAILURE; + } catch(...) { + return TSS2_ESYS_RC_GENERAL_FAILURE; + } +} + +/** + * Encrypts or decrypts @p data using the symmetric cipher specified. + * The bytes in @p data are encrypted/decrypted in-place. + */ +[[nodiscard]] TSS2_RC symmetric_algo(Botan::Cipher_Dir direction, + 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 { + return thunk([&] { + if(!key) { + return (direction == Botan::Cipher_Dir::Encryption) ? TSS2_ESYS_RC_NO_ENCRYPT_PARAM + : TSS2_ESYS_RC_NO_DECRYPT_PARAM; + } + + const auto cipher_name = Botan::TPM2::cipher_tss2_to_botan({ + .algorithm = tpm_sym_alg, + .keyBits = {.sym = key_bits}, + .mode = {.sym = tpm_mode}, + }); + if(!cipher_name) { + return TSS2_ESYS_RC_NOT_SUPPORTED; + } + + auto cipher = Botan::Cipher_Mode::create(cipher_name.value(), direction); + if(!cipher) { + return TSS2_ESYS_RC_NOT_IMPLEMENTED; + } + + // AEADs aren't supported by the crypto callback API, as there's + // no way to append the authentication tag to the ciphertext. + if(cipher->authenticated()) { + return TSS2_ESYS_RC_INSUFFICIENT_BUFFER; + } + + const size_t keylength = static_cast(key_bits) / 8; + if(!cipher->valid_keylength(keylength)) { + return TSS2_ESYS_RC_BAD_VALUE; + } + + const auto s_key = std::span{key, keylength}; + const auto s_iv = [&]() -> std::span { + if(iv) { + return {iv, cipher->default_nonce_length()}; + } else { + return {}; + } + }(); + + cipher->set_key(s_key); + cipher->start(s_iv); + cipher->process(data); + return TSS2_RC_SUCCESS; + }); +} + +} // namespace + +extern "C" { + +/** Provide the context for the computation of a hash digest. + * + * The context will be created and initialized according to the hash function. + * @param[out] context The created context (callee-allocated). + * @param[in] hash_alg The hash algorithm for the creation of the context. + * @param[in,out] userdata information. + * @retval TSS2_RC_SUCCESS on success. + * @retval USER_DEFINED user defined errors on failure. + */ +static TSS2_RC hash_start(ESYS_CRYPTO_CONTEXT_BLOB** context, TPM2_ALG_ID hash_alg, void* userdata) { + BOTAN_UNUSED(userdata); + return thunk([&] { + const auto hash_name = Botan::TPM2::hash_algo_tss2_to_botan(hash_alg); + if(!hash_name) { + return TSS2_ESYS_RC_NOT_SUPPORTED; + } + + auto hash = Botan::HashFunction::create(hash_name.value()); + if(!hash) { + return TSS2_ESYS_RC_NOT_IMPLEMENTED; + } + + *context = new DigestCallbackState{std::move(hash)}; + return TSS2_RC_SUCCESS; + }); +} + +/** Update the digest value of a digest object from a byte buffer. + * + * The context of a digest object will be updated according to the hash + * algorithm of the context. < + * @param[in,out] context The context of the digest object which will be updated. + * @param[in] buffer The data for the update. + * @param[in] size The size of the data buffer. + * @param[in,out] userdata information. + * @retval TSS2_RC_SUCCESS on success. + * @retval USER_DEFINED user defined errors on failure. + */ +static TSS2_RC hash_update(ESYS_CRYPTO_CONTEXT_BLOB* context, const uint8_t* buffer, size_t size, void* userdata) { + BOTAN_UNUSED(userdata); + return thunk([&] { + const auto hash = get(context); + if(!hash) { + return TSS2_ESYS_RC_BAD_REFERENCE; + } + + hash->get().update(std::span{buffer, size}); + return TSS2_RC_SUCCESS; + }); +} + +/** Get the digest value of a digest object and close the context. + * + * The digest value will written to a passed buffer and the resources of the + * digest object are released. + * @param[in,out] context The context of the digest object to be released + * @param[out] buffer The buffer for the digest value (caller-allocated). + * @param[out] size The size of the digest. + * @param[in,out] userdata information. + * @retval TSS2_RC_SUCCESS on success. + * @retval USER_DEFINED user defined errors on failure. + */ +static TSS2_RC hash_finish(ESYS_CRYPTO_CONTEXT_BLOB** context, uint8_t* buffer, size_t* size, void* userdata) { + BOTAN_UNUSED(userdata); + return thunk([&] { + auto hash = get(*context); + if(!hash) { + return TSS2_ESYS_RC_BAD_REFERENCE; + } + + *size = hash->get().output_length(); + hash->get().final(std::span{buffer, *size}); + + delete *context; + *context = nullptr; + return TSS2_RC_SUCCESS; + }); +} + +/** Release the resources of a digest object. + * + * The assigned resources will be released and the context will be set to NULL. + * @param[in,out] context The context of the digest object. + * @param[in,out] userdata information. + */ +static void hash_abort(ESYS_CRYPTO_CONTEXT_BLOB** context, void* userdata) { + BOTAN_UNUSED(userdata); + delete *context; + *context = nullptr; +} + +/** Provide the context an HMAC digest object from a byte buffer key. + * + * The context will be created and initialized according to the hash function + * and the used HMAC key. + * @param[out] context The created context (callee-allocated). + * @param[in] hash_alg The hash algorithm for the HMAC computation. + * @param[in] key The byte buffer of the HMAC key. + * @param[in] size The size of the HMAC key. + * @param[in,out] userdata information. + * @retval TSS2_RC_SUCCESS on success. + * @retval USER_DEFINED user defined errors on failure. + */ +static 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([&] { + const auto hash_name = Botan::TPM2::hash_algo_tss2_to_botan(hash_alg); + if(!hash_name) { + return TSS2_ESYS_RC_NOT_SUPPORTED; + } + + auto hmac = Botan::MessageAuthenticationCode::create(Botan::fmt("HMAC({})", hash_name.value())); + if(!hmac) { + return TSS2_ESYS_RC_NOT_IMPLEMENTED; + } + + hmac->set_key(std::span{key, size}); + + *context = new DigestCallbackState{std::move(hmac)}; + return TSS2_RC_SUCCESS; + }); +} + +/** Update and HMAC digest value from a byte buffer. + * + * The context of a digest object will be updated according to the hash + * algorithm and the key of the context. + * @param[in,out] context The context of the digest object which will be updated. + * @param[in] buffer The data for the update. + * @param[in] size The size of the data buffer. + * @param[in,out] userdata information. + * @retval TSS2_RC_SUCCESS on success. + * @retval USER_DEFINED user defined errors on failure. + */ +static TSS2_RC hmac_update(ESYS_CRYPTO_CONTEXT_BLOB* context, const uint8_t* buffer, size_t size, void* userdata) { + BOTAN_UNUSED(userdata); + return thunk([&] { + auto hmac = get(context); + if(!hmac) { + return TSS2_ESYS_RC_BAD_REFERENCE; + } + + hmac->get().update(std::span{buffer, size}); + return TSS2_RC_SUCCESS; + }); +} + +/** Write the HMAC digest value to a byte buffer and close the context. + * + * The digest value will written to a passed buffer and the resources of the + * HMAC object are released. + * @param[in,out] context The context of the HMAC object. + * @param[out] buffer The buffer for the digest value (caller-allocated). + * @param[out] size The size of the digest. + * @param[in,out] userdata information. + * @retval TSS2_RC_SUCCESS on success. + * @retval USER_DEFINED user defined errors on failure. + */ +static TSS2_RC hmac_finish(ESYS_CRYPTO_CONTEXT_BLOB** context, uint8_t* buffer, size_t* size, void* userdata) { + BOTAN_UNUSED(userdata); + return thunk([&] { + auto hmac = get(*context); + if(!hmac) { + return TSS2_ESYS_RC_BAD_REFERENCE; + } + + *size = hmac->get().output_length(); + hmac->get().final(std::span{buffer, *size}); + + delete *context; + *context = nullptr; + return TSS2_RC_SUCCESS; + }); +} + +/** Release the resources of an HMAC object. + * + * The assigned resources will be released and the context will be set to NULL. + * @param[in,out] context The context of the HMAC object. + * @param[in,out] userdata information. + */ +static void hmac_abort(ESYS_CRYPTO_CONTEXT_BLOB** context, void* userdata) { + BOTAN_UNUSED(userdata); + delete *context; + *context = nullptr; +} + +/** Compute random TPM2B data. + * + * The random data will be generated and written to a passed TPM2B structure. + * @param[out] nonce The TPM2B structure for the random data (caller-allocated). + * @param[in] num_bytes The number of bytes to be generated. + * @param[in,out] userdata information. + * @retval TSS2_RC_SUCCESS on success. + * @retval USER_DEFINED user defined errors on failure. + * @note: the TPM should not be used to obtain the random data + */ +static TSS2_RC get_random2b(TPM2B_NONCE* nonce, size_t num_bytes, void* userdata) { + return thunk([&] { + auto ccs = get(userdata); + if(!ccs) { + return TSS2_ESYS_RC_BAD_REFERENCE; + } + + nonce->size = num_bytes; + ccs->get().rng->randomize(Botan::TPM2::as_span(*nonce)); + return TSS2_RC_SUCCESS; + }); +} + +/** Encryption of a buffer using a public (RSA) key. + * + * Encrypting a buffer using a public key is used for example during + * Esys_StartAuthSession in order to encrypt the salt value. + * @param[in] pub_tpm_key The key to be used for encryption. + * @param[in] in_size The size of the buffer to be encrypted. + * @param[in] in_buffer The data buffer to be encrypted. + * @param[in] max_out_size The maximum size for the output encrypted buffer. + * @param[out] out_buffer The encrypted buffer. + * @param[out] out_size The size of the encrypted output. + * @param[in] label The label used in the encryption scheme. + * @param[in,out] userdata information. + * @retval TSS2_RC_SUCCESS on success + * @retval USER_DEFINED user defined errors on failure. + */ +static TSS2_RC rsa_pk_encrypt(TPM2B_PUBLIC* pub_tpm_key, + size_t in_size, + BYTE* in_buffer, + size_t max_out_size, + BYTE* out_buffer, + size_t* out_size, + const char* label, + void* userdata) { + // TODO: This is currently a dumpster fire of code duplication and + // YOLO manual padding. + // + // I'm hoping that a follow-up of Jack's work will help clean + // this up. See the extensive discussions in: + // + // https://github.com/randombit/botan/pull/4318#issuecomment-2297682058 + + #if defined(BOTAN_HAS_TPM2_RSA_ADAPTER) + auto create_eme = [&]( + const TPMT_RSA_SCHEME& scheme, + [[maybe_unused]] TPM2_ALG_ID name_algo, + [[maybe_unused]] TPMU_ASYM_SCHEME scheme_detail) -> std::optional> { + // OAEP is more complex by requiring a hash function and an optional + // label. To avoid marshalling this into Botan's algorithm descriptor + // we create an OAEP instance manually. + auto create_oaep = [&]() -> std::optional> { + #if defined(BOTAN_HAS_EME_OAEP) + // TPM Library, Part 1: Architecture, Annex B.4 + // The RSA key's scheme hash algorithm (or, if it is TPM_ALG_NULL, + // the RSA key's Name algorithm) is used to compute H(label). + const auto label_hash = Botan::TPM2::hash_algo_tss2_to_botan( + (scheme_detail.oaep.hashAlg == TPM2_ALG_NULL || scheme_detail.oaep.hashAlg == TPM2_ALG_ERROR) + ? pub_tpm_key->publicArea.nameAlg + : scheme_detail.oaep.hashAlg); + + // TPM Library, Part 1: Architecture, Annex B.4 + // The mask-generation function uses the Name algorithm of the RSA + // key as the hash algorithm. + const auto mgf1_hash = Botan::TPM2::hash_algo_tss2_to_botan(name_algo); + if(!label_hash || !mgf1_hash) { + return std::nullopt; // -> not supported + } + + auto H_label = Botan::HashFunction::create(label_hash.value()); + auto H_mgf1 = Botan::HashFunction::create(mgf1_hash.value()); + if(!H_label || !H_mgf1) { + return nullptr; // -> not implemented + } + + // TPM Library, Part 1: Architecture, Annex B.4 + // [...] is used to compute lhash := H(label), and the null + // termination octet is included in the digest. + std::string_view label_with_zero_terminator{label, std::strlen(label) + 1}; + return std::make_unique(std::move(H_label), std::move(H_mgf1), label_with_zero_terminator); + #else + BOTAN_UNUSED(label); + return nullptr; // -> not implemented + #endif + }; + + try { // EME::create throws if algorithm is not available + switch(scheme.scheme) { + case TPM2_ALG_OAEP: + return create_oaep(); + case TPM2_ALG_NULL: + return Botan::EME::create("Raw"); + case TPM2_ALG_RSAES: + return Botan::EME::create("PKCS1v15"); + default: + return std::nullopt; // -> not supported + } + } catch(const Botan::Algorithm_Not_Found&) { + /* ignore */ + } + + return nullptr; // -> not implemented (EME::create() threw) + }; + + return thunk([&] { + BOTAN_ASSERT_NONNULL(pub_tpm_key); + BOTAN_ASSERT_NOMSG(pub_tpm_key->publicArea.type == TPM2_ALG_RSA); + + const auto maybe_eme = create_eme(pub_tpm_key->publicArea.parameters.rsaDetail.scheme, + pub_tpm_key->publicArea.nameAlg, + pub_tpm_key->publicArea.parameters.rsaDetail.scheme.details); + if(!maybe_eme.has_value()) { + return TSS2_ESYS_RC_NOT_SUPPORTED; + } + + const auto& eme = maybe_eme.value(); + if(!eme) { + return TSS2_ESYS_RC_NOT_IMPLEMENTED; + } + + // The code below is duplicated logic with Botan's PK_Encryptor_EME. + // Currently, there's no way to instantiate the encryptor without + // marshalling the optional `label` into Botan's algorithm name. + // + // The label contains characters that are not allowed in Botan's string- + // based algorithm names, namely the \0 terminator. We currently handle + // the padding manually and then encrypt the padded data with raw RSA. + // + // TODO: Provide a way to instantiate an PK_Encryptor_EME that accepts a + // pre-made EME object. See: https://github.com/randombit/botan/pull/4318 + + const auto pubkey = Botan::TPM2::rsa_pubkey_from_tss2_public(pub_tpm_key); + const auto keybits = pubkey.key_length(); + const auto output_size = keybits / 8; + if(eme->maximum_input_size(keybits) < in_size) { + return TSS2_ESYS_RC_BAD_VALUE; + } + + if(output_size > max_out_size) { + 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); + + // PK_Encryptor_EME does not provide a way to pass in an output buffer. + // 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); + + // 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); + + return TSS2_RC_SUCCESS; + }); + #else + BOTAN_UNUSED(pub_tpm_key, in_size, in_buffer, max_out_size, out_buffer, out_size, label, userdata); + return TSS2_ESYS_RC_NOT_IMPLEMENTED; + #endif +} + +/** Computation of an ephemeral ECC key and shared secret Z. + * + * According to the description in TPM spec part 1 C 6.1 a shared secret + * between application and TPM is computed (ECDH). An ephemeral ECC key and a + * TPM key are used for the ECDH key exchange. + * @param[in] key The key to be used for ECDH key exchange. + * @param[in] max_out_size the max size for the output of the public key of the + * computed ephemeral key. + * @param[out] Z The computed shared secret. + * @param[out] Q The public part of the ephemeral key in TPM format. + * @param[out] out_buffer The public part of the ephemeral key will be marshaled + * to this buffer. + * @param[out] out_size The size of the marshaled output. + * @param[in,out] userdata information. + * @retval TSS2_RC_SUCCESS on success + * @retval USER_DEFINED user defined errors on failure. + */ +static TSS2_RC get_ecdh_point(TPM2B_PUBLIC* key, + size_t max_out_size, + TPM2B_ECC_PARAMETER* Z, + TPMS_ECC_POINT* Q, + 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. + return TSS2_ESYS_RC_NOT_IMPLEMENTED; +} + +/** Encrypt data with AES. + * + * @param[in] key key used for AES. + * @param[in] tpm_sym_alg AES type in TSS2 notation (must be TPM2_ALG_AES). + * @param[in] key_bits Key size in bits. + * @param[in] tpm_mode Block cipher mode of opertion in TSS2 notation (CFB). + * For parameter encryption only CFB can be used. + * @param[in,out] buffer Data to be encrypted. The encrypted date will be stored + * in this buffer. + * @param[in] buffer_size size of data to be encrypted. + * @param[in] iv The initialization vector. + * @param[in,out] userdata information. + * @retval TSS2_RC_SUCCESS on success + * @retval USER_DEFINED user defined errors on failure. + */ +static TSS2_RC aes_encrypt(uint8_t* key, + TPM2_ALG_ID tpm_sym_alg, + TPMI_AES_KEY_BITS key_bits, + TPM2_ALG_ID tpm_mode, + uint8_t* buffer, + size_t buffer_size, + uint8_t* iv, + void* userdata) { + BOTAN_UNUSED(userdata); + if(tpm_sym_alg != TPM2_ALG_AES) { + 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}); +} + +/** Decrypt data with AES. + * + * @param[in] key key used for AES. + * @param[in] tpm_sym_alg AES type in TSS2 notation (must be TPM2_ALG_AES). + * @param[in] key_bits Key size in bits. + * @param[in] tpm_mode Block cipher mode of opertion in TSS2 notation (CFB). + * For parameter encryption only CFB can be used. + * @param[in,out] buffer Data to be decrypted. The decrypted date will be stored + * in this buffer. + * @param[in] buffer_size size of data to be encrypted. + * @param[in] iv The initialization vector. + * @param[in,out] userdata information. + * @retval TSS2_RC_SUCCESS on success + * @retval USER_DEFINED user defined errors on failure. + */ +static TSS2_RC aes_decrypt(uint8_t* key, + TPM2_ALG_ID tpm_sym_alg, + TPMI_AES_KEY_BITS key_bits, + TPM2_ALG_ID tpm_mode, + uint8_t* buffer, + size_t buffer_size, + uint8_t* iv, + void* userdata) { + BOTAN_UNUSED(userdata); + if(tpm_sym_alg != TPM2_ALG_AES) { + 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}); +} + + #if defined(BOTAN_TSS2_SUPPORTS_SM4_IN_CRYPTO_CALLBACKS) + +/** Encrypt data with SM4. + * + * @param[in] key key used for SM4. + * @param[in] tpm_sym_alg SM4 type in TSS2 notation (must be TPM2_ALG_SM4). + * @param[in] key_bits Key size in bits. + * @param[in] tpm_mode Block cipher mode of opertion in TSS2 notation (CFB). + * For parameter encryption only CFB can be used. + * @param[in,out] buffer Data to be encrypted. The encrypted date will be stored + * in this buffer. + * @param[in] buffer_size size of data to be encrypted. + * @param[in] iv The initialization vector. + * @param[in,out] userdata information. + * @retval TSS2_RC_SUCCESS on success + * @retval USER_DEFINED user defined errors on failure. + */ +static TSS2_RC sm4_encrypt(uint8_t* key, + TPM2_ALG_ID tpm_sym_alg, + TPMI_SM4_KEY_BITS key_bits, + TPM2_ALG_ID tpm_mode, + uint8_t* buffer, + size_t buffer_size, + uint8_t* iv, + void* userdata) { + BOTAN_UNUSED(userdata); + if(tpm_sym_alg != TPM2_ALG_SM4) { + 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}); +} + +/** Decrypt data with SM4. + * + * @param[in] key key used for SM4. + * @param[in] tpm_sym_alg SM4 type in TSS2 notation (must be TPM2_ALG_SM4). + * @param[in] key_bits Key size in bits. + * @param[in] tpm_mode Block cipher mode of opertion in TSS2 notation (CFB). + * For parameter encryption only CFB can be used. + * @param[in,out] buffer Data to be decrypted. The decrypted date will be stored + * in this buffer. + * @param[in] buffer_size size of data to be encrypted. + * @param[in] iv The initialization vector. + * @param[in,out] userdata information. + * @retval TSS2_RC_SUCCESS on success + * @retval USER_DEFINED user defined errors on failure. + */ +static TSS2_RC sm4_decrypt(uint8_t* key, + TPM2_ALG_ID tpm_sym_alg, + TPMI_SM4_KEY_BITS key_bits, + TPM2_ALG_ID tpm_mode, + uint8_t* buffer, + size_t buffer_size, + uint8_t* iv, + void* userdata) { + BOTAN_UNUSED(userdata); + if(tpm_sym_alg != TPM2_ALG_SM4) { + 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}); +} + + #endif /* TPM2_ALG_SM4 */ + +/** Initialize crypto backend. + * + * Initialize internal tables of crypto backend. + * + * @param[in,out] userdata Optional userdata pointer. + * + * @retval TSS2_RC_SUCCESS ong success. + * @retval USER_DEFINED user defined errors on failure. + */ +static TSS2_RC init(void* userdata) { + // No dedicated initialization required. Just check if the userdata is valid. + auto ccs = get(userdata); + if(!ccs) { + return TSS2_ESYS_RC_BAD_REFERENCE; + } + if(!ccs->get().rng) { + return TSS2_ESYS_RC_BAD_SEQUENCE; + } + return TSS2_RC_SUCCESS; +} + +} // extern "C" + +#endif /* BOTAN_TSS2_SUPPORTS_CRYPTO_CALLBACKS */ + +namespace Botan::TPM2 { + +/** + * Enable the Botan crypto callbacks for the given ESYS context. + * + * The callbacks may maintain two types of state: + * + * * 'userdata' is a pointer to a CryptoCallbackState object that is passed + * to all callback functions. This provides access to a random + * number generator specified by the user. + * The lifetime of this object is bound to the TPM2::Context. + * + * * 'context' is a pointer to a DigestCallbackState object that contains + * either a HashFunction or a MessageAuthenticationCode object. + * This holds the hash state between update callback invocations. + * The lifetime of this object is bound to the digest callbacks, + * hence *_finish() and *_abort() will delete the object. + * + * The runtime crypto backend is available since TSS2 4.0.0 and later. Explicit + * support for SM4 was added in TSS2 4.1.0. + * + * Error code conventions: + * + * * TSS2_ESYS_RC_BAD_REFERENCE: reference (typically userdata) invalid + * * TSS2_ESYS_RC_NOT_SUPPORTED: algorithm identifier not mapped to Botan + * * TSS2_ESYS_RC_NOT_IMPLEMENTED: algorithm not available (e.g. disabled) + */ +void enable_crypto_callbacks(const std::shared_ptr& ctx) { +#if defined(BOTAN_TSS2_SUPPORTS_CRYPTO_CALLBACKS) + BOTAN_ASSERT_NONNULL(ctx); + + // clang-format off + ESYS_CRYPTO_CALLBACKS callbacks{ + .rsa_pk_encrypt = &rsa_pk_encrypt, + .hash_start = &hash_start, + .hash_update = &hash_update, + .hash_finish = &hash_finish, + .hash_abort = &hash_abort, + .hmac_start = &hmac_start, + .hmac_update = &hmac_update, + .hmac_finish = &hmac_finish, + .hmac_abort = &hmac_abort, + .get_random2b = &get_random2b, + .get_ecdh_point = &get_ecdh_point, + .aes_encrypt = &aes_encrypt, + .aes_decrypt = &aes_decrypt, + .init = &init, + .userdata = &ctx->crypto_callback_state(), +#if defined(BOTAN_TSS2_SUPPORTS_SM4_IN_CRYPTO_CALLBACKS) + .sm4_encrypt = &sm4_encrypt, + .sm4_decrypt = &sm4_decrypt, +#endif + }; + // clang-format on + + check_rc("Esys_SetCryptoCallbacks", Esys_SetCryptoCallbacks(*ctx, &callbacks)); +#else + BOTAN_UNUSED(ctx); + throw Not_Implemented( + "This build of botan was compiled with a TSS2 version lower than 4.0.0, " + "which does not support custom runtime crypto backends"); +#endif +} + +} // namespace Botan::TPM2 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 new file mode 100644 index 0000000000..952368cb9b --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_crypto_backend/tpm2_crypto_backend.h @@ -0,0 +1,40 @@ +/* +* TPM 2 TSS crypto callbacks backend +* (C) 2024 Jack Lloyd +* (C) 2024 René Meusel, Amos Treiber - Rohde & Schwarz Cybersecurity GmbH +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_TPM2_CRYPTO_BACKEND_H_ +#define BOTAN_TPM2_CRYPTO_BACKEND_H_ + +#include + +namespace Botan { +class RandomNumberGenerator; +} + +namespace Botan::TPM2 { + +class Context; + +/** + * This state object is available to all crypto callbacks. + * Its lifetime is managed by the TPM2::Context. + */ +struct CryptoCallbackState { + CryptoCallbackState(std::shared_ptr rng_in) : rng(std::move(rng_in)) {} + + std::shared_ptr rng; // NOLINT(misc-non-private-member-variables-in-classes) +}; + +/** + * Enable Botan's crypto callbacks in the TPM2-TSS for the given @p context. + * @throws Not_Implemented if the TPM2-TSS does not support crypto callbacks. + */ +void enable_crypto_callbacks(const std::shared_ptr& context); + +} // namespace Botan::TPM2 + +#endif diff --git a/src/tests/test_tpm2.cpp b/src/tests/test_tpm2.cpp index 8183b25fd1..02f1ced36b 100644 --- a/src/tests/test_tpm2.cpp +++ b/src/tests/test_tpm2.cpp @@ -31,6 +31,12 @@ namespace Botan_Tests { #if defined(BOTAN_HAS_TPM2) namespace { + #if defined(BOTAN_HAS_TPM2_CRYPTO_BACKEND) && defined(BOTAN_TSS2_SUPPORTS_CRYPTO_CALLBACKS) +constexpr bool crypto_backend_should_be_available = true; + #else +constexpr bool crypto_backend_should_be_available = false; + #endif + std::shared_ptr get_tpm2_context(std::string_view rng_tag) { const auto tcti_name = Test::options().tpm2_tcti_name(); if(tcti_name.value() == "disabled") { @@ -43,7 +49,9 @@ std::shared_ptr get_tpm2_context(std::string_view rng_tag) return {}; } - BOTAN_UNUSED(rng_tag); + if(ctx->supports_botan_crypto_backend()) { + ctx->use_botan_crypto_backend(Test::new_rng(rng_tag)); + } return ctx; } @@ -133,6 +141,28 @@ std::vector test_tpm2_context() { !Botan::value_exists(handles, persistent_key_id + 1)); }), + CHECK("Crypto backend", + [&](Test::Result& result) { + const bool backend_supported = ctx->supports_botan_crypto_backend(); + const bool backend_used = ctx->uses_botan_crypto_backend(); + result.require("Crypto backend availability", + backend_supported == crypto_backend_should_be_available); + result.require("Crypto backend is used in the tests, if it is available", + backend_used == backend_supported); + + if(backend_used) { + result.test_throws( + "If the backend is already in use, we cannot enable it once more", + [&] { ctx->use_botan_crypto_backend(Test::new_rng(__func__)); }); + } + + if(!backend_supported) { + result.test_throws( + "If the backend is not supported, we cannot enable it", + [&] { ctx->use_botan_crypto_backend(Test::new_rng(__func__)); }); + } + }), + // 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) {