diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 208d629b5..280ddc751 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -269,9 +269,13 @@ def _treat_as_renewal(config, domains): def _report_new_cert(cert_path): """Reports the creation of a new certificate to the user.""" + expiry = crypto_util.notAfter(cert_path).date() reporter_util = zope.component.getUtility(interfaces.IReporter) reporter_util.add_message("Congratulations! Your certificate has been " - "saved at {0}.".format(cert_path), + "saved at {0} and will expire on {1}. To obtain " + "a new version of the certificate in the " + "future, simply run Let's Encrypt again.".format( + cert_path, expiry), reporter_util.MEDIUM_PRIORITY) @@ -301,12 +305,6 @@ def _auth_from_domains(le_client, config, domains, plugins): raise Error("Certificate could not be obtained") _report_new_cert(lineage.cert) - reporter_util = zope.component.getUtility(interfaces.IReporter) - reporter_util.add_message( - "Your certificate will expire on {0}. To obtain a new version of the " - "certificate in the future, simply run this client again.".format( - lineage.notafter().date()), - reporter_util.MEDIUM_PRIORITY) return lineage @@ -453,9 +451,9 @@ def auth(args, config, plugins): if args.csr is not None: certr, chain = le_client.obtain_certificate_from_csr(le_util.CSR( file=args.csr[0], data=args.csr[1], form="der")) - le_client.save_certificate( + cert_path, _ = le_client.save_certificate( certr, chain, args.cert_path, args.chain_path) - _report_new_cert(args.cert_path) + _report_new_cert(cert_path) else: domains = _find_domains(args, installer) _auth_from_domains(le_client, config, domains, plugins) diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index 61aa8b0db..f897ec852 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -8,6 +8,7 @@ import logging import os import OpenSSL +import pyrfc3339 import zope.component from acme import crypto_util as acme_crypto_util @@ -276,3 +277,48 @@ def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM): # assumes that OpenSSL.crypto.dump_certificate includes ending # newline character return "".join(_dump_cert(cert) for cert in chain) + + +def notBefore(cert_path): + """When does the cert at cert_path start being valid? + + :param str cert_path: path to a cert in PEM format + + :returns: the notBefore value from the cert at cert_path + :rtype: :class:`datetime.datetime` + + """ + return _notAfterBefore(cert_path, OpenSSL.crypto.X509.get_notBefore) + + +def notAfter(cert_path): + """When does the cert at cert_path stop being valid? + + :param str cert_path: path to a cert in PEM format + + :returns: the notAfter value from the cert at cert_path + :rtype: :class:`datetime.datetime` + + """ + return _notAfterBefore(cert_path, OpenSSL.crypto.X509.get_notAfter) + + +def _notAfterBefore(cert_path, method): + """Internal helper function for finding notbefore/notafter. + + :param str cert_path: path to a cert in PEM format + :param function method: one of ``OpenSSL.crypto.X509.get_notBefore`` + or ``OpenSSL.crypto.X509.get_notAfter`` + + :returns: the notBefore or notAfter value from the cert at cert_path + :rtype: :class:`datetime.datetime` + + """ + with open(cert_path) as f: + x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, + f.read()) + timestamp = method(x509) + reformatted_timestamp = [timestamp[0:4], "-", timestamp[4:6], "-", + timestamp[6:8], "T", timestamp[8:10], ":", + timestamp[10:12], ":", timestamp[12:]] + return pyrfc3339.parse("".join(reformatted_timestamp)) diff --git a/letsencrypt/storage.py b/letsencrypt/storage.py index 8a0f4829e..12411ffb4 100644 --- a/letsencrypt/storage.py +++ b/letsencrypt/storage.py @@ -5,10 +5,8 @@ import re import time import configobj -import OpenSSL import parsedatetime import pytz -import pyrfc3339 from letsencrypt import constants from letsencrypt import crypto_util @@ -381,47 +379,6 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes for kind in ALL_FOUR: self._update_link_to(kind, version) - def _notafterbefore(self, method, version): - """Internal helper function for finding notbefore/notafter.""" - if version is None: - target = self.current_target("cert") - else: - target = self.version("cert", version) - pem = open(target).read() - x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, - pem) - i = method(x509) - return pyrfc3339.parse(i[0:4] + "-" + i[4:6] + "-" + i[6:8] + "T" + - i[8:10] + ":" + i[10:12] + ":" + i[12:]) - - def notbefore(self, version=None): - """When does the specified cert version start being valid? - - (If no version is specified, use the current version.) - - :param int version: the desired version number - - :returns: the notBefore value from the specified cert version in - this lineage - :rtype: :class:`datetime.datetime` - - """ - return self._notafterbefore(lambda x509: x509.get_notBefore(), version) - - def notafter(self, version=None): - """When does the specified cert version stop being valid? - - (If no version is specified, use the current version.) - - :param int version: the desired version number - - :returns: the notAfter value from the specified cert version in - this lineage - :rtype: :class:`datetime.datetime` - - """ - return self._notafterbefore(lambda x509: x509.get_notAfter(), version) - def names(self, version=None): """What are the subject names of this certificate? @@ -470,7 +427,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes interval = self.configuration.get("deploy_before_expiry", "5 days") autodeploy_interval = parse_time_interval(interval) - expiry = self.notafter() + expiry = crypto_util.notAfter(self.current_target("cert")) now = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC) remaining = expiry - now if remaining < autodeploy_interval: @@ -537,7 +494,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes # Renewals on the basis of expiry time interval = self.configuration.get("renew_before_expiry", "10 days") autorenew_interval = parse_time_interval(interval) - expiry = self.notafter(self.latest_common_version()) + expiry = crypto_util.notAfter(self.version( + "cert", self.latest_common_version())) now = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC) remaining = expiry - now if remaining < autorenew_interval: diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 447c19d7a..4522ccbf4 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -141,18 +141,23 @@ class CLITest(unittest.TestCase): ret, _, _, _ = self._call(['-a', 'bad_auth', 'auth']) self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed') + @mock.patch('letsencrypt.crypto_util.notAfter') @mock.patch('letsencrypt.cli.zope.component.getUtility') - def test_auth_new_request_success(self, mock_get_utility): + def test_auth_new_request_success(self, mock_get_utility, mock_notAfter): cert_path = '/etc/letsencrypt/live/foo.bar' + date = '1970-01-01' + mock_notAfter().date.return_value = date + mock_lineage = mock.MagicMock(cert=cert_path) mock_client = mock.MagicMock() mock_client.obtain_and_enroll_certificate.return_value = mock_lineage self._auth_new_request_common(mock_client) self.assertEqual( mock_client.obtain_and_enroll_certificate.call_count, 1) - msg = mock_get_utility().add_message.call_args_list[0][0][0] - self.assertTrue(cert_path in msg) - self.assertEqual(mock_get_utility().add_message.call_count, 2) + self.assertTrue( + cert_path in mock_get_utility().add_message.call_args[0][0]) + self.assertTrue( + date in mock_get_utility().add_message.call_args[0][0]) def test_auth_new_request_failure(self): mock_client = mock.MagicMock() @@ -172,6 +177,7 @@ class CLITest(unittest.TestCase): @mock.patch('letsencrypt.cli._init_le_client') def test_auth_renewal(self, mock_init, mock_renewal, mock_get_utility): cert_path = '/etc/letsencrypt/live/foo.bar' + mock_lineage = mock.MagicMock(cert=cert_path) mock_cert = mock.MagicMock(body='body') mock_key = mock.MagicMock(pem='pem_key') @@ -187,19 +193,25 @@ class CLITest(unittest.TestCase): self.assertEqual(mock_lineage.save_successor.call_count, 1) mock_lineage.update_all_links_to.assert_called_once_with( mock_lineage.latest_common_version()) - msg = mock_get_utility().add_message.call_args_list[0][0][0] - self.assertTrue(cert_path in msg) - self.assertEqual(mock_get_utility().add_message.call_count, 2) + self.assertTrue( + cert_path in mock_get_utility().add_message.call_args[0][0]) + @mock.patch('letsencrypt.crypto_util.notAfter') @mock.patch('letsencrypt.cli.display_ops.pick_installer') @mock.patch('letsencrypt.cli.zope.component.getUtility') @mock.patch('letsencrypt.cli._init_le_client') - def test_auth_csr(self, mock_init, mock_get_utility, mock_pick_installer): + def test_auth_csr(self, mock_init, mock_get_utility, + mock_pick_installer, mock_notAfter): cert_path = '/etc/letsencrypt/live/foo.bar' + date = '1970-01-01' + mock_notAfter().date.return_value = date + mock_client = mock.MagicMock() mock_client.obtain_certificate_from_csr.return_value = ('certr', 'chain') + mock_client.save_certificate.return_value = cert_path, None mock_init.return_value = mock_client + installer = 'installer' self._call( ['-a', 'standalone', '-i', installer, 'auth', '--csr', CSR, @@ -209,6 +221,8 @@ class CLITest(unittest.TestCase): 'certr', 'chain', cert_path, '/') self.assertTrue( cert_path in mock_get_utility().add_message.call_args[0][0]) + self.assertTrue( + date in mock_get_utility().add_message.call_args[0][0]) @mock.patch('letsencrypt.cli.sys') def test_handle_exception(self, mock_sys): diff --git a/letsencrypt/tests/crypto_util_test.py b/letsencrypt/tests/crypto_util_test.py index 2e04c748a..f0b8c0e4d 100644 --- a/letsencrypt/tests/crypto_util_test.py +++ b/letsencrypt/tests/crypto_util_test.py @@ -15,6 +15,7 @@ from letsencrypt.tests import test_util RSA256_KEY = test_util.load_vector('rsa256_key.pem') RSA512_KEY = test_util.load_vector('rsa512_key.pem') +CERT_PATH = test_util.vector_path('cert.pem') CERT = test_util.load_vector('cert.pem') SAN_CERT = test_util.load_vector('cert-san.pem') @@ -232,5 +233,23 @@ class CertLoaderTest(unittest.TestCase): pyopenssl_load_certificate(bad_cert_data) +class NotBeforeTest(unittest.TestCase): + """Tests for letsencrypt.crypto_util.notBefore""" + + def test_notBefore(self): + from letsencrypt.crypto_util import notBefore + self.assertEqual(notBefore(CERT_PATH).isoformat(), + '2014-12-11T22:34:45+00:00') + + +class NotAfterTest(unittest.TestCase): + """Tests for letsencrypt.crypto_util.notAfter""" + + def test_notAfter(self): + from letsencrypt.crypto_util import notAfter + self.assertEqual(notAfter(CERT_PATH).isoformat(), + '2014-12-18T22:34:45+00:00') + + if __name__ == '__main__': unittest.main() # pragma: no cover diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index 3077be25d..3599d8ad3 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -7,7 +7,6 @@ import unittest import configobj import mock -import pytz from letsencrypt import configuration from letsencrypt import errors @@ -348,26 +347,6 @@ class RenewableCertTests(BaseRenewableCertTest): self.assertEqual(self.test_rc.names(12), ["example.com", "www.example.com"]) - def _test_notafterbefore(self, function, timestamp): - test_cert = test_util.load_vector("cert.pem") - os.symlink(os.path.join("..", "..", "archive", "example.org", - "cert12.pem"), self.test_rc.cert) - with open(self.test_rc.cert, "w") as f: - f.write(test_cert) - desired_time = datetime.datetime.utcfromtimestamp(timestamp) - desired_time = desired_time.replace(tzinfo=pytz.UTC) - for result in (function(), function(12)): - self.assertEqual(result, desired_time) - self.assertEqual(result.utcoffset(), datetime.timedelta(0)) - - def test_notbefore(self): - self._test_notafterbefore(self.test_rc.notbefore, 1418337285) - # 2014-12-11 22:34:45+00:00 = Unix time 1418337285 - - def test_notafter(self): - self._test_notafterbefore(self.test_rc.notafter, 1418942085) - # 2014-12-18 22:34:45+00:00 = Unix time 1418942085 - @mock.patch("letsencrypt.storage.datetime") def test_time_interval_judgments(self, mock_datetime): """Test should_autodeploy() and should_autorenew() on the basis