PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
secure_dicom/secure_echo_scu.cpp

A secure DICOM client demonstrating TLS 1.2/1.3 configuration for encrypted DICOM communication. This sample shows proper certificate handling for both server verification and mutual TLS (client cert).

See also
Issue #110 - TLS DICOM Connection Sample
DICOM PS3.15 - Security and System Management Profiles
DICOM PS3.7 Section 9.1 - C-ECHO Service

Usage: secure_echo_scu <host> <port> <called_ae> –cert <cert> –key <key> [options]

Example: secure_echo_scu localhost 2762 PACS_SCP –cert client.crt –key client.key –ca ca.crt

#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <iostream>
#include <string>
namespace {
constexpr const char* default_calling_ae = "SECURE_SCU";
constexpr auto default_timeout = std::chrono::milliseconds{30000};
struct tls_options {
std::filesystem::path cert_path;
std::filesystem::path key_path;
std::filesystem::path ca_path;
bool verify_server = true;
std::string tls_version = "1.2";
};
void print_usage(const char* program_name) {
std::cout << R"(
Secure Echo SCU - TLS-secured DICOM Connectivity Test Client
Usage: )" << program_name << R"( <host> <port> <called_ae> [--cert <file> --key <file>] [options]
Arguments:
host Remote host address (IP or hostname)
port Remote port number (typically 2762 for DICOM TLS)
called_ae Called AE Title (remote SCP's AE title)
TLS Options:
--cert <file> Client certificate file for mutual TLS (PEM format)
--key <file> Client private key file for mutual TLS (PEM format)
--ca <file> CA certificate for server verification (PEM format)
--no-verify Disable server certificate verification (not recommended)
--tls-version <ver> Minimum TLS version: 1.2 or 1.3 (default: 1.2)
Other Options:
--calling-ae <ae> Calling AE Title (default: SECURE_SCU)
--timeout <ms> Operation timeout in milliseconds (default: 30000)
--help Show this help message
Examples:
# Basic TLS connection (server cert verification only)
)" << program_name << R"( localhost 2762 PACS_SCP --ca ca.crt
# Mutual TLS (client and server certificates)
)" << program_name << R"( localhost 2762 PACS_SCP --cert client.crt --key client.key --ca ca.crt
# TLS 1.3 with custom AE title
)" << program_name << R"( 192.168.1.100 2762 REMOTE_PACS --ca ca.crt --tls-version 1.3 --calling-ae MY_SCANNER
Notes:
- Standard DICOM TLS port is 2762
- For production, always verify server certificates (avoid --no-verify)
- Mutual TLS requires both --cert and --key
Exit Codes:
0 Success - Echo response received
1 Error - Connection, TLS, or echo failed
)";
}
bool parse_arguments(
int argc,
char* argv[],
std::string& host,
uint16_t& port,
std::string& called_ae,
std::string& calling_ae,
tls_options& tls,
std::chrono::milliseconds& timeout) {
if (argc < 4) {
return false;
}
// Check for help flag
for (int i = 1; i < argc; ++i) {
if (std::string(argv[i]) == "--help" || std::string(argv[i]) == "-h") {
return false;
}
}
// Parse required arguments
host = argv[1];
// Parse port
try {
int port_int = std::stoi(argv[2]);
if (port_int < 1 || port_int > 65535) {
std::cerr << "Error: Port must be between 1 and 65535\n";
return false;
}
port = static_cast<uint16_t>(port_int);
} catch (const std::exception&) {
std::cerr << "Error: Invalid port number '" << argv[2] << "'\n";
return false;
}
called_ae = argv[3];
if (called_ae.length() > 16) {
std::cerr << "Error: Called AE title exceeds 16 characters\n";
return false;
}
// Default values
calling_ae = default_calling_ae;
timeout = default_timeout;
tls.verify_server = true;
tls.tls_version = "1.2";
// Parse options
for (int i = 4; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--cert" && i + 1 < argc) {
tls.cert_path = argv[++i];
} else if (arg == "--key" && i + 1 < argc) {
tls.key_path = argv[++i];
} else if (arg == "--ca" && i + 1 < argc) {
tls.ca_path = argv[++i];
} else if (arg == "--no-verify") {
tls.verify_server = false;
std::cerr << "Warning: Server certificate verification disabled. "
<< "This is not recommended for production.\n";
} else if (arg == "--tls-version" && i + 1 < argc) {
tls.tls_version = argv[++i];
if (tls.tls_version != "1.2" && tls.tls_version != "1.3") {
std::cerr << "Error: Invalid TLS version (use 1.2 or 1.3)\n";
return false;
}
} else if (arg == "--calling-ae" && i + 1 < argc) {
calling_ae = argv[++i];
if (calling_ae.length() > 16) {
std::cerr << "Error: Calling AE title exceeds 16 characters\n";
return false;
}
} else if (arg == "--timeout" && i + 1 < argc) {
try {
int val = std::stoi(argv[++i]);
if (val < 0) {
std::cerr << "Error: timeout cannot be negative\n";
return false;
}
timeout = std::chrono::milliseconds{val};
} catch (const std::exception&) {
std::cerr << "Error: Invalid timeout value\n";
return false;
}
} else if (arg[0] == '-') {
std::cerr << "Error: Unknown option '" << arg << "'\n";
return false;
}
}
// Validate mutual TLS configuration
if (!tls.cert_path.empty() != !tls.key_path.empty()) {
std::cerr << "Error: Both --cert and --key are required for mutual TLS\n";
return false;
}
return true;
}
bool validate_tls_files(const tls_options& tls) {
// Check client certificate file if specified
if (!tls.cert_path.empty() && !std::filesystem::exists(tls.cert_path)) {
std::cerr << "Error: Client certificate file not found: " << tls.cert_path << "\n";
return false;
}
// Check client key file if specified
if (!tls.key_path.empty() && !std::filesystem::exists(tls.key_path)) {
std::cerr << "Error: Client key file not found: " << tls.key_path << "\n";
return false;
}
// Check CA file if specified
if (!tls.ca_path.empty() && !std::filesystem::exists(tls.ca_path)) {
std::cerr << "Error: CA certificate file not found: " << tls.ca_path << "\n";
return false;
}
return true;
}
bool perform_secure_echo(
const std::string& host,
uint16_t port,
const std::string& called_ae,
const std::string& calling_ae,
const tls_options& tls,
std::chrono::milliseconds timeout) {
using namespace kcenon::pacs::network;
using namespace kcenon::pacs::services;
using namespace kcenon::pacs::integration;
std::cout << "Connecting securely to " << host << ":" << port << "...\n";
std::cout << " Calling AE: " << calling_ae << "\n";
std::cout << " Called AE: " << called_ae << "\n";
std::cout << " TLS Version: " << tls.tls_version << "+\n";
std::cout << " Verify Server: " << (tls.verify_server ? "Yes" : "No") << "\n";
if (!tls.cert_path.empty()) {
std::cout << " Client Cert: " << tls.cert_path << "\n";
std::cout << " (Mutual TLS enabled)\n";
}
std::cout << "\n";
// Configure TLS
tls_config tls_cfg;
tls_cfg.enabled = true;
tls_cfg.cert_path = tls.cert_path;
tls_cfg.key_path = tls.key_path;
tls_cfg.ca_path = tls.ca_path;
tls_cfg.verify_peer = tls.verify_server;
tls_cfg.min_version = (tls.tls_version == "1.3")
? tls_config::tls_version::v1_3
: tls_config::tls_version::v1_2;
// Validate TLS configuration
auto tls_result = network_adapter::configure_tls(tls_cfg);
if (tls_result.is_err()) {
std::cerr << "TLS configuration error: " << tls_result.error().message << "\n";
return false;
}
// Configure association
config.calling_ae_title = calling_ae;
config.called_ae_title = called_ae;
config.implementation_class_uid = "1.2.826.0.1.3680043.2.1545.1";
config.implementation_version_name = "SECURE_SCU_001";
// Propose Verification SOP Class with Explicit VR Little Endian
config.proposed_contexts.push_back({
1, // Context ID (must be odd: 1, 3, 5, ...)
std::string(verification_sop_class_uid),
{
"1.2.840.10008.1.2.1", // Explicit VR Little Endian
"1.2.840.10008.1.2" // Implicit VR Little Endian
}
});
// Establish secure association
auto start_time = std::chrono::steady_clock::now();
// Note: In a full implementation, the connect method would accept TLS config
// For this sample, we demonstrate the TLS configuration pattern
auto connect_result = association::connect(host, port, config, timeout);
if (connect_result.is_err()) {
std::cerr << "Failed to establish secure association: "
<< connect_result.error().message << "\n";
return false;
}
auto& assoc = connect_result.value();
auto connect_time = std::chrono::steady_clock::now();
auto connect_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
connect_time - start_time);
std::cout << "Secure association established in " << connect_duration.count() << " ms\n";
// Verify we have an accepted context for Verification
if (!assoc.has_accepted_context(verification_sop_class_uid)) {
std::cerr << "Error: Verification SOP Class not accepted by remote SCP\n";
assoc.abort();
return false;
}
// Get the accepted context ID
auto context_id_opt = assoc.accepted_context_id(verification_sop_class_uid);
if (!context_id_opt) {
std::cerr << "Error: Could not get presentation context ID\n";
assoc.abort();
return false;
}
uint8_t context_id = *context_id_opt;
// Create C-ECHO request
auto echo_rq = make_c_echo_rq(1, verification_sop_class_uid);
std::cout << "Sending C-ECHO request (TLS encrypted)...\n";
// Send C-ECHO request
auto send_result = assoc.send_dimse(context_id, echo_rq);
if (send_result.is_err()) {
std::cerr << "Failed to send C-ECHO: " << send_result.error().message << "\n";
assoc.abort();
return false;
}
// Receive C-ECHO response
auto recv_result = assoc.receive_dimse(timeout);
if (recv_result.is_err()) {
std::cerr << "Failed to receive C-ECHO response: "
<< recv_result.error().message << "\n";
assoc.abort();
return false;
}
auto& [recv_context_id, echo_rsp] = recv_result.value();
auto echo_time = std::chrono::steady_clock::now();
auto echo_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
echo_time - connect_time);
// Check response
if (echo_rsp.command() != command_field::c_echo_rsp) {
std::cerr << "Error: Unexpected response (expected C-ECHO-RSP)\n";
assoc.abort();
return false;
}
auto status = echo_rsp.status();
if (status != status_success) {
std::cerr << "C-ECHO failed with status: 0x"
<< std::hex << static_cast<uint16_t>(status) << std::dec << "\n";
(void)assoc.release();
return false;
}
std::cout << "C-ECHO successful! Round-trip time: " << echo_duration.count() << " ms\n";
// Release association gracefully
std::cout << "Releasing secure association...\n";
auto release_result = assoc.release(timeout);
if (release_result.is_err()) {
std::cerr << "Warning: Release failed: " << release_result.error().message << "\n";
}
auto total_time = std::chrono::steady_clock::now();
auto total_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
total_time - start_time);
std::cout << "\nSummary:\n";
std::cout << " Remote AE: " << called_ae << "\n";
std::cout << " Security: TLS " << tls.tls_version << "+\n";
std::cout << " Connection time: " << connect_duration.count() << " ms\n";
std::cout << " Echo time: " << echo_duration.count() << " ms\n";
std::cout << " Total time: " << total_duration.count() << " ms\n";
std::cout << " Status: SUCCESS (SECURE)\n";
return true;
}
} // namespace
int main(int argc, char* argv[]) {
std::cout << R"(
____ _____ ____ _ _ ____ _____ _____ ____ ____
/ ___|| ____/ ___| | | | _ \| ____| | ____/ ___/ ___|
\___ \| _|| | | | | | |_) | _| | _|| | \___ \
___) | |__| |___| |_| | _ <| |___ | |__| |___ ___) |
|____/|_____\____|\___/|_| \_\_____| |_____\____|____/
____ ____ _ _
/ ___| / ___| | | |
\___ \| | | | | |
___) | |___| |_| |
|____/ \____|\___/
TLS-Secured DICOM Connectivity Test Client
)" << "\n";
std::string host;
uint16_t port = 0;
std::string called_ae;
std::string calling_ae;
tls_options tls;
std::chrono::milliseconds timeout;
if (!parse_arguments(argc, argv, host, port, called_ae, calling_ae, tls, timeout)) {
print_usage(argv[0]);
return 1;
}
// Validate TLS files if specified
if (!validate_tls_files(tls)) {
return 1;
}
bool success = perform_secure_echo(host, port, called_ae, calling_ae, tls, timeout);
return success ? 0 : 1;
}
DICOM Association management per PS3.8.
DIMSE message encoding and decoding.
int main()
Definition main.cpp:84
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::vector< proposed_presentation_context > proposed_contexts
DICOM Verification SCP service (C-ECHO handler)