20#include <catch2/catch_test_macros.hpp>
21#include <catch2/matchers/catch_matchers_string.hpp>
44struct test_certificate_bundle {
45 std::filesystem::path ca_cert;
46 std::filesystem::path ca_key;
47 std::filesystem::path server_cert;
48 std::filesystem::path server_key;
49 std::filesystem::path client_cert;
50 std::filesystem::path client_key;
51 std::filesystem::path other_ca_cert;
56 [[nodiscard]]
bool all_exist()
const {
57 return std::filesystem::exists(ca_cert) &&
58 std::filesystem::exists(ca_key) &&
59 std::filesystem::exists(server_cert) &&
60 std::filesystem::exists(server_key) &&
61 std::filesystem::exists(client_cert) &&
62 std::filesystem::exists(client_key);
70test_certificate_bundle get_test_certificates() {
72 std::vector<std::filesystem::path> search_paths = {
73 std::filesystem::current_path() /
"test_data" /
"certs",
74 std::filesystem::current_path() /
"bin" /
"test_data" /
"certs",
75 std::filesystem::path(__FILE__).parent_path() /
"test_data" /
"certs"
79 if (
const char* cert_dir = std::getenv(
"PACS_TEST_CERT_DIR")) {
80 search_paths.insert(search_paths.begin(), cert_dir);
83 for (
const auto& cert_dir : search_paths) {
84 if (std::filesystem::exists(cert_dir /
"ca.crt")) {
88 cert_dir /
"server.crt",
89 cert_dir /
"server.key",
90 cert_dir /
"client.crt",
91 cert_dir /
"client.key",
92 cert_dir /
"other_ca.crt"
98 auto default_dir = std::filesystem::path(__FILE__).parent_path() /
"test_data" /
"certs";
100 default_dir /
"ca.crt",
101 default_dir /
"ca.key",
102 default_dir /
"server.crt",
103 default_dir /
"server.key",
104 default_dir /
"client.crt",
105 default_dir /
"client.key",
106 default_dir /
"other_ca.crt"
115class tls_test_server {
117 explicit tls_test_server(
119 const std::string& ae_title,
122 , ae_title_(ae_title)
123 , tls_cfg_(tls_cfg) {
134 auto tls_result = network_adapter::configure_tls(tls_cfg_);
135 if (tls_result.is_err()) {
142 server_ = network_adapter::create_server(config, tls_cfg_);
150 tls_test_server(
const tls_test_server&) =
delete;
151 tls_test_server& operator=(
const tls_test_server&) =
delete;
152 tls_test_server(tls_test_server&&) =
delete;
153 tls_test_server& operator=(tls_test_server&&) =
delete;
155 template <
typename Service>
156 void register_service(std::shared_ptr<Service> service) {
158 server_->register_service(std::move(service));
162 [[nodiscard]]
bool start() {
163 if (!server_ || !tls_valid_) {
166 auto result = server_->start();
167 if (result.is_ok()) {
169 std::this_thread::sleep_for(std::chrono::milliseconds{100});
171 return result.is_ok();
175 if (running_ && server_) {
181 [[nodiscard]] uint16_t port() const noexcept {
return port_; }
182 [[nodiscard]]
const std::string& ae_title() const noexcept {
return ae_title_; }
183 [[nodiscard]]
bool is_running() const noexcept {
return running_; }
184 [[nodiscard]]
bool is_tls_valid() const noexcept {
return tls_valid_; }
189 std::string ae_title_;
191 std::unique_ptr<kcenon::pacs::network::dicom_server> server_;
192 bool running_{
false};
193 bool tls_valid_{
false};
199class tls_test_client {
202 const std::string& host,
204 const std::string& called_ae,
205 const std::string& calling_ae,
210 auto tls_result = network_adapter::configure_tls(tls_cfg);
211 if (tls_result.is_err()) {
212 return tls_result.error();
222 uint8_t context_id = 1;
223 for (
const auto& sop_class : sop_classes) {
228 "1.2.840.10008.1.2.1",
247TEST_CASE(
"TLS C-ECHO connection",
"[tls][connectivity]") {
248 auto certs = get_test_certificates();
251 if (!certs.all_exist()) {
252 WARN(
"Skipping TLS tests: certificates not found at " << certs.ca_cert.parent_path());
253 SKIP(
"Test certificates not available");
256 SECTION(
"TLS server accepts connection and responds to C-ECHO") {
260 server_tls.
cert_path = certs.server_cert;
261 server_tls.
key_path = certs.server_key;
262 server_tls.
ca_path = certs.ca_cert;
264 server_tls.
min_version = tls_config::tls_version::v1_2;
267 tls_test_server server(port,
"TLS_SCP", server_tls);
269 if (!server.is_tls_valid()) {
270 WARN(
"TLS configuration not valid, skipping test");
271 SKIP(
"TLS not properly configured");
274 server.register_service(std::make_shared<verification_scp>());
275 REQUIRE(server.start());
276 REQUIRE(server.is_running());
281 client_tls.
ca_path = certs.ca_cert;
283 client_tls.
min_version = tls_config::tls_version::v1_2;
285 auto connect_result = tls_test_client::connect(
286 "localhost", port, server.ae_title(),
"TLS_SCU", client_tls);
288 REQUIRE(connect_result.is_ok());
289 auto& assoc = connect_result.value();
295 REQUIRE(context_id_opt.has_value());
300 auto send_result = assoc.send_dimse(*context_id_opt, echo_rq);
301 REQUIRE(send_result.is_ok());
304 REQUIRE(recv_result.is_ok());
306 auto& [recv_ctx, echo_rsp] = recv_result.value();
307 REQUIRE(echo_rsp.command() == command_field::c_echo_rsp);
308 REQUIRE(echo_rsp.status() == status_success);
320TEST_CASE(
"TLS certificate validation",
"[tls][security]") {
321 auto certs = get_test_certificates();
323 if (!certs.all_exist()) {
324 SKIP(
"Test certificates not available");
330 server_tls.
cert_path = certs.server_cert;
331 server_tls.
key_path = certs.server_key;
332 server_tls.
ca_path = certs.ca_cert;
334 server_tls.
min_version = tls_config::tls_version::v1_2;
337 tls_test_server server(port,
"TLS_SCP", server_tls);
339 if (!server.is_tls_valid()) {
340 SKIP(
"TLS not properly configured");
343 server.register_service(std::make_shared<verification_scp>());
344 REQUIRE(server.start());
346 SECTION(
"Valid certificate succeeds") {
349 client_tls.
ca_path = certs.ca_cert;
351 client_tls.
min_version = tls_config::tls_version::v1_2;
353 auto result = tls_test_client::connect(
354 "localhost", port, server.ae_title(),
"TLS_SCU", client_tls);
356 REQUIRE(result.is_ok());
357 auto& assoc = result.value();
361 SECTION(
"Wrong CA fails validation") {
363 if (!std::filesystem::exists(certs.other_ca_cert)) {
364 WARN(
"other_ca.crt not found, skipping wrong CA test");
371 client_tls.
ca_path = certs.other_ca_cert;
373 client_tls.
min_version = tls_config::tls_version::v1_2;
375 auto result = tls_test_client::connect(
376 "localhost", port, server.ae_title(),
"TLS_SCU", client_tls);
381 if (result.is_ok()) {
382 auto& assoc = result.value();
387 SECTION(
"TLS configuration validation") {
391 invalid_tls.
cert_path =
"/nonexistent/cert.pem";
392 invalid_tls.
key_path =
"/nonexistent/key.pem";
394 auto result = network_adapter::configure_tls(invalid_tls);
396 REQUIRE(result.is_err());
407 auto certs = get_test_certificates();
409 if (!certs.all_exist()) {
410 SKIP(
"Test certificates not available");
413 SECTION(
"Client with valid certificate succeeds") {
417 server_tls.
cert_path = certs.server_cert;
418 server_tls.
key_path = certs.server_key;
419 server_tls.
ca_path = certs.ca_cert;
421 server_tls.
min_version = tls_config::tls_version::v1_2;
424 tls_test_server server(port,
"MTLS_SCP", server_tls);
426 if (!server.is_tls_valid()) {
427 SKIP(
"TLS not properly configured");
430 server.register_service(std::make_shared<verification_scp>());
431 REQUIRE(server.start());
436 client_tls.
cert_path = certs.client_cert;
437 client_tls.
key_path = certs.client_key;
438 client_tls.
ca_path = certs.ca_cert;
440 client_tls.
min_version = tls_config::tls_version::v1_2;
442 auto result = tls_test_client::connect(
443 "localhost", port, server.ae_title(),
"MTLS_SCU", client_tls);
445 REQUIRE(result.is_ok());
446 auto& assoc = result.value();
454 auto send_result = assoc.send_dimse(ctx_id, echo_rq);
455 REQUIRE(send_result.is_ok());
458 REQUIRE(recv_result.is_ok());
460 auto& [recv_ctx, rsp] = recv_result.value();
461 REQUIRE(rsp.status() == status_success);
467 SECTION(
"Client without certificate fails when server requires it") {
470 server_tls.
cert_path = certs.server_cert;
471 server_tls.
key_path = certs.server_key;
472 server_tls.
ca_path = certs.ca_cert;
474 server_tls.
min_version = tls_config::tls_version::v1_2;
477 tls_test_server server(port,
"MTLS_SCP", server_tls);
479 if (!server.is_tls_valid()) {
480 SKIP(
"TLS not properly configured");
483 server.register_service(std::make_shared<verification_scp>());
484 REQUIRE(server.start());
489 client_tls.
ca_path = certs.ca_cert;
492 client_tls.
min_version = tls_config::tls_version::v1_2;
494 auto result = tls_test_client::connect(
495 "localhost", port, server.ae_title(),
"NO_CERT_SCU", client_tls);
499 if (result.is_ok()) {
500 auto& assoc = result.value();
514 auto certs = get_test_certificates();
516 if (!certs.all_exist()) {
517 SKIP(
"Test certificates not available");
520 SECTION(
"TLS 1.2 connection") {
523 server_tls.
cert_path = certs.server_cert;
524 server_tls.
key_path = certs.server_key;
525 server_tls.
ca_path = certs.ca_cert;
527 server_tls.
min_version = tls_config::tls_version::v1_2;
530 tls_test_server server(port,
"TLS12_SCP", server_tls);
532 if (!server.is_tls_valid()) {
533 SKIP(
"TLS not properly configured");
536 server.register_service(std::make_shared<verification_scp>());
537 REQUIRE(server.start());
541 client_tls.
ca_path = certs.ca_cert;
543 client_tls.
min_version = tls_config::tls_version::v1_2;
545 auto result = tls_test_client::connect(
546 "localhost", port, server.ae_title(),
"TLS12_SCU", client_tls);
548 REQUIRE(result.is_ok());
549 auto& assoc = result.value();
554 SECTION(
"TLS 1.3 connection") {
557 server_tls.
cert_path = certs.server_cert;
558 server_tls.
key_path = certs.server_key;
559 server_tls.
ca_path = certs.ca_cert;
561 server_tls.
min_version = tls_config::tls_version::v1_3;
564 tls_test_server server(port,
"TLS13_SCP", server_tls);
566 if (!server.is_tls_valid()) {
567 SKIP(
"TLS not properly configured");
570 server.register_service(std::make_shared<verification_scp>());
571 REQUIRE(server.start());
575 client_tls.
ca_path = certs.ca_cert;
577 client_tls.
min_version = tls_config::tls_version::v1_3;
579 auto result = tls_test_client::connect(
580 "localhost", port, server.ae_title(),
"TLS13_SCU", client_tls);
583 if (result.is_ok()) {
584 auto& assoc = result.value();
587 INFO(
"TLS 1.3 not supported: " << result.error().message);
598TEST_CASE(
"Multiple concurrent TLS connections",
"[tls][concurrent]") {
599 auto certs = get_test_certificates();
601 if (!certs.all_exist()) {
602 SKIP(
"Test certificates not available");
607 server_tls.
cert_path = certs.server_cert;
608 server_tls.
key_path = certs.server_key;
609 server_tls.
ca_path = certs.ca_cert;
611 server_tls.
min_version = tls_config::tls_version::v1_2;
614 tls_test_server server(port,
"CONCURRENT_TLS", server_tls);
616 if (!server.is_tls_valid()) {
617 SKIP(
"TLS not properly configured");
620 server.register_service(std::make_shared<verification_scp>());
621 REQUIRE(server.start());
623 constexpr int num_connections = 3;
624 std::vector<std::thread> threads;
625 std::atomic<int> success_count{0};
627 for (
int i = 0; i < num_connections; ++i) {
628 threads.emplace_back([&, i]() {
631 client_tls.
ca_path = certs.ca_cert;
633 client_tls.
min_version = tls_config::tls_version::v1_2;
635 auto result = tls_test_client::connect(
636 "localhost", port, server.ae_title(),
637 "TLS_SCU_" + std::to_string(i), client_tls);
639 if (result.is_err()) {
643 auto& assoc = result.value();
651 auto send_result = assoc.send_dimse(*ctx_opt, echo_rq);
652 if (send_result.is_err()) {
657 if (recv_result.is_ok()) {
658 auto& [ctx, rsp] = recv_result.value();
659 if (rsp.status() == status_success) {
668 for (
auto& t : threads) {
674 REQUIRE(success_count == num_connections);
681TEST_CASE(
"TLS configuration validation",
"[tls][config]") {
682 SECTION(
"Disabled TLS is always valid") {
689 SECTION(
"Enabled TLS requires cert and key") {
705 SECTION(
"CA path is optional") {
724#ifdef PACS_WITH_NETWORK_SYSTEM
739class tls_test_server_v2 {
741 explicit tls_test_server_v2(
743 const std::string& ae_title,
746 , ae_title_(ae_title)
747 , tls_cfg_(tls_cfg) {
758 auto tls_result = network_adapter::configure_tls(tls_cfg_);
759 if (tls_result.is_err()) {
767 server_ = std::make_unique<kcenon::pacs::network::v2::dicom_server_v2>(config);
770 ~tls_test_server_v2() {
774 tls_test_server_v2(
const tls_test_server_v2&) =
delete;
775 tls_test_server_v2& operator=(
const tls_test_server_v2&) =
delete;
776 tls_test_server_v2(tls_test_server_v2&&) =
delete;
777 tls_test_server_v2& operator=(tls_test_server_v2&&) =
delete;
779 template <
typename Service>
780 void register_service(std::shared_ptr<Service> service) {
782 server_->register_service(std::move(service));
786 [[nodiscard]]
bool start() {
787 if (!server_ || !tls_valid_) {
790 auto result = server_->start();
791 if (result.is_ok()) {
793 std::this_thread::sleep_for(std::chrono::milliseconds{100});
795 return result.is_ok();
799 if (running_ && server_) {
805 [[nodiscard]] uint16_t port() const noexcept {
return port_; }
806 [[nodiscard]]
const std::string& ae_title() const noexcept {
return ae_title_; }
807 [[nodiscard]]
bool is_running() const noexcept {
return running_; }
808 [[nodiscard]]
bool is_tls_valid() const noexcept {
return tls_valid_; }
813 std::string ae_title_;
815 std::unique_ptr<kcenon::pacs::network::v2::dicom_server_v2> server_;
816 bool running_{
false};
817 bool tls_valid_{
false};
822TEST_CASE(
"TLS C-ECHO with dicom_server_v2",
"[tls][v2][connectivity]") {
823 auto certs = get_test_certificates();
825 if (!certs.all_exist()) {
826 WARN(
"Skipping TLS V2 tests: certificates not found");
827 SKIP(
"Test certificates not available");
830 SECTION(
"V2 TLS server accepts connection and responds to C-ECHO") {
833 server_tls.
cert_path = certs.server_cert;
834 server_tls.
key_path = certs.server_key;
835 server_tls.
ca_path = certs.ca_cert;
837 server_tls.
min_version = tls_config::tls_version::v1_2;
840 tls_test_server_v2 server(port,
"TLS_V2_SCP", server_tls);
842 if (!server.is_tls_valid()) {
843 WARN(
"TLS configuration not valid for V2, skipping test");
844 SKIP(
"TLS not properly configured");
847 server.register_service(std::make_shared<verification_scp>());
848 REQUIRE(server.start());
849 REQUIRE(server.is_running());
853 client_tls.
ca_path = certs.ca_cert;
855 client_tls.
min_version = tls_config::tls_version::v1_2;
857 auto connect_result = tls_test_client::connect(
858 "localhost", port, server.ae_title(),
"TLS_V2_SCU", client_tls);
860 REQUIRE(connect_result.is_ok());
861 auto& assoc = connect_result.value();
866 REQUIRE(context_id_opt.has_value());
870 auto send_result = assoc.send_dimse(*context_id_opt, echo_rq);
871 REQUIRE(send_result.is_ok());
874 REQUIRE(recv_result.is_ok());
876 auto& [recv_ctx, echo_rsp] = recv_result.value();
877 REQUIRE(echo_rsp.command() == command_field::c_echo_rsp);
878 REQUIRE(echo_rsp.status() == status_success);
885TEST_CASE(
"TLS concurrent connections with dicom_server_v2",
"[tls][v2][concurrent]") {
886 auto certs = get_test_certificates();
888 if (!certs.all_exist()) {
889 SKIP(
"Test certificates not available");
894 server_tls.
cert_path = certs.server_cert;
895 server_tls.
key_path = certs.server_key;
896 server_tls.
ca_path = certs.ca_cert;
898 server_tls.
min_version = tls_config::tls_version::v1_2;
901 tls_test_server_v2 server(port,
"TLS_V2_CONCURRENT", server_tls);
903 if (!server.is_tls_valid()) {
904 SKIP(
"TLS not properly configured for V2");
907 server.register_service(std::make_shared<verification_scp>());
908 REQUIRE(server.start());
910 constexpr int num_connections = 5;
911 std::vector<std::thread> threads;
912 std::atomic<int> success_count{0};
914 for (
int i = 0; i < num_connections; ++i) {
915 threads.emplace_back([&, i]() {
918 client_tls.
ca_path = certs.ca_cert;
920 client_tls.
min_version = tls_config::tls_version::v1_2;
922 auto result = tls_test_client::connect(
923 "localhost", port, server.ae_title(),
924 "TLS_V2_SCU_" + std::to_string(i), client_tls);
926 if (result.is_err()) {
930 auto& assoc = result.value();
938 auto send_result = assoc.send_dimse(*ctx_opt, echo_rq);
939 if (send_result.is_err()) {
944 if (recv_result.is_ok()) {
945 auto& [ctx, rsp] = recv_result.value();
946 if (rsp.status() == status_success) {
955 for (
auto& t : threads) {
961 REQUIRE(success_count == num_connections);
Adapter for integrating network_system for DICOM protocol.
static Result< association > connect(const std::string &host, uint16_t port, const association_config &config, duration timeout=default_timeout)
Initiate an SCU association to a remote SCP.
DICOM server using network_system's messaging_server for connection management.
DICOM server implementation using network_system's messaging_server.
DIMSE message encoding and decoding.
uint16_t find_available_port(uint16_t start=default_test_port, int max_attempts=200)
Find an available port for testing.
std::chrono::milliseconds default_timeout()
Default timeout for test operations (5s normal, 30s CI)
TEST_CASE("test_data_generator::ct generates valid CT dataset", "[data_generator][ct]")
constexpr std::string_view verification_sop_class_uid
Verification SOP Class UID (1.2.840.10008.1.1)
Adapter for integrating network_system for DICOM protocol.
Configuration for TLS/SSL secure transport.
bool enabled
Enable TLS for connections.
enum kcenon::pacs::integration::tls_config::tls_version min_version
bool is_valid() const noexcept
Check if TLS configuration is valid.
std::filesystem::path cert_path
Path to certificate file (PEM format)
std::filesystem::path ca_path
Path to CA certificate file for verification (optional)
bool verify_peer
Verify peer certificate.
std::filesystem::path key_path
Path to private key file (PEM format)
Configuration for SCU association request.
std::string called_ae_title
Remote AE Title (16 chars max)
std::string calling_ae_title
Our AE Title (16 chars max)
std::string implementation_class_uid
std::string implementation_version_name
std::vector< proposed_presentation_context > proposed_contexts
size_t max_associations
Maximum concurrent associations (0 = unlimited)
std::chrono::seconds idle_timeout
Idle timeout for associations (0 = no timeout)
std::string implementation_version_name
Implementation Version Name.
uint16_t port
Port to listen on (default: 11112, standard alternate DICOM port)
std::string ae_title
Application Entity Title for this server (16 chars max)
std::string implementation_class_uid
Implementation Class UID.
Common test fixtures and utilities for integration tests.
DICOM Verification SCP service (C-ECHO handler)