mirror of
https://github.com/certbot/certbot.git
synced 2026-08-03 03:52:02 +02:00
Merge pull request #1020 from letsencrypt/report_csr_expiry
Report expiration when using a CSR
This commit is contained in:
+7
-9
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user