# HG changeset patch # User Sebastien Jodogne # Date 1439462552 -7200 # Node ID 95b3b0260240071d394395103fcdbf9a67a622ea # Parent 0011cc99443c01008fab2fe49943b095c29f762b Options to validate peers against CA certificates in HTTPS requests diff -r 0011cc99443c -r 95b3b0260240 Core/HttpClient.cpp --- a/Core/HttpClient.cpp Wed Aug 12 17:52:10 2015 +0200 +++ b/Core/HttpClient.cpp Thu Aug 13 12:42:32 2015 +0200 @@ -42,8 +42,8 @@ #include -static std::string cacert_; -static bool httpsVerifyPeers_ = true; +static std::string globalCACertificates_; +static bool globalVerifyPeers_ = true; extern "C" { @@ -131,18 +131,6 @@ CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HEADER, 0)); CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_FOLLOWLOCATION, 1)); -#if ORTHANC_SSL_ENABLED == 1 - if (httpsVerifyPeers_) - { - CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CAINFO, cacert_.c_str())); - CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYPEER, 1)); - } - else - { - CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYPEER, 0)); - } -#endif - // This fixes the "longjmp causes uninitialized stack frame" crash // that happens on modern Linux versions. // http://stackoverflow.com/questions/9191668/error-longjmp-causes-uninitialized-stack-frame @@ -153,6 +141,7 @@ lastStatus_ = HttpStatus_200_Ok; isVerbose_ = false; timeout_ = 0; + verifyPeers_ = globalVerifyPeers_; } @@ -206,6 +195,19 @@ CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_URL, url_.c_str())); CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_WRITEDATA, &answer)); + // Setup HTTPS-related options +#if ORTHANC_SSL_ENABLED == 1 + if (IsHttpsVerifyPeers()) + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_CAINFO, GetHttpsCACertificates().c_str())); + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYPEER, 1)); + } + else + { + CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_SSL_VERIFYPEER, 0)); + } +#endif + // Reset the parameters from previous calls to Apply() CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPHEADER, NULL)); CheckCode(curl_easy_setopt(pimpl_->curl_, CURLOPT_HTTPGET, 0L)); @@ -336,29 +338,36 @@ } + const std::string& HttpClient::GetHttpsCACertificates() const + { + if (caCertificates_.empty()) + { + return globalCACertificates_; + } + else + { + return caCertificates_; + } + } + + void HttpClient::GlobalInitialize(bool httpsVerifyPeers, const std::string& httpsVerifyCertificates) { -#if ORTHANC_SSL_ENABLED == 1 - httpsVerifyPeers_ = httpsVerifyPeers; - cacert_ = httpsVerifyCertificates; + globalVerifyPeers_ = httpsVerifyPeers; + globalCACertificates_ = httpsVerifyCertificates; - // TODO - /*if (cacert_.empty()) - { - cacert_ = "/etc/ssl/certs/ca-certificates.crt"; - }*/ - +#if ORTHANC_SSL_ENABLED == 1 if (httpsVerifyPeers) { - if (cacert_.empty()) + if (globalCACertificates_.empty()) { LOG(WARNING) << "No certificates are provided to validate peers, " - << "set \"HttpsCertificatesFile\" if you need to do HTTPS requests"; + << "set \"HttpsCACertificates\" if you need to do HTTPS requests"; } else { - LOG(WARNING) << "HTTPS will use the certificates from this file: " << cacert_; + LOG(WARNING) << "HTTPS will use the CA certificates from this file: " << globalCACertificates_; } } else diff -r 0011cc99443c -r 95b3b0260240 Core/HttpClient.h --- a/Core/HttpClient.h Wed Aug 12 17:52:10 2015 +0200 +++ b/Core/HttpClient.h Thu Aug 13 12:42:32 2015 +0200 @@ -54,6 +54,8 @@ bool isVerbose_; long timeout_; std::string proxy_; + bool verifyPeers_; + std::string caCertificates_; void Setup(); @@ -140,8 +142,25 @@ proxy_ = proxy; } + void SetHttpsVerifyPeers(bool verify) + { + verifyPeers_ = verify; + } + + bool IsHttpsVerifyPeers() const + { + return verifyPeers_; + } + + void SetHttpsCACertificates(const std::string& certificates) + { + caCertificates_ = certificates; + } + + const std::string& GetHttpsCACertificates() const; + static void GlobalInitialize(bool httpsVerifyPeers, - const std::string& httpsVerifyCertificates); + const std::string& httpsCACertificates); static void GlobalFinalize(); }; diff -r 0011cc99443c -r 95b3b0260240 NEWS --- a/NEWS Wed Aug 12 17:52:10 2015 +0200 +++ b/NEWS Thu Aug 13 12:42:32 2015 +0200 @@ -7,7 +7,7 @@ Maintenance ----------- -* Options to validate peers in HTTPS requests +* Options to validate peers against CA certificates in HTTPS requests * Upgrade to curl 7.44.0 for static and Windows builds * Upgrade to libcurl 1.0.2d for static and Windows builds diff -r 0011cc99443c -r 95b3b0260240 OrthancServer/OrthancInitialization.cpp --- a/OrthancServer/OrthancInitialization.cpp Wed Aug 12 17:52:10 2015 +0200 +++ b/OrthancServer/OrthancInitialization.cpp Thu Aug 13 12:42:32 2015 +0200 @@ -316,7 +316,7 @@ ReadGlobalConfiguration(configurationFile); HttpClient::GlobalInitialize(GetGlobalBoolParameterInternal("HttpsVerifyPeers", true), - GetGlobalStringParameterInternal("HttpsVerifyCertificates", "")); + GetGlobalStringParameterInternal("HttpsCACertificates", "")); RegisterUserMetadata(); RegisterUserContentType(); diff -r 0011cc99443c -r 95b3b0260240 Resources/Configuration.json --- a/Resources/Configuration.json Wed Aug 12 17:52:10 2015 +0200 +++ b/Resources/Configuration.json Thu Aug 13 12:42:32 2015 +0200 @@ -240,12 +240,13 @@ "HttpCompressionEnabled" : true, // Enable the verification of the peers during HTTPS requests. + // Reference: http://curl.haxx.se/docs/sslcerts.html "HttpsVerifyPeers" : true, - // Path to the certificates to validate peers in HTTPS - // requests. From curl documentation: "Tells curl to use the - // specified certificate file to verify the peers. The file may - // contain multiple CA certificates. The certificate(s) must be in - // PEM format." - "HttpsVerifyCertificates" : "" + // Path to the CA (certification authority) certificates to validate + // peers in HTTPS requests. From curl documentation ("--cacert" + // option): "Tells curl to use the specified certificate file to + // verify the peers. The file may contain multiple CA + // certificates. The certificate(s) must be in PEM format." + "HttpsCACertificates" : "" } diff -r 0011cc99443c -r 95b3b0260240 Resources/RetrieveCACertificates.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/RetrieveCACertificates.py Thu Aug 13 12:42:32 2015 +0200 @@ -0,0 +1,70 @@ +#!/usr/bin/python + +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# In addition, as a special exception, the copyright holders of this +# program give permission to link the code of its release with the +# OpenSSL project's "OpenSSL" library (or with modified versions of it +# that use the same license as the "OpenSSL" library), and distribute +# the linked executables. You must obey the GNU General Public License +# in all respects for all of the code used other than "OpenSSL". If you +# modify file(s) with this exception, you may extend this exception to +# your version of the file(s), but you are not obligated to do so. If +# you do not wish to do so, delete this exception statement from your +# version. If you delete this exception statement from all source files +# in the program, then also delete it here. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import re +import sys +import subprocess +import urllib2 + + +if len(sys.argv) <= 2: + print('Download a set of CA certificates, convert them to PEM, then format them as a C macro') + print('Usage: %s [Macro] [Certificate1] ...' % sys.argv[0]) + print('') + print('Example: %s BITBUCKET_CERTIFICATES https://www.digicert.com/CACerts/DigiCertHighAssuranceEVRootCA.crt' % sys.argv[0]) + print('') + sys.exit(-1) + +MACRO = sys.argv[1] + +sys.stdout.write('#define %s ' % MACRO) + +for url in sys.argv[2:]: + # Download the certificate from the CA authority, in the DES format + des = urllib2.urlopen(url).read() + + # Convert DES to PEM + p = subprocess.Popen([ 'openssl', 'x509', '-inform', 'DES', '-outform', 'PEM' ], + stdin = subprocess.PIPE, + stdout = subprocess.PIPE) + pem = p.communicate(input = des)[0] + pem = re.sub(r'\r', '', pem) # Remove any carriage return + pem = re.sub(r'\\', r'\\\\', pem) # Escape any backslash + pem = re.sub(r'"', r'\\"', pem) # Escape any quote + + # Write the PEM data into the macro + for line in pem.split('\n'): + sys.stdout.write(' \\\n') + sys.stdout.write('"%s\\n" ' % line) + +sys.stdout.write('\n') +sys.stderr.write('Done!\n') diff -r 0011cc99443c -r 95b3b0260240 UnitTestsSources/BitbucketCACertificates.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/UnitTestsSources/BitbucketCACertificates.h Thu Aug 13 12:42:32 2015 +0200 @@ -0,0 +1,25 @@ +#define BITBUCKET_CERTIFICATES \ +"-----BEGIN CERTIFICATE-----\n" \ +"MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs\n" \ +"MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" \ +"d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j\n" \ +"ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL\n" \ +"MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3\n" \ +"LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug\n" \ +"RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm\n" \ +"+9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW\n" \ +"PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM\n" \ +"xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB\n" \ +"Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3\n" \ +"hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg\n" \ +"EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF\n" \ +"MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA\n" \ +"FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec\n" \ +"nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z\n" \ +"eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF\n" \ +"hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2\n" \ +"Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe\n" \ +"vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep\n" \ +"+OkuE6N36B9K\n" \ +"-----END CERTIFICATE-----\n" \ +"\n" diff -r 0011cc99443c -r 95b3b0260240 UnitTestsSources/RestApiTests.cpp --- a/UnitTestsSources/RestApiTests.cpp Wed Aug 12 17:52:10 2015 +0200 +++ b/UnitTestsSources/RestApiTests.cpp Thu Aug 13 12:42:32 2015 +0200 @@ -50,6 +50,8 @@ #error "Please set UNIT_TESTS_WITH_HTTP_CONNEXIONS" #endif + + TEST(HttpClient, Basic) { HttpClient c; @@ -69,18 +71,61 @@ #if UNIT_TESTS_WITH_HTTP_CONNEXIONS == 1 + +/** + The HTTPS CA certificates for BitBucket were extracted as follows: + + (1) We retrieve the certification chain of BitBucket: + + # echo | openssl s_client -showcerts -connect www.bitbucket.org:443 + + (2) We see that the certification authority (CA) is + "www.digicert.com", and the root certificate is "DigiCert High + Assurance EV Root CA". As a consequence, we navigate to DigiCert to + find the URL to this CA certificate: + + firefox https://www.digicert.com/digicert-root-certificates.htm + + (3) Once we get the URL to the CA certificate, we convert it to a C + macro that can be used by libcurl: + + # cd UnitTestsSources + # ../Resources/RetrieveCACertificates.py BITBUCKET_CERTIFICATES https://www.digicert.com/CACerts/DigiCertHighAssuranceEVRootCA.crt > BitbucketCACertificates.h +**/ + +#include "BitbucketCACertificates.h" + TEST(HttpClient, Ssl) { + Toolbox::WriteFile(BITBUCKET_CERTIFICATES, "UnitTestsResults/bitbucket.cert"); + + /*{ + std::string s; + Toolbox::ReadFile(s, "/usr/share/ca-certificates/mozilla/WoSign.crt"); + Toolbox::WriteFile(s, "UnitTestsResults/bitbucket.cert"); + }*/ + HttpClient c; + c.SetHttpsVerifyPeers(true); + c.SetHttpsCACertificates("UnitTestsResults/bitbucket.cert"); c.SetUrl("https://bitbucket.org/sjodogne/orthanc/raw/Orthanc-0.9.3/Resources/Configuration.json"); - std::string s; - c.Apply(s); + Json::Value v; + c.Apply(v); + ASSERT_TRUE(v.isMember("LuaScripts")); +} - /*Json::Value v; +TEST(HttpClient, SslNoVerification) +{ + HttpClient c; + c.SetHttpsVerifyPeers(false); + c.SetUrl("https://bitbucket.org/sjodogne/orthanc/raw/Orthanc-0.9.3/Resources/Configuration.json"); + + Json::Value v; c.Apply(v); - ASSERT_TRUE(v.isMember("LuaScripts"));*/ + ASSERT_TRUE(v.isMember("LuaScripts")); } + #endif