From 3f4910ba30b2294bbca7b3187f301cb5ba9e0169 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Thu, 23 Jul 2026 15:29:42 -0300 Subject: [PATCH 1/3] Add RSA-OAEP encrypt/decrypt across all crypto backends Signed-off-by: Juan Cruz Viotti --- src/core/crypto/CMakeLists.txt | 11 +- src/core/crypto/crypto_rsa_oaep_apple.cc | 108 ++++++++ src/core/crypto/crypto_rsa_oaep_openssl.cc | 117 +++++++++ src/core/crypto/crypto_rsa_oaep_other.cc | 237 ++++++++++++++++++ src/core/crypto/crypto_rsa_oaep_windows.cc | 127 ++++++++++ src/core/crypto/crypto_sha1_other.cc | 16 +- src/core/crypto/crypto_sha1_other.h | 17 ++ .../crypto/include/sourcemeta/core/crypto.h | 1 + .../include/sourcemeta/core/crypto_rsa_oaep.h | 61 +++++ test/crypto/CMakeLists.txt | 2 +- test/crypto/crypto_rsa_oaep_test.cc | 134 ++++++++++ test/crypto/wycheproof_test.cc | 84 +++++++ 12 files changed, 905 insertions(+), 10 deletions(-) create mode 100644 src/core/crypto/crypto_rsa_oaep_apple.cc create mode 100644 src/core/crypto/crypto_rsa_oaep_openssl.cc create mode 100644 src/core/crypto/crypto_rsa_oaep_other.cc create mode 100644 src/core/crypto/crypto_rsa_oaep_windows.cc create mode 100644 src/core/crypto/crypto_sha1_other.h create mode 100644 src/core/crypto/include/sourcemeta/core/crypto_rsa_oaep.h create mode 100644 test/crypto/crypto_rsa_oaep_test.cc diff --git a/src/core/crypto/CMakeLists.txt b/src/core/crypto/CMakeLists.txt index b511734cfe..43247aa31b 100644 --- a/src/core/crypto/CMakeLists.txt +++ b/src/core/crypto/CMakeLists.txt @@ -3,7 +3,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME crypto sha512.h sha1.h fnv128.h uuid.h crc32.h base64.h secure.h verify.h sign.h aes_gcm.h aes_cbc_hmac.h - aes_kw.h + aes_kw.h rsa_oaep.h SOURCES crypto_sha256.cc crypto_hmac_sha256.cc crypto_hmac_sha384.cc crypto_hmac_sha512.cc crypto_sha384.cc @@ -26,7 +26,7 @@ if(SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL) crypto_sha512_openssl.cc crypto_random_openssl.cc crypto_verify_openssl.cc crypto_secure_equals_openssl.cc crypto_sign_openssl.cc crypto_aes_gcm_openssl.cc crypto_aes_cbc_openssl.cc - crypto_aes_kw_openssl.cc + crypto_aes_kw_openssl.cc crypto_rsa_oaep_openssl.cc crypto_openssl.h crypto_pkcs8.h) target_link_libraries(sourcemeta_core_crypto PRIVATE OpenSSL::Crypto) elseif(APPLE) @@ -117,7 +117,7 @@ elseif(APPLE) crypto_sha512_apple.cc crypto_random_apple.cc crypto_verify_apple.cc crypto_secure_equals_apple.cc crypto_sign_apple.cc crypto_aes_gcm_apple.cc crypto_aes_cbc_apple.cc - crypto_aes_kw_apple.cc + crypto_aes_kw_apple.cc crypto_rsa_oaep_apple.cc crypto_eddsa_cryptokit.mm "${CRYPTOKIT_OBJECT}" crypto_eddsa.h crypto_eddsa_apple.h crypto_apple.h crypto_aes_apple.h crypto_bignum.h crypto_shake256.h crypto_pkcs8.h) @@ -148,7 +148,7 @@ elseif(WIN32) crypto_sha512_windows.cc crypto_random_windows.cc crypto_verify_windows.cc crypto_secure_equals_generic.cc crypto_sign_windows.cc crypto_aes_gcm_windows.cc crypto_aes_cbc_windows.cc - crypto_aes_kw_windows.cc + crypto_aes_kw_windows.cc crypto_rsa_oaep_windows.cc crypto_eddsa.h crypto_windows.h crypto_aes_kw_loop.h crypto_bignum.h crypto_ecc.h crypto_shake256.h crypto_pkcs8.h) @@ -163,7 +163,8 @@ else() crypto_sha512_other.cc crypto_random_other.cc crypto_verify_other.cc crypto_secure_equals_generic.cc crypto_sign_other.cc crypto_aes_gcm_other.cc crypto_aes_cbc_other.cc crypto_aes_kw_other.cc - crypto_aes_block.h crypto_aes_kw_loop.h + crypto_rsa_oaep_other.cc + crypto_aes_block.h crypto_aes_kw_loop.h crypto_sha1_other.h crypto_bignum.h crypto_ecc.h crypto_eddsa.h crypto_shake256.h crypto_pkcs8.h) endif() diff --git a/src/core/crypto/crypto_rsa_oaep_apple.cc b/src/core/crypto/crypto_rsa_oaep_apple.cc new file mode 100644 index 0000000000..dc48d2c195 --- /dev/null +++ b/src/core/crypto/crypto_rsa_oaep_apple.cc @@ -0,0 +1,108 @@ +#include + +#include // CF*, kCF* +#include // Sec*, kSec* + +#include // std::size_t +#include // std::uint8_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::unreachable + +namespace sourcemeta::core { + +// The layout matches the definitions in the sibling Apple key backends, since +// each translation unit that reads a key redeclares its file-private members +struct PublicKey::Internal { + PublicKey::Type kind; + SecKeyRef key; + std::string modulus; + std::size_t field_bytes; + std::string edwards_point; + EdwardsCurve edwards_curve; +}; + +struct PrivateKey::Internal { + PrivateKey::Type kind; + SecKeyRef key; + std::size_t field_bytes; + std::string edwards_seed; + EdwardsCurve edwards_curve; + bool rsa_pss_restricted{false}; +}; + +namespace { +auto make_data(const std::string_view value) -> CFDataRef { + return CFDataCreate(kCFAllocatorDefault, + reinterpret_cast(value.data()), + static_cast(value.size())); +} + +auto to_oaep_algorithm(const RSAOAEPHash hash) -> SecKeyAlgorithm { + switch (hash) { + case RSAOAEPHash::SHA1: + return kSecKeyAlgorithmRSAEncryptionOAEPSHA1; + case RSAOAEPHash::SHA256: + return kSecKeyAlgorithmRSAEncryptionOAEPSHA256; + } + + std::unreachable(); +} +} // namespace + +auto rsa_oaep_encrypt(const PublicKey &key, const RSAOAEPHash hash, + const std::string_view plaintext) + -> std::optional { + const auto *const internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA) { + return std::nullopt; + } + + auto plaintext_data{make_data(plaintext)}; + if (plaintext_data == nullptr) { + return std::nullopt; + } + + auto ciphertext{SecKeyCreateEncryptedData( + internal->key, to_oaep_algorithm(hash), plaintext_data, nullptr)}; + CFRelease(plaintext_data); + if (ciphertext == nullptr) { + return std::nullopt; + } + + std::string result{ + reinterpret_cast(CFDataGetBytePtr(ciphertext)), + static_cast(CFDataGetLength(ciphertext))}; + CFRelease(ciphertext); + return result; +} + +auto rsa_oaep_decrypt(const PrivateKey &key, const RSAOAEPHash hash, + const std::string_view ciphertext) + -> std::optional { + const auto *const internal{key.internal()}; + if (internal == nullptr || internal->kind != PrivateKey::Type::RSA) { + return std::nullopt; + } + + auto ciphertext_data{make_data(ciphertext)}; + if (ciphertext_data == nullptr) { + return std::nullopt; + } + + auto plaintext{SecKeyCreateDecryptedData( + internal->key, to_oaep_algorithm(hash), ciphertext_data, nullptr)}; + CFRelease(ciphertext_data); + if (plaintext == nullptr) { + return std::nullopt; + } + + std::string result{ + reinterpret_cast(CFDataGetBytePtr(plaintext)), + static_cast(CFDataGetLength(plaintext))}; + CFRelease(plaintext); + return result; +} + +} // namespace sourcemeta::core diff --git a/src/core/crypto/crypto_rsa_oaep_openssl.cc b/src/core/crypto/crypto_rsa_oaep_openssl.cc new file mode 100644 index 0000000000..679b403c14 --- /dev/null +++ b/src/core/crypto/crypto_rsa_oaep_openssl.cc @@ -0,0 +1,117 @@ +#include + +#include // EVP_* +#include // RSA_PKCS1_OAEP_PADDING + +#include // std::size_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::move, std::unreachable + +namespace sourcemeta::core { + +// The layout matches the definitions in the sibling OpenSSL key backends, since +// each translation unit that reads a key redeclares its file-private members +struct PublicKey::Internal { + PublicKey::Type kind; + EVP_PKEY *key; + std::string modulus; + std::size_t field_bytes; + std::size_t signature_bytes; +}; + +struct PrivateKey::Internal { + PrivateKey::Type kind; + EVP_PKEY *key; + std::size_t field_bytes; + bool rsa_pss_restricted{false}; +}; + +namespace { +auto to_message_digest(const RSAOAEPHash hash) -> const EVP_MD * { + switch (hash) { + case RSAOAEPHash::SHA1: + return EVP_sha1(); + case RSAOAEPHash::SHA256: + return EVP_sha256(); + } + + std::unreachable(); +} + +auto configure(EVP_PKEY_CTX *context, const RSAOAEPHash hash) -> bool { + return EVP_PKEY_CTX_set_rsa_padding(context, RSA_PKCS1_OAEP_PADDING) == 1 && + EVP_PKEY_CTX_set_rsa_oaep_md(context, to_message_digest(hash)) == 1 && + EVP_PKEY_CTX_set_rsa_mgf1_md(context, to_message_digest(hash)) == 1; +} +} // namespace + +auto rsa_oaep_encrypt(const PublicKey &key, const RSAOAEPHash hash, + const std::string_view plaintext) + -> std::optional { + const auto *const internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA) { + return std::nullopt; + } + + auto *context{EVP_PKEY_CTX_new(internal->key, nullptr)}; + if (context == nullptr) { + return std::nullopt; + } + + const auto *const input{ + reinterpret_cast(plaintext.data())}; + std::optional result; + std::size_t length{0}; + if (EVP_PKEY_encrypt_init(context) == 1 && configure(context, hash) && + EVP_PKEY_encrypt(context, nullptr, &length, input, plaintext.size()) == + 1) { + std::string ciphertext(length, '\x00'); + if (EVP_PKEY_encrypt(context, + reinterpret_cast(ciphertext.data()), + &length, input, plaintext.size()) == 1) { + ciphertext.resize(length); + result = std::move(ciphertext); + } + } + + EVP_PKEY_CTX_free(context); + return result; +} + +auto rsa_oaep_decrypt(const PrivateKey &key, const RSAOAEPHash hash, + const std::string_view ciphertext) + -> std::optional { + const auto *const internal{key.internal()}; + if (internal == nullptr || internal->kind != PrivateKey::Type::RSA) { + return std::nullopt; + } + + auto *context{EVP_PKEY_CTX_new(internal->key, nullptr)}; + if (context == nullptr) { + return std::nullopt; + } + + const auto *const input{ + reinterpret_cast(ciphertext.data())}; + std::optional result; + std::size_t length{0}; + if (EVP_PKEY_decrypt_init(context) == 1 && configure(context, hash) && + EVP_PKEY_decrypt(context, nullptr, &length, input, ciphertext.size()) == + 1) { + std::string plaintext(length, '\x00'); + // A failed OAEP check surfaces as a non-positive return here + if (EVP_PKEY_decrypt(context, + reinterpret_cast(plaintext.data()), + &length, input, ciphertext.size()) == 1) { + plaintext.resize(length); + result = std::move(plaintext); + } + } + + EVP_PKEY_CTX_free(context); + return result; +} + +} // namespace sourcemeta::core diff --git a/src/core/crypto/crypto_rsa_oaep_other.cc b/src/core/crypto/crypto_rsa_oaep_other.cc new file mode 100644 index 0000000000..6aedfe5614 --- /dev/null +++ b/src/core/crypto/crypto_rsa_oaep_other.cc @@ -0,0 +1,237 @@ +#include +#include + +#include "crypto_bignum.h" +#include "crypto_random.h" +#include "crypto_sha1_other.h" + +#include // std::size_t +#include // std::uint8_t, std::uint32_t +#include // std::exception +#include // std::optional, std::nullopt +#include // std::span +#include // std::string +#include // std::string_view +#include // std::unreachable + +// A from-scratch RSA-OAEP (RFC 8017 Section 7.1) for the reference backend, +// over the shared big integer arithmetic. The decode is not constant-time, +// which is acceptable only because this backend is the non-production fallback +// and the production backends run the platform's constant-time implementation. +// A failed check still surfaces as a single uniform failure, never a +// distinguishing one + +namespace sourcemeta::core { + +// The layout matches the definitions in the sibling reference key backends, +// since each translation unit that reads a key redeclares its file-private +// members +struct PublicKey::Internal { + PublicKey::Type kind; + std::string modulus; + std::string exponent; + std::string coordinate_x; + std::string coordinate_y; + EllipticCurve elliptic_curve; + EdwardsCurve edwards_curve; +}; + +struct PrivateKey::Internal { + PrivateKey::Type kind; + std::string modulus; + std::string public_exponent; + std::string private_exponent; + std::string scalar; + EllipticCurve elliptic_curve; + std::string edwards_seed; + EdwardsCurve edwards_curve; + bool rsa_pss_restricted{false}; + std::string coordinate_x; + std::string coordinate_y; +}; + +namespace { +auto hash_length(const RSAOAEPHash hash) -> std::size_t { + switch (hash) { + case RSAOAEPHash::SHA1: + return 20; + case RSAOAEPHash::SHA256: + return 32; + } + + std::unreachable(); +} + +auto oaep_hash(const RSAOAEPHash hash, const std::string_view input) + -> std::string { + switch (hash) { + case RSAOAEPHash::SHA1: { + const auto digest{sha1_digest(input)}; + return std::string{reinterpret_cast(digest.data()), + digest.size()}; + } + case RSAOAEPHash::SHA256: { + const auto digest{sha256_digest(input)}; + return std::string{reinterpret_cast(digest.data()), + digest.size()}; + } + } + + std::unreachable(); +} + +// The mask generation function (RFC 8017 Appendix B.2.1) +auto mask_generation(const RSAOAEPHash hash, const std::string_view seed, + const std::size_t length) -> std::string { + std::string result; + result.reserve(length + hash_length(hash)); + std::uint32_t counter{0}; + while (result.size() < length) { + std::string block{seed}; + block.push_back(static_cast((counter >> 24u) & 0xffu)); + block.push_back(static_cast((counter >> 16u) & 0xffu)); + block.push_back(static_cast((counter >> 8u) & 0xffu)); + block.push_back(static_cast(counter & 0xffu)); + result.append(oaep_hash(hash, block)); + counter += 1; + } + + result.resize(length); + return result; +} + +auto exclusive_or(const std::string_view left, const std::string_view right) + -> std::string { + std::string result(left.size(), '\x00'); + for (std::size_t index = 0; index < left.size(); ++index) { + result[index] = static_cast(static_cast(left[index]) ^ + static_cast(right[index])); + } + + return result; +} +} // namespace + +auto rsa_oaep_encrypt(const PublicKey &key, const RSAOAEPHash hash, + const std::string_view plaintext) + -> std::optional { + const auto *const internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA) { + return std::nullopt; + } + + const auto key_length{internal->modulus.size()}; + const auto digest_length{hash_length(hash)}; + if (key_length < (2 * digest_length) + 2 || + plaintext.size() > key_length - (2 * digest_length) - 2) { + return std::nullopt; + } + + // DB = lHash || PS || 0x01 || M (RFC 8017 Section 7.1.1) + const auto label_hash{oaep_hash(hash, "")}; + std::string data_block{label_hash}; + data_block.append(key_length - plaintext.size() - (2 * digest_length) - 2, + '\x00'); + data_block.push_back('\x01'); + data_block.append(plaintext); + + std::string seed(digest_length, '\x00'); + try { + fill_random_bytes(std::span{ + reinterpret_cast(seed.data()), digest_length}); + } catch (const std::exception &) { + return std::nullopt; + } + + const auto masked_data_block{exclusive_or( + data_block, mask_generation(hash, seed, key_length - digest_length - 1))}; + const auto masked_seed{exclusive_or( + seed, mask_generation(hash, masked_data_block, digest_length))}; + + // EM = 0x00 || maskedSeed || maskedDB + std::string encoded_message; + encoded_message.reserve(key_length); + encoded_message.push_back('\x00'); + encoded_message.append(masked_seed); + encoded_message.append(masked_data_block); + + const auto ciphertext{bignum_mod_exp(bignum_from_bytes(encoded_message), + bignum_from_bytes(internal->exponent), + bignum_from_bytes(internal->modulus))}; + return bignum_to_bytes(ciphertext, key_length); +} + +auto rsa_oaep_decrypt(const PrivateKey &key, const RSAOAEPHash hash, + const std::string_view ciphertext) + -> std::optional { + const auto *const internal{key.internal()}; + if (internal == nullptr || internal->kind != PrivateKey::Type::RSA) { + return std::nullopt; + } + + const auto key_length{internal->modulus.size()}; + const auto digest_length{hash_length(hash)}; + if (key_length < (2 * digest_length) + 2 || ciphertext.size() != key_length) { + return std::nullopt; + } + + const auto modulus{bignum_from_bytes(internal->modulus)}; + auto ciphertext_number{bignum_from_bytes(ciphertext)}; + if (bignum_compare(ciphertext_number, modulus) >= 0) { + return std::nullopt; + } + + auto exponent{bignum_from_bytes(internal->private_exponent)}; + const SecureBignumScope exponent_scope{exponent}; + auto message_number{ + bignum_mod_exp_ct(ciphertext_number, exponent, barrett_context(modulus))}; + const SecureBignumScope message_scope{message_number}; + const auto encoded_message{bignum_to_bytes(message_number, key_length)}; + + // EM = Y || maskedSeed || maskedDB (RFC 8017 Section 7.1.2) + const std::string_view masked_seed{encoded_message.data() + 1, digest_length}; + const std::string_view masked_data_block{encoded_message.data() + 1 + + digest_length, + key_length - digest_length - 1}; + const auto seed{exclusive_or( + masked_seed, mask_generation(hash, masked_data_block, digest_length))}; + const auto data_block{exclusive_or( + masked_data_block, + mask_generation(hash, seed, key_length - digest_length - 1))}; + + const auto label_hash{oaep_hash(hash, "")}; + // Accumulate every failure into a single value so that a malformed input is + // rejected uniformly, never revealing which check failed + std::uint8_t error{static_cast(encoded_message[0])}; + for (std::size_t index = 0; index < digest_length; ++index) { + error = static_cast( + error | (static_cast(data_block[index]) ^ + static_cast(label_hash[index]))); + } + + std::size_t separator{0}; + std::uint8_t found{0}; + for (std::size_t index = digest_length; index < data_block.size(); ++index) { + const auto byte{static_cast(data_block[index])}; + const std::uint8_t is_one{static_cast(byte == 0x01 ? 1 : 0)}; + const std::uint8_t is_zero{static_cast(byte == 0x00 ? 1 : 0)}; + const std::uint8_t not_found{static_cast(1 - found)}; + separator += static_cast(not_found & is_one) * index; + // A byte before the separator that is neither the padding zero nor the + // separator one is malformed + error = static_cast( + error | (not_found & static_cast(1 - is_zero) & + static_cast(1 - is_one))); + found = static_cast(found | is_one); + } + + error = + static_cast(error | static_cast(1 - found)); + if (error != 0) { + return std::nullopt; + } + + return data_block.substr(separator + 1); +} + +} // namespace sourcemeta::core diff --git a/src/core/crypto/crypto_rsa_oaep_windows.cc b/src/core/crypto/crypto_rsa_oaep_windows.cc new file mode 100644 index 0000000000..099904e11c --- /dev/null +++ b/src/core/crypto/crypto_rsa_oaep_windows.cc @@ -0,0 +1,127 @@ +#include + +#include // ULONG, PUCHAR, LPCWSTR +// clang-format off +#include // BCrypt*, BCRYPT_* +// clang-format on + +#include // std::size_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::move, std::unreachable + +namespace sourcemeta::core { + +// The layout matches the definitions in the sibling Windows key backends, since +// each translation unit that reads a key redeclares its file-private members +struct PublicKey::Internal { + PublicKey::Type kind; + BCRYPT_ALG_HANDLE algorithm; + BCRYPT_KEY_HANDLE key; + std::size_t field_bytes; + std::string modulus; + std::string edwards_point; + EdwardsCurve edwards_curve; +}; + +struct PrivateKey::Internal { + PrivateKey::Type kind; + BCRYPT_ALG_HANDLE algorithm; + BCRYPT_KEY_HANDLE key; + std::size_t field_bytes; + std::string edwards_seed; + EdwardsCurve edwards_curve; + bool rsa_pss_restricted{false}; +}; + +namespace { +auto to_cng_algorithm(const RSAOAEPHash hash) -> LPCWSTR { + switch (hash) { + case RSAOAEPHash::SHA1: + return BCRYPT_SHA1_ALGORITHM; + case RSAOAEPHash::SHA256: + return BCRYPT_SHA256_ALGORITHM; + } + + std::unreachable(); +} + +auto as_buffer(const std::string_view value) -> PUCHAR { + return reinterpret_cast(const_cast( + value.data())); // NOLINT(cppcoreguidelines-pro-type-const-cast) +} +} // namespace + +auto rsa_oaep_encrypt(const PublicKey &key, const RSAOAEPHash hash, + const std::string_view plaintext) + -> std::optional { + const auto *const internal{key.internal()}; + if (internal == nullptr || internal->kind != PublicKey::Type::RSA) { + return std::nullopt; + } + + BCRYPT_OAEP_PADDING_INFO padding{}; + padding.pszAlgId = to_cng_algorithm(hash); + padding.pbLabel = nullptr; + padding.cbLabel = 0; + + ULONG length{0}; + if (!BCRYPT_SUCCESS(BCryptEncrypt(internal->key, as_buffer(plaintext), + static_cast(plaintext.size()), + &padding, nullptr, 0, nullptr, 0, &length, + BCRYPT_PAD_OAEP))) { + return std::nullopt; + } + + std::string ciphertext(length, '\x00'); + ULONG written{0}; + if (!BCRYPT_SUCCESS(BCryptEncrypt(internal->key, as_buffer(plaintext), + static_cast(plaintext.size()), + &padding, nullptr, 0, as_buffer(ciphertext), + static_cast(ciphertext.size()), + &written, BCRYPT_PAD_OAEP))) { + return std::nullopt; + } + + ciphertext.resize(written); + return ciphertext; +} + +auto rsa_oaep_decrypt(const PrivateKey &key, const RSAOAEPHash hash, + const std::string_view ciphertext) + -> std::optional { + const auto *const internal{key.internal()}; + if (internal == nullptr || internal->kind != PrivateKey::Type::RSA) { + return std::nullopt; + } + + BCRYPT_OAEP_PADDING_INFO padding{}; + padding.pszAlgId = to_cng_algorithm(hash); + padding.pbLabel = nullptr; + padding.cbLabel = 0; + + ULONG length{0}; + // A failed OAEP check surfaces as an unsuccessful status + if (!BCRYPT_SUCCESS(BCryptDecrypt(internal->key, as_buffer(ciphertext), + static_cast(ciphertext.size()), + &padding, nullptr, 0, nullptr, 0, &length, + BCRYPT_PAD_OAEP))) { + return std::nullopt; + } + + std::string plaintext(length, '\x00'); + ULONG written{0}; + if (!BCRYPT_SUCCESS(BCryptDecrypt(internal->key, as_buffer(ciphertext), + static_cast(ciphertext.size()), + &padding, nullptr, 0, as_buffer(plaintext), + static_cast(plaintext.size()), + &written, BCRYPT_PAD_OAEP))) { + return std::nullopt; + } + + plaintext.resize(written); + return plaintext; +} + +} // namespace sourcemeta::core diff --git a/src/core/crypto/crypto_sha1_other.cc b/src/core/crypto/crypto_sha1_other.cc index 89fc906284..e5a7a47df6 100644 --- a/src/core/crypto/crypto_sha1_other.cc +++ b/src/core/crypto/crypto_sha1_other.cc @@ -1,10 +1,13 @@ #include #include +#include "crypto_sha1_other.h" + #include // std::array #include // std::size_t -#include // std::uint32_t, std::uint64_t +#include // std::uint8_t, std::uint32_t, std::uint64_t #include // std::memcpy +#include // std::string namespace { @@ -95,7 +98,7 @@ inline auto sha1_process_block(const unsigned char *block, namespace sourcemeta::core { -auto sha1(const std::string_view input) -> std::string { +auto sha1_digest(const std::string_view input) -> std::array { // Initial hash values (RFC 3174 Section 6.1) std::array state{}; state[0] = 0x67452301U; @@ -146,14 +149,19 @@ auto sha1(const std::string_view input) -> std::string { sha1_process_block(final_block.data() + 64u, state); } - std::array digest{}; + std::array digest{}; for (std::uint64_t state_index = 0u; state_index < 5u; ++state_index) { for (std::uint64_t byte_index = 0u; byte_index < 4u; ++byte_index) { - digest[(state_index * 4u) + byte_index] = static_cast( + digest[(state_index * 4u) + byte_index] = static_cast( (state[state_index] >> (8u * (3u - byte_index))) & 0xffu); } } + return digest; +} + +auto sha1(const std::string_view input) -> std::string { + const auto digest{sha1_digest(input)}; return bytes_to_hex( {reinterpret_cast(digest.data()), digest.size()}); } diff --git a/src/core/crypto/crypto_sha1_other.h b/src/core/crypto/crypto_sha1_other.h new file mode 100644 index 0000000000..17457d28cd --- /dev/null +++ b/src/core/crypto/crypto_sha1_other.h @@ -0,0 +1,17 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_SHA1_OTHER_H_ +#define SOURCEMETA_CORE_CRYPTO_SHA1_OTHER_H_ + +// The raw SHA-1 digest (RFC 3174) for the fallback backend, used by the modes +// that need the digest bytes rather than the hexadecimal string + +#include // std::array +#include // std::uint8_t +#include // std::string_view + +namespace sourcemeta::core { + +auto sha1_digest(const std::string_view input) -> std::array; + +} // namespace sourcemeta::core + +#endif diff --git a/src/core/crypto/include/sourcemeta/core/crypto.h b/src/core/crypto/include/sourcemeta/core/crypto.h index 5480c68945..9aa7bedb2f 100644 --- a/src/core/crypto/include/sourcemeta/core/crypto.h +++ b/src/core/crypto/include/sourcemeta/core/crypto.h @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include diff --git a/src/core/crypto/include/sourcemeta/core/crypto_rsa_oaep.h b/src/core/crypto/include/sourcemeta/core/crypto_rsa_oaep.h new file mode 100644 index 0000000000..e8a92ab690 --- /dev/null +++ b/src/core/crypto/include/sourcemeta/core/crypto_rsa_oaep.h @@ -0,0 +1,61 @@ +#ifndef SOURCEMETA_CORE_CRYPTO_RSA_OAEP_H_ +#define SOURCEMETA_CORE_CRYPTO_RSA_OAEP_H_ + +#ifndef SOURCEMETA_CORE_CRYPTO_EXPORT +#include +#endif + +#include // PrivateKey +#include // PublicKey + +#include // std::uint8_t +#include // std::optional +#include // std::string +#include // std::string_view + +namespace sourcemeta::core { + +/// @ingroup crypto +/// The hash function that RSA-OAEP uses for both the label digest and the mask +/// generation function. +enum class RSAOAEPHash : std::uint8_t { SHA1, SHA256 }; + +/// @ingroup crypto +/// Encrypt a short key or message with RSA-OAEP (RFC 8017) under an RSA public +/// key, where SHA1 selects RSA-OAEP and SHA256 selects RSA-OAEP-256. Returns no +/// value when the key is not RSA, the plaintext is too long for the modulus, or +/// the operation fails. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto wrapped{sourcemeta::core::rsa_oaep_encrypt( +/// public_key, sourcemeta::core::RSAOAEPHash::SHA256, key)}; +/// assert(wrapped.has_value()); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT rsa_oaep_encrypt( + const PublicKey &key, const RSAOAEPHash hash, + const std::string_view plaintext) -> std::optional; + +/// @ingroup crypto +/// Decrypt a message produced by rsa_oaep_encrypt under the matching RSA +/// private key and hash, returning the original plaintext. Returns no value +/// when the key is not RSA, the ciphertext is malformed, or the padding check +/// fails. For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// const auto key{sourcemeta::core::rsa_oaep_decrypt( +/// private_key, sourcemeta::core::RSAOAEPHash::SHA256, wrapped.value())}; +/// assert(key.has_value()); +/// ``` +auto SOURCEMETA_CORE_CRYPTO_EXPORT rsa_oaep_decrypt( + const PrivateKey &key, const RSAOAEPHash hash, + const std::string_view ciphertext) -> std::optional; + +} // namespace sourcemeta::core + +#endif diff --git a/test/crypto/CMakeLists.txt b/test/crypto/CMakeLists.txt index 0e6dfc116e..7619f3bf08 100644 --- a/test/crypto/CMakeLists.txt +++ b/test/crypto/CMakeLists.txt @@ -22,7 +22,7 @@ sourcemeta_test(NAMESPACE sourcemeta PROJECT core NAME crypto crypto_random_test.cc crypto_aes_gcm_test.cc crypto_aes_cbc_hmac_test.cc - crypto_aes_kw_test.cc) + crypto_aes_kw_test.cc crypto_rsa_oaep_test.cc) target_link_libraries(sourcemeta_core_crypto_unit PRIVATE sourcemeta::core::crypto) diff --git a/test/crypto/crypto_rsa_oaep_test.cc b/test/crypto/crypto_rsa_oaep_test.cc new file mode 100644 index 0000000000..8ece998518 --- /dev/null +++ b/test/crypto/crypto_rsa_oaep_test.cc @@ -0,0 +1,134 @@ +#include +#include + +#include // std::string +#include // std::string_view + +// A 2048-bit RSA test key in unencrypted PKCS#8, from which the public key is +// derived for the encryption side of each round trip +static const std::string PRIVATE_KEY_PEM{ + R"(-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwidOHq7tHMyQk +rKtggHsdpTNL7fv+Rjlve5AgNGbTDMJCWga7LbOxS1B/iaOVyn7pNX4+XVXgRGYG +qwOOaiKnU0S2aE7TL7aqTwaPLABGS3e0A2yx1vm6sDMi5UpvboEX5Da49xZp5nU3 +EFS87UEGStBggySt6L1CFClaYyp7hq7C+/ah3DVQlrLYQzWACIm6U5T3MEMrC3kH +efg0RUPlFUgoBlX6JtA51Z7TB5BDsUGwj9oQhdrSOaMenYtsoVaDqna1SMZcsxJ7 +x625rWaSz7F2gHJ+MVxsbFzMnrjF58OirgAO+6ARFkXbi7zzsDWl9eK1VRMe+61I +c4Bc4D2RAgMBAAECggEACUtlXZmOG3Fa6hsk7fiPutOmee08gOJ8vkTJtZdasUqE +/QIxQ+gz424LHb7e8+lIkaHPTOxkM8G4IypcbmJiQw/eUJH48fObdPeiiVXMSScq +pQmv3qRaSzDhDVmX99pQHcRWOPpp1b8kXVb81iJR48lbM5SeARG9iqHF/Fg3na+W ++XIzDS0+gHav7E+rmsKG1fqyyyXiTx+0dfx074/8FM7q8USeXaafeAEWIngXFiro +7xSVs081QOt80XmOtpOwz3yBz/PZX2WRDW2U0bfzWDAb9Fx+8wkL/HX6q/ozj5tH +B+x4ZQCA3+3aN6aYUqDEFk4damboi8C1QDVvnHPGAQKBgQDkN6RREwyP0USnbDGq +nrqKj9VH9Mkw34jl4AmvRqUHg9vX/j5z8SEm7sTrEPM2ZKUQSEvF6ZejBHTmiM1v ++5VE3+awVN1ejRCutZ6J4fXlBv4hqPo2yxZByKdssXwZHeCQXn1sMDhWkZCLsBcW +vV9isLTn7QRoyWU0AsdXkruDkQKBgQDGB54TNztJuR8qYlZtmh7FJp+crBC5AwRw +ibM487c3RWpxYNUcprsBJu3NaTmy3tR3Vdr0GowJagG5frvq0PtYvIRladNUwx+S +ZCL0Twl7BCGkncVGTWaahookxc7oSZiwKStYt4IVtiFm8WY2GTmsc3JUaV6oyskP +O48d3kgaAQKBgC9MtPq0twLLva3xh620LjdvriW2v0go3EfAkmFIhAW1t4fhwvHm +xRc7n4aUwcPBQlDImyPO0UXRM6VgLywJeZuHyFk9jY/+Lrn3HH+5XGx2cY9qZUmb +9+X0f3bQpkbSYWuqmdYlfFe83beF7JMi9rUOqL/06NgK/bW06bnlyIVxAoGBALgz +HbaCCYPpzjN+hSYt1bFM2+q/ok51oAuOCkdPa3zd9MTBbXnZHNlVfJ0045ad7sOR ++LhWeSvIjxIb8G/bM/C2afBrH0gYt6VTv6/AVPE4RpL4ridWl7nkq1rJJEytKHmz +0b4T88x+PVOj948ap6rhN9eMq6AWAC9LFzSRLkABAoGBAMiDTmhfnkMtv3Uh4ruT +vqfGU9UbfEmK5SGb6YTClT9hIZS5erKSwfETRsWqkRLzgFG0BkLQRzfxn/29DMQM +QHq1PUPjkaKpLxvFyUVnuOw0kkO3xTQPStepKrJpdvzzWCxgTa7wkeBagxW+Ibp1 +VoqHMouZBlcLfRprpPpQWlay +-----END PRIVATE KEY----- +)"}; + +TEST(rsa_oaep_round_trips_with_sha256) { + const std::string_view plaintext{"a content encryption key"}; + const auto private_key{sourcemeta::core::make_private_key(PRIVATE_KEY_PEM)}; + EXPECT_TRUE(private_key.has_value()); + const auto public_key{ + sourcemeta::core::derive_public_key(private_key.value())}; + EXPECT_TRUE(public_key.has_value()); + const auto wrapped{sourcemeta::core::rsa_oaep_encrypt( + public_key.value(), sourcemeta::core::RSAOAEPHash::SHA256, plaintext)}; + EXPECT_TRUE(wrapped.has_value()); + const auto unwrapped{sourcemeta::core::rsa_oaep_decrypt( + private_key.value(), sourcemeta::core::RSAOAEPHash::SHA256, + wrapped.value())}; + EXPECT_TRUE(unwrapped.has_value()); + EXPECT_EQ(unwrapped.value(), plaintext); +} + +TEST(rsa_oaep_round_trips_with_sha1) { + const std::string_view plaintext{"a content encryption key"}; + const auto private_key{sourcemeta::core::make_private_key(PRIVATE_KEY_PEM)}; + EXPECT_TRUE(private_key.has_value()); + const auto public_key{ + sourcemeta::core::derive_public_key(private_key.value())}; + EXPECT_TRUE(public_key.has_value()); + const auto wrapped{sourcemeta::core::rsa_oaep_encrypt( + public_key.value(), sourcemeta::core::RSAOAEPHash::SHA1, plaintext)}; + EXPECT_TRUE(wrapped.has_value()); + const auto unwrapped{sourcemeta::core::rsa_oaep_decrypt( + private_key.value(), sourcemeta::core::RSAOAEPHash::SHA1, + wrapped.value())}; + EXPECT_TRUE(unwrapped.has_value()); + EXPECT_EQ(unwrapped.value(), plaintext); +} + +TEST(rsa_oaep_ciphertext_matches_the_modulus_size) { + const auto private_key{sourcemeta::core::make_private_key(PRIVATE_KEY_PEM)}; + const auto public_key{ + sourcemeta::core::derive_public_key(private_key.value())}; + const auto wrapped{sourcemeta::core::rsa_oaep_encrypt( + public_key.value(), sourcemeta::core::RSAOAEPHash::SHA256, "short")}; + EXPECT_TRUE(wrapped.has_value()); + EXPECT_EQ(wrapped.value().size(), std::string::size_type{256}); +} + +TEST(rsa_oaep_encrypt_is_randomized) { + const auto private_key{sourcemeta::core::make_private_key(PRIVATE_KEY_PEM)}; + const auto public_key{ + sourcemeta::core::derive_public_key(private_key.value())}; + const auto first{sourcemeta::core::rsa_oaep_encrypt( + public_key.value(), sourcemeta::core::RSAOAEPHash::SHA256, "identical")}; + const auto second{sourcemeta::core::rsa_oaep_encrypt( + public_key.value(), sourcemeta::core::RSAOAEPHash::SHA256, "identical")}; + EXPECT_TRUE(first.has_value()); + EXPECT_TRUE(second.has_value()); + EXPECT_NE(first.value(), second.value()); +} + +TEST(rsa_oaep_decrypt_rejects_a_tampered_ciphertext) { + const auto private_key{sourcemeta::core::make_private_key(PRIVATE_KEY_PEM)}; + const auto public_key{ + sourcemeta::core::derive_public_key(private_key.value())}; + auto wrapped{ + sourcemeta::core::rsa_oaep_encrypt( + public_key.value(), sourcemeta::core::RSAOAEPHash::SHA256, "hello") + .value()}; + wrapped.back() = static_cast(wrapped.back() ^ 0x01); + EXPECT_FALSE( + sourcemeta::core::rsa_oaep_decrypt( + private_key.value(), sourcemeta::core::RSAOAEPHash::SHA256, wrapped) + .has_value()); +} + +TEST(rsa_oaep_decrypt_rejects_the_wrong_hash) { + const auto private_key{sourcemeta::core::make_private_key(PRIVATE_KEY_PEM)}; + const auto public_key{ + sourcemeta::core::derive_public_key(private_key.value())}; + const auto wrapped{sourcemeta::core::rsa_oaep_encrypt( + public_key.value(), sourcemeta::core::RSAOAEPHash::SHA256, "hello")}; + EXPECT_FALSE(sourcemeta::core::rsa_oaep_decrypt( + private_key.value(), sourcemeta::core::RSAOAEPHash::SHA1, + wrapped.value()) + .has_value()); +} + +TEST(rsa_oaep_encrypt_rejects_a_too_long_plaintext) { + const auto private_key{sourcemeta::core::make_private_key(PRIVATE_KEY_PEM)}; + const auto public_key{ + sourcemeta::core::derive_public_key(private_key.value())}; + // The limit for SHA-256 over a 2048-bit modulus is 190 bytes + const std::string plaintext(200, 'x'); + EXPECT_FALSE( + sourcemeta::core::rsa_oaep_encrypt( + public_key.value(), sourcemeta::core::RSAOAEPHash::SHA256, plaintext) + .has_value()); +} diff --git a/test/crypto/wycheproof_test.cc b/test/crypto/wycheproof_test.cc index 4f319bc88b..b15636c8ca 100644 --- a/test/crypto/wycheproof_test.cc +++ b/test/crypto/wycheproof_test.cc @@ -578,6 +578,82 @@ auto register_aes_kw_tests(const std::filesystem::path &path, } } } + +auto to_oaep_hash(const std::string_view name) + -> std::optional { + if (name == "SHA-1") { + return sourcemeta::core::RSAOAEPHash::SHA1; + } else if (name == "SHA-256") { + return sourcemeta::core::RSAOAEPHash::SHA256; + } else { + return std::nullopt; + } +} + +// A valid vector must decrypt to exactly the plaintext. An invalid vector must +// be rejected outright, never decrypted to some other value, so that a failed +// padding check cannot be masked +auto check_rsa_oaep(const std::string_view pem, + const sourcemeta::core::RSAOAEPHash hash, + const std::string_view ciphertext, + const std::string_view plaintext, const bool expected) + -> void { + const auto private_key{sourcemeta::core::make_private_key(pem)}; + EXPECT_TRUE(private_key.has_value()); + const auto decrypted{sourcemeta::core::rsa_oaep_decrypt(private_key.value(), + hash, ciphertext)}; + if (expected) { + EXPECT_TRUE(decrypted.has_value()); + EXPECT_EQ(decrypted.value(), plaintext); + } else { + EXPECT_FALSE(decrypted.has_value()); + } +} + +auto register_rsa_oaep_tests(const std::filesystem::path &path, + const std::string &suite_name) -> void { + const auto document{load(path)}; + const auto stem{path.stem().string()}; + for (const auto &group : document.at("testGroups").as_array()) { + const auto hash{to_oaep_hash(group.at("sha").to_string())}; + // The supported algorithms use the same hash for the label and the mask, + // and an empty label + if (!hash.has_value() || + group.at("sha").to_string() != group.at("mgfSha").to_string()) { + continue; + } + + const auto key{sourcemeta::core::hex_to_bytes( + group.at("privateKeyPkcs8").to_string())}; + if (!key.has_value()) { + continue; + } + + const auto pem{to_pkcs8_pem(key.value())}; + for (const auto &test : group.at("tests").as_array()) { + const std::string result{test.at("result").to_string()}; + if (result == "acceptable" || !test.at("label").to_string().empty()) { + continue; + } + + const auto message{ + sourcemeta::core::hex_to_bytes(test.at("msg").to_string())}; + const auto ciphertext{ + sourcemeta::core::hex_to_bytes(test.at("ct").to_string())}; + if (!message.has_value() || !ciphertext.has_value()) { + continue; + } + + register_case( + suite_name, + stem + "_tc" + std::to_string(test.at("tcId").to_integer()), + [pem, hash = hash.value(), ciphertext = ciphertext.value(), + plaintext = message.value(), expected = (result == "valid")]() { + check_rsa_oaep(pem, hash, ciphertext, plaintext, expected); + }); + } + } +} } // namespace auto main(int argc, char **argv) -> int { @@ -646,6 +722,14 @@ auto main(int argc, char **argv) -> int { register_aes_cbc_hmac_tests(vectors / "a256cbc_hs512_test.json", "Wycheproof_A256CBC_HS512"); register_aes_kw_tests(vectors / "aes_wrap_test.json", "Wycheproof_AES_KW"); + register_rsa_oaep_tests(vectors / "rsa_oaep_2048_sha1_mgf1sha1_test.json", + "Wycheproof_RSA_OAEP_SHA1"); + register_rsa_oaep_tests(vectors / "rsa_oaep_2048_sha256_mgf1sha256_test.json", + "Wycheproof_RSA_OAEP_2048_SHA256"); + register_rsa_oaep_tests(vectors / "rsa_oaep_3072_sha256_mgf1sha256_test.json", + "Wycheproof_RSA_OAEP_3072_SHA256"); + register_rsa_oaep_tests(vectors / "rsa_oaep_4096_sha256_mgf1sha256_test.json", + "Wycheproof_RSA_OAEP_4096_SHA256"); return sourcemeta::core::test_run(argc, argv); } From 47f4c746886646138a1fe2c1784300c8eee1c81e Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Thu, 23 Jul 2026 15:34:24 -0300 Subject: [PATCH 2/3] Fix Signed-off-by: Juan Cruz Viotti --- src/core/crypto/include/sourcemeta/core/crypto_rsa_oaep.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/core/crypto/include/sourcemeta/core/crypto_rsa_oaep.h b/src/core/crypto/include/sourcemeta/core/crypto_rsa_oaep.h index e8a92ab690..26c79c66a1 100644 --- a/src/core/crypto/include/sourcemeta/core/crypto_rsa_oaep.h +++ b/src/core/crypto/include/sourcemeta/core/crypto_rsa_oaep.h @@ -18,7 +18,12 @@ namespace sourcemeta::core { /// @ingroup crypto /// The hash function that RSA-OAEP uses for both the label digest and the mask /// generation function. -enum class RSAOAEPHash : std::uint8_t { SHA1, SHA256 }; +enum class RSAOAEPHash : std::uint8_t { + /// The SHA-1 hash function, selecting RSA-OAEP. + SHA1, + /// The SHA-256 hash function, selecting RSA-OAEP-256. + SHA256 +}; /// @ingroup crypto /// Encrypt a short key or message with RSA-OAEP (RFC 8017) under an RSA public From 8aaa245af11270fa49d7ec23cbf62625c9741e2a Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Thu, 23 Jul 2026 15:50:35 -0300 Subject: [PATCH 3/3] More Signed-off-by: Juan Cruz Viotti --- src/core/crypto/crypto_rsa_oaep_apple.cc | 3 +- src/core/crypto/crypto_rsa_oaep_openssl.cc | 11 +++- src/core/crypto/crypto_rsa_oaep_other.cc | 3 +- src/core/crypto/crypto_rsa_oaep_windows.cc | 16 ++++- test/crypto/crypto_rsa_oaep_test.cc | 72 ++++++++++++++++++++++ test/crypto/wycheproof_test.cc | 4 ++ 6 files changed, 104 insertions(+), 5 deletions(-) diff --git a/src/core/crypto/crypto_rsa_oaep_apple.cc b/src/core/crypto/crypto_rsa_oaep_apple.cc index dc48d2c195..6e9718fceb 100644 --- a/src/core/crypto/crypto_rsa_oaep_apple.cc +++ b/src/core/crypto/crypto_rsa_oaep_apple.cc @@ -82,7 +82,8 @@ auto rsa_oaep_decrypt(const PrivateKey &key, const RSAOAEPHash hash, const std::string_view ciphertext) -> std::optional { const auto *const internal{key.internal()}; - if (internal == nullptr || internal->kind != PrivateKey::Type::RSA) { + if (internal == nullptr || internal->kind != PrivateKey::Type::RSA || + internal->rsa_pss_restricted) { return std::nullopt; } diff --git a/src/core/crypto/crypto_rsa_oaep_openssl.cc b/src/core/crypto/crypto_rsa_oaep_openssl.cc index 679b403c14..145daafec3 100644 --- a/src/core/crypto/crypto_rsa_oaep_openssl.cc +++ b/src/core/crypto/crypto_rsa_oaep_openssl.cc @@ -84,7 +84,16 @@ auto rsa_oaep_decrypt(const PrivateKey &key, const RSAOAEPHash hash, const std::string_view ciphertext) -> std::optional { const auto *const internal{key.internal()}; - if (internal == nullptr || internal->kind != PrivateKey::Type::RSA) { + if (internal == nullptr || internal->kind != PrivateKey::Type::RSA || + internal->rsa_pss_restricted) { + return std::nullopt; + } + + // OpenSSL reads the ciphertext as a raw integer, so a shorter input with a + // dropped leading zero would decrypt as the same value. Require the exact + // modulus size so that a malformed ciphertext is rejected + if (ciphertext.size() != + static_cast(EVP_PKEY_get_size(internal->key))) { return std::nullopt; } diff --git a/src/core/crypto/crypto_rsa_oaep_other.cc b/src/core/crypto/crypto_rsa_oaep_other.cc index 6aedfe5614..8570ef371b 100644 --- a/src/core/crypto/crypto_rsa_oaep_other.cc +++ b/src/core/crypto/crypto_rsa_oaep_other.cc @@ -165,7 +165,8 @@ auto rsa_oaep_decrypt(const PrivateKey &key, const RSAOAEPHash hash, const std::string_view ciphertext) -> std::optional { const auto *const internal{key.internal()}; - if (internal == nullptr || internal->kind != PrivateKey::Type::RSA) { + if (internal == nullptr || internal->kind != PrivateKey::Type::RSA || + internal->rsa_pss_restricted) { return std::nullopt; } diff --git a/src/core/crypto/crypto_rsa_oaep_windows.cc b/src/core/crypto/crypto_rsa_oaep_windows.cc index 099904e11c..1a7492141e 100644 --- a/src/core/crypto/crypto_rsa_oaep_windows.cc +++ b/src/core/crypto/crypto_rsa_oaep_windows.cc @@ -1,6 +1,6 @@ #include -#include // ULONG, PUCHAR, LPCWSTR +#include // ULONG, PUCHAR, LPCWSTR, DWORD // clang-format off #include // BCrypt*, BCRYPT_* // clang-format on @@ -92,7 +92,19 @@ auto rsa_oaep_decrypt(const PrivateKey &key, const RSAOAEPHash hash, const std::string_view ciphertext) -> std::optional { const auto *const internal{key.internal()}; - if (internal == nullptr || internal->kind != PrivateKey::Type::RSA) { + if (internal == nullptr || internal->kind != PrivateKey::Type::RSA || + internal->rsa_pss_restricted) { + return std::nullopt; + } + + // Reject any ciphertext that is not exactly the modulus size, so that an + // input with appended or missing bytes cannot decrypt as the same integer + DWORD key_bits{0}; + ULONG property_size{0}; + if (!BCRYPT_SUCCESS(BCryptGetProperty(internal->key, BCRYPT_KEY_LENGTH, + reinterpret_cast(&key_bits), + sizeof(key_bits), &property_size, 0)) || + ciphertext.size() != (key_bits + 7u) / 8u) { return std::nullopt; } diff --git a/test/crypto/crypto_rsa_oaep_test.cc b/test/crypto/crypto_rsa_oaep_test.cc index 8ece998518..042994e68e 100644 --- a/test/crypto/crypto_rsa_oaep_test.cc +++ b/test/crypto/crypto_rsa_oaep_test.cc @@ -37,6 +37,39 @@ VoqHMouZBlcLfRprpPpQWlay -----END PRIVATE KEY----- )"}; +// A 2048-bit key whose PKCS#8 algorithm is id-RSASSA-PSS rather than +// rsaEncryption, so it is restricted to PSS and must refuse OAEP decryption +static const std::string PSS_RESTRICTED_PRIVATE_KEY_PEM{ + R"(-----BEGIN PRIVATE KEY----- +MIIEugIBADALBgkqhkiG9w0BAQoEggSmMIIEogIBAAKCAQEAmLPlf54G5cTEBgZs +sMSaCFtdTcy0Yjv6HAHK0BLYqdsnff6WMGmuSDdNZ/Oh++a3hTGV6ZdpvNFEo1MD +Te9CrIL9RwdrY+q9g4KsJhrmBdJPEfg0yIzJwTexQYiovy26X8KFQ3QJ3I6M3evZ +bjvskmIMbpvtbucT5JVRaQ1RwlxSzZj0EsY6ur5UVwgYYotsfmmqurzyyfqUrq6O +tifWiCDSpXk/0d920NikHFyR7xbRGVTXyL+iiCDHI7EO91NnA7RzIoFWRhAedkRQ +7Kt9qj4PdRwMc/bXeihq+7cb+zM17N1jcbLynu+39cPiq1YKqULCY3WiWDmjaq5U +XsONjQIDAQABAoIBAB0yHhG7ktmI+KqnPU5B1Kp+33TBzAZRLdV/gTmttMerC16z +V59bgVM04aObqQ+a0eFRNPazuKd9gmhQtZvHwGFv7QGQ2VdB+SiFCimB8JNR3cTT +hjIG+wcqgQVE3fCpi04GSMj4DW+iQKwojQqewfFN9k8KmIeg/kRwyR8zCPwGM36Y +OUPZmlhWTBYX3J/Qju2KQU+bvarJ+LWRifFl7CgTSFeIjOzFJfWf5yasP/U3CdEB +9qP+UhCswg2x2y+V0BeBM9KaEX5OOl/yAUggMYkQ5uq8ZOKpuCP4yS5yhd3Fvo1I +XIMCFBzfrzOyGKasSgQITo2Pltk0pOQUShyE+rECgYEA1wOh9AdvbQH3Qf19cvKZ +pGFs8zhoVEGJ2xz04wT78cfjZJE1Yd/WYN6GacN5dbYt/Psoc62v5l/xDB4MZXYh +B8noyAiYitIBkQ5P75ycrhjk+fqU/uhJvHNOb6tnu3XL4phJl2NmfYcnpMtjxVGT +fJdhfTGM6MScjYrxG711nf0CgYEAtc+O8KNdlSKufsrUgNzsJJxOgi2MOxDxu9Il +4YHoegl9Z6W1FCLVFyLcpkfCFjDewXQfyEhgkojwYDbbbwyHMOCugysnrEYWs1JX +btxRulHdzRhvIFHz3u51ewvAS6FDdpfM/dUcl1sdVcwRJ1c872zTOkcL9EMxqNto +xx8vetECgYBAWzX+dLtFRXFcryL9ZN/X89FIe3m+vl8k1mX2DWfb1piZYV05DmZ8 +WB6jSX7xXLYnIoXZGgOsUMs1dUkAlXsNecHTHb+KzZDqef4zGg1Ljuf6aqZuJdjs +LxcrFYLW+UstZ6efSIFE0U9sY/RY+zHJ+QWVE1+5zB+PviasxuiNgQKBgGLutOuB +GhVjL+zS1lvg26b4X0g7HMmvaLs5mV9i32w46cKSyzxP0ACs+cCJ37VPlodSd1D3 +AYX7ekIA19tPx+jy+kNqIkZ+RTADKIys2tQ2ZCmMmDvQHJI81DTGqjb9Y8aOx/+A +DfTWodnkF5l+wSvP3gkiTAD453bpHdTsxVthAoGAB3Um+ocT8vaeV1PaS90Ztll1 ++mIMYna3Slag9HTvUJEDxndXDezR+vJPmX6bNgGyoSYu9mQ7yu9n/QlWl/eR2ilB +8xn3yBD8MzAdeRaWKQOrsg+2UIWW+I2H90xdyp57V15Hw0XTDZ05jAuflOFj+cPK +zz/p2wIRJlfNHxRap+Q= +-----END PRIVATE KEY----- +)"}; + TEST(rsa_oaep_round_trips_with_sha256) { const std::string_view plaintext{"a content encryption key"}; const auto private_key{sourcemeta::core::make_private_key(PRIVATE_KEY_PEM)}; @@ -132,3 +165,42 @@ TEST(rsa_oaep_encrypt_rejects_a_too_long_plaintext) { public_key.value(), sourcemeta::core::RSAOAEPHash::SHA256, plaintext) .has_value()); } + +TEST(rsa_oaep_decrypt_rejects_a_ciphertext_of_the_wrong_length) { + const auto private_key{sourcemeta::core::make_private_key(PRIVATE_KEY_PEM)}; + EXPECT_TRUE(private_key.has_value()); + const auto public_key{ + sourcemeta::core::derive_public_key(private_key.value())}; + EXPECT_TRUE(public_key.has_value()); + const auto wrapped{sourcemeta::core::rsa_oaep_encrypt( + public_key.value(), sourcemeta::core::RSAOAEPHash::SHA256, "hello")}; + EXPECT_TRUE(wrapped.has_value()); + + const std::string appended{wrapped.value() + '\x00'}; + EXPECT_FALSE( + sourcemeta::core::rsa_oaep_decrypt( + private_key.value(), sourcemeta::core::RSAOAEPHash::SHA256, appended) + .has_value()); + + const std::string truncated{wrapped.value().substr(1)}; + EXPECT_FALSE( + sourcemeta::core::rsa_oaep_decrypt( + private_key.value(), sourcemeta::core::RSAOAEPHash::SHA256, truncated) + .has_value()); +} + +TEST(rsa_oaep_decrypt_refuses_a_pss_restricted_key) { + const auto private_key{ + sourcemeta::core::make_private_key(PSS_RESTRICTED_PRIVATE_KEY_PEM)}; + EXPECT_TRUE(private_key.has_value()); + const auto public_key{ + sourcemeta::core::derive_public_key(private_key.value())}; + EXPECT_TRUE(public_key.has_value()); + const auto wrapped{sourcemeta::core::rsa_oaep_encrypt( + public_key.value(), sourcemeta::core::RSAOAEPHash::SHA256, "hello")}; + EXPECT_TRUE(wrapped.has_value()); + EXPECT_FALSE(sourcemeta::core::rsa_oaep_decrypt( + private_key.value(), sourcemeta::core::RSAOAEPHash::SHA256, + wrapped.value()) + .has_value()); +} diff --git a/test/crypto/wycheproof_test.cc b/test/crypto/wycheproof_test.cc index b15636c8ca..047eead270 100644 --- a/test/crypto/wycheproof_test.cc +++ b/test/crypto/wycheproof_test.cc @@ -600,6 +600,10 @@ auto check_rsa_oaep(const std::string_view pem, -> void { const auto private_key{sourcemeta::core::make_private_key(pem)}; EXPECT_TRUE(private_key.has_value()); + if (!private_key.has_value()) { + return; + } + const auto decrypted{sourcemeta::core::rsa_oaep_decrypt(private_key.value(), hash, ciphertext)}; if (expected) {