Skip to content

Commit

Permalink
🗜️ Simplify reading of EC keys (#185)
Browse files Browse the repository at this point in the history
* simplify reading of EC keys

* fix formatting
  • Loading branch information
kleinmrk authored Nov 19, 2021
1 parent b8e55e2 commit 66eea98
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 52 deletions.
154 changes: 116 additions & 38 deletions include/jwt-cpp/jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<EVP_PKEY>
load_public_ec_key_from_string(const std::string& key, const std::string& password, std::error_code& ec) {
ec.clear();
std::unique_ptr<BIO, decltype(&BIO_free_all)> 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<int>(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<int>(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<EVP_PKEY> 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 ecdsa_exception if an error occurred
*/
inline std::shared_ptr<EVP_PKEY> 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<EVP_PKEY>
load_private_ec_key_from_string(const std::string& key, const std::string& password, std::error_code& ec) {
std::unique_ptr<BIO, decltype(&BIO_free_all)> 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<int>(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<EVP_PKEY> pkey(
PEM_read_bio_PrivateKey(privkey_bio.get(), nullptr, nullptr, const_cast<char*>(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 ecdsa_exception if an error occurred
*/
inline std::shared_ptr<EVP_PKEY> 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
Expand Down Expand Up @@ -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<BIO, decltype(&BIO_free_all)> 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<int>(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<int>(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<BIO, decltype(&BIO_free_all)> 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<int>(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<char*>(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);
}
Expand Down
42 changes: 34 additions & 8 deletions tests/OpenSSLErrorTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 ============
*/
Expand Down Expand Up @@ -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<multitest_entry> 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<multitest_entry> 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(); }
});
Expand All @@ -632,7 +658,7 @@ TEST(OpenSSLErrorTest, ECDSAKey) {
TEST(OpenSSLErrorTest, ECDSACertificate) {
std::vector<multitest_entry> 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},
Expand Down
6 changes: 0 additions & 6 deletions tests/TokenTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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));
Expand Down

0 comments on commit 66eea98

Please sign in to comment.