From 25dd4af00fd4ab555b55b114a6a1fed604377977 Mon Sep 17 00:00:00 2001 From: Marek Date: Sun, 7 Nov 2021 13:49:30 +0100 Subject: [PATCH] simplify reading of EC keys --- include/jwt-cpp/jwt.h | 154 ++++++++++++++++++++++++++++--------- tests/OpenSSLErrorTest.cpp | 42 ++++++++-- tests/TokenTest.cpp | 6 -- 3 files changed, 150 insertions(+), 52 deletions(-) diff --git a/include/jwt-cpp/jwt.h b/include/jwt-cpp/jwt.h index d03bb5def..c40961b55 100644 --- a/include/jwt-cpp/jwt.h +++ b/include/jwt-cpp/jwt.h @@ -630,6 +630,111 @@ namespace jwt { return res; } + /** + * \brief Load a public key from a string. + * + * The string should contain a pem encoded certificate or public key + * + * \param certstr String containing the certificate encoded as pem + * \param pw Password used to decrypt certificate (leave empty if not encrypted) + * \param ec error_code for error_detection (gets cleared if no error occures) + */ + inline std::shared_ptr load_public_ec_key_from_string(const std::string& key, + const std::string& password, std::error_code& ec) { + ec.clear(); + std::unique_ptr pubkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); + if (!pubkey_bio) { + ec = error::ecdsa_error::create_mem_bio_failed; + return nullptr; + } + if (key.substr(0, 27) == "-----BEGIN CERTIFICATE-----") { + auto epkey = helper::extract_pubkey_from_cert(key, password, ec); + if (ec) return nullptr; + const int len = static_cast(epkey.size()); + if (BIO_write(pubkey_bio.get(), epkey.data(), len) != len) { + ec = error::ecdsa_error::load_key_bio_write; + return nullptr; + } + } else { + const int len = static_cast(key.size()); + if (BIO_write(pubkey_bio.get(), key.data(), len) != len) { + ec = error::ecdsa_error::load_key_bio_write; + return nullptr; + } + } + + std::shared_ptr pkey( + PEM_read_bio_PUBKEY(pubkey_bio.get(), nullptr, nullptr, + (void*)password.data()), // NOLINT(google-readability-casting) requires `const_cast` + EVP_PKEY_free); + if (!pkey) { + ec = error::ecdsa_error::load_key_bio_read; + return nullptr; + } + return pkey; + } + + /** + * \brief Load a public key from a string. + * + * The string should contain a pem encoded certificate or public key + * + * \param certstr String containing the certificate or key encoded as pem + * \param pw Password used to decrypt certificate or key (leave empty if not encrypted) + * \throw rsa_exception if an error occurred + */ + inline std::shared_ptr load_public_ec_key_from_string(const std::string& key, + const std::string& password = "") { + std::error_code ec; + auto res = load_public_ec_key_from_string(key, password, ec); + error::throw_if_error(ec); + return res; + } + + /** + * \brief Load a private key from a string. + * + * \param key String containing a private key as pem + * \param pw Password used to decrypt key (leave empty if not encrypted) + * \param ec error_code for error_detection (gets cleared if no error occures) + */ + inline std::shared_ptr + load_private_ec_key_from_string(const std::string& key, const std::string& password, std::error_code& ec) { + std::unique_ptr privkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); + if (!privkey_bio) { + ec = error::ecdsa_error::create_mem_bio_failed; + return nullptr; + } + const int len = static_cast(key.size()); + if (BIO_write(privkey_bio.get(), key.data(), len) != len) { + ec = error::ecdsa_error::load_key_bio_write; + return nullptr; + } + std::shared_ptr pkey( + PEM_read_bio_PrivateKey(privkey_bio.get(), nullptr, nullptr, const_cast(password.c_str())), + EVP_PKEY_free); + if (!pkey) { + ec = error::ecdsa_error::load_key_bio_read; + return nullptr; + } + return pkey; + } + + /** + * \brief Load a private key from a string. + * + * \param key String containing a private key as pem + * \param pw Password used to decrypt key (leave empty if not encrypted) + * \throw rsa_exception if an error occurred + */ + inline std::shared_ptr load_private_ec_key_from_string(const std::string& key, + const std::string& password = "") { + std::error_code ec; + auto res = load_private_ec_key_from_string(key, password, ec); + error::throw_if_error(ec); + return res; + } + /** * Convert a OpenSSL BIGNUM to a std::string * \param bn BIGNUM to convert @@ -883,46 +988,19 @@ namespace jwt { ecdsa(const std::string& public_key, const std::string& private_key, const std::string& public_key_password, const std::string& private_key_password, const EVP_MD* (*md)(), std::string name, size_t siglen) : md(md), alg_name(std::move(name)), signature_length(siglen) { - if (!public_key.empty()) { - std::unique_ptr pubkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); - if (!pubkey_bio) throw ecdsa_exception(error::ecdsa_error::create_mem_bio_failed); - if (public_key.substr(0, 27) == "-----BEGIN CERTIFICATE-----") { - auto epkey = helper::extract_pubkey_from_cert(public_key, public_key_password); - const int len = static_cast(epkey.size()); - if (BIO_write(pubkey_bio.get(), epkey.data(), len) != len) - throw ecdsa_exception(error::ecdsa_error::load_key_bio_write); - } else { - const int len = static_cast(public_key.size()); - if (BIO_write(pubkey_bio.get(), public_key.data(), len) != len) - throw ecdsa_exception(error::ecdsa_error::load_key_bio_write); - } - - pkey.reset(PEM_read_bio_EC_PUBKEY( - pubkey_bio.get(), nullptr, nullptr, - (void*)public_key_password - .c_str()), // NOLINT(google-readability-casting) requires `const_cast` - EC_KEY_free); - if (!pkey) throw ecdsa_exception(error::ecdsa_error::load_key_bio_read); - size_t keysize = EC_GROUP_get_degree(EC_KEY_get0_group(pkey.get())); - if (keysize != signature_length * 4 && (signature_length != 132 || keysize != 521)) - throw ecdsa_exception(error::ecdsa_error::invalid_key_size); - } - if (!private_key.empty()) { - std::unique_ptr privkey_bio(BIO_new(BIO_s_mem()), BIO_free_all); - if (!privkey_bio) throw ecdsa_exception(error::ecdsa_error::create_mem_bio_failed); - const int len = static_cast(private_key.size()); - if (BIO_write(privkey_bio.get(), private_key.data(), len) != len) - throw ecdsa_exception(error::ecdsa_error::load_key_bio_write); - pkey.reset(PEM_read_bio_ECPrivateKey(privkey_bio.get(), nullptr, nullptr, - const_cast(private_key_password.c_str())), - EC_KEY_free); - if (!pkey) throw ecdsa_exception(error::ecdsa_error::load_key_bio_read); - size_t keysize = EC_GROUP_get_degree(EC_KEY_get0_group(pkey.get())); - if (keysize != signature_length * 4 && (signature_length != 132 || keysize != 521)) - throw ecdsa_exception(error::ecdsa_error::invalid_key_size); + auto epkey = helper::load_private_ec_key_from_string(private_key, private_key_password); + pkey.reset(EVP_PKEY_get1_EC_KEY(epkey.get()), EC_KEY_free); + } else if (!public_key.empty()) { + auto epkey = helper::load_public_ec_key_from_string(public_key, public_key_password); + pkey.reset(EVP_PKEY_get1_EC_KEY(epkey.get()), EC_KEY_free); + } else { + throw ecdsa_exception(error::ecdsa_error::no_key_provided); } - if (!pkey) throw ecdsa_exception(error::ecdsa_error::no_key_provided); + if (!pkey) throw ecdsa_exception(error::ecdsa_error::invalid_key); + size_t keysize = EC_GROUP_get_degree(EC_KEY_get0_group(pkey.get())); + if (keysize != signature_length * 4 && (signature_length != 132 || keysize != 521)) + throw ecdsa_exception(error::ecdsa_error::invalid_key_size); if (EC_KEY_check_key(pkey.get()) == 0) throw ecdsa_exception(error::ecdsa_error::invalid_key); } diff --git a/tests/OpenSSLErrorTest.cpp b/tests/OpenSSLErrorTest.cpp index 7aedee0bb..d8efa5fed 100644 --- a/tests/OpenSSLErrorTest.cpp +++ b/tests/OpenSSLErrorTest.cpp @@ -42,6 +42,7 @@ static uint64_t fail_EVP_DigestSignInit = 0; static uint64_t fail_EVP_DigestSign = 0; static uint64_t fail_EVP_DigestVerifyInit = 0; static uint64_t fail_EVP_DigestVerify = 0; +static uint64_t fail_EVP_PKEY_get1_EC_KEY = 0; BIO* BIO_new(const BIO_METHOD* type) { static BIO* (*origMethod)(const BIO_METHOD*) = nullptr; @@ -342,6 +343,17 @@ int EVP_DigestVerify(EVP_MD_CTX* ctx, unsigned char* sigret, size_t* siglen, con return origMethod(ctx, sigret, siglen, tbs, tbslen); } +EC_KEY* EVP_PKEY_get1_EC_KEY(EVP_PKEY *pkey) { + static EC_KEY* (*origMethod)(EVP_PKEY *pkey) = nullptr; + if (origMethod == nullptr) origMethod = (decltype(origMethod))dlsym(RTLD_NEXT, "EVP_PKEY_get1_EC_KEY"); + bool fail = fail_EVP_PKEY_get1_EC_KEY & 1; + fail_EVP_PKEY_get1_EC_KEY = fail_EVP_PKEY_get1_EC_KEY >> 1; + if (fail) + return nullptr; + else + return origMethod(pkey); +} + /** * =========== End of black magic ============ */ @@ -609,21 +621,35 @@ TEST(OpenSSLErrorTest, RS256VerifyErrorCode) { run_multitest(mapping, [&alg, &signature](std::error_code& ec) { alg.verify("testdata", signature, ec); }); } -TEST(OpenSSLErrorTest, ECDSAKey) { +TEST(OpenSSLErrorTest, LoadECDSAPrivateKeyFromString) { + std::vector mapping{ + {&fail_BIO_new, 1, jwt::error::ecdsa_error::create_mem_bio_failed}, + {&fail_BIO_write, 1, jwt::error::ecdsa_error::load_key_bio_write}, + {&fail_PEM_read_bio_PrivateKey, 1, jwt::error::ecdsa_error::load_key_bio_read}, + {&fail_EC_KEY_check_key, 1, jwt::error::ecdsa_error::invalid_key}, + {&fail_EVP_PKEY_get1_EC_KEY, 1, jwt::error::ecdsa_error::invalid_key}, + }; + + run_multitest(mapping, [](std::error_code& ec) { + try { + jwt::algorithm::es256 alg{"", ecdsa256_priv_key}; + FAIL(); // Should never reach this + } catch (const std::system_error& e) { ec = e.code(); } + }); +} + +TEST(OpenSSLErrorTest, LoadECDSAPublicKeyFromString) { std::vector mapping{ {&fail_BIO_new, 1, jwt::error::ecdsa_error::create_mem_bio_failed}, {&fail_BIO_write, 1, jwt::error::ecdsa_error::load_key_bio_write}, - {&fail_PEM_read_bio_EC_PUBKEY, 1, jwt::error::ecdsa_error::load_key_bio_read}, - // Privkey section - {&fail_BIO_new, 2, jwt::error::ecdsa_error::create_mem_bio_failed}, - {&fail_BIO_write, 2, jwt::error::ecdsa_error::load_key_bio_write}, - {&fail_PEM_read_bio_ECPrivateKey, 1, jwt::error::ecdsa_error::load_key_bio_read}, + {&fail_PEM_read_bio_PUBKEY, 1, jwt::error::ecdsa_error::load_key_bio_read}, {&fail_EC_KEY_check_key, 1, jwt::error::ecdsa_error::invalid_key}, + {&fail_EVP_PKEY_get1_EC_KEY, 1, jwt::error::ecdsa_error::invalid_key}, }; run_multitest(mapping, [](std::error_code& ec) { try { - jwt::algorithm::es256 alg{ecdsa256_pub_key, ecdsa256_priv_key}; + jwt::algorithm::es256 alg{ecdsa256_pub_key, ""}; FAIL(); // Should never reach this } catch (const std::system_error& e) { ec = e.code(); } }); @@ -632,7 +658,7 @@ TEST(OpenSSLErrorTest, ECDSAKey) { TEST(OpenSSLErrorTest, ECDSACertificate) { std::vector mapping{{&fail_BIO_new, 1, jwt::error::ecdsa_error::create_mem_bio_failed}, {&fail_BIO_write, 1, jwt::error::ecdsa_error::load_key_bio_write}, - {&fail_PEM_read_bio_EC_PUBKEY, 1, jwt::error::ecdsa_error::load_key_bio_read}, + {&fail_PEM_read_bio_PUBKEY, 1, jwt::error::ecdsa_error::load_key_bio_read}, // extract_pubkey_from_cert {&fail_BIO_new, 2, jwt::error::rsa_error::create_mem_bio_failed}, {&fail_PEM_read_bio_X509, 1, jwt::error::rsa_error::cert_load_failed}, diff --git a/tests/TokenTest.cpp b/tests/TokenTest.cpp index bea047ffd..5be01bf5e 100644 --- a/tests/TokenTest.cpp +++ b/tests/TokenTest.cpp @@ -748,8 +748,6 @@ TEST(TokenTest, ThrowInvalidKeyLength) { // But also if only one cert has the wrong size ASSERT_THROW(jwt::algorithm::es256(ecdsa256_pub_key, ecdsa384_priv_key), jwt::ecdsa_exception); ASSERT_THROW(jwt::algorithm::es256(ecdsa256_pub_key, ecdsa521_priv_key), jwt::ecdsa_exception); - ASSERT_THROW(jwt::algorithm::es256(ecdsa384_pub_key, ecdsa256_priv_key), jwt::ecdsa_exception); - ASSERT_THROW(jwt::algorithm::es256(ecdsa521_pub_key, ecdsa256_priv_key), jwt::ecdsa_exception); ASSERT_THROW(jwt::algorithm::es384(ecdsa256_pub_key, ""), jwt::ecdsa_exception); ASSERT_THROW(jwt::algorithm::es384("", ecdsa256_priv_key), jwt::ecdsa_exception); @@ -760,8 +758,6 @@ TEST(TokenTest, ThrowInvalidKeyLength) { ASSERT_THROW(jwt::algorithm::es384(ecdsa384_pub_key, ecdsa256_priv_key), jwt::ecdsa_exception); ASSERT_THROW(jwt::algorithm::es384(ecdsa384_pub_key, ecdsa521_priv_key), jwt::ecdsa_exception); - ASSERT_THROW(jwt::algorithm::es384(ecdsa256_pub_key, ecdsa384_priv_key), jwt::ecdsa_exception); - ASSERT_THROW(jwt::algorithm::es384(ecdsa521_pub_key, ecdsa384_priv_key), jwt::ecdsa_exception); ASSERT_THROW(jwt::algorithm::es512(ecdsa256_pub_key, ""), jwt::ecdsa_exception); ASSERT_THROW(jwt::algorithm::es512("", ecdsa256_priv_key), jwt::ecdsa_exception); @@ -772,8 +768,6 @@ TEST(TokenTest, ThrowInvalidKeyLength) { ASSERT_THROW(jwt::algorithm::es512(ecdsa521_pub_key, ecdsa256_priv_key), jwt::ecdsa_exception); ASSERT_THROW(jwt::algorithm::es512(ecdsa521_pub_key, ecdsa384_priv_key), jwt::ecdsa_exception); - ASSERT_THROW(jwt::algorithm::es512(ecdsa256_pub_key, ecdsa521_priv_key), jwt::ecdsa_exception); - ASSERT_THROW(jwt::algorithm::es512(ecdsa384_pub_key, ecdsa521_priv_key), jwt::ecdsa_exception); // Make sure we do not throw if the correct params are passed ASSERT_NO_THROW(jwt::algorithm::es256(ecdsa256_pub_key, ecdsa256_priv_key));