28#ifdef PACS_WITH_DIGITAL_SIGNATURES
29#include <openssl/bio.h>
30#include <openssl/err.h>
31#include <openssl/evp.h>
32#include <openssl/pem.h>
44constexpr std::array<int, 256> make_decode_table() {
45 std::array<int, 256>
table{};
46 for (
auto& v : table) v = -1;
48 for (
int i = 0; i < 26; ++i) table[static_cast<unsigned char>(
'A' + i)] = i;
49 for (
int i = 0; i < 26; ++i) table[static_cast<unsigned char>(
'a' + i)] = 26 + i;
50 for (
int i = 0; i < 10; ++i) table[static_cast<unsigned char>(
'0' + i)] = 52 + i;
51 table[
static_cast<unsigned char>(
'-')] = 62;
52 table[
static_cast<unsigned char>(
'_')] = 63;
57constexpr auto kDecodeTable = make_decode_table();
60std::string json_string(
const crow::json::rvalue& json,
const char* key) {
61 if (!json.has(key))
return {};
63 if (v.t() == crow::json::type::String) {
64 return std::string(v.s());
70std::int64_t json_int64(
const crow::json::rvalue& json,
const char* key) {
71 if (!json.has(key))
return 0;
73 if (v.t() == crow::json::type::Number) {
80std::int64_t current_timestamp() {
81 return std::chrono::duration_cast<std::chrono::seconds>(
82 std::chrono::system_clock::now().time_since_epoch())
87std::vector<std::string> parse_scopes(
const std::string& scope_string) {
88 std::vector<std::string> scopes;
90 for (
char c : scope_string) {
92 if (!current.empty()) {
93 scopes.push_back(std::move(current));
100 if (!current.empty()) {
101 scopes.push_back(std::move(current));
109 if (input.empty())
return std::string{};
112 size_t padding = (4 - (input.size() % 4)) % 4;
113 size_t total_len = input.size() + padding;
114 size_t output_len = (total_len / 4) * 3;
117 result.reserve(output_len);
120 int bits_collected = 0;
122 for (
char c : input) {
123 if (c ==
'=')
continue;
125 int val = kDecodeTable[
static_cast<unsigned char>(c)];
126 if (val < 0)
return std::nullopt;
128 buffer = (buffer << 6) | static_cast<uint32_t>(val);
131 if (bits_collected >= 8) {
133 result +=
static_cast<char>((buffer >> bits_collected) & 0xFF);
149 return "malformed JWT token: expected 3 dot-separated segments";
151 return "invalid Base64url encoding in JWT segment";
153 return "invalid JSON in JWT segment";
155 return "unsupported JWT signing algorithm";
157 return "JWT signature verification failed";
159 return "JWT token has expired";
161 return "JWT token is not yet valid";
163 return "JWT issuer does not match expected value";
165 return "JWT audience does not match expected value";
167 return "required JWT claim is missing";
169 return "signature verification not available (OpenSSL required)";
171 return "unknown JWT error";
182 std::string_view token_string)
const {
187 auto dot1 = token_string.find(
'.');
188 if (dot1 == std::string_view::npos) {
192 auto dot2 = token_string.find(
'.', dot1 + 1);
193 if (dot2 == std::string_view::npos) {
198 if (token_string.find(
'.', dot2 + 1) != std::string_view::npos) {
202 auto header_b64 = token_string.substr(0, dot1);
203 auto payload_b64 = token_string.substr(dot1 + 1, dot2 - dot1 - 1);
204 auto signature_b64 = token_string.substr(dot2 + 1);
229 auto header_rv = crow::json::load(*header_json);
234 token.
header.
alg = json_string(header_rv,
"alg");
235 token.
header.
typ = json_string(header_rv,
"typ");
236 token.
header.
kid = json_string(header_rv,
"kid");
243 static const std::set<std::string> kDangerousAlgorithms = {
244 "none",
"None",
"NONE",
"HS256",
"HS384",
"HS512"};
245 if (kDangerousAlgorithms.count(token.
header.
alg)) {
251 if (!allowed.empty()) {
252 bool alg_allowed = std::any_of(
253 allowed.begin(), allowed.end(),
254 [&](
const std::string& a) { return a == token.header.alg; });
261 auto payload_rv = crow::json::load(*payload_json);
266 token.
claims.
iss = json_string(payload_rv,
"iss");
267 token.
claims.
sub = json_string(payload_rv,
"sub");
268 token.
claims.
aud = json_string(payload_rv,
"aud");
269 token.
claims.
exp = json_int64(payload_rv,
"exp");
270 token.
claims.
iat = json_int64(payload_rv,
"iat");
271 token.
claims.
nbf = json_int64(payload_rv,
"nbf");
272 token.
claims.
jti = json_string(payload_rv,
"jti");
275 auto scope_str = json_string(payload_rv,
"scope");
276 if (!scope_str.empty()) {
284 auto now = current_timestamp();
288 if (claims.
exp > 0 && now > claims.
exp + skew) {
293 if (claims.
nbf > 0 && now < claims.
nbf - skew) {
308 if (claims.
sub.empty()) {
319#ifdef PACS_WITH_DIGITAL_SIGNATURES
325 void operator()(BIO* bio)
const { BIO_free_all(bio); }
327using bio_ptr = std::unique_ptr<BIO, bio_deleter>;
331 void operator()(EVP_PKEY* key)
const { EVP_PKEY_free(key); }
333using pkey_ptr = std::unique_ptr<EVP_PKEY, pkey_deleter>;
336struct md_ctx_deleter {
337 void operator()(EVP_MD_CTX* ctx)
const { EVP_MD_CTX_free(ctx); }
339using md_ctx_ptr = std::unique_ptr<EVP_MD_CTX, md_ctx_deleter>;
342pkey_ptr load_public_key(std::string_view pem) {
343 bio_ptr bio(BIO_new_mem_buf(pem.data(),
static_cast<int>(pem.size())));
344 if (!bio)
return nullptr;
346 EVP_PKEY* key = PEM_read_bio_PUBKEY(bio.get(),
nullptr,
nullptr,
nullptr);
347 return pkey_ptr(key);
351bool verify_evp_signature(
354 const std::string& signed_data,
355 const std::string& signature) {
357 md_ctx_ptr ctx(EVP_MD_CTX_new());
358 if (!ctx)
return false;
360 if (EVP_DigestVerifyInit(ctx.get(),
nullptr, md,
nullptr, key) != 1) {
364 if (EVP_DigestVerifyUpdate(ctx.get(), signed_data.data(),
365 signed_data.size()) != 1) {
369 int rc = EVP_DigestVerifyFinal(
371 reinterpret_cast<const unsigned char*
>(signature.data()),
380std::string es256_raw_to_der(
const std::string& raw_sig) {
381 if (raw_sig.size() != 64)
return {};
383 auto encode_integer = [](
const unsigned char* data,
size_t len,
387 while (start < len - 1 && data[start] == 0) ++start;
390 bool need_pad = (data[start] & 0x80) != 0;
392 out +=
static_cast<char>(0x02);
393 out +=
static_cast<char>(len - start + (need_pad ? 1 : 0));
394 if (need_pad) out +=
'\0';
395 out.append(
reinterpret_cast<const char*
>(data + start), len - start);
398 const auto* sig_data =
reinterpret_cast<const unsigned char*
>(raw_sig.data());
400 std::string r_encoded, s_encoded;
401 encode_integer(sig_data, 32, r_encoded);
402 encode_integer(sig_data + 32, 32, s_encoded);
405 der +=
static_cast<char>(0x30);
406 der +=
static_cast<char>(r_encoded.size() + s_encoded.size());
416 const jwt_token& token,
417 std::string_view public_key_pem)
const {
419 auto key = load_public_key(public_key_pem);
420 if (!key)
return false;
422 return verify_evp_signature(
423 key.get(), EVP_sha256(),
424 token.header_payload, token.signature_bytes);
428 const jwt_token& token,
429 std::string_view public_key_pem)
const {
431 auto key = load_public_key(public_key_pem);
432 if (!key)
return false;
435 auto der_sig = es256_raw_to_der(token.signature_bytes);
436 if (der_sig.empty())
return false;
438 return verify_evp_signature(
439 key.get(), EVP_sha256(),
440 token.header_payload, der_sig);
447 std::string_view )
const {
453 std::string_view )
const {
465 std::string_view scope)
noexcept {
467 return std::any_of(claims.scopes.begin(), claims.scopes.end(),
468 [scope](
const std::string& s) { return s == scope; });
473 const std::vector<std::string>& scopes)
noexcept {
475 return std::any_of(scopes.begin(), scopes.end(),
476 [&claims](
const std::string& scope) {
477 return has_scope(claims, scope);
jwt_validator(const oauth2_config &config)
Construct validator with OAuth 2.0 configuration.
bool verify_es256(const jwt_token &token, std::string_view public_key_pem) const
Verify ES256 (ECDSA-SHA256) signature.
static bool has_any_scope(const jwt_claims &claims, const std::vector< std::string > &scopes) noexcept
Check if token has any of the specified scopes.
jwt_error validate_claims(const jwt_claims &claims) const
Validate JWT claims against configuration.
static bool has_scope(const jwt_claims &claims, std::string_view scope) noexcept
Check if token has a specific scope.
const oauth2_config & config() const noexcept
Get the OAuth 2.0 configuration.
bool verify_rs256(const jwt_token &token, std::string_view public_key_pem) const
Verify RS256 (RSA-SHA256) signature.
std::pair< jwt_token, jwt_error > decode(std::string_view token_string) const
Decode a JWT token string into its components.
JWT (JSON Web Token) validation for OAuth 2.0.
jwt_error
JWT validation error codes.
@ invalid_base64
Base64url decoding failed.
@ invalid_signature
Signature verification failed.
@ invalid_audience
Audience doesn't match expected value.
@ token_expired
Token has expired (exp claim)
@ invalid_issuer
Issuer doesn't match expected value.
@ malformed_token
Token doesn't have 3 dot-separated parts.
@ token_not_yet_valid
Token not yet valid (nbf claim)
@ signature_not_available
OpenSSL not available for verification.
@ unsupported_algorithm
Algorithm not in allowed list.
@ missing_required_claim
Required claim is missing.
@ invalid_json
JSON parsing failed.
std::string_view jwt_error_message(jwt_error error) noexcept
Get human-readable description for a JWT error.
std::optional< std::string > base64url_decode(std::string_view input)
Decode a Base64url-encoded string (RFC 4648 Section 5)
Decoded JWT claims (payload)
std::string sub
Subject (user identifier)
std::int64_t exp
Expiration time (Unix timestamp)
std::int64_t nbf
Not before (Unix timestamp)
std::vector< std::string > scopes
OAuth 2.0 scopes (from "scope" claim)
std::int64_t iat
Issued at (Unix timestamp)
Decoded JWT token with raw segments for signature verification.
std::string signature_bytes
Decoded signature bytes.
std::string header_payload
"header.payload" for signature input
OAuth 2.0 configuration for DICOMweb authorization.
std::string audience
Expected audience (aud claim). Empty = skip audience validation.
std::uint32_t clock_skew_seconds
Allowed clock skew in seconds for exp/nbf validation.
std::string issuer
Expected token issuer (iss claim). Empty = skip issuer validation.
std::vector< std::string > allowed_algorithms
Allowed signing algorithms (default: RS256, ES256)