PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
certificate.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
13
14#include <openssl/bio.h>
15#include <openssl/err.h>
16#include <openssl/evp.h>
17#include <openssl/pem.h>
18#include <openssl/x509.h>
19#include <openssl/x509v3.h>
20
21#include <fstream>
22#include <sstream>
23
24namespace kcenon::pacs::security {
25
26namespace {
27
34inline auto portable_timegm(struct tm* tm_time) -> time_t {
35#ifdef _WIN32
36 return _mkgmtime(tm_time);
37#else
38 return timegm(tm_time);
39#endif
40}
41
45auto get_openssl_error() -> std::string {
46 unsigned long err = ERR_get_error();
47 if (err == 0) {
48 return "Unknown error";
49 }
50 char buf[256];
51 ERR_error_string_n(err, buf, sizeof(buf));
52 return std::string(buf);
53}
54
58auto read_file(std::string_view path) -> kcenon::common::Result<std::string> {
59 std::ifstream file(std::string(path), std::ios::binary);
60 if (!file.is_open()) {
61 return kcenon::common::make_error<std::string>(
62 1, "Failed to open file: " + std::string(path), "certificate");
63 }
64
65 std::ostringstream ss;
66 ss << file.rdbuf();
67 return ss.str();
68}
69
73struct bio_deleter {
74 void operator()(BIO* bio) const {
75 if (bio) BIO_free(bio);
76 }
77};
78using bio_ptr = std::unique_ptr<BIO, bio_deleter>;
79
83auto asn1_time_to_time_point(const ASN1_TIME* asn1_time)
84 -> std::chrono::system_clock::time_point {
85 if (!asn1_time) {
86 return std::chrono::system_clock::time_point{};
87 }
88
89 struct tm tm_time = {};
90 int offset_sec = 0;
91
92 // Parse the ASN1 time
93 if (ASN1_TIME_to_tm(asn1_time, &tm_time) != 1) {
94 return std::chrono::system_clock::time_point{};
95 }
96
97 // Convert to time_t (using portable function for cross-platform support)
98 time_t time = portable_timegm(&tm_time);
99
100 return std::chrono::system_clock::from_time_t(time);
101}
102
103} // anonymous namespace
104
105// ============================================================================
106// certificate_impl - PIMPL implementation
107// ============================================================================
108
110public:
111 certificate_impl() : x509_(nullptr) {}
112
113 explicit certificate_impl(X509* cert) : x509_(cert) {}
114
116 if (x509_) {
117 X509_free(x509_);
118 }
119 }
120
121 // Non-copyable but movable
123 if (other.x509_) {
124 x509_ = X509_dup(other.x509_);
125 } else {
126 x509_ = nullptr;
127 }
128 }
129
131 : x509_(other.x509_) {
132 other.x509_ = nullptr;
133 }
134
136 if (this != &other) {
137 if (x509_) {
138 X509_free(x509_);
139 }
140 if (other.x509_) {
141 x509_ = X509_dup(other.x509_);
142 } else {
143 x509_ = nullptr;
144 }
145 }
146 return *this;
147 }
148
150 if (this != &other) {
151 if (x509_) {
152 X509_free(x509_);
153 }
154 x509_ = other.x509_;
155 other.x509_ = nullptr;
156 }
157 return *this;
158 }
159
160 [[nodiscard]] auto x509() const noexcept -> X509* { return x509_; }
161 [[nodiscard]] auto is_loaded() const noexcept -> bool { return x509_ != nullptr; }
162
163private:
164 X509* x509_;
165};
166
167// ============================================================================
168// private_key_impl - PIMPL implementation
169// ============================================================================
170
172public:
173 private_key_impl() : pkey_(nullptr) {}
174
175 explicit private_key_impl(EVP_PKEY* key) : pkey_(key) {}
176
178 if (pkey_) {
179 EVP_PKEY_free(pkey_);
180 }
181 }
182
183 // Non-copyable, movable
185 auto operator=(const private_key_impl&) -> private_key_impl& = delete;
186
188 : pkey_(other.pkey_) {
189 other.pkey_ = nullptr;
190 }
191
193 if (this != &other) {
194 if (pkey_) {
195 EVP_PKEY_free(pkey_);
196 }
197 pkey_ = other.pkey_;
198 other.pkey_ = nullptr;
199 }
200 return *this;
201 }
202
203 [[nodiscard]] auto pkey() const noexcept -> EVP_PKEY* { return pkey_; }
204 [[nodiscard]] auto is_loaded() const noexcept -> bool { return pkey_ != nullptr; }
205
206private:
207 EVP_PKEY* pkey_;
208};
209
210// ============================================================================
211// certificate implementation
212// ============================================================================
213
214certificate::certificate() : impl_(std::make_unique<certificate_impl>()) {}
215
217 : impl_(std::make_unique<certificate_impl>(*other.impl_)) {}
218
219certificate::certificate(certificate&& other) noexcept = default;
220
222 if (this != &other) {
223 impl_ = std::make_unique<certificate_impl>(*other.impl_);
224 }
225 return *this;
226}
227
228auto certificate::operator=(certificate&& other) noexcept -> certificate& = default;
229
230certificate::~certificate() = default;
231
232auto certificate::load_from_pem(std::string_view path)
234 auto content_result = read_file(path);
235 if (content_result.is_err()) {
236 return kcenon::common::make_error<certificate>(
237 content_result.error().code,
238 content_result.error().message,
239 "certificate");
240 }
241
242 return load_from_pem_string(content_result.value());
243}
244
245auto certificate::load_from_pem_string(std::string_view pem_data)
247 bio_ptr bio(BIO_new_mem_buf(pem_data.data(), static_cast<int>(pem_data.size())));
248 if (!bio) {
249 return kcenon::common::make_error<certificate>(
250 2, "Failed to create BIO: " + get_openssl_error(), "certificate");
251 }
252
253 X509* x509 = PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr);
254 if (!x509) {
255 return kcenon::common::make_error<certificate>(
256 3, "Failed to parse PEM certificate: " + get_openssl_error(), "certificate");
257 }
258
259 certificate cert;
260 cert.impl_ = std::make_unique<certificate_impl>(x509);
261 return cert;
262}
263
264auto certificate::load_from_der(std::span<const std::uint8_t> der_data)
266 const unsigned char* data = der_data.data();
267 X509* x509 = d2i_X509(nullptr, &data, static_cast<long>(der_data.size()));
268 if (!x509) {
269 return kcenon::common::make_error<certificate>(
270 4, "Failed to parse DER certificate: " + get_openssl_error(), "certificate");
271 }
272
273 certificate cert;
274 cert.impl_ = std::make_unique<certificate_impl>(x509);
275 return cert;
276}
277
278auto certificate::subject_name() const -> std::string {
279 if (!impl_->is_loaded()) {
280 return "";
281 }
282
283 X509_NAME* name = X509_get_subject_name(impl_->x509());
284 if (!name) {
285 return "";
286 }
287
288 bio_ptr bio(BIO_new(BIO_s_mem()));
289 X509_NAME_print_ex(bio.get(), name, 0, XN_FLAG_RFC2253);
290
291 char* data = nullptr;
292 long len = BIO_get_mem_data(bio.get(), &data);
293 return std::string(data, static_cast<size_t>(len));
294}
295
296auto certificate::subject_common_name() const -> std::string {
297 if (!impl_->is_loaded()) {
298 return "";
299 }
300
301 X509_NAME* name = X509_get_subject_name(impl_->x509());
302 if (!name) {
303 return "";
304 }
305
306 int idx = X509_NAME_get_index_by_NID(name, NID_commonName, -1);
307 if (idx < 0) {
308 return "";
309 }
310
311 X509_NAME_ENTRY* entry = X509_NAME_get_entry(name, idx);
312 if (!entry) {
313 return "";
314 }
315
316 ASN1_STRING* data = X509_NAME_ENTRY_get_data(entry);
317 if (!data) {
318 return "";
319 }
320
321 unsigned char* utf8 = nullptr;
322 int len = ASN1_STRING_to_UTF8(&utf8, data);
323 if (len < 0) {
324 return "";
325 }
326
327 std::string result(reinterpret_cast<char*>(utf8), static_cast<size_t>(len));
328 OPENSSL_free(utf8);
329 return result;
330}
331
332auto certificate::subject_organization() const -> std::string {
333 if (!impl_->is_loaded()) {
334 return "";
335 }
336
337 X509_NAME* name = X509_get_subject_name(impl_->x509());
338 if (!name) {
339 return "";
340 }
341
342 int idx = X509_NAME_get_index_by_NID(name, NID_organizationName, -1);
343 if (idx < 0) {
344 return "";
345 }
346
347 X509_NAME_ENTRY* entry = X509_NAME_get_entry(name, idx);
348 if (!entry) {
349 return "";
350 }
351
352 ASN1_STRING* data = X509_NAME_ENTRY_get_data(entry);
353 if (!data) {
354 return "";
355 }
356
357 unsigned char* utf8 = nullptr;
358 int len = ASN1_STRING_to_UTF8(&utf8, data);
359 if (len < 0) {
360 return "";
361 }
362
363 std::string result(reinterpret_cast<char*>(utf8), static_cast<size_t>(len));
364 OPENSSL_free(utf8);
365 return result;
366}
367
368auto certificate::issuer_name() const -> std::string {
369 if (!impl_->is_loaded()) {
370 return "";
371 }
372
373 X509_NAME* name = X509_get_issuer_name(impl_->x509());
374 if (!name) {
375 return "";
376 }
377
378 bio_ptr bio(BIO_new(BIO_s_mem()));
379 X509_NAME_print_ex(bio.get(), name, 0, XN_FLAG_RFC2253);
380
381 char* data = nullptr;
382 long len = BIO_get_mem_data(bio.get(), &data);
383 return std::string(data, static_cast<size_t>(len));
384}
385
386auto certificate::serial_number() const -> std::string {
387 if (!impl_->is_loaded()) {
388 return "";
389 }
390
391 const ASN1_INTEGER* serial = X509_get_serialNumber(impl_->x509());
392 if (!serial) {
393 return "";
394 }
395
396 BIGNUM* bn = ASN1_INTEGER_to_BN(serial, nullptr);
397 if (!bn) {
398 return "";
399 }
400
401 char* hex = BN_bn2hex(bn);
402 BN_free(bn);
403
404 if (!hex) {
405 return "";
406 }
407
408 std::string result(hex);
409 OPENSSL_free(hex);
410 return result;
411}
412
413auto certificate::thumbprint() const -> std::string {
414 if (!impl_->is_loaded()) {
415 return "";
416 }
417
418 unsigned char hash[EVP_MAX_MD_SIZE];
419 unsigned int hash_len = 0;
420
421 if (X509_digest(impl_->x509(), EVP_sha256(), hash, &hash_len) != 1) {
422 return "";
423 }
424
425 std::string result;
426 result.reserve(hash_len * 2);
427
428 static const char hex_chars[] = "0123456789ABCDEF";
429 for (unsigned int i = 0; i < hash_len; ++i) {
430 result += hex_chars[(hash[i] >> 4) & 0x0F];
431 result += hex_chars[hash[i] & 0x0F];
432 }
433
434 return result;
435}
436
437auto certificate::not_before() const -> std::chrono::system_clock::time_point {
438 if (!impl_->is_loaded()) {
439 return std::chrono::system_clock::time_point{};
440 }
441
442 const ASN1_TIME* time = X509_get0_notBefore(impl_->x509());
443 return asn1_time_to_time_point(time);
444}
445
446auto certificate::not_after() const -> std::chrono::system_clock::time_point {
447 if (!impl_->is_loaded()) {
448 return std::chrono::system_clock::time_point{};
449 }
450
451 const ASN1_TIME* time = X509_get0_notAfter(impl_->x509());
452 return asn1_time_to_time_point(time);
453}
454
455auto certificate::is_valid() const -> bool {
456 if (!impl_->is_loaded()) {
457 return false;
458 }
459
460 auto now = std::chrono::system_clock::now();
461 return now >= not_before() && now <= not_after();
462}
463
464auto certificate::is_expired() const -> bool {
465 if (!impl_->is_loaded()) {
466 return true;
467 }
468
469 auto now = std::chrono::system_clock::now();
470 return now > not_after();
471}
472
473auto certificate::to_pem() const -> std::string {
474 if (!impl_->is_loaded()) {
475 return "";
476 }
477
478 bio_ptr bio(BIO_new(BIO_s_mem()));
479 if (PEM_write_bio_X509(bio.get(), impl_->x509()) != 1) {
480 return "";
481 }
482
483 char* data = nullptr;
484 long len = BIO_get_mem_data(bio.get(), &data);
485 return std::string(data, static_cast<size_t>(len));
486}
487
488auto certificate::to_der() const -> std::vector<std::uint8_t> {
489 if (!impl_->is_loaded()) {
490 return {};
491 }
492
493 unsigned char* data = nullptr;
494 int len = i2d_X509(impl_->x509(), &data);
495 if (len <= 0) {
496 return {};
497 }
498
499 std::vector<std::uint8_t> result(data, data + len);
500 OPENSSL_free(data);
501 return result;
502}
503
504auto certificate::is_loaded() const noexcept -> bool {
505 return impl_ && impl_->is_loaded();
506}
507
508auto certificate::impl() const noexcept -> const certificate_impl* {
509 return impl_.get();
510}
511
513 return impl_.get();
514}
515
516// ============================================================================
517// private_key implementation
518// ============================================================================
519
520private_key::private_key() : impl_(std::make_unique<private_key_impl>()) {}
521
522private_key::private_key(private_key&& other) noexcept = default;
523
524auto private_key::operator=(private_key&& other) noexcept -> private_key& = default;
525
526private_key::~private_key() = default;
527
528auto private_key::load_from_pem(std::string_view path, std::string_view password)
530 auto content_result = read_file(path);
531 if (content_result.is_err()) {
532 return kcenon::common::make_error<private_key>(
533 content_result.error().code,
534 content_result.error().message,
535 "private_key");
536 }
537
538 return load_from_pem_string(content_result.value(), password);
539}
540
541auto private_key::load_from_pem_string(std::string_view pem_data, std::string_view password)
543 bio_ptr bio(BIO_new_mem_buf(pem_data.data(), static_cast<int>(pem_data.size())));
544 if (!bio) {
545 return kcenon::common::make_error<private_key>(
546 2, "Failed to create BIO: " + get_openssl_error(), "private_key");
547 }
548
549 EVP_PKEY* pkey = nullptr;
550 if (password.empty()) {
551 pkey = PEM_read_bio_PrivateKey(bio.get(), nullptr, nullptr, nullptr);
552 } else {
553 pkey = PEM_read_bio_PrivateKey(
554 bio.get(), nullptr, nullptr,
555 const_cast<char*>(std::string(password).c_str()));
556 }
557
558 if (!pkey) {
559 return kcenon::common::make_error<private_key>(
560 3, "Failed to parse PEM private key: " + get_openssl_error(), "private_key");
561 }
562
563 private_key key;
564 key.impl_ = std::make_unique<private_key_impl>(pkey);
565 return key;
566}
567
568auto private_key::algorithm_name() const -> std::string {
569 if (!impl_->is_loaded()) {
570 return "";
571 }
572
573 int type = EVP_PKEY_base_id(impl_->pkey());
574 switch (type) {
575 case EVP_PKEY_RSA:
576 case EVP_PKEY_RSA2:
577 return "RSA";
578 case EVP_PKEY_EC:
579 return "EC";
580 case EVP_PKEY_DSA:
581 return "DSA";
582 case EVP_PKEY_ED25519:
583 return "ED25519";
584 case EVP_PKEY_ED448:
585 return "ED448";
586 default:
587 return "Unknown";
588 }
589}
590
591auto private_key::key_size() const -> int {
592 if (!impl_->is_loaded()) {
593 return 0;
594 }
595
596 return EVP_PKEY_bits(impl_->pkey());
597}
598
599auto private_key::is_loaded() const noexcept -> bool {
600 return impl_ && impl_->is_loaded();
601}
602
603auto private_key::impl() const noexcept -> const private_key_impl* {
604 return impl_.get();
605}
606
608 return impl_.get();
609}
610
611// ============================================================================
612// certificate_chain implementation
613// ============================================================================
614
616 certs_.push_back(std::move(cert));
617}
618
620 if (certs_.empty()) {
621 return nullptr;
622 }
623 return &certs_.front();
624}
625
626auto certificate_chain::certificates() const -> const std::vector<certificate>& {
627 return certs_;
628}
629
630auto certificate_chain::empty() const noexcept -> bool {
631 return certs_.empty();
632}
633
634auto certificate_chain::size() const noexcept -> size_t {
635 return certs_.size();
636}
637
638auto certificate_chain::load_from_pem(std::string_view path)
640 auto content_result = read_file(path);
641 if (content_result.is_err()) {
642 return kcenon::common::make_error<certificate_chain>(
643 content_result.error().code,
644 content_result.error().message,
645 "certificate_chain");
646 }
647
648 const std::string& content = content_result.value();
649 certificate_chain chain;
650
651 // Parse multiple certificates from PEM
652 bio_ptr bio(BIO_new_mem_buf(content.data(), static_cast<int>(content.size())));
653 if (!bio) {
654 return kcenon::common::make_error<certificate_chain>(
655 2, "Failed to create BIO: " + get_openssl_error(), "certificate_chain");
656 }
657
658 while (true) {
659 X509* x509 = PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr);
660 if (!x509) {
661 // Check if it's EOF or an error
662 unsigned long err = ERR_peek_last_error();
663 if (ERR_GET_REASON(err) == PEM_R_NO_START_LINE) {
664 ERR_clear_error();
665 break; // End of file
666 }
667 break; // Error or EOF
668 }
669
670 certificate cert;
671 cert.impl_ = std::make_unique<certificate_impl>(x509);
672 chain.add(std::move(cert));
673 }
674
675 if (chain.empty()) {
676 return kcenon::common::make_error<certificate_chain>(
677 3, "No certificates found in PEM file", "certificate_chain");
678 }
679
680 return chain;
681}
682
683} // namespace kcenon::pacs::security
X.509 Certificate and Private Key handling for DICOM digital signatures.
Represents a certificate chain for validation.
auto size() const noexcept -> size_t
Get number of certificates in chain.
static auto load_from_pem(std::string_view path) -> kcenon::common::Result< certificate_chain >
Load certificate chain from PEM file.
std::vector< certificate > certs_
auto empty() const noexcept -> bool
Check if chain is empty.
auto end_entity() const -> const certificate *
Get the end-entity (leaf) certificate.
void add(certificate cert)
Add a certificate to the chain.
auto certificates() const -> const std::vector< certificate > &
Get all certificates in the chain.
auto is_loaded() const noexcept -> bool
auto operator=(const certificate_impl &other) -> certificate_impl &
certificate_impl(certificate_impl &&other) noexcept
auto operator=(certificate_impl &&other) noexcept -> certificate_impl &
auto x509() const noexcept -> X509 *
certificate_impl(const certificate_impl &other)
auto issuer_name() const -> std::string
Get the issuer distinguished name.
auto serial_number() const -> std::string
Get the certificate serial number.
std::unique_ptr< certificate_impl > impl_
auto to_der() const -> std::vector< std::uint8_t >
Export certificate as DER bytes.
auto is_expired() const -> bool
Check if the certificate has expired.
static auto load_from_der(std::span< const std::uint8_t > der_data) -> kcenon::common::Result< certificate >
Load certificate from DER-encoded bytes.
static auto load_from_pem(std::string_view path) -> kcenon::common::Result< certificate >
Load certificate from PEM file.
auto impl() const noexcept -> const certificate_impl *
Get internal implementation (for internal use only)
auto subject_name() const -> std::string
Get the subject distinguished name.
auto is_valid() const -> bool
Check if the certificate is currently valid.
auto not_before() const -> std::chrono::system_clock::time_point
Get the not-before date.
auto to_pem() const -> std::string
Export certificate as PEM string.
auto subject_common_name() const -> std::string
Get the common name from the subject.
auto operator=(const certificate &other) -> certificate &
Copy assignment.
auto thumbprint() const -> std::string
Get the certificate thumbprint (SHA-256)
auto is_loaded() const noexcept -> bool
Check if certificate is loaded.
static auto load_from_pem_string(std::string_view pem_data) -> kcenon::common::Result< certificate >
Load certificate from PEM string.
auto not_after() const -> std::chrono::system_clock::time_point
Get the not-after date.
certificate()
Default constructor - creates an empty certificate.
auto subject_organization() const -> std::string
Get the organization from the subject.
private_key_impl(private_key_impl &&other) noexcept
private_key_impl(const private_key_impl &)=delete
auto operator=(private_key_impl &&other) noexcept -> private_key_impl &
auto pkey() const noexcept -> EVP_PKEY *
auto is_loaded() const noexcept -> bool
auto operator=(const private_key_impl &) -> private_key_impl &=delete
auto impl() const noexcept -> const private_key_impl *
Get internal implementation (for internal use only)
auto operator=(const private_key &) -> private_key &=delete
Copy assignment (deleted)
private_key()
Default constructor - creates an empty key.
auto algorithm_name() const -> std::string
Get the algorithm name.
auto is_loaded() const noexcept -> bool
Check if key is loaded.
~private_key()
Destructor - securely erases key material.
static auto load_from_pem_string(std::string_view pem_data, std::string_view password="") -> kcenon::common::Result< private_key >
Load private key from PEM string.
auto key_size() const -> int
Get the key size in bits.
static auto load_from_pem(std::string_view path, std::string_view password="") -> kcenon::common::Result< private_key >
Load private key from PEM file.
std::unique_ptr< private_key_impl > impl_
@ hash
Hash the value for research linkage.
std::string_view name