changeset 2019:9c9332e486ca

HTTPS client certificates can be associated with Orthanc peers to enhance security over Internet
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 14 Jun 2016 17:53:23 +0200
parents 300599489cab
children a0bd8cd55da7
files Core/HttpClient.cpp Core/HttpClient.h NEWS OrthancServer/OrthancPeerParameters.cpp OrthancServer/OrthancPeerParameters.h OrthancServer/Scheduler/StorePeerCommand.cpp Plugins/Engine/OrthancPlugins.cpp Plugins/Include/orthanc/OrthancCPlugin.h Resources/Configuration.json
diffstat 9 files changed, 342 insertions(+), 37 deletions(-) [+]
line wrap: on
line diff
--- a/Core/HttpClient.cpp	Tue Jun 14 15:51:00 2016 +0200
+++ b/Core/HttpClient.cpp	Tue Jun 14 17:53:23 2016 +0200
@@ -349,6 +349,26 @@
       CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_PROXY, proxy_.c_str()));
     }
 
+    // Set the HTTPS client certificate
+    if (!clientCertificateFile_.empty())
+    {
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLCERTTYPE, "PEM"));
+      CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLCERT, clientCertificateFile_.c_str()));
+
+      if (!clientCertificateKeyPassword_.empty())
+      {
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_KEYPASSWD, clientCertificateKeyPassword_.c_str()));
+      }
+
+      // NB: If no "clientKeyFile_" is provided, the key must be
+      // prepended to the certificate file
+      if (!clientCertificateKeyFile_.empty())
+      {
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEYTYPE, "PEM"));
+        CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSLKEY, clientCertificateKeyFile_.c_str()));
+      }
+    }
+
     switch (method_)
     {
     case HttpMethod_Get:
@@ -530,4 +550,32 @@
       ThrowException(GetLastStatus());
     }
   }
+
+
+  void HttpClient::SetClientCertificate(const std::string& certificateFile,
+                                        const std::string& certificateKeyFile,
+                                        const std::string& certificateKeyPassword)
+  {
+    if (certificateFile.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if (!Toolbox::IsRegularFile(certificateFile))
+    {
+      LOG(ERROR) << "Cannot open certificate file: " << certificateFile;
+      throw OrthancException(ErrorCode_InexistentFile);
+    }
+
+    if (!certificateKeyFile.empty() && 
+        !Toolbox::IsRegularFile(certificateKeyFile))
+    {
+      LOG(ERROR) << "Cannot open key file: " << certificateKeyFile;
+      throw OrthancException(ErrorCode_InexistentFile);
+    }
+
+    clientCertificateFile_ = certificateFile;
+    clientCertificateKeyFile_ = certificateKeyFile;
+    clientCertificateKeyPassword_ = certificateKeyPassword;
+  }
 }
--- a/Core/HttpClient.h	Tue Jun 14 15:51:00 2016 +0200
+++ b/Core/HttpClient.h	Tue Jun 14 17:53:23 2016 +0200
@@ -58,6 +58,9 @@
     std::string proxy_;
     bool verifyPeers_;
     std::string caCertificates_;
+    std::string clientCertificateFile_;
+    std::string clientCertificateKeyFile_;
+    std::string clientCertificateKeyPassword_;
 
     void Setup();
 
@@ -168,6 +171,25 @@
       return caCertificates_;
     }
 
+    void SetClientCertificate(const std::string& certificateFile,
+                              const std::string& certificateKeyFile,
+                              const std::string& certificateKeyPassword);
+
+    const std::string& GetClientCertificateFile() const
+    {
+      return clientCertificateFile_;
+    }
+
+    const std::string& GetClientCertificateKeyFile() const
+    {
+      return clientCertificateKeyFile_;
+    }
+
+    const std::string& GetClientCertificateKeyPassword() const
+    {
+      return clientCertificateKeyPassword_;
+    }
+
     static void GlobalInitialize();
   
     static void GlobalFinalize();
--- a/NEWS	Tue Jun 14 15:51:00 2016 +0200
+++ b/NEWS	Tue Jun 14 17:53:23 2016 +0200
@@ -1,6 +1,12 @@
 Pending changes in the mainline
 ===============================
 
+General
+-------
+
+* HTTPS client certificates can be associated with Orthanc peers to enhance security over Internet
+* New option "--logfile" to output the Orthanc log to the given file
+* Support of SIGHUP signal (restart Orthanc only if the configuration files have changed)
 
 REST API
 --------
@@ -43,8 +49,6 @@
 Maintenance
 -----------
 
-* New option "--logfile" to output the Orthanc log to the given file
-* Support of SIGHUP signal (restart Orthanc only if the configuration files have changed)
 * New logo of Orthanc
 * Fix issue 11 (is_regular_file() fails for FILE_ATTRIBUTE_REPARSE_POINT)
 * Fix issue 16 ("Limit" parameter error in REST API /tools/find method)
--- a/OrthancServer/OrthancPeerParameters.cpp	Tue Jun 14 15:51:00 2016 +0200
+++ b/OrthancServer/OrthancPeerParameters.cpp	Tue Jun 14 17:53:23 2016 +0200
@@ -33,39 +33,152 @@
 #include "PrecompiledHeadersServer.h"
 #include "OrthancPeerParameters.h"
 
+#include "../Core/Logging.h"
+#include "../Core/Toolbox.h"
 #include "../Core/OrthancException.h"
 
 namespace Orthanc
 {
   OrthancPeerParameters::OrthancPeerParameters() : 
+    advancedFormat_(false),
     url_("http://localhost:8042/")
   {
   }
 
 
+  void OrthancPeerParameters::SetClientCertificate(const std::string& certificateFile,
+                                                   const std::string& certificateKeyFile,
+                                                   const std::string& certificateKeyPassword)
+  {
+    if (certificateFile.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if (!Toolbox::IsRegularFile(certificateFile))
+    {
+      LOG(ERROR) << "Cannot open certificate file: " << certificateFile;
+      throw OrthancException(ErrorCode_InexistentFile);
+    }
+
+    if (!certificateKeyFile.empty() && 
+        !Toolbox::IsRegularFile(certificateKeyFile))
+    {
+      LOG(ERROR) << "Cannot open key file: " << certificateKeyFile;
+      throw OrthancException(ErrorCode_InexistentFile);
+    }
+
+    advancedFormat_ = true;
+    certificateFile_ = certificateFile;
+    certificateKeyFile_ = certificateKeyFile;
+    certificateKeyPassword_ = certificateKeyPassword;
+  }
+
+
+  static void AddTrailingSlash(std::string& url)
+  {
+    if (url.size() != 0 && 
+        url[url.size() - 1] != '/')
+    {
+      url += '/';
+    }
+  }
+
+
+  void OrthancPeerParameters::FromJsonArray(const Json::Value& peer)
+  {
+    assert(peer.isArray());
+
+    advancedFormat_ = false;
+
+    if (peer.size() != 1 && 
+        peer.size() != 3)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    std::string url = peer.get(0u, "").asString();
+    if (url.empty())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    AddTrailingSlash(url);
+    SetUrl(url);
+
+    if (peer.size() == 1)
+    {
+      SetUsername("");
+      SetPassword("");
+    }
+    else if (peer.size() == 3)
+    {
+      SetUsername(peer.get(1u, "").asString());
+      SetPassword(peer.get(2u, "").asString());
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  static std::string GetStringMember(const Json::Value& peer,
+                                     const std::string& key,
+                                     const std::string& defaultValue)
+  {
+    if (!peer.isMember(key))
+    {
+      return defaultValue;
+    }
+    else if (peer[key].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+    else
+    {
+      return peer[key].asString();
+    }
+  }
+
+
+  void OrthancPeerParameters::FromJsonObject(const Json::Value& peer)
+  {
+    assert(peer.isObject());
+    advancedFormat_ = true;
+
+    std::string url = GetStringMember(peer, "Url", "");
+    if (url.empty())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    AddTrailingSlash(url);
+    SetUrl(url);
+
+    SetUsername(GetStringMember(peer, "Username", ""));
+    SetPassword(GetStringMember(peer, "Password", ""));
+
+    if (peer.isMember("CertificateFile"))
+    {
+      SetClientCertificate(GetStringMember(peer, "CertificateFile", ""),
+                           GetStringMember(peer, "CertificateKeyFile", ""),
+                           GetStringMember(peer, "CertificateKeyPassword", ""));
+    }
+  }
+
+
   void OrthancPeerParameters::FromJson(const Json::Value& peer)
   {
-    if (!peer.isArray() ||
-        (peer.size() != 1 && peer.size() != 3))
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-
-    std::string url;
-
     try
     {
-      url = peer.get(0u, "").asString();
-
-      if (peer.size() == 1)
+      if (peer.isArray())
       {
-        SetUsername("");
-        SetPassword("");
+        FromJsonArray(peer);
       }
-      else if (peer.size() == 3)
+      else if (peer.isObject())
       {
-        SetUsername(peer.get(1u, "").asString());
-        SetPassword(peer.get(2u, "").asString());
+        FromJsonObject(peer);
       }
       else
       {
@@ -76,21 +189,65 @@
     {
       throw OrthancException(ErrorCode_BadFileFormat);
     }
-
-    if (url.size() != 0 && url[url.size() - 1] != '/')
-    {
-      url += '/';
-    }
-
-    SetUrl(url);
   }
 
 
   void OrthancPeerParameters::ToJson(Json::Value& value) const
   {
-    value = Json::arrayValue;
-    value.append(GetUrl());
-    value.append(GetUsername());
-    value.append(GetPassword());
+    if (advancedFormat_)
+    {
+      value = Json::objectValue;
+      value["Url"] = url_;
+
+      if (!username_.empty() ||
+          !password_.empty())
+      {
+        value["Username"] = username_;
+        value["Password"] = password_;
+      }
+
+      if (!certificateFile_.empty())
+      {
+        value["CertificateFile"] = certificateFile_;
+      }
+
+      if (!certificateKeyFile_.empty())
+      {
+        value["CertificateKeyFile"] = certificateKeyFile_;
+      }
+
+      if (!certificateKeyPassword_.empty())
+      {
+        value["CertificateKeyPassword"] = certificateKeyPassword_;
+      }
+    }
+    else
+    {
+      value = Json::arrayValue;
+      value.append(url_);
+
+      if (!username_.empty() ||
+          !password_.empty())
+      {
+        value.append(username_);
+        value.append(password_);
+      }
+    }
+  }
+
+
+  void OrthancPeerParameters::ConfigureClient(HttpClient& client) const
+  {
+    if (username_.size() != 0 && 
+        password_.size() != 0)
+    {
+      client.SetCredentials(username_.c_str(), 
+                            password_.c_str());
+    }
+
+    if (!GetCertificateFile().empty())
+    {
+      client.SetClientCertificate(certificateFile_, certificateKeyFile_, certificateKeyPassword_);
+    }
   }
 }
--- a/OrthancServer/OrthancPeerParameters.h	Tue Jun 14 15:51:00 2016 +0200
+++ b/OrthancServer/OrthancPeerParameters.h	Tue Jun 14 17:53:23 2016 +0200
@@ -32,6 +32,8 @@
 
 #pragma once
 
+#include "../Core/HttpClient.h"
+
 #include <string>
 #include <json/json.h>
 
@@ -40,9 +42,17 @@
   class OrthancPeerParameters
   {
   private:
+    bool        advancedFormat_;
     std::string url_;
     std::string username_;
     std::string password_;
+    std::string certificateFile_;
+    std::string certificateKeyFile_;
+    std::string certificateKeyPassword_;
+
+    void FromJsonArray(const Json::Value& peer);
+
+    void FromJsonObject(const Json::Value& peer);
 
   public:
     OrthancPeerParameters();
@@ -77,8 +87,29 @@
       password_ = password;
     }
 
+    void SetClientCertificate(const std::string& certificateFile,
+                              const std::string& certificateKeyFile,
+                              const std::string& certificateKeyPassword);
+
+    const std::string& GetCertificateFile() const
+    {
+      return certificateFile_;
+    }
+
+    const std::string& GetCertificateKeyFile() const
+    {
+      return certificateKeyFile_;
+    }
+
+    const std::string& GetCertificateKeyPassword() const
+    {
+      return certificateKeyPassword_;
+    }
+
     void FromJson(const Json::Value& peer);
 
     void ToJson(Json::Value& value) const;
+
+    void ConfigureClient(HttpClient& client) const;
   };
 }
--- a/OrthancServer/Scheduler/StorePeerCommand.cpp	Tue Jun 14 15:51:00 2016 +0200
+++ b/OrthancServer/Scheduler/StorePeerCommand.cpp	Tue Jun 14 17:53:23 2016 +0200
@@ -52,12 +52,7 @@
   {
     // Configure the HTTP client
     HttpClient client;
-    if (peer_.GetUsername().size() != 0 && 
-        peer_.GetPassword().size() != 0)
-    {
-      client.SetCredentials(peer_.GetUsername().c_str(), 
-                            peer_.GetPassword().c_str());
-    }
+    peer_.ConfigureClient(client);
 
     client.SetUrl(peer_.GetUrl() + "instances");
     client.SetMethod(HttpMethod_Post);
--- a/Plugins/Engine/OrthancPlugins.cpp	Tue Jun 14 15:51:00 2016 +0200
+++ b/Plugins/Engine/OrthancPlugins.cpp	Tue Jun 14 17:53:23 2016 +0200
@@ -1793,6 +1793,24 @@
       client.SetCredentials(p.username, p.password);
     }
 
+    if (p.certificateFile != NULL)
+    {
+      std::string certificate(p.certificateFile);
+      std::string key, password;
+
+      if (p.certificateKeyFile)
+      {
+        key.assign(p.certificateKeyFile);
+      }
+
+      if (p.certificateKeyPassword)
+      {
+        password.assign(p.certificateKeyPassword);
+      }
+
+      client.SetClientCertificate(certificate, key, password);
+    }
+
     for (uint32_t i = 0; i < p.headersCount; i++)
     {
       if (p.headersKeys[i] == NULL ||
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Tue Jun 14 15:51:00 2016 +0200
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Tue Jun 14 17:53:23 2016 +0200
@@ -4943,6 +4943,9 @@
     const char*                 username;
     const char*                 password;
     uint32_t                    timeout;
+    const char*                 certificateFile;
+    const char*                 certificateKeyFile;
+    const char*                 certificateKeyPassword;
   } _OrthancPluginCallHttpClient2;
 
 
@@ -4966,6 +4969,12 @@
    * @param body The body of the POST request.
    * @param bodySize The size of the body.
    * @param timeout Timeout in seconds (0 for default timeout).
+   * @param certificateFile Path to the client certificate for HTTPS, in PEM format
+   * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS).
+   * @param certificateKeyFile Path to the key of the client certificate for HTTPS, in PEM format
+   * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS).
+   * @param certificateKeyPassword Password to unlock the key of the client certificate 
+   * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS).
    * @return 0 if success, or the error code if failure.
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpClient(
@@ -4981,7 +4990,10 @@
     uint32_t                    bodySize,
     const char*                 username,
     const char*                 password,
-    uint32_t                    timeout)
+    uint32_t                    timeout,
+    const char*                 certificateFile,
+    const char*                 certificateKeyFile,
+    const char*                 certificateKeyPassword)
   {
     _OrthancPluginCallHttpClient2 params;
     memset(&params, 0, sizeof(params));
@@ -4998,6 +5010,9 @@
     params.username = username;
     params.password = password;
     params.timeout = timeout;
+    params.certificateFile = certificateFile;
+    params.certificateKeyFile = certificateKeyFile;
+    params.certificateKeyPassword = certificateKeyPassword;
 
     return context->InvokeService(context, _OrthancPluginService_CallHttpClient2, &params);
   }
--- a/Resources/Configuration.json	Tue Jun 14 15:51:00 2016 +0200
+++ b/Resources/Configuration.json	Tue Jun 14 17:53:23 2016 +0200
@@ -118,7 +118,8 @@
   // Whether or not SSL is enabled
   "SslEnabled" : false,
 
-  // Path to the SSL certificate (meaningful only if SSL is enabled)
+  // Path to the SSL certificate in the PEM format (meaningful only if
+  // SSL is enabled)
   "SslCertificate" : "certificate.pem",
 
   // Whether or not the password protection is enabled
@@ -166,6 +167,20 @@
      **/
     // "peer"  : [ "http://localhost:8043/", "alice", "alicePassword" ]
     // "peer2" : [ "http://localhost:8044/" ]
+
+    /**
+     * This is another, more advanced format to define Orthanc
+     * peers. It notably allows to specify a HTTPS client certificate
+     * in the PEM format, as in the "--cert" option of curl.
+     **/
+    // "peer" : {
+    //   "Url" : "http://localhost:8043/",
+    //   "Username" : "alice",
+    //   "Password" : "alicePassword",
+    //   "CertificateFile" : "client.crt",
+    //   "CertificateKeyFile" : "client.key",
+    //   "CertificateKeyPassword" : "certpass"
+    // }
   },
 
   // Parameters of the HTTP proxy to be used by Orthanc. If set to the