From 293f18593307f5a90890848d77c439b3d3f1dd0d Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Tue, 21 Feb 2017 20:23:34 -0500 Subject: test fixes for openshift_certificates_expiry - create pytest fixtures for building certs at runtime - update tests to use the fixtures - add tests for load_and_handle_cert - fix py2/py3 encode/decode issues raised by tests - add get_extension_count method to fakeOpenSSLCertificate - avoid using a temp file for passing ssl certificate to openssl subprocess - other test tweaks: - exclude conftest.py and tests from coverage report - reduce the fail_under to 26%, since the tests being included were inflating our coverage --- .coveragerc | 7 +- .../library/openshift_cert_expiry.py | 116 +++++++------------- .../openshift_certificate_expiry/test/conftest.py | 116 ++++++++++++++++++++ .../test/master.server.crt | 42 ------- .../test/master.server.crt.txt | 82 -------------- .../test/system-node-m01.example.com.crt | 19 ---- .../test/system-node-m01.example.com.crt.txt | 75 ------------- .../test/test_fakeopensslclasses.py | 121 +++++++++++---------- .../test/test_load_and_handle_cert.py | 67 ++++++++++++ 9 files changed, 293 insertions(+), 352 deletions(-) create mode 100644 roles/openshift_certificate_expiry/test/conftest.py delete mode 100644 roles/openshift_certificate_expiry/test/master.server.crt delete mode 100644 roles/openshift_certificate_expiry/test/master.server.crt.txt delete mode 100644 roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt delete mode 100644 roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt.txt create mode 100644 roles/openshift_certificate_expiry/test/test_load_and_handle_cert.py diff --git a/.coveragerc b/.coveragerc index 1e819e157..6f33e0bfe 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,11 +8,12 @@ omit = # TODO(rhcarvalho): this is used to ignore test files from coverage report. # We can make this less generic when we stick with a single test pattern in # the repo. - test_*.py - *_tests.py + */conftest.py + */test_*.py + */*_tests.py [report] -fail_under = 28 +fail_under = 26 [html] directory = cover diff --git a/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py b/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py index b093d84fe..33a4faf3e 100644 --- a/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py +++ b/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py @@ -8,15 +8,12 @@ import datetime import io import os import subprocess -import sys -import tempfile +import yaml -# File pointers from io.open require unicode inputs when using their -# `write` method -import six from six.moves import configparser -import yaml +from ansible.module_utils.basic import AnsibleModule + try: # You can comment this import out and include a 'pass' in this # block if you're manually testing this module on a NON-ATOMIC @@ -24,13 +21,14 @@ try: # available). That will force the `load_and_handle_cert` function # to use the Fake OpenSSL classes. import OpenSSL.crypto + HAS_OPENSSL = True except ImportError: # Some platforms (such as RHEL Atomic) may not have the Python # OpenSSL library installed. In this case we will use a manual # work-around to parse each certificate. # # Check for 'OpenSSL.crypto' in `sys.modules` later. - pass + HAS_OPENSSL = False DOCUMENTATION = ''' --- @@ -158,6 +156,10 @@ might return: [('O=system', 'nodes'), ('CN=system', 'node:m01.example.com')] 'subjectAltName'""" return self.extensions[i] + def get_extension_count(self): + """ get_extension_count """ + return len(self.extensions) + def get_notAfter(self): """Returns a date stamp as a string in the form '20180922170439Z'. strptime the result with format param: @@ -268,30 +270,23 @@ A tuple of the form: # around a missing library on the target host. # # pylint: disable=redefined-variable-type - if 'OpenSSL.crypto' in sys.modules: + if HAS_OPENSSL: # No work-around required cert_loaded = OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, _cert_string) else: - # Missing library, work-around required. We need to write the - # cert out to disk temporarily so we can run the 'openssl' + # Missing library, work-around required. Run the 'openssl' # command on it to decode it - _, path = tempfile.mkstemp() - with io.open(path, 'w') as fp: - fp.write(six.u(_cert_string)) - fp.flush() - - cmd = 'openssl x509 -in {} -text'.format(path) + cmd = 'openssl x509 -text' try: - openssl_decoded = subprocess.Popen(cmd.split(), - stdout=subprocess.PIPE) + openssl_proc = subprocess.Popen(cmd.split(), + stdout=subprocess.PIPE, + stdin=subprocess.PIPE) except OSError: ans_module.fail_json(msg="Error: The 'OpenSSL' python library and CLI command were not found on the target host. Unable to parse any certificates. This host will not be included in generated reports.") else: - openssl_decoded = openssl_decoded.communicate()[0] + openssl_decoded = openssl_proc.communicate(_cert_string.encode('utf-8'))[0].decode('utf-8') cert_loaded = FakeOpenSSLCertificate(openssl_decoded) - finally: - os.remove(path) ###################################################################### # Read all possible names from the cert @@ -301,34 +296,12 @@ A tuple of the form: # To read SANs from a cert we must read the subjectAltName # extension from the X509 Object. What makes this more difficult - # is that pyOpenSSL does not give extensions as a list, nor does - # it provide a count of all loaded extensions. - # - # Rather, extensions are REQUESTED by index. We must iterate over - # all extensions until we find the one called 'subjectAltName'. If - # we don't find that extension we'll eventually request an - # extension at an index where no extension exists (IndexError is - # raised). When that happens we know that the cert has no SANs so - # we break out of the loop. - i = 0 - checked_all_extensions = False - while not checked_all_extensions: - try: - # Read the extension at index 'i' - ext = cert_loaded.get_extension(i) - except IndexError: - # We tried to read an extension but it isn't there, that - # means we ran out of extensions to check. Abort - san = None - checked_all_extensions = True - else: - # We were able to load the extension at index 'i' - if ext.get_short_name() == 'subjectAltName': - san = ext - checked_all_extensions = True - else: - # Try reading the next extension - i += 1 + # is that pyOpenSSL does not give extensions as an iterable + san = None + for i in range(cert_loaded.get_extension_count()): + ext = cert_loaded.get_extension(i) + if ext.get_short_name() == 'subjectAltName': + san = ext if san is not None: # The X509Extension object for subjectAltName prints as a @@ -341,9 +314,13 @@ A tuple of the form: ###################################################################### # Grab the expiration date + not_after = cert_loaded.get_notAfter() + # example get_notAfter() => 20180922170439Z + if isinstance(not_after, bytes): + not_after = not_after.decode('utf-8') + cert_expiry_date = datetime.datetime.strptime( - cert_loaded.get_notAfter(), - # example get_notAfter() => 20180922170439Z + not_after, '%Y%m%d%H%M%SZ') time_remaining = cert_expiry_date - now @@ -455,13 +432,11 @@ an OpenShift Container Platform cluster ) # Basic scaffolding for OpenShift specific certs - openshift_base_config_path = module.params['config_base'] - openshift_master_config_path = os.path.normpath( - os.path.join(openshift_base_config_path, "master/master-config.yaml") - ) - openshift_node_config_path = os.path.normpath( - os.path.join(openshift_base_config_path, "node/node-config.yaml") - ) + openshift_base_config_path = os.path.realpath(module.params['config_base']) + openshift_master_config_path = os.path.join(openshift_base_config_path, + "master", "master-config.yaml") + openshift_node_config_path = os.path.join(openshift_base_config_path, + "node", "node-config.yaml") openshift_cert_check_paths = [ openshift_master_config_path, openshift_node_config_path, @@ -476,9 +451,7 @@ an OpenShift Container Platform cluster kubeconfig_paths = [] for m_kube_config in master_kube_configs: kubeconfig_paths.append( - os.path.normpath( - os.path.join(openshift_base_config_path, "master/%s.kubeconfig" % m_kube_config) - ) + os.path.join(openshift_base_config_path, "master", m_kube_config + ".kubeconfig") ) # Validate some paths we have the ability to do ahead of time @@ -527,7 +500,7 @@ an OpenShift Container Platform cluster ###################################################################### for os_cert in filter_paths(openshift_cert_check_paths): # Open up that config file and locate the cert and CA - with open(os_cert, 'r') as fp: + with io.open(os_cert, 'r', encoding='utf-8') as fp: cert_meta = {} cfg = yaml.load(fp) # cert files are specified in parsed `fp` as relative to the path @@ -542,7 +515,7 @@ an OpenShift Container Platform cluster # Load the certificate and the CA, parse their expiration dates into # datetime objects so we can manipulate them later for _, v in cert_meta.items(): - with open(v, 'r') as fp: + with io.open(v, 'r', encoding='utf-8') as fp: cert = fp.read() (cert_subject, cert_expiry_date, @@ -575,7 +548,7 @@ an OpenShift Container Platform cluster try: # Try to read the standard 'node-config.yaml' file to check if # this host is a node. - with open(openshift_node_config_path, 'r') as fp: + with io.open(openshift_node_config_path, 'r', encoding='utf-8') as fp: cfg = yaml.load(fp) # OK, the config file exists, therefore this is a @@ -588,7 +561,7 @@ an OpenShift Container Platform cluster cfg_path = os.path.dirname(fp.name) node_kubeconfig = os.path.join(cfg_path, node_masterKubeConfig) - with open(node_kubeconfig, 'r') as fp: + with io.open(node_kubeconfig, 'r', encoding='utf8') as fp: # Read in the nodes kubeconfig file and grab the good stuff cfg = yaml.load(fp) @@ -613,7 +586,7 @@ an OpenShift Container Platform cluster pass for kube in filter_paths(kubeconfig_paths): - with open(kube, 'r') as fp: + with io.open(kube, 'r', encoding='utf-8') as fp: # TODO: Maybe consider catching exceptions here? cfg = yaml.load(fp) @@ -656,7 +629,7 @@ an OpenShift Container Platform cluster etcd_certs = [] etcd_cert_params.append('dne') try: - with open('/etc/etcd/etcd.conf', 'r') as fp: + with io.open('/etc/etcd/etcd.conf', 'r', encoding='utf-8') as fp: etcd_config = configparser.ConfigParser() # Reason: This check is disabled because the issue was introduced # during a period where the pylint checks weren't enabled for this file @@ -675,7 +648,7 @@ an OpenShift Container Platform cluster pass for etcd_cert in filter_paths(etcd_certs_to_check): - with open(etcd_cert, 'r') as fp: + with io.open(etcd_cert, 'r', encoding='utf-8') as fp: c = fp.read() (cert_subject, cert_expiry_date, @@ -697,7 +670,7 @@ an OpenShift Container Platform cluster # Now the embedded etcd ###################################################################### try: - with open('/etc/origin/master/master-config.yaml', 'r') as fp: + with io.open('/etc/origin/master/master-config.yaml', 'r', encoding='utf-8') as fp: cfg = yaml.load(fp) except IOError: # Not present @@ -864,10 +837,5 @@ an OpenShift Container Platform cluster ) -###################################################################### -# It's just the way we do things in Ansible. So disable this warning -# -# pylint: disable=wrong-import-position,import-error -from ansible.module_utils.basic import AnsibleModule # noqa: E402 if __name__ == '__main__': main() diff --git a/roles/openshift_certificate_expiry/test/conftest.py b/roles/openshift_certificate_expiry/test/conftest.py new file mode 100644 index 000000000..4ca35ecbc --- /dev/null +++ b/roles/openshift_certificate_expiry/test/conftest.py @@ -0,0 +1,116 @@ +# pylint: disable=missing-docstring,invalid-name,redefined-outer-name +import pytest +from OpenSSL import crypto + +# Parameter list for valid_cert fixture +VALID_CERTIFICATE_PARAMS = [ + { + 'short_name': 'client', + 'cn': 'client.example.com', + 'serial': 4, + 'uses': b'clientAuth', + 'dns': [], + 'ip': [], + }, + { + 'short_name': 'server', + 'cn': 'server.example.com', + 'serial': 5, + 'uses': b'serverAuth', + 'dns': ['kubernetes', 'openshift'], + 'ip': ['10.0.0.1', '192.168.0.1'] + }, + { + 'short_name': 'combined', + 'cn': 'combined.example.com', + 'serial': 6, + 'uses': b'clientAuth, serverAuth', + 'dns': ['etcd'], + 'ip': ['10.0.0.2', '192.168.0.2'] + } +] + +# Extract the short_name from VALID_CERTIFICATE_PARAMS to provide +# friendly naming for the valid_cert fixture +VALID_CERTIFICATE_IDS = [param['short_name'] for param in VALID_CERTIFICATE_PARAMS] + + +@pytest.fixture(scope='session') +def ca(tmpdir_factory): + ca_dir = tmpdir_factory.mktemp('ca') + + key = crypto.PKey() + key.generate_key(crypto.TYPE_RSA, 2048) + + cert = crypto.X509() + cert.set_version(3) + cert.set_serial_number(1) + cert.get_subject().commonName = 'test-signer' + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(24 * 60 * 60) + cert.set_issuer(cert.get_subject()) + cert.set_pubkey(key) + cert.add_extensions([ + crypto.X509Extension(b'basicConstraints', True, b'CA:TRUE, pathlen:0'), + crypto.X509Extension(b'keyUsage', True, + b'digitalSignature, keyEncipherment, keyCertSign, cRLSign'), + crypto.X509Extension(b'subjectKeyIdentifier', False, b'hash', subject=cert) + ]) + cert.add_extensions([ + crypto.X509Extension(b'authorityKeyIdentifier', False, b'keyid:always', issuer=cert) + ]) + cert.sign(key, 'sha256') + + return { + 'dir': ca_dir, + 'key': key, + 'cert': cert, + } + + +@pytest.fixture(scope='session', + ids=VALID_CERTIFICATE_IDS, + params=VALID_CERTIFICATE_PARAMS) +def valid_cert(request, ca): + common_name = request.param['cn'] + + key = crypto.PKey() + key.generate_key(crypto.TYPE_RSA, 2048) + + cert = crypto.X509() + cert.set_serial_number(request.param['serial']) + cert.gmtime_adj_notBefore(0) + cert.gmtime_adj_notAfter(24 * 60 * 60) + cert.set_issuer(ca['cert'].get_subject()) + cert.set_pubkey(key) + cert.set_version(3) + cert.get_subject().commonName = common_name + cert.add_extensions([ + crypto.X509Extension(b'basicConstraints', True, b'CA:FALSE'), + crypto.X509Extension(b'keyUsage', True, b'digitalSignature, keyEncipherment'), + crypto.X509Extension(b'extendedKeyUsage', False, request.param['uses']), + ]) + + if request.param['dns'] or request.param['ip']: + san_list = ['DNS:{}'.format(common_name)] + san_list.extend(['DNS:{}'.format(x) for x in request.param['dns']]) + san_list.extend(['IP:{}'.format(x) for x in request.param['ip']]) + + cert.add_extensions([ + crypto.X509Extension(b'subjectAltName', False, ', '.join(san_list).encode('utf8')) + ]) + cert.sign(ca['key'], 'sha256') + + cert_contents = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) + cert_file = ca['dir'].join('{}.crt'.format(common_name)) + cert_file.write_binary(cert_contents) + + return { + 'common_name': common_name, + 'serial': request.param['serial'], + 'dns': request.param['dns'], + 'ip': request.param['ip'], + 'uses': request.param['uses'], + 'cert_file': cert_file, + 'cert': cert + } diff --git a/roles/openshift_certificate_expiry/test/master.server.crt b/roles/openshift_certificate_expiry/test/master.server.crt deleted file mode 100644 index 51aa85c8c..000000000 --- a/roles/openshift_certificate_expiry/test/master.server.crt +++ /dev/null @@ -1,42 +0,0 @@ ------BEGIN CERTIFICATE----- -MIID7zCCAtegAwIBAgIBBDANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu -c2hpZnQtc2lnbmVyQDE0ODY0OTExNTgwHhcNMTcwMjA3MTgxMjM5WhcNMTkwMjA3 -MTgxMjQwWjAVMRMwEQYDVQQDEwoxNzIuMzAuMC4xMIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEA44n6kVlnRXSwgnKhXX7JrRvxZm+nCqEE/vpKRfNtrMDP -AuVtcLUWdEDdT0L7QdceLTCBFe7VugrfokPhVi0XavrC2xFpYJ6+wPpuo7HyBRhf -z/8rOxftAnMeFU5JhFDaeLwSbDjiRgjE1ZYYz8Hcq9YlPujptD6j6YaW1Inae+Vs -QKXc1uAobemhClLKazEzccVGu53CaSHe4kJoKUZwJ8Ujt/nRHUr+wekbkpx0NfmF -UEGgNRXN46cq7ZwkLHsjtuR2pttC6JhF+KHgXTRyWM9ssfvL2McmhTFxrToAlhsq -8MuHMn0y9DMzmAK6EntvlC5AscxTRljtwHZEicspFwIDAQABo4IBNzCCATMwDgYD -VR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAw -gf0GA1UdEQSB9TCB8oIKa3ViZXJuZXRlc4ISa3ViZXJuZXRlcy5kZWZhdWx0ghZr -dWJlcm5ldGVzLmRlZmF1bHQuc3ZjgiRrdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNs -dXN0ZXIubG9jYWyCD20wMS5leGFtcGxlLmNvbYIJb3BlbnNoaWZ0ghFvcGVuc2hp -ZnQuZGVmYXVsdIIVb3BlbnNoaWZ0LmRlZmF1bHQuc3ZjgiNvcGVuc2hpZnQuZGVm -YXVsdC5zdmMuY2x1c3Rlci5sb2NhbIIKMTcyLjMwLjAuMYIPMTkyLjE2OC4xMjIu -MjQxhwSsHgABhwTAqHrxMA0GCSqGSIb3DQEBCwUAA4IBAQDSdKBpUVB5Sgn1JB// -bk804+zrUf01koaT83/17WMI+zG8IOwCZ9Be5+zDe4ThXH+PQC6obbwUi9dn8SN6 -rlihvrhNvAJaknY1YRjW07L7aus2RFKXpzsLuWoWLVlLXBTpmfWhQ2w40bCo4Kri -jQqvezBQ+u1otFzozWmF7nrI/JK+7o89hLvaobx+mDj5wCPQLO+cM/q11Jcz3htv -VOTFsMh2VnuKOxZqLGJz2CXkr6YXvAhJiFQWaRCnJEaA2ogTYbDahV5ixFKwqpGZ -o+yDEroPlCw54Bxs0P1ewUx4TRsqd+Qzhnr73xiFBQ0M7JjhKHF6EczHt87XPvsn -HEL2 ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIC6jCCAdKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu -c2hpZnQtc2lnbmVyQDE0ODY0OTExNTgwHhcNMTcwMjA3MTgxMjM3WhcNMjIwMjA2 -MTgxMjM4WjAmMSQwIgYDVQQDDBtvcGVuc2hpZnQtc2lnbmVyQDE0ODY0OTExNTgw -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDdyU8AD7sTXHP5jk04i1HY -cmUXuSiXdIByeIAtTZqiHU46Od0qnZib7tY1NSbo9FtGRl5YEvrfNL+1ig0hZjDh -gKZK4fNbsanuKgj2SWx11bt4yJH0YSbm9+H45y0E15IY1h30jGHnHFNFZDdYwxtO -8u+GShb4MOqZL9aUEfvfaoGJIIpfR+eW5NaBZQr6tiM89Z1wJYqoqzgzI/XIyUXR -zWLOayP1M/eeSXPvBncwZfTPLzphZjB2rz3MdloPrdYMm2b5tfbEOjD7L2aYOJJU -nVSkgjoFXAazL8KuXSIGcdrdDecyJ4ta8ijD4VIZRN9PnBlYiKaz0DsagkGjUVRd -AgMBAAGjIzAhMA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG -SIb3DQEBCwUAA4IBAQAZ/Kerb5eJXbByQ29fq60V+MQgLIJ1iTo3+zfaXxGlmV9v -fTp3S1xQhdGyhww7UC0Ze940eRq6BQ5/I6qPcgSGNpUB064pnjSf0CexCY4qoGqK -4VSvHRrG9TP5V+YIlX9UR1zuPy//a+wuCwKaqiWedTMb4jpvj5jsEOGIrciSmurg -/9nKvvJXRbgqRYQeJGLT5QW5clHywsyTrE7oYytYSEcAvEs3UZT37H74wj2RFxk6 -KcEzsxUB3W+iYst0QdOPByt64OCwAaUJ96VJstaOYMmyWSShAxGAKDSjcrr4JJnF -KtqOC1K56x0ONuBsY4MB15TNGPp8SbOhVV6OfIWj ------END CERTIFICATE----- diff --git a/roles/openshift_certificate_expiry/test/master.server.crt.txt b/roles/openshift_certificate_expiry/test/master.server.crt.txt deleted file mode 100644 index 6b3c8fb03..000000000 --- a/roles/openshift_certificate_expiry/test/master.server.crt.txt +++ /dev/null @@ -1,82 +0,0 @@ -Certificate: - Data: - Version: 3 (0x2) - Serial Number: 4 (0x4) - Signature Algorithm: sha256WithRSAEncryption - Issuer: CN=openshift-signer@1486491158 - Validity - Not Before: Feb 7 18:12:39 2017 GMT - Not After : Feb 7 18:12:40 2019 GMT - Subject: CN=172.30.0.1 - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - Public-Key: (2048 bit) - Modulus: - 00:e3:89:fa:91:59:67:45:74:b0:82:72:a1:5d:7e: - c9:ad:1b:f1:66:6f:a7:0a:a1:04:fe:fa:4a:45:f3: - 6d:ac:c0:cf:02:e5:6d:70:b5:16:74:40:dd:4f:42: - fb:41:d7:1e:2d:30:81:15:ee:d5:ba:0a:df:a2:43: - e1:56:2d:17:6a:fa:c2:db:11:69:60:9e:be:c0:fa: - 6e:a3:b1:f2:05:18:5f:cf:ff:2b:3b:17:ed:02:73: - 1e:15:4e:49:84:50:da:78:bc:12:6c:38:e2:46:08: - c4:d5:96:18:cf:c1:dc:ab:d6:25:3e:e8:e9:b4:3e: - a3:e9:86:96:d4:89:da:7b:e5:6c:40:a5:dc:d6:e0: - 28:6d:e9:a1:0a:52:ca:6b:31:33:71:c5:46:bb:9d: - c2:69:21:de:e2:42:68:29:46:70:27:c5:23:b7:f9: - d1:1d:4a:fe:c1:e9:1b:92:9c:74:35:f9:85:50:41: - a0:35:15:cd:e3:a7:2a:ed:9c:24:2c:7b:23:b6:e4: - 76:a6:db:42:e8:98:45:f8:a1:e0:5d:34:72:58:cf: - 6c:b1:fb:cb:d8:c7:26:85:31:71:ad:3a:00:96:1b: - 2a:f0:cb:87:32:7d:32:f4:33:33:98:02:ba:12:7b: - 6f:94:2e:40:b1:cc:53:46:58:ed:c0:76:44:89:cb: - 29:17 - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Key Usage: critical - Digital Signature, Key Encipherment - X509v3 Extended Key Usage: - TLS Web Server Authentication - X509v3 Basic Constraints: critical - CA:FALSE - X509v3 Subject Alternative Name: - DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:m01.example.com, DNS:openshift, DNS:openshift.default, DNS:openshift.default.svc, DNS:openshift.default.svc.cluster.local, DNS:172.30.0.1, DNS:192.168.122.241, IP Address:172.30.0.1, IP Address:192.168.122.241 - Signature Algorithm: sha256WithRSAEncryption - d2:74:a0:69:51:50:79:4a:09:f5:24:1f:ff:6e:4f:34:e3:ec: - eb:51:fd:35:92:86:93:f3:7f:f5:ed:63:08:fb:31:bc:20:ec: - 02:67:d0:5e:e7:ec:c3:7b:84:e1:5c:7f:8f:40:2e:a8:6d:bc: - 14:8b:d7:67:f1:23:7a:ae:58:a1:be:b8:4d:bc:02:5a:92:76: - 35:61:18:d6:d3:b2:fb:6a:eb:36:44:52:97:a7:3b:0b:b9:6a: - 16:2d:59:4b:5c:14:e9:99:f5:a1:43:6c:38:d1:b0:a8:e0:aa: - e2:8d:0a:af:7b:30:50:fa:ed:68:b4:5c:e8:cd:69:85:ee:7a: - c8:fc:92:be:ee:8f:3d:84:bb:da:a1:bc:7e:98:38:f9:c0:23: - d0:2c:ef:9c:33:fa:b5:d4:97:33:de:1b:6f:54:e4:c5:b0:c8: - 76:56:7b:8a:3b:16:6a:2c:62:73:d8:25:e4:af:a6:17:bc:08: - 49:88:54:16:69:10:a7:24:46:80:da:88:13:61:b0:da:85:5e: - 62:c4:52:b0:aa:91:99:a3:ec:83:12:ba:0f:94:2c:39:e0:1c: - 6c:d0:fd:5e:c1:4c:78:4d:1b:2a:77:e4:33:86:7a:fb:df:18: - 85:05:0d:0c:ec:98:e1:28:71:7a:11:cc:c7:b7:ce:d7:3e:fb: - 27:1c:42:f6 ------BEGIN CERTIFICATE----- -MIID7zCCAtegAwIBAgIBBDANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu -c2hpZnQtc2lnbmVyQDE0ODY0OTExNTgwHhcNMTcwMjA3MTgxMjM5WhcNMTkwMjA3 -MTgxMjQwWjAVMRMwEQYDVQQDEwoxNzIuMzAuMC4xMIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEA44n6kVlnRXSwgnKhXX7JrRvxZm+nCqEE/vpKRfNtrMDP -AuVtcLUWdEDdT0L7QdceLTCBFe7VugrfokPhVi0XavrC2xFpYJ6+wPpuo7HyBRhf -z/8rOxftAnMeFU5JhFDaeLwSbDjiRgjE1ZYYz8Hcq9YlPujptD6j6YaW1Inae+Vs -QKXc1uAobemhClLKazEzccVGu53CaSHe4kJoKUZwJ8Ujt/nRHUr+wekbkpx0NfmF -UEGgNRXN46cq7ZwkLHsjtuR2pttC6JhF+KHgXTRyWM9ssfvL2McmhTFxrToAlhsq -8MuHMn0y9DMzmAK6EntvlC5AscxTRljtwHZEicspFwIDAQABo4IBNzCCATMwDgYD -VR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAw -gf0GA1UdEQSB9TCB8oIKa3ViZXJuZXRlc4ISa3ViZXJuZXRlcy5kZWZhdWx0ghZr -dWJlcm5ldGVzLmRlZmF1bHQuc3ZjgiRrdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNs -dXN0ZXIubG9jYWyCD20wMS5leGFtcGxlLmNvbYIJb3BlbnNoaWZ0ghFvcGVuc2hp -ZnQuZGVmYXVsdIIVb3BlbnNoaWZ0LmRlZmF1bHQuc3ZjgiNvcGVuc2hpZnQuZGVm -YXVsdC5zdmMuY2x1c3Rlci5sb2NhbIIKMTcyLjMwLjAuMYIPMTkyLjE2OC4xMjIu -MjQxhwSsHgABhwTAqHrxMA0GCSqGSIb3DQEBCwUAA4IBAQDSdKBpUVB5Sgn1JB// -bk804+zrUf01koaT83/17WMI+zG8IOwCZ9Be5+zDe4ThXH+PQC6obbwUi9dn8SN6 -rlihvrhNvAJaknY1YRjW07L7aus2RFKXpzsLuWoWLVlLXBTpmfWhQ2w40bCo4Kri -jQqvezBQ+u1otFzozWmF7nrI/JK+7o89hLvaobx+mDj5wCPQLO+cM/q11Jcz3htv -VOTFsMh2VnuKOxZqLGJz2CXkr6YXvAhJiFQWaRCnJEaA2ogTYbDahV5ixFKwqpGZ -o+yDEroPlCw54Bxs0P1ewUx4TRsqd+Qzhnr73xiFBQ0M7JjhKHF6EczHt87XPvsn -HEL2 ------END CERTIFICATE----- diff --git a/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt b/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt deleted file mode 100644 index cd13ddc38..000000000 --- a/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDEzCCAfugAwIBAgIBCzANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu -c2hpZnQtc2lnbmVyQDE0ODY0OTExNTgwHhcNMTcwMjA3MTgxOTM0WhcNMTkwMjA3 -MTgxOTM1WjA9MRUwEwYDVQQKEwxzeXN0ZW06bm9kZXMxJDAiBgNVBAMTG3N5c3Rl -bTpub2RlOm0wMS5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBAOcVdDaSmeXuSp+7VCHUjEDeTP3j9aH0nreBj3079sEzethlLoQmwAqf -CZp23qXGYm0R89+CC55buaH1FN/ltQ8QDGUzi4tdow9Af/0OcD0EINO2ukmyG5/9 -N+X905mo+y923wppvrchAA6AcxxeDyA63zouGS4exI98iuZlcdS48zbsGERkRPGg -hoGCo7HoiKtUNL5X8MYibnFYnA4EUngdHZsRKuKte4t8GY4PYq4cxIOYXsJsNmT5 -mkFy4ThGFfR9IGg/VfyeWIkRe2VWyaUgzL0gHytAhlRJ9l54ynx96YEWrjCtp/kh -d3KeVj0IUcMzvoXX5hipYUPkoezcxI8CAwEAAaM1MDMwDgYDVR0PAQH/BAQDAgWg -MBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQEL -BQADggEBAM1jexLtuOOTsOPfEal/ICzfP9aX3m0R/yGPjwQv43jOc81NcL5d+CeD -MX36tKAxFIe+wvXo0kUQOzTK3D7ol4x2YTtB4uDzNE5tVh5dWi2LrKYSqZDIrhKO -MOmJRWR3AFEopaaGQpxsD/FSfZ5Mg0OMMBPHABxMrsiserHO1nh4ax3+SI0i7Jen -gVsB4B/Xxg9Lw9JDX3/XMcI+fyLVw5ctO62BaluljpT+HkdbRWnH8ar7TmcJjzTo -/TyXOeOLkbozx0BQK16d/CbtLomJ+PO4cdwCNs2Z6HGSPTL7S9y0pct52N0kfJfx -ZGXMsW+N62S2vVSXEekMR0GJgJnLNSo= ------END CERTIFICATE----- diff --git a/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt.txt b/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt.txt deleted file mode 100644 index 67a3eb81c..000000000 --- a/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt.txt +++ /dev/null @@ -1,75 +0,0 @@ -Certificate: - Data: - Version: 3 (0x2) - Serial Number: 11 (0xb) - Signature Algorithm: sha256WithRSAEncryption - Issuer: CN=openshift-signer@1486491158 - Validity - Not Before: Feb 7 18:19:34 2017 GMT - Not After : Feb 7 18:19:35 2019 GMT - Subject: O=system:nodes, CN=system:node:m01.example.com - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - Public-Key: (2048 bit) - Modulus: - 00:e7:15:74:36:92:99:e5:ee:4a:9f:bb:54:21:d4: - 8c:40:de:4c:fd:e3:f5:a1:f4:9e:b7:81:8f:7d:3b: - f6:c1:33:7a:d8:65:2e:84:26:c0:0a:9f:09:9a:76: - de:a5:c6:62:6d:11:f3:df:82:0b:9e:5b:b9:a1:f5: - 14:df:e5:b5:0f:10:0c:65:33:8b:8b:5d:a3:0f:40: - 7f:fd:0e:70:3d:04:20:d3:b6:ba:49:b2:1b:9f:fd: - 37:e5:fd:d3:99:a8:fb:2f:76:df:0a:69:be:b7:21: - 00:0e:80:73:1c:5e:0f:20:3a:df:3a:2e:19:2e:1e: - c4:8f:7c:8a:e6:65:71:d4:b8:f3:36:ec:18:44:64: - 44:f1:a0:86:81:82:a3:b1:e8:88:ab:54:34:be:57: - f0:c6:22:6e:71:58:9c:0e:04:52:78:1d:1d:9b:11: - 2a:e2:ad:7b:8b:7c:19:8e:0f:62:ae:1c:c4:83:98: - 5e:c2:6c:36:64:f9:9a:41:72:e1:38:46:15:f4:7d: - 20:68:3f:55:fc:9e:58:89:11:7b:65:56:c9:a5:20: - cc:bd:20:1f:2b:40:86:54:49:f6:5e:78:ca:7c:7d: - e9:81:16:ae:30:ad:a7:f9:21:77:72:9e:56:3d:08: - 51:c3:33:be:85:d7:e6:18:a9:61:43:e4:a1:ec:dc: - c4:8f - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Key Usage: critical - Digital Signature, Key Encipherment - X509v3 Extended Key Usage: - TLS Web Client Authentication - X509v3 Basic Constraints: critical - CA:FALSE - Signature Algorithm: sha256WithRSAEncryption - cd:63:7b:12:ed:b8:e3:93:b0:e3:df:11:a9:7f:20:2c:df:3f: - d6:97:de:6d:11:ff:21:8f:8f:04:2f:e3:78:ce:73:cd:4d:70: - be:5d:f8:27:83:31:7d:fa:b4:a0:31:14:87:be:c2:f5:e8:d2: - 45:10:3b:34:ca:dc:3e:e8:97:8c:76:61:3b:41:e2:e0:f3:34: - 4e:6d:56:1e:5d:5a:2d:8b:ac:a6:12:a9:90:c8:ae:12:8e:30: - e9:89:45:64:77:00:51:28:a5:a6:86:42:9c:6c:0f:f1:52:7d: - 9e:4c:83:43:8c:30:13:c7:00:1c:4c:ae:c8:ac:7a:b1:ce:d6: - 78:78:6b:1d:fe:48:8d:22:ec:97:a7:81:5b:01:e0:1f:d7:c6: - 0f:4b:c3:d2:43:5f:7f:d7:31:c2:3e:7f:22:d5:c3:97:2d:3b: - ad:81:6a:5b:a5:8e:94:fe:1e:47:5b:45:69:c7:f1:aa:fb:4e: - 67:09:8f:34:e8:fd:3c:97:39:e3:8b:91:ba:33:c7:40:50:2b: - 5e:9d:fc:26:ed:2e:89:89:f8:f3:b8:71:dc:02:36:cd:99:e8: - 71:92:3d:32:fb:4b:dc:b4:a5:cb:79:d8:dd:24:7c:97:f1:64: - 65:cc:b1:6f:8d:eb:64:b6:bd:54:97:11:e9:0c:47:41:89:80: - 99:cb:35:2a ------BEGIN CERTIFICATE----- -MIIDEzCCAfugAwIBAgIBCzANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtvcGVu -c2hpZnQtc2lnbmVyQDE0ODY0OTExNTgwHhcNMTcwMjA3MTgxOTM0WhcNMTkwMjA3 -MTgxOTM1WjA9MRUwEwYDVQQKEwxzeXN0ZW06bm9kZXMxJDAiBgNVBAMTG3N5c3Rl -bTpub2RlOm0wMS5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBAOcVdDaSmeXuSp+7VCHUjEDeTP3j9aH0nreBj3079sEzethlLoQmwAqf -CZp23qXGYm0R89+CC55buaH1FN/ltQ8QDGUzi4tdow9Af/0OcD0EINO2ukmyG5/9 -N+X905mo+y923wppvrchAA6AcxxeDyA63zouGS4exI98iuZlcdS48zbsGERkRPGg -hoGCo7HoiKtUNL5X8MYibnFYnA4EUngdHZsRKuKte4t8GY4PYq4cxIOYXsJsNmT5 -mkFy4ThGFfR9IGg/VfyeWIkRe2VWyaUgzL0gHytAhlRJ9l54ynx96YEWrjCtp/kh -d3KeVj0IUcMzvoXX5hipYUPkoezcxI8CAwEAAaM1MDMwDgYDVR0PAQH/BAQDAgWg -MBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQEL -BQADggEBAM1jexLtuOOTsOPfEal/ICzfP9aX3m0R/yGPjwQv43jOc81NcL5d+CeD -MX36tKAxFIe+wvXo0kUQOzTK3D7ol4x2YTtB4uDzNE5tVh5dWi2LrKYSqZDIrhKO -MOmJRWR3AFEopaaGQpxsD/FSfZ5Mg0OMMBPHABxMrsiserHO1nh4ax3+SI0i7Jen -gVsB4B/Xxg9Lw9JDX3/XMcI+fyLVw5ctO62BaluljpT+HkdbRWnH8ar7TmcJjzTo -/TyXOeOLkbozx0BQK16d/CbtLomJ+PO4cdwCNs2Z6HGSPTL7S9y0pct52N0kfJfx -ZGXMsW+N62S2vVSXEekMR0GJgJnLNSo= ------END CERTIFICATE----- diff --git a/roles/openshift_certificate_expiry/test/test_fakeopensslclasses.py b/roles/openshift_certificate_expiry/test/test_fakeopensslclasses.py index 2e245191f..ccdd48fa8 100644 --- a/roles/openshift_certificate_expiry/test/test_fakeopensslclasses.py +++ b/roles/openshift_certificate_expiry/test/test_fakeopensslclasses.py @@ -1,82 +1,89 @@ -#!/usr/bin/env python ''' Unit tests for the FakeOpenSSL classes ''' - import os +import subprocess import sys -import unittest + import pytest -# Disable import-error b/c our libraries aren't loaded in jenkins -# pylint: disable=import-error,wrong-import-position -# place class in our python path -module_path = os.path.join('/'.join(os.path.realpath(__file__).split(os.path.sep)[:-1]), 'library') -sys.path.insert(0, module_path) -openshift_cert_expiry = pytest.importorskip("openshift_cert_expiry") +MODULE_PATH = os.path.realpath(os.path.join(__file__, os.pardir, os.pardir, 'library')) +sys.path.insert(1, MODULE_PATH) + +# pylint: disable=import-error,wrong-import-position,missing-docstring +# pylint: disable=invalid-name,redefined-outer-name +from openshift_cert_expiry import FakeOpenSSLCertificate # noqa: E402 + + +@pytest.fixture(scope='module') +def fake_valid_cert(valid_cert): + cmd = ['openssl', 'x509', '-in', str(valid_cert['cert_file']), '-text'] + cert = subprocess.check_output(cmd) + return FakeOpenSSLCertificate(cert.decode('utf8')) + +def test_not_after(valid_cert, fake_valid_cert): + ''' Validate value returned back from get_notAfter() ''' + real_cert = valid_cert['cert'] -@pytest.mark.skip('Skipping all tests because of unresolved import errors') -class TestFakeOpenSSLClasses(unittest.TestCase): - ''' - Test class for FakeOpenSSL classes - ''' + # Internal representation of pyOpenSSL is bytes, while FakeOpenSSLCertificate + # is text, so decode the result from pyOpenSSL prior to comparing + assert real_cert.get_notAfter().decode('utf8') == fake_valid_cert.get_notAfter() - def setUp(self): - ''' setup method for other tests ''' - with open('test/system-node-m01.example.com.crt.txt', 'r') as fp: - self.cert_string = fp.read() - self.fake_cert = openshift_cert_expiry.FakeOpenSSLCertificate(self.cert_string) +def test_serial(valid_cert, fake_valid_cert): + ''' Validate value returned back form get_serialnumber() ''' + real_cert = valid_cert['cert'] + assert real_cert.get_serial_number() == fake_valid_cert.get_serial_number() - with open('test/master.server.crt.txt', 'r') as fp: - self.cert_san_string = fp.read() - self.fake_san_cert = openshift_cert_expiry.FakeOpenSSLCertificate(self.cert_san_string) +def test_get_subject(valid_cert, fake_valid_cert): + ''' Validate the certificate subject ''' - def test_FakeOpenSSLCertificate_get_serial_number(self): - """We can read the serial number from the cert""" - self.assertEqual(11, self.fake_cert.get_serial_number()) + # Gather the subject components and create a list of colon separated strings. + # Since the internal representation of pyOpenSSL uses bytes, we need to decode + # the results before comparing. + c_subjects = valid_cert['cert'].get_subject().get_components() + c_subj = ', '.join(['{}:{}'.format(x.decode('utf8'), y.decode('utf8')) for x, y in c_subjects]) + f_subjects = fake_valid_cert.get_subject().get_components() + f_subj = ', '.join(['{}:{}'.format(x, y) for x, y in f_subjects]) + assert c_subj == f_subj - def test_FakeOpenSSLCertificate_get_notAfter(self): - """We can read the cert expiry date""" - expiry = self.fake_cert.get_notAfter() - self.assertEqual('20190207181935Z', expiry) - def test_FakeOpenSSLCertificate_get_sans(self): - """We can read Subject Alt Names from a cert""" - ext = self.fake_san_cert.get_extension(0) +def get_san_extension(cert): + # Internal representation of pyOpenSSL is bytes, while FakeOpenSSLCertificate + # is text, so we need to set the value to search for accordingly. + if isinstance(cert, FakeOpenSSLCertificate): + san_short_name = 'subjectAltName' + else: + san_short_name = b'subjectAltName' - if ext.get_short_name() == 'subjectAltName': - sans = str(ext) + for i in range(cert.get_extension_count()): + ext = cert.get_extension(i) + if ext.get_short_name() == san_short_name: + # return the string representation to compare the actual SAN + # values instead of the data types + return str(ext) - self.assertEqual('DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:m01.example.com, DNS:openshift, DNS:openshift.default, DNS:openshift.default.svc, DNS:openshift.default.svc.cluster.local, DNS:172.30.0.1, DNS:192.168.122.241, IP Address:172.30.0.1, IP Address:192.168.122.241', sans) + return None - def test_FakeOpenSSLCertificate_get_sans_no_sans(self): - """We can tell when there are no Subject Alt Names in a cert""" - with self.assertRaises(IndexError): - self.fake_cert.get_extension(0) - def test_FakeOpenSSLCertificate_get_subject(self): - """We can read the Subject from a cert""" - # Subject: O=system:nodes, CN=system:node:m01.example.com - subject = self.fake_cert.get_subject() - subjects = [] - for name, value in subject.get_components(): - subjects.append('{}={}'.format(name, value)) +def test_subject_alt_names(valid_cert, fake_valid_cert): + real_cert = valid_cert['cert'] - self.assertEqual('O=system:nodes, CN=system:node:m01.example.com', ', '.join(subjects)) + san = get_san_extension(real_cert) + f_san = get_san_extension(fake_valid_cert) - def test_FakeOpenSSLCertificate_get_subject_san_cert(self): - """We can read the Subject from a cert with sans""" - # Subject: O=system:nodes, CN=system:node:m01.example.com - subject = self.fake_san_cert.get_subject() - subjects = [] - for name, value in subject.get_components(): - subjects.append('{}={}'.format(name, value)) + assert san == f_san - self.assertEqual('CN=172.30.0.1', ', '.join(subjects)) + # If there are either dns or ip sans defined, verify common_name present + if valid_cert['ip'] or valid_cert['dns']: + assert 'DNS:' + valid_cert['common_name'] in f_san + # Verify all ip sans are present + for ip in valid_cert['ip']: + assert 'IP Address:' + ip in f_san -if __name__ == "__main__": - unittest.main() + # Verify all dns sans are present + for name in valid_cert['dns']: + assert 'DNS:' + name in f_san diff --git a/roles/openshift_certificate_expiry/test/test_load_and_handle_cert.py b/roles/openshift_certificate_expiry/test/test_load_and_handle_cert.py new file mode 100644 index 000000000..98792e2ee --- /dev/null +++ b/roles/openshift_certificate_expiry/test/test_load_and_handle_cert.py @@ -0,0 +1,67 @@ +''' + Unit tests for the load_and_handle_cert method +''' +import datetime +import os +import sys + +import pytest + +MODULE_PATH = os.path.realpath(os.path.join(__file__, os.pardir, os.pardir, 'library')) +sys.path.insert(1, MODULE_PATH) + +# pylint: disable=import-error,wrong-import-position,missing-docstring +# pylint: disable=invalid-name,redefined-outer-name +import openshift_cert_expiry # noqa: E402 + +# TODO: More testing on the results of the load_and_handle_cert function +# could be implemented here as well, such as verifying subjects +# match up. + + +@pytest.fixture(params=['OpenSSLCertificate', 'FakeOpenSSLCertificate']) +def loaded_cert(request, valid_cert): + """ parameterized fixture to provide load_and_handle_cert results + for both OpenSSL and FakeOpenSSL parsed certificates + """ + now = datetime.datetime.now() + + openshift_cert_expiry.HAS_OPENSSL = request.param == 'OpenSSLCertificate' + + # valid_cert['cert_file'] is a `py.path.LocalPath` object and + # provides a read_text() method for reading the file contents. + cert_string = valid_cert['cert_file'].read_text('utf8') + + (subject, + expiry_date, + time_remaining, + serial) = openshift_cert_expiry.load_and_handle_cert(cert_string, now) + + return { + 'now': now, + 'subject': subject, + 'expiry_date': expiry_date, + 'time_remaining': time_remaining, + 'serial': serial, + } + + +def test_serial(loaded_cert, valid_cert): + """Params: + + * `loaded_cert` comes from the `loaded_cert` fixture in this file + * `valid_cert` comes from the 'valid_cert' fixture in conftest.py + """ + valid_cert_serial = valid_cert['cert'].get_serial_number() + assert loaded_cert['serial'] == valid_cert_serial + + +def test_expiry(loaded_cert): + """Params: + + * `loaded_cert` comes from the `loaded_cert` fixture in this file + """ + expiry_date = loaded_cert['expiry_date'] + time_remaining = loaded_cert['time_remaining'] + now = loaded_cert['now'] + assert expiry_date == now + time_remaining -- cgit v1.2.1