From 288e304643a6a02e6d90ed5c1b4e7f6b349ad929 Mon Sep 17 00:00:00 2001 From: Tim Bielawa Date: Mon, 20 Feb 2017 15:58:06 -0800 Subject: Implement fake openssl cert classes --- .../library/openshift_cert_expiry.py | 197 +++++++++++++++++++-- .../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 | 86 +++++++++ 6 files changed, 485 insertions(+), 16 deletions(-) create mode 100644 roles/openshift_certificate_expiry/test/master.server.crt create mode 100644 roles/openshift_certificate_expiry/test/master.server.crt.txt create mode 100644 roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt create mode 100644 roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt.txt create mode 100644 roles/openshift_certificate_expiry/test/test_fakeopensslclasses.py (limited to 'roles') diff --git a/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py b/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py index 85671b164..33930c0c1 100644 --- a/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py +++ b/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py @@ -5,13 +5,32 @@ """For details on this module see DOCUMENTATION (below)""" import datetime +import io import os import subprocess +import sys +import tempfile +# File pointers from io.open require unicode inputs when using their +# `write` method +import six from six.moves import configparser import yaml -import OpenSSL.crypto +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 + # HOST (or any host that just doesn't have PyOpenSSL + # available). That will force the `load_and_handle_cert` function + # to use the Fake OpenSSL classes. + import OpenSSL.crypto +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 DOCUMENTATION = ''' --- @@ -66,6 +85,128 @@ EXAMPLES = ''' ''' +class FakeOpenSSLCertificate(object): + """This provides a rough mock of what you get from +`OpenSSL.crypto.load_certificate()`. This is a work-around for +platforms missing the Python OpenSSL library. + """ + def __init__(self, cert_string): + """`cert_string` is a certificate in the form you get from running a +.crt through 'openssl x509 -in CERT.cert -text'""" + self.cert_string = cert_string + self.serial = None + self.subject = None + self.extensions = [] + self.not_after = None + self._parse_cert() + + def _parse_cert(self): + """Manually parse the certificate line by line""" + self.extensions = [] + + PARSING_ALT_NAMES = False + for line in self.cert_string.split('\n'): + l = line.strip() + if PARSING_ALT_NAMES: + # We're parsing a 'Subject Alternative Name' line + self.extensions.append( + FakeOpenSSLCertificateSANExtension(l)) + + PARSING_ALT_NAMES = False + continue + + # parse out the bits that we can + if l.startswith('Serial Number:'): + # Serial Number: 11 (0xb) + # => 11 + self.serial = int(l.split()[-2]) + + elif l.startswith('Not After :'): + # Not After : Feb 7 18:19:35 2019 GMT + # => strptime(str, '%b %d %H:%M:%S %Y %Z') + # => strftime('%Y%m%d%H%M%SZ') + # => 20190207181935Z + not_after_raw = l.partition(' : ')[-1] + # Last item: ('Not After', ' : ', 'Feb 7 18:19:35 2019 GMT') + not_after_parsed = datetime.datetime.strptime(not_after_raw, '%b %d %H:%M:%S %Y %Z') + self.not_after = not_after_parsed.strftime('%Y%m%d%H%M%SZ') + + elif l.startswith('X509v3 Subject Alternative Name:'): + PARSING_ALT_NAMES = True + continue + + elif l.startswith('Subject:'): + # O=system:nodes, CN=system:node:m01.example.com + self.subject = FakeOpenSSLCertificateSubjects(l.partition(': ')[-1]) + + def get_serial_number(self): + """Return the serial number of the cert""" + return self.serial + + def get_subject(self): + """Subjects must implement get_components() and return dicts or +tuples. An 'openssl x509 -in CERT.cert -text' with 'Subject': + + Subject: Subject: O=system:nodes, CN=system:node:m01.example.com + +might return: [('O=system', 'nodes'), ('CN=system', 'node:m01.example.com')] + """ + return self.subject + + def get_extension(self, i): + """Extensions must implement get_short_name() and return the string +'subjectAltName'""" + return self.extensions[i] + + def get_notAfter(self): + """Returns a date stamp as a string in the form +'20180922170439Z'. strptime the result with format param: +'%Y%m%d%H%M%SZ'.""" + return self.not_after + + +class FakeOpenSSLCertificateSANExtension(object): # pylint: disable=too-few-public-methods + """Mocks what happens when `get_extension` is called on a certificate +object""" + + def __init__(self, san_string): + """With `san_string` as you get from: + + $ openssl x509 -in certificate.crt -text + """ + self.san_string = san_string + self.short_name = 'subjectAltName' + + def get_short_name(self): + """Return the 'type' of this extension. It's always the same though +because we only care about subjectAltName's""" + return self.short_name + + def __str__(self): + """Return this extension and the value as a simple string""" + return self.san_string + + +# pylint: disable=too-few-public-methods +class FakeOpenSSLCertificateSubjects(object): + """Mocks what happens when `get_subject` is called on a certificate +object""" + + def __init__(self, subject_string): + """With `subject_string` as you get from: + + $ openssl x509 -in certificate.crt -text + """ + self.subjects = [] + for s in subject_string.split(', '): + name, _, value = s.partition('=') + self.subjects.append((name, value)) + + def get_components(self): + """Returns a list of tuples""" + return self.subjects + + # We only need this for one thing, we don't care if it doesn't have # that many public methods # @@ -100,7 +241,8 @@ will be returned return [p for p in path_list if os.path.exists(os.path.realpath(p))] -def load_and_handle_cert(cert_string, now, base64decode=False): +# pylint: disable=too-many-locals,too-many-branches +def load_and_handle_cert(cert_string, now, base64decode=False, ans_module=None): """Load a certificate, split off the good parts, and return some useful data @@ -109,6 +251,7 @@ Params: - `cert_string` (string) - a certificate loaded into a string object - `now` (datetime) - a datetime object of the time to calculate the certificate 'time_remaining' against - `base64decode` (bool) - run .decode('base64') on the input? +- `ans_module` (AnsibleModule) - The AnsibleModule object for this module (so we can raise errors) Returns: A 3-tuple of the form: (certificate_common_name, certificate_expiry_date, certificate_time_remaining) @@ -119,10 +262,33 @@ A 3-tuple of the form: (certificate_common_name, certificate_expiry_date, certif else: _cert_string = cert_string - cert_loaded = OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, _cert_string) - - cert_serial = cert_loaded.get_serial_number() + # Disable this. We 'redefine' the type because we are working + # around a missing library on the target host. + # + # pylint: disable=redefined-variable-type + if 'OpenSSL.crypto' in sys.modules: + # 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' + # 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) + try: + openssl_decoded = subprocess.Popen(cmd.split(), + stdout=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] + os.remove(path) + cert_loaded = FakeOpenSSLCertificate(openssl_decoded) ###################################################################### # Read all possible names from the cert @@ -172,15 +338,14 @@ A 3-tuple of the form: (certificate_common_name, certificate_expiry_date, certif ###################################################################### # Grab the expiration date - cert_expiry = cert_loaded.get_notAfter() cert_expiry_date = datetime.datetime.strptime( - cert_expiry, + cert_loaded.get_notAfter(), # example get_notAfter() => 20180922170439Z '%Y%m%d%H%M%SZ') time_remaining = cert_expiry_date - now - return (cert_subject, cert_expiry_date, time_remaining, cert_serial) + return (cert_subject, cert_expiry_date, time_remaining, cert_loaded.get_serial_number()) def classify_cert(cert_meta, now, time_remaining, expire_window, cert_list): @@ -379,7 +544,7 @@ an OpenShift Container Platform cluster (cert_subject, cert_expiry_date, time_remaining, - cert_serial) = load_and_handle_cert(cert, now) + cert_serial) = load_and_handle_cert(cert, now, ans_module=module) expire_check_result = { 'cert_cn': cert_subject, @@ -428,7 +593,7 @@ an OpenShift Container Platform cluster (cert_subject, cert_expiry_date, time_remaining, - cert_serial) = load_and_handle_cert(c, now, base64decode=True) + cert_serial) = load_and_handle_cert(c, now, base64decode=True, ans_module=module) expire_check_result = { 'cert_cn': cert_subject, @@ -458,7 +623,7 @@ an OpenShift Container Platform cluster (cert_subject, cert_expiry_date, time_remaining, - cert_serial) = load_and_handle_cert(c, now, base64decode=True) + cert_serial) = load_and_handle_cert(c, now, base64decode=True, ans_module=module) expire_check_result = { 'cert_cn': cert_subject, @@ -512,7 +677,7 @@ an OpenShift Container Platform cluster (cert_subject, cert_expiry_date, time_remaining, - cert_serial) = load_and_handle_cert(c, now) + cert_serial) = load_and_handle_cert(c, now, ans_module=module) expire_check_result = { 'cert_cn': cert_subject, @@ -551,7 +716,7 @@ an OpenShift Container Platform cluster (cert_subject, cert_expiry_date, time_remaining, - cert_serial) = load_and_handle_cert(etcd_fp.read(), now) + cert_serial) = load_and_handle_cert(etcd_fp.read(), now, ans_module=module) expire_check_result = { 'cert_cn': cert_subject, @@ -597,7 +762,7 @@ an OpenShift Container Platform cluster (cert_subject, cert_expiry_date, time_remaining, - cert_serial) = load_and_handle_cert(router_c, now, base64decode=True) + cert_serial) = load_and_handle_cert(router_c, now, base64decode=True, ans_module=module) expire_check_result = { 'cert_cn': cert_subject, @@ -628,7 +793,7 @@ an OpenShift Container Platform cluster (cert_subject, cert_expiry_date, time_remaining, - cert_serial) = load_and_handle_cert(registry_c, now, base64decode=True) + cert_serial) = load_and_handle_cert(registry_c, now, base64decode=True, ans_module=module) expire_check_result = { 'cert_cn': cert_subject, diff --git a/roles/openshift_certificate_expiry/test/master.server.crt b/roles/openshift_certificate_expiry/test/master.server.crt new file mode 100644 index 000000000..51aa85c8c --- /dev/null +++ b/roles/openshift_certificate_expiry/test/master.server.crt @@ -0,0 +1,42 @@ +-----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 new file mode 100644 index 000000000..6b3c8fb03 --- /dev/null +++ b/roles/openshift_certificate_expiry/test/master.server.crt.txt @@ -0,0 +1,82 @@ +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 new file mode 100644 index 000000000..cd13ddc38 --- /dev/null +++ b/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt @@ -0,0 +1,19 @@ +-----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 new file mode 100644 index 000000000..67a3eb81c --- /dev/null +++ b/roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt.txt @@ -0,0 +1,75 @@ +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 new file mode 100644 index 000000000..e98d6ac64 --- /dev/null +++ b/roles/openshift_certificate_expiry/test/test_fakeopensslclasses.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +''' + Unit tests for the FakeOpenSSL classes +''' + +import os +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('/')[:-1]), 'library') +sys.path.insert(0, module_path) +openshift_cert_expiry = pytest.importorskip("openshift_cert_expiry") + + +@pytest.mark.skip('Skipping all tests because of unresolved import errors') +class TestFakeOpenSSLClasses(unittest.TestCase): + ''' + Test class for FakeOpenSSL classes + ''' + + 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) + + 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_FakeOpenSSLCertificate_get_serial_number(self): + """We can read the serial number from the cert""" + self.assertEqual(11, self.fake_cert.get_serial_number()) + + 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) + + if ext.get_short_name() == 'subjectAltName': + sans = 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) + + 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)) + + self.assertEqual('O=system:nodes, CN=system:node:m01.example.com', ', '.join(subjects)) + + 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)) + + self.assertEqual('CN=172.30.0.1', ', '.join(subjects)) + + def tearDown(self): + '''TearDown method''' + pass + + +if __name__ == "__main__": + unittest.main() -- cgit v1.2.3 From 003bc8d5b9233e61c7a2a5f0a27b66ac1babc1a0 Mon Sep 17 00:00:00 2001 From: Tim Bielawa Date: Tue, 21 Feb 2017 11:00:00 -0800 Subject: Address cert expiry parsing review comments --- .../library/openshift_cert_expiry.py | 9 ++++++--- .../openshift_certificate_expiry/test/test_fakeopensslclasses.py | 6 +----- 2 files changed, 7 insertions(+), 8 deletions(-) (limited to 'roles') diff --git a/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py b/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py index 33930c0c1..b093d84fe 100644 --- a/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py +++ b/roles/openshift_certificate_expiry/library/openshift_cert_expiry.py @@ -242,6 +242,8 @@ will be returned # pylint: disable=too-many-locals,too-many-branches +# +# TODO: Break this function down into smaller chunks def load_and_handle_cert(cert_string, now, base64decode=False, ans_module=None): """Load a certificate, split off the good parts, and return some useful data @@ -254,8 +256,8 @@ Params: - `ans_module` (AnsibleModule) - The AnsibleModule object for this module (so we can raise errors) Returns: -A 3-tuple of the form: (certificate_common_name, certificate_expiry_date, certificate_time_remaining) - +A tuple of the form: + (cert_subject, cert_expiry_date, time_remaining, cert_serial_number) """ if base64decode: _cert_string = cert_string.decode('base-64') @@ -287,8 +289,9 @@ A 3-tuple of the form: (certificate_common_name, certificate_expiry_date, certif 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] - os.remove(path) cert_loaded = FakeOpenSSLCertificate(openssl_decoded) + finally: + os.remove(path) ###################################################################### # Read all possible names from the cert diff --git a/roles/openshift_certificate_expiry/test/test_fakeopensslclasses.py b/roles/openshift_certificate_expiry/test/test_fakeopensslclasses.py index e98d6ac64..2e245191f 100644 --- a/roles/openshift_certificate_expiry/test/test_fakeopensslclasses.py +++ b/roles/openshift_certificate_expiry/test/test_fakeopensslclasses.py @@ -11,7 +11,7 @@ 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('/')[:-1]), 'library') +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") @@ -77,10 +77,6 @@ class TestFakeOpenSSLClasses(unittest.TestCase): self.assertEqual('CN=172.30.0.1', ', '.join(subjects)) - def tearDown(self): - '''TearDown method''' - pass - if __name__ == "__main__": unittest.main() -- cgit v1.2.3