From 635e5852262ac1ea5c23351d0ad963032aef6f68 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 15 Jun 2015 10:17:17 +0000 Subject: [PATCH 1/8] Initial support for "auth --csr" (fixes: #370) --- letsencrypt/cli.py | 70 +++++++++++------- letsencrypt/client.py | 123 +++++++++++++++++-------------- letsencrypt/crypto_util.py | 21 ++++-- letsencrypt/tests/client_test.py | 57 ++++++++++++-- 4 files changed, 179 insertions(+), 92 deletions(-) diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 3bdf2bfc6..ba5a0de9e 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -49,16 +49,20 @@ def _account_init(args, config): return None -def _common_run(args, config, acc, authenticator, installer): +def _find_domains(args, installer): if args.domains is None: - doms = display_ops.choose_names(installer) + domains = display_ops.choose_names(installer) else: - doms = args.domains + domains = args.domains - if not doms: + if not domains: sys.exit("Please specify --domains, or --installer that will " "help in domain names autodiscovery") + return domains + + +def _init_acme(config, acc, authenticator, installer): acme = client.Client(config, acc, authenticator, installer) # Validate the key and csr @@ -71,7 +75,7 @@ def _common_run(args, config, acc, authenticator, installer): except errors.LetsEncryptClientError: sys.exit("Unable to register an account with ACME server") - return acme, doms + return acme def run(args, config, plugins): @@ -99,18 +103,24 @@ def run(args, config, plugins): if installer is None or authenticator is None: return "Configurator could not be determined" - acme, doms = _common_run(args, config, acc, authenticator, installer) - # TODO: Handle errors from _common_run? - lineage = acme.obtain_and_enroll_certificate(doms, authenticator, - installer, plugins) + domains = _find_domains(args, installer) + # TODO: Handle errors from _init_acme? + acme = _init_acme(config, acc, authenticator, installer) + lineage = acme.obtain_and_enroll_certificate( + domains, authenticator, installer, plugins) if not lineage: return "Certificate could not be obtained" - acme.deploy_certificate(doms, lineage.privkey, lineage.cert, lineage.chain) - acme.enhance_config(doms, args.redirect) + acme.deploy_certificate(domains, lineage.privkey, lineage.cert, lineage.chain) + acme.enhance_config(domains, args.redirect) def auth(args, config, plugins): """Obtain a certificate (no install).""" + if args.domains is not None and args.csr is not None: + # TODO: --csr could have a priority, when --domains is + # supplied, check if CSR matches given domains? + return "--domains and --csr are mutually exclusive" + # XXX: Update for renewer / RenewableCert acc = _account_init(args, config) if acc is None: @@ -126,13 +136,18 @@ def auth(args, config, plugins): else: installer = None - # TODO: Handle errors from _common_run? - acme, doms = _common_run( - args, config, acc, authenticator=authenticator, installer=installer) - if not acme.obtain_and_enroll_certificate(doms, authenticator, installer, - plugins): - return "Certificate could not be obtained" + # TODO: Handle errors from _init_acme? + acme = _init_acme(config, acc, authenticator, installer) + if args.csr is not None: + certr, chain = acme.obtain_certificate_from_csr(le_util.CSR( + file=args.csr[0], data=args.csr[1], form="der")) + acme.save_certificate(certr, chain, args.cert_path, args.chain_path) + else: + domains = _find_domains(args, installer) + if not acme.obtain_and_enroll_certificate( + domains, authenticator, installer, plugins): + return "Certificate could not be obtained" def install(args, config, plugins): """Install (no auth).""" @@ -144,11 +159,11 @@ def install(args, config, plugins): installer = display_ops.pick_installer(config, args.installer, plugins) if installer is None: return "Installer could not be determined" - acme, doms = _common_run( - args, config, acc, authenticator=None, installer=installer) + domains = _find_domains(args, installer) + acme = _init_acme(config, acc, authenticator=None, installer=installer) assert args.cert_path is not None - acme.deploy_certificate(doms, acc.key.file, args.cert_path, args.chain_path) - acme.enhance_config(doms, args.redirect) + acme.deploy_certificate(domains, acc.key.file, args.cert_path, args.chain_path) + acme.enhance_config(domains, args.redirect) def revoke(args, unused_config, unused_plugins): @@ -203,10 +218,11 @@ def plugins_cmd(args, config, plugins): # TODO: Use IDiplay rathern than print print str(available) -def read_file(filename): - """Returns the given file's contents with universal new line support. +def read_file(filename, mode="rb"): + """Returns the given file's contents. :param str filename: Filename + :param str mode: open mode (see `open`) :returns: A tuple of filename and its contents :rtype: tuple @@ -215,7 +231,7 @@ def read_file(filename): """ try: - return filename, open(filename, "rU").read() + return filename, open(filename, mode).read() except IOError as exc: raise argparse.ArgumentTypeError(exc.strerror) @@ -276,12 +292,16 @@ def create_parser(plugins): return subparser add_subparser("run", run) - add_subparser("auth", auth) + parser_auth = add_subparser("auth", auth) add_subparser("install", install) parser_revoke = add_subparser("revoke", revoke) parser_rollback = add_subparser("rollback", rollback) add_subparser("config_changes", config_changes) + parser_auth.add_argument( + "--csr", type=read_file, help="Path to a Certificate Signing " + "Request (CSR) in DER format.") + parser_plugins = add_subparser("plugins", plugins_cmd) parser_plugins.add_argument("--init", action="store_true") parser_plugins.add_argument("--prepare", action="store_true") diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 17bee6069..40fe899f3 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -4,6 +4,7 @@ import os import pkg_resources import M2Crypto +import OpenSSL.crypto import zope.component from acme import jose @@ -123,22 +124,17 @@ class Client(object): "{0}.".format(self.account.email)) reporter.add_message(recovery_msg, reporter.HIGH_PRIORITY, True) - def obtain_certificate(self, domains, csr=None): - """Obtains a certificate from the ACME server. + def _obtain_certificate(self, domains, csr): + """Obtain certificate. - :meth:`.register` must be called before :meth:`.obtain_certificate` + Internal function with precondition that `domains` are + consistent with identifiers present in the `csr`. - .. todo:: This function does not currently handle CSR correctly. + :param .le_util.CSR csr: Certificate Signing Request must + contain requested domains, the key used to generate this CSR + can be different than self.authkey. - :param set domains: domains to get a certificate - - :param csr: CSR must contain requested domains, the key used to generate - this CSR can be different than self.authkey - :type csr: :class:`CSR` - - :returns: Certificate, private key, and certificate chain (all - PEM-encoded). - :rtype: `tuple` of `str` + :returns: Certificate Resource and certificate chain. """ if self.auth_handler is None: @@ -150,37 +146,47 @@ class Client(object): raise errors.LetsEncryptClientError( "Please register with the ACME server first.") - # Perform Challenges/Get Authorizations + logging.debug("CSR: %s, domains: %s", csr, domains) + authzr = self.auth_handler.get_authorizations(domains) - - # Create CSR from names - cert_key = crypto_util.init_save_key( - self.config.rsa_key_size, self.config.key_dir) - csr = crypto_util.init_save_csr( - cert_key, domains, self.config.cert_dir) - - # Retrieve certificate certr = self.network.request_issuance( jose.ComparableX509( M2Crypto.X509.load_request_der_string(csr.data)), authzr) + return certr, self.network.fetch_chain(certr) - cert_pem = certr.body.as_pem() - chain_pem = None - if certr.cert_chain_uri is not None: - chain_pem = self.network.fetch_chain(certr) + def obtain_certificate_from_csr(self, csr): + """Obtain certficiate from CSR. - if chain_pem is None: - # XXX: just to stop RenewableCert from complaining; this is - # probably not a good solution - chain_pem = "" - else: - chain_pem = chain_pem.as_pem() + :param .le_util.CSR csr: Certificate Signing Request. - return cert_pem, cert_key.pem, chain_pem + """ + return self._obtain_certificate( + # TODO: add CN to domains? + crypto_util.get_sans_from_csr( + csr.data, OpenSSL.crypto.FILETYPE_ASN1), csr) + + def obtain_certificate(self, domains): + """Obtains a certificate from the ACME server. + + :meth:`.register` must be called before :meth:`.obtain_certificate` + + :param set domains: domains to get a certificate + + :returns: Certificate, private key, and certificate chain (all + PEM-encoded). + :rtype: `tuple` of `str` + + """ + # Create CSR from names + key = crypto_util.init_save_key( + self.config.rsa_key_size, self.config.key_dir) + csr = crypto_util.init_save_csr(key, domains, self.config.cert_dir) + + return key, csr, self._obtain_certificate(domains, csr) def obtain_and_enroll_certificate( - self, domains, authenticator, installer, plugins, csr=None): + self, domains, authenticator, installer, plugins): """Obtain and enroll certificate. Get a new certificate for the specified domains using the specified @@ -196,14 +202,14 @@ class Client(object): :param plugins: A PluginsFactory object. - :param str csr: A preexisting CSR to use with this request. - :returns: A new :class:`letsencrypt.storage.RenewableCert` instance referred to the enrolled cert lineage, or False if the cert could not be obtained. """ - cert, privkey, chain = self.obtain_certificate(domains, csr) + key, _, (certr, chain) = self.obtain_certificate(domains) + + # TODO: remove this dirty hack self.config.namespace.authenticator = plugins.find_init( authenticator).name if installer is not None: @@ -218,8 +224,12 @@ class Client(object): params = vars(self.config.namespace) config = {"renewer_config_file": params["renewer_config_file"]} if "renewer_config_file" in params else None - renewable_cert = storage.RenewableCert.new_lineage(domains[0], cert, privkey, - chain, params, config) + + # XXX: just to stop RenewableCert from complaining; this is + # probably not a good solution + chain_pem = "" if chain is None else chain.as_pem() + renewable_cert = storage.RenewableCert.new_lineage( + domains[0], certr.body.as_pem(), key.pem, chain_pem, params, config) self._report_renewal_status(renewable_cert) return renewable_cert @@ -251,13 +261,15 @@ class Client(object): reporter = zope.component.getUtility(interfaces.IReporter) reporter.add_message(msg, reporter.LOW_PRIORITY, True) - def save_certificate(self, certr, cert_path, chain_path): + def save_certificate(self, certr, chain_cert, cert_path, chain_path): # pylint: disable=no-self-use """Saves the certificate received from the ACME server. :param certr: ACME "certificate" resource. :type certr: :class:`acme.messages.Certificate` + :param chain_cert: + :param str cert_path: Path to attempt to save the cert file :param str chain_path: Path to attempt to save the chain file @@ -267,6 +279,10 @@ class Client(object): :raises IOError: If unable to find room to write the cert files """ + for path in cert_path, chain_path: + le_util.make_or_verify_dir( + os.path.dirname(path), 0o755, os.geteuid()) + # try finally close cert_chain_abspath = None cert_file, act_cert_path = le_util.unique_file(cert_path, 0o644) @@ -279,22 +295,20 @@ class Client(object): logging.info("Server issued certificate; certificate written to %s", act_cert_path) - if certr.cert_chain_uri is not None: + if chain_cert is not None: + chain_file, act_chain_path = le_util.unique_file( + chain_path, 0o644) # TODO: Except - chain_cert = self.network.fetch_chain(certr) - if chain_cert is not None: - chain_file, act_chain_path = le_util.unique_file( - chain_path, 0o644) - chain_pem = chain_cert.as_pem() - try: - chain_file.write(chain_pem) - finally: - chain_file.close() + chain_pem = chain_cert.as_pem() + try: + chain_file.write(chain_pem) + finally: + chain_file.close() - logging.info("Cert chain written to %s", act_chain_path) + logging.info("Cert chain written to %s", act_chain_path) - # This expects a valid chain file - cert_chain_abspath = os.path.abspath(act_chain_path) + # 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 @@ -383,8 +397,7 @@ def validate_key_csr(privkey, csr=None): :param privkey: Key associated with CSR :type privkey: :class:`letsencrypt.le_util.Key` - :param csr: CSR - :type csr: :class:`letsencrypt.le_util.CSR` + :param .le_util.CSR csr: CSR :raises letsencrypt.errors.LetsEncryptClientError: when validation fails diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index 1eb565289..b2b5ecf58 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -90,6 +90,9 @@ def make_csr(key_str, domains): :param str key_str: RSA key. :param list domains: Domains included in the certificate. + .. todo:: Detect duplicates in `domains`? Using a set doesn't + preserve order... + :returns: new CSR in PEM and DER form containing all domains :rtype: tuple @@ -101,13 +104,17 @@ def make_csr(key_str, domains): csr = M2Crypto.X509.Request() csr.set_pubkey(pubkey) - name = csr.get_subject() - name.C = "US" - name.ST = "Michigan" - name.L = "Ann Arbor" - name.O = "EFF" - name.OU = "University of Michigan" - name.CN = domains[0] + # TODO: "The CSR MUST contain at least one extensionRequest + # attribute {{RFC2985}} requesting a subjectAltName extension, + # containing the requested identifiers." -> Subject (CN in + # particular) ignored? can be empty? + #name = csr.get_subject() + #name.C = "US" + #name.ST = "Michigan" + #name.L = "Ann Arbor" + #name.O = "EFF" + #name.OU = "University of Michigan" + #name.CN = domains[0] extstack = M2Crypto.X509.X509_Extension_Stack() ext = M2Crypto.X509.new_extension( diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 1fb9c2a03..511b2df60 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -6,8 +6,11 @@ import shutil import tempfile import configobj +import M2Crypto.X509 import mock +from acme import jose + from letsencrypt import account from letsencrypt import configuration from letsencrypt import le_util @@ -15,6 +18,8 @@ from letsencrypt import le_util KEY = pkg_resources.resource_string( __name__, os.path.join("testdata", "rsa512_key.pem")) +CSR_SAN = pkg_resources.resource_string( + __name__, os.path.join("testdata", "csr-san.der")) class ClientTest(unittest.TestCase): @@ -26,16 +31,58 @@ class ClientTest(unittest.TestCase): self.account = mock.MagicMock(**{"key.pem": KEY}) from letsencrypt.client import Client - with mock.patch("letsencrypt.client.network2") as network2: + with mock.patch("letsencrypt.client.network2.Network") as network: self.client = Client( - config=self.config, account_=self.account, dv_auth=None, - installer=None) - self.network2 = network2 + config=self.config, account_=self.account, + dv_auth=None, installer=None) + self.network = network def test_init_network_verify_ssl(self): - self.network2.Network.assert_called_once_with( + self.network.assert_called_once_with( mock.ANY, mock.ANY, verify_ssl=True) + def _mock_obtain_certificate(self): + self.client.auth_handler = mock.MagicMock() + self.network().request_issuance.return_value = mock.sentinel.certr + self.network().fetch_chain.return_value = mock.sentinel.chain + + def _check_obtain_certificate(self): + self.client.auth_handler.get_authorizations.assert_called_once_with( + ["example.com", "www.example.com"]) + self.network.request_issuance.assert_callend_once_with( + jose.ComparableX509( + M2Crypto.X509.load_request_der_string(CSR_SAN)), + self.client.auth_handler.get_authorizations()) + self.network().fetch_chain.assert_called_once_with(mock.sentinel.certr) + + def test_obtain_certificate_from_csr(self): + self._mock_obtain_certificate() + self.assertEqual( + (mock.sentinel.certr, mock.sentinel.chain), + self.client.obtain_certificate_from_csr(le_util.CSR( + form="der", file=None, data=CSR_SAN))) + self._check_obtain_certificate() + + @mock.patch("letsencrypt.client.crypto_util") + def test_obtain_certificate(self, mock_crypto_util): + self._mock_obtain_certificate() + + csr = le_util.CSR(form="der", file=None, data=CSR_SAN) + mock_crypto_util.init_save_csr.return_value = csr + mock_crypto_util.init_save_key.return_value = mock.sentinel.key + domains = ["example.com", "www.example.com"] + + self.assertEqual( + self.client.obtain_certificate(domains), + (mock.sentinel.key, csr, ( + mock.sentinel.certr, mock.sentinel.chain))) + + mock_crypto_util.init_save_key.assert_called_once_with( + self.config.rsa_key_size, self.config.key_dir) + mock_crypto_util.init_save_csr.assert_called_once_with( + mock.sentinel.key, domains, self.config.cert_dir) + self._check_obtain_certificate() + @mock.patch("letsencrypt.client.zope.component.getUtility") def test_report_new_account(self, mock_zope): # pylint: disable=protected-access From 60cc02565845296ae0d68ab4a9991a207abe7c51 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 15 Jun 2015 11:03:11 +0000 Subject: [PATCH 2/8] Add generete-csr.sh script to examples. --- examples/.gitignore | 3 +++ examples/generate-csr.sh | 28 ++++++++++++++++++++++++++++ examples/openssl.cnf | 5 +++++ 3 files changed, 36 insertions(+) create mode 100644 examples/.gitignore create mode 100755 examples/generate-csr.sh create mode 100644 examples/openssl.cnf diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 000000000..abaf425d1 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,3 @@ +# generate-csr.sh: +/key.pem +/csr.der \ No newline at end of file diff --git a/examples/generate-csr.sh b/examples/generate-csr.sh new file mode 100755 index 000000000..c63f3c2d1 --- /dev/null +++ b/examples/generate-csr.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# This script generates a simple SAN CSR to be used with Let's Encrypt +# CA. Mostly intedened for "auth --csr" testing, but, since its easily +# auditable, feel free to adjust it and use on you production web +# server. + +if [ "$#" -lt 1 ] +then + echo "Usage: $0 domain [domain...]" >&2 + exit 1 +fi + +domains="DNS:$1" +shift +for x in "$@" +do + domains="$domains,DNS:$x" +done + +SAN="$domains" openssl req -config openssl.cnf \ + -new -nodes -subj '/' -reqexts san \ + -out csr.der \ + -keyout key.pem \ + -newkey rsa:2048 \ + -outform DER +# 512 or 1024 too low for Boulder, 2048 is smallest for tests + +echo "You can now run: letsencrypt auth --csr csr.der" diff --git a/examples/openssl.cnf b/examples/openssl.cnf new file mode 100644 index 000000000..a3e6f3895 --- /dev/null +++ b/examples/openssl.cnf @@ -0,0 +1,5 @@ +[ req ] +distinguished_name = req_distinguished_name +[ req_distinguished_name ] +[ san ] +subjectAltName=${ENV::SAN} From 8a9759bf88b9ac87b969c063b2aca3439b5bff2a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Tue, 16 Jun 2015 06:26:44 +0000 Subject: [PATCH 3/8] Update Client.obtain_* docs, simplify obtain_certificate() rtype. --- letsencrypt/client.py | 32 +++++++++++++++++++++----------- letsencrypt/tests/client_test.py | 3 +-- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 40fe899f3..da225690d 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -130,11 +130,14 @@ class Client(object): Internal function with precondition that `domains` are consistent with identifiers present in the `csr`. - :param .le_util.CSR csr: Certificate Signing Request must - contain requested domains, the key used to generate this CSR - can be different than self.authkey. + :param list domains: Domain names. + :param .le_util.CSR csr: DER-encoded Certificate Signing + Request. The key used to generate this CSR can be different + than `authkey`. - :returns: Certificate Resource and certificate chain. + :returns: `.CertificateResource` and certificate chain (as + returned by `.fetch_chain`). + :rtype: tuple """ if self.auth_handler is None: @@ -158,7 +161,12 @@ class Client(object): def obtain_certificate_from_csr(self, csr): """Obtain certficiate from CSR. - :param .le_util.CSR csr: Certificate Signing Request. + :param .le_util.CSR csr: DER-encoded Certificate Signing + Request. + + :returns: `.CertificateResource` and certificate chain (as + returned by `.fetch_chain`). + :rtype: tuple """ return self._obtain_certificate( @@ -169,13 +177,15 @@ class Client(object): def obtain_certificate(self, domains): """Obtains a certificate from the ACME server. - :meth:`.register` must be called before :meth:`.obtain_certificate` + `.register` must be called before `.obtain_certificate` :param set domains: domains to get a certificate - :returns: Certificate, private key, and certificate chain (all - PEM-encoded). - :rtype: `tuple` of `str` + :returns: `.CertificateResource`, certificate chain (as + returned by `.fetch_chain`), and newly generated private key + (`.le_util.Key`) and DER-encoded Certificate Signing Request + (`.le_util.CSR`). + :rtype: tuple """ # Create CSR from names @@ -183,7 +193,7 @@ class Client(object): self.config.rsa_key_size, self.config.key_dir) csr = crypto_util.init_save_csr(key, domains, self.config.cert_dir) - return key, csr, self._obtain_certificate(domains, csr) + return self._obtain_certificate(domains, csr) + (key, csr) def obtain_and_enroll_certificate( self, domains, authenticator, installer, plugins): @@ -207,7 +217,7 @@ class Client(object): not be obtained. """ - key, _, (certr, chain) = self.obtain_certificate(domains) + certr, chain, key, _ = self.obtain_certificate(domains) # TODO: remove this dirty hack self.config.namespace.authenticator = plugins.find_init( diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 511b2df60..7216acea7 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -74,8 +74,7 @@ class ClientTest(unittest.TestCase): self.assertEqual( self.client.obtain_certificate(domains), - (mock.sentinel.key, csr, ( - mock.sentinel.certr, mock.sentinel.chain))) + (mock.sentinel.certr, mock.sentinel.chain, mock.sentinel.key, csr)) mock_crypto_util.init_save_key.assert_called_once_with( self.config.rsa_key_size, self.config.key_dir) From 1cd47d4af3f0bd7a2b094ad2c2c38051f981511c Mon Sep 17 00:00:00 2001 From: James Kasten Date: Mon, 22 Jun 2015 23:23:57 -0700 Subject: [PATCH 4/8] first pass for ssl labs --- letsencrypt/display/ops.py | 17 +++++++++++++++-- letsencrypt/tests/display/ops_test.py | 21 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index fa77e3566..0bb7f5977 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -205,8 +205,21 @@ def success_installation(domains): """ util(interfaces.IDisplay).notification( - "Congratulations! You have successfully enabled " - "%s!" % _gen_https_names(domains), pause=False) + "Congratulations! You have successfully enabled {0}! You should test your configuration:{1}" + "{2}".format( + _gen_https_names(domains), + os.linesep, + os.linesep.join(_gen_ssl_lab_urls(domains))), + pause=False) + + +def _gen_ssl_lab_urls(domains): + """Returns a list of urls. + + :param list domains: Each domain is a 'str' + + """ + return ["https://www.ssllabs.com/ssltest/analyze.html?d=%s", dom for dom in domains] def _gen_https_names(domains): diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index 01daf0004..a1f9a65f6 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -182,6 +182,27 @@ class ChooseAccountTest(unittest.TestCase): self.assertTrue(self._call([self.acc1, self.acc2]) is None) +class GenSSLLabURLs(unittest.TestCase): + """Loose test of _gen_ssl_lab_urls. URL can change easily in the future""" + def setUp(self): + zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) + + @classmethod + def _call(cls, domains): + from letsencrypt.display.ops import _gen_ssl_lab_urls + return _gen_ssl_lab_urls(domains) + + def test_zero(self): + self.assertEqual(self._call([]), []) + + def test_one(self): + self.assertTrue("eff.org" in self._call(["eff.org"])[0]) + + def test_two(self): + urls = self._call(["eff.org", "umich.edu"]) + self.assertTrue("eff.org" in urls[0]) + self.assertTrue("umich.edu" in urls[1]) + class GenHttpsNamesTest(unittest.TestCase): """Test _gen_https_names.""" def setUp(self): From f8384127c093b576d73840a529defe1751cb2685 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Wed, 24 Jun 2015 10:55:08 -0700 Subject: [PATCH 5/8] Format and fix ssl_labs printout --- letsencrypt/display/ops.py | 7 ++++--- letsencrypt/tests/display/ops_test.py | 5 +---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 0bb7f5977..48718f88c 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -205,11 +205,12 @@ def success_installation(domains): """ util(interfaces.IDisplay).notification( - "Congratulations! You have successfully enabled {0}! You should test your configuration:{1}" - "{2}".format( + "Congratulations! You have successfully enabled {0}!{1}{1}" + "You should test your configuration at:{1}{2}".format( _gen_https_names(domains), os.linesep, os.linesep.join(_gen_ssl_lab_urls(domains))), + height=(10 + len(domains)), pause=False) @@ -219,7 +220,7 @@ def _gen_ssl_lab_urls(domains): :param list domains: Each domain is a 'str' """ - return ["https://www.ssllabs.com/ssltest/analyze.html?d=%s", dom for dom in domains] + return ["https://www.ssllabs.com/ssltest/analyze.html?d=%s" % dom for dom in domains] def _gen_https_names(domains): diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index a1f9a65f6..48a51bf27 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -195,9 +195,6 @@ class GenSSLLabURLs(unittest.TestCase): def test_zero(self): self.assertEqual(self._call([]), []) - def test_one(self): - self.assertTrue("eff.org" in self._call(["eff.org"])[0]) - def test_two(self): urls = self._call(["eff.org", "umich.edu"]) self.assertTrue("eff.org" in urls[0]) @@ -328,7 +325,7 @@ class SuccessInstallationTest(unittest.TestCase): names = ["example.com", "abc.com"] self._call(names) - + self.assertEqual(mock_util().notification.call_count, 1) arg = mock_util().notification.call_args_list[0][0][0] From 67f67fea02446e53ccc0de78a7f566d04a2a9607 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Wed, 24 Jun 2015 14:53:28 -0700 Subject: [PATCH 6/8] Remove trailing whitespace --- letsencrypt/display/ops.py | 2 +- letsencrypt/tests/display/ops_test.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 48718f88c..1a1887b9a 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -216,7 +216,7 @@ def success_installation(domains): def _gen_ssl_lab_urls(domains): """Returns a list of urls. - + :param list domains: Each domain is a 'str' """ diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index 48a51bf27..25be6bebc 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -183,7 +183,7 @@ class ChooseAccountTest(unittest.TestCase): class GenSSLLabURLs(unittest.TestCase): - """Loose test of _gen_ssl_lab_urls. URL can change easily in the future""" + """Loose test of _gen_ssl_lab_urls. URL can change easily in the future.""" def setUp(self): zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) @@ -325,7 +325,7 @@ class SuccessInstallationTest(unittest.TestCase): names = ["example.com", "abc.com"] self._call(names) - + self.assertEqual(mock_util().notification.call_count, 1) arg = mock_util().notification.call_args_list[0][0][0] From 4057734c333f8b7e914f8029aed9230b26c406d8 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 25 Jun 2015 16:05:25 +0000 Subject: [PATCH 7/8] Add integrations tests for CSR. --- examples/generate-csr.sh | 6 +++--- tests/boulder-integration.sh | 38 +++++++++++++++++++++++------------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/examples/generate-csr.sh b/examples/generate-csr.sh index c63f3c2d1..617319c3d 100755 --- a/examples/generate-csr.sh +++ b/examples/generate-csr.sh @@ -17,12 +17,12 @@ do domains="$domains,DNS:$x" done -SAN="$domains" openssl req -config openssl.cnf \ +SAN="$domains" openssl req -config "${OPENSSL_CNF:-openssl.cnf}" \ -new -nodes -subj '/' -reqexts san \ - -out csr.der \ + -out "${CSR_PATH:-csr.der}" \ -keyout key.pem \ -newkey rsa:2048 \ -outform DER # 512 or 1024 too low for Boulder, 2048 is smallest for tests -echo "You can now run: letsencrypt auth --csr csr.der" +echo "You can now run: letsencrypt auth --csr ${CSR_PATH:-csr.der}" diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index adb6ab528..fd4c27beb 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -6,17 +6,27 @@ root="$(mktemp -d)" echo "\nRoot integration tests directory: $root" -# first three flags required, rest is handy defaults -letsencrypt \ - --server http://localhost:4000/acme/new-reg \ - --no-verify-ssl \ - --dvsni-port 5001 \ - --config-dir "$root/conf" \ - --work-dir "$root/work" \ - --text \ - --agree-eula \ - --email "" \ - --domains le.wtf \ - --authenticator standalone \ - -vvvvvvv \ - auth +common() { + # first three flags required, rest is handy defaults + letsencrypt \ + --server http://localhost:4000/acme/new-reg \ + --no-verify-ssl \ + --dvsni-port 5001 \ + --config-dir "$root/conf" \ + --work-dir "$root/work" \ + --text \ + --agree-eula \ + --email "" \ + --authenticator standalone \ + -vvvvvvv "$@" +} + +common --domains le.wtf auth + +export CSR_PATH="${root}/csr.der" OPENSSL_CNF=examples/openssl.cnf +./examples/generate-csr.sh le.wtf +common auth --csr "$CSR_PATH" \ + --cert-path "${root}/csr/cert.pem" \ + --chain-path "${root}/csr/chain.pem" +openssl x509 -in "${root}/csr/0000_cert.pem" -text +openssl x509 -in "${root}/csr/0000_chain.pem" -text From d804853958c631074e577c597842f31e94c4410b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 25 Jun 2015 16:08:52 +0000 Subject: [PATCH 8/8] Remove commented suject fields in make_csr --- letsencrypt/crypto_util.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index e6f0ab8bb..943fd27eb 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -104,17 +104,7 @@ def make_csr(key_str, domains): csr = M2Crypto.X509.Request() csr.set_pubkey(pubkey) - # TODO: "The CSR MUST contain at least one extensionRequest - # attribute {{RFC2985}} requesting a subjectAltName extension, - # containing the requested identifiers." -> Subject (CN in - # particular) ignored? can be empty? - #name = csr.get_subject() - #name.C = "US" - #name.ST = "Michigan" - #name.L = "Ann Arbor" - #name.O = "EFF" - #name.OU = "University of Michigan" - #name.CN = domains[0] + # TODO: what to put into csr.get_subject()? extstack = M2Crypto.X509.X509_Extension_Stack() ext = M2Crypto.X509.new_extension(