diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 31891dd5d..3b5339a1a 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -267,21 +267,29 @@ def _treat_as_renewal(config, domains): return None -def _report_new_cert(lineage): +def _report_new_cert(cert_path, fullchain_path): """ Reports the creation of a new certificate to the user. - :param RenewableCert lineage: the lineage of the new cert + :param string cert_path: path to cert + :param string fullchain_path: path to full chain """ - expiry = crypto_util.notAfter(lineage.cert).date() + expiry = crypto_util.notAfter(cert_path).date() reporter_util = zope.component.getUtility(interfaces.IReporter) - # Tell the user about fullchain.pem because that's what modern webservers - # (Nginx and Apache2.4) will want. + if fullchain_path: + # Print the path to fullchain.pem because that's what modern webservers + # (Nginx and Apache2.4) will want. + and_chain = "and chain have" + path = fullchain_path + else: + # Unless we're in .csr mode and there really isn't one + and_chain = "has " + path = cert_path # XXX Perhaps one day we could detect the presence of known old webservers # and say something more informative here. - msg = ("Congratulations! Your certificate and chain have been saved at {0}." - " Your cert will expire on {1}. To obtain a new version of the " + msg = ("Congratulations! Your certificate {0} been saved at {1}." + " Your cert will expire on {2}. To obtain a new version of the " "certificate in the future, simply run Let's Encrypt again." - .format(lineage.fullchain, expiry)) + .format(and_chain, path, expiry)) reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY) @@ -310,7 +318,7 @@ def _auth_from_domains(le_client, config, domains, plugins): if not lineage: raise Error("Certificate could not be obtained") - _report_new_cert(lineage) + _report_new_cert(lineage.cert, lineage.fullchain) return lineage @@ -461,9 +469,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")) - cert_path, _ = le_client.save_certificate( - certr, chain, args.cert_path, args.chain_path) - _report_new_cert(cert_path) + cert_path, _, cert_fullchain = le_client.save_certificate( + certr, chain, args.cert_path, args.chain_path, args.fullchain_path) + _report_new_cert(cert_path, cert_fullchain) else: domains = _find_domains(args, installer) _auth_from_domains(le_client, config, domains, plugins) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 732bdcf03..a7a4847b2 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -258,7 +258,7 @@ class Client(object): params, config, cli_config) return lineage - def save_certificate(self, certr, chain_cert, cert_path, chain_path): + def save_certificate(self, certr, chain_cert, cert_path, chain_path, fullchain_path): # pylint: disable=no-self-use """Saves the certificate received from the ACME server. @@ -268,20 +268,22 @@ class Client(object): :param list chain_cert: :param str cert_path: Candidate path to a certificate. :param str chain_path: Candidate path to a certificate chain. + :param str fullchain_path: Candidate path to a full cert chain. - :returns: cert_path, chain_path (absolute paths to the actual files) + :returns: cert_path, chain_path, fullchain_path (absolute paths to the actual files) :rtype: `tuple` of `str` :raises IOError: If unable to find room to write the cert files """ - for path in cert_path, chain_path: + for path in cert_path, chain_path, fullchain_path: le_util.make_or_verify_dir( os.path.dirname(path), 0o755, os.geteuid(), self.config.strict_permissions) # try finally close cert_chain_abspath = None + fullchain_abspath = None cert_file, act_cert_path = le_util.unique_file(cert_path, 0o644) # TODO: Except cert_pem = OpenSSL.crypto.dump_certificate( @@ -294,8 +296,7 @@ class Client(object): act_cert_path) if chain_cert: - chain_file, act_chain_path = le_util.unique_file( - chain_path, 0o644) + chain_file, act_chain_path = le_util.unique_file(chain_path, 0o644) # TODO: Except chain_pem = crypto_util.dump_pyopenssl_chain(chain_cert) try: @@ -308,7 +309,17 @@ class Client(object): # This expects a valid chain file cert_chain_abspath = os.path.abspath(act_chain_path) - return os.path.abspath(act_cert_path), cert_chain_abspath + # fullchain is cert + chain + fullchain_file, act_fullchain_path = le_util.unique_file( + fullchain_path, 0o644) + try: + fullchain_file.write(cert_pem + chain_pem) + finally: + fullchain_file.close() + logger.info("Cert chain written to %s", act_fullchain_path) + fullchain_abspath = os.path.abspath(act_fullchain_path) + + return os.path.abspath(act_cert_path), cert_chain_abspath, fullchain_abspath def deploy_certificate(self, domains, privkey_path, cert_path, chain_path, fullchain_path): diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 9d9164f24..73ab84bdf 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -149,7 +149,7 @@ class CLITest(unittest.TestCase): date = '1970-01-01' mock_notAfter().date.return_value = date - mock_lineage = mock.MagicMock(cert=cert_path) + mock_lineage = mock.MagicMock(cert=cert_path, fullchain=cert_path) mock_client = mock.MagicMock() mock_client.obtain_and_enroll_certificate.return_value = mock_lineage self._auth_new_request_common(mock_client) @@ -177,9 +177,10 @@ class CLITest(unittest.TestCase): @mock.patch('letsencrypt.cli._treat_as_renewal') @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' + cert_path = '/etc/letsencrypt/live/foo.bar/cert.pem' + chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem' - mock_lineage = mock.MagicMock(cert=cert_path) + mock_lineage = mock.MagicMock(cert=cert_path,fullchain=chain_path) mock_cert = mock.MagicMock(body='body') mock_key = mock.MagicMock(pem='pem_key') mock_renewal.return_value = mock_lineage @@ -195,7 +196,7 @@ class CLITest(unittest.TestCase): mock_lineage.update_all_links_to.assert_called_once_with( mock_lineage.latest_common_version()) self.assertTrue( - cert_path in mock_get_utility().add_message.call_args[0][0]) + chain_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') @@ -203,23 +204,24 @@ class CLITest(unittest.TestCase): @mock.patch('letsencrypt.cli._init_le_client') 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/blahcert.pem' 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_client.save_certificate.return_value = cert_path, None, None mock_init.return_value = mock_client installer = 'installer' self._call( ['-a', 'standalone', '-i', installer, 'auth', '--csr', CSR, - '--cert-path', cert_path, '--chain-path', '/']) + '--cert-path', cert_path, '--fullchain-path', '/', + '--chain-path', '/']) self.assertEqual(mock_pick_installer.call_args[0][1], installer) mock_client.save_certificate.assert_called_once_with( - 'certr', 'chain', cert_path, '/') + 'certr', 'chain', cert_path, '/', '/') self.assertTrue( cert_path in mock_get_utility().add_message.call_args[0][0]) self.assertTrue( diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 3f7b84a64..249778964 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -124,14 +124,18 @@ class ClientTest(unittest.TestCase): cert2 = test_util.load_cert(certs[2]) candidate_cert_path = os.path.join(tmp_path, "certs", "cert.pem") candidate_chain_path = os.path.join(tmp_path, "chains", "chain.pem") + candidate_fullchain_path = os.path.join(tmp_path, "chains", "fullchain.pem") - cert_path, chain_path = self.client.save_certificate( - certr, [cert1, cert2], candidate_cert_path, candidate_chain_path) + cert_path, chain_path, fullchain_path = self.client.save_certificate( + certr, [cert1, cert2], candidate_cert_path, candidate_chain_path, + candidate_fullchain_path) self.assertEqual(os.path.dirname(cert_path), os.path.dirname(candidate_cert_path)) self.assertEqual(os.path.dirname(chain_path), os.path.dirname(candidate_chain_path)) + self.assertEqual(os.path.dirname(fullchain_path), + os.path.dirname(candidate_fullchain_path)) with open(cert_path, "r") as cert_file: cert_contents = cert_file.read()