Skip to content

Commit c870903

Browse files
authored
Add helper for making EC key from components (Thalhammer#358)
1 parent a968dfe commit c870903

File tree

3 files changed

+453
-2
lines changed

3 files changed

+453
-2
lines changed

include/jwt-cpp/jwt.h

+241-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,9 @@ namespace jwt {
169169
get_key_failed,
170170
write_key_failed,
171171
write_cert_failed,
172-
convert_to_pem_failed
172+
convert_to_pem_failed,
173+
unknown_curve,
174+
set_ecdsa_failed
173175
};
174176
/**
175177
* \brief Error category for ECDSA errors
@@ -194,6 +196,8 @@ namespace jwt {
194196
case ecdsa_error::write_key_failed: return "error writing key data in PEM format";
195197
case ecdsa_error::write_cert_failed: return "error writing cert data in PEM format";
196198
case ecdsa_error::convert_to_pem_failed: return "failed to convert key to pem";
199+
case ecdsa_error::unknown_curve: return "unknown curve";
200+
case ecdsa_error::set_ecdsa_failed: return "set parameters to ECDSA failed";
197201
default: return "unknown ECDSA error";
198202
}
199203
}
@@ -1085,6 +1089,242 @@ namespace jwt {
10851089
error::throw_if_error(ec);
10861090
return res;
10871091
}
1092+
1093+
#if defined(JWT_OPENSSL_3_0)
1094+
1095+
/**
1096+
* \brief Convert a curve name to a group name.
1097+
*
1098+
* \param curve string containing curve name
1099+
* \param ec error_code for error_detection
1100+
* \return group name
1101+
*/
1102+
inline std::string curve2group(const std::string curve, std::error_code& ec) {
1103+
if (curve == "P-256") {
1104+
return "prime256v1";
1105+
} else if (curve == "P-384") {
1106+
return "secp384r1";
1107+
} else if (curve == "P-521") {
1108+
return "secp521r1";
1109+
} else {
1110+
ec = jwt::error::ecdsa_error::unknown_curve;
1111+
return {};
1112+
}
1113+
}
1114+
1115+
#else
1116+
1117+
/**
1118+
* \brief Convert a curve name to an ID.
1119+
*
1120+
* \param curve string containing curve name
1121+
* \param ec error_code for error_detection
1122+
* \return ID
1123+
*/
1124+
inline int curve2nid(const std::string curve, std::error_code& ec) {
1125+
if (curve == "P-256") {
1126+
return NID_X9_62_prime256v1;
1127+
} else if (curve == "P-384") {
1128+
return NID_secp384r1;
1129+
} else if (curve == "P-521") {
1130+
return NID_secp521r1;
1131+
} else {
1132+
ec = jwt::error::ecdsa_error::unknown_curve;
1133+
return {};
1134+
}
1135+
}
1136+
1137+
#endif
1138+
1139+
/**
1140+
* Create public key from curve name and coordinates. This is defined in
1141+
* [RFC 7518 Section 6.2](https://www.rfc-editor.org/rfc/rfc7518#section-6.2)
1142+
* Using the required "crv" (Curve), "x" (X Coordinate) and "y" (Y Coordinate) Parameters.
1143+
*
1144+
* \tparam Decode is callable, taking a string_type and returns a string_type.
1145+
* It should ensure the padding of the input and then base64url decode and
1146+
* return the results.
1147+
* \param curve string containing curve name
1148+
* \param x string containing base64url encoded x coordinate
1149+
* \param y string containing base64url encoded y coordinate
1150+
* \param decode The function to decode the RSA parameters
1151+
* \param ec error_code for error_detection (gets cleared if no error occur
1152+
* \return public key in PEM format
1153+
*/
1154+
template<typename Decode>
1155+
std::string create_public_key_from_ec_components(const std::string& curve, const std::string& x,
1156+
const std::string& y, Decode decode, std::error_code& ec) {
1157+
ec.clear();
1158+
auto decoded_x = decode(x);
1159+
auto decoded_y = decode(y);
1160+
1161+
#if defined(JWT_OPENSSL_3_0)
1162+
// OpenSSL deprecated mutable keys and there is a new way for making them
1163+
// https://mta.openssl.org/pipermail/openssl-users/2021-July/013994.html
1164+
// https://www.openssl.org/docs/man3.1/man3/OSSL_PARAM_BLD_new.html#Example-2
1165+
std::unique_ptr<OSSL_PARAM_BLD, decltype(&OSSL_PARAM_BLD_free)> param_bld(OSSL_PARAM_BLD_new(),
1166+
OSSL_PARAM_BLD_free);
1167+
if (!param_bld) {
1168+
ec = error::ecdsa_error::create_context_failed;
1169+
return {};
1170+
}
1171+
1172+
std::string group = helper::curve2group(curve, ec);
1173+
if (ec) return {};
1174+
1175+
// https://github.com/openssl/openssl/issues/16270#issuecomment-895734092
1176+
std::string pub = std::string("\x04").append(decoded_x).append(decoded_y);
1177+
1178+
if (OSSL_PARAM_BLD_push_utf8_string(param_bld.get(), "group", group.data(), group.size()) != 1 ||
1179+
OSSL_PARAM_BLD_push_octet_string(param_bld.get(), "pub", pub.data(), pub.size()) != 1) {
1180+
ec = error::ecdsa_error::set_ecdsa_failed;
1181+
return {};
1182+
}
1183+
1184+
std::unique_ptr<OSSL_PARAM, decltype(&OSSL_PARAM_free)> params(OSSL_PARAM_BLD_to_param(param_bld.get()),
1185+
OSSL_PARAM_free);
1186+
if (!params) {
1187+
ec = error::ecdsa_error::set_ecdsa_failed;
1188+
return {};
1189+
}
1190+
1191+
std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)> ctx(
1192+
EVP_PKEY_CTX_new_from_name(nullptr, "EC", nullptr), EVP_PKEY_CTX_free);
1193+
if (!ctx) {
1194+
ec = error::ecdsa_error::create_context_failed;
1195+
return {};
1196+
}
1197+
1198+
// https://www.openssl.org/docs/man3.0/man3/EVP_PKEY_fromdata.html#EXAMPLES
1199+
// Error codes based on https://www.openssl.org/docs/manmaster/man3/EVP_PKEY_fromdata_init.html#RETURN-VALUES
1200+
EVP_PKEY* pkey = NULL;
1201+
if (EVP_PKEY_fromdata_init(ctx.get()) <= 0 ||
1202+
EVP_PKEY_fromdata(ctx.get(), &pkey, EVP_PKEY_KEYPAIR, params.get()) <= 0) {
1203+
// It's unclear if this can fail after allocating but free it anyways
1204+
// https://www.openssl.org/docs/man3.0/man3/EVP_PKEY_fromdata.html
1205+
EVP_PKEY_free(pkey);
1206+
1207+
ec = error::ecdsa_error::cert_load_failed;
1208+
return {};
1209+
}
1210+
1211+
// Transfer ownership so we get ref counter and cleanup
1212+
evp_pkey_handle ecdsa(pkey);
1213+
1214+
#else
1215+
int nid = helper::curve2nid(curve, ec);
1216+
if (ec) return {};
1217+
1218+
auto qx = helper::raw2bn(decoded_x, ec);
1219+
if (ec) return {};
1220+
auto qy = helper::raw2bn(decoded_y, ec);
1221+
if (ec) return {};
1222+
1223+
std::unique_ptr<EC_GROUP, decltype(&EC_GROUP_free)> ecgroup(EC_GROUP_new_by_curve_name(nid), EC_GROUP_free);
1224+
if (!ecgroup) {
1225+
ec = error::ecdsa_error::set_ecdsa_failed;
1226+
return {};
1227+
}
1228+
1229+
EC_GROUP_set_asn1_flag(ecgroup.get(), OPENSSL_EC_NAMED_CURVE);
1230+
1231+
std::unique_ptr<EC_POINT, decltype(&EC_POINT_free)> ecpoint(EC_POINT_new(ecgroup.get()), EC_POINT_free);
1232+
if (!ecpoint ||
1233+
EC_POINT_set_affine_coordinates_GFp(ecgroup.get(), ecpoint.get(), qx.get(), qy.get(), nullptr) != 1) {
1234+
ec = error::ecdsa_error::set_ecdsa_failed;
1235+
return {};
1236+
}
1237+
1238+
std::unique_ptr<EC_KEY, decltype(&EC_KEY_free)> ecdsa(EC_KEY_new(), EC_KEY_free);
1239+
if (!ecdsa || EC_KEY_set_group(ecdsa.get(), ecgroup.get()) != 1 ||
1240+
EC_KEY_set_public_key(ecdsa.get(), ecpoint.get()) != 1) {
1241+
ec = error::ecdsa_error::set_ecdsa_failed;
1242+
return {};
1243+
}
1244+
1245+
#endif
1246+
1247+
auto pub_key_bio = make_mem_buf_bio();
1248+
if (!pub_key_bio) {
1249+
ec = error::ecdsa_error::create_mem_bio_failed;
1250+
return {};
1251+
}
1252+
1253+
auto write_pem_to_bio =
1254+
#if defined(JWT_OPENSSL_3_0)
1255+
// https://www.openssl.org/docs/man3.1/man3/PEM_write_bio_EC_PUBKEY.html
1256+
&PEM_write_bio_PUBKEY;
1257+
#else
1258+
&PEM_write_bio_EC_PUBKEY;
1259+
#endif
1260+
if (write_pem_to_bio(pub_key_bio.get(), ecdsa.get()) != 1) {
1261+
ec = error::ecdsa_error::load_key_bio_write;
1262+
return {};
1263+
}
1264+
1265+
return write_bio_to_string<error::ecdsa_error>(pub_key_bio, ec);
1266+
}
1267+
1268+
/**
1269+
* Create public key from curve name and coordinates. This is defined in
1270+
* [RFC 7518 Section 6.2](https://www.rfc-editor.org/rfc/rfc7518#section-6.2)
1271+
* Using the required "crv" (Curve), "x" (X Coordinate) and "y" (Y Coordinate) Parameters.
1272+
*
1273+
* \tparam Decode is callable, taking a string_type and returns a string_type.
1274+
* It should ensure the padding of the input and then base64url decode and
1275+
* return the results.
1276+
* \param curve string containing curve name
1277+
* \param x string containing base64url encoded x coordinate
1278+
* \param y string containing base64url encoded y coordinate
1279+
* \param decode The function to decode the RSA parameters
1280+
* \return public key in PEM format
1281+
*/
1282+
template<typename Decode>
1283+
std::string create_public_key_from_ec_components(const std::string& curve, const std::string& x,
1284+
const std::string& y, Decode decode) {
1285+
std::error_code ec;
1286+
auto res = create_public_key_from_ec_components(curve, x, y, decode, ec);
1287+
error::throw_if_error(ec);
1288+
return res;
1289+
}
1290+
1291+
#ifndef JWT_DISABLE_BASE64
1292+
/**
1293+
* Create public key from curve name and coordinates. This is defined in
1294+
* [RFC 7518 Section 6.2](https://www.rfc-editor.org/rfc/rfc7518#section-6.2)
1295+
* Using the required "crv" (Curve), "x" (X Coordinate) and "y" (Y Coordinate) Parameters.
1296+
*
1297+
* \param curve string containing curve name
1298+
* \param x string containing base64url encoded x coordinate
1299+
* \param y string containing base64url encoded y coordinate
1300+
* \param ec error_code for error_detection (gets cleared if no error occur
1301+
* \return public key in PEM format
1302+
*/
1303+
inline std::string create_public_key_from_ec_components(const std::string& curve, const std::string& x,
1304+
const std::string& y, std::error_code& ec) {
1305+
auto decode = [](const std::string& token) {
1306+
return base::decode<alphabet::base64url>(base::pad<alphabet::base64url>(token));
1307+
};
1308+
return create_public_key_from_ec_components(curve, x, y, std::move(decode), ec);
1309+
}
1310+
/**
1311+
* Create public key from curve name and coordinates. This is defined in
1312+
* [RFC 7518 Section 6.2](https://www.rfc-editor.org/rfc/rfc7518#section-6.2)
1313+
* Using the required "crv" (Curve), "x" (X Coordinate) and "y" (Y Coordinate) Parameters.
1314+
*
1315+
* \param curve string containing curve name
1316+
* \param x string containing base64url encoded x coordinate
1317+
* \param y string containing base64url encoded y coordinate
1318+
* \return public key in PEM format
1319+
*/
1320+
inline std::string create_public_key_from_ec_components(const std::string& curve, const std::string& x,
1321+
const std::string& y) {
1322+
std::error_code ec;
1323+
auto res = create_public_key_from_ec_components(curve, x, y, ec);
1324+
error::throw_if_error(ec);
1325+
return res;
1326+
}
1327+
#endif
10881328
} // namespace helper
10891329

10901330
/**

tests/HelperTest.cpp

+18-1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,23 @@ JQIDAQAB
4545
ASSERT_EQ(public_key, public_key_expected);
4646
}
4747

48+
TEST(HelperTest, EcFromComponents) {
49+
const std::string public_key_expected =
50+
R"(-----BEGIN PUBLIC KEY-----
51+
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE0uQ1+1P/wmhOuYvVtTogHOSBLC05IvK7
52+
L6sTPIX8Dl4Bg9nhC3v/FsgifjnXnijUxVJSyWa9SuxwBonUhg6SiCEv+ixb74hj
53+
DesC4D7OwllVcnkDJmOy/NMx4N7yDPJp
54+
-----END PUBLIC KEY-----
55+
)";
56+
const std::string curve = R"(P-384)";
57+
const std::string x = R"(0uQ1-1P_wmhOuYvVtTogHOSBLC05IvK7L6sTPIX8Dl4Bg9nhC3v_FsgifjnXnijU)";
58+
const std::string y = R"(xVJSyWa9SuxwBonUhg6SiCEv-ixb74hjDesC4D7OwllVcnkDJmOy_NMx4N7yDPJp)";
59+
60+
const auto public_key = jwt::helper::create_public_key_from_ec_components(curve, x, y);
61+
62+
ASSERT_EQ(public_key, public_key_expected);
63+
}
64+
4865
TEST(HelperTest, ErrorCodeMessages) {
4966
ASSERT_EQ(std::error_code(jwt::error::rsa_error::ok).message(), "no error");
5067
ASSERT_EQ(std::error_code(static_cast<jwt::error::rsa_error>(-1)).message(), "unknown RSA error");
@@ -80,7 +97,7 @@ TEST(HelperTest, ErrorCodeMessages) {
8097
ASSERT_EQ(std::error_code(static_cast<jwt::error::rsa_error>(i)).message(),
8198
std::error_code(static_cast<jwt::error::rsa_error>(-1)).message());
8299

83-
for (i = 10; i < 22; i++) {
100+
for (i = 10; i < 24; i++) {
84101
ASSERT_NE(std::error_code(static_cast<jwt::error::ecdsa_error>(i)).message(),
85102
std::error_code(static_cast<jwt::error::ecdsa_error>(-1)).message());
86103
}

0 commit comments

Comments
 (0)