changeset 1534:95b3b0260240

Options to validate peers against CA certificates in HTTPS requests
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 13 Aug 2015 12:42:32 +0200
parents 0011cc99443c
children 34c8954544e8
files Core/HttpClient.cpp Core/HttpClient.h NEWS OrthancServer/OrthancInitialization.cpp Resources/Configuration.json Resources/RetrieveCACertificates.py UnitTestsSources/BitbucketCACertificates.h UnitTestsSources/RestApiTests.cpp
diffstat 8 files changed, 208 insertions(+), 39 deletions(-) [+]
line wrap: on
line diff
--- 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 <boost/algorithm/string/predicate.hpp>
 
 
-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
--- 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();
   };
--- 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
 
--- 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();
--- 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" : ""
 }
--- /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 <http://www.gnu.org/licenses/>.
+
+
+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] <Certificate2>...' % 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')
--- /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" 
--- 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