Mercurial > hg > orthanc
changeset 4438:4a4e33c9082d
configuration options for DICOM TLS in Orthanc SCU
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Thu, 07 Jan 2021 16:53:35 +0100 |
parents | d9473bd5ed43 |
children | 5209a9ff6e38 |
files | NEWS OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.cpp OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.cpp OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.h OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h OrthancFramework/UnitTestsSources/JobsTests.cpp OrthancServer/Resources/Configuration.json OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp OrthancServer/Sources/main.cpp |
diffstat | 12 files changed, 434 insertions(+), 55 deletions(-) [+] |
line wrap: on
line diff
--- a/NEWS Wed Jan 06 17:27:28 2021 +0100 +++ b/NEWS Thu Jan 07 16:53:35 2021 +0100 @@ -1,7 +1,21 @@ Pending changes in the mainline =============================== +General +------- + +* Support of DICOM TLS +* New configuration options related to DICOM TLS: + - "DicomTlsEnabled" to enable DICOM TLS in Orthanc SCP + - "DicomTlsCertificate" to provide the TLS certificate to be used in both Orthanc SCU and SCP + - "DicomTlsPrivateKey" to provide the private key of the TLS certificate + - "DicomTlsTrustedCertificates" to provide the list of TLS certificates to be trusted by Orthanc + - "UseDicomTls" in "DicomModalities" to enable DICOM TLS in outgoing SCU on a per-modality basis * New command-line option: "--openapi" to write the OpenAPI documentation of the REST API to a file + +Maintenance +----------- + * Upgraded dependencies for static builds (notably on Windows): - jsoncpp 1.9.4
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp Wed Jan 06 17:27:28 2021 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociation.cpp Thu Jan 07 16:53:35 2021 +0100 @@ -276,16 +276,16 @@ CheckConnecting(parameters, ASC_createAssociationParameters(¶ms_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU)); #if ORTHANC_ENABLE_SSL == 1 - if (false) // TODO - Configuration option + if (parameters.GetRemoteModality().IsDicomTlsEnabled()) { try { assert(net_ != NULL && params_ != NULL); - // TODO - Configuration options - tls_.reset(Internals::InitializeDicomTls(net_, NET_REQUESTOR, - "/tmp/j/Client.key", "/tmp/j/Client.crt", "/tmp/j/Server.crt")); + tls_.reset(Internals::InitializeDicomTls(net_, NET_REQUESTOR, parameters.GetOwnPrivateKeyPath(), + parameters.GetOwnCertificatePath(), + parameters.GetTrustedCertificatesPath())); } catch (OrthancException&) {
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.cpp Wed Jan 06 17:27:28 2021 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.cpp Thu Jan 07 16:53:35 2021 +0100 @@ -27,13 +27,17 @@ #include "../Logging.h" #include "../OrthancException.h" #include "../SerializationToolbox.h" +#include "../SystemToolbox.h" #include "NetworkingCompatibility.h" #include <boost/thread/mutex.hpp> -// By default, the timeout for client DICOM connections is set to 10 seconds -static boost::mutex defaultTimeoutMutex_; -static uint32_t defaultTimeout_ = 10; +// By default, the default timeout for client DICOM connections is set to 10 seconds +static boost::mutex defaultConfigurationMutex_; +static uint32_t defaultTimeout_ = 10; +static std::string defaultOwnPrivateKeyPath_; +static std::string defaultOwnCertificatePath_; +static std::string defaultTrustedCertificatesPath_; namespace Orthanc @@ -50,25 +54,37 @@ uint32_t DicomAssociationParameters::GetDefaultTimeout() { - boost::mutex::scoped_lock lock(defaultTimeoutMutex_); + boost::mutex::scoped_lock lock(defaultConfigurationMutex_); return defaultTimeout_; } + void DicomAssociationParameters::SetDefaultParameters() + { + boost::mutex::scoped_lock lock(defaultConfigurationMutex_); + timeout_ = defaultTimeout_; + ownPrivateKeyPath_ = defaultOwnPrivateKeyPath_; + ownCertificatePath_ = defaultOwnCertificatePath_; + trustedCertificatesPath_ = defaultTrustedCertificatesPath_; + } + + DicomAssociationParameters::DicomAssociationParameters() : localAet_("ORTHANC"), - timeout_(GetDefaultTimeout()) + timeout_(0) // Will be set by SetDefaultParameters() { remote_.SetApplicationEntityTitle("ANY-SCP"); + SetDefaultParameters(); } DicomAssociationParameters::DicomAssociationParameters(const std::string& localAet, const RemoteModalityParameters& remote) : localAet_(localAet), - timeout_(GetDefaultTimeout()) + timeout_(0) // Will be set by SetDefaultParameters() { SetRemoteModality(remote); + SetDefaultParameters(); } const std::string &DicomAssociationParameters::GetLocalApplicationEntityTitle() const @@ -142,9 +158,67 @@ } + void DicomAssociationParameters::CheckDicomTlsConfiguration() const + { + if (!remote_.IsDicomTlsEnabled()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, "DICOM TLS is not enabled"); + } + else if (ownPrivateKeyPath_.empty()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "DICOM TLS - No path to the private key of the local certificate was provided"); + } + else if (ownCertificatePath_.empty()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "DICOM TLS - No path to the local certificate was provided"); + } + else if (trustedCertificatesPath_.empty()) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls, + "DICOM TLS - No path to the trusted remote certificates was provided"); + } + } + + void DicomAssociationParameters::SetOwnCertificatePath(const std::string& privateKeyPath, + const std::string& certificatePath) + { + ownPrivateKeyPath_ = privateKeyPath; + ownCertificatePath_ = certificatePath; + } + + void DicomAssociationParameters::SetTrustedCertificatesPath(const std::string& path) + { + trustedCertificatesPath_ = path; + } + + const std::string& DicomAssociationParameters::GetOwnPrivateKeyPath() const + { + CheckDicomTlsConfiguration(); + return ownPrivateKeyPath_; + } + + const std::string& DicomAssociationParameters::GetOwnCertificatePath() const + { + CheckDicomTlsConfiguration(); + return ownCertificatePath_; + } + + const std::string& DicomAssociationParameters::GetTrustedCertificatesPath() const + { + CheckDicomTlsConfiguration(); + return trustedCertificatesPath_; + } + + + static const char* const LOCAL_AET = "LocalAet"; static const char* const REMOTE = "Remote"; - static const char* const TIMEOUT = "Timeout"; // New in Orthanc in 1.7.0 + static const char* const TIMEOUT = "Timeout"; // New in Orthanc in 1.7.0 + static const char* const OWN_PRIVATE_KEY = "OwnPrivateKey"; // New in Orthanc 1.9.0 + static const char* const OWN_CERTIFICATE = "OwnCertificate"; // New in Orthanc 1.9.0 + static const char* const TRUSTED_CERTIFICATES = "TrustedCertificates"; // New in Orthanc 1.9.0 void DicomAssociationParameters::SerializeJob(Json::Value& target) const @@ -158,6 +232,34 @@ target[LOCAL_AET] = localAet_; remote_.Serialize(target[REMOTE], true /* force advanced format */); target[TIMEOUT] = timeout_; + + // Don't write the DICOM TLS parameters if they are not required + if (ownPrivateKeyPath_.empty()) + { + target.removeMember(OWN_PRIVATE_KEY); + } + else + { + target[OWN_PRIVATE_KEY] = ownPrivateKeyPath_; + } + + if (ownCertificatePath_.empty()) + { + target.removeMember(OWN_CERTIFICATE); + } + else + { + target[OWN_CERTIFICATE] = ownCertificatePath_; + } + + if (trustedCertificatesPath_.empty()) + { + target.removeMember(TRUSTED_CERTIFICATES); + } + else + { + target[TRUSTED_CERTIFICATES] = trustedCertificatesPath_; + } } } @@ -167,11 +269,43 @@ if (serialized.type() == Json::objectValue) { DicomAssociationParameters result; - + + if (!serialized.isMember(REMOTE)) + { + throw OrthancException(ErrorCode_BadFileFormat); + } + result.remote_ = RemoteModalityParameters(serialized[REMOTE]); result.localAet_ = SerializationToolbox::ReadString(serialized, LOCAL_AET); result.timeout_ = SerializationToolbox::ReadInteger(serialized, TIMEOUT, GetDefaultTimeout()); + if (serialized.isMember(OWN_PRIVATE_KEY)) + { + result.ownPrivateKeyPath_ = SerializationToolbox::ReadString(serialized, OWN_PRIVATE_KEY); + } + else + { + result.ownPrivateKeyPath_.clear(); + } + + if (serialized.isMember(OWN_CERTIFICATE)) + { + result.ownCertificatePath_ = SerializationToolbox::ReadString(serialized, OWN_CERTIFICATE); + } + else + { + result.ownCertificatePath_.clear(); + } + + if (serialized.isMember(TRUSTED_CERTIFICATES)) + { + result.trustedCertificatesPath_ = SerializationToolbox::ReadString(serialized, TRUSTED_CERTIFICATES); + } + else + { + result.trustedCertificatesPath_.clear(); + } + return result; } else @@ -187,8 +321,77 @@ << seconds << " seconds (0 = no timeout)"; { - boost::mutex::scoped_lock lock(defaultTimeoutMutex_); + boost::mutex::scoped_lock lock(defaultConfigurationMutex_); defaultTimeout_ = seconds; } } + + + void DicomAssociationParameters::SetDefaultOwnCertificatePath(const std::string& privateKeyPath, + const std::string& certificatePath) + { + if (!privateKeyPath.empty() && + !certificatePath.empty()) + { + CLOG(INFO, DICOM) << "Setting the default TLS certificate for DICOM SCU connections: " + << privateKeyPath << " (key), " << certificatePath << " (certificate)"; + + if (certificatePath.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, "No path to the default DICOM TLS certificate was provided"); + } + + if (privateKeyPath.empty()) + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "No path to the private key for the default DICOM TLS certificate was provided"); + } + + if (!SystemToolbox::IsRegularFile(privateKeyPath)) + { + throw OrthancException(ErrorCode_InexistentFile, "Inexistent file: " + privateKeyPath); + } + + if (!SystemToolbox::IsRegularFile(certificatePath)) + { + throw OrthancException(ErrorCode_InexistentFile, "Inexistent file: " + certificatePath); + } + + { + boost::mutex::scoped_lock lock(defaultConfigurationMutex_); + defaultOwnPrivateKeyPath_ = privateKeyPath; + defaultOwnCertificatePath_ = certificatePath; + } + } + else + { + boost::mutex::scoped_lock lock(defaultConfigurationMutex_); + defaultOwnPrivateKeyPath_.clear(); + defaultOwnCertificatePath_.clear(); + } + } + + + void DicomAssociationParameters::SetDefaultTrustedCertificatesPath(const std::string& path) + { + if (!path.empty()) + { + CLOG(INFO, DICOM) << "Setting the default trusted certificates for DICOM SCU connections: " << path; + + if (!SystemToolbox::IsRegularFile(path)) + { + throw OrthancException(ErrorCode_InexistentFile, "Inexistent file: " + path); + } + + { + boost::mutex::scoped_lock lock(defaultConfigurationMutex_); + defaultTrustedCertificatesPath_ = path; + } + } + else + { + boost::mutex::scoped_lock lock(defaultConfigurationMutex_); + defaultTrustedCertificatesPath_.clear(); + } + } }
--- a/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h Wed Jan 06 17:27:28 2021 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h Thu Jan 07 16:53:35 2021 +0100 @@ -36,9 +36,16 @@ std::string localAet_; RemoteModalityParameters remote_; uint32_t timeout_; + std::string ownPrivateKeyPath_; + std::string ownCertificatePath_; + std::string trustedCertificatesPath_; static void CheckHost(const std::string& host); + void SetDefaultParameters(); + + void CheckDicomTlsConfiguration() const; + public: DicomAssociationParameters(); @@ -70,12 +77,34 @@ bool HasTimeout() const; + // This corresponds to the "--enable-tls" or "+tls" argument of + // the command-line tools of DCMTK. Both files must be in the PEM format. + // The private key file must not be password-protected. + void SetOwnCertificatePath(const std::string& privateKeyPath, + const std::string& certificatePath); + + // This corresponds to the "--add-cert-file" or "+cf" argument of + // the command-line tools of DCMTK. The file must contain a list + // of PEM certificates. + void SetTrustedCertificatesPath(const std::string& path); + + const std::string& GetOwnPrivateKeyPath() const; + + const std::string& GetOwnCertificatePath() const; + + const std::string& GetTrustedCertificatesPath() const; + void SerializeJob(Json::Value& target) const; - + static DicomAssociationParameters UnserializeJob(const Json::Value& serialized); static void SetDefaultTimeout(uint32_t seconds); static uint32_t GetDefaultTimeout(); + + static void SetDefaultOwnCertificatePath(const std::string& privateKeyPath, + const std::string& certificatePath); + + static void SetDefaultTrustedCertificatesPath(const std::string& path); }; }
--- a/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.cpp Wed Jan 06 17:27:28 2021 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.cpp Thu Jan 07 16:53:35 2021 +0100 @@ -46,9 +46,9 @@ { DcmTLSTransportLayer* InitializeDicomTls(T_ASC_Network *network, T_ASC_NetworkRole role, - const std::string& ownPrivateKeyFile, - const std::string& ownCertificateFile, - const std::string& trustedCertificatesFile) + const std::string& ownPrivateKeyPath, + const std::string& ownCertificatePath, + const std::string& trustedCertificatesPath) { if (network == NULL) { @@ -61,22 +61,22 @@ throw OrthancException(ErrorCode_ParameterOutOfRange, "Unknown role"); } - if (!SystemToolbox::IsRegularFile(trustedCertificatesFile)) + if (!SystemToolbox::IsRegularFile(trustedCertificatesPath)) { throw OrthancException(ErrorCode_InexistentFile, "Cannot read file with trusted certificates for DICOM TLS: " + - trustedCertificatesFile); + trustedCertificatesPath); } - if (!SystemToolbox::IsRegularFile(ownPrivateKeyFile)) + if (!SystemToolbox::IsRegularFile(ownPrivateKeyPath)) { throw OrthancException(ErrorCode_InexistentFile, "Cannot read file with own private key for DICOM TLS: " + - ownPrivateKeyFile); + ownPrivateKeyPath); } - if (!SystemToolbox::IsRegularFile(ownCertificateFile)) + if (!SystemToolbox::IsRegularFile(ownCertificatePath)) { throw OrthancException(ErrorCode_InexistentFile, "Cannot read file with own certificate for DICOM TLS: " + - ownCertificateFile); + ownCertificatePath); } CLOG(INFO, DICOM) << "Initializing DICOM TLS for Orthanc " @@ -105,28 +105,28 @@ new DcmTLSTransportLayer(tmpRole /*opt_networkRole*/, NULL /*opt_readSeedFile*/, OFFalse /*initializeOpenSSL, done by Orthanc::Toolbox::InitializeOpenSsl()*/)); - if (tls->addTrustedCertificateFile(trustedCertificatesFile.c_str(), DCF_Filetype_PEM /*opt_keyFileFormat*/) != TCS_ok) + if (tls->addTrustedCertificateFile(trustedCertificatesPath.c_str(), DCF_Filetype_PEM /*opt_keyFileFormat*/) != TCS_ok) { throw OrthancException(ErrorCode_BadFileFormat, "Cannot parse PEM file with trusted certificates for DICOM TLS: " + - trustedCertificatesFile); + trustedCertificatesPath); } - if (tls->setPrivateKeyFile(ownPrivateKeyFile.c_str(), DCF_Filetype_PEM /*opt_keyFileFormat*/) != TCS_ok) + if (tls->setPrivateKeyFile(ownPrivateKeyPath.c_str(), DCF_Filetype_PEM /*opt_keyFileFormat*/) != TCS_ok) { throw OrthancException(ErrorCode_BadFileFormat, "Cannot parse PEM file with private key for DICOM TLS: " + - ownPrivateKeyFile); + ownPrivateKeyPath); } - if (tls->setCertificateFile(ownCertificateFile.c_str(), DCF_Filetype_PEM /*opt_keyFileFormat*/) != TCS_ok) + if (tls->setCertificateFile(ownCertificatePath.c_str(), DCF_Filetype_PEM /*opt_keyFileFormat*/) != TCS_ok) { throw OrthancException(ErrorCode_BadFileFormat, "Cannot parse PEM file with own certificate for DICOM TLS: " + - ownCertificateFile); + ownCertificatePath); } if (!tls->checkPrivateKeyMatchesCertificate()) { throw OrthancException(ErrorCode_BadFileFormat, "The private key doesn't match the own certificate: " + - ownPrivateKeyFile + " vs. " + ownCertificateFile); + ownPrivateKeyPath + " vs. " + ownCertificatePath); } #if DCMTK_VERSION_NUMBER >= 364
--- a/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.h Wed Jan 06 17:27:28 2021 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/Internals/DicomTls.h Thu Jan 07 16:53:35 2021 +0100 @@ -46,8 +46,8 @@ DcmTLSTransportLayer* InitializeDicomTls( T_ASC_Network *network, T_ASC_NetworkRole role, - const std::string& ownPrivateKeyFile, // This is the first argument of "+tls" option from DCMTK command-line tools - const std::string& ownCertificateFile, // This is the second argument of "+tls" option - const std::string& trustedCertificatesFile); // This is the "--add-cert-file" ("+cf") option + const std::string& ownPrivateKeyPath, // This is the first argument of "+tls" option from DCMTK command-line tools + const std::string& ownCertificatePath, // This is the second argument of "+tls" option + const std::string& trustedCertificatesPath); // This is the "--add-cert-file" ("+cf") option } }
--- a/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp Wed Jan 06 17:27:28 2021 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.cpp Thu Jan 07 16:53:35 2021 +0100 @@ -36,14 +36,15 @@ static const char* KEY_ALLOW_FIND = "AllowFind"; static const char* KEY_ALLOW_GET = "AllowGet"; static const char* KEY_ALLOW_MOVE = "AllowMove"; -static const char* KEY_ALLOW_STORE = "AllowStore"; static const char* KEY_ALLOW_N_ACTION = "AllowNAction"; static const char* KEY_ALLOW_N_EVENT_REPORT = "AllowEventReport"; static const char* KEY_ALLOW_STORAGE_COMMITMENT = "AllowStorageCommitment"; +static const char* KEY_ALLOW_STORE = "AllowStore"; static const char* KEY_ALLOW_TRANSCODING = "AllowTranscoding"; static const char* KEY_HOST = "Host"; static const char* KEY_MANUFACTURER = "Manufacturer"; static const char* KEY_PORT = "Port"; +static const char* KEY_USE_DICOM_TLS = "UseDicomTls"; namespace Orthanc @@ -62,6 +63,7 @@ allowNAction_ = true; // For storage commitment allowNEventReport_ = true; // For storage commitment allowTranscoding_ = true; + useDicomTls_ = false; } @@ -279,6 +281,11 @@ { allowTranscoding_ = SerializationToolbox::ReadBoolean(serialized, KEY_ALLOW_TRANSCODING); } + + if (serialized.isMember(KEY_USE_DICOM_TLS)) + { + useDicomTls_ = SerializationToolbox::ReadBoolean(serialized, KEY_USE_DICOM_TLS); + } } @@ -361,7 +368,8 @@ !allowMove_ || !allowNAction_ || !allowNEventReport_ || - !allowTranscoding_); + !allowTranscoding_ || + useDicomTls_); } @@ -384,6 +392,7 @@ target[KEY_ALLOW_N_ACTION] = allowNAction_; target[KEY_ALLOW_N_EVENT_REPORT] = allowNEventReport_; target[KEY_ALLOW_TRANSCODING] = allowTranscoding_; + target[KEY_USE_DICOM_TLS] = useDicomTls_; } else { @@ -424,4 +433,14 @@ { allowTranscoding_ = allowed; } + + bool RemoteModalityParameters::IsDicomTlsEnabled() const + { + return useDicomTls_; + } + + void RemoteModalityParameters::SetDicomTlsEnabled(bool enabled) + { + useDicomTls_ = enabled; + } }
--- a/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h Wed Jan 06 17:27:28 2021 +0100 +++ b/OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h Thu Jan 07 16:53:35 2021 +0100 @@ -45,6 +45,7 @@ bool allowNAction_; bool allowNEventReport_; bool allowTranscoding_; + bool useDicomTls_; void Clear(); @@ -95,5 +96,9 @@ bool IsTranscodingAllowed() const; void SetTranscodingAllowed(bool allowed); + + bool IsDicomTlsEnabled() const; + + void SetDicomTlsEnabled(bool enabled); }; }
--- a/OrthancFramework/UnitTestsSources/JobsTests.cpp Wed Jan 06 17:27:28 2021 +0100 +++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp Thu Jan 07 16:53:35 2021 +0100 @@ -28,6 +28,7 @@ #include <gtest/gtest.h> #include "../../OrthancFramework/Sources/Compatibility.h" +#include "../../OrthancFramework/Sources/DicomNetworking/DicomAssociationParameters.h" #include "../../OrthancFramework/Sources/DicomNetworking/RemoteModalityParameters.h" #include "../../OrthancFramework/Sources/DicomParsing/DicomModification.h" #include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h" @@ -1269,6 +1270,7 @@ ASSERT_FALSE(modality.IsAdvancedFormatNeeded()); modality.Serialize(s, false); ASSERT_EQ(Json::arrayValue, s.type()); + ASSERT_FALSE(modality.IsDicomTlsEnabled()); } { @@ -1285,19 +1287,20 @@ ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); ASSERT_TRUE(modality.IsTranscodingAllowed()); + ASSERT_FALSE(modality.IsDicomTlsEnabled()); } s = Json::nullValue; { RemoteModalityParameters modality; - ASSERT_FALSE(modality.IsAdvancedFormatNeeded()); ASSERT_THROW(modality.SetPortNumber(0), OrthancException); ASSERT_THROW(modality.SetPortNumber(65535), OrthancException); modality.SetApplicationEntityTitle("HELLO"); modality.SetHost("world"); modality.SetPortNumber(45); modality.SetManufacturer(ModalityManufacturer_GenericNoWildcardInDates); + ASSERT_FALSE(modality.IsAdvancedFormatNeeded()); modality.Serialize(s, true); ASSERT_EQ(Json::objectValue, s.type()); } @@ -1316,6 +1319,7 @@ ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); ASSERT_TRUE(modality.IsTranscodingAllowed()); + ASSERT_FALSE(modality.IsDicomTlsEnabled()); } s["Port"] = "46"; @@ -1383,6 +1387,7 @@ ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction)); ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); ASSERT_TRUE(modality.IsTranscodingAllowed()); + ASSERT_FALSE(modality.IsDicomTlsEnabled()); } { @@ -1393,6 +1398,7 @@ t["Host"] = "host"; t["Port"] = "104"; t["AllowTranscoding"] = false; + t["UseDicomTls"] = true; RemoteModalityParameters modality(t); ASSERT_TRUE(modality.IsAdvancedFormatNeeded()); @@ -1402,6 +1408,7 @@ ASSERT_FALSE(modality.IsRequestAllowed(DicomRequestType_NAction)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); ASSERT_FALSE(modality.IsTranscodingAllowed()); + ASSERT_TRUE(modality.IsDicomTlsEnabled()); } { @@ -1420,5 +1427,60 @@ ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NAction)); ASSERT_TRUE(modality.IsRequestAllowed(DicomRequestType_NEventReport)); ASSERT_TRUE(modality.IsTranscodingAllowed()); + ASSERT_FALSE(modality.IsDicomTlsEnabled()); } } + + + +TEST(JobsSerialization, DicomAssociationParameters) +{ + { + DicomAssociationParameters a; + + Json::Value v = Json::objectValue; + a.SerializeJob(v); + ASSERT_EQ(Json::objectValue, v.type()); + ASSERT_EQ("ORTHANC", v["LocalAet"].asString()); + ASSERT_EQ(DicomAssociationParameters::GetDefaultTimeout(), v["Timeout"].asInt()); + ASSERT_TRUE(v.isMember("Remote")); + + ASSERT_EQ(3u, v.getMemberNames().size()); + + DicomAssociationParameters b; + b.UnserializeJob(v); + ASSERT_EQ("ANY-SCP", b.GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ("127.0.0.1", b.GetRemoteModality().GetHost()); + ASSERT_EQ(104u, b.GetRemoteModality().GetPortNumber()); + ASSERT_EQ("ORTHANC", b.GetLocalApplicationEntityTitle()); + ASSERT_FALSE(b.GetRemoteModality().IsDicomTlsEnabled()); + } + + { + RemoteModalityParameters p; + p.SetApplicationEntityTitle("WORLD"); + p.SetPortNumber(4242); + p.SetHost("hello.world.com"); + p.SetDicomTlsEnabled(true); + + DicomAssociationParameters a("HELLO", p); + a.SetOwnCertificatePath("key", "crt"); + a.SetTrustedCertificatesPath("trusted"); + + Json::Value v = Json::objectValue; + a.SerializeJob(v); + + ASSERT_EQ(6u, v.getMemberNames().size()); + + DicomAssociationParameters b = DicomAssociationParameters::UnserializeJob(v); + + ASSERT_EQ("WORLD", b.GetRemoteModality().GetApplicationEntityTitle()); + ASSERT_EQ("hello.world.com", b.GetRemoteModality().GetHost()); + ASSERT_EQ(4242u, b.GetRemoteModality().GetPortNumber()); + ASSERT_EQ("HELLO", b.GetLocalApplicationEntityTitle()); + ASSERT_TRUE(b.GetRemoteModality().IsDicomTlsEnabled()); + ASSERT_EQ("key", b.GetOwnPrivateKeyPath()); + ASSERT_EQ("crt", b.GetOwnCertificatePath()); + ASSERT_EQ("trusted", b.GetTrustedCertificatesPath()); + } +}
--- a/OrthancServer/Resources/Configuration.json Wed Jan 06 17:27:28 2021 +0100 +++ b/OrthancServer/Resources/Configuration.json Thu Jan 07 16:53:35 2021 +0100 @@ -220,6 +220,56 @@ /** + * Security-related options for the DICOM connections (SCU/SCP) + **/ + + // Whether DICOM TLS is enabled in the Orthanc SCP (new in Orthanc 1.9.0) + "DicomTlsEnabled" : false, + + // Path to the TLS certificate file (in PEM format) to be used for + // both Orthanc SCP (incoming DICOM connections) and Orthanc SCU + // (outgoing DICOM connections). Note that contrarily to the + // "SslCertificate" option, the certificate and its private key must + // be split into two separate files. (new in Orthanc 1.9.0) + /** + "DicomTlsCertificate" : "orthanc.crt", + **/ + + // Path to the file containing the private key (in PEM format) that + // corresponds to the TLS certificate specified in option + // "DicomTlsCertificate". (new in Orthanc 1.9.0) + /** + "DicomTlsPrivateKey" : "orthanc.key", + **/ + + // Path to a file containing all the TLS certificates that Orthanc + // can trust, both for its SCP (incoming DICOM connections) and SCU + // (outgoing DICOM connections). This file must contain a sequence + // of PEM certificates. (new in Orthanc 1.9.0) + /** + "DicomTlsTrustedCertificates" : "trusted.crt", + **/ + + // Whether the Orthanc SCP allows incoming C-Echo requests, even + // from SCU modalities it does not know about (i.e. that are not + // listed in the "DicomModalities" option above). Orthanc 1.3.0 + // is the only version to behave as if this argument was set to "false". + "DicomAlwaysAllowEcho" : true, + + // Whether the Orthanc SCP allows incoming C-Store requests, even + // from SCU modalities it does not know about (i.e. that are not + // listed in the "DicomModalities" option above) + "DicomAlwaysAllowStore" : true, + + // Whether Orthanc checks the IP/hostname address of the remote + // modality initiating a DICOM connection (as listed in the + // "DicomModalities" option above). If this option is set to + // "false", Orthanc only checks the AET of the remote modality. + "DicomCheckModalityHost" : false, + + + + /** * Network topology **/ @@ -276,6 +326,10 @@ * By default, all "Allow*" options are true. * "AllowStorageCommitment" is actually an alias for * "AllowNAction" & "AllowEventReport". + * + * The "UseDicomTls" option specifies whether DICOM TLS should be + * used when opening a SCU connection from Orthanc to this remote + * modality. By default, DICOM TLS is not enabled. **/ //"untrusted" : { // "AET" : "ORTHANC", @@ -288,7 +342,8 @@ // "AllowMove" : false, // "AllowStore" : true, // "AllowStorageCommitment" : false, // new in 1.6.0 - // "AllowTranscoding" : true // new in 1.7.0 + // "AllowTranscoding" : true, // new in 1.7.0 + // "UseDicomTls" : false // new in 1.9.0 //} }, @@ -296,23 +351,6 @@ // instead of in this configuration file (new in Orthanc 1.5.0) "DicomModalitiesInDatabase" : false, - // Whether the Orthanc SCP allows incoming C-Echo requests, even - // from SCU modalities it does not know about (i.e. that are not - // listed in the "DicomModalities" option above). Orthanc 1.3.0 - // is the only version to behave as if this argument was set to "false". - "DicomAlwaysAllowEcho" : true, - - // Whether the Orthanc SCP allows incoming C-Store requests, even - // from SCU modalities it does not know about (i.e. that are not - // listed in the "DicomModalities" option above) - "DicomAlwaysAllowStore" : true, - - // Whether Orthanc checks the IP/hostname address of the remote - // modality initiating a DICOM connection (as listed in the - // "DicomModalities" option above). If this option is set to - // "false", Orthanc only checks the AET of the remote modality. - "DicomCheckModalityHost" : false, - // Whether the C-ECHO SCU is automatically followed by a C-FIND SCU, // while testing the connectivity from Orthanc to a remote DICOM // modality. This allows one to check that the remote modality does
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Wed Jan 06 17:27:28 2021 +0100 +++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp Thu Jan 07 16:53:35 2021 +0100 @@ -121,7 +121,9 @@ .SetRequestField("Port", RestApiCallDocumentation::Type_Number, "TCP port of the remote DICOM modality", true) .SetRequestField("Manufacturer", RestApiCallDocumentation::Type_String, "Manufacturer of the remote DICOM " - "modality (check configuration option `DicomModalities` for possible values", false); + "modality (check configuration option `DicomModalities` for possible values", false) + .SetRequestField("UseDicomTls", RestApiCallDocumentation::Type_Boolean, "Whether to use DICOM TLS " + "in the SCU connection initiated by Orthanc (new in Orthanc 1.9.0)", false); if (includePermissions) {
--- a/OrthancServer/Sources/main.cpp Wed Jan 06 17:27:28 2021 +0100 +++ b/OrthancServer/Sources/main.cpp Thu Jan 07 16:53:35 2021 +0100 @@ -1437,6 +1437,13 @@ { LOG(WARNING) << "Setting option \"JobsHistorySize\" to zero is not recommended"; } + + // Configuration of DICOM TLS (since Orthanc 1.9.0) + DicomAssociationParameters::SetDefaultOwnCertificatePath( + lock.GetConfiguration().GetStringParameter("DicomTlsPrivateKey", ""), + lock.GetConfiguration().GetStringParameter("DicomTlsCertificate", "")); + DicomAssociationParameters::SetDefaultTrustedCertificatesPath( + lock.GetConfiguration().GetStringParameter("DicomTlsTrustedCertificates", "")); } ServerContext context(database, storageArea, false /* not running unit tests */, maxCompletedJobs);