Skip to content

Commit 0cadf3d

Browse files
committed
read rsa public key from JWK
1 parent 89b4e8a commit 0cadf3d

File tree

3 files changed

+196
-4
lines changed

3 files changed

+196
-4
lines changed

include/jwt-cpp/jwt.h

Lines changed: 131 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@
7373
#define JWT_CLAIM_EXPLICIT explicit
7474
#endif
7575

76+
#ifdef JWT_OPENSSL_3_0
77+
#include <openssl/param_build.h>
78+
#endif
79+
7680
/**
7781
* \brief JSON Web Token
7882
*
@@ -976,6 +980,9 @@ namespace jwt {
976980
} else
977981
throw rsa_exception(error::rsa_error::no_key_provided);
978982
}
983+
984+
rsa(helper::evp_pkey_handle pkey, const EVP_MD* (*md)(), std::string name)
985+
: pkey(pkey), md(md), alg_name(std::move(name)) {}
979986
/**
980987
* Sign jwt data
981988
* \param data The data to sign
@@ -3134,10 +3141,28 @@ namespace jwt {
31343141

31353142
public:
31363143
JWT_CLAIM_EXPLICIT jwk(const typename json_traits::string_type& str)
3137-
: jwk_claims(details::map_of_claims<json_traits>::parse_claims(str)) {}
3144+
: jwk(details::map_of_claims<json_traits>::parse_claims(str)) {}
3145+
3146+
JWT_CLAIM_EXPLICIT jwk(const typename json_traits::value_type& json) : jwk(json_traits::as_object(json)) {}
3147+
3148+
JWT_CLAIM_EXPLICIT jwk(const typename json_traits::object_type& json)
3149+
: jwk_claims(json), key(build_key(jwk_claims)) {
3150+
// https://datatracker.ietf.org/doc/html/rfc7518#section-6.1
3151+
// * indicate required params
3152+
// "kty"* : "EC", "RSA", "oct"
31383153

3139-
JWT_CLAIM_EXPLICIT jwk(const typename json_traits::value_type& json)
3140-
: jwk_claims(json_traits::as_object(json)) {}
3154+
// if "EC", then "crv"*, then "x"*. if "crv" is any of "P-256", "P-384", "P-521", then "y"*
3155+
// if "EC" and private key, then "d"*
3156+
3157+
// if "RSA", then "n"*, "e"*
3158+
// if "RSA" and private, then "d"*
3159+
// if "RSA" and any of the following is present, then all must be present
3160+
// "p", "q", "dp", "dq", "qi"
3161+
// "oth" - array of objects consisting of "r"*, "d"*, "t"*
3162+
3163+
// if "oct", then "k"*
3164+
// if "oct", then SHOULD contain "alg"
3165+
}
31413166

31423167
/**
31433168
* Get key type claim
@@ -3316,6 +3341,109 @@ namespace jwt {
33163341
}
33173342

33183343
bool empty() const noexcept { return jwk_claims.empty(); }
3344+
3345+
helper::evp_pkey_handle get_pkey() const { return key.get_asymmetric_key(); }
3346+
3347+
std::string get_oct_key() const { return key.get_symmetric_key(); }
3348+
3349+
private:
3350+
class key {
3351+
public:
3352+
static key symmetric(const std::string& bytes) { return key(bytes); }
3353+
3354+
static key asymmetric(helper::evp_pkey_handle pkey) { return key(pkey); }
3355+
3356+
std::string get_symmetric_key() const {
3357+
if (!is_symmetric) { throw std::logic_error("not a symmetric key"); }
3358+
3359+
return oct_key;
3360+
}
3361+
3362+
helper::evp_pkey_handle get_asymmetric_key() const {
3363+
if (is_symmetric) { throw std::logic_error("not an asymmetric key"); }
3364+
3365+
return pkey;
3366+
}
3367+
3368+
private:
3369+
key(const std::string& key) {
3370+
is_symmetric = true;
3371+
oct_key = key;
3372+
}
3373+
3374+
key(helper::evp_pkey_handle key) {
3375+
is_symmetric = false;
3376+
pkey = key;
3377+
}
3378+
3379+
bool is_symmetric;
3380+
helper::evp_pkey_handle pkey;
3381+
std::string oct_key;
3382+
};
3383+
3384+
static helper::evp_pkey_handle build_rsa_key(const details::map_of_claims<json_traits>& claims) {
3385+
EVP_PKEY* evp_key = nullptr;
3386+
auto n = jwt::helper::raw2bn(
3387+
base::decode<alphabet::base64url>(base::pad<alphabet::base64url>(claims.get_claim("n").as_string())));
3388+
auto e = jwt::helper::raw2bn(
3389+
base::decode<alphabet::base64url>(base::pad<alphabet::base64url>(claims.get_claim("e").as_string())));
3390+
#ifdef JWT_OPENSSL_3_0
3391+
// https://www.openssl.org/docs/manmaster/man7/EVP_PKEY-RSA.html
3392+
// see https://www.openssl.org/docs/man3.0/man3/EVP_PKEY_fromdata.html
3393+
// and https://stackoverflow.com/questions/68465716/how-to-properly-create-an-rsa-key-from-raw-data-in-openssl-3-0-in-c-language
3394+
std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)> ctx(
3395+
EVP_PKEY_CTX_new_from_name(NULL, "RSA", NULL), EVP_PKEY_CTX_free);
3396+
if (!ctx) { throw std::runtime_error("EVP_PKEY_CTX_new_from_name failed"); }
3397+
3398+
std::unique_ptr<OSSL_PARAM_BLD, decltype(&OSSL_PARAM_BLD_free)> params_build(OSSL_PARAM_BLD_new(),
3399+
OSSL_PARAM_BLD_free);
3400+
OSSL_PARAM_BLD_push_BN(params_build.get(), "n", n.get());
3401+
OSSL_PARAM_BLD_push_BN(params_build.get(), "e", e.get());
3402+
3403+
std::unique_ptr<OSSL_PARAM, decltype(&OSSL_PARAM_free)> params(OSSL_PARAM_BLD_to_param(params_build.get()),
3404+
OSSL_PARAM_free);
3405+
EVP_PKEY_fromdata_init(ctx.get());
3406+
EVP_PKEY_fromdata(ctx.get(), &evp_key, EVP_PKEY_PUBLIC_KEY, params.get());
3407+
return helper::evp_pkey_handle(evp_key);
3408+
#else
3409+
RSA* rsa = RSA_new();
3410+
evp_key = EVP_PKEY_new();
3411+
#if defined(JWT_OPENSSL_1_0_0) && !defined(LIBWOLFSSL_VERSION_HEX)
3412+
rsa->e = e.release();
3413+
rsa->n = n.release();
3414+
#else
3415+
RSA_set0_key(rsa, n.release(), e.release(), nullptr);
3416+
#endif
3417+
EVP_PKEY_assign_RSA(evp_key, rsa);
3418+
return helper::evp_pkey_handle(evp_key);
3419+
#endif
3420+
}
3421+
3422+
static key build_key(const details::map_of_claims<json_traits>& claims) {
3423+
if (!claims.has_claim("kty")) {
3424+
// TODO: custom exception or error code
3425+
throw std::runtime_error("missing required claim \"kty\"");
3426+
}
3427+
3428+
if (claims.get_claim("kty").get_type() != json::type::string) {
3429+
// TODO: custom exception or error code
3430+
throw std::runtime_error("\"kty\" claim must be of type 'string'");
3431+
}
3432+
3433+
if (claims.get_claim("kty").as_string() == "RSA") {
3434+
return key::asymmetric(build_rsa_key(claims));
3435+
} else if (claims.get_claim("kty").as_string() == "EC") {
3436+
// TODO: build EC key
3437+
throw std::runtime_error("not implemented");
3438+
} else if (claims.get_claim("kty").as_string() == "oct") {
3439+
return key::symmetric(base::decode<alphabet::base64url>(claims.get_claim("k").as_string()));
3440+
} else {
3441+
// TODO: do not build error messages like this
3442+
throw std::runtime_error("unknown key type (\"kty\"):" + claims.get_claim("kty").as_string());
3443+
}
3444+
}
3445+
3446+
key key;
33193447
};
33203448

33213449
/**

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)

tests/JwkTest.cpp

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#include "jwt-cpp/jwt.h"
2+
#include <gtest/gtest.h>
3+
4+
/*
5+
-----BEGIN RSA PRIVATE KEY-----
6+
MIIEpAIBAAKCAQEA2KvGTTxorqX2rvLOASWcBOqevGo5eIFFUVSlQIWe8x/a19mT
7+
XbYz6l3FYZ0s2W172nBpzJggi6PLK3397JsjjrzXb1pgXCVjIjM4hs5HDNtoWmC3
8+
/+O1jWknfrUHS95KWJkKg77B3ShGO1n8drfXCL4lWdo8F3UtVjHgfYEHI1xLSv3n
9+
v8QfGpOnWh4MAdmXhcankZQNuip6pwKlF+3y2WfukHN3rAmMx1MyID77srgPW7A3
10+
f0FTiGUz5gfUdp+5MMSQbhSZTM9fXu0buU4cmL35W9GoExjfjXlixpl70HWdyeLY
11+
SNyZKaw/Tqjwq769xf5sJZBe5UP6hL/dbLYhvQIDAQABAoIBACtorsAGnEpxQazn
12+
RFKCgHGTt92zwnPcIlEbDkiQ/Llk5mlcU+PwfxIzWzolTTj6cFfhMbElwU94r1m1
13+
Ukw3ALa2KstKZgfQDb5qWKbZaO6wfoWs3vBLZLJCIQGHr0CJ9octkie27gwq53c4
14+
nhYC2vgLcFxCFsv0U/Ly5zD9yrpQgv3DElKbc2zal/Z+kBt9MAN+2S4Fh2LaEUUl
15+
8QXjxdxbe3PHvX4nO5TWM3ztcfANzPDAWFJDeOgciUK6wEqTxkmgjh4uMaDG5X3V
16+
5xQRLBnFVXYzdwAVjzJVk9RvIDSQnEgYyHLBX6d190F85G8zQMVEwvs2VD1qJO+0
17+
BppwloECgYEA7T2xC7xGtxn/Tg9lhItZzE371mTffZDNbhL2YDQt6jBHq40cmmBi
18+
MzAYhV0Z7nky3bVHQUdaDLnJYIsqIrqqxGUZjcnkajhsSd1YGBwTHAv1njr+BX9a
19+
zY15u/pNb+OYY6naFHuTmen/NKSha+s+kHmQGCEKzErhfZ4yXhrPETECgYEA6c2y
20+
3iojU/P73RyUcoWQnDdOuQ8YNMAqoGwh/FzkG9futAiyhB3mGPmn7nw79rIO02Og
21+
Rjk7t1qSSL5DX1oAP3Fq/G1HE9epgM4j82Sa9vpUZpoLBefD2wUDjD4QmzKcFuYv
22+
M/Wl6dMLURllL24IdsEctR1p76Y7Spm251k/1k0CgYBpMxMAFjPxW6jXb4JfvP9L
23+
1kTXNBHad0xxBB2WWW0GzPPrAX7ugdDpy+kDsl4eXkYNBCadrssim3vNwMglcErr
24+
Hb2wHxeXdn+mXW2D+2cJ58+5o4Ui4O9d+N9DWOHfvLfFcfsPXCD+fkG5kUs3NLCg
25+
lhcsa/KC1q2Y636ANjkd8QKBgQCP882AilNMGnnljvY7eM8rz8XRnWCbAgJ82Xcn
26+
aY4tMotPH9fCDqKgl/50kNterg0AzGNfOVfyMXrF/ReAOurSJSPpHeNYbT15B/MM
27+
pdHf5QtYTNoinatyS6j+jSwuUj/WvY0sob+wsvdRzKAHTuk5LPde8ChMnH3/FZuO
28+
3921NQKBgQCSJf1kVFuxoWtdxqII0QJv6jSMaftK5xNkWCiLpmdttDpOdLRnMrYb
29+
XXkgbME3NheApU1oXIehLyXG46DmFXCNKME98NuJ5ENvLVkOsGyPhZDtQtQT099J
30+
z8gE19JF3RSmwvcaNpsLRg24BId/GmrZKgz9TEQMm+5wt93XcKtj6w==
31+
-----END RSA PRIVATE KEY-----
32+
-----BEGIN PUBLIC KEY-----
33+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2KvGTTxorqX2rvLOASWc
34+
BOqevGo5eIFFUVSlQIWe8x/a19mTXbYz6l3FYZ0s2W172nBpzJggi6PLK3397Jsj
35+
jrzXb1pgXCVjIjM4hs5HDNtoWmC3/+O1jWknfrUHS95KWJkKg77B3ShGO1n8drfX
36+
CL4lWdo8F3UtVjHgfYEHI1xLSv3nv8QfGpOnWh4MAdmXhcankZQNuip6pwKlF+3y
37+
2WfukHN3rAmMx1MyID77srgPW7A3f0FTiGUz5gfUdp+5MMSQbhSZTM9fXu0buU4c
38+
mL35W9GoExjfjXlixpl70HWdyeLYSNyZKaw/Tqjwq769xf5sJZBe5UP6hL/dbLYh
39+
vQIDAQAB
40+
-----END PUBLIC KEY-----
41+
*/
42+
43+
TEST(JwkTest, ParseKey) {
44+
std::string token =
45+
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.IzM0dgbhU1CsRbjmwyPHXkc8LagqFtsZD6p1ls_"
46+
"WBugkEKNfFmZmhOM1YYiFg59xId_KtzNdp4puzGIafut15U06DL2ZGH_H4xE7ONy6WLA_i5z5H8gPxD3ui2W4nHEEf-mvqKSn-"
47+
"bU8YPUydrwK3dVRfP5JA9XJT0KhssSCnty99y853xvuTh0484atxMjIk2LvnIWlYXFgoggC8TMY-4AtAJDfF8aVJXT0m-"
48+
"90oNevJbxMsuf5XFKo30TWxlnRw-y-QsYr9pxj2sA0BdwqRKVRRg5KF-"
49+
"p6rIEbAv3A6UuzLORvtixp5AASS7nrBlZ1BB8q2hYFCPtOv6UETIIkaQ";
50+
std::string public_key = R"({
51+
"kty": "RSA",
52+
"n": "2KvGTTxorqX2rvLOASWcBOqevGo5eIFFUVSlQIWe8x_a19mTXbYz6l3FYZ0s2W172nBpzJggi6PLK3397JsjjrzXb1pgXCVjIjM4hs5HDNtoWmC3_-O1jWknfrUHS95KWJkKg77B3ShGO1n8drfXCL4lWdo8F3UtVjHgfYEHI1xLSv3nv8QfGpOnWh4MAdmXhcankZQNuip6pwKlF-3y2WfukHN3rAmMx1MyID77srgPW7A3f0FTiGUz5gfUdp-5MMSQbhSZTM9fXu0buU4cmL35W9GoExjfjXlixpl70HWdyeLYSNyZKaw_Tqjwq769xf5sJZBe5UP6hL_dbLYhvQ",
53+
"e": "AQAB"
54+
})";
55+
56+
auto jwk = jwt::parse_jwk(public_key);
57+
ASSERT_EQ("RSA", jwk.get_key_type());
58+
auto alg = jwt::algorithm::rsa(jwk.get_pkey(), EVP_sha256, "RS256");
59+
auto verify = jwt::verify();
60+
auto decoded_token = jwt::decode(token);
61+
62+
ASSERT_NO_THROW(verify.do_verify(jwk, decoded_token));
63+
}

0 commit comments

Comments
 (0)