Merge pull request #1020 from letsencrypt/report_csr_expiry

Report expiration when using a CSR
This commit is contained in:
bmw 2015-10-20 09:41:48 -07:00
commit 85e6f50f68
6 changed files with 97 additions and 83 deletions

View file

@ -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)

View file

@ -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))

View file

@ -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:

View file

@ -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):

View file

@ -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

View file

@ -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