From 4119ad46dc35a7285c7019f91b8ffef08703103f Mon Sep 17 00:00:00 2001 From: Varac <varac@varac.net> Date: Tue, 7 May 2019 12:25:35 +0200 Subject: [PATCH] Test certs with openssl and requests We now add the letsencrypt staging CA bundle to the trust store and validate certs against it. Closes #132 --- test/{pytest => }/README.md | 12 ++++ test/pytest/le-staging-bundle.pem | 57 ++++++++++++++++++ test/pytest/test_certs.py | 99 +++++++++++++++++++++++-------- test/requirements.txt | 2 + 4 files changed, 144 insertions(+), 26 deletions(-) rename test/{pytest => }/README.md (55%) create mode 100644 test/pytest/le-staging-bundle.pem mode change 100644 => 100755 test/pytest/test_certs.py diff --git a/test/pytest/README.md b/test/README.md similarity index 55% rename from test/pytest/README.md rename to test/README.md index a7dbbc11b..1bc4c9b90 100644 --- a/test/pytest/README.md +++ b/test/README.md @@ -8,6 +8,18 @@ Specify host manually: py.test -v --hosts='ssh://root@varac-oas.openappstack.net' +Run cert test manually using the ansible inventory file: + + OAS_DOMAIN='varac-oas.openappstack.net' py.test -v -m 'certs' \ + --connection=ansible \ + --ansible-inventory=../ansible/inventory.yml \ + --hosts='ansible://*' + +Run cert test manually against a different cluster, not configured in any +ansible inventory file: + + OAS_DOMAIN='varac-oas.openappstack.net' py.test -v -m 'certs' + # Issues - Default ssh backend is `paramiko`, which doesn't work oout of the diff --git a/test/pytest/le-staging-bundle.pem b/test/pytest/le-staging-bundle.pem new file mode 100644 index 000000000..b7654a101 --- /dev/null +++ b/test/pytest/le-staging-bundle.pem @@ -0,0 +1,57 @@ +-----BEGIN CERTIFICATE----- +MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw +GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2 +MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0 +8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym +oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0 +ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN +xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56 +dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9 +AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw +HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0 +BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu +b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu +Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq +hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF +UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9 +AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp +DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7 +IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf +zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI +PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w +SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em +2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0 +WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt +n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU= +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIFATCCAumgAwIBAgIRAKc9ZKBASymy5TLOEp57N98wDQYJKoZIhvcNAQELBQAw +GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDMyMzIyNTM0NloXDTM2 +MDMyMzIyNTM0NlowGjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMIICIjANBgkq +hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+pYHvQw5iU3v2b3iNuYNKYgsWD6KU7aJ +diddtZQxSWYzUI3U0I1UsRPTxnhTifs/M9NW4ZlV13ZfB7APwC8oqKOIiwo7IwlP +xg0VKgyz+kT8RJfYr66PPIYP0fpTeu42LpMJ+CKo9sbpgVNDZN2z/qiXrRNX/VtG +TkPV7a44fZ5bHHVruAxvDnylpQxJobtCBWlJSsbIRGFHMc2z88eUz9NmIOWUKGGj +EmP76x8OfRHpIpuxRSCjn0+i9+hR2siIOpcMOGd+40uVJxbRRP5ZXnUFa2fF5FWd +O0u0RPI8HON0ovhrwPJY+4eWKkQzyC611oLPYGQ4EbifRsTsCxUZqyUuStGyp8oa +aoSKfF6X0+KzGgwwnrjRTUpIl19A92KR0Noo6h622OX+4sZiO/JQdkuX5w/HupK0 +A0M0WSMCvU6GOhjGotmh2VTEJwHHY4+TUk0iQYRtv1crONklyZoAQPD76hCrC8Cr +IbgsZLfTMC8TWUoMbyUDgvgYkHKMoPm0VGVVuwpRKJxv7+2wXO+pivrrUl2Q9fPe +Kk055nJLMV9yPUdig8othUKrRfSxli946AEV1eEOhxddfEwBE3Lt2xn0hhiIedbb +Ftf/5kEWFZkXyUmMJK8Ra76Kus2ABueUVEcZ48hrRr1Hf1N9n59VbTUaXgeiZA50 +qXf2bymE6F8CAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wHQYDVR0OBBYEFMEmdKSKRKDm+iAo2FwjmkWIGHngMA0GCSqGSIb3DQEBCwUA +A4ICAQBCPw74M9X/Xx04K1VAES3ypgQYH5bf9FXVDrwhRFSVckria/7dMzoF5wln +uq9NGsjkkkDg17AohcQdr8alH4LvPdxpKr3BjpvEcmbqF8xH+MbbeUEnmbSfLI8H +sefuhXF9AF/9iYvpVNC8FmJ0OhiVv13VgMQw0CRKkbtjZBf8xaEhq/YqxWVsgOjm +dm5CAQ2X0aX7502x8wYRgMnZhA5goC1zVWBVAi8yhhmlhhoDUfg17cXkmaJC5pDd +oenZ9NVhW8eDb03MFCrWNvIh89DDeCGWuWfDltDq0n3owyL0IeSn7RfpSclpxVmV +/53jkYjwIgxIG7Gsv0LKMbsf6QdBcTjhvfZyMIpBRkTe3zuHd2feKzY9lEkbRvRQ +zbh4Ps5YBnG6CKJPTbe2hfi3nhnw/MyEmF3zb0hzvLWNrR9XW3ibb2oL3424XOwc +VjrTSCLzO9Rv6s5wi03qoWvKAQQAElqTYRHhynJ3w6wuvKYF5zcZF3MDnrVGLbh1 +Q9ePRFBCiXOQ6wPLoUhrrbZ8LpFUFYDXHMtYM7P9sc9IAWoONXREJaO08zgFtMp4 +8iyIYUyQAbsvx8oD2M8kRvrIRSrRJSl6L957b4AFiLIQ/GgV2curs0jje7Edx34c +idWw1VrejtwclobqNMVtG3EiPUIpJGpbMcJgbiLSmKkrvQtGng== +-----END CERTIFICATE----- diff --git a/test/pytest/test_certs.py b/test/pytest/test_certs.py old mode 100644 new mode 100755 index 75789c212..d7d35a991 --- a/test/pytest/test_certs.py +++ b/test/pytest/test_certs.py @@ -1,44 +1,91 @@ #!/usr/bin/env python3 -"""Validates remote TLS certs.""" - -import pycurl import certifi -from io import BytesIO import os import pytest +import requests +import socket +import shutil +import sys +from OpenSSL import SSL + + +def add_custom_cert_authorities(ca_file: str, + custom_ca_files: list, + destination_file: str = + '/tmp/custom_ca_bundle.crt'): + """Concanates existing cert bundle with custom CAs.""" + destination = open(destination_file, 'wb') + shutil.copyfileobj(open(ca_file, 'rb'), destination) + for custom_ca_file in custom_ca_files: + shutil.copyfileobj(open(custom_ca_file, 'rb'), destination) + destination.close() -def check_cert_url(url: str): - print('Testing URL: ', url) - buffer = BytesIO() - c = pycurl.Curl() - c.setopt(c.URL, url) - c.setopt(c.WRITEDATA, buffer) - c.setopt(c.CAINFO, certifi.where()) - c.setopt(c.VERBOSE, True) +def fetch_certs(domain: str, port: int = 443): + """Fetches cert fom given domain.""" + + s = socket.socket() + context = SSL.Context(SSL.TLSv1_2_METHOD) + conn = SSL.Connection(context, s) + conn.set_tlsext_host_name(domain.encode('utf-8')) + certs = [] try: - c.perform() - valid_cert = True - except pycurl.error as e: - valid_cert = False - print('Cert error!') - if e.args[0] == pycurl.E_COULDNT_CONNECT and c.exception: - print(c.exception) - else: - print(e) - c.close() + conn.connect((domain, port)) + except socket.gaierror as e: + print('socket.gaierror: {0}'.format(str(e))) + sys.exit(1) + + conn.do_handshake() + try: + certs = conn.get_peer_cert_chain() + except SSL.Error as e: + print('SSL Error: {0}'.format(str(e))) + sys.exit(1) + + return certs + + +def inspect_certs(certs: list): + """Show CN and issuer CN of cert list.""" - return valid_cert + for cert in certs: + subject = cert.get_subject() + cn = subject.CN + issuer = cert.get_issuer().CN + # issued_by = issuer.CN + print('CN: {0} (Issuer: {1})'.format(cn, issuer)) + + +def valid_cert(domain: str, ca_file: str = '/tmp/custom_ca_bundle.crt'): + """Validate cert of given domain against a ca_file bundle.""" + + url = 'https://' + domain + print('Validating cert from {0} ...'.format(url)) + inspect_certs(fetch_certs(domain)) + + try: + requests.get(url, verify=ca_file) + except requests.exceptions.SSLError as ex: + print('SSL Verification Error %s' % ex) + return False + print('Successfully Verified SSL Cert.\n') + return True @pytest.mark.certs def test_cert_validation(host): - domain = os.environ.get("OAS_DOMAIN") assert domain, "Please export OAS_DOMAIN as environment variable." - # Check traefik cert - assert check_cert_url('https://traefi.%s/' % domain) + add_custom_cert_authorities(certifi.where(), + ['pytest/le-staging-bundle.pem']) + + # Check nextcloud cert + assert valid_cert('files.%s' % domain) + + +if __name__ == "__main__": + test_cert_validation('') diff --git a/test/requirements.txt b/test/requirements.txt index 3f168e536..e3927fc39 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -1,9 +1,11 @@ ansible>=2.7.0 behave-webdriver>=0.2.2 +certifi>=2019.3.9 # Needed for ansible k8s resource openshift>=0.8.6 psutil>=5.5.0 pycurl>=7.43.0.2 +pyopenssl>=19.0.0 pytest>=4.3.0 requests>=2.19.1 tabulate>=0.8.3 -- GitLab