cxxmcp 1.1.6
C++ MCP SDK
Loading...
Searching...
No Matches
jwk.hpp
Go to the documentation of this file.
1// Copyright (c) 2025 [caomengxuan666]
2
3#pragma once
4
5#include <openssl/bn.h>
6#include <openssl/core_names.h>
7#include <openssl/evp.h>
8#include <openssl/obj_mac.h>
9#include <openssl/param_build.h>
10
11#include <memory>
12#include <optional>
13#include <string>
14#include <string_view>
15#include <utility>
16#include <vector>
17
18#include "cxxmcp/auth/jwks.hpp"
21
24
25namespace mcp::auth::openssl {
26
27namespace detail {
28
30 void operator()(BIGNUM* value) const noexcept { BN_free(value); }
31};
32
34 void operator()(EVP_PKEY* value) const noexcept { EVP_PKEY_free(value); }
35};
36
38 void operator()(EVP_PKEY_CTX* value) const noexcept {
39 EVP_PKEY_CTX_free(value);
40 }
41};
42
44 void operator()(OSSL_PARAM_BLD* value) const noexcept {
45 OSSL_PARAM_BLD_free(value);
46 }
47};
48
50 void operator()(OSSL_PARAM* value) const noexcept { OSSL_PARAM_free(value); }
51};
52
53using BignumPtr = std::unique_ptr<BIGNUM, BignumDeleter>;
54using EvpPkeyPtr = std::unique_ptr<EVP_PKEY, EvpPkeyDeleter>;
55using EvpPkeyCtxPtr = std::unique_ptr<EVP_PKEY_CTX, EvpPkeyCtxDeleter>;
56using OssParamBldPtr = std::unique_ptr<OSSL_PARAM_BLD, OssParamBldDeleter>;
57using OssParamPtr = std::unique_ptr<OSSL_PARAM, OssParamDeleter>;
58
59inline core::Result<BignumPtr> jwk_bignum_from_base64url(
60 const std::optional<std::string>& value, const char* name) {
61 if (!value.has_value() || value->empty()) {
62 return core::unexpected(
63 make_jose_error(JoseErrorCode::kInvalidJwk,
64 std::string("JWK ") + name + " is required"));
65 }
66
67 auto bytes = base64url_decode(*value);
68 if (!bytes.has_value()) {
69 return core::unexpected(bytes.error());
70 }
71 if (bytes->empty()) {
72 return core::unexpected(make_jose_error(
73 JoseErrorCode::kInvalidJwk, std::string("JWK ") + name + " is empty"));
74 }
75
76 BignumPtr bignum(
77 BN_bin2bn(bytes->data(), static_cast<int>(bytes->size()), nullptr));
78 if (!bignum) {
79 return core::unexpected(
80 make_jose_error(JoseErrorCode::kInvalidJwk,
81 std::string("failed to decode JWK ") + name));
82 }
83 return bignum;
84}
85
86inline core::Result<std::vector<unsigned char>> jwk_bytes_from_base64url(
87 const std::optional<std::string>& value, const char* name,
88 std::size_t expected_size) {
89 if (!value.has_value() || value->empty()) {
90 return core::unexpected(
91 make_jose_error(JoseErrorCode::kInvalidJwk,
92 std::string("JWK ") + name + " is required"));
93 }
94 auto bytes = base64url_decode(*value);
95 if (!bytes.has_value()) {
96 return core::unexpected(bytes.error());
97 }
98 if (bytes->size() != expected_size) {
99 return core::unexpected(
100 make_jose_error(JoseErrorCode::kInvalidJwk,
101 std::string("JWK ") + name + " has an invalid size"));
102 }
103 return bytes;
104}
105
106inline core::Result<BignumPtr> evp_pkey_bignum_param(EVP_PKEY* key,
107 const char* name) {
108 BIGNUM* raw = nullptr;
109 if (key == nullptr || EVP_PKEY_get_bn_param(key, name, &raw) != 1) {
110 return core::unexpected(
111 make_jose_error(JoseErrorCode::kInvalidJwk,
112 std::string("missing OpenSSL key parameter ") + name));
113 }
114 return BignumPtr(raw);
115}
116
117inline std::vector<unsigned char> bignum_to_bytes(const BIGNUM* value) {
118 std::vector<unsigned char> bytes(
119 static_cast<std::size_t>(BN_num_bytes(value)));
120 BN_bn2bin(value, bytes.data());
121 return bytes;
122}
123
124inline core::Result<std::vector<unsigned char>> bignum_to_fixed_bytes(
125 const BIGNUM* value, std::size_t size) {
126 std::vector<unsigned char> bytes(size);
127 if (BN_bn2binpad(value, bytes.data(), static_cast<int>(bytes.size())) !=
128 static_cast<int>(bytes.size())) {
129 return core::unexpected(make_jose_error(
130 JoseErrorCode::kInvalidJwk, "failed to serialize fixed-size BIGNUM"));
131 }
132 return bytes;
133}
134
135inline const char* ec_group_name_for_jose_algorithm(std::string_view algorithm,
136 std::string_view curve) {
137 if (algorithm == "ES256" && curve == "P-256") {
138 return SN_X9_62_prime256v1;
139 }
140 if (algorithm == "ES384" && curve == "P-384") {
141 return SN_secp384r1;
142 }
143 if (algorithm == "ES512" && curve == "P-521") {
144 return SN_secp521r1;
145 }
146 return nullptr;
147}
148
149inline std::size_t ec_coordinate_size_for_jose_algorithm(
150 std::string_view algorithm) {
151 if (algorithm == "ES256") {
152 return 32;
153 }
154 if (algorithm == "ES384") {
155 return 48;
156 }
157 if (algorithm == "ES512") {
158 return 66;
159 }
160 return 0;
161}
162
163inline core::Result<EvpPkeyPtr> evp_pkey_from_params(const char* key_type,
164 OSSL_PARAM* params) {
165 EvpPkeyCtxPtr context(EVP_PKEY_CTX_new_from_name(nullptr, key_type, nullptr));
166 if (!context || EVP_PKEY_fromdata_init(context.get()) <= 0) {
167 return core::unexpected(make_jose_error(
168 JoseErrorCode::kInvalidJwk, "failed to initialize OpenSSL key import"));
169 }
170
171 EVP_PKEY* raw_key = nullptr;
172 if (EVP_PKEY_fromdata(context.get(), &raw_key, EVP_PKEY_PUBLIC_KEY, params) <=
173 0) {
174 return core::unexpected(make_jose_error(JoseErrorCode::kInvalidJwk,
175 "failed to import JWK public key"));
176 }
177 return EvpPkeyPtr(raw_key);
178}
179
180} // namespace detail
181
182using EvpPkeyPtr = detail::EvpPkeyPtr;
183
185 EvpPkeyPtr key;
186 std::string algorithm;
187 std::optional<std::string> key_id;
188};
189
190inline core::Result<OpenSslPublicKey> rsa_public_key_from_jwk(
191 const JsonWebKey& jwk, std::string algorithm) {
192 auto modulus = detail::jwk_bignum_from_base64url(jwk.modulus, "n");
193 if (!modulus.has_value()) {
194 return core::unexpected(modulus.error());
195 }
196 auto exponent = detail::jwk_bignum_from_base64url(jwk.exponent, "e");
197 if (!exponent.has_value()) {
198 return core::unexpected(exponent.error());
199 }
200
201 detail::OssParamBldPtr builder(OSSL_PARAM_BLD_new());
202 if (!builder ||
203 OSSL_PARAM_BLD_push_BN(builder.get(), OSSL_PKEY_PARAM_RSA_N,
204 modulus->get()) <= 0 ||
205 OSSL_PARAM_BLD_push_BN(builder.get(), OSSL_PKEY_PARAM_RSA_E,
206 exponent->get()) <= 0) {
207 return core::unexpected(make_jose_error(
208 JoseErrorCode::kInvalidJwk, "failed to build RSA JWK parameters"));
209 }
210
211 detail::OssParamPtr params(OSSL_PARAM_BLD_to_param(builder.get()));
212 if (!params) {
213 return core::unexpected(make_jose_error(
214 JoseErrorCode::kInvalidJwk, "failed to finalize RSA JWK parameters"));
215 }
216
217 auto key = detail::evp_pkey_from_params("RSA", params.get());
218 if (!key.has_value()) {
219 return core::unexpected(key.error());
220 }
221
222 OpenSslPublicKey result;
223 result.key = std::move(*key);
224 result.algorithm = std::move(algorithm);
225 result.key_id = jwk.key_id;
226 return result;
227}
228
229inline core::Result<OpenSslPublicKey> ec_public_key_from_jwk(
230 const JsonWebKey& jwk, std::string algorithm) {
231 const std::string curve = jwk.curve.value_or("");
232 const char* group_name =
233 detail::ec_group_name_for_jose_algorithm(algorithm, curve);
234 const std::size_t coordinate_size =
235 detail::ec_coordinate_size_for_jose_algorithm(algorithm);
236 if (group_name == nullptr || coordinate_size == 0) {
237 return core::unexpected(
238 make_jose_error(JoseErrorCode::kUnsupportedJoseAlgorithm,
239 "unsupported EC JWK algorithm or curve"));
240 }
241
242 auto x = detail::jwk_bytes_from_base64url(jwk.x, "x", coordinate_size);
243 if (!x.has_value()) {
244 return core::unexpected(x.error());
245 }
246 auto y = detail::jwk_bytes_from_base64url(jwk.y, "y", coordinate_size);
247 if (!y.has_value()) {
248 return core::unexpected(y.error());
249 }
250
251 std::vector<unsigned char> public_key;
252 public_key.reserve(1 + x->size() + y->size());
253 public_key.push_back(0x04);
254 public_key.insert(public_key.end(), x->begin(), x->end());
255 public_key.insert(public_key.end(), y->begin(), y->end());
256
257 detail::OssParamBldPtr builder(OSSL_PARAM_BLD_new());
258 if (!builder ||
259 OSSL_PARAM_BLD_push_utf8_string(builder.get(), OSSL_PKEY_PARAM_GROUP_NAME,
260 const_cast<char*>(group_name), 0) <= 0 ||
261 OSSL_PARAM_BLD_push_octet_string(builder.get(), OSSL_PKEY_PARAM_PUB_KEY,
262 public_key.data(),
263 public_key.size()) <= 0) {
264 return core::unexpected(make_jose_error(
265 JoseErrorCode::kInvalidJwk, "failed to build EC JWK parameters"));
266 }
267
268 detail::OssParamPtr params(OSSL_PARAM_BLD_to_param(builder.get()));
269 if (!params) {
270 return core::unexpected(make_jose_error(
271 JoseErrorCode::kInvalidJwk, "failed to finalize EC JWK parameters"));
272 }
273
274 auto key = detail::evp_pkey_from_params("EC", params.get());
275 if (!key.has_value()) {
276 return core::unexpected(key.error());
277 }
278
279 OpenSslPublicKey result;
280 result.key = std::move(*key);
281 result.algorithm = std::move(algorithm);
282 result.key_id = jwk.key_id;
283 return result;
284}
285
286inline core::Result<OpenSslPublicKey> public_key_from_jwk(
287 const JsonWebKey& jwk, std::optional<std::string> required_algorithm = {}) {
288 std::string algorithm = required_algorithm.has_value()
289 ? *required_algorithm
290 : jwk.algorithm.value_or("");
291 if (algorithm.empty()) {
292 return core::unexpected(
293 make_jose_error(JoseErrorCode::kInvalidJwk, "JWK alg is required"));
294 }
295 if (jwk.algorithm.has_value() && *jwk.algorithm != algorithm) {
296 return core::unexpected(make_jose_error(
297 JoseErrorCode::kInvalidJwk, "JWK alg does not match required alg"));
298 }
299
300 if (jwk.key_type == "RSA" && algorithm == "RS256") {
301 return rsa_public_key_from_jwk(jwk, std::move(algorithm));
302 }
303 if (jwk.key_type == "EC" &&
304 (algorithm == "ES256" || algorithm == "ES384" || algorithm == "ES512")) {
305 return ec_public_key_from_jwk(jwk, std::move(algorithm));
306 }
307
308 return core::unexpected(
309 make_jose_error(JoseErrorCode::kUnsupportedJoseAlgorithm,
310 "unsupported JWK key type or JOSE algorithm"));
311}
312
313inline core::Result<JsonWebKey> public_jwk_from_evp_pkey(
314 EVP_PKEY* key, std::string algorithm,
315 std::optional<std::string> key_id = {}) {
316 if (key == nullptr) {
317 return core::unexpected(make_jose_error(JoseErrorCode::kInvalidJwk,
318 "OpenSSL public key is required"));
319 }
320
321 JsonWebKey jwk;
322 jwk.public_key_use = "sig";
323 jwk.algorithm = algorithm;
324 jwk.key_id = std::move(key_id);
325
326 if (algorithm == "RS256") {
327 auto modulus = detail::evp_pkey_bignum_param(key, OSSL_PKEY_PARAM_RSA_N);
328 if (!modulus.has_value()) {
329 return core::unexpected(modulus.error());
330 }
331 auto exponent = detail::evp_pkey_bignum_param(key, OSSL_PKEY_PARAM_RSA_E);
332 if (!exponent.has_value()) {
333 return core::unexpected(exponent.error());
334 }
335
336 jwk.key_type = "RSA";
337 jwk.modulus = base64url_encode(detail::bignum_to_bytes(modulus->get()));
338 jwk.exponent = base64url_encode(detail::bignum_to_bytes(exponent->get()));
339 return jwk;
340 }
341
342 const std::size_t coordinate_size =
343 detail::ec_coordinate_size_for_jose_algorithm(algorithm);
344 if (coordinate_size == 0) {
345 return core::unexpected(
346 make_jose_error(JoseErrorCode::kUnsupportedJoseAlgorithm,
347 "unsupported JOSE algorithm for public JWK export"));
348 }
349 auto x = detail::evp_pkey_bignum_param(key, OSSL_PKEY_PARAM_EC_PUB_X);
350 if (!x.has_value()) {
351 return core::unexpected(x.error());
352 }
353 auto y = detail::evp_pkey_bignum_param(key, OSSL_PKEY_PARAM_EC_PUB_Y);
354 if (!y.has_value()) {
355 return core::unexpected(y.error());
356 }
357 auto x_bytes = detail::bignum_to_fixed_bytes(x->get(), coordinate_size);
358 if (!x_bytes.has_value()) {
359 return core::unexpected(x_bytes.error());
360 }
361 auto y_bytes = detail::bignum_to_fixed_bytes(y->get(), coordinate_size);
362 if (!y_bytes.has_value()) {
363 return core::unexpected(y_bytes.error());
364 }
365
366 jwk.key_type = "EC";
367 if (algorithm == "ES256") {
368 jwk.curve = "P-256";
369 } else if (algorithm == "ES384") {
370 jwk.curve = "P-384";
371 } else if (algorithm == "ES512") {
372 jwk.curve = "P-521";
373 }
374 jwk.x = base64url_encode(*x_bytes);
375 jwk.y = base64url_encode(*y_bytes);
376 return jwk;
377}
378
379} // namespace mcp::auth::openssl
JOSE base64url helpers shared by optional OpenSSL auth code.
JWKS value models, parsing, fetch, and cache contracts.
Shared result and error primitives used by the public cxxmcp SDK.
tl::expected< T, Error > Result
Alias for the SDK result type.
Definition result.hpp:64
Public JSON Web Key value model.
Definition jwks.hpp:24