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
6 changed files with 97 additions and 83 deletions
+7 -9
View File
@@ -269,9 +269,13 @@ def _treat_as_renewal(config, domains):
def _report_new_cert(cert_path): def _report_new_cert(cert_path):
"""Reports the creation of a new certificate to the user.""" """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 = zope.component.getUtility(interfaces.IReporter)
reporter_util.add_message("Congratulations! Your certificate has been " 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) reporter_util.MEDIUM_PRIORITY)
@@ -301,12 +305,6 @@ def _auth_from_domains(le_client, config, domains, plugins):
raise Error("Certificate could not be obtained") raise Error("Certificate could not be obtained")
_report_new_cert(lineage.cert) _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 return lineage
@@ -453,9 +451,9 @@ def auth(args, config, plugins):
if args.csr is not None: if args.csr is not None:
certr, chain = le_client.obtain_certificate_from_csr(le_util.CSR( certr, chain = le_client.obtain_certificate_from_csr(le_util.CSR(
file=args.csr[0], data=args.csr[1], form="der")) 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) certr, chain, args.cert_path, args.chain_path)
_report_new_cert(args.cert_path) _report_new_cert(cert_path)
else: else:
domains = _find_domains(args, installer) domains = _find_domains(args, installer)
_auth_from_domains(le_client, config, domains, plugins) _auth_from_domains(le_client, config, domains, plugins)
+46
View File
@@ -8,6 +8,7 @@ import logging
import os import os
import OpenSSL import OpenSSL
import pyrfc3339
import zope.component import zope.component
from acme import crypto_util as acme_crypto_util 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 # assumes that OpenSSL.crypto.dump_certificate includes ending
# newline character # newline character
return "".join(_dump_cert(cert) for cert in chain) 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))
+3 -45
View File
@@ -5,10 +5,8 @@ import re
import time import time
import configobj import configobj
import OpenSSL
import parsedatetime import parsedatetime
import pytz import pytz
import pyrfc3339
from letsencrypt import constants from letsencrypt import constants
from letsencrypt import crypto_util from letsencrypt import crypto_util
@@ -381,47 +379,6 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
for kind in ALL_FOUR: for kind in ALL_FOUR:
self._update_link_to(kind, version) 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): def names(self, version=None):
"""What are the subject names of this certificate? """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", interval = self.configuration.get("deploy_before_expiry",
"5 days") "5 days")
autodeploy_interval = parse_time_interval(interval) 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) now = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC)
remaining = expiry - now remaining = expiry - now
if remaining < autodeploy_interval: if remaining < autodeploy_interval:
@@ -537,7 +494,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
# Renewals on the basis of expiry time # Renewals on the basis of expiry time
interval = self.configuration.get("renew_before_expiry", "10 days") interval = self.configuration.get("renew_before_expiry", "10 days")
autorenew_interval = parse_time_interval(interval) 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) now = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC)
remaining = expiry - now remaining = expiry - now
if remaining < autorenew_interval: if remaining < autorenew_interval:
+22 -8
View File
@@ -141,18 +141,23 @@ class CLITest(unittest.TestCase):
ret, _, _, _ = self._call(['-a', 'bad_auth', 'auth']) ret, _, _, _ = self._call(['-a', 'bad_auth', 'auth'])
self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed') 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') @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' 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_lineage = mock.MagicMock(cert=cert_path)
mock_client = mock.MagicMock() mock_client = mock.MagicMock()
mock_client.obtain_and_enroll_certificate.return_value = mock_lineage mock_client.obtain_and_enroll_certificate.return_value = mock_lineage
self._auth_new_request_common(mock_client) self._auth_new_request_common(mock_client)
self.assertEqual( self.assertEqual(
mock_client.obtain_and_enroll_certificate.call_count, 1) mock_client.obtain_and_enroll_certificate.call_count, 1)
msg = mock_get_utility().add_message.call_args_list[0][0][0] self.assertTrue(
self.assertTrue(cert_path in msg) cert_path in mock_get_utility().add_message.call_args[0][0])
self.assertEqual(mock_get_utility().add_message.call_count, 2) self.assertTrue(
date in mock_get_utility().add_message.call_args[0][0])
def test_auth_new_request_failure(self): def test_auth_new_request_failure(self):
mock_client = mock.MagicMock() mock_client = mock.MagicMock()
@@ -172,6 +177,7 @@ class CLITest(unittest.TestCase):
@mock.patch('letsencrypt.cli._init_le_client') @mock.patch('letsencrypt.cli._init_le_client')
def test_auth_renewal(self, mock_init, mock_renewal, mock_get_utility): def test_auth_renewal(self, mock_init, mock_renewal, mock_get_utility):
cert_path = '/etc/letsencrypt/live/foo.bar' cert_path = '/etc/letsencrypt/live/foo.bar'
mock_lineage = mock.MagicMock(cert=cert_path) mock_lineage = mock.MagicMock(cert=cert_path)
mock_cert = mock.MagicMock(body='body') mock_cert = mock.MagicMock(body='body')
mock_key = mock.MagicMock(pem='pem_key') mock_key = mock.MagicMock(pem='pem_key')
@@ -187,19 +193,25 @@ class CLITest(unittest.TestCase):
self.assertEqual(mock_lineage.save_successor.call_count, 1) self.assertEqual(mock_lineage.save_successor.call_count, 1)
mock_lineage.update_all_links_to.assert_called_once_with( mock_lineage.update_all_links_to.assert_called_once_with(
mock_lineage.latest_common_version()) mock_lineage.latest_common_version())
msg = mock_get_utility().add_message.call_args_list[0][0][0] self.assertTrue(
self.assertTrue(cert_path in msg) cert_path in mock_get_utility().add_message.call_args[0][0])
self.assertEqual(mock_get_utility().add_message.call_count, 2)
@mock.patch('letsencrypt.crypto_util.notAfter')
@mock.patch('letsencrypt.cli.display_ops.pick_installer') @mock.patch('letsencrypt.cli.display_ops.pick_installer')
@mock.patch('letsencrypt.cli.zope.component.getUtility') @mock.patch('letsencrypt.cli.zope.component.getUtility')
@mock.patch('letsencrypt.cli._init_le_client') @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' cert_path = '/etc/letsencrypt/live/foo.bar'
date = '1970-01-01'
mock_notAfter().date.return_value = date
mock_client = mock.MagicMock() mock_client = mock.MagicMock()
mock_client.obtain_certificate_from_csr.return_value = ('certr', mock_client.obtain_certificate_from_csr.return_value = ('certr',
'chain') 'chain')
mock_client.save_certificate.return_value = cert_path, None
mock_init.return_value = mock_client mock_init.return_value = mock_client
installer = 'installer' installer = 'installer'
self._call( self._call(
['-a', 'standalone', '-i', installer, 'auth', '--csr', CSR, ['-a', 'standalone', '-i', installer, 'auth', '--csr', CSR,
@@ -209,6 +221,8 @@ class CLITest(unittest.TestCase):
'certr', 'chain', cert_path, '/') 'certr', 'chain', cert_path, '/')
self.assertTrue( self.assertTrue(
cert_path in mock_get_utility().add_message.call_args[0][0]) 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') @mock.patch('letsencrypt.cli.sys')
def test_handle_exception(self, mock_sys): def test_handle_exception(self, mock_sys):
+19
View File
@@ -15,6 +15,7 @@ from letsencrypt.tests import test_util
RSA256_KEY = test_util.load_vector('rsa256_key.pem') RSA256_KEY = test_util.load_vector('rsa256_key.pem')
RSA512_KEY = test_util.load_vector('rsa512_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') CERT = test_util.load_vector('cert.pem')
SAN_CERT = test_util.load_vector('cert-san.pem') SAN_CERT = test_util.load_vector('cert-san.pem')
@@ -232,5 +233,23 @@ class CertLoaderTest(unittest.TestCase):
pyopenssl_load_certificate(bad_cert_data) 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__': if __name__ == '__main__':
unittest.main() # pragma: no cover unittest.main() # pragma: no cover
-21
View File
@@ -7,7 +7,6 @@ import unittest
import configobj import configobj
import mock import mock
import pytz
from letsencrypt import configuration from letsencrypt import configuration
from letsencrypt import errors from letsencrypt import errors
@@ -348,26 +347,6 @@ class RenewableCertTests(BaseRenewableCertTest):
self.assertEqual(self.test_rc.names(12), self.assertEqual(self.test_rc.names(12),
["example.com", "www.example.com"]) ["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") @mock.patch("letsencrypt.storage.datetime")
def test_time_interval_judgments(self, mock_datetime): def test_time_interval_judgments(self, mock_datetime):
"""Test should_autodeploy() and should_autorenew() on the basis """Test should_autodeploy() and should_autorenew() on the basis