PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
test_tls_integration.cpp
Go to the documentation of this file.
1
18#include "test_fixtures.h"
19
20#include <catch2/catch_test_macros.hpp>
21#include <catch2/matchers/catch_matchers_string.hpp>
22
26
27#include <filesystem>
28#include <thread>
29
30using namespace kcenon::pacs::integration_test;
31using namespace kcenon::pacs::services;
34
35namespace {
36
37// =============================================================================
38// TLS Test Fixtures
39// =============================================================================
40
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;
52
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);
63 }
64};
65
70test_certificate_bundle get_test_certificates() {
71 // Look for certificates in build directory first, then source directory
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"
76 };
77
78 // Also check environment variable
79 if (const char* cert_dir = std::getenv("PACS_TEST_CERT_DIR")) {
80 search_paths.insert(search_paths.begin(), cert_dir);
81 }
82
83 for (const auto& cert_dir : search_paths) {
84 if (std::filesystem::exists(cert_dir / "ca.crt")) {
85 return {
86 cert_dir / "ca.crt",
87 cert_dir / "ca.key",
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"
93 };
94 }
95 }
96
97 // Return default paths (tests will skip if not found)
98 auto default_dir = std::filesystem::path(__FILE__).parent_path() / "test_data" / "certs";
99 return {
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"
107 };
108}
109
115class tls_test_server {
116public:
117 explicit tls_test_server(
118 uint16_t port,
119 const std::string& ae_title,
120 const tls_config& tls_cfg)
121 : port_(port == 0 ? find_available_port() : port)
122 , ae_title_(ae_title)
123 , tls_cfg_(tls_cfg) {
124
126 config.ae_title = ae_title_;
127 config.port = port_;
128 config.max_associations = 10;
129 config.idle_timeout = std::chrono::seconds{30};
130 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.1";
131 config.implementation_version_name = "TLS_TEST_SCP";
132
133 // Validate TLS config first
134 auto tls_result = network_adapter::configure_tls(tls_cfg_);
135 if (tls_result.is_err()) {
136 tls_valid_ = false;
137 return;
138 }
139 tls_valid_ = true;
140
141 // Create server with TLS
142 server_ = network_adapter::create_server(config, tls_cfg_);
143 }
144
145 ~tls_test_server() {
146 stop();
147 }
148
149 // Non-copyable, non-movable
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;
154
155 template <typename Service>
156 void register_service(std::shared_ptr<Service> service) {
157 if (server_) {
158 server_->register_service(std::move(service));
159 }
160 }
161
162 [[nodiscard]] bool start() {
163 if (!server_ || !tls_valid_) {
164 return false;
165 }
166 auto result = server_->start();
167 if (result.is_ok()) {
168 running_ = true;
169 std::this_thread::sleep_for(std::chrono::milliseconds{100});
170 }
171 return result.is_ok();
172 }
173
174 void stop() {
175 if (running_ && server_) {
176 server_->stop();
177 running_ = false;
178 }
179 }
180
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_; }
185 [[nodiscard]] kcenon::pacs::network::dicom_server* server() { return server_.get(); }
186
187private:
188 uint16_t port_;
189 std::string ae_title_;
190 tls_config tls_cfg_;
191 std::unique_ptr<kcenon::pacs::network::dicom_server> server_;
192 bool running_{false};
193 bool tls_valid_{false};
194};
195
199class tls_test_client {
200public:
202 const std::string& host,
203 uint16_t port,
204 const std::string& called_ae,
205 const std::string& calling_ae,
206 const tls_config& tls_cfg,
207 const std::vector<std::string>& sop_classes = {std::string(verification_sop_class_uid)}) {
208
209 // Validate TLS configuration
210 auto tls_result = network_adapter::configure_tls(tls_cfg);
211 if (tls_result.is_err()) {
212 return tls_result.error();
213 }
214
215 // Configure association
217 config.calling_ae_title = calling_ae;
218 config.called_ae_title = called_ae;
219 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.2";
220 config.implementation_version_name = "TLS_TEST_SCU";
221
222 uint8_t context_id = 1;
223 for (const auto& sop_class : sop_classes) {
224 config.proposed_contexts.push_back({
225 context_id,
226 sop_class,
227 {
228 "1.2.840.10008.1.2.1", // Explicit VR Little Endian
229 "1.2.840.10008.1.2" // Implicit VR Little Endian
230 }
231 });
232 context_id += 2;
233 }
234
235 // Connect with TLS configuration
236 // Note: In full implementation, TLS config would be passed to connect
238 }
239};
240
241} // namespace
242
243// =============================================================================
244// Scenario 1: Basic TLS Connection
245// =============================================================================
246
247TEST_CASE("TLS C-ECHO connection", "[tls][connectivity]") {
248 auto certs = get_test_certificates();
249
250 // Skip if certificates not available
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");
254 }
255
256 SECTION("TLS server accepts connection and responds to C-ECHO") {
257 // Configure TLS server
258 tls_config server_tls;
259 server_tls.enabled = true;
260 server_tls.cert_path = certs.server_cert;
261 server_tls.key_path = certs.server_key;
262 server_tls.ca_path = certs.ca_cert;
263 server_tls.verify_peer = false; // Don't require client cert
264 server_tls.min_version = tls_config::tls_version::v1_2;
265
266 auto port = find_available_port();
267 tls_test_server server(port, "TLS_SCP", server_tls);
268
269 if (!server.is_tls_valid()) {
270 WARN("TLS configuration not valid, skipping test");
271 SKIP("TLS not properly configured");
272 }
273
274 server.register_service(std::make_shared<verification_scp>());
275 REQUIRE(server.start());
276 REQUIRE(server.is_running());
277
278 // Configure TLS client
279 tls_config client_tls;
280 client_tls.enabled = true;
281 client_tls.ca_path = certs.ca_cert;
282 client_tls.verify_peer = true; // Verify server cert
283 client_tls.min_version = tls_config::tls_version::v1_2;
284
285 auto connect_result = tls_test_client::connect(
286 "localhost", port, server.ae_title(), "TLS_SCU", client_tls);
287
288 REQUIRE(connect_result.is_ok());
289 auto& assoc = connect_result.value();
290
291 // Verify connection
292 REQUIRE(assoc.has_accepted_context(verification_sop_class_uid));
293
294 auto context_id_opt = assoc.accepted_context_id(verification_sop_class_uid);
295 REQUIRE(context_id_opt.has_value());
296
297 // Send C-ECHO
298 using namespace kcenon::pacs::network::dimse;
299 auto echo_rq = make_c_echo_rq(1, verification_sop_class_uid);
300 auto send_result = assoc.send_dimse(*context_id_opt, echo_rq);
301 REQUIRE(send_result.is_ok());
302
303 auto recv_result = assoc.receive_dimse(default_timeout());
304 REQUIRE(recv_result.is_ok());
305
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);
309
310 // Clean up
311 (void)assoc.release(default_timeout());
312 server.stop();
313 }
314}
315
316// =============================================================================
317// Scenario 2: Certificate Validation
318// =============================================================================
319
320TEST_CASE("TLS certificate validation", "[tls][security]") {
321 auto certs = get_test_certificates();
322
323 if (!certs.all_exist()) {
324 SKIP("Test certificates not available");
325 }
326
327 // Start TLS server
328 tls_config server_tls;
329 server_tls.enabled = true;
330 server_tls.cert_path = certs.server_cert;
331 server_tls.key_path = certs.server_key;
332 server_tls.ca_path = certs.ca_cert;
333 server_tls.verify_peer = false;
334 server_tls.min_version = tls_config::tls_version::v1_2;
335
336 auto port = find_available_port();
337 tls_test_server server(port, "TLS_SCP", server_tls);
338
339 if (!server.is_tls_valid()) {
340 SKIP("TLS not properly configured");
341 }
342
343 server.register_service(std::make_shared<verification_scp>());
344 REQUIRE(server.start());
345
346 SECTION("Valid certificate succeeds") {
347 tls_config client_tls;
348 client_tls.enabled = true;
349 client_tls.ca_path = certs.ca_cert;
350 client_tls.verify_peer = true;
351 client_tls.min_version = tls_config::tls_version::v1_2;
352
353 auto result = tls_test_client::connect(
354 "localhost", port, server.ae_title(), "TLS_SCU", client_tls);
355
356 REQUIRE(result.is_ok());
357 auto& assoc = result.value();
358 (void)assoc.release(default_timeout());
359 }
360
361 SECTION("Wrong CA fails validation") {
362 // Skip if other_ca doesn't exist
363 if (!std::filesystem::exists(certs.other_ca_cert)) {
364 WARN("other_ca.crt not found, skipping wrong CA test");
365 server.stop();
366 return;
367 }
368
369 tls_config client_tls;
370 client_tls.enabled = true;
371 client_tls.ca_path = certs.other_ca_cert; // Wrong CA
372 client_tls.verify_peer = true;
373 client_tls.min_version = tls_config::tls_version::v1_2;
374
375 auto result = tls_test_client::connect(
376 "localhost", port, server.ae_title(), "TLS_SCU", client_tls);
377
378 // Should fail due to certificate verification failure
379 // Note: Actual behavior depends on implementation
380 // Some implementations may still connect but with warnings
381 if (result.is_ok()) {
382 auto& assoc = result.value();
383 assoc.abort();
384 }
385 }
386
387 SECTION("TLS configuration validation") {
388 // Test invalid TLS configuration
389 tls_config invalid_tls;
390 invalid_tls.enabled = true;
391 invalid_tls.cert_path = "/nonexistent/cert.pem";
392 invalid_tls.key_path = "/nonexistent/key.pem";
393
394 auto result = network_adapter::configure_tls(invalid_tls);
395 // Should fail validation
396 REQUIRE(result.is_err());
397 }
398
399 server.stop();
400}
401
402// =============================================================================
403// Scenario 3: Mutual TLS (mTLS)
404// =============================================================================
405
406TEST_CASE("Mutual TLS authentication", "[tls][mtls]") {
407 auto certs = get_test_certificates();
408
409 if (!certs.all_exist()) {
410 SKIP("Test certificates not available");
411 }
412
413 SECTION("Client with valid certificate succeeds") {
414 // Server requires client certificate
415 tls_config server_tls;
416 server_tls.enabled = true;
417 server_tls.cert_path = certs.server_cert;
418 server_tls.key_path = certs.server_key;
419 server_tls.ca_path = certs.ca_cert;
420 server_tls.verify_peer = true; // Require client cert
421 server_tls.min_version = tls_config::tls_version::v1_2;
422
423 auto port = find_available_port();
424 tls_test_server server(port, "MTLS_SCP", server_tls);
425
426 if (!server.is_tls_valid()) {
427 SKIP("TLS not properly configured");
428 }
429
430 server.register_service(std::make_shared<verification_scp>());
431 REQUIRE(server.start());
432
433 // Client with certificate
434 tls_config client_tls;
435 client_tls.enabled = true;
436 client_tls.cert_path = certs.client_cert;
437 client_tls.key_path = certs.client_key;
438 client_tls.ca_path = certs.ca_cert;
439 client_tls.verify_peer = true;
440 client_tls.min_version = tls_config::tls_version::v1_2;
441
442 auto result = tls_test_client::connect(
443 "localhost", port, server.ae_title(), "MTLS_SCU", client_tls);
444
445 REQUIRE(result.is_ok());
446 auto& assoc = result.value();
447
448 // Verify we can communicate
449 REQUIRE(assoc.has_accepted_context(verification_sop_class_uid));
450
451 using namespace kcenon::pacs::network::dimse;
452 auto ctx_id = *assoc.accepted_context_id(verification_sop_class_uid);
453 auto echo_rq = make_c_echo_rq(1, verification_sop_class_uid);
454 auto send_result = assoc.send_dimse(ctx_id, echo_rq);
455 REQUIRE(send_result.is_ok());
456
457 auto recv_result = assoc.receive_dimse(default_timeout());
458 REQUIRE(recv_result.is_ok());
459
460 auto& [recv_ctx, rsp] = recv_result.value();
461 REQUIRE(rsp.status() == status_success);
462
463 (void)assoc.release(default_timeout());
464 server.stop();
465 }
466
467 SECTION("Client without certificate fails when server requires it") {
468 tls_config server_tls;
469 server_tls.enabled = true;
470 server_tls.cert_path = certs.server_cert;
471 server_tls.key_path = certs.server_key;
472 server_tls.ca_path = certs.ca_cert;
473 server_tls.verify_peer = true; // Require client cert
474 server_tls.min_version = tls_config::tls_version::v1_2;
475
476 auto port = find_available_port();
477 tls_test_server server(port, "MTLS_SCP", server_tls);
478
479 if (!server.is_tls_valid()) {
480 SKIP("TLS not properly configured");
481 }
482
483 server.register_service(std::make_shared<verification_scp>());
484 REQUIRE(server.start());
485
486 // Client without certificate
487 tls_config client_tls;
488 client_tls.enabled = true;
489 client_tls.ca_path = certs.ca_cert;
490 client_tls.verify_peer = true;
491 // No client cert/key
492 client_tls.min_version = tls_config::tls_version::v1_2;
493
494 auto result = tls_test_client::connect(
495 "localhost", port, server.ae_title(), "NO_CERT_SCU", client_tls);
496
497 // Should fail due to missing client certificate
498 // Note: Actual behavior depends on server configuration
499 if (result.is_ok()) {
500 auto& assoc = result.value();
501 // Connection might succeed but operations may fail
502 assoc.abort();
503 }
504
505 server.stop();
506 }
507}
508
509// =============================================================================
510// Scenario 4: TLS Version Negotiation
511// =============================================================================
512
513TEST_CASE("TLS version negotiation", "[tls][version]") {
514 auto certs = get_test_certificates();
515
516 if (!certs.all_exist()) {
517 SKIP("Test certificates not available");
518 }
519
520 SECTION("TLS 1.2 connection") {
521 tls_config server_tls;
522 server_tls.enabled = true;
523 server_tls.cert_path = certs.server_cert;
524 server_tls.key_path = certs.server_key;
525 server_tls.ca_path = certs.ca_cert;
526 server_tls.verify_peer = false;
527 server_tls.min_version = tls_config::tls_version::v1_2;
528
529 auto port = find_available_port();
530 tls_test_server server(port, "TLS12_SCP", server_tls);
531
532 if (!server.is_tls_valid()) {
533 SKIP("TLS not properly configured");
534 }
535
536 server.register_service(std::make_shared<verification_scp>());
537 REQUIRE(server.start());
538
539 tls_config client_tls;
540 client_tls.enabled = true;
541 client_tls.ca_path = certs.ca_cert;
542 client_tls.verify_peer = true;
543 client_tls.min_version = tls_config::tls_version::v1_2;
544
545 auto result = tls_test_client::connect(
546 "localhost", port, server.ae_title(), "TLS12_SCU", client_tls);
547
548 REQUIRE(result.is_ok());
549 auto& assoc = result.value();
550 (void)assoc.release(default_timeout());
551 server.stop();
552 }
553
554 SECTION("TLS 1.3 connection") {
555 tls_config server_tls;
556 server_tls.enabled = true;
557 server_tls.cert_path = certs.server_cert;
558 server_tls.key_path = certs.server_key;
559 server_tls.ca_path = certs.ca_cert;
560 server_tls.verify_peer = false;
561 server_tls.min_version = tls_config::tls_version::v1_3;
562
563 auto port = find_available_port();
564 tls_test_server server(port, "TLS13_SCP", server_tls);
565
566 if (!server.is_tls_valid()) {
567 SKIP("TLS not properly configured");
568 }
569
570 server.register_service(std::make_shared<verification_scp>());
571 REQUIRE(server.start());
572
573 tls_config client_tls;
574 client_tls.enabled = true;
575 client_tls.ca_path = certs.ca_cert;
576 client_tls.verify_peer = true;
577 client_tls.min_version = tls_config::tls_version::v1_3;
578
579 auto result = tls_test_client::connect(
580 "localhost", port, server.ae_title(), "TLS13_SCU", client_tls);
581
582 // TLS 1.3 may not be supported on all systems
583 if (result.is_ok()) {
584 auto& assoc = result.value();
585 (void)assoc.release(default_timeout());
586 } else {
587 INFO("TLS 1.3 not supported: " << result.error().message);
588 }
589
590 server.stop();
591 }
592}
593
594// =============================================================================
595// Scenario 5: Multiple TLS Connections
596// =============================================================================
597
598TEST_CASE("Multiple concurrent TLS connections", "[tls][concurrent]") {
599 auto certs = get_test_certificates();
600
601 if (!certs.all_exist()) {
602 SKIP("Test certificates not available");
603 }
604
605 tls_config server_tls;
606 server_tls.enabled = true;
607 server_tls.cert_path = certs.server_cert;
608 server_tls.key_path = certs.server_key;
609 server_tls.ca_path = certs.ca_cert;
610 server_tls.verify_peer = false;
611 server_tls.min_version = tls_config::tls_version::v1_2;
612
613 auto port = find_available_port();
614 tls_test_server server(port, "CONCURRENT_TLS", server_tls);
615
616 if (!server.is_tls_valid()) {
617 SKIP("TLS not properly configured");
618 }
619
620 server.register_service(std::make_shared<verification_scp>());
621 REQUIRE(server.start());
622
623 constexpr int num_connections = 3;
624 std::vector<std::thread> threads;
625 std::atomic<int> success_count{0};
626
627 for (int i = 0; i < num_connections; ++i) {
628 threads.emplace_back([&, i]() {
629 tls_config client_tls;
630 client_tls.enabled = true;
631 client_tls.ca_path = certs.ca_cert;
632 client_tls.verify_peer = true;
633 client_tls.min_version = tls_config::tls_version::v1_2;
634
635 auto result = tls_test_client::connect(
636 "localhost", port, server.ae_title(),
637 "TLS_SCU_" + std::to_string(i), client_tls);
638
639 if (result.is_err()) {
640 return;
641 }
642
643 auto& assoc = result.value();
644 auto ctx_opt = assoc.accepted_context_id(verification_sop_class_uid);
645 if (!ctx_opt) {
646 return;
647 }
648
649 using namespace kcenon::pacs::network::dimse;
650 auto echo_rq = make_c_echo_rq(1, verification_sop_class_uid);
651 auto send_result = assoc.send_dimse(*ctx_opt, echo_rq);
652 if (send_result.is_err()) {
653 return;
654 }
655
656 auto recv_result = assoc.receive_dimse(default_timeout());
657 if (recv_result.is_ok()) {
658 auto& [ctx, rsp] = recv_result.value();
659 if (rsp.status() == status_success) {
660 ++success_count;
661 }
662 }
663
664 (void)assoc.release(default_timeout());
665 });
666 }
667
668 for (auto& t : threads) {
669 t.join();
670 }
671
672 server.stop();
673
674 REQUIRE(success_count == num_connections);
675}
676
677// =============================================================================
678// TLS Configuration Validation Tests
679// =============================================================================
680
681TEST_CASE("TLS configuration validation", "[tls][config]") {
682 SECTION("Disabled TLS is always valid") {
683 tls_config cfg;
684 cfg.enabled = false;
685
686 REQUIRE(cfg.is_valid() == true);
687 }
688
689 SECTION("Enabled TLS requires cert and key") {
690 tls_config cfg;
691 cfg.enabled = true;
692
693 // Missing both cert and key
694 REQUIRE(cfg.is_valid() == false);
695
696 // Only cert
697 cfg.cert_path = "/some/cert.pem";
698 REQUIRE(cfg.is_valid() == false);
699
700 // Both cert and key
701 cfg.key_path = "/some/key.pem";
702 REQUIRE(cfg.is_valid() == true);
703 }
704
705 SECTION("CA path is optional") {
706 tls_config cfg;
707 cfg.enabled = true;
708 cfg.cert_path = "/some/cert.pem";
709 cfg.key_path = "/some/key.pem";
710
711 // Valid without CA
712 REQUIRE(cfg.is_valid() == true);
713
714 // Still valid with CA
715 cfg.ca_path = "/some/ca.pem";
716 REQUIRE(cfg.is_valid() == true);
717 }
718}
719
720// =============================================================================
721// Scenario 6: TLS Integration with dicom_server_v2
722// =============================================================================
723
724#ifdef PACS_WITH_NETWORK_SYSTEM
725
727
728namespace {
729
739class tls_test_server_v2 {
740public:
741 explicit tls_test_server_v2(
742 uint16_t port,
743 const std::string& ae_title,
744 const tls_config& tls_cfg)
745 : port_(port == 0 ? find_available_port() : port)
746 , ae_title_(ae_title)
747 , tls_cfg_(tls_cfg) {
748
750 config.ae_title = ae_title_;
751 config.port = port_;
752 config.max_associations = 10;
753 config.idle_timeout = std::chrono::seconds{30};
754 config.implementation_class_uid = "1.2.826.0.1.3680043.9.9999.200";
755 config.implementation_version_name = "TLS_V2_SCP";
756
757 // Validate TLS config first
758 auto tls_result = network_adapter::configure_tls(tls_cfg_);
759 if (tls_result.is_err()) {
760 tls_valid_ = false;
761 return;
762 }
763 tls_valid_ = true;
764
765 // Create V2 server
766 // Note: TLS integration for V2 server uses network_system's TLS support
767 server_ = std::make_unique<kcenon::pacs::network::v2::dicom_server_v2>(config);
768 }
769
770 ~tls_test_server_v2() {
771 stop();
772 }
773
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;
778
779 template <typename Service>
780 void register_service(std::shared_ptr<Service> service) {
781 if (server_) {
782 server_->register_service(std::move(service));
783 }
784 }
785
786 [[nodiscard]] bool start() {
787 if (!server_ || !tls_valid_) {
788 return false;
789 }
790 auto result = server_->start();
791 if (result.is_ok()) {
792 running_ = true;
793 std::this_thread::sleep_for(std::chrono::milliseconds{100});
794 }
795 return result.is_ok();
796 }
797
798 void stop() {
799 if (running_ && server_) {
800 server_->stop();
801 running_ = false;
802 }
803 }
804
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_; }
809 [[nodiscard]] kcenon::pacs::network::v2::dicom_server_v2* server() { return server_.get(); }
810
811private:
812 uint16_t port_;
813 std::string ae_title_;
814 tls_config tls_cfg_;
815 std::unique_ptr<kcenon::pacs::network::v2::dicom_server_v2> server_;
816 bool running_{false};
817 bool tls_valid_{false};
818};
819
820} // namespace
821
822TEST_CASE("TLS C-ECHO with dicom_server_v2", "[tls][v2][connectivity]") {
823 auto certs = get_test_certificates();
824
825 if (!certs.all_exist()) {
826 WARN("Skipping TLS V2 tests: certificates not found");
827 SKIP("Test certificates not available");
828 }
829
830 SECTION("V2 TLS server accepts connection and responds to C-ECHO") {
831 tls_config server_tls;
832 server_tls.enabled = true;
833 server_tls.cert_path = certs.server_cert;
834 server_tls.key_path = certs.server_key;
835 server_tls.ca_path = certs.ca_cert;
836 server_tls.verify_peer = false;
837 server_tls.min_version = tls_config::tls_version::v1_2;
838
839 auto port = find_available_port();
840 tls_test_server_v2 server(port, "TLS_V2_SCP", server_tls);
841
842 if (!server.is_tls_valid()) {
843 WARN("TLS configuration not valid for V2, skipping test");
844 SKIP("TLS not properly configured");
845 }
846
847 server.register_service(std::make_shared<verification_scp>());
848 REQUIRE(server.start());
849 REQUIRE(server.is_running());
850
851 tls_config client_tls;
852 client_tls.enabled = true;
853 client_tls.ca_path = certs.ca_cert;
854 client_tls.verify_peer = true;
855 client_tls.min_version = tls_config::tls_version::v1_2;
856
857 auto connect_result = tls_test_client::connect(
858 "localhost", port, server.ae_title(), "TLS_V2_SCU", client_tls);
859
860 REQUIRE(connect_result.is_ok());
861 auto& assoc = connect_result.value();
862
863 REQUIRE(assoc.has_accepted_context(verification_sop_class_uid));
864
865 auto context_id_opt = assoc.accepted_context_id(verification_sop_class_uid);
866 REQUIRE(context_id_opt.has_value());
867
868 using namespace kcenon::pacs::network::dimse;
869 auto echo_rq = make_c_echo_rq(1, verification_sop_class_uid);
870 auto send_result = assoc.send_dimse(*context_id_opt, echo_rq);
871 REQUIRE(send_result.is_ok());
872
873 auto recv_result = assoc.receive_dimse(default_timeout());
874 REQUIRE(recv_result.is_ok());
875
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);
879
880 (void)assoc.release(default_timeout());
881 server.stop();
882 }
883}
884
885TEST_CASE("TLS concurrent connections with dicom_server_v2", "[tls][v2][concurrent]") {
886 auto certs = get_test_certificates();
887
888 if (!certs.all_exist()) {
889 SKIP("Test certificates not available");
890 }
891
892 tls_config server_tls;
893 server_tls.enabled = true;
894 server_tls.cert_path = certs.server_cert;
895 server_tls.key_path = certs.server_key;
896 server_tls.ca_path = certs.ca_cert;
897 server_tls.verify_peer = false;
898 server_tls.min_version = tls_config::tls_version::v1_2;
899
900 auto port = find_available_port();
901 tls_test_server_v2 server(port, "TLS_V2_CONCURRENT", server_tls);
902
903 if (!server.is_tls_valid()) {
904 SKIP("TLS not properly configured for V2");
905 }
906
907 server.register_service(std::make_shared<verification_scp>());
908 REQUIRE(server.start());
909
910 constexpr int num_connections = 5;
911 std::vector<std::thread> threads;
912 std::atomic<int> success_count{0};
913
914 for (int i = 0; i < num_connections; ++i) {
915 threads.emplace_back([&, i]() {
916 tls_config client_tls;
917 client_tls.enabled = true;
918 client_tls.ca_path = certs.ca_cert;
919 client_tls.verify_peer = true;
920 client_tls.min_version = tls_config::tls_version::v1_2;
921
922 auto result = tls_test_client::connect(
923 "localhost", port, server.ae_title(),
924 "TLS_V2_SCU_" + std::to_string(i), client_tls);
925
926 if (result.is_err()) {
927 return;
928 }
929
930 auto& assoc = result.value();
931 auto ctx_opt = assoc.accepted_context_id(verification_sop_class_uid);
932 if (!ctx_opt) {
933 return;
934 }
935
936 using namespace kcenon::pacs::network::dimse;
937 auto echo_rq = make_c_echo_rq(1, verification_sop_class_uid);
938 auto send_result = assoc.send_dimse(*ctx_opt, echo_rq);
939 if (send_result.is_err()) {
940 return;
941 }
942
943 auto recv_result = assoc.receive_dimse(default_timeout());
944 if (recv_result.is_ok()) {
945 auto& [ctx, rsp] = recv_result.value();
946 if (rsp.status() == status_success) {
947 ++success_count;
948 }
949 }
950
951 (void)assoc.release(default_timeout());
952 });
953 }
954
955 for (auto& t : threads) {
956 t.join();
957 }
958
959 server.stop();
960
961 REQUIRE(success_count == num_connections);
962}
963
964#endif // PACS_WITH_NETWORK_SYSTEM
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::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)