PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
jwks_provider.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
16
17#include <crow/json.h>
18
19#include <algorithm>
20#include <cstddef>
21#include <cstring>
22
23// OpenSSL for JWK-to-PEM conversion (conditional)
24#ifdef PACS_WITH_DIGITAL_SIGNATURES
25#include <openssl/bio.h>
26#include <openssl/bn.h>
27#include <openssl/ec.h>
28#include <openssl/evp.h>
29#include <openssl/pem.h>
30#include <openssl/rsa.h>
31#endif
32
34
35// =============================================================================
36// JWK to PEM Conversion (OpenSSL)
37// =============================================================================
38
39#ifdef PACS_WITH_DIGITAL_SIGNATURES
40
41namespace {
42
44struct bn_deleter {
45 void operator()(BIGNUM* bn) const { BN_free(bn); }
46};
47using bn_ptr = std::unique_ptr<BIGNUM, bn_deleter>;
48
50struct pkey_deleter {
51 void operator()(EVP_PKEY* key) const { EVP_PKEY_free(key); }
52};
53using pkey_ptr = std::unique_ptr<EVP_PKEY, pkey_deleter>;
54
56struct bio_deleter {
57 void operator()(BIO* bio) const { BIO_free_all(bio); }
58};
59using bio_ptr = std::unique_ptr<BIO, bio_deleter>;
60
62struct pkey_ctx_deleter {
63 void operator()(EVP_PKEY_CTX* ctx) const { EVP_PKEY_CTX_free(ctx); }
64};
65using pkey_ctx_ptr = std::unique_ptr<EVP_PKEY_CTX, pkey_ctx_deleter>;
66
68bn_ptr bytes_to_bn(const std::string& bytes) {
69 return bn_ptr(BN_bin2bn(
70 reinterpret_cast<const unsigned char*>(bytes.data()),
71 static_cast<int>(bytes.size()), nullptr));
72}
73
75std::string pkey_to_pem(EVP_PKEY* key) {
76 bio_ptr bio(BIO_new(BIO_s_mem()));
77 if (!bio) return {};
78
79 if (PEM_write_bio_PUBKEY(bio.get(), key) != 1) return {};
80
81 char* data = nullptr;
82 long len = BIO_get_mem_data(bio.get(), &data);
83 if (len <= 0 || !data) return {};
84
85 return std::string(data, static_cast<size_t>(len));
86}
87
89std::string rsa_jwk_to_pem(const std::string& n_b64, const std::string& e_b64) {
90 auto n_bytes = base64url_decode(n_b64);
91 auto e_bytes = base64url_decode(e_b64);
92 if (!n_bytes || !e_bytes) return {};
93
94 auto n = bytes_to_bn(*n_bytes);
95 auto e = bytes_to_bn(*e_bytes);
96 if (!n || !e) return {};
97
98 // Create RSA key using EVP_PKEY_fromdata (OpenSSL 3.0+)
99 pkey_ctx_ptr ctx(EVP_PKEY_CTX_new_from_name(nullptr, "RSA", nullptr));
100 if (!ctx) return {};
101
102 if (EVP_PKEY_fromdata_init(ctx.get()) != 1) return {};
103
104 OSSL_PARAM params[3];
105 params[0] = OSSL_PARAM_construct_BN("n",
106 const_cast<unsigned char*>(reinterpret_cast<const unsigned char*>(n_bytes->data())),
107 n_bytes->size());
108 params[1] = OSSL_PARAM_construct_BN("e",
109 const_cast<unsigned char*>(reinterpret_cast<const unsigned char*>(e_bytes->data())),
110 e_bytes->size());
111 params[2] = OSSL_PARAM_construct_end();
112
113 EVP_PKEY* pkey = nullptr;
114 if (EVP_PKEY_fromdata(ctx.get(), &pkey, EVP_PKEY_PUBLIC_KEY, params) != 1) {
115 return {};
116 }
117 pkey_ptr key(pkey);
118
119 return pkey_to_pem(key.get());
120}
121
123std::string ec_jwk_to_pem(const std::string& x_b64, const std::string& y_b64,
124 const std::string& crv) {
125 auto x_bytes = base64url_decode(x_b64);
126 auto y_bytes = base64url_decode(y_b64);
127 if (!x_bytes || !y_bytes) return {};
128
129 // Only P-256 (ES256) supported
130 if (crv != "P-256") return {};
131
132 // Build uncompressed point: 0x04 || x || y
133 std::string point;
134 point.reserve(1 + x_bytes->size() + y_bytes->size());
135 point += '\x04';
136 point += *x_bytes;
137 point += *y_bytes;
138
139 // Create EC key using EVP_PKEY_fromdata (OpenSSL 3.0+)
140 pkey_ctx_ptr ctx(EVP_PKEY_CTX_new_from_name(nullptr, "EC", nullptr));
141 if (!ctx) return {};
142
143 if (EVP_PKEY_fromdata_init(ctx.get()) != 1) return {};
144
145 const char* group_name = "P-256";
146 OSSL_PARAM params[3];
147 params[0] = OSSL_PARAM_construct_utf8_string("group",
148 const_cast<char*>(group_name), 0);
149 params[1] = OSSL_PARAM_construct_octet_string("pub",
150 const_cast<char*>(point.data()), point.size());
151 params[2] = OSSL_PARAM_construct_end();
152
153 EVP_PKEY* pkey = nullptr;
154 if (EVP_PKEY_fromdata(ctx.get(), &pkey, EVP_PKEY_PUBLIC_KEY, params) != 1) {
155 return {};
156 }
157 pkey_ptr key(pkey);
158
159 return pkey_to_pem(key.get());
160}
161
163std::optional<jwk_key> parse_jwk(const crow::json::rvalue& jwk) {
164 jwk_key key;
165
166 if (jwk.has("kid") && jwk["kid"].t() == crow::json::type::String) {
167 key.kid = std::string(jwk["kid"].s());
168 }
169 if (jwk.has("kty") && jwk["kty"].t() == crow::json::type::String) {
170 key.kty = std::string(jwk["kty"].s());
171 }
172 if (jwk.has("alg") && jwk["alg"].t() == crow::json::type::String) {
173 key.alg = std::string(jwk["alg"].s());
174 }
175 if (jwk.has("use") && jwk["use"].t() == crow::json::type::String) {
176 key.use = std::string(jwk["use"].s());
177 }
178
179 // Skip non-signature keys
180 if (!key.use.empty() && key.use != "sig") return std::nullopt;
181
182 if (key.kty == "RSA") {
183 if (!jwk.has("n") || !jwk.has("e")) return std::nullopt;
184 auto n = std::string(jwk["n"].s());
185 auto e = std::string(jwk["e"].s());
186 key.pem = rsa_jwk_to_pem(n, e);
187 if (key.pem.empty()) return std::nullopt;
188
189 // Infer algorithm if not specified
190 if (key.alg.empty()) key.alg = "RS256";
191 } else if (key.kty == "EC") {
192 if (!jwk.has("x") || !jwk.has("y")) return std::nullopt;
193 auto x = std::string(jwk["x"].s());
194 auto y = std::string(jwk["y"].s());
195 auto crv = jwk.has("crv") ? std::string(jwk["crv"].s()) : std::string("P-256");
196 key.pem = ec_jwk_to_pem(x, y, crv);
197 if (key.pem.empty()) return std::nullopt;
198
199 if (key.alg.empty()) key.alg = "ES256";
200 } else {
201 return std::nullopt; // Unsupported key type
202 }
203
204 return key;
205}
206
207} // namespace
208
209#else // !PACS_WITH_DIGITAL_SIGNATURES
210
211namespace {
212
213std::optional<jwk_key> parse_jwk(const crow::json::rvalue& /*jwk*/) {
214 return std::nullopt; // Cannot convert without OpenSSL
215}
216
217} // namespace
218
219#endif // PACS_WITH_DIGITAL_SIGNATURES
220
221// =============================================================================
222// jwks_provider Implementation
223// =============================================================================
224
226
227bool jwks_provider::load_from_json(std::string_view jwks_json) {
228 auto json = crow::json::load(std::string(jwks_json));
229 if (!json || !json.has("keys")) return false;
230
231 auto& keys_arr = json["keys"];
232 if (keys_arr.t() != crow::json::type::List) return false;
233
234 std::vector<jwk_key> new_keys;
235 for (size_t i = 0; i < keys_arr.size(); ++i) {
236 auto key = parse_jwk(keys_arr[i]);
237 if (key) {
238 new_keys.push_back(std::move(*key));
239 }
240 }
241
242 if (new_keys.empty()) return false;
243
244 std::lock_guard<std::mutex> lock(mutex_);
245 keys_ = std::move(new_keys);
246 last_fetch_ = std::chrono::steady_clock::now();
247 return true;
248}
249
251 std::lock_guard<std::mutex> lock(mutex_);
252 fetcher_ = std::move(fetcher);
253}
254
255void jwks_provider::set_jwks_url(const std::string& url) {
256 std::lock_guard<std::mutex> lock(mutex_);
257 jwks_url_ = url;
258}
259
260std::optional<jwk_key> jwks_provider::get_key(std::string_view kid) const {
261 std::lock_guard<std::mutex> lock(mutex_);
262
263 // Try refresh if cache expired and fetcher available
264 if (is_cache_expired() && fetcher_ && !jwks_url_.empty()) {
265 try_refresh();
266 }
267
268 for (const auto& key : keys_) {
269 if (key.kid == kid) return key;
270 }
271 return std::nullopt;
272}
273
274std::optional<jwk_key> jwks_provider::get_key_by_alg(
275 std::string_view alg) const {
276 std::lock_guard<std::mutex> lock(mutex_);
277
278 if (is_cache_expired() && fetcher_ && !jwks_url_.empty()) {
279 try_refresh();
280 }
281
282 for (const auto& key : keys_) {
283 if (key.alg == alg) return key;
284 }
285 return std::nullopt;
286}
287
289 std::lock_guard<std::mutex> lock(mutex_);
290 return try_refresh();
291}
292
293const std::vector<jwk_key>& jwks_provider::keys() const {
294 return keys_;
295}
296
297void jwks_provider::set_cache_ttl(std::uint32_t seconds) {
298 cache_ttl_seconds_ = seconds;
299}
300
302 if (last_fetch_ == std::chrono::steady_clock::time_point{}) return true;
303
304 auto elapsed = std::chrono::steady_clock::now() - last_fetch_;
305 return elapsed > std::chrono::seconds(cache_ttl_seconds_);
306}
307
309 if (!fetcher_ || jwks_url_.empty()) return false;
310
311 auto json_body = fetcher_(jwks_url_);
312 if (!json_body) return false;
313
314 auto json = crow::json::load(*json_body);
315 if (!json || !json.has("keys")) return false;
316
317 auto& keys_arr = json["keys"];
318 if (keys_arr.t() != crow::json::type::List) return false;
319
320 std::vector<jwk_key> new_keys;
321 for (size_t i = 0; i < keys_arr.size(); ++i) {
322 auto key = parse_jwk(keys_arr[i]);
323 if (key) {
324 new_keys.push_back(std::move(*key));
325 }
326 }
327
328 if (new_keys.empty()) return false;
329
330 keys_ = std::move(new_keys);
331 last_fetch_ = std::chrono::steady_clock::now();
332 return true;
333}
334
335} // namespace kcenon::pacs::web::auth
bool is_cache_expired() const
Check if the cache has expired.
std::chrono::steady_clock::time_point last_fetch_
bool load_from_json(std::string_view jwks_json)
Load keys from a JWKS JSON string.
const std::vector< jwk_key > & keys() const
Get all currently loaded keys.
std::optional< jwk_key > get_key(std::string_view kid) const
Get a key by Key ID (kid)
void set_jwks_url(const std::string &url)
Set the JWKS endpoint URL for fetching.
void set_fetcher(jwks_fetch_callback fetcher)
Set a callback for fetching JWKS from a URL.
bool refresh()
Force refresh keys from the configured JWKS URL.
void set_cache_ttl(std::uint32_t seconds)
Set cache TTL in seconds (default: 3600)
std::optional< jwk_key > get_key_by_alg(std::string_view alg) const
Get the first key matching the given algorithm.
JSON Web Key Set (JWKS) provider with key caching.
JWT (JSON Web Token) validation for OAuth 2.0.
std::function< std::optional< std::string >(const std::string &url)> jwks_fetch_callback
Callback type for fetching JWKS JSON from a URL.
std::optional< std::string > base64url_decode(std::string_view input)
Decode a Base64url-encoded string (RFC 4648 Section 5)