Skip to content

Commit 7767b6f

Browse files
committed
read rsa public key from JWK
1 parent c050d5b commit 7767b6f

File tree

2 files changed

+94
-4
lines changed

2 files changed

+94
-4
lines changed

include/jwt-cpp/jwt.h

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@
6464
#define JWT_CLAIM_EXPLICIT explicit
6565
#endif
6666

67+
#ifdef JWT_OPENSSL_3_0
68+
#include <openssl/param_build.h>
69+
#endif
70+
6771
/**
6872
* \brief JSON Web Token
6973
*
@@ -3061,10 +3065,28 @@ namespace jwt {
30613065

30623066
public:
30633067
JWT_CLAIM_EXPLICIT jwk(const typename json_traits::string_type& str)
3064-
: jwk_claims(details::map_of_claims<json_traits>::parse_claims(str)) {}
3068+
: jwk(details::map_of_claims<json_traits>::parse_claims(str)) {}
3069+
3070+
JWT_CLAIM_EXPLICIT jwk(const typename json_traits::value_type& json) : jwk(json_traits::as_object(json)) {}
30653071

3066-
JWT_CLAIM_EXPLICIT jwk(const typename json_traits::value_type& json)
3067-
: jwk_claims(json_traits::as_object(json)) {}
3072+
JWT_CLAIM_EXPLICIT jwk(const typename json_traits::object_type& json) : jwk_claims(json) {
3073+
this->key = build_key(jwk_claims);
3074+
// https://datatracker.ietf.org/doc/html/rfc7518#section-6.1
3075+
// * indicate required params
3076+
// "kty"* : "EC", "RSA", "oct"
3077+
3078+
// if "EC", then "crv"*, then "x"*. if "crv" is any of "P-256", "P-384", "P-521", then "y"*
3079+
// if "EC" and private key, then "d"*
3080+
3081+
// if "RSA", then "n"*, "e"*
3082+
// if "RSA" and private, then "d"*
3083+
// if "RSA" and any of the following is present, then all must be present
3084+
// "p", "q", "dp", "dq", "qi"
3085+
// "oth" - array of objects consisting of "r"*, "d"*, "t"*
3086+
3087+
// if "oct", then "k"*
3088+
// if "oct", then SHOULD contain "alg"
3089+
}
30683090

30693091
/**
30703092
* Get key type claim
@@ -3243,6 +3265,73 @@ namespace jwt {
32433265
}
32443266

32453267
bool empty() const noexcept { return jwk_claims.empty(); }
3268+
3269+
private:
3270+
static std::shared_ptr<EVP_PKEY> build_rsa_key(const details::map_of_claims<json_traits>& claims) {
3271+
EVP_PKEY* evp_key = nullptr;
3272+
auto n = jwt::helper::raw2bn(
3273+
base::decode<alphabet::base64url>(base::pad<alphabet::base64url>(claims.get_claim("n").as_string())));
3274+
auto e = jwt::helper::raw2bn(
3275+
base::decode<alphabet::base64url>(base::pad<alphabet::base64url>(claims.get_claim("e").as_string())));
3276+
#ifdef JWT_OPENSSL_3_0
3277+
// https://www.openssl.org/docs/manmaster/man7/EVP_PKEY-RSA.html
3278+
// see https://www.openssl.org/docs/man3.0/man3/EVP_PKEY_fromdata.html
3279+
// and https://stackoverflow.com/questions/68465716/how-to-properly-create-an-rsa-key-from-raw-data-in-openssl-3-0-in-c-language
3280+
std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)> ctx(
3281+
EVP_PKEY_CTX_new_from_name(NULL, "RSA", NULL), EVP_PKEY_CTX_free);
3282+
if (!ctx) { throw std::runtime_error("EVP_PKEY_CTX_new_from_name failed"); }
3283+
3284+
std::unique_ptr<OSSL_PARAM_BLD, decltype(&OSSL_PARAM_BLD_free)> params_build(OSSL_PARAM_BLD_new(),
3285+
OSSL_PARAM_BLD_free);
3286+
OSSL_PARAM_BLD_push_BN(params_build.get(), "n", n.get());
3287+
OSSL_PARAM_BLD_push_BN(params_build.get(), "e", e.get());
3288+
3289+
std::unique_ptr<OSSL_PARAM, decltype(&OSSL_PARAM_free)> params(OSSL_PARAM_BLD_to_param(params_build.get()),
3290+
OSSL_PARAM_free);
3291+
EVP_PKEY_fromdata_init(ctx.get());
3292+
EVP_PKEY_fromdata(ctx.get(), &evp_key, EVP_PKEY_PUBLIC_KEY, params.get());
3293+
return std::shared_ptr<EVP_PKEY>(evp_key, EVP_PKEY_free);
3294+
#else
3295+
RSA* rsa = RSA_new();
3296+
evp_key = EVP_PKEY_new();
3297+
#if defined(JWT_OPENSSL_1_0_0) && !defined(LIBWOLFSSL_VERSION_HEX)
3298+
rsa->e = e.release();
3299+
rsa->n = n.release();
3300+
#else
3301+
RSA_set0_key(rsa, n.release(), e.release(), nullptr);
3302+
#endif
3303+
EVP_PKEY_assign_RSA(evp_key, rsa);
3304+
return std::shared_ptr<EVP_PKEY>(evp_key, EVP_PKEY_free);
3305+
#endif
3306+
}
3307+
3308+
static std::shared_ptr<EVP_PKEY> build_key(const details::map_of_claims<json_traits>& claims) {
3309+
if (!claims.has_claim("kty")) {
3310+
// TODO: custom exception or error code
3311+
throw std::runtime_error("missing required claim \"kty\"");
3312+
}
3313+
3314+
if (claims.get_claim("kty").get_type() != json::type::string) {
3315+
// TODO: custom exception or error code
3316+
throw std::runtime_error("\"kty\" claim must be of type 'string'");
3317+
}
3318+
3319+
if (claims.get_claim("kty").as_string() == "RSA") {
3320+
// TODO: build RSA key
3321+
return build_rsa_key(claims);
3322+
} else if (claims.get_claim("kty").as_string() == "EC") {
3323+
// TODO: build EC key
3324+
} else if (claims.get_claim("kty").as_string() == "oct") {
3325+
// TODO: store in std::string or something more unsigned-ish?
3326+
} else {
3327+
// TODO: do not build error messages like this
3328+
throw std::runtime_error("unknown key type (\"kty\"):" + claims.get_claim("kty").as_string());
3329+
}
3330+
3331+
return nullptr;
3332+
}
3333+
3334+
std::shared_ptr<EVP_PKEY> key;
32463335
};
32473336

32483337
/**

tests/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ set(TEST_SOURCES
1818
${CMAKE_CURRENT_SOURCE_DIR}/Keys.cpp ${CMAKE_CURRENT_SOURCE_DIR}/HelperTest.cpp
1919
${CMAKE_CURRENT_SOURCE_DIR}/TestMain.cpp ${CMAKE_CURRENT_SOURCE_DIR}/TokenFormatTest.cpp
2020
${CMAKE_CURRENT_SOURCE_DIR}/TokenTest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/JwksTest.cpp
21-
${CMAKE_CURRENT_SOURCE_DIR}/OpenSSLErrorTest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/traits/NlohmannTest.cpp)
21+
${CMAKE_CURRENT_SOURCE_DIR}/OpenSSLErrorTest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/traits/NlohmannTest.cpp
22+
${CMAKE_CURRENT_SOURCE_DIR}/JwkTest.cpp)
2223

2324
find_package(jsoncons CONFIG)
2425
if(TARGET jsoncons)

0 commit comments

Comments
 (0)