From d3ad7899a1f3c788080bafee733bd8ccbd3b9d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Meusel?= Date: Thu, 5 Sep 2024 10:59:25 +0200 Subject: [PATCH 1/7] Add helpers to interact with C-based APIs Co-Authored-By: Amos Treiber --- src/lib/utils/stl_util.h | 51 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/lib/utils/stl_util.h b/src/lib/utils/stl_util.h index 9356b2db5a3..be100ee42aa 100644 --- a/src/lib/utils/stl_util.h +++ b/src/lib/utils/stl_util.h @@ -415,6 +415,57 @@ auto to_underlying(T e) noexcept { return static_cast>(e); } +// TODO: C++23 - use std::out_ptr +template +[[nodiscard]] constexpr auto out_ptr(T& outptr) noexcept { + class out_ptr_t { + public: + constexpr ~out_ptr_t() noexcept { + m_ptr.reset(m_rawptr); + m_rawptr = nullptr; + } + + constexpr out_ptr_t(T& outptr) noexcept : m_ptr(outptr), m_rawptr(nullptr) {} + + out_ptr_t(const out_ptr_t&) = delete; + out_ptr_t(out_ptr_t&&) = delete; + out_ptr_t& operator=(const out_ptr_t&) = delete; + out_ptr_t& operator=(out_ptr_t&&) = delete; + + [[nodiscard]] constexpr operator typename T::element_type **() && noexcept { return &m_rawptr; } + + private: + T& m_ptr; + typename T::element_type* m_rawptr; + }; + + return out_ptr_t{outptr}; +} + +template + requires std::is_default_constructible_v +[[nodiscard]] constexpr auto out_opt(std::optional& outopt) noexcept { + class out_opt_t { + public: + constexpr ~out_opt_t() noexcept { m_opt = m_raw; } + + constexpr out_opt_t(std::optional& outopt) noexcept : m_opt(outopt) {} + + out_opt_t(const out_opt_t&) = delete; + out_opt_t(out_opt_t&&) = delete; + out_opt_t& operator=(const out_opt_t&) = delete; + out_opt_t& operator=(out_opt_t&&) = delete; + + [[nodiscard]] constexpr operator T*() && noexcept { return &m_raw; } + + private: + std::optional& m_opt; + T m_raw; + }; + + return out_opt_t{outopt}; +} + } // namespace Botan #endif From 09eb13f24006fcdac4c58524402f2df6269710c3 Mon Sep 17 00:00:00 2001 From: Amos Treiber Date: Thu, 5 Sep 2024 10:57:48 +0200 Subject: [PATCH 2/7] CI and build infrastructure for TPM 2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Installs swtpm on the CI build machines to simulate a TPM 2.0 chip and adds infrastructure to pass TPM-related parameters to the unit test binary. Also adds an empty tpm2 module that can be enabled by configuring with ./configure.py --with-tpm2 Co-Authored-By: René Meusel --- .github/actions/setup-build-agent/action.yml | 5 + .github/workflows/ci.yml | 2 +- configure.py | 2 +- src/cli/argparse.h | 12 ++ src/lib/prov/tpm2/info.txt | 14 +++ src/scripts/ci/setup_gh_actions.sh | 39 ++++++- src/scripts/ci/start_tpm2_simulator.sh | 110 +++++++++++++++++++ src/scripts/ci_build.py | 18 +++ src/tests/main.cpp | 14 ++- src/tests/tests.h | 30 +++++ 10 files changed, 238 insertions(+), 8 deletions(-) create mode 100644 src/lib/prov/tpm2/info.txt create mode 100755 src/scripts/ci/start_tpm2_simulator.sh diff --git a/.github/actions/setup-build-agent/action.yml b/.github/actions/setup-build-agent/action.yml index fdc8f7fb353..b73ea56eb14 100644 --- a/.github/actions/setup-build-agent/action.yml +++ b/.github/actions/setup-build-agent/action.yml @@ -53,3 +53,8 @@ runs: run: ${{ github.action_path }}/../../../src/scripts/ci/setup_gh_actions_after_vcvars.ps1 ${{ inputs.target }} shell: pwsh if: runner.os == 'Windows' + + - name: Start TPM 2.0 Simulator + run: ${{ github.action_path }}/../../../src/scripts/ci/start_tpm2_simulator.sh "${{ github.workspace }}/tpm2_simulator_state" + shell: bash + if: runner.os != 'Windows' && env.BOTAN_TPM2_ENABLED == 'test' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51ed30da842..66ad5463f04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,7 +135,7 @@ jobs: cache-key: linux-x86_64-clang-tidy - name: Configure Build - run: python3 ./configure.py --cc=clang --build-targets=shared,cli,tests,examples,bogo_shim --build-fuzzers=test --with-boost --with-sqlite --with-zlib --with-lzma --with-bzip2 + run: python3 ./configure.py --cc=clang --build-targets=shared,cli,tests,examples,bogo_shim --build-fuzzers=test --with-boost --with-sqlite --with-zlib --with-lzma --with-bzip2 --with-tpm2 - name: Run Clang Tidy run: | diff --git a/configure.py b/configure.py index 04a50fc2f1e..4e01296ef60 100755 --- a/configure.py +++ b/configure.py @@ -582,7 +582,7 @@ def add_enable_disable_pair(group, what, default, msg=optparse.SUPPRESS_HELP): 'disable building of deprecated features and modules') # Should be derived from info.txt but this runs too early - third_party = ['boost', 'bzip2', 'lzma', 'commoncrypto', 'sqlite3', 'zlib', 'tpm'] + third_party = ['boost', 'bzip2', 'lzma', 'commoncrypto', 'sqlite3', 'zlib', 'tpm', 'tpm2'] for mod in third_party: mods_group.add_option('--with-%s' % (mod), diff --git a/src/cli/argparse.h b/src/cli/argparse.h index 83f768f16ec..e53e5b6654a 100644 --- a/src/cli/argparse.h +++ b/src/cli/argparse.h @@ -32,6 +32,8 @@ class Argument_Parser final { size_t get_arg_sz(const std::string& option) const; + size_t get_arg_hex_sz_or(const std::string& opt_name, const std::string& otherwise) const; + std::vector get_arg_list(const std::string& what) const; static std::vector split_on(const std::string& str, char delim); @@ -110,6 +112,16 @@ size_t Argument_Parser::get_arg_sz(const std::string& opt_name) const { } } +size_t Argument_Parser::get_arg_hex_sz_or(const std::string& opt_name, const std::string& otherwise) const { + const std::string s = get_arg_or(opt_name, otherwise); + + try { + return static_cast(std::stoul(s, nullptr, 16)); + } catch(std::exception&) { + throw CLI_Usage_Error("Invalid hex integer value '" + s + "' for option " + opt_name); + } +} + std::vector Argument_Parser::get_arg_list(const std::string& what) const { if(what == m_spec_rest) { return m_user_rest; diff --git a/src/lib/prov/tpm2/info.txt b/src/lib/prov/tpm2/info.txt new file mode 100644 index 00000000000..f627644c763 --- /dev/null +++ b/src/lib/prov/tpm2/info.txt @@ -0,0 +1,14 @@ + +TPM2 -> 20240610 + + + +name -> "TPM2" +brief -> "Wrappers and Utilites to interact with TPM2" + + +load_on vendor + + +all -> tss2-esys,tss2-rc,tss2-tctildr,tss2-mu + diff --git a/src/scripts/ci/setup_gh_actions.sh b/src/scripts/ci/setup_gh_actions.sh index a45d02f5f12..b06b8ebccdf 100755 --- a/src/scripts/ci/setup_gh_actions.sh +++ b/src/scripts/ci/setup_gh_actions.sh @@ -17,6 +17,30 @@ ARCH="$2" SCRIPT_LOCATION=$(cd "$(dirname "$0")"; pwd) if type -p "apt-get"; then + # TPM2-TSS library (to build the library against) + tpm2_specific_packages=("libtss2-dev") + + # Our simulated TPM 2.0 setup depends on convenience features that are + # available only in Ubuntu 24.04. Technically, most of the TPM 2.0 support + # in Botan should work on 22.04 as well. + # + # TODO: Look into whether we can use the TPM 2.0 simulator on 22.04 to be + # able to run the tests on that version as well. + if [ "$(lsb_release -sr)" = "24.04" ]; then + # Additional TPM 2.0 related packages to set up a simulated + # TPM 2.0 environment for testing. + tpm2_specific_packages+=("tpm2-tools" # CLI tools to interact with the TPM + "swtpm" # TPM 2.0 simulator + "swtpm-tools" # CLI tools to set up the TPM simulator + "tpm2-abrmd" # user-space resource manager for TPM 2.0 + "libtss2-tcti-tabrmd0") # TCTI (TPM Command Transmission Interface) for the user-space resource manager) + ci_support_of_tpm2="test" + else + # If we are not on Ubuntu 24.04, we can't set up a TPM 2.0 simulator + # and potentially just build the library with TPM 2.0 support but don't + # run the tests. + ci_support_of_tpm2="build" + fi if [ "$(lsb_release -sr)" = "22.04" ]; then # Hack to deal with https://github.com/actions/runner-images/issues/8659 @@ -33,8 +57,16 @@ if type -p "apt-get"; then # (l)ist mode (avoiding https://github.com/actions/runner-images/issues/9996) sudo NEEDRESTART_MODE=l apt-get -qq install valgrind - elif [ "$TARGET" = "shared" ] || [ "$TARGET" = "examples" ] || [ "$TARGET" = "tlsanvil" ] || [ "$TARGET" = "clang-tidy" ] ; then - sudo apt-get -qq install libboost-dev + elif [ "$TARGET" = "static" ]; then + sudo apt-get -qq install "${tpm2_specific_packages[@]}" + echo "BOTAN_TPM2_ENABLED=${ci_support_of_tpm2}" >> "$GITHUB_ENV" + + elif [ "$TARGET" = "shared" ]; then + sudo apt-get -qq install libboost-dev "${tpm2_specific_packages[@]}" + echo "BOTAN_TPM2_ENABLED=${ci_support_of_tpm2}" >> "$GITHUB_ENV" + + elif [ "$TARGET" = "examples" ] || [ "$TARGET" = "tlsanvil" ] || [ "$TARGET" = "clang-tidy" ] ; then + sudo apt-get -qq install libboost-dev libtss2-dev elif [ "$TARGET" = "clang" ]; then sudo apt-get -qq install clang @@ -119,7 +151,8 @@ if type -p "apt-get"; then curl -L https://coveralls.io/coveralls-linux.tar.gz | tar -xz -C /usr/local/bin fi - sudo apt-get -qq install softhsm2 libtspi-dev libboost-dev + sudo apt-get -qq install softhsm2 libtspi-dev libboost-dev "${tpm2_specific_packages[@]}" + echo "BOTAN_TPM2_ENABLED=${ci_support_of_tpm2}" >> "$GITHUB_ENV" echo "$HOME/.local/bin" >> "$GITHUB_PATH" diff --git a/src/scripts/ci/start_tpm2_simulator.sh b/src/scripts/ci/start_tpm2_simulator.sh new file mode 100755 index 00000000000..7649ea58e17 --- /dev/null +++ b/src/scripts/ci/start_tpm2_simulator.sh @@ -0,0 +1,110 @@ +#/bin/bash + +# +# Sets up a TPM2 simulator that is running behind a user-space TPM2 resource +# manager. Applications can discover the resource manager via D-Bus and use +# the resource manager's TCTI (aka. tabrmd). +# +# The simulator is populated with persistent keys for testing. +# +# If you need the simulated TPM 2.0 setup in your deveolpment environment, you +# can run this script manually. If something goes wrong, you can re-initialize +# the TPM 2.0 simulator by running: +# +# kill $(pgrep swtpm); kill $(pgrep tpm2-abrmd); sh src/scripts/ci/start_tpm2_simulator.sh +# + +set -e + +tmp_dir="${1:-/tmp/mytpm2}" +dbus_name="net.randombit.botan.tabrmd" +tcti_name="tabrmd" +tcti_conf="bus_name=${dbus_name},bus_type=session" +tcti="${tcti_name}:${tcti_conf}" +test_pwd="password" +persistent_rsa_key_handle="0x81000008" + +if ! systemctl is-active --quiet dbus; then + echo "DBus is not running. Starting it..." + sudo systemctl start dbus +fi + +echo "Setting up TPM..." +swtpm_setup --create-config-files overwrite + +# "Endorsement Key" - baked into the TPM (signed by the manufacturer) +# "Platform Key" - signed serial number of the EK (signed by the OEM; eg. the laptop manufacturer) +# "Storage Root Key" - created by the user (signed by the EK) +rm -fR $tmp_dir && mkdir $tmp_dir +swtpm_setup --tpmstate $tmp_dir \ + --create-ek-cert \ + --create-platform-cert \ + --create-spk \ + --overwrite --tpm2 \ + --display + +echo "Starting TPM2 simulator..." +swtpm socket --tpmstate dir=$tmp_dir \ + --ctrl type=tcp,port=2322 \ + --server type=tcp,port=2321 \ + --flags not-need-init \ + --daemon --tpm2 + +echo "Starting TPM2 resource manager..." +tpm2-abrmd --tcti=swtpm --session --dbus-name="${dbus_name}" & +echo "Resource manager running as PID: $!" + +echo "Waiting a for the dbus name to be available..." +waited=5 +while ! dbus-send --session --dest=org.freedesktop.DBus --type=method_call --print-reply \ + /org/freedesktop/DBus org.freedesktop.DBus.ListNames | grep -q "${dbus_name}"; do + sleep 1 + echo "..." + waited=$((waited - 1)) + if [ $waited -eq 0 ]; then + echo "Failed to start the TPM2 resource manager" + exit 1 + fi +done + +echo "Create a key to play with..." +tpm2_createprimary --tcti="$tcti" \ + --hierarchy e \ + --hash-algorithm sha256 \ + --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" \ + --parent-context $tmp_dir/primary.ctx \ + --key-algorithm rsa \ + --public $tmp_dir/rsa.pub \ + --private $tmp_dir/rsa.priv \ + --key-auth $test_pwd +tpm2_load --tcti="$tcti" \ + --parent-context $tmp_dir/primary.ctx \ + --public $tmp_dir/rsa.pub \ + --private $tmp_dir/rsa.priv \ + --key-context $tmp_dir/rsa.ctx +tpm2_evictcontrol --tcti="$tcti" \ + --hierarchy o \ + --object-context $tmp_dir/rsa.ctx \ + $persistent_rsa_key_handle + +echo "Effectively disable dictionary attack lockout..." +tpm2_dictionarylockout --tcti="$tcti" \ + --setup-parameters \ + --max-tries=1000 \ + --recovery-time=1 \ + --lockout-recovery-time=1 + +# Propagate relevant information about the simulated TPM 2.0 setup to +# the test scripts that are going to run, if we're running on GitHub Actions. +if [ -n "$GITHUB_ACTIONS" ]; then + echo "Setting up GitHub Actions environment..." + echo "BOTAN_TPM2_TCTI_NAME=$tcti_name" >> $GITHUB_ENV + 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 +fi diff --git a/src/scripts/ci_build.py b/src/scripts/ci_build.py index cf4f90406db..c6ca407b794 100755 --- a/src/scripts/ci_build.py +++ b/src/scripts/ci_build.py @@ -452,6 +452,24 @@ def add_boost_support(target, target_os): if target_os == 'linux': flags += ['--with-lzma'] + if is_running_in_github_actions() and 'BOTAN_TPM2_ENABLED' in os.environ: + flags += ['--with-tpm2'] + + if os.environ.get('BOTAN_TPM2_ENABLED') == 'test': + # run the TPM2 tests + 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-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 + # TCTI name 'disabled' is a special value that disables the TPM2 tests + # + # TODO: This is a hack. It would be great if `./botan-test --skip-tests=` + # would also work for test categories like `tpm2`. Currently it + # only works for individual test names. + test_cmd += ["--tpm2-tcti-name=disabled"] + if target in ['coverage']: flags += ['--with-tpm'] test_cmd += ['--run-online-tests'] diff --git a/src/tests/main.cpp b/src/tests/main.cpp index 110d45bc1bd..8030af25d0c 100644 --- a/src/tests/main.cpp +++ b/src/tests/main.cpp @@ -60,9 +60,12 @@ int main(int argc, char* argv[]) { try { const std::string arg_spec = "botan-test --verbose --help --data-dir= --pkcs11-lib= --provider= " - "--log-success --abort-on-first-fail --no-stdout --no-avoid-undefined --skip-tests= " - "--test-threads=0 --test-results-dir= --run-long-tests --run-memory-intensive-tests " - "--run-online-tests --test-runs=1 --drbg-seed= --report-properties= *suites"; + "--tpm2-tcti-name= --tpm2-tcti-conf= --tpm2-persistent-rsa-handle=0x81000008 " + "--tpm2-persistent-ecc-handle=0x81000010 --tpm2-persistent-auth-value=password " + "--log-success --abort-on-first-fail --no-stdout --no-avoid-undefined " + "--skip-tests= --test-threads=0 --test-results-dir= --run-long-tests " + "--run-memory-intensive-tests --run-online-tests --test-runs=1 " + "--drbg-seed= --report-properties= *suites"; Botan_CLI::Argument_Parser parser(arg_spec); @@ -88,6 +91,11 @@ int main(int argc, char* argv[]) { parser.get_arg_or("data-dir", "src/tests/data"), parser.get_arg("pkcs11-lib"), parser.get_arg("provider"), + parser.get_arg("tpm2-tcti-name"), + parser.get_arg("tpm2-tcti-conf"), + parser.get_arg_hex_sz_or("tpm2-persistent-rsa-handle", "0x81000008"), + parser.get_arg_hex_sz_or("tpm2-persistent-ecc-handle", "0x81000010"), + parser.get_arg_or("tpm2-persistent-auth-value", "password"), parser.get_arg("drbg-seed"), parser.get_arg("test-results-dir"), parser.get_arg_list("report-properties"), diff --git a/src/tests/tests.h b/src/tests/tests.h index e8c8b0fad86..d0fbc87c55e 100644 --- a/src/tests/tests.h +++ b/src/tests/tests.h @@ -9,6 +9,7 @@ #define BOTAN_TESTS_H_ #include +#include #include #include #include @@ -65,6 +66,11 @@ class Test_Options { const std::string& data_dir, const std::string& pkcs11_lib, const std::string& provider, + const std::string& tpm2_tcti_name, + const std::string& tpm2_tcti_conf, + size_t tpm2_persistent_rsa_handle, + size_t tpm2_persistent_ecc_handle, + const std::string& tpm2_persistent_auth_value, const std::string& drbg_seed, const std::string& xml_results_dir, const std::vector& report_properties, @@ -82,6 +88,11 @@ class Test_Options { m_data_dir(data_dir), m_pkcs11_lib(pkcs11_lib), m_provider(provider), + m_tpm2_tcti_name(tpm2_tcti_name), + m_tpm2_tcti_conf(tpm2_tcti_conf), + m_tpm2_persistent_rsa_handle(tpm2_persistent_rsa_handle), + m_tpm2_persistent_ecc_handle(tpm2_persistent_ecc_handle), + m_tpm2_persistent_auth_value(tpm2_persistent_auth_value), m_drbg_seed(drbg_seed), m_xml_results_dir(xml_results_dir), m_report_properties(report_properties), @@ -105,6 +116,20 @@ class Test_Options { const std::string& provider() const { return m_provider; } + const std::optional& tpm2_tcti_name() const { return m_tpm2_tcti_name; } + + const std::optional& tpm2_tcti_conf() const { return m_tpm2_tcti_conf; } + + uint32_t tpm2_persistent_rsa_handle() const { return static_cast(m_tpm2_persistent_rsa_handle); } + + uint32_t tpm2_persistent_ecc_handle() const { return static_cast(m_tpm2_persistent_ecc_handle); } + + std::vector tpm2_persistent_auth_value() const { + std::span auth_value(Botan::cast_char_ptr_to_uint8(m_tpm2_persistent_auth_value.data()), + m_tpm2_persistent_auth_value.size()); + return std::vector(auth_value.begin(), auth_value.end()); + } + const std::string& drbg_seed() const { return m_drbg_seed; } const std::string& xml_results_dir() const { return m_xml_results_dir; } @@ -135,6 +160,11 @@ class Test_Options { std::string m_data_dir; std::string m_pkcs11_lib; std::string m_provider; + std::optional m_tpm2_tcti_name; + std::optional m_tpm2_tcti_conf; + size_t m_tpm2_persistent_rsa_handle; + size_t m_tpm2_persistent_ecc_handle; + std::string m_tpm2_persistent_auth_value; std::string m_drbg_seed; std::string m_xml_results_dir; std::vector m_report_properties; From 31f8eff8e008ad63fbd2e7ee62a9129dbf4dabe6 Mon Sep 17 00:00:00 2001 From: Amos Treiber Date: Thu, 5 Sep 2024 12:24:46 +0200 Subject: [PATCH 3/7] Basic TPM 2.0 functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Namely: * TPM2::Context - allowing to request basic authorative information * TPM2::Session - to create an unauth'ed HMAC session with the TPM * TPM2::Error - Botan exception encapsulation TPM-related errors * TPM2::Object - handles the lifetime of TPM object handles * tpm2_util.h - utility functions to handle calls to ESYS API * tpm2_algo_mappings.h - mappings from Botan algo strings to TSS' algorithm IDs and scheme definitions Note, that some of those things aren't actively used in this commit, yet. Co-Authored-By: René Meusel --- src/lib/prov/tpm2/info.txt | 17 ++ src/lib/prov/tpm2/tpm2_algo_mappings.h | 342 +++++++++++++++++++++++++ src/lib/prov/tpm2/tpm2_context.cpp | 270 +++++++++++++++++++ src/lib/prov/tpm2/tpm2_context.h | 100 ++++++++ src/lib/prov/tpm2/tpm2_error.cpp | 262 +++++++++++++++++++ src/lib/prov/tpm2/tpm2_error.h | 42 +++ src/lib/prov/tpm2/tpm2_object.cpp | 161 ++++++++++++ src/lib/prov/tpm2/tpm2_object.h | 139 ++++++++++ src/lib/prov/tpm2/tpm2_session.cpp | 102 ++++++++ src/lib/prov/tpm2/tpm2_session.h | 173 +++++++++++++ src/lib/prov/tpm2/tpm2_util.h | 287 +++++++++++++++++++++ src/tests/test_tpm2.cpp | 150 +++++++++++ 12 files changed, 2045 insertions(+) create mode 100644 src/lib/prov/tpm2/tpm2_algo_mappings.h create mode 100644 src/lib/prov/tpm2/tpm2_context.cpp create mode 100644 src/lib/prov/tpm2/tpm2_context.h create mode 100644 src/lib/prov/tpm2/tpm2_error.cpp create mode 100644 src/lib/prov/tpm2/tpm2_error.h create mode 100644 src/lib/prov/tpm2/tpm2_object.cpp create mode 100644 src/lib/prov/tpm2/tpm2_object.h create mode 100644 src/lib/prov/tpm2/tpm2_session.cpp create mode 100644 src/lib/prov/tpm2/tpm2_session.h create mode 100644 src/lib/prov/tpm2/tpm2_util.h create mode 100644 src/tests/test_tpm2.cpp diff --git a/src/lib/prov/tpm2/info.txt b/src/lib/prov/tpm2/info.txt index f627644c763..48685e12ab8 100644 --- a/src/lib/prov/tpm2/info.txt +++ b/src/lib/prov/tpm2/info.txt @@ -12,3 +12,20 @@ load_on vendor all -> tss2-esys,tss2-rc,tss2-tctildr,tss2-mu + + +rng +pubkey + + + +tpm2_algo_mappings.h +tpm2_util.h + + + +tpm2_context.h +tpm2_error.h +tpm2_object.h +tpm2_session.h + diff --git a/src/lib/prov/tpm2/tpm2_algo_mappings.h b/src/lib/prov/tpm2/tpm2_algo_mappings.h new file mode 100644 index 00000000000..569ad933812 --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_algo_mappings.h @@ -0,0 +1,342 @@ +/* +* TPM 2 algorithm mappings +* (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_ALGORITHM_MAPPINGS_H_ +#define BOTAN_TPM2_ALGORITHM_MAPPINGS_H_ + +#include + +#include +#include + +#include +#include +#include + +#include + +namespace Botan::TPM2 { + +[[nodiscard]] inline std::optional asymmetric_algorithm_botan_to_tss2( + std::string_view algo_name) noexcept { + if(algo_name == "RSA") { + return TPM2_ALG_RSA; + } else if(algo_name == "ECC") { + return TPM2_ALG_ECC; + } else if(algo_name == "ECDSA") { + return TPM2_ALG_ECC; + } else if(algo_name == "ECDH") { + return TPM2_ALG_ECDH; + } else if(algo_name == "ECDAA") { + return TPM2_ALG_ECDAA; + } else { + return std::nullopt; + } +} + +/** + * @returns a TPMI_ALG_HASH value if the @p hash_name is known, + * otherwise std::nullopt + */ +[[nodiscard]] inline std::optional hash_algo_botan_to_tss2(std::string_view hash_name) noexcept { + if(hash_name == "SHA-1") { + return TPM2_ALG_SHA1; + } else if(hash_name == "SHA-256") { + return TPM2_ALG_SHA256; + } else if(hash_name == "SHA-384") { + return TPM2_ALG_SHA384; + } else if(hash_name == "SHA-512") { + return TPM2_ALG_SHA512; + } else if(hash_name == "SHA-3(256)") { + return TPM2_ALG_SHA3_256; + } else if(hash_name == "SHA-3(384)") { + return TPM2_ALG_SHA3_384; + } else if(hash_name == "SHA-3(512)") { + return TPM2_ALG_SHA3_512; + } else if(hash_name == "SM3") { + return TPM2_ALG_SM3_256; + } else { + return std::nullopt; + } +} + +/** + * @returns a TPMI_ALG_HASH value if the @p hash_name is known, + * otherwise throws Lookup_Error + */ +[[nodiscard]] inline TPMI_ALG_HASH get_tpm2_hash_type(std::string_view hash_name) { + if(auto hash_id = hash_algo_botan_to_tss2(hash_name)) { + return hash_id.value(); + } + + throw Lookup_Error("TPM 2.0 Hash", hash_name); +} + +/** + * @returns a Botan hash name string if the @p hash_id value is known, + * otherwise std::nullopt + */ +[[nodiscard]] inline std::optional hash_algo_tss2_to_botan(TPMI_ALG_HASH hash_id) noexcept { + switch(hash_id) { + case TPM2_ALG_SHA1: + return "SHA-1"; + case TPM2_ALG_SHA256: + return "SHA-256"; + case TPM2_ALG_SHA384: + return "SHA-384"; + case TPM2_ALG_SHA512: + return "SHA-512"; + case TPM2_ALG_SHA3_256: + return "SHA-3(256)"; + case TPM2_ALG_SHA3_384: + return "SHA-3(384)"; + case TPM2_ALG_SHA3_512: + return "SHA-3(512)"; + case TPM2_ALG_SM3_256: + return "SM3"; + default: // TPMI_ALG_HASH is not an enum + return std::nullopt; + } +} + +/** + * @returns a Botan hash name string if the @p hash_id value is known, + * otherwise throws Invalid_State + */ +[[nodiscard]] inline std::string get_botan_hash_name(TPM2_ALG_ID hash_id) { + if(auto hash_name = hash_algo_tss2_to_botan(hash_id)) { + return hash_name.value(); + } + + throw Invalid_State("TPM 2.0 hash object with unexpected hash type"); +} + +[[nodiscard]] inline std::optional block_cipher_tss2_to_botan(TPMI_ALG_SYM cipher_id, + TPM2_KEY_BITS key_bits) noexcept { + switch(cipher_id) { + case TPM2_ALG_AES: + if(key_bits == 128) { + return "AES-128"; + } else if(key_bits == 192) { + return "AES-192"; + } else if(key_bits == 256) { + return "AES-256"; + } + break; + + case TPM2_ALG_SM4: + if(key_bits == 128) { + return "SM4"; + } + break; + + case TPM2_ALG_CAMELLIA: + if(key_bits == 128) { + return "Camellia-128"; + } else if(key_bits == 192) { + return "Camellia-192"; + } else if(key_bits == 256) { + return "Camellia-256"; + } + break; + + case TPM2_ALG_TDES: + return "3DES"; + + default: + break; + } + + return std::nullopt; +} + +[[nodiscard]] inline std::optional> block_cipher_botan_to_tss2( + std::string_view cipher_name) noexcept { + if(cipher_name == "AES-128") { + return std::pair{TPM2_ALG_AES, 128}; + } else if(cipher_name == "AES-192") { + return std::pair{TPM2_ALG_AES, 192}; + } else if(cipher_name == "AES-256") { + return std::pair{TPM2_ALG_AES, 256}; + } else if(cipher_name == "SM4") { + return std::pair{TPM2_ALG_SM4, 128}; + } else if(cipher_name == "Camellia-128") { + return std::pair{TPM2_ALG_CAMELLIA, 128}; + } else if(cipher_name == "Camellia-192") { + return std::pair{TPM2_ALG_CAMELLIA, 192}; + } else if(cipher_name == "Camellia-256") { + return std::pair{TPM2_ALG_CAMELLIA, 256}; + } else if(cipher_name == "3DES") { + return std::pair{TPM2_ALG_TDES, 168}; + } else { + return {}; + } +} + +[[nodiscard]] inline std::optional cipher_mode_tss2_to_botan(TPMI_ALG_SYM_MODE mode_id) { + switch(mode_id) { + case TPM2_ALG_CFB: + return "CFB"; + case TPM2_ALG_CBC: + return "CBC"; + case TPM2_ALG_ECB: + return "ECB"; + case TPM2_ALG_OFB: + return "OFB"; + case TPM2_ALG_CTR: + return "CTR"; + default: // TPMI_ALG_SYM_MODE is not an enum + 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; + } else if(mode_name == "CBC") { + return TPM2_ALG_CBC; + } else if(mode_name == "ECB") { + return TPM2_ALG_ECB; + } else if(mode_name == "OFB") { + return TPM2_ALG_OFB; + } else if(mode_name == "CTR" || mode_name == "CTR-BE") { + return TPM2_ALG_CTR; + } else { + return std::nullopt; + } +} + +/** + * @returns a Botan cipher mode name string if the @p cipher_id, @p key_bits and + * @p mode_name are known, otherwise std::nullopt + */ +[[nodiscard]] inline std::optional cipher_tss2_to_botan(TPMT_SYM_DEF cipher_def) noexcept { + const auto cipher_name = block_cipher_tss2_to_botan(cipher_def.algorithm, cipher_def.keyBits.sym); + if(!cipher_name) { + return std::nullopt; + } + + const auto mode_name = cipher_mode_tss2_to_botan(cipher_def.mode.sym); + if(!mode_name) { + return std::nullopt; + } + + return Botan::fmt("{}({})", mode_name.value(), cipher_name.value()); +} + +[[nodiscard]] inline std::optional cipher_botan_to_tss2(std::string_view algo_name) { + SCAN_Name spec(algo_name); + if(spec.arg_count() == 0) { + return std::nullopt; + } + + const auto cipher = block_cipher_botan_to_tss2(spec.arg(0)); + const auto mode = cipher_mode_botan_to_tss2(spec.algo_name()); + + if(!cipher || !mode) { + return std::nullopt; + } + + return TPMT_SYM_DEF{ + .algorithm = cipher->first, + .keyBits = {.sym = cipher->second}, + .mode = {.sym = mode.value()}, + }; +} + +[[nodiscard]] inline TPMT_SYM_DEF get_tpm2_sym_cipher_spec(std::string_view algo_name) { + if(auto cipher = cipher_botan_to_tss2(algo_name)) { + return cipher.value(); + } + + throw Lookup_Error("TPM 2.0 Symmetric Cipher Spec", algo_name); +} + +[[nodiscard]] inline std::optional rsa_signature_padding_botan_to_tss2( + std::string_view padding_name) noexcept { + if(padding_name == "EMSA_PKCS1" || padding_name == "PKCS1v15" || padding_name == "EMSA-PKCS1-v1_5" || + padding_name == "EMSA3") { + return TPM2_ALG_RSASSA; + } else if(padding_name == "PSS" || padding_name == "PSSR" || padding_name == "EMSA-PSS" || + padding_name == "PSS-MGF1" || padding_name == "EMSA4") { + return TPM2_ALG_RSAPSS; + } else { + return std::nullopt; + } +} + +[[nodiscard]] inline std::optional rsa_signature_scheme_botan_to_tss2(std::string_view name) { + const SCAN_Name req(name); + if(req.arg_count() == 0) { + return std::nullopt; + } + + const auto scheme = rsa_signature_padding_botan_to_tss2(req.algo_name()); + const auto hash = hash_algo_botan_to_tss2(req.arg(0)); + if(!scheme || !hash) { + return std::nullopt; + } + + if(scheme.value() == TPM2_ALG_RSAPSS && req.arg_count() != 1) { + // RSA signing using PSS with MGF1 + return std::nullopt; + } + + return TPMT_SIG_SCHEME{ + .scheme = scheme.value(), + .details = {.any = {.hashAlg = hash.value()}}, + }; +} + +[[nodiscard]] inline std::optional rsa_encryption_padding_botan_to_tss2( + std::string_view name) noexcept { + if(name == "OAEP" || name == "EME-OAEP" || name == "EME1") { + return TPM2_ALG_OAEP; + } else if(name == "PKCS1v15" || name == "EME-PKCS1-v1_5") { + return TPM2_ALG_RSAES; + } else if(name == "Raw") { + return TPM2_ALG_NULL; + } else { + return std::nullopt; + } +} + +[[nodiscard]] inline std::optional rsa_encryption_scheme_botan_to_tss2(std::string_view padding) { + const SCAN_Name req(padding); + const auto scheme = rsa_encryption_padding_botan_to_tss2(req.algo_name()); + if(!scheme) { + return std::nullopt; + } + + if(scheme.value() == TPM2_ALG_OAEP) { + if(req.arg_count() < 1) { + return std::nullopt; + } + + const auto hash = hash_algo_botan_to_tss2(req.arg(0)); + if(!hash) { + return std::nullopt; + } + + return TPMT_RSA_DECRYPT{ + .scheme = scheme.value(), + .details = {.oaep = {.hashAlg = hash.value()}}, + }; + } else if(scheme.value() == TPM2_ALG_RSAES) { + return TPMT_RSA_DECRYPT{ + .scheme = scheme.value(), + .details = {.rsaes = {}}, + }; + } else { + return std::nullopt; + } +} + +} // namespace Botan::TPM2 + +#endif diff --git a/src/lib/prov/tpm2/tpm2_context.cpp b/src/lib/prov/tpm2/tpm2_context.cpp new file mode 100644 index 00000000000..b4494aabd30 --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_context.cpp @@ -0,0 +1,270 @@ +/* +* TPM 2 interface +* (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 +#include +#include + +#include +#include +#include + +namespace Botan::TPM2 { + +struct Context::Impl { + TSS2_TCTI_CONTEXT* m_tcti_ctx; + ESYS_CONTEXT* m_ctx; +}; + +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())); +} + +std::shared_ptr Context::create(std::optional tcti, std::optional conf) { + const auto tcti_ptr = tcti.has_value() ? tcti->c_str() : nullptr; + const auto conf_ptr = conf.has_value() ? conf->c_str() : nullptr; + + // We cannot std::make_shared as the constructor is private + return std::shared_ptr(new Context(tcti_ptr, conf_ptr)); +} + +Context::Context(const char* tcti_nameconf) : m_impl(std::make_unique()) { + check_rc("TCTI Initialization", Tss2_TctiLdr_Initialize(tcti_nameconf, &m_impl->m_tcti_ctx)); + BOTAN_ASSERT_NONNULL(m_impl->m_tcti_ctx); + check_rc("TPM2 Initialization", Esys_Initialize(&m_impl->m_ctx, m_impl->m_tcti_ctx, nullptr /* ABI version */)); + BOTAN_ASSERT_NONNULL(m_impl->m_ctx); +} + +Context::Context(const char* tcti_name, const char* tcti_conf) : m_impl(std::make_unique()) { + check_rc("TCTI Initialization", Tss2_TctiLdr_Initialize_Ex(tcti_name, tcti_conf, &m_impl->m_tcti_ctx)); + BOTAN_ASSERT_NONNULL(m_impl->m_tcti_ctx); + check_rc("TPM2 Initialization", Esys_Initialize(&m_impl->m_ctx, m_impl->m_tcti_ctx, nullptr /* ABI version */)); + BOTAN_ASSERT_NONNULL(m_impl->m_ctx); +} + +ESYS_CONTEXT* Context::esys_context() noexcept { + return m_impl->m_ctx; +} + +namespace { + +uint32_t get_tpm_property(ESYS_CONTEXT* ctx, TPM2_PT property) { + // We expect to retrieve a single piece of information, not a list. + constexpr uint32_t property_count = 1; + constexpr TPM2_CAP capability = TPM2_CAP_TPM_PROPERTIES; + + unique_esys_ptr capability_data; + check_rc("Esys_GetCapability", + Esys_GetCapability(ctx, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + capability, + property, + property_count, + nullptr /* more data? - we don't care here */, + out_ptr(capability_data))); + BOTAN_ASSERT_NONNULL(capability_data); + BOTAN_ASSERT_NOMSG(capability_data->capability == capability); + BOTAN_ASSERT_NOMSG(capability_data->data.tpmProperties.count == property_count); + BOTAN_ASSERT_NOMSG(capability_data->data.tpmProperties.tpmProperty[0].property == property); + + return capability_data->data.tpmProperties.tpmProperty[0].value; +} + +template +[[nodiscard]] std::vector get_tpm_property_list(ESYS_CONTEXT* ctx, TPM2_PT property, uint32_t count) { + auto extract = [](const TPMU_CAPABILITIES& caps, uint32_t max_count) { + std::vector result; + if constexpr(capability == TPM2_CAP_HANDLES) { + const auto to_read = std::min(caps.handles.count, max_count); + result.reserve(to_read); + for(size_t i = 0; i < to_read; ++i) { + result.push_back(caps.handles.handle[i]); + } + } else if constexpr(capability == TPM2_CAP_ALGS) { + const auto to_read = std::min(caps.algorithms.count, max_count); + result.reserve(to_read); + for(size_t i = 0; i < to_read; ++i) { + // TODO: This also contains an algProperties.algProperties bitfield + // that defines some characteristics of the algorithm. + // Currently, we don't need that information and ignore it. + result.push_back(caps.algorithms.algProperties[i].alg); + } + } else { + // TODO: support reading other capability types as needed + static_assert(capability != TPM2_CAP_HANDLES, "Unsupported capability"); + } + return result; + }; + + TPMI_YES_NO more_data = TPM2_YES; + std::vector properties; + while(more_data == TPM2_YES && count > 0) { + unique_esys_ptr capability_data; + check_rc("Esys_GetCapability", + Esys_GetCapability(ctx, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + capability, + property, + count, + &more_data, + out_ptr(capability_data))); + BOTAN_ASSERT_NONNULL(capability_data); + BOTAN_ASSERT_NOMSG(capability_data->capability == capability); + + const auto new_properties = extract(capability_data->data, count); + BOTAN_ASSERT_NOMSG(new_properties.size() <= count); + properties.insert(properties.end(), new_properties.begin(), new_properties.end()); + count -= new_properties.size(); + } + + return properties; +} + +} // namespace + +std::string Context::vendor() const { + constexpr std::array properties = { + TPM2_PT_VENDOR_STRING_1, TPM2_PT_VENDOR_STRING_2, TPM2_PT_VENDOR_STRING_3, TPM2_PT_VENDOR_STRING_4}; + std::array vendor_string{}; + + BufferStuffer bs(vendor_string); + + // The vendor name is transported in several uint32_t fields that are + // loaded as big-endian bytes and concatenated to form the vendor string. + for(auto prop : properties) { + bs.append(store_be(get_tpm_property(m_impl->m_ctx, prop))); + } + + BOTAN_ASSERT_NOMSG(bs.remaining_capacity() == 1); // the ensured zero-termination + return std::string(cast_uint8_ptr_to_char(vendor_string.data())); +} + +std::string Context::manufacturer() const { + std::array manufacturer_data{}; + store_be(std::span{manufacturer_data}.first<4>(), get_tpm_property(m_impl->m_ctx, TPM2_PT_MANUFACTURER)); + return std::string(cast_uint8_ptr_to_char(manufacturer_data.data())); +} + +bool Context::supports_algorithm(std::string_view algo_name) const { + // Go through all the string mappings we have available and check if we + // can find the algorithm name in any of them. If we do, we can check if + // the TPM supports the required algorithms. + const auto required_alg_ids = [&]() -> std::vector { + std::vector result; + if(auto algo_id = asymmetric_algorithm_botan_to_tss2(algo_name)) { + result.push_back(algo_id.value()); + } + + if(auto hash_id = hash_algo_botan_to_tss2(algo_name)) { + result.push_back(hash_id.value()); + } + + if(auto block_id = block_cipher_botan_to_tss2(algo_name)) { + result.push_back(block_id->first); + } + + if(auto cipher_mode_id = cipher_mode_botan_to_tss2(algo_name)) { + result.push_back(cipher_mode_id.value()); + } + + if(auto cipher_spec = cipher_botan_to_tss2(algo_name)) { + result.push_back(cipher_spec->algorithm); + result.push_back(cipher_spec->mode.sym); + } + + if(auto sig_padding = rsa_signature_padding_botan_to_tss2(algo_name)) { + result.push_back(sig_padding.value()); + } + + if(auto sig = rsa_signature_scheme_botan_to_tss2(algo_name)) { + result.push_back(sig->scheme); + result.push_back(sig->details.any.hashAlg); + } + + if(auto enc_scheme = rsa_encryption_scheme_botan_to_tss2(algo_name)) { + result.push_back(enc_scheme->scheme); + if(enc_scheme->scheme == TPM2_ALG_OAEP) { + result.push_back(enc_scheme->details.oaep.hashAlg); + } + } + + if(auto enc_id = rsa_encryption_padding_botan_to_tss2(algo_name)) { + result.push_back(enc_id.value()); + } + + return result; + }(); + + if(required_alg_ids.empty()) { + // The algorithm name is not known to us, so we cannot check for support. + return false; + } + + const auto algo_caps = + get_tpm_property_list(m_impl->m_ctx, TPM2_ALG_FIRST, TPM2_MAX_CAP_ALGS); + + return std::all_of( + required_alg_ids.begin(), required_alg_ids.end(), [&](TPM2_ALG_ID id) { return value_exists(algo_caps, id); }); +} + +size_t Context::max_random_bytes_per_request() const { + return get_tpm_property(m_impl->m_ctx, TPM2_PT_MAX_DIGEST); +} + +std::vector Context::transient_handles() const { + return get_tpm_property_list(m_impl->m_ctx, TPM2_TRANSIENT_FIRST, TPM2_MAX_CAP_HANDLES); +} + +std::optional Context::find_free_persistent_handle() const { + const auto occupied_handles = persistent_handles(); + + // This is modeled after the implementation in tpm2-tools, which also takes + // "platform persistent" handles into account. We don't do that here, but + // we might need to in the future. + // + // See: https://github.com/tpm2-software/tpm2-tools/blob/bd832d3f79/lib/tpm2_capability.c#L143-L196 + + // all persistent handles are occupied + if(occupied_handles.size() >= TPM2_MAX_CAP_HANDLES) { + return std::nullopt; + } + + // find the lowest handle that is not occupied + for(TPM2_HANDLE i = TPM2_PERSISTENT_FIRST; i < TPM2_PERSISTENT_LAST; ++i) { + if(!value_exists(occupied_handles, i)) { + return i; + } + } + + BOTAN_ASSERT_UNREACHABLE(); +} + +std::vector Context::persistent_handles() const { + return get_tpm_property_list( + m_impl->m_ctx, TPM2_PERSISTENT_FIRST, TPM2_MAX_CAP_HANDLES); +} + +Context::~Context() { + if(m_impl) { + Esys_Finalize(&m_impl->m_ctx); + Tss2_TctiLdr_Finalize(&m_impl->m_tcti_ctx); + } +} + +} // namespace Botan::TPM2 diff --git a/src/lib/prov/tpm2/tpm2_context.h b/src/lib/prov/tpm2/tpm2_context.h new file mode 100644 index 00000000000..e0be2de5b0b --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_context.h @@ -0,0 +1,100 @@ +/* +* TPM 2 interface +* (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_CONTEXT_H_ +#define BOTAN_TPM2_CONTEXT_H_ + +#include +#include + +#include +#include +#include + +/// Forward declaration of TSS2 type for convenience +using TPM2_HANDLE = uint32_t; + +/// Forward declaration of TSS2 type for convenience +using ESYS_TR = uint32_t; + +struct ESYS_CONTEXT; + +namespace Botan::TPM2 { + +class SessionBundle; + +/** + * Central class for interacting with a TPM2. Additional to managing the + * connection to the TPM, this provides authorative information about the TPM's + * capabilities. Also, it allows to persist and evict keys generated by the TPM. + */ +class BOTAN_PUBLIC_API(3, 6) Context final : public std::enable_shared_from_this { + public: + /** + * @param tcti_nameconf this is passed to Tss2_TctiLdr_Initialize verbatim + */ + static std::shared_ptr create(const std::string& tcti_nameconf); + + /** + * @param tcti if set this is passed to Tss2_TctiLdr_Initialize_Ex verbatim + * otherwise a nullptr is passed. + * @param conf if set this is passed to Tss2_TctiLdr_Initialize_Ex verbatim + * otherwise a nullptr is passed. + */ + static std::shared_ptr create(std::optional tcti = {}, + std::optional conf = {}); + + Context(const Context&) = delete; + Context(Context&& ctx) noexcept = default; + ~Context(); + + Context& operator=(const Context&) = delete; + Context& operator=(Context&& ctx) noexcept = default; + + /// @return an ESYS_CONTEXT* for use in other TPM2 functions. + ESYS_CONTEXT* esys_context() noexcept; + + operator ESYS_CONTEXT*() noexcept { return esys_context(); } + + /// @return the Vendor of the TPM2 + std::string vendor() const; + + /// @returns the Manufacturer of the TPM2 + std::string manufacturer() const; + + /** + * The @p algo_name can be any of the string algorithm specifiers used + * elsewhere. For example, "RSA", "AES-128", "SHA-1", "CTR(3DES)", etc. + * + * @returns true if the specified algorithm is supported by the TPM + */ + bool supports_algorithm(std::string_view algo_name) const; + + /// @returns the maximum number of random bytes to be requested at once + size_t max_random_bytes_per_request() const; + + std::vector transient_handles() const; + + /// @returns a persistent handle that is currently not in use + /// or std::nullopt if no such handle is available + std::optional find_free_persistent_handle() const; + + std::vector persistent_handles() const; + + private: + Context(const char* tcti_nameconf); + Context(const char* tcti_name, const char* tcti_conf); + + private: + struct Impl; // PImpl to avoid TPM2-TSS includes in this header + std::unique_ptr m_impl; +}; + +} // namespace Botan::TPM2 + +#endif diff --git a/src/lib/prov/tpm2/tpm2_error.cpp b/src/lib/prov/tpm2/tpm2_error.cpp new file mode 100644 index 00000000000..e6f6f408f26 --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_error.cpp @@ -0,0 +1,262 @@ +/* +* TPM 2 error handling +* (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 + +namespace Botan::TPM2 { + +TSS2_RC get_raw_rc(TSS2_RC rc) { +#if defined(BOTAN_TSS2_SUPPORTS_ERROR_DECODING) + TSS2_RC_INFO info; + const TSS2_RC decoding_rc = Tss2_RC_DecodeInfo(rc, &info); + if(decoding_rc != TSS2_RC_SUCCESS) [[unlikely]] { + throw Error(fmt("Decoding RC failed (was: {})", rc), decoding_rc); + } + return info.error; +#else + // This fallback implementation is derived from the implementation of + // Tss2_RC_DecodeInfo in tpm2-tss 4.0.0. + const bool formatted = (rc & (1 << 7)) != 0; + if(formatted) { + return (rc & 0x3F) | TPM2_RC_FMT1; + } else { + return rc & 0xFFFF; + } +#endif +}; + +namespace { + +std::string raw_rc_to_string(TSS2_RC rc) noexcept { + switch(rc) { + case TPM2_RC_SUCCESS: + return "TPM2_RC_SUCCESS"; + case TPM2_RC_BAD_TAG: + return "TPM2_RC_BAD_TAG"; + case TPM2_RC_INITIALIZE: + return "TPM2_RC_INITIALIZE"; + case TPM2_RC_FAILURE: + return "TPM2_RC_FAILURE"; + case TPM2_RC_SEQUENCE: + return "TPM2_RC_SEQUENCE"; + case TPM2_RC_PRIVATE: + return "TPM2_RC_PRIVATE"; + case TPM2_RC_HMAC: + return "TPM2_RC_HMAC"; + case TPM2_RC_DISABLED: + return "TPM2_RC_DISABLED"; + case TPM2_RC_EXCLUSIVE: + return "TPM2_RC_EXCLUSIVE"; + case TPM2_RC_AUTH_TYPE: + return "TPM2_RC_AUTH_TYPE"; + case TPM2_RC_AUTH_MISSING: + return "TPM2_RC_AUTH_MISSING"; + case TPM2_RC_POLICY: + return "TPM2_RC_POLICY"; + case TPM2_RC_PCR: + return "TPM2_RC_PCR"; + case TPM2_RC_PCR_CHANGED: + return "TPM2_RC_PCR_CHANGED"; + case TPM2_RC_UPGRADE: + return "TPM2_RC_UPGRADE"; + case TPM2_RC_TOO_MANY_CONTEXTS: + return "TPM2_RC_TOO_MANY_CONTEXTS"; + case TPM2_RC_AUTH_UNAVAILABLE: + return "TPM2_RC_AUTH_UNAVAILABLE"; + case TPM2_RC_REBOOT: + return "TPM2_RC_REBOOT"; + case TPM2_RC_UNBALANCED: + return "TPM2_RC_UNBALANCED"; + case TPM2_RC_COMMAND_SIZE: + return "TPM2_RC_COMMAND_SIZE"; + case TPM2_RC_COMMAND_CODE: + return "TPM2_RC_COMMAND_CODE"; + case TPM2_RC_AUTHSIZE: + return "TPM2_RC_AUTHSIZE"; + case TPM2_RC_AUTH_CONTEXT: + return "TPM2_RC_AUTH_CONTEXT"; + case TPM2_RC_NV_RANGE: + return "TPM2_RC_NV_RANGE"; + case TPM2_RC_NV_SIZE: + return "TPM2_RC_NV_SIZE"; + case TPM2_RC_NV_LOCKED: + return "TPM2_RC_NV_LOCKED"; + case TPM2_RC_NV_AUTHORIZATION: + return "TPM2_RC_NV_AUTHORIZATION"; + case TPM2_RC_NV_UNINITIALIZED: + return "TPM2_RC_NV_UNINITIALIZED"; + case TPM2_RC_NV_SPACE: + return "TPM2_RC_NV_SPACE"; + case TPM2_RC_NV_DEFINED: + return "TPM2_RC_NV_DEFINED"; + case TPM2_RC_BAD_CONTEXT: + return "TPM2_RC_BAD_CONTEXT"; + case TPM2_RC_CPHASH: + return "TPM2_RC_CPHASH"; + case TPM2_RC_PARENT: + return "TPM2_RC_PARENT"; + case TPM2_RC_NEEDS_TEST: + return "TPM2_RC_NEEDS_TEST"; + case TPM2_RC_NO_RESULT: + return "TPM2_RC_NO_RESULT"; + case TPM2_RC_SENSITIVE: + return "TPM2_RC_SENSITIVE"; + case TPM2_RC_MAX_FM0: + return "TPM2_RC_MAX_FM0"; + case TPM2_RC_FMT1: + return "TPM2_RC_FMT1"; + case TPM2_RC_ASYMMETRIC: + return "TPM2_RC_ASYMMETRIC"; + case TPM2_RC_ATTRIBUTES: + return "TPM2_RC_ATTRIBUTES"; + case TPM2_RC_HASH: + return "TPM2_RC_HASH"; + case TPM2_RC_VALUE: + return "TPM2_RC_VALUE"; + case TPM2_RC_HIERARCHY: + return "TPM2_RC_HIERARCHY"; + case TPM2_RC_KEY_SIZE: + return "TPM2_RC_KEY_SIZE"; + case TPM2_RC_MGF: + return "TPM2_RC_MGF"; + case TPM2_RC_MODE: + return "TPM2_RC_MODE"; + case TPM2_RC_TYPE: + return "TPM2_RC_TYPE"; + case TPM2_RC_HANDLE: + return "TPM2_RC_HANDLE"; + case TPM2_RC_KDF: + return "TPM2_RC_KDF"; + case TPM2_RC_RANGE: + return "TPM2_RC_RANGE"; + case TPM2_RC_AUTH_FAIL: + return "TPM2_RC_AUTH_FAIL"; + case TPM2_RC_NONCE: + return "TPM2_RC_NONCE"; + case TPM2_RC_PP: + return "TPM2_RC_PP"; + case TPM2_RC_SCHEME: + return "TPM2_RC_SCHEME"; + case TPM2_RC_SIZE: + return "TPM2_RC_SIZE"; + case TPM2_RC_SYMMETRIC: + return "TPM2_RC_SYMMETRIC"; + case TPM2_RC_TAG: + return "TPM2_RC_TAG"; + case TPM2_RC_SELECTOR: + return "TPM2_RC_SELECTOR"; + case TPM2_RC_INSUFFICIENT: + return "TPM2_RC_INSUFFICIENT"; + case TPM2_RC_SIGNATURE: + return "TPM2_RC_SIGNATURE"; + case TPM2_RC_KEY: + return "TPM2_RC_KEY"; + case TPM2_RC_POLICY_FAIL: + return "TPM2_RC_POLICY_FAIL"; + case TPM2_RC_INTEGRITY: + return "TPM2_RC_INTEGRITY"; + case TPM2_RC_TICKET: + return "TPM2_RC_TICKET"; + case TPM2_RC_RESERVED_BITS: + return "TPM2_RC_RESERVED_BITS"; + case TPM2_RC_BAD_AUTH: + return "TPM2_RC_BAD_AUTH"; + case TPM2_RC_EXPIRED: + return "TPM2_RC_EXPIRED"; + case TPM2_RC_POLICY_CC: + return "TPM2_RC_POLICY_CC"; + case TPM2_RC_BINDING: + return "TPM2_RC_BINDING"; + case TPM2_RC_CURVE: + return "TPM2_RC_CURVE"; + case TPM2_RC_ECC_POINT: + return "TPM2_RC_ECC_POINT"; + case TPM2_RC_WARN: + return "TPM2_RC_WARN"; + case TPM2_RC_CONTEXT_GAP: + return "TPM2_RC_CONTEXT_GAP"; + case TPM2_RC_OBJECT_MEMORY: + return "TPM2_RC_OBJECT_MEMORY"; + case TPM2_RC_SESSION_MEMORY: + return "TPM2_RC_SESSION_MEMORY"; + case TPM2_RC_MEMORY: + return "TPM2_RC_MEMORY"; + case TPM2_RC_SESSION_HANDLES: + return "TPM2_RC_SESSION_HANDLES"; + case TPM2_RC_OBJECT_HANDLES: + return "TPM2_RC_OBJECT_HANDLES"; + case TPM2_RC_LOCALITY: + return "TPM2_RC_LOCALITY"; + case TPM2_RC_YIELDED: + return "TPM2_RC_YIELDED"; + case TPM2_RC_CANCELED: + return "TPM2_RC_CANCELED"; + case TPM2_RC_TESTING: + return "TPM2_RC_TESTING"; + case TPM2_RC_REFERENCE_H0: + return "TPM2_RC_REFERENCE_H0"; + case TPM2_RC_REFERENCE_H1: + return "TPM2_RC_REFERENCE_H1"; + case TPM2_RC_REFERENCE_H2: + return "TPM2_RC_REFERENCE_H2"; + case TPM2_RC_REFERENCE_H3: + return "TPM2_RC_REFERENCE_H3"; + case TPM2_RC_REFERENCE_H4: + return "TPM2_RC_REFERENCE_H4"; + case TPM2_RC_REFERENCE_H5: + return "TPM2_RC_REFERENCE_H5"; + case TPM2_RC_REFERENCE_H6: + return "TPM2_RC_REFERENCE_H6"; + case TPM2_RC_REFERENCE_S0: + return "TPM2_RC_REFERENCE_S0"; + case TPM2_RC_REFERENCE_S1: + return "TPM2_RC_REFERENCE_S1"; + case TPM2_RC_REFERENCE_S2: + return "TPM2_RC_REFERENCE_S2"; + case TPM2_RC_REFERENCE_S3: + return "TPM2_RC_REFERENCE_S3"; + case TPM2_RC_REFERENCE_S4: + return "TPM2_RC_REFERENCE_S4"; + case TPM2_RC_REFERENCE_S5: + return "TPM2_RC_REFERENCE_S5"; + case TPM2_RC_REFERENCE_S6: + return "TPM2_RC_REFERENCE_S6"; + case TPM2_RC_NV_RATE: + return "TPM2_RC_NV_RATE"; + case TPM2_RC_LOCKOUT: + return "TPM2_RC_LOCKOUT"; + case TPM2_RC_RETRY: + return "TPM2_RC_RETRY"; + case TPM2_RC_NV_UNAVAILABLE: + return "TPM2_RC_NV_UNAVAILABLE"; + + default: + return Botan::fmt("Unknown TSS2_RC: {}", rc); + } +} + +} // namespace + +Error::Error(std::string_view location, TSS2_RC rc) : + Exception(fmt("TPM2 Exception in {}: Code {} - {} ({})", + location, + raw_rc_to_string(get_raw_rc(rc)), + rc, + Tss2_RC_Decode(rc))), + m_rc(rc) {} + +std::string Error::error_message() const { + return Tss2_RC_Decode(m_rc); +} + +} // namespace Botan::TPM2 diff --git a/src/lib/prov/tpm2/tpm2_error.h b/src/lib/prov/tpm2/tpm2_error.h new file mode 100644 index 00000000000..b1798b7f7c7 --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_error.h @@ -0,0 +1,42 @@ +/* +* TPM 2 error handling +* (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_ERROR_H_ +#define BOTAN_TPM2_ERROR_H_ + +#include + +/// Forward declaration of TSS2 type for convenience +using TSS2_RC = uint32_t; + +namespace Botan::TPM2 { + +TSS2_RC get_raw_rc(TSS2_RC rc); + +class BOTAN_PUBLIC_API(3, 6) Error final : public Exception { + public: + Error(std::string_view location, TSS2_RC rc); + + ErrorType error_type() const noexcept override { return ErrorType::TPMError; } + + TSS2_RC code() const { return m_rc; } + + int error_code() const noexcept override { + // RC is uint32 but the maximum value is within int32 range as per tss2_common.h + return static_cast(m_rc); + } + + std::string error_message() const; + + private: + TSS2_RC m_rc; +}; + +} // namespace Botan::TPM2 + +#endif diff --git a/src/lib/prov/tpm2/tpm2_object.cpp b/src/lib/prov/tpm2/tpm2_object.cpp new file mode 100644 index 00000000000..fbc070bc8cd --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_object.cpp @@ -0,0 +1,161 @@ +/* +* TPM 2.0 Base Object handling +* (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 + +namespace Botan::TPM2 { + +namespace { + +using ObjectAttributesWrapper = + AttributeWrapper; + +} // namespace + +ObjectAttributes ObjectAttributes::read(TPMA_OBJECT attributes) { + return ObjectAttributesWrapper::read(attributes); +} + +TPMA_OBJECT ObjectAttributes::render(ObjectAttributes attributes) { + return ObjectAttributesWrapper::render(attributes); +} + +Object::Object(std::shared_ptr ctx) : m_ctx(std::move(ctx)), m_handles(std::make_unique()) { + BOTAN_ASSERT_NONNULL(m_ctx); +} + +Object::Object(std::shared_ptr ctx, ESYS_TR handle) : Object(std::move(ctx)) { + m_handles->transient = handle; +} + +Object::Object(Object&& other) noexcept : + m_ctx(std::move(other.m_ctx)), + m_handles(std::move(other.m_handles)), + m_public_info(std::move(other.m_public_info)) { + other.scrub(); +} + +Object::~Object() { + if(m_handles) { + flush(); + } +} + +Object& Object::operator=(Object&& other) noexcept { + if(this != &other) { + flush(); + m_ctx = std::move(other.m_ctx); + m_handles = std::move(other.m_handles); + m_public_info = std::move(other.m_public_info); + other.scrub(); + } + return *this; +} + +/// Flush the object's TPM handles as necessary +void Object::flush() const noexcept { + // Only purely transient objects have to be flushed + if(has_transient_handle()) { + if(has_persistent_handle()) { + Esys_TR_Close(*m_ctx, &m_handles->transient); + } else { + Esys_FlushContext(*m_ctx, m_handles->transient); + } + } +} + +/// Destroy the object's internal state, making the destructor a no-op. +/// No more operations except the destructor must be performed on that object. +void Object::scrub() { + m_ctx.reset(); + m_handles.reset(); + m_public_info.reset(); +} + +/// Flush the object's TPM handles and reset its internal state +void Object::_reset() noexcept { + flush(); + _disengage(); +} + +/// Reset the object's internal state without flushing its TPM handles +void Object::_disengage() noexcept { + m_handles = std::make_unique(); + m_public_info.reset(); +} + +bool Object::has_persistent_handle() const { + return m_handles->persistent.has_value(); +} + +bool Object::has_transient_handle() const { + return m_handles->transient != ESYS_TR_NONE; +} + +TPM2_HANDLE Object::persistent_handle() const { + BOTAN_STATE_CHECK(has_persistent_handle()); + return *m_handles->persistent; +} + +ESYS_TR Object::transient_handle() const noexcept { + return m_handles->transient; +} + +ObjectAttributes Object::attributes(const SessionBundle& sessions) const { + const auto attrs = _public_info(sessions).pub->publicArea.objectAttributes; + return ObjectAttributes::read(attrs); +} + +PublicInfo& Object::_public_info(const SessionBundle& sessions, std::optional expected_type) const { + if(!m_public_info) { + m_public_info = std::make_unique(); + + check_rc("Esys_ReadPublic", + Esys_ReadPublic(*m_ctx, + m_handles->transient, + sessions[0], + sessions[1], + sessions[2], + out_ptr(m_public_info->pub), + out_ptr(m_public_info->name), + out_ptr(m_public_info->qualified_name))); + BOTAN_ASSERT_NONNULL(m_public_info->pub); + + if(expected_type) { + BOTAN_STATE_CHECK(m_public_info->pub->publicArea.type == *expected_type); + } + } + + return *m_public_info; +} + +ObjectHandles& Object::handles() { + return *m_handles; +} + +} // namespace Botan::TPM2 diff --git a/src/lib/prov/tpm2/tpm2_object.h b/src/lib/prov/tpm2/tpm2_object.h new file mode 100644 index 00000000000..ba8ec83cf09 --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_object.h @@ -0,0 +1,139 @@ +/* +* TPM 2.0 Base Object handling +* (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_BASE_OBJECT_H_ +#define BOTAN_TPM2_BASE_OBJECT_H_ + +#include + +/// Forward declaration of TSS2 type for convenience +using TPMA_OBJECT = uint32_t; + +/// Forward declaration of TSS2 type for convenience +using TPMI_ALG_PUBLIC = uint16_t; + +namespace Botan::TPM2 { + +struct PublicInfo; +struct ObjectHandles; +class ObjectSetter; +class SessionBundle; + +/** + * See TPM 2.0 Part 2, Section 8.3.2 + */ +struct ObjectAttributes { + static ObjectAttributes read(TPMA_OBJECT attributes); + static TPMA_OBJECT render(ObjectAttributes attributes); + + /// The hierarchy of the object may or may not change (i.e. when keys are duplicated) + bool fixed_tpm = false; + + /// Saved contexts of this object may or may not be loaded after Startup(CLEAR) + bool st_clear = false; + + /// The parent of the object may or may not change + bool fixed_parent = false; + + /// Indicates that the TPM generated all of the sensitive data other than the authValue + bool sensitive_data_origin = false; + + /// USER role actions may or may not be performed without authorization (HMAC or password) + bool user_with_auth = false; + + /// ADMIN role actions may or may not require a policy session + bool admin_with_policy = false; + + /// If set, the object is not subject to dictionary attack protection + bool no_da = false; + + /// If not set, the object may be duplicated without an inner wrapper on the private portion + /// Otherwise, symmetricAlg must not be TPM_ALG_NULL and newParentHandle must not be TPM_RH_NULL + bool encrypted_duplication = false; + + /// Key usage is restricted to structures of known format + /// (e.g. it won't sign data whose hash was not calculated by the TPM) + bool restricted = false; + + /// The private portion of the key might be used for data decryption + bool decrypt = false; + + /// The private portion of the key might be used for data signing, or + /// data encryption (if the key is a symmetric key) + bool sign_encrypt = false; + + /// The private portion of the key might be used for X.509 certificate signing + /// (normal signing, via Esys_Sign(), of arbitrary data is not allowed) + bool x509sign = false; +}; + +/** + * Wraps and manages the lifetime of TPM2 object handles both for transient and + * persistent objects. When this object is destroyed, the handles are released + * accordingly. + * + * Note that some TSS2 library functions may internally release handles passed + * to them. In such cases, the Object instance can be disengaged, ensuring that + * the handles are not released twice. This is an internal functionality and + * should not be used directly. + */ +class BOTAN_PUBLIC_API(3, 6) Object { + public: + explicit Object(std::shared_ptr ctx); + + /** + * Create an object wrapper from a user-provided transient handle. + * + * Use this to wrap an externally created transient object handle + * into a Botan::TPM2::Object instance. This is useful when the object + * is created by the application and not by the Botan::TPM2 library. + * + * Note that this will take ownership of the ESYS_TR handle and will + * release it when the object is destroyed. + * + * @param ctx the TPM context to use + * @param handle the transient handle to wrap + */ + Object(std::shared_ptr ctx, ESYS_TR handle); + + virtual ~Object(); + Object(const Object&) = delete; + Object& operator=(const Object&) = delete; + Object(Object&& other) noexcept; + Object& operator=(Object&& other) noexcept; + + const std::shared_ptr& context() const { return m_ctx; } + + bool has_persistent_handle() const; + bool has_transient_handle() const; + + TPM2_HANDLE persistent_handle() const; + ESYS_TR transient_handle() const noexcept; + + ObjectAttributes attributes(const SessionBundle& sessions) const; + + void _reset() noexcept; + void _disengage() noexcept; + PublicInfo& _public_info(const SessionBundle& sessions, std::optional expected_type = {}) const; + + private: + friend class ObjectSetter; + ObjectHandles& handles(); + + void flush() const noexcept; + void scrub(); + + private: + std::shared_ptr m_ctx; + std::unique_ptr m_handles; + mutable std::unique_ptr m_public_info; +}; + +} // namespace Botan::TPM2 + +#endif diff --git a/src/lib/prov/tpm2/tpm2_session.cpp b/src/lib/prov/tpm2/tpm2_session.cpp new file mode 100644 index 00000000000..322aafcd11d --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_session.cpp @@ -0,0 +1,102 @@ +/* +* TPM 2 Auth Session Wrapper +* (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 + +namespace Botan::TPM2 { + +namespace { + +using SessionAttributesWrapper = + AttributeWrapper; + +} // namespace + +SessionAttributes SessionAttributes::read(TPMA_SESSION attributes) { + return SessionAttributesWrapper::read(attributes); +} + +TPMA_SESSION SessionAttributes::render(SessionAttributes attributes) { + return SessionAttributesWrapper::render(attributes); +} + +// static +std::shared_ptr Session::unauthenticated_session(const std::shared_ptr& ctx, + std::string_view sym_algo, + std::string_view hash_algo) { + Object session(ctx); + const auto auth_sym = get_tpm2_sym_cipher_spec(sym_algo); + const auto auth_hash_algo = get_tpm2_hash_type(hash_algo); + + BOTAN_ASSERT_NONNULL(ctx); + + check_rc("Esys_StartSession", + Esys_StartAuthSession(*ctx, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + nullptr /*NonceCaller generated automatically*/, + TPM2_SE_HMAC, + &auth_sym, + auth_hash_algo, + out_transient_handle(session))); + + return std::shared_ptr(new Session(std::move(session), + { + .continue_session = true, + .decrypt = true, + .encrypt = true, + })); +} + +Session::Session(Object session, SessionAttributes attributes) : m_session(std::move(session)) { + set_attributes(attributes); +} + +SessionAttributes Session::attributes() const { + TPMA_SESSION attrs; + check_rc("Esys_TRSess_GetAttributes", + Esys_TRSess_GetAttributes(*m_session.context(), m_session.transient_handle(), &attrs)); + return SessionAttributes::read(attrs); +} + +void Session::set_attributes(SessionAttributes attributes) { + check_rc("Esys_TRSess_SetAttributes", + Esys_TRSess_SetAttributes( + *m_session.context(), m_session.transient_handle(), SessionAttributes::render(attributes), 0xFF)); +} + +secure_vector Session::tpm_nonce() const { + unique_esys_ptr nonce; + check_rc("Esys_TRSess_GetNonceTPM", + Esys_TRSess_GetNonceTPM(*m_session.context(), m_session.transient_handle(), out_ptr(nonce))); + return copy_into>(*nonce); +} + +[[nodiscard]] detail::SessionHandle::operator ESYS_TR() && noexcept { + if(m_session) { + return m_session->get().transient_handle(); + } else { + return ESYS_TR_NONE; + } +} + +} // namespace Botan::TPM2 diff --git a/src/lib/prov/tpm2/tpm2_session.h b/src/lib/prov/tpm2/tpm2_session.h new file mode 100644 index 00000000000..f3033257ec6 --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_session.h @@ -0,0 +1,173 @@ +/* +* TPM 2 Auth Session Wrapper +* (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_SESSION_H_ +#define BOTAN_TPM2_SESSION_H_ + +#include +#include +#include + +#include +#include + +namespace Botan::TPM2 { + +using TPMA_SESSION = uint8_t; + +/** + * See TPM 2.0 Part 2, Section 8.4 + */ + +struct SessionAttributes { + static SessionAttributes read(TPMA_SESSION attributes); + static TPMA_SESSION render(SessionAttributes attributes); + + /// The session may or may not remain active after the successful completion of any command. + bool continue_session = false; + + /// Indicates that a command should only be executed if the session is exclusive. + bool audit_exclusive = false; + + /// Indicates that the audit digest should be initialized and exclusive status of the session SET + bool audit_reset = false; + + /// Indicates that the first parameter of the command is to be decrypted by the TPM + bool decrypt = false; + + /// Indicates that the first parameter of a command's response is to be encrypted by the TPM + bool encrypt = false; + + /// Indicates that the session is fused for audit and that audit_exclusive and audit_reset have meaning + bool audit = false; +}; + +class Session; + +namespace detail { + +/** + * This wraps a Session object and ensures that the session's attributes are + * restored to their original state after they have been modified by a (failing) + * TSS2 library function. + * + * This is a workaround for the fact that TSS2 library calls may modify the + * session's attributes and not reset them when the call fails. + */ +class BOTAN_UNSTABLE_API SessionHandle final { + public: + SessionHandle() = default; + + SessionHandle(const SessionHandle&) = delete; + SessionHandle& operator=(const SessionHandle&) = delete; + SessionHandle(SessionHandle&&) = delete; + SessionHandle& operator=(SessionHandle&&) = delete; + + ~SessionHandle(); + [[nodiscard]] operator ESYS_TR() && noexcept; + + private: + friend class Botan::TPM2::Session; + + SessionHandle(Session& session); + + private: + std::optional> m_session; + SessionAttributes m_original_attributes; +}; + +} // namespace detail + +class BOTAN_PUBLIC_API(3, 6) Session { + public: + /** + * Instantiate an unauthenticated session that allows for the encryption + * of sensitive parameters passed to and from the TPM. The application's + * random salt is generated automatically (via the software RNG in the + * TSS2's crypto backend). + * + * Note that such a session is not protected against man-in-the-middle + * attacks with access to the data channel between the application and + * the TPM. + * + * @param ctx the TPM context + * @param sym_algo the symmetric algorithm used for parameter encryption + * @param hash_algo the hash algorithm in the HMAC used for authentication + */ + static std::shared_ptr unauthenticated_session(const std::shared_ptr& ctx, + std::string_view sym_algo = "CFB(AES-256)", + std::string_view hash_algo = "SHA-256"); + + public: + /** + * Create a session object from a user-provided transient handle. + * + * Use this to wrap an externally created session handle into a + * Botan::TPM2::Session instance to use it with the Botan::TPM2 library. + * + * Note that this will take ownership of the ESYS_TR handle and will + * release it when the object is destroyed. + * + * @param ctx the TPM context to use + * @param session_handle the transient handle to wrap + */ + Session(std::shared_ptr ctx, ESYS_TR session_handle) : m_session(std::move(ctx), session_handle) {} + + [[nodiscard]] detail::SessionHandle handle() { return *this; } + + SessionAttributes attributes() const; + void set_attributes(SessionAttributes attributes); + + secure_vector tpm_nonce() const; + + private: + friend class detail::SessionHandle; + + Session(Object session, SessionAttributes attributes); + + ESYS_TR transient_handle() const noexcept { return m_session.transient_handle(); } + + private: + Object m_session; +}; + +inline detail::SessionHandle::~SessionHandle() { + if(m_session) { + m_session->get().set_attributes(m_original_attributes); + } +} + +inline detail::SessionHandle::SessionHandle(Session& session) : + m_session(session), m_original_attributes(session.attributes()) {} + +/** + * This bundles up to three sessions into a single object to be used in a + * single TSS2 library function call to simplify passing the sessions around + * internally. + */ +class SessionBundle { + public: + SessionBundle(std::shared_ptr s1 = nullptr, + std::shared_ptr s2 = nullptr, + std::shared_ptr s3 = nullptr) : + m_sessions({std::move(s1), std::move(s2), std::move(s3)}) {} + + [[nodiscard]] detail::SessionHandle operator[](size_t i) const noexcept { + if(m_sessions[i] == nullptr) { + return {}; + } else { + return m_sessions[i]->handle(); + } + } + + private: + std::array, 3> m_sessions; +}; + +} // namespace Botan::TPM2 + +#endif diff --git a/src/lib/prov/tpm2/tpm2_util.h b/src/lib/prov/tpm2/tpm2_util.h new file mode 100644 index 00000000000..76459ac11d2 --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_util.h @@ -0,0 +1,287 @@ +/* +* TPM 2 internal utilities +* (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_UTIL_H_ +#define BOTAN_TPM2_UTIL_H_ + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include + +// There's no obvious way to get the version of the TSS from its headers, +// instead the existence of certain return code macro definitions is used +// as sentinels to pinpoint the TSS' version. Namely: +// +// - TSS2_BASE_RC_CALLBACK_NULL -> 4.0.0 or later +// - TPM2_RC_FW_LIMITED -> 4.1.0 or later + +#if defined(TSS2_BASE_RC_CALLBACK_NULL) + // The crypto callbacks were added in tpm2-tss 4.0.0. + #define BOTAN_TSS2_SUPPORTS_CRYPTO_CALLBACKS + + // Error decoding was added in tpm2-tss 4.0.0 + #define BOTAN_TSS2_SUPPORTS_ERROR_DECODING +#endif + +#if defined(TPM2_RC_FW_LIMITED) + // The crypto callbacks for SM4 were added in tpm2-tss 4.1.0. + #define BOTAN_TSS2_SUPPORTS_SM4_IN_CRYPTO_CALLBACKS +#endif + +namespace Botan::TPM2 { + +/** + * Check the return code and throw an exception if some error occured. + * + * @throws TPM2::Error if an error occured. + */ +constexpr void check_rc(std::string_view location, TSS2_RC rc) { + if(rc != TSS2_RC_SUCCESS) { + throw Error(location, rc); + } +} + +/** + * Check the return code and throw an exception if an unexpected error occured. + * + * Errors that are listed in the `expected_errors` parameter are considered + * expected and will not cause an exception to be thrown. Instead the error + * code is decoded and returned to the caller for further processing. + * + * @throws TPM2::Error if an unexpected error occured. + * @returns TSS2_RC_SUCCESS or one of the expected error codes. + */ +template + requires(sizeof...(expected_errors) > 0) +[[nodiscard]] constexpr TSS2_RC check_rc_expecting(std::string_view location, TSS2_RC rc) { + // If the RC is success, we can return early and avoid the decoding. + if(rc == TSS2_RC_SUCCESS) { + return rc; + } + + // An error occured, we need to decode it to check if it was expected. + const TSS2_RC decoded_rc = get_raw_rc(rc); + + // Check if the error is one of the expected and return those to the caller. + const bool is_expected_by_caller = ((decoded_rc == expected_errors) || ...); + if(is_expected_by_caller) { + return decoded_rc; + } + + // The error was not expected, so call the normal error handling which + // will throw an exception. + check_rc(location, rc); + + // We know, rc is not 'success', so this won't ever be reached. + return rc; +} + +template +concept tpm2_buffer = requires(T t) { + { t.buffer } -> std::convertible_to; + { t.size } -> std::convertible_to; +}; + +/// Construct a std::span as a view into a TPM2 buffer +constexpr auto as_span(tpm2_buffer auto& data) { + return std::span{data.buffer, data.size}; +} + +/// 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); +} + +/// Create a TPM2 buffer from the provided @p data, assuming that the +/// provided @p data is not larger than the capacity of the buffer type. +template +constexpr T copy_into(std::span data) { + T result; + copy_into(result, data); + return result; +} + +/// Copy the content of the TPM2 buffer @p data into a new resizable byte buffer +/// of the user's choosing. +template +constexpr OutT copy_into(const tpm2_buffer auto& data) { + OutT result; + result.resize(data.size); + copy_mem(result, as_span(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); +} + +struct esys_liberator { + void operator()(void* handle) { Esys_Free(handle); } +}; + +/// A unique pointer type for ESYS handles that automatically frees the handle. +template +using unique_esys_ptr = std::unique_ptr; + +struct PublicInfo { + unique_esys_ptr pub; + unique_esys_ptr name; + unique_esys_ptr qualified_name; +}; + +struct ObjectHandles { + std::optional persistent = std::nullopt; + ESYS_TR transient = ESYS_TR_NONE; +}; + +/** + * Helper type setting a TPM2_HANDLE or ESYS_TR on a given instance of Object + * from a TSS2 library function's out parameter. + * + * This is not used directly, but through the out_transient_handle() and + * out_persistent_handle() respectively. + */ +class ObjectSetter { + public: + constexpr ObjectSetter(Object& object, bool persistent = false) : + m_object(object), m_persistent(persistent), m_handle(persistent ? 0 : ESYS_TR_NONE) {} + + constexpr ~ObjectSetter() noexcept { + if(!was_written()) { + return; + } + + if(m_persistent) { + m_object.handles().persistent = m_handle; + } else { + m_object.handles().transient = m_handle; + } + } + + ObjectSetter(const ObjectSetter&) = delete; + ObjectSetter(ObjectSetter&&) = delete; + ObjectSetter& operator=(const ObjectSetter&) = delete; + ObjectSetter& operator=(ObjectSetter&&) = delete; + + [[nodiscard]] constexpr operator uint32_t*() && noexcept { return &m_handle; } + + private: + constexpr bool was_written() const { return m_handle != (m_persistent ? 0 : ESYS_TR_NONE); } + + private: + Object& m_object; + bool m_persistent; + uint32_t m_handle; /// TPM2_HANDLE or ESYS_TR, both are typedefs to uint32_t +}; + +/// Helper to set the transient handle of an object from a TSS2 library +/// function's out parameter. +constexpr auto out_transient_handle(Object& object) { + return ObjectSetter{object, false}; +} + +/// Helper to set the persistent handle of an object from a TSS2 library +/// function's out parameter. +constexpr auto out_persistent_handle(Object& object) { + return ObjectSetter{object, true}; +} + +/// Helper for the AttributeWrapper to define mappings between +/// boolean members of a struct and the corresponding bit masks +template + requires std::is_member_object_pointer_v +struct PropMap { + FieldPointerT field; + MaskT mask; + + /// Access the boolean member 'field' from the given @p object + [[nodiscard]] constexpr bool& operator()(auto& object) const noexcept { return object.*field; } + + /// Read-only access the boolean member 'field' from the given @p object + [[nodiscard]] constexpr bool operator()(const auto& object) const noexcept { return object.*field; } +}; + +/// Deduction guide to simplify the creation of PropMap instances +template +PropMap(MaskT, FieldPointerT) -> PropMap; + +/** + * This is an internal helper structure to wrap TPMA_* attribute bit fields. + * + * @tparam UnderlyingT the TPMA_* bit field type + * @tparam AttributeWrapperT the C++ struct type that wraps the TPMA_* bit field + * @tparam props a bunch of std::pair mappping boolean members of + * AttributeWrapperT to the bit masks of the TPMA_* type + */ +template ... props> +class AttributeWrapper { + private: + template &> FnT> + static constexpr void for_all(FnT&& fn) { + (fn(props), ...); + } + + static consteval bool all_single_bit_bitmasks() { + bool result = true; + for_all([&](auto&& prop) { result = result && (std::popcount(prop.mask) == 1); }); + return result; + } + + static_assert(all_single_bit_bitmasks(), "props... must contain single-bit flags only"); + + public: + static constexpr UnderlyingT render(AttributeWrapperT attributes) { + UnderlyingT result = 0; + for_all([&](auto&& prop) { + if(prop(attributes)) { + result |= prop.mask; + } + }); + return result; + } + + static constexpr AttributeWrapperT read(UnderlyingT attributes) { + AttributeWrapperT result; + for_all([&](auto&& prop) { prop(result) = (attributes & prop.mask) != 0; }); + return result; + } +}; + +} // namespace Botan::TPM2 + +#endif diff --git a/src/tests/test_tpm2.cpp b/src/tests/test_tpm2.cpp new file mode 100644 index 00000000000..53ba5ff3693 --- /dev/null +++ b/src/tests/test_tpm2.cpp @@ -0,0 +1,150 @@ +/* +* (C) 2024 Jack Lloyd +* (C) 2024 René Meusel, Amos Treiber Rohde & Schwarz Cybersecurity +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include "tests.h" + +#include +#include +#include + +#if defined(BOTAN_HAS_TPM2) + #include + #include + + #include +#endif + +namespace Botan_Tests { + +#if defined(BOTAN_HAS_TPM2) +namespace { + +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") { + // skip the test if the special 'disabled' TCTI is configured + return {}; + } + + auto ctx = Botan::TPM2::Context::create(tcti_name, Test::options().tpm2_tcti_conf()); + if(ctx->vendor() != "SW TPM" || ctx->manufacturer() != "IBM") { + return {}; + } + + BOTAN_UNUSED(rng_tag); + + return ctx; +} + +Test::Result bail_out() { + Test::Result result("TPM2 test bail out"); + + if(Test::options().tpm2_tcti_name() == "disabled") { + result.test_note("TPM2 tests are disabled."); + return result; + } else { + result.test_failure("Not sure we're on a simulated TPM2, cautiously refusing any action."); + return result; + } +} + +std::vector test_tpm2_properties() { + auto ctx = get_tpm2_context(__func__); + if(!ctx) { + return {bail_out()}; + } + + return { + CHECK("Vendor and Manufacturer", + [&](Test::Result& result) { + result.test_eq("Vendor", ctx->vendor(), "SW TPM"); + result.test_eq("Manufacturer", ctx->manufacturer(), "IBM"); + }), + + CHECK("Max random bytes per request", + [&](Test::Result& result) { + const auto prop = ctx->max_random_bytes_per_request(); + result.test_gte("at least as long as SHA-256", prop, 32); + result.test_lte("at most as long as SHA-512", prop, 64); + }), + + CHECK("Supports basic algorithms", + [&](Test::Result& result) { + result.confirm("RSA is supported", ctx->supports_algorithm("RSA")); + result.confirm("AES-128 is supported", ctx->supports_algorithm("AES-128")); + result.confirm("AES-256 is supported", ctx->supports_algorithm("AES-256")); + result.confirm("SHA-1 is supported", ctx->supports_algorithm("SHA-1")); + result.confirm("SHA-256 is supported", ctx->supports_algorithm("SHA-256")); + result.confirm("OFB(AES-128) is supported", ctx->supports_algorithm("OFB(AES-128)")); + result.confirm("OFB is supported", ctx->supports_algorithm("OFB")); + }), + + CHECK("Unsupported algorithms aren't supported", + [&](Test::Result& result) { + result.confirm("Enigma is not supported", !ctx->supports_algorithm("Enigma")); + result.confirm("MD5 is not supported", !ctx->supports_algorithm("MD5")); + result.confirm("DES is not supported", !ctx->supports_algorithm("DES")); + result.confirm("OAEP(Keccak) is not supported", !ctx->supports_algorithm("OAEP(Keccak)")); + }), + }; +} + +std::vector test_tpm2_context() { + auto ctx = get_tpm2_context(__func__); + if(!ctx) { + return {bail_out()}; + } + + const auto persistent_key_id = Test::options().tpm2_persistent_rsa_handle(); + + return { + CHECK("Persistent handles", + [&](Test::Result& result) { + const auto handles = ctx->persistent_handles(); + result.confirm("At least one persistent handle", !handles.empty()); + result.confirm("SRK is in the list", Botan::value_exists(handles, 0x81000001)); + result.confirm("Test private key is in the list", Botan::value_exists(handles, persistent_key_id)); + result.confirm("Test persistence location is not in the list", + !Botan::value_exists(handles, persistent_key_id + 1)); + }), + }; +} + +std::vector test_tpm2_sessions() { + auto ctx = get_tpm2_context(__func__); + if(!ctx) { + return {bail_out()}; + } + + auto ok = [](Test::Result& result, std::string_view name, const std::shared_ptr& session) { + result.require(Botan::fmt("Session '{}' is non-null", name), session != nullptr); + result.confirm(Botan::fmt("Session '{}' has a valid handle", name), session->handle() != ESYS_TR_NONE); + result.confirm(Botan::fmt("Session '{}' has a non-empty nonce", name), !session->tpm_nonce().empty()); + }; + + return { + CHECK("Unauthenticated sessions", + [&](Test::Result& result) { + using Session = Botan::TPM2::Session; + + ok(result, "default", Session::unauthenticated_session(ctx)); + ok(result, "CFB(AES-128)", Session::unauthenticated_session(ctx, "CFB(AES-128)")); + ok(result, "CFB(AES-128),SHA-384", Session::unauthenticated_session(ctx, "CFB(AES-128)", "SHA-384")); + ok(result, "CFB(AES-128),SHA-1", Session::unauthenticated_session(ctx, "CFB(AES-128)", "SHA-1")); + }), + }; +} + +} // namespace + +BOTAN_REGISTER_TEST_FN("tpm2", "tpm2_props", test_tpm2_properties); +BOTAN_REGISTER_TEST_FN("tpm2", "tpm2_ctx", test_tpm2_context); +BOTAN_REGISTER_TEST_FN("tpm2", "tpm2_sessions", test_tpm2_sessions); + +#endif + +} // namespace Botan_Tests From ed3dd6fed300b8b1de517739c0780ff795092830 Mon Sep 17 00:00:00 2001 From: Amos Treiber Date: Thu, 5 Sep 2024 15:09:36 +0200 Subject: [PATCH 4/7] RandomNumberGenerator based on TPM 2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: René Meusel --- src/lib/prov/tpm2/info.txt | 1 + src/lib/prov/tpm2/tpm2_rng.cpp | 49 +++++++++++++++++++++++++ src/lib/prov/tpm2/tpm2_rng.h | 44 +++++++++++++++++++++++ src/tests/test_tpm2.cpp | 65 ++++++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+) create mode 100644 src/lib/prov/tpm2/tpm2_rng.cpp create mode 100644 src/lib/prov/tpm2/tpm2_rng.h diff --git a/src/lib/prov/tpm2/info.txt b/src/lib/prov/tpm2/info.txt index 48685e12ab8..b86268cb2fb 100644 --- a/src/lib/prov/tpm2/info.txt +++ b/src/lib/prov/tpm2/info.txt @@ -27,5 +27,6 @@ tpm2_util.h tpm2_context.h tpm2_error.h tpm2_object.h +tpm2_rng.h tpm2_session.h diff --git a/src/lib/prov/tpm2/tpm2_rng.cpp b/src/lib/prov/tpm2/tpm2_rng.cpp new file mode 100644 index 00000000000..a7f6d25dd69 --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_rng.cpp @@ -0,0 +1,49 @@ +/* +* TPM 2 RNG interface +* (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 + +namespace Botan::TPM2 { + +RandomNumberGenerator::RandomNumberGenerator(std::shared_ptr ctx, SessionBundle sessions) : + m_ctx(std::move(ctx)), m_sessions(std::move(sessions)) { + BOTAN_ASSERT_NONNULL(m_ctx); + m_max_tpm2_rng_bytes = m_ctx->max_random_bytes_per_request(); +} + +void RandomNumberGenerator::fill_bytes_with_input(std::span output, std::span input) { + constexpr size_t MAX_STIR_RANDOM_SIZE = 128; // From specification of tpm2-tool's tpm2_stirrandom + + BufferSlicer in(input); + while(!in.empty()) { + const size_t chunk = std::min(in.remaining(), MAX_STIR_RANDOM_SIZE); + const auto data = copy_into(in.take(chunk)); + + check_rc("Esys_StirRandom", Esys_StirRandom(*m_ctx, m_sessions[0], m_sessions[1], m_sessions[2], &data)); + } + BOTAN_ASSERT_NOMSG(in.empty()); + + BufferStuffer out(output); + while(!out.full()) { + unique_esys_ptr digest = nullptr; + const auto requested_bytes = std::min(out.remaining_capacity(), m_max_tpm2_rng_bytes); + check_rc("Esys_GetRandom", + Esys_GetRandom(*m_ctx, m_sessions[0], m_sessions[1], m_sessions[2], requested_bytes, out_ptr(digest))); + + BOTAN_ASSERT_NOMSG(digest->size == requested_bytes); + out.append(as_span(*digest)); + } + BOTAN_ASSERT_NOMSG(out.full()); +} + +} // namespace Botan::TPM2 diff --git a/src/lib/prov/tpm2/tpm2_rng.h b/src/lib/prov/tpm2/tpm2_rng.h new file mode 100644 index 00000000000..2180aff0984 --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_rng.h @@ -0,0 +1,44 @@ +/* +* TPM 2 RNG interface +* (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_RNG_H_ +#define BOTAN_TPM2_RNG_H_ + +#include +#include +#include + +namespace Botan::TPM2 { + +/** + * This class implements a random number generator that uses the TPM 2.0 device + * as a source of randomness. + */ +class BOTAN_PUBLIC_API(3, 6) RandomNumberGenerator final : public Hardware_RNG { + public: + RandomNumberGenerator(std::shared_ptr ctx, SessionBundle sessions = {}); + + bool accepts_input() const override { return true; } + + std::string name() const override { return "TPM2_RNG"; } + + bool is_seeded() const override { return true; } + + private: + void fill_bytes_with_input(std::span output, std::span input) override; + + private: + std::shared_ptr m_ctx; + SessionBundle m_sessions; + + size_t m_max_tpm2_rng_bytes; +}; + +} // namespace Botan::TPM2 + +#endif diff --git a/src/tests/test_tpm2.cpp b/src/tests/test_tpm2.cpp index 53ba5ff3693..b17c6ca0e08 100644 --- a/src/tests/test_tpm2.cpp +++ b/src/tests/test_tpm2.cpp @@ -13,6 +13,7 @@ #if defined(BOTAN_HAS_TPM2) #include + #include #include #include @@ -52,6 +53,19 @@ Test::Result bail_out() { } } +bool not_zero_64(std::span in) { + Botan::BufferSlicer bs(in); + + while(bs.remaining() > 8) { + if(Botan::load_be(bs.take<8>()) == 0) { + return false; + } + } + // Ignore remaining bytes + + return true; +} + std::vector test_tpm2_properties() { auto ctx = get_tpm2_context(__func__); if(!ctx) { @@ -139,11 +153,62 @@ std::vector test_tpm2_sessions() { }; } +std::vector test_tpm2_rng() { + auto ctx = get_tpm2_context(__func__); + if(!ctx) { + return {bail_out()}; + } + + auto rng = Botan::TPM2::RandomNumberGenerator(ctx, Botan::TPM2::Session::unauthenticated_session(ctx)); + + return { + CHECK("Basic functionalities", + [&](Test::Result& result) { + result.confirm("Accepts input", rng.accepts_input()); + result.confirm("Is seeded", rng.is_seeded()); + result.test_eq("Right name", rng.name(), "TPM2_RNG"); + + result.test_no_throw("Clear", [&] { rng.clear(); }); + }), + + CHECK("Random number generation", + [&](Test::Result& result) { + std::array buf1 = {}; + rng.randomize(buf1); + result.confirm("Is at least not 0 (8)", not_zero_64(buf1)); + + std::array buf2 = {}; + rng.randomize(buf2); + result.confirm("Is at least not 0 (15)", not_zero_64(buf2)); + + std::array buf3 = {}; + rng.randomize(buf3); + result.confirm("Is at least not 0 (256)", not_zero_64(buf3)); + }), + + CHECK("Randomize with inputs", + [&](Test::Result& result) { + std::array buf1 = {}; + rng.randomize_with_input(buf1, std::array{}); + result.confirm("Randomized with inputs is at least not 0 (9)", not_zero_64(buf1)); + + std::array buf2 = {}; + rng.randomize_with_input(buf2, std::array{}); + result.confirm("Randomized with inputs is at least not 0 (66)", not_zero_64(buf2)); + + std::array buf3 = {}; + rng.randomize_with_input(buf3, std::array{}); + result.confirm("Randomized with inputs is at least not 0 (256)", not_zero_64(buf3)); + }), + }; +} + } // namespace BOTAN_REGISTER_TEST_FN("tpm2", "tpm2_props", test_tpm2_properties); BOTAN_REGISTER_TEST_FN("tpm2", "tpm2_ctx", test_tpm2_context); BOTAN_REGISTER_TEST_FN("tpm2", "tpm2_sessions", test_tpm2_sessions); +BOTAN_REGISTER_TEST_FN("tpm2", "tpm2_rng", test_tpm2_rng); #endif From bbb56f5ea5444bbda47af870e3801cd5468ba47e Mon Sep 17 00:00:00 2001 From: Amos Treiber Date: Thu, 5 Sep 2024 15:22:01 +0200 Subject: [PATCH 5/7] Support asymmetric RSA keys on TPM 2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Along with the actual asymmetric algorithm support this adds a number of required auxiliaries. Most notably, TPM2::HashFunction is an internal class exposing the TPM's hashing functionality as a Botan::HashFunction. 'Restricted' keys must use this to obtain a token from the TPM proofing that the data was hashed (and validated) by the TPM. The RSA adapter is stashed into the 'tpm2_rsa' submodule so that RSA support can be disabled at compile time. To facilitate the addition of ECC keys, an abstract TPM2::PrivateKey class is always part of the main TPM2 module. Asymmetric keys can be created, loaded, persisted and evicted as needed. Finally, asymmetric keys (on the TPM) may now be used to establish authenticated sessions. Co-Authored-By: René Meusel --- src/lib/prov/tpm2/info.txt | 2 + src/lib/prov/tpm2/tpm2_context.cpp | 94 ++++ src/lib/prov/tpm2/tpm2_context.h | 15 + src/lib/prov/tpm2/tpm2_hash.cpp | 137 ++++++ src/lib/prov/tpm2/tpm2_hash.h | 69 +++ src/lib/prov/tpm2/tpm2_key.cpp | 265 ++++++++++++ src/lib/prov/tpm2/tpm2_key.h | 221 ++++++++++ src/lib/prov/tpm2/tpm2_rsa/info.txt | 16 + src/lib/prov/tpm2/tpm2_rsa/tpm2_rsa.cpp | 533 +++++++++++++++++++++++ src/lib/prov/tpm2/tpm2_rsa/tpm2_rsa.h | 119 +++++ src/lib/prov/tpm2/tpm2_session.cpp | 33 ++ src/lib/prov/tpm2/tpm2_session.h | 21 + src/tests/test_tpm2.cpp | 549 ++++++++++++++++++++++++ 13 files changed, 2074 insertions(+) create mode 100644 src/lib/prov/tpm2/tpm2_hash.cpp create mode 100644 src/lib/prov/tpm2/tpm2_hash.h create mode 100644 src/lib/prov/tpm2/tpm2_key.cpp create mode 100644 src/lib/prov/tpm2/tpm2_key.h create mode 100644 src/lib/prov/tpm2/tpm2_rsa/info.txt create mode 100644 src/lib/prov/tpm2/tpm2_rsa/tpm2_rsa.cpp create mode 100644 src/lib/prov/tpm2/tpm2_rsa/tpm2_rsa.h diff --git a/src/lib/prov/tpm2/info.txt b/src/lib/prov/tpm2/info.txt index b86268cb2fb..1538ceaf19c 100644 --- a/src/lib/prov/tpm2/info.txt +++ b/src/lib/prov/tpm2/info.txt @@ -20,12 +20,14 @@ pubkey tpm2_algo_mappings.h +tpm2_hash.h tpm2_util.h tpm2_context.h tpm2_error.h +tpm2_key.h tpm2_object.h tpm2_rng.h tpm2_session.h diff --git a/src/lib/prov/tpm2/tpm2_context.cpp b/src/lib/prov/tpm2/tpm2_context.cpp index b4494aabd30..92c1f1f06a0 100644 --- a/src/lib/prov/tpm2/tpm2_context.cpp +++ b/src/lib/prov/tpm2/tpm2_context.cpp @@ -8,6 +8,7 @@ #include +#include #include #include @@ -22,6 +23,12 @@ namespace Botan::TPM2 { +namespace { + +constexpr TPM2_HANDLE storage_root_key_handle = TPM2_HR_PERSISTENT + 1; + +} // namespace + struct Context::Impl { TSS2_TCTI_CONTEXT* m_tcti_ctx; ESYS_CONTEXT* m_ctx; @@ -227,6 +234,11 @@ size_t Context::max_random_bytes_per_request() const { return get_tpm_property(m_impl->m_ctx, TPM2_PT_MAX_DIGEST); } +std::unique_ptr Context::storage_root_key(std::span auth_value, + const SessionBundle& sessions) { + return TPM2::PrivateKey::load_persistent(shared_from_this(), storage_root_key_handle, auth_value, sessions); +} + std::vector Context::transient_handles() const { return get_tpm_property_list(m_impl->m_ctx, TPM2_TRANSIENT_FIRST, TPM2_MAX_CAP_HANDLES); } @@ -260,6 +272,88 @@ std::vector Context::persistent_handles() const { m_impl->m_ctx, TPM2_PERSISTENT_FIRST, TPM2_MAX_CAP_HANDLES); } +TPM2_HANDLE Context::persist(TPM2::PrivateKey& key, + const SessionBundle& sessions, + std::span auth_value, + std::optional persistent_handle) { + auto& handles = key.handles(); + + BOTAN_ARG_CHECK(!persistent_handle || !value_exists(persistent_handles(), persistent_handle.value()), + "Persistent handle already in use"); + BOTAN_ARG_CHECK(!handles.has_persistent_handle(), "Key already has a persistent handle assigned"); + + // 1. Decide on the location to persist the key to. + // This uses either the handle provided by the caller or a free handle. + const TPMI_DH_PERSISTENT new_persistent_handle = [&] { + if(persistent_handle.has_value()) { + return persistent_handle.value(); + } else { + const auto free_persistent_handle = find_free_persistent_handle(); + BOTAN_STATE_CHECK(free_persistent_handle.has_value()); + return free_persistent_handle.value(); + } + }(); + + // 2. Persist the transient key in the TPM's NV storage + // This will flush the transient key handle and replace it with a new + // transient handle that references the persisted key. + check_rc("Esys_EvictControl", + Esys_EvictControl(m_impl->m_ctx, + ESYS_TR_RH_OWNER /*TODO: hierarchy*/, + handles.transient_handle(), + sessions[0], + sessions[1], + sessions[2], + new_persistent_handle, + out_transient_handle(handles))); + BOTAN_ASSERT_NOMSG(handles.has_transient_handle()); + + // 3. Reset the auth value of the key object + // This is necessary to ensure that the key object remains usable after + // the transient handle was recreated inside Esys_EvictControl(). + if(!auth_value.empty()) { + const auto user_auth = copy_into(auth_value); + check_rc("Esys_TR_SetAuth", Esys_TR_SetAuth(m_impl->m_ctx, handles.transient_handle(), &user_auth)); + } + + // 4. Update the key object with the new persistent handle + // This double-checks that the key was persisted at the correct location, + // but also brings the key object into a consistent state. + check_rc("Esys_TR_GetTpmHandle", + Esys_TR_GetTpmHandle(m_impl->m_ctx, handles.transient_handle(), out_persistent_handle(handles))); + + BOTAN_ASSERT_NOMSG(handles.has_persistent_handle()); + BOTAN_ASSERT_EQUAL(new_persistent_handle, handles.persistent_handle(), "key was persisted at the correct location"); + + return new_persistent_handle; +} + +void Context::evict(std::unique_ptr key, const SessionBundle& sessions) { + BOTAN_ASSERT_NONNULL(key); + + auto& handles = key->handles(); + BOTAN_ARG_CHECK(handles.has_persistent_handle(), "Key does not have a persistent handle assigned"); + + // 1. Evict the key from the TPM's NV storage + // This will free the persistent handle, but the transient handle will + // still be valid. + ESYS_TR no_new_handle = ESYS_TR_NONE; + check_rc("Esys_EvictControl", + Esys_EvictControl(m_impl->m_ctx, + ESYS_TR_RH_OWNER /*TODO: hierarchy*/, + handles.transient_handle(), + sessions[0], + sessions[1], + sessions[2], + 0, + &no_new_handle)); + BOTAN_ASSERT(no_new_handle == ESYS_TR_NONE, "When deleting a key, no new handle is returned"); + + // 2. The persistent key was deleted and the transient key was flushed by + // Esys_EvictControl(). + handles._disengage(); +} + Context::~Context() { if(m_impl) { Esys_Finalize(&m_impl->m_ctx); diff --git a/src/lib/prov/tpm2/tpm2_context.h b/src/lib/prov/tpm2/tpm2_context.h index e0be2de5b0b..879903fe8d5 100644 --- a/src/lib/prov/tpm2/tpm2_context.h +++ b/src/lib/prov/tpm2/tpm2_context.h @@ -26,6 +26,7 @@ struct ESYS_CONTEXT; namespace Botan::TPM2 { +class PrivateKey; class SessionBundle; /** @@ -86,6 +87,20 @@ class BOTAN_PUBLIC_API(3, 6) Context final : public std::enable_shared_from_this std::vector persistent_handles() const; + /// Makes @p key persistent at location @p persistent_handle or any free + TPM2_HANDLE persist(TPM2::PrivateKey& key, + const SessionBundle& sessions, + std::span auth_value = {}, + std::optional persistent_handle = std::nullopt); + + /// Evicts a persistent @p key from the TPM. The key cannot be used after. + void evict(std::unique_ptr key, const SessionBundle& sessions); + + // TODO: Currently this assumes that the SRK is a persistent object, + // this assumption may not hold forever. + std::unique_ptr storage_root_key(std::span auth_value, + const SessionBundle& sessions); + private: Context(const char* tcti_nameconf); Context(const char* tcti_name, const char* tcti_conf); diff --git a/src/lib/prov/tpm2/tpm2_hash.cpp b/src/lib/prov/tpm2/tpm2_hash.cpp new file mode 100644 index 00000000000..dd0da8c9b5b --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_hash.cpp @@ -0,0 +1,137 @@ +/* +* TPM 2.0 Hash Function Wrappers +* (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 + +namespace Botan::TPM2 { + +HashFunction::HashFunction(std::shared_ptr ctx, + std::string_view algorithm, + TPMI_RH_HIERARCHY hierarchy, + SessionBundle sessions) : + m_hash_type(get_tpm2_hash_type(algorithm)), + m_hierarchy(hierarchy), + m_handle(std::move(ctx)), + m_sessions(std::move(sessions)) { + // When creating a new hash object we assume that the call will use it to + // hash data and therefore setup the hash object immediately. + lazy_setup(); +} + +std::string HashFunction::name() const { + return get_botan_hash_name(m_hash_type); +} + +size_t HashFunction::output_length() const { + switch(m_hash_type) { + case TPM2_ALG_SHA1: + return 20; + case TPM2_ALG_SHA256: + case TPM2_ALG_SHA3_256: + case TPM2_ALG_SM3_256: + return 32; + case TPM2_ALG_SHA384: + case TPM2_ALG_SHA3_384: + return 48; + case TPM2_ALG_SHA512: + case TPM2_ALG_SHA3_512: + return 64; + return 64; + default: + throw Invalid_State("TPM 2.0 hash object with unexpected hash type"); + } +} + +void HashFunction::clear() { + m_handle._reset(); +} + +std::unique_ptr HashFunction::copy_state() const { + throw Not_Implemented("TPM 2.0 hash functions do not support copy_state"); +} + +std::unique_ptr HashFunction::new_object() const { + return std::make_unique(m_handle.context(), name(), m_hierarchy, m_sessions); +} + +void HashFunction::lazy_setup() { + if(m_handle.has_transient_handle()) { + return; + } + + const auto auth = init_empty(); + const auto rc = check_rc_expecting("Esys_HashSequenceStart", + Esys_HashSequenceStart(*m_handle.context(), + m_sessions[0], + m_sessions[1], + m_sessions[2], + &auth, + m_hash_type, + out_transient_handle(m_handle))); + + if(rc == TPM2_RC_HASH) { + throw Lookup_Error(fmt("TPM 2.0 Hash {} is not supported", name())); + } +} + +void HashFunction::add_data(std::span input) { + lazy_setup(); + + BufferSlicer slicer(input); + while(slicer.remaining() > 0) { + const size_t chunk = std::min(slicer.remaining(), size_t(TPM2_MAX_DIGEST_BUFFER)); + const auto data = copy_into(slicer.take(chunk)); + check_rc( + "Esys_SequenceUpdate", + Esys_SequenceUpdate( + *m_handle.context(), m_handle.transient_handle(), m_sessions[0], m_sessions[1], m_sessions[2], &data)); + } + BOTAN_ASSERT_NOMSG(slicer.empty()); +} + +std::pair, unique_esys_ptr> HashFunction::final_with_ticket() { + BOTAN_STATE_CHECK(m_handle.has_transient_handle()); + + std::pair, unique_esys_ptr> result; + + const auto nodata = init_empty(); + check_rc("Esys_SequenceComplete", + Esys_SequenceComplete(*m_handle.context(), + m_handle.transient_handle(), + m_sessions[0], + m_sessions[1], + m_sessions[2], + &nodata, + m_hierarchy, + out_ptr(result.first), + out_ptr(result.second))); + BOTAN_ASSERT_NONNULL(result.first); + + // Esys_SequenceComplete() destroys the underlying transient object + // so we need to disengage it's RAII wrapper. + m_handle._disengage(); + + return result; +} + +void HashFunction::final_result(std::span output) { + const auto digest_and_ticket = final_with_ticket(); + BOTAN_ASSERT_NONNULL(digest_and_ticket.first); + BOTAN_ASSERT_NOMSG(digest_and_ticket.first->size <= output.size()); + BOTAN_DEBUG_ASSERT(digest_and_ticket.first->size == output_length()); + + copy_mem(output.first(output.size()), as_span(*digest_and_ticket.first)); +} + +} // namespace Botan::TPM2 diff --git a/src/lib/prov/tpm2/tpm2_hash.h b/src/lib/prov/tpm2/tpm2_hash.h new file mode 100644 index 00000000000..0008c28d9b0 --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_hash.h @@ -0,0 +1,69 @@ +/* +* TPM 2.0 Hash Function Wrappers +* (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_HASH_H_ +#define BOTAN_TPM2_HASH_H_ + +#include +#include +#include +#include + +#include + +#include + +namespace Botan::TPM2 { + +/** + * Exposes the hashing capability of a TPM 2.0 device as a Botan::HashFunction. + * Typically this is used to obtain a TPMT_TK_HASHCHECK ticket after the hash + * operation has been completed. Otherwise, the HashFunction behaves like any + * other Botan::HashFunction. + */ +class BOTAN_TEST_API HashFunction final : public Botan::HashFunction { + public: + HashFunction(std::shared_ptr ctx, + std::string_view algorithm, + TPMI_RH_HIERARCHY hierarchy = ESYS_TR_RH_NULL, + SessionBundle sessions = {}); + + std::string name() const override; + size_t output_length() const override; + void clear() override; + + /// @throws Not_Implemented as copying state is not supported within the TPM + std::unique_ptr copy_state() const override; + std::unique_ptr new_object() const override; + + /// @return The hash algorithm identifier as TSS2's TPMI_ALG_HASH + TPMI_ALG_HASH type() const { return m_hash_type; } + + /** + * Finalize the hash operation and return the digest and the ticket + * as TSS2 structures. + * + * @return A pair of TPM2B_DIGEST and TPMT_TK_HASHCHECK + */ + std::pair, unique_esys_ptr> final_with_ticket(); + + private: + void lazy_setup(); + void add_data(std::span input) override; + void final_result(std::span output) override; + + private: + TPMI_ALG_HASH m_hash_type; + TPMI_RH_HIERARCHY m_hierarchy; + Object m_handle; + SessionBundle m_sessions; +}; + +} // namespace Botan::TPM2 + +#endif diff --git a/src/lib/prov/tpm2/tpm2_key.cpp b/src/lib/prov/tpm2/tpm2_key.cpp new file mode 100644 index 00000000000..f405cd8be61 --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_key.cpp @@ -0,0 +1,265 @@ +/* +* TPM 2.0 Key Wrappers' Base Class +* (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 + +#if defined(BOTAN_HAS_TPM2_RSA_ADAPTER) + #include +#endif + +#include +#include +#include +#include +#include + +#include +#include + +namespace Botan::TPM2 { + +namespace { + +Object load_persistent_object(const std::shared_ptr& ctx, + TPM2_HANDLE persistent_object_handle, + std::span auth_value, + const SessionBundle& sessions) { + BOTAN_ARG_CHECK( + TPM2_PERSISTENT_FIRST <= persistent_object_handle && persistent_object_handle <= TPM2_PERSISTENT_LAST, + "persistent_object_handle out of range"); + BOTAN_ASSERT_NONNULL(ctx); + const bool is_persistent = value_exists(ctx->persistent_handles(), persistent_object_handle); + BOTAN_STATE_CHECK(is_persistent); + + Object object(ctx); + + check_rc("Esys_TR_FromTPMPublic", + Esys_TR_FromTPMPublic( + *ctx, persistent_object_handle, sessions[0], sessions[1], sessions[2], out_transient_handle(object))); + + if(!auth_value.empty()) { + const auto user_auth = copy_into(auth_value); + check_rc("Esys_TR_SetAuth", Esys_TR_SetAuth(*ctx, object.transient_handle(), &user_auth)); + } + + check_rc("Esys_TR_GetTpmHandle", + Esys_TR_GetTpmHandle(*ctx, object.transient_handle(), out_persistent_handle(object))); + + const auto key_type = object._public_info(sessions).pub->publicArea.type; + BOTAN_ARG_CHECK(key_type == TPM2_ALG_RSA || key_type == TPM2_ALG_ECC, + "persistent object is neither RSA nor ECC public key"); + + return object; +} + +std::vector marshal_public_blob(const TPM2B_PUBLIC* public_data) { + size_t bytes_required = 0; + std::vector marshalled_blob(sizeof(TPM2B_PUBLIC)); + check_rc("Tss2_MU_TPM2B_PUBLIC_Marshal", + Tss2_MU_TPM2B_PUBLIC_Marshal(public_data, marshalled_blob.data(), marshalled_blob.size(), &bytes_required)); + marshalled_blob.resize(bytes_required); + marshalled_blob.shrink_to_fit(); + return marshalled_blob; +} + +TPM2B_PUBLIC unmarshal_public_blob(std::span marshalled_blob) { + TPM2B_PUBLIC public_data{}; + size_t offset = 0; + check_rc("Tss2_MU_TPM2B_PUBLIC_Unmarshal", + Tss2_MU_TPM2B_PUBLIC_Unmarshal(marshalled_blob.data(), marshalled_blob.size(), &offset, &public_data)); + BOTAN_ASSERT_NOMSG(offset == marshalled_blob.size()); + return public_data; +} + +TPM2B_TEMPLATE marshal_template(const TPMT_PUBLIC& key_template) { + TPM2B_TEMPLATE result = {}; + size_t offset = 0; + check_rc("Tss2_MU_TPMT_PUBLIC_Marshal", + Tss2_MU_TPMT_PUBLIC_Marshal(&key_template, result.buffer, sizeof(TPMT_PUBLIC), &offset)); + result.size = offset; + return result; +} + +} // namespace + +std::unique_ptr PublicKey::load_persistent(const std::shared_ptr& ctx, + TPM2_HANDLE persistent_object_handle, + const SessionBundle& sessions) { + return create(load_persistent_object(ctx, persistent_object_handle, {}, sessions), sessions); +} + +std::unique_ptr PublicKey::load_transient(const std::shared_ptr& ctx, + std::span public_blob, + const SessionBundle& sessions) { + const auto public_data = unmarshal_public_blob(public_blob); + + BOTAN_ASSERT_NONNULL(ctx); + + Object handle(ctx); + check_rc("Esys_LoadExternal", + Esys_LoadExternal(*ctx, + sessions[0], + sessions[1], + sessions[2], + nullptr /* no private data to be loaded */, + &public_data, + TPM2_RH_NULL, + out_transient_handle(handle))); + return create(std::move(handle), sessions); +} + +std::vector PublicKey::raw_public_key_bits() const { + return marshal_public_blob(m_handle._public_info(m_sessions).pub.get()); +} + +std::unique_ptr PublicKey::create(Object handles, const SessionBundle& sessions) { + [[maybe_unused]] const auto* pubinfo = handles._public_info(sessions).pub.get(); +#if defined(BOTAN_HAS_TPM2_RSA_ADAPTER) + if(pubinfo->publicArea.type == TPM2_ALG_RSA) { + return std::unique_ptr(new RSA_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")); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +std::unique_ptr PrivateKey::load_persistent(const std::shared_ptr& ctx, + TPM2_HANDLE persistent_object_handle, + std::span auth_value, + const SessionBundle& sessions) { + return create(load_persistent_object(ctx, persistent_object_handle, auth_value, sessions), + sessions, + nullptr /* pull public info from handle */, + {} /* persistent keys don't have an encrypted private blob */); +} + +std::unique_ptr PrivateKey::load_transient(const std::shared_ptr& ctx, + std::span auth_value, + const TPM2::PrivateKey& parent, + std::span public_blob, + std::span private_blob, + const SessionBundle& sessions) { + BOTAN_ASSERT_NONNULL(ctx); + Object handle(ctx); + + const auto public_data = unmarshal_public_blob(public_blob); + const auto private_data = copy_into(private_blob); + + check_rc("Esys_Load", + Esys_Load(*ctx, + parent.handles().transient_handle(), + sessions[0], + sessions[1], + sessions[2], + &private_data, + &public_data, + out_transient_handle(handle))); + + if(!auth_value.empty()) { + const auto user_auth = copy_into(auth_value); + check_rc("Esys_TR_SetAuth", Esys_TR_SetAuth(*ctx, handle.transient_handle(), &user_auth)); + } + + return create(std::move(handle), sessions, nullptr /* pull public info from handle */, private_blob); +} + +std::unique_ptr PrivateKey::create_transient_from_template(const std::shared_ptr& ctx, + const SessionBundle& sessions, + ESYS_TR parent, + const TPMT_PUBLIC& key_template, + const TPM2B_SENSITIVE_CREATE& sensitive_data) { + BOTAN_ASSERT_NONNULL(ctx); + + switch(key_template.type) { + case TPM2_ALG_RSA: +#if not defined(BOTAN_HAS_TPM2_RSA_ADAPTER) + throw Not_Implemented("TPM2-based RSA keys are not supported in this build"); +#endif + break; + case TPM2_ALG_ECC: + // TODO: support ECC keys + throw Not_Implemented("TPM2-based ECC keys are not yet supported"); + break; + default: + throw Invalid_Argument("Unsupported key type"); + } + + const auto marshalled_template = marshal_template(key_template); + + Object handle(ctx); + unique_esys_ptr private_bytes; + unique_esys_ptr public_info; + + // Esys_CreateLoaded can create different object types depending on the type + // of the parent passed in. Namely, this will create a Primary object if the + // parent is referencing a Primary Seed; an Ordinary Object if the parent is + // referencing a Storage Parent; and a Derived Object if the parent is + // referencing a Derivation Parent. + // + // See the Architecture Document, Section 27.1. + check_rc("Esys_CreateLoaded", + Esys_CreateLoaded(*ctx, + parent, + sessions[0], + sessions[1], + sessions[2], + &sensitive_data, + &marshalled_template, + out_transient_handle(handle), + out_ptr(private_bytes), + out_ptr(public_info))); + BOTAN_ASSERT_NONNULL(private_bytes); + BOTAN_ASSERT_NOMSG(public_info->publicArea.type == key_template.type); + BOTAN_ASSERT_NOMSG(handle.has_transient_handle()); + + return create(std::move(handle), sessions, public_info.get(), as_span(*private_bytes)); +} + +secure_vector PrivateKey::raw_private_key_bits() const { + BOTAN_STATE_CHECK(!m_handle.has_persistent_handle()); + BOTAN_ASSERT_NOMSG(!m_private_blob.empty()); + return Botan::lock(m_private_blob); +} + +std::vector PrivateKey::raw_public_key_bits() const { + return marshal_public_blob(m_handle._public_info(m_sessions).pub.get()); +} + +bool PrivateKey::is_parent() const { + // Architectural Document, Section 4.54 + // any object with the decrypt and restricted attributes SET and the sign + // attribute CLEAR + const auto attrs = m_handle.attributes(m_sessions); + return attrs.decrypt && attrs.restricted && !attrs.sign_encrypt; +} + +std::unique_ptr PrivateKey::create(Object handles, + [[maybe_unused]] const SessionBundle& sessions, + [[maybe_unused]] const TPM2B_PUBLIC* public_info, + [[maybe_unused]] std::span private_blob) { + if(!public_info) { + public_info = handles._public_info(sessions).pub.get(); + } + +#if defined(BOTAN_HAS_TPM2_RSA_ADAPTER) + if(public_info->publicArea.type == TPM2_ALG_RSA) { + return std::unique_ptr( + new RSA_PrivateKey(std::move(handles), sessions, public_info, private_blob)); + } +#endif + + // TODO: Support ECC keys + + throw Not_Implemented(Botan::fmt("Loaded a {} private key of an unsupported type", + handles.has_persistent_handle() ? "persistent" : "transient")); +} + +} // namespace Botan::TPM2 diff --git a/src/lib/prov/tpm2/tpm2_key.h b/src/lib/prov/tpm2/tpm2_key.h new file mode 100644 index 00000000000..a8f96af551d --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_key.h @@ -0,0 +1,221 @@ +/* +* TPM 2.0 Key Wrappers' Base Class +* (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_ASYM_KEYS_H_ +#define BOTAN_TPM2_ASYM_KEYS_H_ + +#include +#include +#include +#include + +struct TPM2B_SENSITIVE_CREATE; +struct TPMT_PUBLIC; +struct TPM2B_PUBLIC; + +namespace Botan::TPM2 { + +/** + * 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 + * methods to obtain a public key handle from a TPM. + */ +class BOTAN_PUBLIC_API(3, 6) PublicKey : public virtual Botan::Public_Key { + public: + /** + * Load a public key that resides in the TPM's persistent storage. + * + * @param ctx The TPM context to use + * @param persistent_object_handle The handle of the persistent object to load + * @param sessions The session bundle to use for loading + */ + static std::unique_ptr load_persistent(const std::shared_ptr& ctx, + TPM2_HANDLE persistent_object_handle, + const SessionBundle& sessions = {}); + + /** + * Load a public key from the public blob obtained by a TPM key creation. + * + * Transient keys don't reside inside the TPM but must be loaded by the + * application as required. Once this object is destructed, the transient + * memory on the TPM is cleared. + * + * @param ctx The TPM context to use + * @param public_blob The public blob of the key to load + * @param sessions The session bundle to use for loading + */ + static std::unique_ptr load_transient(const std::shared_ptr& ctx, + std::span public_blob, + const SessionBundle& sessions); + + public: + std::unique_ptr generate_another(Botan::RandomNumberGenerator&) const override { + throw Not_Implemented("Cannot generate a new TPM-based keypair from this asymmetric key"); + } + + /** + * @returns a TPM2-specific marshalled representation of the public key + */ + std::vector raw_public_key_bits() const override; + + const Object& handles() const { return m_handle; } + + const SessionBundle& sessions() const { return m_sessions; } + + protected: + PublicKey(Object object, SessionBundle sessions) : m_handle(std::move(object)), m_sessions(std::move(sessions)) {} + + static std::unique_ptr create(Object handles, const SessionBundle& sessions); + + private: + Object m_handle; + SessionBundle m_sessions; +}; + +BOTAN_DIAGNOSTIC_PUSH +BOTAN_DIAGNOSTIC_IGNORE_INHERITED_VIA_DOMINANCE + +/** + * This wraps a private key that is hosted in a TPM 2.0 device. This class + * allows performing private-key operations on the TPM. Namely signing and + * decrypting data. + * + * Note that there are two types of keys: persistent and transient. Persistent + * keys are stored in the TPM's NVRAM and can be loaded at any time. Transient + * keys are loaded by the application from an encrypted private blob that is + * only readable by the TPM that created it. Once the key is loaded, the + * application can use it as if it were a persistent key. Once the key is + * destructed, the transient memory on the TPM is cleared. + * + * Applications may persist transient keys in the TPM's NVRAM by using the + * TPM2_Context::persist() method. This allows the key to be loaded at a later + * time without the need to provide the encrypted private blob. Similarly, + * persistent keys may be permanently destroyed using TPM2_Context::evict(). + * + * To obtain the public and private blobs of a transient key, use the + * raw_public_key_bits() and raw_private_key_bits() methods, respectively. + * + * The class does not provide public constructors, but instead provides static + * methods to obtain a private key handle from a TPM. + */ +class BOTAN_PUBLIC_API(3, 6) PrivateKey : public virtual Private_Key { + public: + /** + * Load a private key that resides in the TPM's persistent storage. + * + * @param ctx The TPM context to use + * @param persistent_object_handle The handle of the persistent object to load + * @param auth_value The auth value required to use the key + * @param sessions The session bundle to use for the key's operations + */ + static std::unique_ptr load_persistent(const std::shared_ptr& ctx, + TPM2_HANDLE persistent_object_handle, + std::span auth_value, + const SessionBundle& sessions); + + /** + * Load a private key from the public and private blobs obtained by a TPM + * key creation. + * + * Transient keys don't reside inside the TPM but must be loaded by the + * application as required. Once this object is destructed, the transient + * memory on the TPM is cleared. + * + * @param ctx The TPM context to use + * @param auth_value The auth value required to use the key + * @param parent The parent key the key was originally created under + * @param public_blob The public blob of the key to load + * @param private_blob The private blob of the key to load + * @param sessions The session bundle to use for loading + */ + static std::unique_ptr load_transient(const std::shared_ptr& ctx, + std::span auth_value, + const TPM2::PrivateKey& parent, + std::span public_blob, + std::span private_blob, + const SessionBundle& sessions); + + /** + * This is a wrapper around Esys_CreateLoaded creating a transient key + * from a given @p key_template with @p sensitive_data. It gives maximal + * flexibility to the caller to create a key with their own TSS2 template + * configuration. + * + * Please use this if you know what you are doing, only! Most users should + * use the more convenient create_transient() methods of the derived classes. + * + * @param ctx The TPM context to use + * @param sessions The session bundle to use in Esys_CreateLoaded(). + * @param parent The handle of the parent object to create the new key under + * (this may reference a "Primary Seed" to create a "Primary Key", + * a "Storage Parent" to create an "Ordinary Key", or + * a "Derivation Parent" to create a "Derived Key"). + * @param key_template The template data to use for the key creation. It + * will be passed to Tss2_MU_TPMT_PUBLIC_Marshal() and + * Esys_CreateLoaded(). + * @param sensitive_data The sensitive data (e.g. with the desired auth + * value) to use for the key creation. + */ + static std::unique_ptr create_transient_from_template(const std::shared_ptr& ctx, + const SessionBundle& sessions, + ESYS_TR parent, + const TPMT_PUBLIC& key_template, + const TPM2B_SENSITIVE_CREATE& sensitive_data); + + public: + /// @throws Not_Implemented keys hosted in a TPM2 cannot be exported + secure_vector private_key_bits() const override { + throw Not_Implemented("cannot export private key bits from a TPM2 key, maybe use raw_private_key_bits()?"); + } + + /** + * @returns the encrypted private key blob, if the key is transient + * @throws Invalid_State if the key is persistent + */ + secure_vector raw_private_key_bits() const override; + + /** + * @returns a TPM2-specific marshalled representation of the public key + */ + std::vector raw_public_key_bits() const override; + + Object& handles() { return m_handle; } + + const Object& handles() const { return m_handle; } + + const SessionBundle& sessions() const { return m_sessions; } + + bool is_parent() const; + + protected: + PrivateKey(Object handle, SessionBundle sessions, std::span private_blob = {}) : + m_handle(std::move(handle)), + m_sessions(std::move(sessions)), + m_private_blob(private_blob.begin(), private_blob.end()) {} + + static std::unique_ptr create(Object handles, + const SessionBundle& sessions, + const TPM2B_PUBLIC* public_info, + std::span private_blob); + + private: + Object m_handle; + SessionBundle m_sessions; + + /// Transient keys can be exported as an encrypted private blob that is + /// readable by the TPM that created it. + std::vector m_private_blob; +}; + +BOTAN_DIAGNOSTIC_POP + +} // namespace Botan::TPM2 + +#endif diff --git a/src/lib/prov/tpm2/tpm2_rsa/info.txt b/src/lib/prov/tpm2/tpm2_rsa/info.txt new file mode 100644 index 00000000000..3afea9a3ea8 --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_rsa/info.txt @@ -0,0 +1,16 @@ + +TPM2_RSA_ADAPTER -> 20240819 + + + +name -> "TPM2 RSA Adapter" +brief -> "Support for RSA key pairs hosted on TPM 2.0" + + + +rsa + + + +tpm2_rsa.h + diff --git a/src/lib/prov/tpm2/tpm2_rsa/tpm2_rsa.cpp b/src/lib/prov/tpm2/tpm2_rsa/tpm2_rsa.cpp new file mode 100644 index 00000000000..08b47714e0f --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_rsa/tpm2_rsa.cpp @@ -0,0 +1,533 @@ +/* +* TPM 2.0 RSA Key Wrappres +* (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 +#include +#include +#include +#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)) {} + +RSA_PrivateKey::RSA_PrivateKey(Object handle, + SessionBundle session_bundle, + const TPM2B_PUBLIC* public_blob, + std::span private_blob) : + Botan::TPM2::PrivateKey(std::move(handle), std::move(session_bundle), private_blob), + Botan::RSA_PublicKey(rsa_pubkey_from_tss2_public(public_blob)) {} + +std::unique_ptr RSA_PrivateKey::create_unrestricted_transient(const std::shared_ptr& ctx, + const SessionBundle& sessions, + std::span auth_value, + const TPM2::PrivateKey& parent, + uint16_t keylength, + std::optional exponent) { + BOTAN_ARG_CHECK(parent.is_parent(), "The passed key cannot be used as a parent key"); + + 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_RSA, + + // 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, + .sign_encrypt = true, + }), + + // We currently do not support policy-based authorization + .authPolicy = init_empty(), + .parameters = + { + .rsaDetail = + { + // 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.5 + // When both sign and decrypt are SET, restricted shall be + // CLEAR and scheme shall be 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}}, + }, + .keyBits = keylength, + .exponent = exponent.value_or(0 /* default value - 2^16 + 1*/), + }, + }, + + // For creating an asymmetric key this value is not used. + .unique = {.rsa = init_empty()}, + }; + + return create_transient_from_template( + ctx, sessions, parent.handles().transient_handle(), key_template, sensitive_data); +} + +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) { + throw Invalid_Argument("RSA signing padding scheme must at least specify a hash function"); + } + + auto sig_scheme = rsa_signature_scheme_botan_to_tss2(padding); + if(!sig_scheme) { + throw Not_Implemented(Botan::fmt("RSA signing with padding scheme {}", padding)); + } + + return { + .signature_scheme = sig_scheme.value(), + .hash_name = req.arg(0), + .padding = std::string(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); + } +} + +/** + * 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); + } + + 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; + } + + std::string hash_function() const override { return m_hash->name(); } + + AlgorithmIdentifier algorithm_identifier() const override { + // TODO: This is essentially a copy of the ::algorithm_identifier() + // in `rsa.h`. We should probably refactor this into a common + // function. + + // This EMSA object actually isn't required, we just need it to + // 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); + const std::string emsa_name = emsa->name(); + + try { + const std::string full_name = "RSA/" + emsa_name; + const OID oid = OID::from_string(full_name); + return AlgorithmIdentifier(oid, AlgorithmIdentifier::USE_EMPTY_PARAM); + } catch(Lookup_Error&) {} + + if(emsa_name.starts_with("EMSA4(")) { + auto parameters = PSS_Params::from_emsa_name(emsa_name).serialize(); + return AlgorithmIdentifier("RSA/EMSA4", parameters); + } + + throw Not_Implemented("No algorithm identifier defined for RSA with " + emsa_name); + } + + 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; + } + + throw Invalid_State(fmt("TPM2 returned an unexpected signature scheme {}", signature->sigAlg)); + }(); + + BOTAN_ASSERT_NOMSG(sig.hash == m_scheme.details.any.hashAlg); + + 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)) {} + + public: + RSA_Verification_Operation(const Object& object, const SessionBundle& sessions, std::string_view padding) : + RSA_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)); + + const auto signature = [&]() -> TPMT_SIGNATURE { + TPMT_SIGNATURE sig; + sig.sigAlg = m_scheme.scheme; + sig.signature.any.hashAlg = m_scheme.details.any.hashAlg; + + if(sig.sigAlg == TPM2_ALG_RSASSA) { + copy_into(sig.signature.rsassa.sig, sig_data); + } 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; + }(); + + // 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; + } + + 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) { + auto scheme = rsa_encryption_scheme_botan_to_tss2(padding); + if(!scheme) { + throw Not_Implemented(Botan::fmt("RSA encryption with padding scheme {}", padding)); + } + return scheme.value(); +} + +class RSA_Encryption_Operation final : public PK_Ops::Encryption { + public: + RSA_Encryption_Operation(const Object& object, const SessionBundle& sessions, std::string_view padding) : + m_key_handle(object), m_sessions(sessions), m_scheme(select_encryption_algorithms(padding)) {} + + std::vector encrypt(std::span msg, Botan::RandomNumberGenerator& /* rng */) override { + const auto plaintext = copy_into(msg); + + // TODO: Figure out what this is for. Given that I didn't see any other + // way to pass an EME-OAEP label, I'm guessing that this is what + // it is for. But I'm not sure. + // + // Again, a follow-up of https://github.com/randombit/botan/pull/4318 + // that targets async encryption will probably be quite helpful here. + const auto label = init_empty(); + + unique_esys_ptr ciphertext; + check_rc("Esys_RSA_Encrypt", + Esys_RSA_Encrypt(*m_key_handle.context(), + m_key_handle.transient_handle(), + m_sessions[0], + m_sessions[1], + m_sessions[2], + &plaintext, + &m_scheme, + &label, + out_ptr(ciphertext))); + BOTAN_ASSERT_NONNULL(ciphertext); + return copy_into>(*ciphertext); + } + + // This duplicates quite a bit of domain knowledge about those RSA + // EMEs. And I'm quite certain that I screwed up somewhere. + // + // TODO: See if we can somehow share the logic with the software + // RSA implementation and also PKCS#11 (which I believe is plain wrong). + size_t max_input_bits() const override { + const auto max_ptext_bytes = + (m_key_handle._public_info(m_sessions, TPM2_ALG_RSA).pub->publicArea.parameters.rsaDetail.keyBits - 1) / 8; + auto hash_output_bytes = [](TPM2_ALG_ID hash) -> size_t { + switch(hash) { + case TPM2_ALG_SHA1: + return 160 / 8; + case TPM2_ALG_SHA256: + case TPM2_ALG_SHA3_256: + return 256 / 8; + case TPM2_ALG_SHA384: + case TPM2_ALG_SHA3_384: + return 384 / 8; + case TPM2_ALG_SHA512: + case TPM2_ALG_SHA3_512: + return 512 / 8; + default: + throw Invalid_State("Unexpected hash algorithm"); + } + }; + + const auto max_input_bytes = [&]() -> size_t { + switch(m_scheme.scheme) { + case TPM2_ALG_RSAES: + return max_ptext_bytes - 10; + case TPM2_ALG_OAEP: + return max_ptext_bytes - 2 * hash_output_bytes(m_scheme.details.oaep.hashAlg) - 1; + case TPM2_ALG_NULL: + return max_ptext_bytes; + default: + throw Invalid_State("Unexpected RSA encryption scheme"); + } + }(); + + return max_input_bytes * 8; + } + + size_t ciphertext_length(size_t /* ptext_len */) const override { + return m_key_handle._public_info(m_sessions, TPM2_ALG_RSA).pub->publicArea.parameters.rsaDetail.keyBits - 1; + } + + private: + const Object& m_key_handle; + const SessionBundle& m_sessions; + TPMT_RSA_DECRYPT m_scheme; +}; + +class RSA_Decryption_Operation final : public PK_Ops::Decryption { + public: + RSA_Decryption_Operation(const Object& object, const SessionBundle& sessions, std::string_view padding) : + m_key_handle(object), m_sessions(sessions), m_scheme(select_encryption_algorithms(padding)) {} + + secure_vector decrypt(uint8_t& valid_mask, std::span input) override { + const auto ciphertext = copy_into(input); + const auto label = init_empty(); // TODO: implement? see encrypt operation + unique_esys_ptr plaintext; + + // TODO: I'm not sure that TPM2_RC_FAILURE is the right error code for + // all cases here. It passed the test (with a faulty ciphertext), + // but I didn't find this to be clearly documented. :-( + auto rc = check_rc_expecting("Esys_RSA_Decrypt", + Esys_RSA_Decrypt(*m_key_handle.context(), + m_key_handle.transient_handle(), + m_sessions[0], + m_sessions[1], + m_sessions[2], + &ciphertext, + &m_scheme, + &label, + out_ptr(plaintext))); + + valid_mask = CT::Mask::is_equal(rc, TPM2_RC_SUCCESS).value(); + if(rc == TPM2_RC_SUCCESS) { + BOTAN_ASSERT_NONNULL(plaintext); + return copy_into>(*plaintext); + } else { + return {}; + } + } + + size_t plaintext_length(size_t /* ciphertext_length */) const override { + return m_key_handle._public_info(m_sessions, TPM2_ALG_RSA).pub->publicArea.parameters.rsaDetail.keyBits / 8; + } + + private: + const Object& m_key_handle; + const SessionBundle& m_sessions; + TPMT_RSA_DECRYPT m_scheme; +}; + +} // namespace + +std::unique_ptr RSA_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 RSA_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); +} + +std::unique_ptr RSA_PublicKey::create_encryption_op(Botan::RandomNumberGenerator& rng, + std::string_view params, + std::string_view provider) const { + BOTAN_UNUSED(rng, provider); + return std::make_unique(handles(), sessions(), params); +} + +std::unique_ptr RSA_PrivateKey::create_decryption_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_rsa/tpm2_rsa.h b/src/lib/prov/tpm2/tpm2_rsa/tpm2_rsa.h new file mode 100644 index 00000000000..10c0d486383 --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_rsa/tpm2_rsa.h @@ -0,0 +1,119 @@ +/* +* TPM 2.0 RSA Key Wrappers +* (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_RSA_H_ +#define BOTAN_TPM2_RSA_H_ + +#include +#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: + std::unique_ptr generate_another(Botan::RandomNumberGenerator& rng) const override { + return TPM2::PublicKey::generate_another(rng); + } + + std::vector raw_public_key_bits() const override { return TPM2::PublicKey::raw_public_key_bits(); } + + bool supports_operation(PublicKeyOperation op) const override { + // TODO: Support RSA-KEM + return op == PublicKeyOperation::Encryption || op == PublicKeyOperation::Signature; + } + + std::unique_ptr create_verification_op(std::string_view params, + std::string_view provider) const override; + + std::unique_ptr create_encryption_op(Botan::RandomNumberGenerator& rng, + std::string_view params, + std::string_view provider) const override; + + protected: + friend class TPM2::PublicKey; + + RSA_PublicKey(Object handle, SessionBundle sessions, const TPM2B_PUBLIC* public_blob); +}; + +BOTAN_DIAGNOSTIC_PUSH +BOTAN_DIAGNOSTIC_IGNORE_INHERITED_VIA_DOMINANCE + +class BOTAN_PUBLIC_API(3, 6) RSA_PrivateKey final : public virtual Botan::TPM2::PrivateKey, + public virtual Botan::RSA_PublicKey { + public: + /** + * Create a transient RSA key with the given @p keylength and @p exponent, + * under the given @p parent key, with the given @p auth_value. This key + * may be used for both signatures and data decryption. No restrictions + * on the utilized padding schemes are applied. + * + * TODO: provide the user with some means to specify such restrictions: + * - allowed key use: sign, decrypt, sign+decrypt, x509sign + * - allowed padding schemes: PKCS1v1.5, OAEP, PSS + * - data restrictions ("restricted" field in TPMT_PUBLIC) + * - session authentication requirements (policy, user authentication, ...) + * - fixed to TPM, or fixed to parent? + * - ... + * + * @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 keylength The desired key length + * @param exponent The desired exponent (default: 0x10001) + */ + static std::unique_ptr create_unrestricted_transient(const std::shared_ptr& ctx, + const SessionBundle& sessions, + std::span auth_value, + const TPM2::PrivateKey& parent, + uint16_t keylength, + std::optional exponent = {}); + + public: + std::unique_ptr public_key() const override { + return std::make_unique(algorithm_identifier(), public_key_bits()); + } + + std::vector raw_public_key_bits() const override { return TPM2::PrivateKey::raw_public_key_bits(); } + + bool supports_operation(PublicKeyOperation op) const override { + // TODO: Support RSA-KEM + return op == PublicKeyOperation::Encryption || op == PublicKeyOperation::Signature; + } + + std::unique_ptr create_signature_op(Botan::RandomNumberGenerator& rng, + std::string_view params, + std::string_view provider) const override; + + std::unique_ptr create_decryption_op(Botan::RandomNumberGenerator& rng, + std::string_view params, + std::string_view provider) const override; + + protected: + friend class TPM2::PrivateKey; + + RSA_PrivateKey(Object handle, + SessionBundle sessions, + const TPM2B_PUBLIC* public_blob, + std::span private_blob = {}); +}; + +BOTAN_DIAGNOSTIC_POP + +} // namespace Botan::TPM2 + +#endif diff --git a/src/lib/prov/tpm2/tpm2_session.cpp b/src/lib/prov/tpm2/tpm2_session.cpp index 322aafcd11d..4a5a3fdb031 100644 --- a/src/lib/prov/tpm2/tpm2_session.cpp +++ b/src/lib/prov/tpm2/tpm2_session.cpp @@ -8,6 +8,8 @@ #include +#include + #include #include #include @@ -67,6 +69,37 @@ std::shared_ptr Session::unauthenticated_session(const std::shared_ptr< })); } +std::shared_ptr Session::authenticated_session(const std::shared_ptr& ctx, + const TPM2::PrivateKey& tpm_key, + std::string_view sym_algo, + std::string_view hash_algo) { + Object session(ctx); + const auto auth_sym = get_tpm2_sym_cipher_spec(sym_algo); + const auto auth_hash_algo = get_tpm2_hash_type(hash_algo); + + BOTAN_ASSERT_NONNULL(ctx); + + check_rc("Esys_StartSession", + Esys_StartAuthSession(*ctx, + tpm_key.handles().transient_handle(), + tpm_key.handles().transient_handle(), + ESYS_TR_NONE, + ESYS_TR_NONE, + ESYS_TR_NONE, + nullptr /*NonceCaller generated automatically*/, + TPM2_SE_HMAC, + &auth_sym, + auth_hash_algo, + out_transient_handle(session))); + + return std::shared_ptr(new Session(std::move(session), + { + .continue_session = true, + .decrypt = true, + .encrypt = true, + })); +} + Session::Session(Object session, SessionAttributes attributes) : m_session(std::move(session)) { set_attributes(attributes); } diff --git a/src/lib/prov/tpm2/tpm2_session.h b/src/lib/prov/tpm2/tpm2_session.h index f3033257ec6..9d9f562f4c6 100644 --- a/src/lib/prov/tpm2/tpm2_session.h +++ b/src/lib/prov/tpm2/tpm2_session.h @@ -47,6 +47,7 @@ struct SessionAttributes { }; class Session; +class PrivateKey; namespace detail { @@ -102,6 +103,26 @@ class BOTAN_PUBLIC_API(3, 6) Session { std::string_view sym_algo = "CFB(AES-256)", std::string_view hash_algo = "SHA-256"); + /** + * Instantiate a session based on a salt encrypted for @p tpm_key. This + * allows for the encryption of sensitive parameters passed to and from + * the TPM. The application's random salt is generated automatically (via + * the software RNG in the TSS2's crypto backend). + * + * Such a session is protected against man-in-the-middle attacks with + * access to the data channel between the application and the TPM, under + * the assumption that the @p tpm_key is not compromised. + * + * @param ctx the TPM context + * @param tpm_key the key to use for session establishment + * @param sym_algo the symmetric algorithm used for parameter encryption + * @param hash_algo the hash algorithm in the HMAC used for authentication + */ + static std::shared_ptr authenticated_session(const std::shared_ptr& ctx, + const TPM2::PrivateKey& tpm_key, + std::string_view sym_algo = "CFB(AES-256)", + std::string_view hash_algo = "SHA-256"); + public: /** * Create a session object from a user-provided transient handle. diff --git a/src/tests/test_tpm2.cpp b/src/tests/test_tpm2.cpp index b17c6ca0e08..8183b25fd13 100644 --- a/src/tests/test_tpm2.cpp +++ b/src/tests/test_tpm2.cpp @@ -12,10 +12,17 @@ #include #if defined(BOTAN_HAS_TPM2) + #include + #include + #include #include #include + #if defined(BOTAN_HAS_TPM2_RSA_ADAPTER) + #include + #endif + #include #endif @@ -125,6 +132,17 @@ std::vector test_tpm2_context() { result.confirm("Test persistence location is not in the list", !Botan::value_exists(handles, persistent_key_id + 1)); }), + + // 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) { + auto srk = ctx->storage_root_key({}, {}); + result.require("SRK is not null", srk != nullptr); + result.test_eq("Algo", srk->algo_name(), "RSA"); + result.test_eq("Key size", srk->key_length(), 2048); + result.confirm("Has persistent handle", srk->handles().has_persistent_handle()); + }), + #endif }; } @@ -150,6 +168,17 @@ std::vector test_tpm2_sessions() { ok(result, "CFB(AES-128),SHA-384", Session::unauthenticated_session(ctx, "CFB(AES-128)", "SHA-384")); ok(result, "CFB(AES-128),SHA-1", Session::unauthenticated_session(ctx, "CFB(AES-128)", "SHA-1")); }), + + CHECK("Authenticated sessions", + [&](Test::Result& result) { + using Session = Botan::TPM2::Session; + + auto srk = ctx->storage_root_key({}, {}); + ok(result, "default", Session::authenticated_session(ctx, *srk)); + ok(result, "CFB(AES-128)", Session::authenticated_session(ctx, *srk, "CFB(AES-128)")); + 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")); + }), }; } @@ -203,12 +232,532 @@ std::vector test_tpm2_rng() { }; } + #if defined(BOTAN_HAS_TPM2_RSA_ADAPTER) + +template +auto load_persistent(Test::Result& result, + const std::shared_ptr& ctx, + uint32_t persistent_key_id, + std::span auth_value, + const std::shared_ptr& session) { + 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(), "RSA" /* TODO ECC support*/); + result.test_is_eq("Handle", key->handles().persistent_handle(), persistent_key_id); + return key; +} + +std::vector test_tpm2_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_rsa_handle(); + const auto password = Test::options().tpm2_persistent_auth_value(); + + return { + CHECK("RSA and its helpers are supported", + [&](Test::Result& result) { + result.confirm("RSA is supported", ctx->supports_algorithm("RSA")); + result.confirm("PKCS1 is supported", ctx->supports_algorithm("PKCS1v15")); + result.confirm("PKCS1 with hash is supported", ctx->supports_algorithm("PKCS1v15(SHA-1)")); + result.confirm("OAEP is supported", ctx->supports_algorithm("OAEP")); + result.confirm("OAEP with hash is supported", ctx->supports_algorithm("OAEP(SHA-256)")); + result.confirm("PSS is supported", ctx->supports_algorithm("PSS")); + result.confirm("PSS with hash is supported", ctx->supports_algorithm("PSS(SHA-256)")); + }), + + CHECK("Load the private key multiple times", + [&](Test::Result& result) { + for(size_t i = 0; i < 20; ++i) { + auto key = + load_persistent(result, ctx, persistent_key_id, password, session); + result.test_eq(Botan::fmt("Key loaded successfully ({})", i), key->algo_name(), "RSA"); + } + }), + + CHECK("Sign a message", + [&](Test::Result& result) { + auto key = + load_persistent(result, ctx, persistent_key_id, password, session); + + Botan::Null_RNG null_rng; + Botan::PK_Signer signer(*key, null_rng /* TPM takes care of this */, "PSS(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, "PSS(SHA-256)"); + result.confirm("Signature is valid", verifier.verify_message(message, signature)); + }), + + CHECK("verify signature", + [&](Test::Result& result) { + auto sign = [&](std::span message) { + auto key = + load_persistent(result, ctx, persistent_key_id, password, session); + Botan::Null_RNG null_rng; + Botan::PK_Signer signer(*key, null_rng /* TPM takes care of this */, "PSS(SHA-256)"); + return signer.sign_message(message, null_rng); + }; + + auto verify = [&](std::span msg, std::span sig) { + auto key = + load_persistent(result, ctx, persistent_key_id, password, session); + Botan::PK_Verifier verifier(*key, "PSS(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(result, ctx, persistent_key_id, password, session); + Botan::Null_RNG null_rng; + Botan::PK_Signer signer(*sk, null_rng /* TPM takes care of this */, "PSS(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(result, ctx, persistent_key_id, password, session); + Botan::PK_Verifier verifier(*pk, "PSS(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 = Botan::RSA_PublicKey(pk->algorithm_identifier(), pk->public_key_bits()); + Botan::PK_Verifier soft_verifier(soft_pk, "PSS(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 signing", + [&](Test::Result& result) { + auto key = load_persistent( + 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 */, "PSS(SHA-256)"); + + const auto message = Botan::hex_decode("baadcafe"); + result.test_throws("Fail with wrong password", + [&] { signer.sign_message(message, null_rng); }); + }), + + CHECK("Encrypt a message", + [&](Test::Result& result) { + auto pk = load_persistent(result, ctx, persistent_key_id, password, session); + auto sk = + load_persistent(result, ctx, persistent_key_id, password, session); + + const auto plaintext = Botan::hex_decode("feedc0debaadcafe"); + + // encrypt a message using the TPM's public key + Botan::Null_RNG null_rng; + Botan::PK_Encryptor_EME enc(*pk, null_rng, "OAEP(SHA-256)"); + const auto ciphertext = enc.encrypt(plaintext, null_rng); + + // decrypt the message using the TPM's private RSA key + Botan::PK_Decryptor_EME dec(*sk, null_rng, "OAEP(SHA-256)"); + const auto decrypted = dec.decrypt(ciphertext); + result.test_eq("decrypted message", decrypted, plaintext); + }), + + CHECK("Decrypt a message", + [&](Test::Result& result) { + auto key = + load_persistent(result, ctx, persistent_key_id, password, session); + + const auto plaintext = Botan::hex_decode("feedface"); + + // encrypt a message using a software RSA key for the TPM's private key + auto pk = key->public_key(); + auto rng = Test::new_rng("tpm2 rsa decrypt"); + Botan::PK_Encryptor_EME enc(*pk, *rng, "OAEP(SHA-256)"); + const auto ciphertext = enc.encrypt(plaintext, *rng); + + // decrypt the message using the TPM's private key + Botan::Null_RNG null_rng; + Botan::PK_Decryptor_EME dec(*key, null_rng /* TPM takes care of this */, "OAEP(SHA-256)"); + const auto decrypted = dec.decrypt(ciphertext); + result.test_eq("decrypted message", decrypted, plaintext); + + // corrupt the ciphertext and try to decrypt it + auto mutated_ciphertext = Test::mutate_vec(ciphertext, *rng); + result.test_throws("Fail with wrong ciphertext", + [&] { dec.decrypt(mutated_ciphertext); }); + }), + + CHECK("Create a transient key and encrypt/decrypt a message", + [&](Test::Result& result) { + auto srk = ctx->storage_root_key({}, {}); + auto authed_session = Botan::TPM2::Session::authenticated_session(ctx, *srk); + + const std::array secret = {'s', 'e', 'c', 'r', 'e', 't'}; + auto sk = + Botan::TPM2::RSA_PrivateKey::create_unrestricted_transient(ctx, authed_session, secret, *srk, 2048); + auto pk = sk->public_key(); + + const auto plaintext = Botan::hex_decode("feedc0debaadcafe"); + + // encrypt a message using the TPM's public key + auto rng = Test::new_rng(__func__); + Botan::PK_Encryptor_EME enc(*pk, *rng, "OAEP(SHA-256)"); + const auto ciphertext = enc.encrypt(plaintext, *rng); + + // decrypt the message using the TPM's private RSA key + Botan::Null_RNG null_rng; + Botan::PK_Decryptor_EME dec(*sk, null_rng, "OAEP(SHA-256)"); + const auto decrypted = dec.decrypt(ciphertext); + result.test_eq("decrypted message", decrypted, plaintext); + + // encrypt a message using the TPM's public key (using PKCS#1) + Botan::PK_Encryptor_EME enc_pkcs(*pk, *rng, "PKCS1v15"); + const auto ciphertext_pkcs = enc_pkcs.encrypt(plaintext, *rng); + + // decrypt the message using the TPM's private RSA key (using PKCS#1) + Botan::PK_Decryptor_EME dec_pkcs(*sk, null_rng, "PKCS1v15"); + const auto decrypted_pkcs = dec_pkcs.decrypt(ciphertext_pkcs); + result.test_eq("decrypted message", decrypted_pkcs, plaintext); + }), + + CHECK("Cannot export private key blob from persistent key", + [&](Test::Result& result) { + auto key = + load_persistent(result, ctx, persistent_key_id, password, session); + result.test_throws("Export private key blob not implemented", + [&] { key->private_key_bits(); }); + result.test_throws("Export raw private key blob not implemented", + [&] { key->raw_private_key_bits(); }); + }), + + CHECK("Create a new transient key", + [&](Test::Result& result) { + auto srk = ctx->storage_root_key({}, {}); + + auto authed_session = Botan::TPM2::Session::authenticated_session(ctx, *srk); + + const std::array secret = {'s', 'e', 'c', 'r', 'e', 't'}; + + auto sk = + Botan::TPM2::RSA_PrivateKey::create_unrestricted_transient(ctx, authed_session, secret, *srk, 2048); + + 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 */, "PSS(SHA-256)"); + const auto signature = signer.sign_message(message, null_rng); + result.require("signature is not empty", !signature.empty()); + + Botan::PK_Verifier verifier(*pk, "PSS(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 RSA", sk_loaded->algo_name(), "RSA"); + + 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 */, "PSS(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, "PSS(SHA-256)"); + result.confirm("TPM-verified signature is valid", + verifier_loaded.verify_message(message_loaded, signature_loaded)); + + // Perform a round-trip sign/verify test with the new key pair (PKCS#1) + std::vector message_pkcs = {'b', 'o', 'n', 'j', 'o', 'u', 'r'}; + Botan::PK_Signer signer_pkcs(*sk_loaded, null_rng /* TPM takes care of this */, "PKCS1v15(SHA-256)"); + const auto signature_pkcs = signer_pkcs.sign_message(message_pkcs, null_rng); + result.require("Next signature is not empty", !signature_pkcs.empty()); + result.confirm("Existing verifier cannot validate signature", + !verifier.verify_message(message_pkcs, signature_pkcs)); + + // Create a verifier for PKCS#1 + Botan::PK_Verifier verifier_pkcs(*pk_loaded, "PKCS1v15(SHA-256)"); + result.confirm("TPM-verified signature is valid", + verifier_pkcs.verify_message(message_pkcs, signature_pkcs)); + }), + + CHECK("Make a transient key persistent then remove it again", + [&](Test::Result& result) { + auto srk = ctx->storage_root_key({}, {}); + + 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 */, "PSS(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, "PSS(SHA-256)"); + result.confirm("Signature is valid", verifier.verify_message(message, signature)); + }; + + // Create Key + auto authed_session = Botan::TPM2::Session::authenticated_session(ctx, *srk); + + const std::array secret = {'s', 'e', 'c', 'r', 'e', 't'}; + auto sk = + Botan::TPM2::RSA_PrivateKey::create_unrestricted_transient(ctx, authed_session, secret, *srk, 2048); + 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 + +std::vector test_tpm2_hash() { + auto ctx = get_tpm2_context(__func__); + if(!ctx) { + return {bail_out()}; + } + + auto test = [&](Test::Result& result, std::string_view algo) { + auto tpm_hash = [&]() -> std::unique_ptr { + try { + return std::make_unique( + ctx, algo, ESYS_TR_RH_NULL, Botan::TPM2::Session::unauthenticated_session(ctx)); + } catch(const Botan::Lookup_Error&) { + return {}; + } + }(); + auto soft_hash = Botan::HashFunction::create(algo); + + if(!tpm_hash) { + result.test_note(Botan::fmt("Skipping {}, TPM 2.0 does not support it", algo)); + return; + } + + if(!soft_hash) { + result.test_note(Botan::fmt("Skipping {}, no software equivalent available", algo)); + return; + } + + result.test_eq("Name", tpm_hash->name(), soft_hash->name()); + result.test_eq("Output length", tpm_hash->output_length(), soft_hash->output_length()); + + // multiple update calls + tpm_hash->update("Hello, "); + tpm_hash->update("world!"); + result.test_eq("digest (multi-update)", tpm_hash->final(), soft_hash->process("Hello, world!")); + + // single process call + result.test_eq("digest (single-process)", tpm_hash->process("Hallo, Welt."), soft_hash->process("Hallo, Welt.")); + + // create a message that is larger than the TPM2 max buffer size + const auto long_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; + }(); + + tpm_hash->update(long_message); + result.test_eq("digest (long msg via update)", tpm_hash->final(), soft_hash->process(long_message)); + result.test_eq( + "digest (long msg via process)", tpm_hash->process(long_message), soft_hash->process(long_message)); + + // test clear + tpm_hash->update("Hello"); + tpm_hash->clear(); + tpm_hash->update("Bonjour"); + result.test_eq("digest (clear)", tpm_hash->final(), soft_hash->process("Bonjour")); + + // new_object + auto new_tpm_hash = tpm_hash->new_object(); + result.test_eq("Name (new_object)", new_tpm_hash->name(), tpm_hash->name()); + result.test_eq("Output length (new_object)", new_tpm_hash->output_length(), tpm_hash->output_length()); + result.test_eq("digest (new object)", + new_tpm_hash->process("Salut tout le monde!"), + soft_hash->process("Salut tout le monde!")); + }; + + return { + CHECK("Hashes are supported", + [&](Test::Result& result) { + result.confirm("SHA-1 is supported", ctx->supports_algorithm("SHA-1")); + result.confirm("SHA-256 is supported", ctx->supports_algorithm("SHA-256")); + result.confirm("SHA-384 is supported", ctx->supports_algorithm("SHA-384")); + result.confirm("SHA-512 is supported", ctx->supports_algorithm("SHA-512")); + }), + + CHECK("SHA-1", [&](Test::Result& result) { test(result, "SHA-1"); }), + CHECK("SHA-256", [&](Test::Result& result) { test(result, "SHA-256"); }), + CHECK("SHA-384", [&](Test::Result& result) { test(result, "SHA-384"); }), + CHECK("SHA-512", [&](Test::Result& result) { test(result, "SHA-512"); }), + CHECK("SHA-3(256)", [&](Test::Result& result) { test(result, "SHA-3(256)"); }), + CHECK("SHA-3(384)", [&](Test::Result& result) { test(result, "SHA-3(384)"); }), + CHECK("SHA-3(512)", [&](Test::Result& result) { test(result, "SHA-3(512)"); }), + + CHECK("lookup error", + [&](Test::Result& result) { + result.test_throws( + "Lookup error", [&] { [[maybe_unused]] auto _ = Botan::TPM2::HashFunction(ctx, "MD-5"); }); + }), + + CHECK("copy_state is not implemented", + [&](Test::Result& result) { + auto tpm_hash = Botan::TPM2::HashFunction(ctx, "SHA-256"); + result.test_throws("TPM2 hash does not support copy_state", + [&] { [[maybe_unused]] auto _ = tpm_hash.copy_state(); }); + }), + + CHECK("validation ticket", + [&](Test::Result& result) { + // using the NULL hierarchy essentially disables the validation ticket + auto tpm_hash_null = Botan::TPM2::HashFunction( + ctx, "SHA-256", ESYS_TR_RH_NULL, Botan::TPM2::Session::unauthenticated_session(ctx)); + tpm_hash_null.update("Hola mundo!"); + const auto [digest_null, ticket_null] = tpm_hash_null.final_with_ticket(); + result.require("digest is set", digest_null != nullptr); + result.require("ticket is set", ticket_null != nullptr); + result.confirm("ticket is empty", ticket_null->digest.size == 0); + + // using the OWNER hierarchy (for instance) enables the validation ticket + auto tpm_hash_owner = Botan::TPM2::HashFunction( + ctx, "SHA-256", ESYS_TR_RH_OWNER, Botan::TPM2::Session::unauthenticated_session(ctx)); + tpm_hash_owner.update("Hola mundo!"); + const auto [digest_owner, ticket_owner] = tpm_hash_owner.final_with_ticket(); + result.require("digest is set", digest_owner != nullptr); + result.require("ticket is set", ticket_owner != nullptr); + result.confirm("ticket is not empty", ticket_owner->digest.size > 0); + + const auto digest_vec = Botan::TPM2::copy_into>(*digest_owner); + result.test_eq("digest", + digest_vec, + Botan::hex_decode("1e479f4d871e59e9054aad62105a259726801d5f494acbfcd40591c82f9b3136")); + + result.test_eq("digests are the same, regardless of ticket", + Botan::TPM2::copy_into>(*digest_null), + digest_vec); + }), + }; +} + } // namespace BOTAN_REGISTER_TEST_FN("tpm2", "tpm2_props", test_tpm2_properties); BOTAN_REGISTER_TEST_FN("tpm2", "tpm2_ctx", test_tpm2_context); BOTAN_REGISTER_TEST_FN("tpm2", "tpm2_sessions", test_tpm2_sessions); 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 +BOTAN_REGISTER_TEST_FN("tpm2", "tpm2_hash", test_tpm2_hash); #endif From 00844337fee55c9f1c825ffdfb310978c3d840ec Mon Sep 17 00:00:00 2001 From: Amos Treiber Date: Thu, 5 Sep 2024 15:41:37 +0200 Subject: [PATCH 6/7] 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 | 815 ++++++++++++++++++ .../tpm2_crypto_backend/tpm2_crypto_backend.h | 40 + src/tests/test_tpm2.cpp | 32 +- 6 files changed, 981 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 92c1f1f06a0..28521bed202 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 879903fe8d5..522640e18bc 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 00000000000..065269fab10 --- /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 00000000000..7f2f84f7fbb --- /dev/null +++ b/src/lib/prov/tpm2/tpm2_crypto_backend/tpm2_crypto_backend.cpp @@ -0,0 +1,815 @@ +/* +* 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))}; +} + +template + requires std::constructible_from> +[[nodiscard]] std::optional> get(DigestCallbackState** blob) noexcept { + if(!blob) { + return std::nullopt; + } + + return get(*blob); +} + +/// 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; + } + + BOTAN_ASSERT_NOMSG(key_bits % 8 == 0); + 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; + }); +} + +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. + */ +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; + } + + // Will be deleted in hash_abort() or hash_finish() + *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. + */ +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. + */ +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; // allocated in hash_start() + *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. + */ +void hash_abort(ESYS_CRYPTO_CONTEXT_BLOB** context, void* userdata) { + BOTAN_UNUSED(userdata); + delete *context; // allocated in hash_start() + *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. + */ +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}); + + // Will be deleted in hmac_abort() or hmac_finish() + *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. + */ +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. + */ +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; // allocated in hmac_start() + *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. + */ +void hmac_abort(ESYS_CRYPTO_CONTEXT_BLOB** context, void* userdata) { + BOTAN_UNUSED(userdata); + delete *context; // allocated in hmac_start() + *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 + */ +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. + */ +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. + */ +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. + */ +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. + */ +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. + */ +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. + */ +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. + */ +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" + +} // namespace + +#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 00000000000..952368cb9b4 --- /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 8183b25fd13..02f1ced36bd 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) { From 51fd5dd9b0769c8bb27379d456a9d96e1d3ce3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Meusel?= Date: Thu, 5 Sep 2024 16:05:41 +0200 Subject: [PATCH 7/7] Add documentation for the TPM 2.0 support Co-Authored-By: Amos Treiber --- doc/api_ref/providers.rst | 10 ++ doc/api_ref/rng.rst | 6 +- doc/api_ref/tpm.rst | 249 +++++++++++++++++++++++++++- doc/building.rst | 11 +- doc/dev_ref/contributing.rst | 10 +- doc/dev_ref/todo.rst | 1 + readme.rst | 2 +- src/examples/pkcs10_csr_on_tpm2.cpp | 119 +++++++++++++ 8 files changed, 391 insertions(+), 17 deletions(-) create mode 100644 src/examples/pkcs10_csr_on_tpm2.cpp diff --git a/doc/api_ref/providers.rst b/doc/api_ref/providers.rst index 0ea4683184f..a748a84bf56 100644 --- a/doc/api_ref/providers.rst +++ b/doc/api_ref/providers.rst @@ -23,6 +23,16 @@ The TPM 1.2 standard is a specification for a hardware device which provides cryptographic algorithms. Botan ships a :doc:`TPM provider ` for interacting with TPM devices. It is disabled by default. +TPM 2.0 +^^^^^^^^^^^^^ + +Botan ships a :doc:`TPM 2.0 provider ` for interacting with TPM 2.0 devices. +Access to the TPM is implemented via the TPM Software Stack (TSS) and is tested using +the open source `tpm2-tss implementation `__. +Botan allows to hook into the crypto callbacks of tpm2-tss (requires 4.0 or later) to +avoid pulling in another crypto library as a transitive dependency. +This provider is disabled by default. + CommonCrypto ^^^^^^^^^^^^^ diff --git a/doc/api_ref/rng.rst b/doc/api_ref/rng.rst index c286e1370ca..cfd1b7f4aee 100644 --- a/doc/api_ref/rng.rst +++ b/doc/api_ref/rng.rst @@ -223,10 +223,10 @@ on POWER ``darn``. If the relevant instruction is not available, the constructor of the class will throw at runtime. You can test beforehand by checking the result of ``Processor_RNG::available()``. -TPM_RNG -^^^^^^^^^^^^^^^^^ +TPM_RNG & TPM2_RNG +^^^^^^^^^^^^^^^^^^ -This RNG type allows using the RNG exported from a TPM chip. +These RNG types allow using the RNG exported from a TPM chip. PKCS11_RNG ^^^^^^^^^^^^^^^^^ diff --git a/doc/api_ref/tpm.rst b/doc/api_ref/tpm.rst index 7598c4bd817..c278cbba12b 100644 --- a/doc/api_ref/tpm.rst +++ b/doc/api_ref/tpm.rst @@ -1,8 +1,6 @@ Trusted Platform Module (TPM) ========================================== -.. versionadded:: 1.11.26 - Some computers come with a TPM, which is a small side processor which can perform certain operations which include RSA key generation and signing, a random number generator, accessing a small amount of NVRAM, and a set of PCRs @@ -11,10 +9,249 @@ authenticating a boot sequence). The TPM NVRAM and PCR APIs are not supported by Botan at this time, patches welcome. -Currently only v1.2 TPMs are supported, and the only TPM library supported is -TrouSerS (http://trousers.sourceforge.net/). Hopefully both of these limitations -will be removed in a future release, in order to support newer TPM v2.0 systems. -The current code has been tested with an ST TPM running in a Lenovo laptop. +Currently, we support TPM v1.2 as well as v2.0 systems via independent wrappers +of TrouSerS (http://trousers.sourceforge.net/) for TPM v1.2 and tpm2-tss +(https://github.com/tpm2-software/tpm2-tss) for TPM v2.0. Note however that +the support for TPM v1.2 is deprecated as of Botan 3.5.0 and will be removed in +a future release. + +TPM 2.0 Wrappers +---------------- + +.. versionadded:: 3.6.0 + +Botan's TPM v2.0 support is currently based on a wrapper of the tpm2-tss +library (https://github.com/tpm2-software/tpm2-tss). The code is tested in CI +against the swtpm simulator (https://github.com/stefanberger/swtpm). + +Support for TPM v2.0 is provided by the ``tpm2`` module which is not built by +default as it requires an external dependency. Use the ``BOTAN_HAS_TPM2`` macro +to ensure that support for TPM v2.0 is available in your build of Botan. + +The entire implementation is wrapped into the ``Botan::TPM2`` namespace. The +remainder of this section will omit the namespace prefix for brevity. + +TPM 2.0 Context +~~~~~~~~~~~~~~~ + +The TPM context is the main entry point for all TPM operations. Also, it +provides authorative information about the TPM's capabilities and allows +persisting and evicting keys into the TPM's NVRAM. + +.. cpp:class:: Botan::TPM2::Context + + .. cpp:function:: std::shared_ptr create(const std::string& tcti) + + Create a TPM2 context and connect to it via the given TPM Command + Transmission Interface (TCTI). The TCTI string is a colon-separated specifier + of the form ``tcti_name[:tcti_options=value,...]``. + + .. cpp:function:: std::shared_ptr create(std::optional tcti, std::optional conf) + + Create a TPM2 context and connect to it via the given TPM Command + Transmission Interface (TCTI). The configuration string is passed to the + TCTI. Both values may by empty, in which case the TPM-TSS2 will try to + determine them from default values. + + .. cpp:function:: TPM2_HANDLE persist(TPM2::PrivateKey& key, const SessionBundle& sessions, std::span auth_value, std::optional persistent_handle) + + Persists the given ``key`` in the TPM's NVRAM. The returned handle can be + used to load the key back into the TPM after a reboot. The ``auth_value`` + is used to re-authenticate operations after transforming it to a persistent + key. + + .. cpp:function:: void evict(std::unique_ptr key, const SessionBundle& sessions) + + Evicts the ``key`` from the TPM's NVRAM. The key must be a persistent key + and won't be available for any further use after the eviction. In particular + it won't be re-transformed into a transient key either. + + .. cpp:function:: bool supports_botan_crypto_backend() + + Returns whether the current configuration supports the Botan crypto backend. + This might return false if Botan was not built with the ``tpm2_crypto_backend`` + enabled or the TPM2-TSS library is too old (3.x or older). + + .. cpp:function:: void use_botan_crypto_backend(std::shared_ptr rng) + + Enables the Botan crypto backend for this context. The RNG is needed to + generate key material for the communication with the TPM. It is crucial that + this RNG *does not* depend on the TPM for its entropy as this would create a + chicken-and-egg problem. + + .. cpp:function:: bool supports_algorithm(std::string_view algo_name) + + Returns whether the TPM supports the given algorithm. The ``algo_name`` is + the name of the algorithm as used in Botan. Eg. "RSA", "SHA-256", "AES-128", + "OAEP(SHA-256)", etc. + +For further information about the functionality of the TPM context, please refer +to the doxygen comments in ``tpm2_context.h``. + +TPM 2.0 Sessions +~~~~~~~~~~~~~~~~ + +TPM v2.0 uses sessions to authorize actions on the TPM, encrypt the +communication between the application and the TPM and perform audits of the +operations performed. + +Botan provides a ``Session`` class to handle the creation of sessions and +comes with a ``SessionBundle`` helper to manage multiple sessions to be passed +to the TPM commands. + +.. cpp:class:: Botan::TPM2::Session + + .. cpp:function:: std::shared_ptr unauthenticated_session(const std::shared_ptr& ctx, std::string_view sym_algo, std::string_view hash_algo) + + Creates an unauthenticated session, i.e. does not provide protection against + man-in-the-middle attacks by adversaries who can intercept and modify the + communication between the application and the TPM. + + The ``sym_algo`` and ``hash_algo`` parameters specify the symmetric cipher + used to encrypt parameters flowing to and from the TPM and the hash of the + HMAC algorithm used to protect the integrity of the communication. + + .. cpp:function:: std::shared_ptr authenticated_session(const std::shared_ptr& ctx, const PrivateKey& tpm_key, std::string_view sym_algo, std::string_view hash_algo) + + Creates an authenticated session, i.e. it does provide protection against + man-in-the-middle attacks by adversaries who can intercept and modify the + communication between the application and the TPM, under the assumption that + the ``tpm_key`` is trustworthy and known only to the TPM. + + The ``sym_algo`` and ``hash_algo`` parameters specify the symmetric cipher + used to encrypt parameters flowing to and from the TPM and the hash of the + HMAC algorithm used to protect the integrity of the communication. + +Currently, there's no support for other TPM sessions. + +TPM 2.0 Random Number Generator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``RandomNumberGenerator`` is an adapter to use the TPM's random number +generator as a source of entropy. It behaves exactly like any other RNG in +Botan. + +.. cpp:class:: Botan::TPM2::RandomNumberGenerator + + .. cpp:function:: RandomNumberGenerator(std::shared_ptr ctx, SessionBundle sessions) + + Creates a new RNG object which uses the TPM's random number generator as a + source of entropy. The ``sessions`` parameter is a bundle of sessions to be + used for the RNG operations. + +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. + +Objects of these classes can be used throughout the Botan library to perform +cryptographic operations with TPM keys wherever an abstract +``Botan::Private_Key`` is expected. + +.. cpp:class:: Botan::TPM2::PublicKey + + .. cpp:function:: std::unique_ptr load_persistent(const std::shared_ptr& ctx, TPM2_HANDLE persistent_object_handle, const SessionBundle& sessions) + + Loads a public key that is persistent in the TPM's NVRAM given a + ``persistent_object_handle``. + + .. cpp:function:: std::unique_ptr load_transient(const std::shared_ptr& ctx, std::span public_blob, const SessionBundle& sessions) + + Loads a public key from the given ``public_blob`` which is essentially + a serialization of a public key returned from a TPM key pair creation. + + .. cpp:function:: std::vector raw_public_key_bits() const + + Returns a serialized representation of the public key. This blob can be + loaded back into the TPM as a transient public key. + +.. cpp:class:: Botan::TPM2::PrivateKey + + .. cpp:function:: std::unique_ptr load_persistent(const std::shared_ptr& ctx, TPM2_HANDLE persistent_object_handle, std::span auth_value, const SessionBundle& sessions) + + Loads a private key that is persistent in the TPM's NVRAM given a + ``persistent_object_handle`` and an ``auth_value`` (e.g. a password). + + .. cpp:function:: std::unique_ptr load_transient(const std::shared_ptr& ctx, std::span auth_value, const TPM2::PrivateKey& parent, std::span public_blob, std::span private_blob, const SessionBundle& sessions) + + Loads a private key from the given ``public_blob`` and ``private_blob`` + returned from a TPM key pair creation. To decipher the + ``private_blob``, a ``parent`` key is needed (the same as the one used + to create the key). The ``auth_value`` is used to authenticate private + operations. + + .. cpp:function:: std::unique_ptr create_transient_from_template(const std::shared_ptr& ctx, const SessionBundle& sessions, ESYS_TR parent, const TPMT_PUBLIC& key_template, const TPM2B_SENSITIVE_CREATE& sensitive_data); + + Creates a new transient key pair on the TPM using the given + ``key_template`` and ``sensitive_data`` under the given ``parent`` key. + This is a low-level function, and it assumes that the caller knows how + to create valid ``key_template`` and ``sensitive_data`` structures. + Typically, users should resort to using the creation functions in the + derived private key classes. + + .. cpp:function:: secure_vector raw_private_key_bits() const + + Returns an encrypted "private blob" of the TPM private key if it is a + transient key. This blob can only be decrypted by the TPM that created + it when loading the key back into the TPM. + +Botan provides a set of derived classes for RSA keys, which are used to create +and manage RSA keys on the TPM. + +.. cpp:class:: Botan::TPM2::RSA_PrivateKey + + .. cpp:function:: std::unique_ptr create_unrestricted_transient(const std::shared_ptr& ctx, const SessionBundle& sessions, std::span auth_value, const TPM2::PrivateKey& parent, uint16_t keylength, std::optional exponent); + + Creates a new RSA key pair on the TPM with the given ``keylength`` and + an optional ``exponent``. Typical users should not specify the + exponent, as support for any but the default exponent (65537) is + optional in the TPM v2.0 specification. + + Keys generated with this function are not restricted in their usage. + They may be used both for signing and data encryption with various + padding schemes. 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. + +Botan as a TPM2-TSS Crypto Backend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The TPM2-TSS library (4.0 and later) provides a callback API to override its +default crypto backend (OpenSSL or mbedtls). Botan can optionally use this API +to provide a Botan-based crypto backend for TPM2-TSS and thus allowing to +avoid a dependency on another cryptographic library in applications. + +Once a ``Context`` is created, the Botan-based crypto backend may be enabled for +it via the ``Context::use_botan_crypto_backend`` method. This will only succeed +if the method ``Context::supports_botan_crypto_backend`` returns true. + +TPM 2.0 Example +~~~~~~~~~~~~~~~ + +The following example demonstrates how to create a TPM key pair and sign a +Certificate Signing Request (CSR) with it. This may be useful if one wants +to host a private key for TLS client authentication in a TPM, for example. + +.. literalinclude:: /../src/examples/pkcs10_csr_on_tpm2.cpp + :language: cpp + + +TPM 1.2 Wrappers +---------------- + +.. versionadded:: 1.11.26 + +Currently v1.2 TPMs are supported via a wrapper of the TrouSerS +(http://trousers.sourceforge.net/) library. However, this wrapper is deprecated +and will be removed in a future release. The current code has been tested with +an ST TPM running in a Lenovo laptop. Test for TPM support with the macro ``BOTAN_HAS_TPM``, include ````. diff --git a/doc/building.rst b/doc/building.rst index 5abc50c2f38..5b6e9759146 100644 --- a/doc/building.rst +++ b/doc/building.rst @@ -420,7 +420,9 @@ by the user using - ``--with-sqlite3`` enables using sqlite3 databases in various contexts (TLS session cache, PSK database, etc). - - ``--with-tpm`` adds support for using TPM hardware via the TrouSerS library. + - ``--with-tpm`` adds support for TPM 1.2 hardware via the TrouSerS library. + + - ``--with-tpm2`` adds support for TPM 2.0 hardware via the TSS2 library. - ``--with-boost`` enables using some Boost libraries. In particular Boost.Filesystem is used for a few operations (but on most platforms, a @@ -1070,7 +1072,12 @@ Enable using sqlite3 for data storage ``--with-tpm`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Enable support for TPM +Enable support for TPM 1.2 + +``--with-tpm2`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Enable support for TPM 2.0 ``--program-suffix=SUFFIX`` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/doc/dev_ref/contributing.rst b/doc/dev_ref/contributing.rst index 11240e86c18..ddb3acd3a17 100644 --- a/doc/dev_ref/contributing.rst +++ b/doc/dev_ref/contributing.rst @@ -310,11 +310,11 @@ need this functionality, and it can be done in the library for less than that, then it makes sense to just write the code. Yup. Currently the (optional) external dependencies of the library are several -compression libraries (zlib, bzip2, lzma), sqlite3 database, Trousers (TPM -integration), plus various operating system utilities like basic filesystem -operations. These provide major pieces of functionality which seem worth the -trouble of maintaining an integration with. +compression libraries (zlib, bzip2, lzma), sqlite3 database, Trousers (TPM 1.2 +integration), TSS2 (TPM 2.0 integration) plus various operating system utilities +like basic filesystem operations. These provide major pieces of functionality +which seem worth the trouble of maintaining an integration with. At this point the most plausible examples of an appropriate new external dependency are all deeper integrations with system level cryptographic -interfaces (CommonCrypto, CryptoAPI, /dev/crypto, iOS keychain, TPM 2.0, etc) +interfaces (CommonCrypto, CryptoAPI, /dev/crypto, iOS keychain, etc) diff --git a/doc/dev_ref/todo.rst b/doc/dev_ref/todo.rst index 6e456debfab..d616b194205 100644 --- a/doc/dev_ref/todo.rst +++ b/doc/dev_ref/todo.rst @@ -64,6 +64,7 @@ External Providers * Windows CryptoNG provider (ciphers, hashes) * Extend Apple CommonCrypto provider (HMAC, CMAC, RSA, ECDSA, ECDH) * Add support for iOS keychain access +* Extend support for TPM 2.0 (ECC keys, PCR, NVRAM, Policies, etc) TLS ---------------------------------------- diff --git a/readme.rst b/readme.rst index 3ee1fe5af46..78a84229463 100644 --- a/readme.rst +++ b/readme.rst @@ -125,7 +125,7 @@ Other Useful Things ---------------------------------------- * Full C++ PKCS #11 API wrapper -* Interfaces for TPM v1.2 device access +* Interfaces for TPM v1.2 and v2.0 device access * Simple compression API wrapping zlib, bzip2, and lzma libraries * RNG wrappers for system RNG and hardware RNGs * HMAC_DRBG and entropy collection system for userspace RNGs diff --git a/src/examples/pkcs10_csr_on_tpm2.cpp b/src/examples/pkcs10_csr_on_tpm2.cpp new file mode 100644 index 00000000000..b4979a866d0 --- /dev/null +++ b/src/examples/pkcs10_csr_on_tpm2.cpp @@ -0,0 +1,119 @@ +#include + +#include + +#if defined(BOTAN_HAS_TPM2) + + #include + #include + + #include + #include + #include + #include + + #include + #include + #include + #include + +std::span as_byteview(std::string_view str) { + return {reinterpret_cast(str.data()), str.size()}; +} + +int main() { + // This TCTI configuration is just an example, adjust as needed! + constexpr auto tcti_nameconf = "tabrmd:bus_name=net.randombit.botan.tabrmd,bus_type=session"; + constexpr auto private_key_auth = "notguessable"; + constexpr size_t key_length = 2048; + + // Set up connection to TPM + auto ctx = Botan::TPM2::Context::create(std::string(tcti_nameconf)); + + // Create a TPM-backed RNG + auto tpm_rng = Botan::TPM2::RandomNumberGenerator(ctx); + + if(ctx->supports_botan_crypto_backend()) { + ctx->use_botan_crypto_backend([&] { + // We need an RNG that is functionally independent from the TPM, to use + // in the crypto backend. Also, it is crucial not to use the TPM-backed + // RNG as the underlying source for the software RNG. This could lead + // to TPM command sequence errors when the software RNG decides to + // transparently pull new entropy from the TPM while another TPM + // command is being processed in the crypto backend. + // + // Nevertheless, periodic reseeds from the TPM-backed RNG as shown + // below is fine, as this serializes the TPM commands properly. In this + // example we leave it at a single up-front reseed. + auto software_rng = std::make_shared(); + software_rng->reseed_from_rng(tpm_rng); + return software_rng; + }()); + std::cout << "Botan crypto backend enabled\n"; + } + + // Create an encrypted and "authenticated" session to the TPM using the SRK + // This assumes that the SRK is a persistent object, that is accessible + // without authentication. + auto storage_root_key = ctx->storage_root_key({}, {}); + auto session = Botan::TPM2::Session::authenticated_session(ctx, *storage_root_key); + + // Create a private key and persist it into the TPM + auto cert_private_key = Botan::TPM2::RSA_PrivateKey::create_unrestricted_transient( + ctx, session, as_byteview(private_key_auth), *storage_root_key, key_length); + const auto persistent_handle = ctx->persist(*cert_private_key, session, as_byteview(private_key_auth)); + std::cout << "New private key created\n"; + std::cout << " Persistent handle: 0x" << std::hex << persistent_handle << '\n'; + + // To access the key in the future, load it from the TPM as seen below. + // For now, we still have the key in memory and can use it directly. + // + // auto loaded_private_key = + // Botan::TPM2::PrivateKey::load_persistent(ctx, + // persistent_handle, + // as_byteview(private_key_auth), + // session); + + // Create a Certificate Signing Request (CSR) + const Botan::X509_DN dn({ + {"X520.CommonName", "TPM-hosted test"}, + {"X520.Country", "DE"}, + {"X520.Organization", "Rohde & Schwarz"}, + {"X520.OrganizationalUnit", "GB11"}, + }); + + // Set up relevant extensions + Botan::Extensions extensions; + extensions.add_new(std::make_unique(false /* not a CA */)); + extensions.add_new(std::make_unique( + Botan::Key_Constraints(Botan::Key_Constraints::DigitalSignature | Botan::Key_Constraints::KeyEncipherment))); + extensions.add_new(std::make_unique( + std::vector{Botan::OID::from_name("PKIX.ServerAuth").value()})); + extensions.add_new(std::make_unique([] { + Botan::AlternativeName alt_name; + alt_name.add_dns("rohde-schwarz.com"); + alt_name.add_email("rene.meusel@rohde-schwarz.com"); + return alt_name; + }())); + extensions.add_new( + std::make_unique(cert_private_key->public_key_bits(), "SHA-256")); + + // All done, create the CSR + auto csr = Botan::PKCS10_Request::create(*cert_private_key, dn, extensions, "SHA-256", tpm_rng, "PSS(SHA-256)"); + + // Print results + std::cout << '\n'; + std::cout << "New Certificate Signing Request:\n"; + std::cout << csr.PEM_encode() << '\n'; + + return 0; +} + +#else + +int main() { + std::cerr << "TPM2 support not enabled in this build\n"; + return 1; +} + +#endif