From 774bb6045d25069d34e4d4aa7b1da6d29e6a7d16 Mon Sep 17 00:00:00 2001 From: Michael Boquard Date: Mon, 26 Feb 2024 13:11:21 -0500 Subject: [PATCH] crypto: Added support for asymmetric keys Support loading PEM encoded RSA public and private keys and RSA public keys via modulus and public exponent. Signed-off-by: Michael Boquard --- src/v/crypto/CMakeLists.txt | 1 + src/v/crypto/README.md | 34 +++++ src/v/crypto/crypto.cc | 16 +++ src/v/crypto/include/crypto/crypto.h | 48 +++++++ src/v/crypto/include/crypto/types.h | 12 ++ src/v/crypto/key.cc | 200 +++++++++++++++++++++++++++ src/v/crypto/key.h | 48 +++++++ src/v/crypto/ssl_utils.h | 7 + src/v/crypto/test/CMakeLists.txt | 1 + src/v/crypto/test/key_tests.cc | 115 +++++++++++++++ 10 files changed, 482 insertions(+) create mode 100644 src/v/crypto/key.cc create mode 100644 src/v/crypto/key.h create mode 100644 src/v/crypto/test/key_tests.cc diff --git a/src/v/crypto/CMakeLists.txt b/src/v/crypto/CMakeLists.txt index 1b3ac76817563..0e40e15039ad6 100644 --- a/src/v/crypto/CMakeLists.txt +++ b/src/v/crypto/CMakeLists.txt @@ -6,6 +6,7 @@ v_cc_library( crypto.cc digest.cc hmac.cc + key.cc ssl_utils.cc DEPS absl::node_hash_set diff --git a/src/v/crypto/README.md b/src/v/crypto/README.md index e652805595724..c0b4cf5291396 100644 --- a/src/v/crypto/README.md +++ b/src/v/crypto/README.md @@ -69,3 +69,37 @@ crypto::hmac_ctx ctx(crypto::digest_type::SHA256, key); ctx.update(msg); auto sig = std::move(ctx).final(); ``` + +## Asymmetric Key Handling + +Currently, this library only supports RSA keys. There are two ways of loading a +key from a buffer. + +First, if the key is held within a buffer in PKCS8 format, you can use +`crypto::load_key`: + +```c++ +#include "crypto/crypto.h" + +bytes rsa_priv_key {...}; + +auto key = crypto::key::load_key( + rsa_priv_key, + crypto::format_type::PEM, + crypto::is_private_key_t::yes); +``` + +The above function can determine the type of key held in the buffer but the +caller is responsible for indicating the format the key is in (PEM or DER) and +whether or not it's the public half of the key or the private key. + +The other way of loading a key is by its parts. Currently only RSA public key +loading is available: + +```c++ +#include "crypto/crypto.h" +bytes modulus {...}; +bytes public_exponent {...}; + +auto key = crypto::key::load_rsa_public_key(modulus, public_exponent); +``` diff --git a/src/v/crypto/crypto.cc b/src/v/crypto/crypto.cc index 7e3dbb8f0ba7e..e1fe399eeb94e 100644 --- a/src/v/crypto/crypto.cc +++ b/src/v/crypto/crypto.cc @@ -24,4 +24,20 @@ std::ostream& operator<<(std::ostream& os, digest_type type) { return os; } + +std::ostream& operator<<(std::ostream& os, key_type type) { + switch (type) { + case key_type::RSA: + return os << "RSA"; + } +} + +std::ostream& operator<<(std::ostream& os, format_type type) { + switch (type) { + case format_type::PEM: + return os << "PEM"; + case format_type::DER: + return os << "DER"; + } +} } // namespace crypto diff --git a/src/v/crypto/include/crypto/crypto.h b/src/v/crypto/include/crypto/crypto.h index 4d876656af023..75720a33d1064 100644 --- a/src/v/crypto/include/crypto/crypto.h +++ b/src/v/crypto/include/crypto/crypto.h @@ -208,4 +208,52 @@ struct hmac_ctx final { */ bytes hmac(digest_type type, bytes_view key, bytes_view msg); bytes hmac(digest_type type, std::string_view key, std::string_view msg); + +/////////////////////////////////////////////////////////////////////////////// +/// Asymmetric key operations +/////////////////////////////////////////////////////////////////////////////// +/** + * Structure that holds the key implementation + */ +struct key { +public: + /** + * Attempts to load a key from a buffer + * + * @param key The key buffer + * @param fmt The format of the buffer + * @param is_private_key Whether or not the key is a private key + * @return key The loaded key + * @throws crypto::exception If unable to load the key contained in @p key + * or if @p key contains an unsupported key + */ + static key + load_key(bytes_view key, format_type fmt, is_private_key_t is_private_key); + static key load_key( + std::string_view key, format_type fmt, is_private_key_t is_private_key); + /** + * Loads an RSA public key from its parts + * + * @param n The modulus + * @param e The public exponent + * @return key The loaded key + * @throws crypto::exception If there is an error loading the key + */ + static key load_rsa_public_key(bytes_view n, bytes_view e); + static key load_rsa_public_key(std::string_view n, std::string_view e); + ~key() noexcept; + key(const key&) = delete; + key& operator=(const key&) = delete; + key(key&&) noexcept; + key& operator=(key&&) noexcept; + + key_type get_type() const; + is_private_key_t is_private_key() const; + +private: + class impl; + std::unique_ptr _impl; + + explicit key(std::unique_ptr&& impl); +}; } // namespace crypto diff --git a/src/v/crypto/include/crypto/types.h b/src/v/crypto/include/crypto/types.h index 84fc0d90579db..c318b647aaf57 100644 --- a/src/v/crypto/include/crypto/types.h +++ b/src/v/crypto/include/crypto/types.h @@ -11,9 +11,21 @@ #pragma once +#include "base/seastarx.h" + +#include + #include namespace crypto { enum class digest_type { MD5, SHA256, SHA512 }; std::ostream& operator<<(std::ostream&, digest_type); + +enum class key_type { RSA }; +std::ostream& operator<<(std::ostream&, key_type); + +enum class format_type { PEM, DER }; +std::ostream& operator<<(std::ostream&, format_type); + +using is_private_key_t = ss::bool_class; } // namespace crypto diff --git a/src/v/crypto/key.cc b/src/v/crypto/key.cc new file mode 100644 index 0000000000000..8a4d9ff6eda9f --- /dev/null +++ b/src/v/crypto/key.cc @@ -0,0 +1,200 @@ +/* + * Copyright 2024 Redpanda Data, Inc. + * + * Use of this software is governed by the Business Source License + * included in the file licenses/BSL.md + * + * As of the Change Date specified in that file, in accordance with + * the Business Source License, use of this software will be governed + * by the Apache License, Version 2.0 + */ + +#include "key.h" + +#include "crypto/crypto.h" +#include "crypto/exceptions.h" +#include "crypto/internal.h" +#include "crypto/ssl_utils.h" +#include "crypto/types.h" + +#include +#include +#include +#include +#include +#include + +namespace crypto { + +namespace internal { +EVP_PKEY_ptr load_public_pem_key(bytes_view key) { + BIO_ptr key_bio(BIO_new_mem_buf(key.data(), static_cast(key.size()))); + EVP_PKEY_ptr pkey( + PEM_read_bio_PUBKEY(key_bio.get(), nullptr, nullptr, nullptr)); + if (!pkey) { + throw ossl_error("Failed to load PEM public key"); + } + + return pkey; +} + +EVP_PKEY_ptr load_private_pem_key(bytes_view key) { + BIO_ptr key_bio(BIO_new_mem_buf(key.data(), static_cast(key.size()))); + EVP_PKEY_ptr pkey( + PEM_read_bio_PrivateKey(key_bio.get(), nullptr, nullptr, nullptr)); + if (!pkey) { + throw ossl_error("Failed to load PEM private key"); + } + + return pkey; +} + +// NOLINTNEXTLINE +static const absl::flat_hash_set supported_key_types{EVP_PKEY_RSA}; +} // namespace internal + +key::~key() noexcept = default; +key::key(key&&) noexcept = default; +key& key::operator=(key&&) noexcept = default; + +static_assert( + std::is_nothrow_move_constructible_v, + "key should be nothrow move constructible"); +static_assert( + std::is_nothrow_move_assignable_v, + "key should be nothrow move assignable"); + +key::impl::~impl() noexcept = default; + +key::impl::impl( + _private, internal::EVP_PKEY_ptr pkey, is_private_key_t is_private_key) + : _pkey(std::move(pkey)) + , _is_private_key(is_private_key) { + auto key_type = EVP_PKEY_get_base_id(_pkey.get()); + + if (!internal::supported_key_types.contains(key_type)) { + throw exception(fmt::format("Unsupported key type {}", key_type)); + } +} + +key_type key::impl::get_key_type() const { + auto key_type = EVP_PKEY_get_base_id(_pkey.get()); + + // NOLINTNEXTLINE: For when we add more supported key types + switch (key_type) { + case EVP_PKEY_RSA: + return key_type::RSA; + } + + vassert(false, "Unsupported key type {}", key_type); +} + +key_type key::get_type() const { return _impl->get_key_type(); } +is_private_key_t key::is_private_key() const { return _impl->is_private_key(); } + +std::unique_ptr +key::impl::load_pem_key(bytes_view key, is_private_key_t is_private_key) { + auto key_ptr = is_private_key == is_private_key_t::yes + ? internal::load_private_pem_key(key) + : internal::load_public_pem_key(key); + + return std::make_unique( + key::impl::_private{}, std::move(key_ptr), is_private_key); +} + +std::unique_ptr +key::impl::load_rsa_public_key(bytes_view n, bytes_view e) { + auto n_bn = internal::BN_ptr( + BN_bin2bn(n.data(), static_cast(n.size()), nullptr)); + if (!n_bn) { + throw internal::ossl_error("Failed to load exponent bignum value"); + } + auto e_bn = internal::BN_ptr( + BN_bin2bn(e.data(), static_cast(e.size()), nullptr)); + if (!e_bn) { + throw internal::ossl_error( + "Failed to load public exponent bignum value"); + } + auto param_bld = internal::OSSL_PARAM_BLD_ptr(OSSL_PARAM_BLD_new()); + if (!param_bld) { + throw internal::ossl_error( + "Failed to create param builder for loading RSA public key"); + } + if ( + !OSSL_PARAM_BLD_push_BN(param_bld.get(), "n", n_bn.get()) + || !OSSL_PARAM_BLD_push_BN(param_bld.get(), "e", e_bn.get())) { + throw internal::ossl_error("Failed to set E or N to parameters"); + } + + auto params = internal::OSSL_PARAM_ptr( + OSSL_PARAM_BLD_to_param(param_bld.get())); + if (!params) { + throw internal::ossl_error("Failed to great parameters from builder"); + } + + auto ctx = internal::EVP_PKEY_CTX_ptr( + EVP_PKEY_CTX_new_from_name(nullptr, "RSA", nullptr)); + if (!ctx) { + throw internal::ossl_error("Failed to create RSA PKEY context"); + } + + if (EVP_PKEY_fromdata_init(ctx.get()) != 1) { + throw internal::ossl_error("Failed to initialize EVP PKEY from data"); + } + + EVP_PKEY* pkey = nullptr; + + if ( + 1 + != EVP_PKEY_fromdata( + ctx.get(), &pkey, EVP_PKEY_PUBLIC_KEY, params.get())) { + throw internal::ossl_error( + "Failed to load RSA public key from parameters"); + } + + auto pkey_ptr = internal::EVP_PKEY_ptr(pkey); + { + // For some reason, even though the original PKEY_CTX was used to load + // the key, it's not 'saved' within that ctx so we need to create a + // _new_ one to verify that the key was good + auto verify_ctx = internal::EVP_PKEY_CTX_ptr( + EVP_PKEY_CTX_new_from_pkey(nullptr, pkey_ptr.get(), nullptr)); + if (1 != EVP_PKEY_public_check(verify_ctx.get())) { + throw internal::ossl_error("Failed RSA public key validation"); + } + } + + return std::make_unique( + _private{}, std::move(pkey_ptr), is_private_key_t::no); +} + +key::key(std::unique_ptr&& impl) + : _impl(std::move(impl)) {} + +key key::load_key( + bytes_view key_buffer, format_type fmt, is_private_key_t is_private_key) { + auto key_ptr = fmt == format_type::PEM + ? key::impl::load_pem_key(key_buffer, is_private_key) + : nullptr; + + return key{std::move(key_ptr)}; +} + +key key::load_key( + std::string_view key_buffer, + format_type fmt, + is_private_key_t is_private_key) { + return key::load_key( + internal::string_view_to_bytes_view(key_buffer), fmt, is_private_key); +} + +key key::load_rsa_public_key(bytes_view n, bytes_view e) { + return key{key::impl::load_rsa_public_key(n, e)}; +} + +key key::load_rsa_public_key(std::string_view n, std::string_view e) { + return key::load_rsa_public_key( + internal::string_view_to_bytes_view(n), + internal::string_view_to_bytes_view(e)); +} +} // namespace crypto diff --git a/src/v/crypto/key.h b/src/v/crypto/key.h new file mode 100644 index 0000000000000..ddc8d6322cde1 --- /dev/null +++ b/src/v/crypto/key.h @@ -0,0 +1,48 @@ +/* + * Copyright 2024 Redpanda Data, Inc. + * + * Use of this software is governed by the Business Source License + * included in the file licenses/BSL.md + * + * As of the Change Date specified in that file, in accordance with + * the Business Source License, use of this software will be governed + * by the Apache License, Version 2.0 + */ + +#pragma once + +#include "bytes/bytes.h" +#include "crypto/crypto.h" +#include "crypto/types.h" +#include "ssl_utils.h" + +namespace crypto { +class key::impl { +private: + struct _private {}; + +public: + impl( + _private p, internal::EVP_PKEY_ptr pkey, is_private_key_t is_private_key); + ~impl() noexcept; + impl(const impl&) = delete; + impl& operator=(const impl&) = delete; + impl(impl&&) = default; + impl& operator=(impl&&) = default; + static std::unique_ptr + load_pem_key(bytes_view key, is_private_key_t is_private_key); + static std::unique_ptr + load_der_key(bytes_view key, is_private_key_t is_private_key); + static std::unique_ptr + load_rsa_public_key(bytes_view n, bytes_view e); + + key_type get_key_type() const; + is_private_key_t is_private_key() const { return _is_private_key; } + + EVP_PKEY* get_pkey() const { return _pkey.get(); } + +private: + internal::EVP_PKEY_ptr _pkey; + is_private_key_t _is_private_key; +}; +} // namespace crypto diff --git a/src/v/crypto/ssl_utils.h b/src/v/crypto/ssl_utils.h index c338ec576ff0b..38067d21e30ce 100644 --- a/src/v/crypto/ssl_utils.h +++ b/src/v/crypto/ssl_utils.h @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -43,9 +44,15 @@ struct deleter { template using handle = std::unique_ptr>; +using BIO_ptr = handle; +using BN_ptr = handle; using EVP_MAC_ptr = handle; using EVP_MAC_CTX_ptr = handle; using EVP_MD_CTX_ptr = handle; +using EVP_PKEY_ptr = handle; +using EVP_PKEY_CTX_ptr = handle; +using OSSL_PARAM_ptr = handle; +using OSSL_PARAM_BLD_ptr = handle; /// Exception class used to extract the error from OpenSSL class ossl_error final : public exception { diff --git a/src/v/crypto/test/CMakeLists.txt b/src/v/crypto/test/CMakeLists.txt index 8e751fa0649b0..e7b1842d7225e 100644 --- a/src/v/crypto/test/CMakeLists.txt +++ b/src/v/crypto/test/CMakeLists.txt @@ -5,6 +5,7 @@ rp_test( SOURCES digest_tests.cc hmac_tests.cc + key_tests.cc LIBRARIES v::crypto v::gtest_main diff --git a/src/v/crypto/test/key_tests.cc b/src/v/crypto/test/key_tests.cc new file mode 100644 index 0000000000000..6d752f27f7a38 --- /dev/null +++ b/src/v/crypto/test/key_tests.cc @@ -0,0 +1,115 @@ +/* + * Copyright 2024 Redpanda Data, Inc. + * + * Use of this software is governed by the Business Source License + * included in the file licenses/BSL.md + * + * As of the Change Date specified in that file, in accordance with + * the Business Source License, use of this software will be governed + * by the Apache License, Version 2.0 + */ + +#include "crypto/crypto.h" +#include "crypto/exceptions.h" +#include "crypto/types.h" +#include "crypto_test_utils.h" +#include "gtest/gtest.h" +#include "test_utils/test.h" + +#include + +#include + +// NOLINTBEGIN +static const ss::sstring example_pem_rsa_private_key + = R"(-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCncXJOCFKCBJYm +QEtoNPe1tcNR0aNB51JXjW24Rp07SVEY9Q8m4VLUfRhFPsB630F3F93eGa7WB9To +LlC7m4co5Z3LZeLE5JG3DgVEN2PGI90HbOepfh+YQxmq3Bw9qKtlHVt1aTQbZVIr +rrpQj2npOIpzUDm2ji1h1RUAgRYCmIExbg306RvqeTBOqhTI7tyzMFsQjZcQAIv1 +tpQ6VHb+sBmQMBCRzh2OhPPu4ZkOxXjqGryRoXZa1/jXzVpcDIotRpmZaWHQRh37 +E5ZKIhrku0Xae3DmZoQTrn3dLCT4s4il3yugS+9MD1su8IV/TPxj7DoucG0QQUnL +DYlYc9W/AgMBAAECggEANz6ERn+TbUdDHMqwtmhnY+Hc1+VRNmCyN6W3UgmmPZXC +dnf/8EV+NRIyzEHYcpGvQTI0Jt+VYhNCaPpC86rsLI+ZgK6UY37AHsO29BtMRWa2 +uYjyY+bzWKKm2Mr3XFaGef12G+ZCZVmIA1aKLSMr/+ECOOp6qCL/kRwi6kAsuVz7 +/VKcOB7iFVmDiBgklH6agD2hcjD71jHl+RlTCxOzkC7ilerSr3uV3vU6HACjCklD +wpZoNFXBgX8qTSz/rpoKzlE4KViw6iYdBPkE08UdfOf2Rd5W+ITGWlbYV/y78D6D +xJuwb07TarOE1555FKSRj66HOy5vz31m5avXP3KNiQKBgQDR7s/79Ss2id1e6NTz +EFXSYLL/pdxG77WcOmSNGL2xAQN6fkdWQDpUKPeIqpflfG5vR6Rx1SA52bHZrSBT +PMuVsaKokcwTIIOxebzmXMDA74LK8cCcka/CLByV1dZDlkct28e440WatPYZkP+O +QnNf7xmZLi1z9XybD/tO96oDeQKBgQDML7t0m4xzpm5bCW3P5YPIUx2Yy4FVQCNj +9dbyzc2/oOgHZW6bnXuwaKvE0XKTrsxwEOMlndjMIfOEH8kwlrxJ0upEvYM+2OC2 +mI4y/MEr5uO1AG/mJShV4SOEuINQwM73QlEF9PgjWvLgxJ+H/H0YQNih8JWHtmJo +8qAVjcNc9wKBgQCst504P2J5MX4G2upwu+zP9CzwteYAGrHBQi1+BG/0k8/n1MMe +TCNxIG9fanMkJHa7aSb7XIxx7BAt9gkVUnxwwUABDkrnJaYTuwPWR1NyqNtj2vhM +GHSQ/Tfbcp4g5x/Ss/Kiw6F9ggrDyA7pXPSNZisaYuqUb9E/xitNseeXiQKBgQDH +MTGQWka0dAJocVRdYiwje2H+M1mijwV3eNcO21MCxLhWrs8upH2L5TDcuu8pv3bV +RMQzaD+dNOnZVSDyc7qP0mCUWsT0xKLDvyPJ/eV9LKurYhfHzywAS7hYu5/vYYkG +kf108Dw6UXlraKWxBdILnQc5Q/i8AmMSus8M99VElQKBgQCo33Lgq/8RvmIXeolj +llAP2nTLBdubDka/q4xBaklSAOwBOunmfW43C9VNhrVpMxEohgSkG77L6ZNfYjmt +CSuBq0QNxNf1hEjVOzd9TdqMGHJ16I6RJHpaFGZyKa7mY6ds7V5KyiMrLQ3UWETY +Pu74Px6xOVztryyCGWU5V2jgLg== +-----END PRIVATE KEY-----)"; + +static const ss::sstring example_pem_rsa_public_key + = R"(-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp3FyTghSggSWJkBLaDT3 +tbXDUdGjQedSV41tuEadO0lRGPUPJuFS1H0YRT7Aet9Bdxfd3hmu1gfU6C5Qu5uH +KOWdy2XixOSRtw4FRDdjxiPdB2znqX4fmEMZqtwcPairZR1bdWk0G2VSK666UI9p +6TiKc1A5to4tYdUVAIEWApiBMW4N9Okb6nkwTqoUyO7cszBbEI2XEACL9baUOlR2 +/rAZkDAQkc4djoTz7uGZDsV46hq8kaF2Wtf4181aXAyKLUaZmWlh0EYd+xOWSiIa +5LtF2ntw5maEE6593Swk+LOIpd8roEvvTA9bLvCFf0z8Y+w6LnBtEEFJyw2JWHPV +vwIDAQAB +-----END PUBLIC KEY-----)"; + +static const ss::sstring example_pem_ec_public_key + = R"(-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE+oxV254t9JXTgcR2VlCwyiUhsfHS +pBo8R0hyKF8DjuMn49KNqfL2Aq9jXtr8l7JVfBaMGu1rSgch1OhiC2Lw1Q== +-----END PUBLIC KEY-----)"; + +const auto rsa_pub_key_n = convert_from_hex( + "ddc1676352ca011a235db9b4bb41eab81a9f3447a34c3626a531e3319665edd9c9e269788323" + "ac7f2db36b9106f4b2148b7c7a309a0b7482ff08cc97c792bf8e2319f42aa51078a29a4ff90c" + "0e29563059a8608e8809a04bf45f1334b23631d99253ba230dc640ffc3a70c27ce5fc7ebd1ad" + "fe68e4462790007b39f5d5b47dd9bd04d0d08ac3b586fd6cc8e178d52ecbc09434d4b89d83ca" + "def6c53cce17788e87b551aa0b507893f308e23da919a4aa01183ddc831a99a3e3c4e5bffdc7" + "e8c8b6800699abdf11569ba66e5892b2e55c6f8578a12f5e304dc28ffbd5ee2dfd2bafabac77" + "ba67031f588e73cf7ba344396d166f5392ad36187b45e15916aaf5b7"); + +const auto rsa_pub_key_e = convert_from_hex("748d77"); +// NOLINTEND + +TEST(crypto_key, load_pem_private_key) { + auto key = crypto::key::load_key( + example_pem_rsa_private_key, + crypto::format_type::PEM, + crypto::is_private_key_t::yes); + + ASSERT_EQ(key.get_type(), crypto::key_type::RSA); + ASSERT_TRUE(key.is_private_key()); +} + +TEST(crypto_key, load_pem_public_key) { + auto key = crypto::key::load_key( + example_pem_rsa_public_key, + crypto::format_type::PEM, + crypto::is_private_key_t::no); + + ASSERT_EQ(key.get_type(), crypto::key_type::RSA); + ASSERT_FALSE(key.is_private_key()); +} + +TEST(crypto_key, load_ec_key) { + EXPECT_THROW( + crypto::key::load_key( + example_pem_ec_public_key, + crypto::format_type::PEM, + crypto::is_private_key_t::no), + crypto::exception); +} + +TEST(crypto_key, load_rsa_pub_key_components) { + ASSERT_NO_THROW( + crypto::key::load_rsa_public_key(rsa_pub_key_n, rsa_pub_key_e)); +}