# HG changeset patch # User Sebastien Jodogne # Date 1465919603 -7200 # Node ID 9c9332e486ca4dac57ce673994b309fd09679e97 # Parent 300599489cab8ef6c6b0580b77c6fa1b525896e9 HTTPS client certificates can be associated with Orthanc peers to enhance security over Internet diff -r 300599489cab -r 9c9332e486ca Core/HttpClient.cpp --- 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; + } } diff -r 300599489cab -r 9c9332e486ca Core/HttpClient.h --- 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(); diff -r 300599489cab -r 9c9332e486ca NEWS --- 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) diff -r 300599489cab -r 9c9332e486ca OrthancServer/OrthancPeerParameters.cpp --- 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_); + } } } diff -r 300599489cab -r 9c9332e486ca OrthancServer/OrthancPeerParameters.h --- 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 #include @@ -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; }; } diff -r 300599489cab -r 9c9332e486ca OrthancServer/Scheduler/StorePeerCommand.cpp --- 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); diff -r 300599489cab -r 9c9332e486ca Plugins/Engine/OrthancPlugins.cpp --- 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 || diff -r 300599489cab -r 9c9332e486ca Plugins/Include/orthanc/OrthancCPlugin.h --- 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 NULL 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 NULL if no client certificate or if not using HTTPS). + * @param certificateKeyPassword Password to unlock the key of the client certificate + * (can be NULL 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(¶ms, 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, ¶ms); } diff -r 300599489cab -r 9c9332e486ca Resources/Configuration.json --- 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