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 <filesystem>
22#include <fstream>
23#include <system_error>
24
25namespace kcenon::pacs::security {
26
27namespace {
28
35inline auto portable_timegm(struct tm* tm_time) -> time_t {
36#ifdef _WIN32
37 return _mkgmtime(tm_time);
38#else
39 return timegm(tm_time);
40#endif
41}
42
46auto get_openssl_error() -> std::string {
47 unsigned long err = ERR_get_error();
48 if (err == 0) {
49 return "Unknown error";
50 }
51 char buf[256];
52 ERR_error_string_n(err, buf, sizeof(buf));
53 return std::string(buf);
54}
55
74auto read_file(std::string_view path) -> kcenon::common::Result<std::string> {
75 const std::string path_str(path);
76 std::error_code ec;
77
78 // Validate path is a regular file. On macOS, attempting to open a
79 // symlink whose target was removed (a frequent failure mode for test
80 // fixtures regenerated on each CI run) returns an opened ifstream
81 // whose subsequent reads are empty, masking the real error.
82 if (!std::filesystem::is_regular_file(path_str, ec) || ec) {
83 return kcenon::common::make_error<std::string>(
84 1, "Failed to open file (not a regular file): " + path_str,
85 "certificate");
86 }
87
88 const auto size = std::filesystem::file_size(path_str, ec);
89 if (ec) {
90 return kcenon::common::make_error<std::string>(
91 1, "Failed to stat file: " + path_str, "certificate");
92 }
93
94 std::ifstream file(path_str, std::ios::binary);
95 if (!file.is_open()) {
96 return kcenon::common::make_error<std::string>(
97 1, "Failed to open file: " + path_str, "certificate");
98 }
99
100 std::string content;
101 content.resize(static_cast<std::size_t>(size));
102 if (size > 0) {
103 file.read(content.data(), static_cast<std::streamsize>(size));
104 if (!file) {
105 return kcenon::common::make_error<std::string>(
106 1, "Failed to read file: " + path_str, "certificate");
107 }
108 }
109 return content;
110}
111
115struct bio_deleter {
116 void operator()(BIO* bio) const {
117 if (bio) BIO_free(bio);
118 }
119};
120using bio_ptr = std::unique_ptr<BIO, bio_deleter>;
121
125auto asn1_time_to_time_point(const ASN1_TIME* asn1_time)
126 -> std::chrono::system_clock::time_point {
127 if (!asn1_time) {
128 return std::chrono::system_clock::time_point{};
129 }
130
131 struct tm tm_time = {};
132 int offset_sec = 0;
133
134 // Parse the ASN1 time
135 if (ASN1_TIME_to_tm(asn1_time, &tm_time) != 1) {
136 return std::chrono::system_clock::time_point{};
137 }
138
139 // Convert to time_t (using portable function for cross-platform support)
140 time_t time = portable_timegm(&tm_time);
141
142 return std::chrono::system_clock::from_time_t(time);
143}
144
145} // anonymous namespace
146
147// ============================================================================
148// certificate_impl - PIMPL implementation
149// ============================================================================
150
152public:
153 certificate_impl() : x509_(nullptr) {}
154
155 explicit certificate_impl(X509* cert) : x509_(cert) {}
156
158 if (x509_) {
159 X509_free(x509_);
160 }
161 }
162
163 // Non-copyable but movable
165 if (other.x509_) {
166 x509_ = X509_dup(other.x509_);
167 } else {
168 x509_ = nullptr;
169 }
170 }
171
173 : x509_(other.x509_) {
174 other.x509_ = nullptr;
175 }
176
178 if (this != &other) {
179 if (x509_) {
180 X509_free(x509_);
181 }
182 if (other.x509_) {
183 x509_ = X509_dup(other.x509_);
184 } else {
185 x509_ = nullptr;
186 }
187 }
188 return *this;
189 }
190
192 if (this != &other) {
193 if (x509_) {
194 X509_free(x509_);
195 }
196 x509_ = other.x509_;
197 other.x509_ = nullptr;
198 }
199 return *this;
200 }
201
202 [[nodiscard]] auto x509() const noexcept -> X509* { return x509_; }
203 [[nodiscard]] auto is_loaded() const noexcept -> bool { return x509_ != nullptr; }
204
205private:
206 X509* x509_;
207};
208
209// ============================================================================
210// private_key_impl - PIMPL implementation
211// ============================================================================
212
214public:
215 private_key_impl() : pkey_(nullptr) {}
216
217 explicit private_key_impl(EVP_PKEY* key) : pkey_(key) {}
218
220 if (pkey_) {
221 EVP_PKEY_free(pkey_);
222 }
223 }
224
225 // Non-copyable, movable
227 auto operator=(const private_key_impl&) -> private_key_impl& = delete;
228
230 : pkey_(other.pkey_) {
231 other.pkey_ = nullptr;
232 }
233
235 if (this != &other) {
236 if (pkey_) {
237 EVP_PKEY_free(pkey_);
238 }
239 pkey_ = other.pkey_;
240 other.pkey_ = nullptr;
241 }
242 return *this;
243 }
244
245 [[nodiscard]] auto pkey() const noexcept -> EVP_PKEY* { return pkey_; }
246 [[nodiscard]] auto is_loaded() const noexcept -> bool { return pkey_ != nullptr; }
247
248private:
249 EVP_PKEY* pkey_;
250};
251
252// ============================================================================
253// certificate implementation
254// ============================================================================
255
256certificate::certificate() : impl_(std::make_unique<certificate_impl>()) {}
257
259 : impl_(std::make_unique<certificate_impl>(*other.impl_)) {}
260
261certificate::certificate(certificate&& other) noexcept = default;
262
264 if (this != &other) {
265 impl_ = std::make_unique<certificate_impl>(*other.impl_);
266 }
267 return *this;
268}
269
270auto certificate::operator=(certificate&& other) noexcept -> certificate& = default;
271
272certificate::~certificate() = default;
273
274auto certificate::load_from_pem(std::string_view path)
276 auto content_result = read_file(path);
277 if (content_result.is_err()) {
278 return kcenon::common::make_error<certificate>(
279 content_result.error().code,
280 content_result.error().message,
281 "certificate");
282 }
283
284 return load_from_pem_string(content_result.value());
285}
286
287auto certificate::load_from_pem_string(std::string_view pem_data)
289 bio_ptr bio(BIO_new_mem_buf(pem_data.data(), static_cast<int>(pem_data.size())));
290 if (!bio) {
291 return kcenon::common::make_error<certificate>(
292 2, "Failed to create BIO: " + get_openssl_error(), "certificate");
293 }
294
295 X509* x509 = PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr);
296 if (!x509) {
297 return kcenon::common::make_error<certificate>(
298 3, "Failed to parse PEM certificate: " + get_openssl_error(), "certificate");
299 }
300
301 certificate cert;
302 cert.impl_ = std::make_unique<certificate_impl>(x509);
303 return cert;
304}
305
306auto certificate::load_from_der(std::span<const std::uint8_t> der_data)
308 const unsigned char* data = der_data.data();
309 X509* x509 = d2i_X509(nullptr, &data, static_cast<long>(der_data.size()));
310 if (!x509) {
311 return kcenon::common::make_error<certificate>(
312 4, "Failed to parse DER certificate: " + get_openssl_error(), "certificate");
313 }
314
315 certificate cert;
316 cert.impl_ = std::make_unique<certificate_impl>(x509);
317 return cert;
318}
319
320auto certificate::subject_name() const -> std::string {
321 if (!impl_->is_loaded()) {
322 return "";
323 }
324
325 X509_NAME* name = X509_get_subject_name(impl_->x509());
326 if (!name) {
327 return "";
328 }
329
330 bio_ptr bio(BIO_new(BIO_s_mem()));
331 X509_NAME_print_ex(bio.get(), name, 0, XN_FLAG_RFC2253);
332
333 char* data = nullptr;
334 long len = BIO_get_mem_data(bio.get(), &data);
335 return std::string(data, static_cast<size_t>(len));
336}
337
338auto certificate::subject_common_name() const -> std::string {
339 if (!impl_->is_loaded()) {
340 return "";
341 }
342
343 X509_NAME* name = X509_get_subject_name(impl_->x509());
344 if (!name) {
345 return "";
346 }
347
348 int idx = X509_NAME_get_index_by_NID(name, NID_commonName, -1);
349 if (idx < 0) {
350 return "";
351 }
352
353 X509_NAME_ENTRY* entry = X509_NAME_get_entry(name, idx);
354 if (!entry) {
355 return "";
356 }
357
358 ASN1_STRING* data = X509_NAME_ENTRY_get_data(entry);
359 if (!data) {
360 return "";
361 }
362
363 unsigned char* utf8 = nullptr;
364 int len = ASN1_STRING_to_UTF8(&utf8, data);
365 if (len < 0) {
366 return "";
367 }
368
369 std::string result(reinterpret_cast<char*>(utf8), static_cast<size_t>(len));
370 OPENSSL_free(utf8);
371 return result;
372}
373
374auto certificate::subject_organization() const -> std::string {
375 if (!impl_->is_loaded()) {
376 return "";
377 }
378
379 X509_NAME* name = X509_get_subject_name(impl_->x509());
380 if (!name) {
381 return "";
382 }
383
384 int idx = X509_NAME_get_index_by_NID(name, NID_organizationName, -1);
385 if (idx < 0) {
386 return "";
387 }
388
389 X509_NAME_ENTRY* entry = X509_NAME_get_entry(name, idx);
390 if (!entry) {
391 return "";
392 }
393
394 ASN1_STRING* data = X509_NAME_ENTRY_get_data(entry);
395 if (!data) {
396 return "";
397 }
398
399 unsigned char* utf8 = nullptr;
400 int len = ASN1_STRING_to_UTF8(&utf8, data);
401 if (len < 0) {
402 return "";
403 }
404
405 std::string result(reinterpret_cast<char*>(utf8), static_cast<size_t>(len));
406 OPENSSL_free(utf8);
407 return result;
408}
409
410auto certificate::issuer_name() const -> std::string {
411 if (!impl_->is_loaded()) {
412 return "";
413 }
414
415 X509_NAME* name = X509_get_issuer_name(impl_->x509());
416 if (!name) {
417 return "";
418 }
419
420 bio_ptr bio(BIO_new(BIO_s_mem()));
421 X509_NAME_print_ex(bio.get(), name, 0, XN_FLAG_RFC2253);
422
423 char* data = nullptr;
424 long len = BIO_get_mem_data(bio.get(), &data);
425 return std::string(data, static_cast<size_t>(len));
426}
427
428auto certificate::serial_number() const -> std::string {
429 if (!impl_->is_loaded()) {
430 return "";
431 }
432
433 const ASN1_INTEGER* serial = X509_get_serialNumber(impl_->x509());
434 if (!serial) {
435 return "";
436 }
437
438 BIGNUM* bn = ASN1_INTEGER_to_BN(serial, nullptr);
439 if (!bn) {
440 return "";
441 }
442
443 char* hex = BN_bn2hex(bn);
444 BN_free(bn);
445
446 if (!hex) {
447 return "";
448 }
449
450 std::string result(hex);
451 OPENSSL_free(hex);
452 return result;
453}
454
455auto certificate::thumbprint() const -> std::string {
456 if (!impl_->is_loaded()) {
457 return "";
458 }
459
460 unsigned char hash[EVP_MAX_MD_SIZE];
461 unsigned int hash_len = 0;
462
463 if (X509_digest(impl_->x509(), EVP_sha256(), hash, &hash_len) != 1) {
464 return "";
465 }
466
467 std::string result;
468 result.reserve(hash_len * 2);
469
470 static const char hex_chars[] = "0123456789ABCDEF";
471 for (unsigned int i = 0; i < hash_len; ++i) {
472 result += hex_chars[(hash[i] >> 4) & 0x0F];
473 result += hex_chars[hash[i] & 0x0F];
474 }
475
476 return result;
477}
478
479auto certificate::not_before() const -> std::chrono::system_clock::time_point {
480 if (!impl_->is_loaded()) {
481 return std::chrono::system_clock::time_point{};
482 }
483
484 const ASN1_TIME* time = X509_get0_notBefore(impl_->x509());
485 return asn1_time_to_time_point(time);
486}
487
488auto certificate::not_after() const -> std::chrono::system_clock::time_point {
489 if (!impl_->is_loaded()) {
490 return std::chrono::system_clock::time_point{};
491 }
492
493 const ASN1_TIME* time = X509_get0_notAfter(impl_->x509());
494 return asn1_time_to_time_point(time);
495}
496
497auto certificate::is_valid() const -> bool {
498 if (!impl_->is_loaded()) {
499 return false;
500 }
501
502 auto now = std::chrono::system_clock::now();
503 return now >= not_before() && now <= not_after();
504}
505
506auto certificate::is_expired() const -> bool {
507 if (!impl_->is_loaded()) {
508 return true;
509 }
510
511 auto now = std::chrono::system_clock::now();
512 return now > not_after();
513}
514
515auto certificate::to_pem() const -> std::string {
516 if (!impl_->is_loaded()) {
517 return "";
518 }
519
520 bio_ptr bio(BIO_new(BIO_s_mem()));
521 if (PEM_write_bio_X509(bio.get(), impl_->x509()) != 1) {
522 return "";
523 }
524
525 char* data = nullptr;
526 long len = BIO_get_mem_data(bio.get(), &data);
527 return std::string(data, static_cast<size_t>(len));
528}
529
530auto certificate::to_der() const -> std::vector<std::uint8_t> {
531 if (!impl_->is_loaded()) {
532 return {};
533 }
534
535 unsigned char* data = nullptr;
536 int len = i2d_X509(impl_->x509(), &data);
537 if (len <= 0) {
538 return {};
539 }
540
541 std::vector<std::uint8_t> result(data, data + len);
542 OPENSSL_free(data);
543 return result;
544}
545
546auto certificate::is_loaded() const noexcept -> bool {
547 return impl_ && impl_->is_loaded();
548}
549
550auto certificate::impl() const noexcept -> const certificate_impl* {
551 return impl_.get();
552}
553
555 return impl_.get();
556}
557
558// ============================================================================
559// private_key implementation
560// ============================================================================
561
562private_key::private_key() : impl_(std::make_unique<private_key_impl>()) {}
563
564private_key::private_key(private_key&& other) noexcept = default;
565
566auto private_key::operator=(private_key&& other) noexcept -> private_key& = default;
567
568private_key::~private_key() = default;
569
570auto private_key::load_from_pem(std::string_view path, std::string_view password)
572 auto content_result = read_file(path);
573 if (content_result.is_err()) {
574 return kcenon::common::make_error<private_key>(
575 content_result.error().code,
576 content_result.error().message,
577 "private_key");
578 }
579
580 return load_from_pem_string(content_result.value(), password);
581}
582
583auto private_key::load_from_pem_string(std::string_view pem_data, std::string_view password)
585 bio_ptr bio(BIO_new_mem_buf(pem_data.data(), static_cast<int>(pem_data.size())));
586 if (!bio) {
587 return kcenon::common::make_error<private_key>(
588 2, "Failed to create BIO: " + get_openssl_error(), "private_key");
589 }
590
591 EVP_PKEY* pkey = nullptr;
592 if (password.empty()) {
593 pkey = PEM_read_bio_PrivateKey(bio.get(), nullptr, nullptr, nullptr);
594 } else {
595 pkey = PEM_read_bio_PrivateKey(
596 bio.get(), nullptr, nullptr,
597 const_cast<char*>(std::string(password).c_str()));
598 }
599
600 if (!pkey) {
601 return kcenon::common::make_error<private_key>(
602 3, "Failed to parse PEM private key: " + get_openssl_error(), "private_key");
603 }
604
605 private_key key;
606 key.impl_ = std::make_unique<private_key_impl>(pkey);
607 return key;
608}
609
610auto private_key::algorithm_name() const -> std::string {
611 if (!impl_->is_loaded()) {
612 return "";
613 }
614
615 int type = EVP_PKEY_base_id(impl_->pkey());
616 switch (type) {
617 case EVP_PKEY_RSA:
618 case EVP_PKEY_RSA2:
619 return "RSA";
620 case EVP_PKEY_EC:
621 return "EC";
622 case EVP_PKEY_DSA:
623 return "DSA";
624 case EVP_PKEY_ED25519:
625 return "ED25519";
626 case EVP_PKEY_ED448:
627 return "ED448";
628 default:
629 return "Unknown";
630 }
631}
632
633auto private_key::key_size() const -> int {
634 if (!impl_->is_loaded()) {
635 return 0;
636 }
637
638 return EVP_PKEY_bits(impl_->pkey());
639}
640
641auto private_key::is_loaded() const noexcept -> bool {
642 return impl_ && impl_->is_loaded();
643}
644
645auto private_key::impl() const noexcept -> const private_key_impl* {
646 return impl_.get();
647}
648
650 return impl_.get();
651}
652
653// ============================================================================
654// certificate_chain implementation
655// ============================================================================
656
658 certs_.push_back(std::move(cert));
659}
660
662 if (certs_.empty()) {
663 return nullptr;
664 }
665 return &certs_.front();
666}
667
668auto certificate_chain::certificates() const -> const std::vector<certificate>& {
669 return certs_;
670}
671
672auto certificate_chain::empty() const noexcept -> bool {
673 return certs_.empty();
674}
675
676auto certificate_chain::size() const noexcept -> size_t {
677 return certs_.size();
678}
679
680auto certificate_chain::load_from_pem(std::string_view path)
682 auto content_result = read_file(path);
683 if (content_result.is_err()) {
684 return kcenon::common::make_error<certificate_chain>(
685 content_result.error().code,
686 content_result.error().message,
687 "certificate_chain");
688 }
689
690 const std::string& content = content_result.value();
691 certificate_chain chain;
692
693 // Parse multiple certificates from PEM
694 bio_ptr bio(BIO_new_mem_buf(content.data(), static_cast<int>(content.size())));
695 if (!bio) {
696 return kcenon::common::make_error<certificate_chain>(
697 2, "Failed to create BIO: " + get_openssl_error(), "certificate_chain");
698 }
699
700 while (true) {
701 X509* x509 = PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr);
702 if (!x509) {
703 // Check if it's EOF or an error
704 unsigned long err = ERR_peek_last_error();
705 if (ERR_GET_REASON(err) == PEM_R_NO_START_LINE) {
706 ERR_clear_error();
707 break; // End of file
708 }
709 break; // Error or EOF
710 }
711
712 certificate cert;
713 cert.impl_ = std::make_unique<certificate_impl>(x509);
714 chain.add(std::move(cert));
715 }
716
717 if (chain.empty()) {
718 return kcenon::common::make_error<certificate_chain>(
719 3, "No certificates found in PEM file", "certificate_chain");
720 }
721
722 return chain;
723}
724
725} // 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)
Represents an X.509 certificate.
Definition certificate.h:56
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
Represents a private key for signing operations.
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