summaryrefslogtreecommitdiffstats
path: root/roles
diff options
context:
space:
mode:
Diffstat (limited to 'roles')
-rw-r--r--roles/openshift_certificate_expiry/library/openshift_cert_expiry.py116
-rw-r--r--roles/openshift_certificate_expiry/test/conftest.py116
-rw-r--r--roles/openshift_certificate_expiry/test/master.server.crt42
-rw-r--r--roles/openshift_certificate_expiry/test/master.server.crt.txt82
-rw-r--r--roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt19
-rw-r--r--roles/openshift_certificate_expiry/test/system-node-m01.example.com.crt.txt75
-rw-r--r--roles/openshift_certificate_expiry/test/test_fakeopensslclasses.py121
-rw-r--r--roles/openshift_certificate_expiry/test/test_load_and_handle_cert.py67
8 files changed, 289 insertions, 349 deletions
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