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(&params_, /*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);