35constexpr const char* default_calling_ae =
"SECURE_SCU";
44 std::filesystem::path cert_path;
45 std::filesystem::path key_path;
46 std::filesystem::path ca_path;
47 bool verify_server =
true;
48 std::string tls_version =
"1.2";
55void print_usage(
const char* program_name) {
57Secure Echo SCU - TLS-secured DICOM Connectivity Test Client
59Usage: )" << program_name << R"( <host> <port> <called_ae> [--cert <file> --key <file>] [options]
62 host Remote host address (IP or hostname)
63 port Remote port number (typically 2762 for DICOM TLS)
64 called_ae Called AE Title (remote SCP's AE title)
67 --cert <file> Client certificate file for mutual TLS (PEM format)
68 --key <file> Client private key file for mutual TLS (PEM format)
69 --ca <file> CA certificate for server verification (PEM format)
70 --no-verify Disable server certificate verification (not recommended)
71 --tls-version <ver> Minimum TLS version: 1.2 or 1.3 (default: 1.2)
74 --calling-ae <ae> Calling AE Title (default: SECURE_SCU)
75 --timeout <ms> Operation timeout in milliseconds (default: 30000)
76 --help Show this help message
79 # Basic TLS connection (server cert verification only)
80 )" << program_name << R"( localhost 2762 PACS_SCP --ca ca.crt
82 # Mutual TLS (client and server certificates)
83 )" << program_name << R"( localhost 2762 PACS_SCP --cert client.crt --key client.key --ca ca.crt
85 # TLS 1.3 with custom AE title
86 )" << program_name << R"( 192.168.1.100 2762 REMOTE_PACS --ca ca.crt --tls-version 1.3 --calling-ae MY_SCANNER
89 - Standard DICOM TLS port is 2762
90 - For production, always verify server certificates (avoid --no-verify)
91 - Mutual TLS requires both --cert and --key
94 0 Success - Echo response received
95 1 Error - Connection, TLS, or echo failed
107 std::string& called_ae,
108 std::string& calling_ae,
110 std::chrono::milliseconds& timeout) {
117 for (
int i = 1; i < argc; ++i) {
118 if (std::string(argv[i]) ==
"--help" || std::string(argv[i]) ==
"-h") {
128 int port_int = std::stoi(argv[2]);
129 if (port_int < 1 || port_int > 65535) {
130 std::cerr <<
"Error: Port must be between 1 and 65535\n";
133 port =
static_cast<uint16_t
>(port_int);
134 }
catch (
const std::exception&) {
135 std::cerr <<
"Error: Invalid port number '" << argv[2] <<
"'\n";
140 if (called_ae.length() > 16) {
141 std::cerr <<
"Error: Called AE title exceeds 16 characters\n";
146 calling_ae = default_calling_ae;
148 tls.verify_server =
true;
149 tls.tls_version =
"1.2";
152 for (
int i = 4; i < argc; ++i) {
153 std::string arg = argv[i];
155 if (arg ==
"--cert" && i + 1 < argc) {
156 tls.cert_path = argv[++i];
157 }
else if (arg ==
"--key" && i + 1 < argc) {
158 tls.key_path = argv[++i];
159 }
else if (arg ==
"--ca" && i + 1 < argc) {
160 tls.ca_path = argv[++i];
161 }
else if (arg ==
"--no-verify") {
162 tls.verify_server =
false;
163 std::cerr <<
"Warning: Server certificate verification disabled. "
164 <<
"This is not recommended for production.\n";
165 }
else if (arg ==
"--tls-version" && i + 1 < argc) {
166 tls.tls_version = argv[++i];
167 if (
tls.tls_version !=
"1.2" &&
tls.tls_version !=
"1.3") {
168 std::cerr <<
"Error: Invalid TLS version (use 1.2 or 1.3)\n";
171 }
else if (arg ==
"--calling-ae" && i + 1 < argc) {
172 calling_ae = argv[++i];
173 if (calling_ae.length() > 16) {
174 std::cerr <<
"Error: Calling AE title exceeds 16 characters\n";
177 }
else if (arg ==
"--timeout" && i + 1 < argc) {
179 int val = std::stoi(argv[++i]);
181 std::cerr <<
"Error: timeout cannot be negative\n";
184 timeout = std::chrono::milliseconds{val};
185 }
catch (
const std::exception&) {
186 std::cerr <<
"Error: Invalid timeout value\n";
189 }
else if (arg[0] ==
'-') {
190 std::cerr <<
"Error: Unknown option '" << arg <<
"'\n";
196 if (!
tls.cert_path.empty() != !
tls.key_path.empty()) {
197 std::cerr <<
"Error: Both --cert and --key are required for mutual TLS\n";
207bool validate_tls_files(
const tls_options& tls) {
209 if (!
tls.cert_path.empty() && !std::filesystem::exists(
tls.cert_path)) {
210 std::cerr <<
"Error: Client certificate file not found: " <<
tls.cert_path <<
"\n";
215 if (!
tls.key_path.empty() && !std::filesystem::exists(
tls.key_path)) {
216 std::cerr <<
"Error: Client key file not found: " <<
tls.key_path <<
"\n";
221 if (!
tls.ca_path.empty() && !std::filesystem::exists(
tls.ca_path)) {
222 std::cerr <<
"Error: CA certificate file not found: " <<
tls.ca_path <<
"\n";
232bool perform_secure_echo(
233 const std::string& host,
235 const std::string& called_ae,
236 const std::string& calling_ae,
237 const tls_options& tls,
238 std::chrono::milliseconds timeout) {
245 std::cout <<
"Connecting securely to " << host <<
":" << port <<
"...\n";
246 std::cout <<
" Calling AE: " << calling_ae <<
"\n";
247 std::cout <<
" Called AE: " << called_ae <<
"\n";
248 std::cout <<
" TLS Version: " <<
tls.tls_version <<
"+\n";
249 std::cout <<
" Verify Server: " << (
tls.verify_server ?
"Yes" :
"No") <<
"\n";
250 if (!
tls.cert_path.empty()) {
251 std::cout <<
" Client Cert: " <<
tls.cert_path <<
"\n";
252 std::cout <<
" (Mutual TLS enabled)\n";
258 tls_cfg.enabled =
true;
259 tls_cfg.cert_path =
tls.cert_path;
260 tls_cfg.key_path =
tls.key_path;
261 tls_cfg.ca_path =
tls.ca_path;
262 tls_cfg.verify_peer =
tls.verify_server;
263 tls_cfg.min_version = (
tls.tls_version ==
"1.3")
264 ? tls_config::tls_version::v1_3
265 : tls_config::tls_version::v1_2;
268 auto tls_result = network_adapter::configure_tls(tls_cfg);
269 if (tls_result.is_err()) {
270 std::cerr <<
"TLS configuration error: " << tls_result.error().message <<
"\n";
284 std::string(verification_sop_class_uid),
286 "1.2.840.10008.1.2.1",
292 auto start_time = std::chrono::steady_clock::now();
296 auto connect_result = association::connect(host, port, config, timeout);
298 if (connect_result.is_err()) {
299 std::cerr <<
"Failed to establish secure association: "
300 << connect_result.error().message <<
"\n";
304 auto& assoc = connect_result.value();
305 auto connect_time = std::chrono::steady_clock::now();
306 auto connect_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
307 connect_time - start_time);
309 std::cout <<
"Secure association established in " << connect_duration.count() <<
" ms\n";
312 if (!assoc.has_accepted_context(verification_sop_class_uid)) {
313 std::cerr <<
"Error: Verification SOP Class not accepted by remote SCP\n";
319 auto context_id_opt = assoc.accepted_context_id(verification_sop_class_uid);
320 if (!context_id_opt) {
321 std::cerr <<
"Error: Could not get presentation context ID\n";
325 uint8_t context_id = *context_id_opt;
328 auto echo_rq = make_c_echo_rq(1, verification_sop_class_uid);
330 std::cout <<
"Sending C-ECHO request (TLS encrypted)...\n";
333 auto send_result = assoc.send_dimse(context_id, echo_rq);
334 if (send_result.is_err()) {
335 std::cerr <<
"Failed to send C-ECHO: " << send_result.error().message <<
"\n";
341 auto recv_result = assoc.receive_dimse(timeout);
342 if (recv_result.is_err()) {
343 std::cerr <<
"Failed to receive C-ECHO response: "
344 << recv_result.error().message <<
"\n";
349 auto& [recv_context_id, echo_rsp] = recv_result.value();
351 auto echo_time = std::chrono::steady_clock::now();
352 auto echo_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
353 echo_time - connect_time);
356 if (echo_rsp.command() != command_field::c_echo_rsp) {
357 std::cerr <<
"Error: Unexpected response (expected C-ECHO-RSP)\n";
362 auto status = echo_rsp.status();
363 if (status != status_success) {
364 std::cerr <<
"C-ECHO failed with status: 0x"
365 << std::hex << static_cast<uint16_t>(status) << std::dec <<
"\n";
366 (void)assoc.release();
370 std::cout <<
"C-ECHO successful! Round-trip time: " << echo_duration.count() <<
" ms\n";
373 std::cout <<
"Releasing secure association...\n";
374 auto release_result = assoc.release(timeout);
375 if (release_result.is_err()) {
376 std::cerr <<
"Warning: Release failed: " << release_result.error().message <<
"\n";
379 auto total_time = std::chrono::steady_clock::now();
380 auto total_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
381 total_time - start_time);
383 std::cout <<
"\nSummary:\n";
384 std::cout <<
" Remote AE: " << called_ae <<
"\n";
385 std::cout <<
" Security: TLS " <<
tls.tls_version <<
"+\n";
386 std::cout <<
" Connection time: " << connect_duration.count() <<
" ms\n";
387 std::cout <<
" Echo time: " << echo_duration.count() <<
" ms\n";
388 std::cout <<
" Total time: " << total_duration.count() <<
" ms\n";
389 std::cout <<
" Status: SUCCESS (SECURE)\n";
396int main(
int argc,
char* argv[]) {
398 ____ _____ ____ _ _ ____ _____ _____ ____ ____
399 / ___|| ____/ ___| | | | _ \| ____| | ____/ ___/ ___|
400 \___ \| _|| | | | | | |_) | _| | _|| | \___ \
401 ___) | |__| |___| |_| | _ <| |___ | |__| |___ ___) |
402 |____/|_____\____|\___/|_| \_\_____| |_____\____|____/
409 TLS-Secured DICOM Connectivity Test Client
414 std::string called_ae;
415 std::string calling_ae;
417 std::chrono::milliseconds timeout;
419 if (!parse_arguments(argc, argv, host, port, called_ae, calling_ae, tls, timeout)) {
420 print_usage(argv[0]);
425 if (!validate_tls_files(tls)) {
429 bool success = perform_secure_echo(host, port, called_ae, calling_ae, tls, timeout);
431 return success ? 0 : 1;
DICOM Association management per PS3.8.
DIMSE message encoding and decoding.
std::chrono::milliseconds default_timeout()
Default timeout for test operations (5s normal, 30s CI)
@ tls
TLS over TCP (RFC 5425) — Secure.
constexpr int timeout
Lock timeout exceeded.
Adapter for integrating network_system for DICOM protocol.
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
DICOM Verification SCP service (C-ECHO handler)