17#include <openssl/bio.h>
18#include <openssl/err.h>
19#include <openssl/evp.h>
20#include <openssl/pem.h>
21#include <openssl/rand.h>
22#include <openssl/x509.h>
32class certificate_impl;
33class private_key_impl;
43inline auto portable_gmtime_r(
const time_t* timer,
struct tm* result) ->
struct tm* {
45 return (gmtime_s(result, timer) == 0) ? result :
nullptr;
47 return gmtime_r(timer, result);
57inline auto portable_timegm(
struct tm* tm_time) -> time_t {
59 return _mkgmtime(tm_time);
61 return timegm(tm_time);
66constexpr core::dicom_tag digital_signature_sequence_tag{0x0400, 0x0561};
67constexpr core::dicom_tag signature_uid_tag{0x0400, 0x0100};
68constexpr core::dicom_tag signature_datetime_tag{0x0400, 0x0105};
69constexpr core::dicom_tag certificate_type_tag{0x0400, 0x0110};
70constexpr core::dicom_tag certificate_of_signer_tag{0x0400, 0x0115};
71constexpr core::dicom_tag signature_tag{0x0400, 0x0120};
72constexpr core::dicom_tag mac_id_number_tag{0x0400, 0x0005};
73constexpr core::dicom_tag mac_calculation_transfer_syntax_tag{0x0400, 0x0010};
74constexpr core::dicom_tag mac_algorithm_tag{0x0400, 0x0015};
75constexpr core::dicom_tag data_elements_signed_tag{0x0400, 0x0020};
80auto get_openssl_error() -> std::string {
81 unsigned long err = ERR_get_error();
83 return "Unknown error";
86 ERR_error_string_n(err, buf,
sizeof(buf));
87 return std::string(buf);
93struct evp_md_ctx_deleter {
94 void operator()(EVP_MD_CTX* ctx)
const {
95 if (ctx) EVP_MD_CTX_free(ctx);
98using evp_md_ctx_ptr = std::unique_ptr<EVP_MD_CTX, evp_md_ctx_deleter>;
155 auto now = std::chrono::system_clock::now();
156 auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
157 now.time_since_epoch()).count();
159 unsigned char random_bytes[4];
160 RAND_bytes(random_bytes,
sizeof(random_bytes));
162 std::ostringstream oss;
163 oss <<
"1.2.840.10008.5.1.4.1.1.66."
165 <<
static_cast<unsigned int>(random_bytes[0])
166 <<
static_cast<unsigned int>(random_bytes[1])
167 <<
static_cast<unsigned int>(random_bytes[2])
168 <<
static_cast<unsigned int>(random_bytes[3]);
176auto format_dicom_datetime() -> std::string {
177 auto now = std::chrono::system_clock::now();
178 auto time_t_now = std::chrono::system_clock::to_time_t(now);
179 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
180 now.time_since_epoch()) % 1000;
183 portable_gmtime_r(&time_t_now, &tm_now);
185 std::ostringstream oss;
186 oss << std::put_time(&tm_now,
"%Y%m%d%H%M%S")
187 <<
"." << std::setfill(
'0') << std::setw(3) << ms.count()
196auto parse_dicom_datetime(std::string_view dt_str)
197 -> std::chrono::system_clock::time_point {
198 if (dt_str.size() < 14) {
199 return std::chrono::system_clock::time_point{};
203 std::istringstream ss(std::string(dt_str.substr(0, 14)));
204 ss >> std::get_time(&tm,
"%Y%m%d%H%M%S");
207 return std::chrono::system_clock::time_point{};
210 time_t
time = portable_timegm(&tm);
211 return std::chrono::system_clock::from_time_t(time);
217auto serialize_elements_for_mac(
218 const core::dicom_dataset& dataset,
219 std::span<const core::dicom_tag> tags
220) -> std::vector<std::uint8_t> {
221 std::vector<std::uint8_t> result;
223 for (
const auto& tag : tags) {
224 const auto* elem = dataset.get(tag);
230 uint16_t group = tag.group();
231 uint16_t element = tag.element();
233 result.push_back(
static_cast<uint8_t
>(group & 0xFF));
234 result.push_back(
static_cast<uint8_t
>((group >> 8) & 0xFF));
235 result.push_back(
static_cast<uint8_t
>(element & 0xFF));
236 result.push_back(
static_cast<uint8_t
>((element >> 8) & 0xFF));
239 const auto& data = elem->raw_data();
240 for (
auto byte : data) {
241 result.push_back(
byte);
251auto get_signable_tags(
const core::dicom_dataset& dataset)
252 -> std::vector<core::dicom_tag> {
253 std::vector<core::dicom_tag> tags;
255 for (
const auto& [tag, elem] : dataset) {
257 if (tag.group() == 0x0400) {
263 std::sort(tags.begin(), tags.end());
270 EVP_PKEY* get_pkey_from_private_key(
const private_key& key);
277 auto* impl = cert.
impl();
278 if (!impl)
return nullptr;
281 return reinterpret_cast<X509*
>(
const_cast<void*
>(
282 static_cast<const void*
>(impl)));
294) -> kcenon::common::VoidResult {
296 auto tags = get_signable_tags(dataset);
297 return sign_tags(dataset, cert, key, tags, algo);
304 std::span<const core::dicom_tag> tags_to_sign,
306) -> kcenon::common::VoidResult {
307 if (!cert.is_loaded()) {
308 return kcenon::common::make_error<std::monostate>(
309 1,
"Certificate not loaded",
"digital_signature");
312 if (!key.is_loaded()) {
313 return kcenon::common::make_error<std::monostate>(
314 2,
"Private key not loaded",
"digital_signature");
317 if (!cert.is_valid()) {
318 return kcenon::common::make_error<std::monostate>(
319 3,
"Certificate is not valid (expired or not yet valid)",
"digital_signature");
323 auto mac_algo = get_mac_algorithm(algo);
324 auto mac = compute_mac(dataset, tags_to_sign, mac_algo);
327 auto signature_result = sign_mac(mac, key, algo);
328 if (signature_result.is_err()) {
329 return kcenon::common::make_error<std::monostate>(
330 signature_result.error().code,
331 signature_result.error().message,
332 "digital_signature");
335 const auto& signature_bytes = signature_result.value();
342 std::string sig_uid = generate_signature_uid();
345 auto cert_der = cert.to_der();
348 std::vector<std::uint8_t> signed_tags_data;
349 for (
const auto& tag : tags_to_sign) {
350 uint16_t group = tag.group();
351 uint16_t element = tag.element();
352 signed_tags_data.push_back(
static_cast<uint8_t
>(group & 0xFF));
353 signed_tags_data.push_back(
static_cast<uint8_t
>((group >> 8) & 0xFF));
354 signed_tags_data.push_back(
static_cast<uint8_t
>(element & 0xFF));
355 signed_tags_data.push_back(
static_cast<uint8_t
>((element >> 8) & 0xFF));
366 certificate_of_signer_tag,
382 data_elements_signed_tag,
386 return kcenon::common::ok();
393 if (!has_signature(dataset)) {
398 auto info = get_signature_info(dataset);
400 return kcenon::common::make_error<signature_status>(
401 1,
"Failed to extract signature info",
"digital_signature");
405 const auto* cert_elem = dataset.get(certificate_of_signer_tag);
407 return kcenon::common::make_error<signature_status>(
408 2,
"No certificate in signature",
"digital_signature");
412 if (cert_result.is_err()) {
413 return kcenon::common::make_error<signature_status>(
414 3,
"Failed to load certificate: " + cert_result.error().message,
415 "digital_signature");
418 const auto& cert = cert_result.value();
421 if (cert.is_expired()) {
425 if (!cert.is_valid()) {
430 const auto* signed_tags_elem = dataset.get(data_elements_signed_tag);
431 std::vector<core::dicom_tag> signed_tags;
433 if (signed_tags_elem) {
434 const auto& tag_data = signed_tags_elem->raw_data();
435 for (
size_t i = 0; i + 3 < tag_data.size(); i += 4) {
436 uint16_t group =
static_cast<uint16_t
>(tag_data[i]) |
437 (
static_cast<uint16_t
>(tag_data[i + 1]) << 8);
438 uint16_t element =
static_cast<uint16_t
>(tag_data[i + 2]) |
439 (
static_cast<uint16_t
>(tag_data[i + 3]) << 8);
440 signed_tags.emplace_back(group, element);
444 signed_tags = get_signable_tags(dataset);
451 const auto* sig_elem = dataset.get(signature_tag);
453 return kcenon::common::make_error<signature_status>(
454 4,
"No signature data found",
"digital_signature");
458 bool valid = verify_mac_signature(
460 sig_elem->raw_data(),
469 const std::vector<certificate>& trusted_certs
472 auto basic_result = verify(dataset);
473 if (basic_result.is_err()) {
477 auto status = basic_result.value();
483 const auto* cert_elem = dataset.get(certificate_of_signer_tag);
489 if (cert_result.is_err()) {
493 const auto& signer_cert = cert_result.value();
494 std::string signer_thumbprint = signer_cert.thumbprint();
497 for (
const auto& trusted : trusted_certs) {
498 if (trusted.thumbprint() == signer_thumbprint) {
508) -> std::optional<signature_info> {
509 if (!has_signature(dataset)) {
516 info.signature_uid = dataset.get_string(signature_uid_tag);
519 std::string dt_str = dataset.get_string(signature_datetime_tag);
520 if (!dt_str.empty()) {
521 info.timestamp = parse_dicom_datetime(dt_str);
525 const auto* cert_elem = dataset.get(certificate_of_signer_tag);
528 if (cert_result.is_ok()) {
529 const auto& cert = cert_result.value();
530 info.signer_name = cert.subject_common_name();
531 info.signer_organization = cert.subject_organization();
532 info.certificate_thumbprint = cert.thumbprint();
540 const auto* signed_tags_elem = dataset.get(data_elements_signed_tag);
541 if (signed_tags_elem) {
542 const auto& tag_data = signed_tags_elem->raw_data();
543 for (
size_t i = 0; i + 3 < tag_data.size(); i += 4) {
544 uint16_t group =
static_cast<uint16_t
>(tag_data[i]) |
545 (
static_cast<uint16_t
>(tag_data[i + 1]) << 8);
546 uint16_t element =
static_cast<uint16_t
>(tag_data[i + 2]) |
547 (
static_cast<uint16_t
>(tag_data[i + 3]) << 8);
548 info.signed_tags.push_back(
549 (
static_cast<uint32_t
>(group) << 16) | element);
558) -> std::vector<signature_info> {
560 std::vector<signature_info> result;
562 auto info = get_signature_info(dataset);
564 result.push_back(std::move(*info));
574 return dataset.contains(signature_tag) ||
575 dataset.contains(digital_signature_sequence_tag);
581 bool removed =
false;
584 removed |= dataset.remove(digital_signature_sequence_tag);
585 removed |= dataset.remove(signature_uid_tag);
586 removed |= dataset.remove(signature_datetime_tag);
587 removed |= dataset.remove(certificate_type_tag);
588 removed |= dataset.remove(certificate_of_signer_tag);
589 removed |= dataset.remove(signature_tag);
590 removed |= dataset.remove(mac_id_number_tag);
591 removed |= dataset.remove(mac_calculation_transfer_syntax_tag);
592 removed |= dataset.remove(mac_algorithm_tag);
593 removed |= dataset.remove(data_elements_signed_tag);
599 return generate_uid();
604 std::span<const core::dicom_tag> tags,
606) -> std::vector<std::uint8_t> {
608 auto data = serialize_elements_for_mac(dataset, tags);
611 const EVP_MD* md = get_mac_evp_md(algo);
612 unsigned int md_len = EVP_MD_size(md);
614 std::vector<std::uint8_t> result(md_len);
616 evp_md_ctx_ptr ctx(EVP_MD_CTX_new());
621 if (EVP_DigestInit_ex(ctx.get(), md,
nullptr) != 1) {
625 if (EVP_DigestUpdate(ctx.get(), data.data(), data.size()) != 1) {
629 if (EVP_DigestFinal_ex(ctx.get(), result.data(), &md_len) != 1) {
633 result.resize(md_len);
638 std::span<const std::uint8_t> mac_data,
642 if (!key.is_loaded()) {
643 return kcenon::common::make_error<std::vector<std::uint8_t>>(
644 1,
"Private key not loaded",
"digital_signature");
649 auto* key_impl = key.impl();
651 return kcenon::common::make_error<std::vector<std::uint8_t>>(
652 2,
"Invalid private key",
"digital_signature");
657 EVP_PKEY* pkey =
reinterpret_cast<EVP_PKEY*
>(
const_cast<void*
>(
658 static_cast<const void*
>(key_impl)));
660 const EVP_MD* md = get_evp_md(algo);
662 evp_md_ctx_ptr ctx(EVP_MD_CTX_new());
664 return kcenon::common::make_error<std::vector<std::uint8_t>>(
665 3,
"Failed to create context: " + get_openssl_error(),
"digital_signature");
668 if (EVP_DigestSignInit(ctx.get(),
nullptr, md,
nullptr, pkey) != 1) {
669 return kcenon::common::make_error<std::vector<std::uint8_t>>(
670 4,
"Failed to init sign: " + get_openssl_error(),
"digital_signature");
673 if (EVP_DigestSignUpdate(ctx.get(), mac_data.data(), mac_data.size()) != 1) {
674 return kcenon::common::make_error<std::vector<std::uint8_t>>(
675 5,
"Failed to update sign: " + get_openssl_error(),
"digital_signature");
680 if (EVP_DigestSignFinal(ctx.get(),
nullptr, &sig_len) != 1) {
681 return kcenon::common::make_error<std::vector<std::uint8_t>>(
682 6,
"Failed to get signature length: " + get_openssl_error(),
"digital_signature");
685 std::vector<std::uint8_t> signature(sig_len);
686 if (EVP_DigestSignFinal(ctx.get(), signature.data(), &sig_len) != 1) {
687 return kcenon::common::make_error<std::vector<std::uint8_t>>(
688 7,
"Failed to create signature: " + get_openssl_error(),
"digital_signature");
691 signature.resize(sig_len);
696 std::span<const std::uint8_t> mac_data,
697 std::span<const std::uint8_t> signature,
701 if (!cert.is_loaded()) {
706 auto* cert_impl = cert.impl();
712 X509* x509 =
reinterpret_cast<X509*
>(
const_cast<void*
>(
713 static_cast<const void*
>(cert_impl)));
715 EVP_PKEY* pkey = X509_get_pubkey(x509);
720 const EVP_MD* md = get_evp_md(algo);
722 evp_md_ctx_ptr ctx(EVP_MD_CTX_new());
730 if (EVP_DigestVerifyInit(ctx.get(),
nullptr, md,
nullptr, pkey) == 1 &&
731 EVP_DigestVerifyUpdate(ctx.get(), mac_data.data(), mac_data.size()) == 1 &&
732 EVP_DigestVerifyFinal(ctx.get(), signature.data(), signature.size()) == 1) {
static auto load_from_der(std::span< const std::uint8_t > der_data) -> kcenon::common::Result< certificate >
Load certificate from DER-encoded bytes.
auto impl() const noexcept -> const certificate_impl *
Get internal implementation (for internal use only)
static auto compute_mac(const core::dicom_dataset &dataset, std::span< const core::dicom_tag > tags, mac_algorithm algo) -> std::vector< std::uint8_t >
static auto sign_mac(std::span< const std::uint8_t > mac_data, const private_key &key, signature_algorithm algo) -> kcenon::common::Result< std::vector< std::uint8_t > >
static auto has_signature(const core::dicom_dataset &dataset) -> bool
Check if a dataset contains digital signatures.
static auto remove_signatures(core::dicom_dataset &dataset) -> bool
Remove all digital signatures from a dataset.
static auto sign(core::dicom_dataset &dataset, const certificate &cert, const private_key &key, signature_algorithm algo=signature_algorithm::rsa_sha256) -> kcenon::common::VoidResult
Sign a DICOM dataset.
static auto verify_with_trust(const core::dicom_dataset &dataset, const std::vector< certificate > &trusted_certs) -> kcenon::common::Result< signature_status >
Verify digital signatures with a trusted certificate store.
static auto verify(const core::dicom_dataset &dataset) -> kcenon::common::Result< signature_status >
Verify digital signatures in a dataset.
static auto sign_tags(core::dicom_dataset &dataset, const certificate &cert, const private_key &key, std::span< const core::dicom_tag > tags_to_sign, signature_algorithm algo=signature_algorithm::rsa_sha256) -> kcenon::common::VoidResult
Sign specific tags in a DICOM dataset.
static auto verify_mac_signature(std::span< const std::uint8_t > mac_data, std::span< const std::uint8_t > signature, const certificate &cert, signature_algorithm algo) -> bool
static auto get_signature_info(const core::dicom_dataset &dataset) -> std::optional< signature_info >
Get information about signatures in a dataset.
static auto get_all_signatures(const core::dicom_dataset &dataset) -> std::vector< signature_info >
Get all signatures in a dataset.
static auto generate_signature_uid() -> std::string
Generate a new Digital Signature UID.
DICOM Data Element representation (Tag, VR, Value)
DICOM Digital Signature creation and verification per PS3.15.
@ OB
Other Byte (variable length)
@ DT
Date Time (26 chars max)
@ UI
Unique Identifier (64 chars max)
@ CS
Code String (16 chars max, uppercase + digits + space + underscore)
@ AT
Attribute Tag (4 bytes)
std::string generate_uid(const std::string &root="1.2.826.0.1.3680043.9.9999")
Generate a unique UID for testing.
constexpr std::string_view to_dicom_uid(mac_algorithm algo)
Convert mac_algorithm to DICOM UID string.
X509 * get_x509_from_certificate(const certificate &cert)
mac_algorithm
MAC algorithm identifiers per DICOM PS3.15.
@ sha256
SHA-256 (recommended)
@ untrusted_signer
Signer certificate is not trusted.
@ no_signature
No signature present in dataset.
@ valid
Signature is valid and trusted.
@ expired
Signer certificate has expired.
@ invalid
Signature verification failed (tampered data)
signature_algorithm
Signature algorithms supported for DICOM digital signatures.
@ rsa_sha512
RSA with SHA-512 (highest security)
@ ecdsa_sha256
ECDSA with SHA-256 (compact signatures)
@ ecdsa_sha384
ECDSA with SHA-384.
@ rsa_sha384
RSA with SHA-384.
@ rsa_sha256
RSA with SHA-256 (recommended for most use cases)
Information about a digital signature.