Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add helper for making RSA key from exponent and modulus #307

Merged
merged 78 commits into from
Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
11f82b7
- Added creation of public key from its components
zofer1zohargo Jul 30, 2023
f8023ee
- Added creation of public key from its components
zofer1zohargo Jul 30, 2023
8bd2d6f
Fixed missing lines
zofer1zohargo Jul 31, 2023
4e43738
Added unit tests
zofer1 Aug 1, 2023
dfe801d
Additional changes
zofer1 Aug 1, 2023
dbbd4c8
delete
zofer1 Aug 1, 2023
509cd23
refactor free
zofer1 Aug 1, 2023
fd6a174
remove testing
zofer1 Aug 1, 2023
b07e94a
align white space
zofer1 Aug 2, 2023
a56290b
Update tests/PubKeyTest.cpp
zofer1 Aug 2, 2023
761e67f
Add support for 1.0.2
zofer1 Aug 9, 2023
f2e28c3
refactor a bit
prince-chrismc Aug 21, 2023
03ad40c
user jwt define for SSL API that is cross-library
prince-chrismc Aug 21, 2023
4084ff9
add support for no base64 from jwt
prince-chrismc Aug 21, 2023
65ef8c8
refactor a bit more
prince-chrismc Aug 21, 2023
5ec65a7
fixup wolfssl + linters
prince-chrismc Aug 21, 2023
930ca53
missing include for openssl 3
prince-chrismc Aug 21, 2023
5408a66
quick draft of openssl 3 implementation
prince-chrismc Aug 21, 2023
16e3fde
linter
prince-chrismc Aug 21, 2023
3db461f
add in a health layer of error handling and memory management
prince-chrismc Aug 21, 2023
1becdb8
flip function order
prince-chrismc Aug 22, 2023
2de1b89
fix typo
prince-chrismc Aug 22, 2023
3a420a3
Merge branch 'master' into pr-298
prince-chrismc Dec 11, 2023
1577e9f
error tests for openssl
prince-chrismc Dec 11, 2023
acee64e
more all helper tests to one file
prince-chrismc Dec 11, 2023
5d620d2
update cmake
prince-chrismc Dec 11, 2023
6707752
update tests
prince-chrismc Dec 11, 2023
a183d49
more negative test cases
prince-chrismc Dec 12, 2023
48dcc3b
fixup old api calls depending on version
prince-chrismc Dec 12, 2023
ff9ca4c
linter
prince-chrismc Dec 12, 2023
60466e3
fixup return types
prince-chrismc Dec 12, 2023
1fdc5c4
disable failing tests
prince-chrismc Dec 12, 2023
e8e2041
fixup copy passed
prince-chrismc Dec 12, 2023
f9bd367
disable more tests
prince-chrismc Dec 12, 2023
65661ec
Merge branch 'master' into pr-298
prince-chrismc Dec 16, 2023
6351d8e
fixup broke tests
prince-chrismc Dec 16, 2023
8916764
better error code handling
prince-chrismc Dec 17, 2023
f31a3f7
test asan with more openssl versions
prince-chrismc Dec 17, 2023
5c1d410
fix template render path
prince-chrismc Dec 17, 2023
1817cf7
fix cache conflict
prince-chrismc Dec 17, 2023
e6add1b
add missing lib to tests
prince-chrismc Dec 17, 2023
b0ca86f
run both openssl versions with asan
prince-chrismc Dec 17, 2023
45e85b8
openssl code always calls new and returns an adress in most cases
prince-chrismc Dec 17, 2023
7d378eb
go back to old raw2bn implementation
prince-chrismc Dec 19, 2023
b33960b
Revert "openssl code always calls new and returns an adress in most c…
prince-chrismc Dec 19, 2023
eb2ad84
remove add new tests (needs to be a seperate pr)
prince-chrismc Dec 20, 2023
1bb3b4f
fix jwks verify example + run with asan+lsan
prince-chrismc Dec 20, 2023
694b172
some extra backwards compat for ssl1.1
prince-chrismc Dec 21, 2023
27aa353
compat rsa keygen
prince-chrismc Dec 21, 2023
ae7761c
fixup copy paste
prince-chrismc Dec 21, 2023
766d213
fix typo
prince-chrismc Dec 21, 2023
9b3cee8
linter
prince-chrismc Dec 21, 2023
9af941f
wolfssl compat
prince-chrismc Dec 21, 2023
78cca3a
compat 1.1.0i
prince-chrismc Dec 21, 2023
a78fb83
more compat
prince-chrismc Dec 21, 2023
96fa4d7
linter
prince-chrismc Dec 21, 2023
da8a9aa
fix 1.1.0
prince-chrismc Dec 21, 2023
b24a639
more compat
prince-chrismc Dec 21, 2023
1266a9b
more compat
prince-chrismc Dec 21, 2023
5b3051d
updated example with hardcoded values
prince-chrismc Dec 22, 2023
e2583d9
Merge remote-tracking branch 'upstream/master' into pr-298
prince-chrismc Dec 22, 2023
bb695ed
action to install openssl now sets ENV vars for CMake
prince-chrismc Dec 24, 2023
18a6a1f
update example to create a valid token
prince-chrismc Dec 24, 2023
59fb8cb
update asan to latest compiler
prince-chrismc Dec 24, 2023
a29443b
clang tidy
prince-chrismc Dec 24, 2023
cca8b30
fix memory leak in openssl 1.1.1
prince-chrismc Dec 24, 2023
494b5af
clang format
prince-chrismc Dec 24, 2023
bec2c24
improve docs
prince-chrismc Dec 24, 2023
766b6f8
enable one more test
prince-chrismc Dec 24, 2023
0129940
fix test impl signature
prince-chrismc Dec 25, 2023
5551b42
more test for bio to std::string
prince-chrismc Dec 25, 2023
c7a1fcd
DRY up code with new helper for write_bio_to_string
prince-chrismc Dec 25, 2023
d4e9a52
fix copy paste + spell checking
prince-chrismc Dec 26, 2023
4976de9
linter
prince-chrismc Dec 26, 2023
61cae6f
fix typo
prince-chrismc Dec 26, 2023
b650a64
fix return type
prince-chrismc Dec 26, 2023
f53e166
BIO_get_mem_data is just a define
prince-chrismc Dec 26, 2023
19f5eb9
put extra tests in too many places
prince-chrismc Dec 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 209 additions & 31 deletions include/jwt-cpp/jwt.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@

#if OPENSSL_VERSION_NUMBER >= 0x30000000L // 3.0.0
#define JWT_OPENSSL_3_0
#include <openssl/param_build.h>
#elif OPENSSL_VERSION_NUMBER >= 0x10101000L // 1.1.1
#define JWT_OPENSSL_1_1_1
#elif OPENSSL_VERSION_NUMBER >= 0x10100000L // 1.1.0
Expand Down Expand Up @@ -120,7 +121,9 @@ namespace jwt {
load_key_bio_write,
load_key_bio_read,
create_mem_bio_failed,
no_key_provided
no_key_provided,
set_rsa_failed,
create_context_failed
};
/**
* \brief Error category for RSA errors
Expand All @@ -141,6 +144,8 @@ namespace jwt {
case rsa_error::load_key_bio_read: return "failed to load key: bio read failed";
case rsa_error::create_mem_bio_failed: return "failed to create memory bio";
case rsa_error::no_key_provided: return "at least one of public or private key need to be present";
case rsa_error::set_rsa_failed: return "set modulus and exponent to RSA failed";
case rsa_error::create_context_failed: return "failed to create context";
default: return "unknown RSA error";
}
}
Expand Down Expand Up @@ -626,7 +631,6 @@ namespace jwt {
* \brief Convert the certificate provided as DER to PEM.
*
* \param cert_der_str String containing the DER certificate
* \param decode The function to decode the cert
* \throw rsa_exception if an error occurred
*/
inline std::string convert_der_to_pem(const std::string& cert_der_str) {
Expand Down Expand Up @@ -804,6 +808,51 @@ namespace jwt {
return pkey;
}

/**
* Convert a OpenSSL BIGNUM to a std::string
* \param bn BIGNUM to convert
* \return bignum as string
*/
inline
#ifdef JWT_OPENSSL_1_0_0
std::string
bn2raw(BIGNUM* bn)
#else
std::string
bn2raw(const BIGNUM* bn)
#endif
{
std::string res(BN_num_bytes(bn), '\0');
BN_bn2bin(bn, (unsigned char*)res.data()); // NOLINT(google-readability-casting) requires `const_cast`
return res;
}
/**
* Convert an std::string to a OpenSSL BIGNUM
* \param raw String to convert
* \param ec error_code for error_detection (gets cleared if no error occurs)
* \return BIGNUM representation
*/
inline std::unique_ptr<BIGNUM, decltype(&BN_free)> raw2bn(const std::string& raw, std::error_code& ec) {
auto bn =
BN_bin2bn(reinterpret_cast<const unsigned char*>(raw.data()), static_cast<int>(raw.size()), nullptr);
if (!bn) {
ec = error::rsa_error::set_rsa_failed;
return {nullptr, BN_free};
}
return {bn, BN_free};
}
/**
* Convert an std::string to a OpenSSL BIGNUM
* \param raw String to convert
* \return BIGNUM representation
*/
inline std::unique_ptr<BIGNUM, decltype(&BN_free)> raw2bn(const std::string& raw) {
std::error_code ec;
auto res = raw2bn(raw, ec);
error::throw_if_error(ec);
return res;
}

/**
* \brief Load a public key from a string.
*
Expand Down Expand Up @@ -846,6 +895,164 @@ namespace jwt {
return pkey;
}

/**
* \brief create public key from modulus and exponent
*
* \param modulus string containing base64 encoded modulus
* \param exponent string containing base64 encoded exponent
* \param decode The function to decode the certs)
* \param ec error_code for error_detection (gets cleared if no error occur
* \return public key in PEM format
*/
template<typename Decode>
std::string create_public_key_from_rsa_components(const std::string& modulus, const std::string& exponent,
Decode decode, std::error_code& ec) {
auto decoded_modulus = decode(modulus);
prince-chrismc marked this conversation as resolved.
Show resolved Hide resolved
auto decoded_exponent = decode(exponent);

auto n = helper::raw2bn(decoded_modulus);
auto e = helper::raw2bn(decoded_exponent);

#if defined(JWT_OPENSSL_3_0)
// OpenSSL deprecated mutable keys and there is a new way for making them
// https://mta.openssl.org/pipermail/openssl-users/2021-July/013994.html
// https://www.openssl.org/docs/man3.1/man3/OSSL_PARAM_BLD_new.html#Example-2
std::unique_ptr<OSSL_PARAM_BLD, decltype(&OSSL_PARAM_BLD_free)> param_bld(OSSL_PARAM_BLD_new(),
OSSL_PARAM_BLD_free);
if (!param_bld) {
ec = error::rsa_error::create_context_failed;
return {};
}

if (OSSL_PARAM_BLD_push_BN(param_bld.get(), "n", n.get()) != 1 ||
OSSL_PARAM_BLD_push_BN(param_bld.get(), "e", e.get()) != 1) {
ec = error::rsa_error::set_rsa_failed;
return {};
}

std::unique_ptr<OSSL_PARAM, decltype(&OSSL_PARAM_free)> params(OSSL_PARAM_BLD_to_param(param_bld.get()),
OSSL_PARAM_free);
if (!params) {
ec = error::rsa_error::set_rsa_failed;
return {};
}

std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)> ctx(
EVP_PKEY_CTX_new_from_name(nullptr, "RSA", nullptr), EVP_PKEY_CTX_free);
if (!ctx) {
ec = error::rsa_error::create_context_failed;
return {};
}

// https://www.openssl.org/docs/man3.0/man3/EVP_PKEY_fromdata.html#EXAMPLES
EVP_PKEY* pkey = NULL;
if (EVP_PKEY_fromdata_init(ctx.get()) < 0 ||
EVP_PKEY_fromdata(ctx.get(), &pkey, EVP_PKEY_KEYPAIR, params.get()) < 0) {
// It's unclear if this can fail after allocating but free it anyways
// https://www.openssl.org/docs/man3.0/man3/EVP_PKEY_fromdata.html
EVP_PKEY_free(pkey);

ec = error::rsa_error::cert_load_failed;
return {};
}

// Transfer ownership so we get ref counter and cleanup
evp_pkey_handle rsa(pkey);

#else
std::unique_ptr<RSA, decltype(&RSA_free)> rsa(RSA_new(), RSA_free);

// After this call RSA_free will also free the n and e big numbers
// See https://github.com/Thalhammer/jwt-cpp/pull/298#discussion_r1282619186
#if defined(JWT_OPENSSL_1_1_1) || defined(JWT_OPENSSL_1_1_0)
if (RSA_set0_key(rsa.get(), n.release(), e.release(), nullptr) != 1) {
ec = error::rsa_error::set_rsa_failed;
return {};
}
#elif defined(JWT_OPENSSL_1_0_0)
rsa->e = e.release();
rsa->n = n.release();
rsa->d = nullptr;
#endif
#endif

auto pub_key_bio = make_mem_buf_bio();
if (!pub_key_bio) {
ec = error::rsa_error::create_mem_bio_failed;
return {};
}

auto write_pem_to_bio =
#if defined(JWT_OPENSSL_3_0)
// https://www.openssl.org/docs/man3.1/man3/PEM_write_bio_RSA_PUBKEY.html
&PEM_write_bio_PUBKEY;
#else
&PEM_write_bio_RSA_PUBKEY;
#endif
if (write_pem_to_bio(pub_key_bio.get(), rsa.get()) != 1) {
ec = error::rsa_error::load_key_bio_write;
return {};
}

char* ptr = nullptr;
const auto len = BIO_get_mem_data(pub_key_bio.get(), &ptr);
if (len <= 0 || ptr == nullptr) {
ec = error::rsa_error::convert_to_pem_failed;
return {};
}

return std::string{ptr, static_cast<size_t>(len)};
}

/**
* \brief create public key from modulus and exponent
*
* \param modulus string containing base64 encoded modulus
* \param exponent string containing base64 encoded exponent
* \param decode The function to decode the certs)
* \return public key in PEM format
*/
template<typename Decode>
std::string create_public_key_from_rsa_components(const std::string& modulus, const std::string& exponent,
Decode decode) {
std::error_code ec;
auto res = create_public_key_from_rsa_components(modulus, exponent, decode, ec);
error::throw_if_error(ec);
return res;
}

#ifndef JWT_DISABLE_BASE64
/**
* \brief create public key from modulus and exponent
*
* \param modulus string containing base64 encoded modulus
* \param exponent string containing base64 encoded exponent
* \param ec error_code for error_detection (gets cleared if no error occur
* \return public key in PEM format
*/
inline std::string create_public_key_from_rsa_components(const std::string& modulus,
const std::string& exponent, std::error_code& ec) {
auto decode = [](const std::string& token) {
return base::decode<alphabet::base64url>(base::pad<alphabet::base64url>(token));
};
return create_public_key_from_rsa_components(modulus, exponent, std::move(decode), ec);
}
/**
* \brief create public key from modulus and exponent
*
* \param modulus string containing base64 encoded modulus
* \param exponent string containing base64 encoded exponent
* \return public key in PEM format
*/
inline std::string create_public_key_from_rsa_components(const std::string& modulus,
const std::string& exponent) {
std::error_code ec;
auto res = create_public_key_from_rsa_components(modulus, exponent, ec);
error::throw_if_error(ec);
return res;
}
#endif

/**
* \brief Load a private key from a string.
*
Expand All @@ -860,35 +1067,6 @@ namespace jwt {
error::throw_if_error(ec);
return res;
}

/**
* Convert a OpenSSL BIGNUM to a std::string
* \param bn BIGNUM to convert
* \return bignum as string
*/
inline
#ifdef JWT_OPENSSL_1_0_0
std::string
bn2raw(BIGNUM* bn)
#else
std::string
bn2raw(const BIGNUM* bn)
#endif
{
std::string res(BN_num_bytes(bn), '\0');
BN_bn2bin(bn, (unsigned char*)res.data()); // NOLINT(google-readability-casting) requires `const_cast`
return res;
}
/**
* Convert an std::string to a OpenSSL BIGNUM
* \param raw String to convert
* \return BIGNUM representation
*/
inline std::unique_ptr<BIGNUM, decltype(&BN_free)> raw2bn(const std::string& raw) {
return std::unique_ptr<BIGNUM, decltype(&BN_free)>(
BN_bin2bn(reinterpret_cast<const unsigned char*>(raw.data()), static_cast<int>(raw.size()), nullptr),
BN_free);
}
} // namespace helper

/**
Expand Down
7 changes: 4 additions & 3 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ set(TEST_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/Keys.cpp ${CMAKE_CURRENT_SOURCE_DIR}/HelperTest.cpp
${CMAKE_CURRENT_SOURCE_DIR}/TestMain.cpp ${CMAKE_CURRENT_SOURCE_DIR}/TokenFormatTest.cpp
${CMAKE_CURRENT_SOURCE_DIR}/TokenTest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/JwksTest.cpp
${CMAKE_CURRENT_SOURCE_DIR}/OpenSSLErrorTest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/traits/NlohmannTest.cpp)
${CMAKE_CURRENT_SOURCE_DIR}/OpenSSLErrorTest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/traits/NlohmannTest.cpp
${CMAKE_CURRENT_SOURCE_DIR}/PubKeyTest.cpp)

find_package(jsoncons CONFIG)
if(TARGET jsoncons)
Expand All @@ -34,7 +35,7 @@ add_executable(jwt-cpp-test ${TEST_SOURCES})

# NOTE: Don't use space inside a generator expression here, because the function prematurely breaks the expression into
# multiple lines. https://cmake.org/pipermail/cmake/2018-April/067422.html
set(JWT_TESTER_GCC_FLAGS -Wall -Wextra -Wpedantic)
set(JWT_TESTER_GCC_FLAGS -Wall -Wextra -Wpedantic -ggdb)
set(JWT_TESTER_CLANG_FLAGS -Weverything -Wno-c++98-compat -Wno-global-constructors -Wno-weak-vtables)
target_compile_options(
jwt-cpp-test PRIVATE $<$<CXX_COMPILER_ID:MSVC>:/W4> $<$<CXX_COMPILER_ID:GNU>:${JWT_TESTER_GCC_FLAGS}>
Expand All @@ -55,7 +56,7 @@ endif()
target_link_libraries(jwt-cpp-test PRIVATE jwt-cpp nlohmann_json::nlohmann_json
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:${CMAKE_DL_LIBS}>)

gtest_add_tests(TARGET jwt-cpp-test)
gtest_discover_tests(jwt-cpp-test)

if(JWT_ENABLE_COVERAGE)
include("code-coverage")
Expand Down
2 changes: 1 addition & 1 deletion tests/HelperTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ TEST(HelperTest, ErrorCodeMessages) {
std::string("token_verification_error"));

int i = 10;
for (i = 10; i < 19; i++) {
for (i = 10; i < 21; i++) {
ASSERT_NE(std::error_code(static_cast<jwt::error::rsa_error>(i)).message(),
std::error_code(static_cast<jwt::error::rsa_error>(-1)).message());
}
Expand Down
24 changes: 24 additions & 0 deletions tests/PubKeyTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#include "jwt-cpp/jwt.h"
#include <gtest/gtest.h>

std::string pubkey_expected =
R"(-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwoB3iVm4RW+6StkR+nut
x1fQevu2+t0Fu6KBcbvhfyHSXy7w0nJOdTT4jWLjStpRkNQBPZwMwHH35i+21gdn
JtDe/xfO8IX9McFmyodlBUcqX8CruIzDv9AXf2OjXPBG+4aq+03XKl5/muATl32+
+301Vw1dXoGYNeoWQqLTsHT3WS3tOOf+ehuzNuZ+rj+ephaD3lMBToEArrtC9R91
KTTN6YSAOK48NxTA8CfOMFK5itxfIqB5+E9OSQTidXyqLyoeA+xxTKMqYfxvypEe
k1oueAhY9u67NCBdmuavxtfyvwp7+o6Sd+NsewxAhmRKFexw13KOYzDhC+9aMJcu
JQIDAQAB
-----END PUBLIC KEY-----
)";

std::string modulus =
R"(AMKAd4lZuEVvukrZEfp7rcdX0Hr7tvrdBbuigXG74X8h0l8u8NJyTnU0-I1i40raUZDUAT2cDMBx9-YvttYHZybQ3v8XzvCF_THBZsqHZQVHKl_Aq7iMw7_QF39jo1zwRvuGqvtN1ypef5rgE5d9vvt9NVcNXV6BmDXqFkKi07B091kt7Tjn_nobszbmfq4_nqYWg95TAU6BAK67QvUfdSk0zemEgDiuPDcUwPAnzjBSuYrcXyKgefhPTkkE4nV8qi8qHgPscUyjKmH8b8qRHpNaLngIWPbuuzQgXZrmr8bX8r8Ke_qOknfjbHsMQIZkShXscNdyjmMw4QvvWjCXLiU)";
std::string exponent = R"(AQAB)";

TEST(PubKeyTest, RSAFromComponents) {
const auto pubkey = jwt::helper::create_public_key_from_rsa_components(modulus, exponent);

ASSERT_EQ(pubkey, pubkey_expected);
}
2 changes: 1 addition & 1 deletion tests/TokenTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,7 @@ TEST(TokenTest, VerifyTokenType) {
}

TEST(TokenTest, GetClaimThrows) {
auto token = "eyJhbGciOiJub25lIiwidHlwIjoiSldTIn0.eyJpc3MiOiJhdXRoMCJ9.";
const auto* token = "eyJhbGciOiJub25lIiwidHlwIjoiSldTIn0.eyJpc3MiOiJhdXRoMCJ9.";
auto decoded_token = jwt::decode(token);

ASSERT_THROW(decoded_token.get_header_claim("test"), jwt::error::claim_not_present_exception);
Expand Down