diff --git a/.gitignore b/.gitignore index ace5d7a0f..9dac99790 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,7 @@ dist/ # editor temporary files *~ *.swp -\#*# \ No newline at end of file +\#*# + +# auth --cert-path --chain-path +/*.pem \ No newline at end of file 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..617319c3d --- /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:-openssl.cnf}" \ + -new -nodes -subj '/' -reqexts san \ + -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_PATH:-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} diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index b2ecd4887..8fab21f44 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -89,16 +89,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 @@ -111,7 +115,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): @@ -139,19 +143,26 @@ 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): """Authenticate & obtain cert, but do not install it.""" # XXX: Update for renewer / RenewableCert + + 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" + acc = _account_init(args, config) if acc is None: return None @@ -166,13 +177,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 a previously obtained cert in a server.""" @@ -184,11 +200,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): @@ -243,10 +259,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 @@ -255,7 +272,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) @@ -466,12 +483,22 @@ def create_parser(plugins, args): 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_auth.add_argument( + "--cert-path", default=flag_default("cert_path"), + help="When using --csr this is where certificate is saved.") + parser_auth.add_argument( + "--chain-path", default=flag_default("chain_path"), + help="When using --csr this is where certificate chain is saved.") + parser_plugins = add_subparser("plugins", plugins_cmd) parser_plugins.add_argument("--init", action="store_true") parser_plugins.add_argument("--prepare", action="store_true") @@ -539,10 +566,6 @@ def _paths_parser(helpful): help=config_help("cert_dir")) add("paths", "--le-vhost-ext", default="-le-ssl.conf", help=config_help("le_vhost_ext")) - add("paths", "--cert-path", default=flag_default("cert_path"), - help=config_help("cert_path")) - add("paths", "--chain-path", default=flag_default("chain_path"), - help=config_help("chain_path")) add("paths", "--renewer-config-file", default=flag_default("renewer_config_file"), help=config_help("renewer_config_file")) add("paths", "-s", "--server", default=flag_default("server"), diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 4436bb6c7..5c54835f8 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 @@ -125,22 +126,20 @@ 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 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`. - :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: `.CertificateResource` and certificate chain (as + returned by `.fetch_chain`). + :rtype: tuple """ if self.auth_handler is None: @@ -151,37 +150,54 @@ class Client(object): if self.account.regr is None: raise errors.Error("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: DER-encoded Certificate Signing + Request. - return cert_pem, cert_key.pem, chain_pem + :returns: `.CertificateResource` and certificate chain (as + returned by `.fetch_chain`). + :rtype: tuple + + """ + 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. + + `.register` must be called before `.obtain_certificate` + + :param set domains: domains to get a certificate + + :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 + 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 self._obtain_certificate(domains, csr) + (key, 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 @@ -197,14 +213,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) + certr, chain, key, _ = self.obtain_certificate(domains) + + # TODO: remove this dirty hack self.config.namespace.authenticator = plugins.find_init( authenticator).name if installer is not None: @@ -226,8 +242,12 @@ class Client(object): "Non-standard path(s), might not work with crontab installed " "by your operating system package manager") + # XXX: just to stop RenewableCert from complaining; this is + # probably not a good solution + chain_pem = "" if chain is None else chain.as_pem() lineage = storage.RenewableCert.new_lineage( - domains[0], cert, privkey, chain, params, config, cli_config) + domains[0], certr.body.as_pem(), key.pem, chain_pem, params, + config, cli_config) self._report_renewal_status(lineage) return lineage @@ -258,13 +278,14 @@ 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: Candidate path to a certificate. :param str chain_path: Candidate path to a certificate chain. @@ -274,6 +295,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) @@ -286,22 +311,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 @@ -390,8 +413,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 .errors.Error: when validation fails diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 5595c71c4..2a9e87ade 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -59,15 +59,15 @@ class NamespaceConfig(object): def backup_dir(self): # pylint: disable=missing-docstring return os.path.join(self.namespace.work_dir, constants.BACKUP_DIR) + @property + def cert_dir(self): # pylint: disable=missing-docstring + return os.path.join(self.namespace.config_dir, constants.CERT_DIR) + @property def cert_key_backup(self): # pylint: disable=missing-docstring return os.path.join(self.namespace.work_dir, constants.CERT_KEY_BACKUP_DIR, self.server_path) - @property - def cert_dir(self): # pylint: disable=missing-docstring - return os.path.join(self.namespace.config_dir, constants.CERT_DIR) - @property def in_progress_dir(self): # pylint: disable=missing-docstring return os.path.join(self.namespace.work_dir, constants.IN_PROGRESS_DIR) diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index c77d11775..81ec4a90b 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -17,13 +17,13 @@ CLI_DEFAULTS = dict( work_dir="/var/lib/letsencrypt", no_verify_ssl=False, dvsni_port=challenges.DVSNI.PORT, + cert_path="./cert.pem", + chain_path="./chain.pem", # TODO: blocked by #485, values ignored backup_dir="not used", key_dir="not used", certs_dir="not used", - cert_path="not used", - chain_path="not used", renewer_config_file="not used", ) """Defaults for CLI flags and `.IConfig` attributes.""" @@ -69,13 +69,13 @@ ACCOUNT_KEYS_DIR = "keys" BACKUP_DIR = "backups" """Directory (relative to `IConfig.work_dir`) where backups are kept.""" +CERT_DIR = "certs" +"""See `.IConfig.cert_dir`.""" + CERT_KEY_BACKUP_DIR = "keys-certs" """Directory where all certificates and keys are stored (relative to `IConfig.work_dir`). Used for easy revocation.""" -CERT_DIR = "certs" -"""See `.IConfig.cert_dir`.""" - IN_PROGRESS_DIR = "IN_PROGRESS" """Directory used before a permanent checkpoint is finalized (relative to `IConfig.work_dir`).""" diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index 31df697c0..943fd27eb 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,7 @@ 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: what to put into csr.get_subject()? extstack = M2Crypto.X509.X509_Extension_Stack() ext = M2Crypto.X509.new_extension( diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index fa77e3566..1a1887b9a 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -205,8 +205,22 @@ 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}!{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) + + +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/interfaces.py b/letsencrypt/interfaces.py index 3c71fc1fa..ce12c4a56 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -190,8 +190,6 @@ class IConfig(zope.interface.Interface): # TODO: the following are not used, but blocked by #485 le_vhost_ext = zope.interface.Attribute("not used") - cert_path = zope.interface.Attribute("not used") - chain_path = zope.interface.Attribute("not used") class IInstaller(IPlugin): diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index 188eb72cf..d2c0b8e7d 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -83,14 +83,15 @@ def renew(cert, old_version): our_client = client.Client(config, account, authenticator, None) with open(cert.version("cert", old_version)) as f: sans = crypto_util.get_sans_from_cert(f.read()) - new_cert, new_key, new_chain = our_client.obtain_certificate(sans) - if new_cert and new_key and new_chain: + new_certr, new_chain, new_key, _ = our_client.obtain_certificate(sans) + if new_chain is not None: # XXX: Assumes that there was no key change. We need logic # for figuring out whether there was or not. Probably # best is to have obtain_certificate return None for # new_key if the old key is to be used (since save_successor # already understands this distinction!) - return cert.save_successor(old_version, new_cert, new_key, new_chain) + return cert.save_successor(old_version, new_certr.body.as_pem(), + new_key.pem, new_chain.as_pem()) # TODO: Notify results else: # TODO: Notify negative results diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 593d02cdd..9b7634e20 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): @@ -27,16 +32,57 @@ class ClientTest(unittest.TestCase): self.account = mock.MagicMock(**{"key.pem": KEY}) from letsencrypt.client import Client - with mock.patch("letsencrypt.client.network") as network: + with mock.patch("letsencrypt.client.network.Network") as network: self.client = Client( - config=self.config, account_=self.account, dv_auth=None, - installer=None) + config=self.config, account_=self.account, + dv_auth=None, installer=None) self.network = network def test_init_network_verify_ssl(self): - self.network.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.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) + 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 diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index 01daf0004..25be6bebc 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -182,6 +182,24 @@ 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_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): diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index a690ddfc2..1ba58a7c8 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -581,13 +581,18 @@ class RenewableCertTests(unittest.TestCase): self.assertFalse(renewer.renew(self.test_rc, 1)) self.test_rc.configfile["renewalparams"]["authenticator"] = "apache" mock_client = mock.MagicMock() - mock_client.obtain_certificate.return_value = ("cert", "key", "chain") + # pylint: disable=star-args + mock_client.obtain_certificate.return_value = ( + mock.Mock(**{'body.as_pem.return_value': 'cert'}), + mock.Mock(**{'as_pem.return_value': 'chain'}), + mock.Mock(pem="key"), mock.sentinel.csr) mock_c.return_value = mock_client self.assertEqual(2, renewer.renew(self.test_rc, 1)) # TODO: We could also make several assertions about calls that should # have been made to the mock functions here. self.assertEqual(mock_da.call_count, 1) - mock_client.obtain_certificate.return_value = (None, None, None) + mock_client.obtain_certificate.return_value = ( + mock.sentinel.certr, None, mock.sentinel.key, mock.sentinel.csr) # This should fail because the renewal itself appears to fail self.assertFalse(renewer.renew(self.test_rc, 1)) diff --git a/tests/boulder-integration.sh b/tests/boulder-integration.sh index 596fbf748..c3cc49c70 100755 --- a/tests/boulder-integration.sh +++ b/tests/boulder-integration.sh @@ -7,19 +7,29 @@ root="$(mktemp -d)" echo "\nRoot integration tests directory: $root" store_flags="--config-dir $root/conf --work-dir $root/work" -# first three flags required, rest is handy defaults -letsencrypt \ - --server http://localhost:4000/acme/new-reg \ - --no-verify-ssl \ - --dvsni-port 5001 \ - $store_flags \ - --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 \ + $store_flags \ + --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 # the following assumes that Boulder issues certificates for less than # 10 years, otherwise renewal will not take place