From 4e16d0c890dd9b2643246d25103742dc859b08cc Mon Sep 17 00:00:00 2001
From: Christian Heimes <cheimes@redhat.com>
Date: Fri, 3 Jul 2015 14:47:05 +0200
Subject: [PATCH] Implement server cert validation in PKIConnection

PKIConnection disabled verification of TLS server certs and no API to
enable it. Now it has options and a method to enable verification.
By default it uses the system's trust store. Optionally it can be
configured to use a PEM file instead.

For deployment the validation flag can set in the config file with two
new options 'pki_client_verify_connection' and
'pki_client_connection_ca_pem_file'.

https://fedorahosted.org/pki/ticket/1253
---
 base/common/python/pki/client.py                   | 61 +++++++++++++++++++---
 base/server/etc/default.cfg                        |  2 +
 .../python/pki/server/deployment/pkihelper.py      | 18 +++++--
 .../python/pki/server/deployment/pkiparser.py      | 10 +++-
 4 files changed, 76 insertions(+), 15 deletions(-)

diff --git a/base/common/python/pki/client.py b/base/common/python/pki/client.py
index 90939930a7e2526122f5184f3d47dde2e86a0300..de4f7727ad9d125e1924c219c549d4ae3823a7ef 100644
--- a/base/common/python/pki/client.py
+++ b/base/common/python/pki/client.py
@@ -19,6 +19,7 @@
 # All rights reserved.
 #
 
+import os
 import requests
 
 
@@ -29,7 +30,8 @@ class PKIConnection:
     """
 
     def __init__(self, protocol='http', hostname='localhost', port='8080',
-                 subsystem='ca', accept='application/json'):
+                 subsystem='ca', accept='application/json', verify=False,
+                 verify_location=None):
         """
         Set the parameters for a python-requests based connection to a
         Dogtag subsystem.
@@ -44,6 +46,10 @@ class PKIConnection:
         :param accept: value of accept header.  Supported values are usually
            'application/json' or 'application/xml'
         :type accept: str
+        :param verify: verify server certs
+        :type verify: bool
+        :param verify_location: override default location of trust anchors
+        :type verify_location: str, None
         :return: PKIConnection object.
         """
 
@@ -59,6 +65,11 @@ class PKIConnection:
         self.session = requests.Session()
         if accept:
             self.session.headers.update({'Accept': accept})
+        # TLS server cert validation is disabled by default
+        if verify:
+            self.set_verify(verify_location if verify_location else True)
+        else:
+            self.set_verify(False)
 
     def authenticate(self, username=None, password=None):
         """
@@ -91,6 +102,33 @@ class PKIConnection:
             raise Exception("No path for the certificate specified.")
         self.session.cert = pem_cert_path
 
+    def set_verify(self, flag_or_path):
+        """Set verification flag and root CA certs
+
+        *flag_or_path* can be any of:
+
+        True
+          Enable server certificate validation and use
+          ``requests.utils.DEFAULT_CA_BUNDLE_PATH`` as trust anchors.
+
+        False
+          Disable server certificate validation
+
+        str with /path/to/ca.pem
+          Enable server certificate validation and use string as path to
+          trust anchors.
+
+        :param flag_or_path: path to PEM file with root CA certs or boolean
+        :type flag_or_path: str, bool
+        :return: None
+        """
+        if flag_or_path in {True, False}:
+            self.session.verify = flag_or_path
+        elif not os.path.isfile(flag_or_path):
+            raise IOError('%s is not a regular file' % flag_or_path)
+        else:
+            self.session.verify = flag_or_path
+
     def get(self, path, headers=None, params=None, payload=None):
         """
         Uses python-requests to issue a GET request to the server.
@@ -109,7 +147,6 @@ class PKIConnection:
         """
         r = self.session.get(
             self.serverURI + path,
-            verify=False,
             headers=headers,
             params=params,
             data=payload)
@@ -134,7 +171,6 @@ class PKIConnection:
         """
         r = self.session.post(
             self.serverURI + path,
-            verify=False,
             data=payload,
             headers=headers,
             params=params)
@@ -155,7 +191,10 @@ class PKIConnection:
         :raises: Exception from python-requests in case the PUT was not
             successful, or returns an error code.
         """
-        r = self.session.put(self.serverURI + path, payload, headers=headers)
+        r = self.session.put(
+            self.serverURI + path,
+            payload,
+            headers=headers)
         r.raise_for_status()
         return r
 
@@ -171,7 +210,9 @@ class PKIConnection:
         :raises: Exception from python-requests in case the DEL was not
             successful, or returns an error code.
         """
-        r = self.session.delete(self.serverURI + path, headers=headers)
+        r = self.session.delete(
+            self.serverURI + path,
+            headers=headers)
         r.raise_for_status()
         return r
 
@@ -181,11 +222,15 @@ def main():
     Test code for the PKIConnection class.
     :return: None
     """
-    conn = PKIConnection()
+    # $ pki ca-cert-find | grep -B1 'CA Signing'
+    # $ pki ca-cert-show 0x1 --output /tmp/ca.pem
+    import socket
+    conn = PKIConnection('https', socket.gethostname(), '8443',
+                         verify=True, verify_location='/tmp/ca.pem')
     headers = {'Content-type': 'application/json',
                'Accept': 'application/json'}
-    conn.set_authentication_cert('/root/temp4.pem')
-    print conn.get("", headers).json()
+    # conn.set_authentication_cert('/root/temp4.pem')
+    print conn.get("", headers).text
 
 if __name__ == "__main__":
     main()
diff --git a/base/server/etc/default.cfg b/base/server/etc/default.cfg
index 58f338692ac4f1c2637b575e27db2bec6254905b..f7c00e3f648583f2758054923fb6b7f9bfaaf8dd 100644
--- a/base/server/etc/default.cfg
+++ b/base/server/etc/default.cfg
@@ -82,6 +82,8 @@ pki_client_database_password=
 pki_client_database_purge=True
 pki_client_dir=%(home_dir)s/.dogtag/%(pki_instance_name)s
 pki_client_pkcs12_password=
+pki_client_verify_connection=False
+pki_client_connection_ca_pem_file=
 pki_ds_bind_dn=cn=Directory Manager
 pki_ds_create_new_db=True
 pki_ds_ldap_port=389
diff --git a/base/server/python/pki/server/deployment/pkihelper.py b/base/server/python/pki/server/deployment/pkihelper.py
index 5bc4ffab814891aadf0508d0ae20bb8cc315bc90..09f60071086c3234b87e0d326f6cdb77e96e85d7 100644
--- a/base/server/python/pki/server/deployment/pkihelper.py
+++ b/base/server/python/pki/server/deployment/pkihelper.py
@@ -1067,7 +1067,10 @@ class Instance:
             hostname=self.mdict['pki_hostname'],
             port=self.mdict['pki_https_port'],
             subsystem=self.mdict['pki_subsystem_type'],
-            accept='application/xml')
+            accept='application/xml',
+            verify=config.str2bool(self.mdict['pki_client_verify_connection']),
+            verify_location=self.mdict['pki_client_connection_ca_pem_file']
+        )
 
         # catching all exceptions because we do not want to break if underlying
         # requests or urllib3 use a different exception.
@@ -3022,13 +3025,15 @@ class KRAConnector:
                 raise
         return
 
-    @staticmethod
-    def get_ca_list_from_security_domain(sechost, secport):
+    def get_ca_list_from_security_domain(self, sechost, secport):
         sd_connection = pki.client.PKIConnection(
             protocol='https',
             hostname=sechost,
             port=secport,
-            subsystem='ca')
+            subsystem='ca',
+            verify=config.str2bool(self.mdict['pki_client_verify_connection']),
+            verify_location=self.mdict['pki_client_connection_ca_pem_file']
+        )
         sd = pki.system.SecurityDomainClient(sd_connection)
         try:
             info = sd.get_security_domain_info()
@@ -3792,7 +3797,10 @@ class ConfigClient:
             protocol='https',
             hostname=self.mdict['pki_hostname'],
             port=self.mdict['pki_https_port'],
-            subsystem=self.mdict['pki_subsystem_type'])
+            subsystem=self.mdict['pki_subsystem_type'],
+            verify=config.str2bool(self.mdict['pki_client_verify_connection']),
+            verify_location=self.mdict['pki_client_connection_ca_pem_file']
+        )
 
         try:
             client = pki.system.SystemConfigClient(connection)
diff --git a/base/server/python/pki/server/deployment/pkiparser.py b/base/server/python/pki/server/deployment/pkiparser.py
index 7e1813c75b891466600117998559c6c1c714ef94..1a1f2a68154603f0086ab472694332d9fe6416e6 100644
--- a/base/server/python/pki/server/deployment/pkiparser.py
+++ b/base/server/python/pki/server/deployment/pkiparser.py
@@ -464,7 +464,10 @@ class PKIConfigParser:
             protocol='https',
             hostname=self.mdict['pki_security_domain_hostname'],
             port=self.mdict['pki_security_domain_https_port'],
-            subsystem='ca')
+            subsystem='ca',
+            verify=config.str2bool(self.mdict['pki_client_verify_connection']),
+            verify_location=self.mdict['pki_client_connection_ca_pem_file']
+        )
 
     def sd_get_info(self):
         sd = pki.system.SecurityDomainClient(self.sd_connection)
@@ -531,7 +534,10 @@ class PKIConfigParser:
             protocol=parse.scheme,
             hostname=parse.hostname,
             port=str(parse.port),
-            subsystem=system_type)
+            subsystem=system_type,
+            verify=config.str2bool(self.mdict['pki_client_verify_connection']),
+            verify_location=self.mdict['pki_client_connection_ca_pem_file']
+        )
         client = pki.system.SystemStatusClient(conn)
         response = client.get_status()
         root = ET.fromstring(response)
-- 
2.4.3

