From 7470bc8db6264da74cbd2d6ebd4969bc32d4366f Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sun, 5 Jul 2015 08:31:48 +0000 Subject: [PATCH 01/30] RegistrationResource: return any phone/email from phones/emails or None. --- acme/messages.py | 24 ++++++++++++++++++------ acme/messages_test.py | 7 +++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/acme/messages.py b/acme/messages.py index c224e9bde..4d82a7723 100644 --- a/acme/messages.py +++ b/acme/messages.py @@ -182,15 +182,27 @@ class Registration(ResourceBody): @property def phone(self): - """Phone.""" - assert len(self.phones) == 1 - return self.phones[0] + """Phone. + + Picks any phone from `phones` or ``None`` if not available. + + """ + try: + return self.phones[0] + except IndexError: + return None @property def email(self): - """Email.""" - assert len(self.emails) == 1 - return self.emails[0] + """Email. + + Picks any email from `emails` or ``None`` if not available. + + """ + try: + return self.emails[0] + except IndexError: + return None class RegistrationResource(ResourceWithURI): diff --git a/acme/messages_test.py b/acme/messages_test.py index 59b1685dc..4f99b538d 100644 --- a/acme/messages_test.py +++ b/acme/messages_test.py @@ -122,6 +122,7 @@ class RegistrationTest(unittest.TestCase): self.reg = Registration( key=key, contact=contact, recovery_token=recovery_token, agreement=agreement) + self.reg_none = Registration() self.jobj_to = { 'contact': contact, @@ -149,9 +150,15 @@ class RegistrationTest(unittest.TestCase): def test_phone(self): self.assertEqual('1234', self.reg.phone) + def test_phone_none(self): + self.assertTrue(self.reg_none.phone is None) + def test_email(self): self.assertEqual('admin@foo.com', self.reg.email) + def test_email_none(self): + self.assertTrue(self.reg_none.email is None) + def test_to_partial_json(self): self.assertEqual(self.jobj_to, self.reg.to_partial_json()) From 0d087788da53f4a118930ff365875e11ee914293 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 4 Jul 2015 06:22:11 +0000 Subject: [PATCH 02/30] Accept new_reg in acme.client.Client.register. --- acme/client.py | 9 ++++----- acme/client_test.py | 4 ++-- examples/acme_client.py | 2 +- letsencrypt/network.py | 9 +++------ letsencrypt/tests/network_test.py | 13 ++++++++----- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/acme/client.py b/acme/client.py index 67d4f1a69..064bd8cd2 100644 --- a/acme/client.py +++ b/acme/client.py @@ -64,8 +64,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes new_authzr_uri=new_authzr_uri, terms_of_service=terms_of_service) - def register(self, contact=messages.Registration._fields[ - 'contact'].default): + def register(self, new_reg=None): """Register. :param contact: Contact list, as accepted by `.Registration` @@ -77,14 +76,14 @@ class Client(object): # pylint: disable=too-many-instance-attributes :raises .UnexpectedUpdate: """ - new_reg = messages.Registration(contact=contact) + new_reg = messages.Registration() if new_reg is None else new_reg response = self.net.post(self.new_reg_uri, new_reg) assert response.status_code == httplib.CREATED # TODO: handle errors regr = self._regr_from_response(response) - if (regr.body.key != self.key.public_key() - or regr.body.contact != contact): + if (regr.body.key != self.key.public_key() or + regr.body.contact != new_reg.contact): raise errors.UnexpectedUpdate(regr) return regr diff --git a/acme/client_test.py b/acme/client_test.py index b25a1866c..bbed2ed87 100644 --- a/acme/client_test.py +++ b/acme/client_test.py @@ -82,14 +82,14 @@ class ClientTest(unittest.TestCase): 'terms-of-service': {'url': self.regr.terms_of_service}, }) - self.assertEqual(self.regr, self.client.register(self.contact)) + self.assertEqual(self.regr, self.client.register(self.regr.body)) # TODO: test POST call arguments # TODO: split here and separate test reg_wrong_key = self.regr.body.update(key=KEY2.public_key()) self.response.json.return_value = reg_wrong_key.to_json() self.assertRaises( - errors.UnexpectedUpdate, self.client.register, self.contact) + errors.UnexpectedUpdate, self.client.register, self.regr.body) def test_register_missing_next(self): self.response.status_code = httplib.CREATED diff --git a/examples/acme_client.py b/examples/acme_client.py index e07031fbe..0d906d539 100644 --- a/examples/acme_client.py +++ b/examples/acme_client.py @@ -26,7 +26,7 @@ key = jose.JWKRSA(key=rsa.generate_private_key( backend=default_backend())) acme = client.Client(NEW_REG_URL, key) -regr = acme.register(contact=()) +regr = acme.register() logging.info('Auto-accepting TOS: %s', regr.terms_of_service) acme.update_registration(regr.update( body=regr.body.update(agreement=regr.terms_of_service))) diff --git a/letsencrypt/network.py b/letsencrypt/network.py index 0f4d9d29b..a890c94e2 100644 --- a/letsencrypt/network.py +++ b/letsencrypt/network.py @@ -1,5 +1,6 @@ """Networking for ACME protocol.""" from acme import client +from acme import messages class Network(client.Client): @@ -17,10 +18,6 @@ class Network(client.Client): :rtype: :class:`letsencrypt.account.Account` """ - details = ( - "mailto:" + account.email if account.email is not None else None, - "tel:" + account.phone if account.phone is not None else None, - ) - account.regr = self.register(contact=tuple( - det for det in details if det is not None)) + account.regr = self.register(messages.Registration.from_data( + email=account.email, phone=account.phone)) return account diff --git a/letsencrypt/tests/network_test.py b/letsencrypt/tests/network_test.py index 6acb11315..aed681871 100644 --- a/letsencrypt/tests/network_test.py +++ b/letsencrypt/tests/network_test.py @@ -5,6 +5,7 @@ import unittest import mock +from acme import messages from letsencrypt import account @@ -17,7 +18,6 @@ class NetworkTest(unittest.TestCase): new_reg_uri=None, key=None, alg=None, verify_ssl=None) self.config = mock.Mock(accounts_dir=tempfile.mkdtemp()) - self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212') def tearDown(self): shutil.rmtree(self.config.accounts_dir) @@ -30,7 +30,10 @@ class NetworkTest(unittest.TestCase): self.net.register_from_account(acc) - self.net.register.assert_called_with(contact=self.contact) + self.net.register.assert_called_once() + self.assertEqual( + set(self.net.register.mock_calls[0][1][0].contact), + set(('mailto:cert-admin@example.com', 'tel:+12025551212'))) def test_register_from_account_partial_info(self): self.net.register = mock.Mock() @@ -39,11 +42,11 @@ class NetworkTest(unittest.TestCase): acc2 = account.Account(self.config, 'key') self.net.register_from_account(acc) - self.net.register.assert_called_with( - contact=('mailto:cert-admin@example.com',)) + self.net.register.assert_called_with(messages.Registration( + contact=('mailto:cert-admin@example.com',))) self.net.register_from_account(acc2) - self.net.register.assert_called_with(contact=()) + self.net.register.assert_called_with(messages.Registration()) if __name__ == '__main__': From 7dc64e03874c2fa66a7b5c4311272d939d4f88ae Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 4 Jul 2015 06:46:36 +0000 Subject: [PATCH 03/30] Rewrite acccounts and registration. Save accounts to: /etc/letsencrypt/accounts/www.letsencrypt-dmeo.org/acme/new-reg/ \ kuba.le.wtf@2015-07-04T14:04:10Z/ \ {regr.json,meta.json,private_key.json} Account now represents a combination of private key, Registration Resource and client account metadata. `Account.id` based on the account metadata (creation host and datetime). UI interface (`cli._determine_account`) based on the `id`, and not on email as previously. Add `AccountStorage` interface and `AccountFileStorage`, `AccountMemoryStorage` implementations (latter, in-memory, useful for testing). Create Account only after Registration Resource is received (`register()` returns `Account`). Allow `client.Client(..., acme=acme, ...)`: API client might reuse acme.client.Client as returned by `register()`. Move report_new_account to letsencrypt.account, client.Client.register into client.register. Use Registration.from_data acme API. achallenges.AChallenge.key is now the `acme.jose.JWK`, not `le_util.Key`. Plugins have to export PEM/DER as necessary (c.f. `letsencrypt.plugins.common.Dvsni.get_key_path`) Add --agree-tos, save --agree-eula to "args.eula". Prompt for EULA as soon as client is launched, add prompt for TOS. Remove unnecessary letsencrypt.network. Remove, now irrelevant, `IConfig.account_keys_dir`. Based on the draft from https://github.com/letsencrypt/letsencrypt/pull/362#issuecomment-97946817. --- docs/api/network.rst | 5 - letsencrypt/account.py | 361 ++++++++---------- letsencrypt/achallenges.py | 2 +- letsencrypt/auth_handler.py | 16 +- letsencrypt/cli.py | 148 ++++--- letsencrypt/client.py | 197 +++++----- letsencrypt/configuration.py | 5 - letsencrypt/constants.py | 3 - letsencrypt/crypto_util.py | 15 +- letsencrypt/display/ops.py | 25 +- letsencrypt/errors.py | 8 + letsencrypt/interfaces.py | 38 +- letsencrypt/le_util.py | 18 + letsencrypt/network.py | 23 -- letsencrypt/plugins/common.py | 16 + letsencrypt/plugins/common_test.py | 4 +- .../plugins/standalone/authenticator.py | 6 +- .../standalone/tests/authenticator_test.py | 14 +- letsencrypt/renewer.py | 11 +- letsencrypt/revoker.py | 9 +- letsencrypt/tests/account_test.py | 307 +++++++-------- letsencrypt/tests/achallenges_test.py | 11 +- letsencrypt/tests/auth_handler_test.py | 4 +- letsencrypt/tests/cli_test.py | 68 +++- letsencrypt/tests/client_test.py | 109 ++---- letsencrypt/tests/configuration_test.py | 4 - letsencrypt/tests/display/ops_test.py | 50 ++- letsencrypt/tests/le_util_test.py | 27 ++ letsencrypt/tests/network_test.py | 53 --- letsencrypt/tests/renewer_test.py | 7 +- letsencrypt/tests/revoker_test.py | 36 +- letsencrypt_apache/dvsni.py | 3 +- letsencrypt_nginx/dvsni.py | 2 +- tests/integration/_common.sh | 1 + 34 files changed, 849 insertions(+), 757 deletions(-) delete mode 100644 docs/api/network.rst delete mode 100644 letsencrypt/network.py delete mode 100644 letsencrypt/tests/network_test.py diff --git a/docs/api/network.rst b/docs/api/network.rst deleted file mode 100644 index 0359379dd..000000000 --- a/docs/api/network.rst +++ /dev/null @@ -1,5 +0,0 @@ -:mod:`letsencrypt.network` --------------------------- - -.. automodule:: letsencrypt.network - :members: diff --git a/letsencrypt/account.py b/letsencrypt/account.py index 3304f6452..2f8eaab27 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -1,20 +1,21 @@ """Creates ACME accounts for server.""" +import datetime import logging import os -import re +import socket -import configobj +import pyrfc3339 +import pytz import zope.component +from acme import fields as acme_fields +from acme import jose from acme import messages -from letsencrypt import crypto_util from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util -from letsencrypt.display import util as display_util - logger = logging.getLogger(__name__) @@ -22,212 +23,178 @@ logger = logging.getLogger(__name__) class Account(object): """ACME protocol registration. - :ivar config: Client configuration object - :type config: :class:`~letsencrypt.interfaces.IConfig` - :ivar key: Account/Authorized Key - :type key: :class:`~letsencrypt.le_util.Key` + :ivar .RegistrationResource regr: Registration Resource + :ivar .JWK key: Authorized Account Key + :ivar .Meta: Account metadata - :ivar str email: Client's email address - :ivar str phone: Client's phone number - - :ivar regr: Registration Resource - :type regr: :class:`~acme.messages.RegistrationResource` + .. note:: ``creation_dt`` and ``creation_host`` are useful in + cross-machine migration scenarios. """ - # Just make sure we don't get pwned - # Make sure that it also doesn't start with a period or have two consecutive - # periods <- this needs to be done in addition to the regex - EMAIL_REGEX = re.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+$") + class Meta(jose.JSONObjectWithFields): + """Account metadata - def __init__(self, config, key, email=None, phone=None, regr=None): - le_util.make_or_verify_dir( - config.accounts_dir, 0o700, os.geteuid()) - self.key = key - self.config = config - if email is not None and self.safe_email(email): - self.email = email - else: - self.email = None - self.phone = phone - - self.regr = regr - - @property - def uri(self): - """URI link for new registrations.""" - if self.regr is not None: - return self.regr.uri - else: - return None - - @property - def new_authzr_uri(self): # pylint: disable=missing-docstring - if self.regr is not None: - return self.regr.new_authzr_uri - else: - return None - - @property - def terms_of_service(self): # pylint: disable=missing-docstring - if self.regr is not None: - return self.regr.terms_of_service - else: - return None - - @property - def recovery_token(self): # pylint: disable=missing-docstring - if self.regr is not None and self.regr.body is not None: - return self.regr.body.recovery_token - else: - return None - - def save(self): - """Save account to disk.""" - le_util.make_or_verify_dir( - self.config.accounts_dir, 0o700, os.geteuid()) - - acc_config = configobj.ConfigObj() - acc_config.filename = os.path.join( - self.config.accounts_dir, self._get_config_filename(self.email)) - - acc_config.initial_comment = [ - "DO NOT EDIT THIS FILE", - "Account information for %s under %s" % ( - self._get_config_filename(self.email), self.config.server), - ] - - acc_config["key"] = self.key.file - acc_config["phone"] = self.phone - - if self.regr is not None: - acc_config["RegistrationResource"] = {} - acc_config["RegistrationResource"]["uri"] = self.uri - acc_config["RegistrationResource"]["new_authzr_uri"] = ( - self.new_authzr_uri) - acc_config["RegistrationResource"]["terms_of_service"] = ( - self.terms_of_service) - - regr_dict = self.regr.body.to_json() - acc_config["RegistrationResource"]["body"] = regr_dict - - acc_config.write() - - @classmethod - def _get_config_filename(cls, email): - return email if email is not None and email else "default" - - @classmethod - def from_existing_account(cls, config, email=None): - """Populate an account from an existing email.""" - config_fp = os.path.join( - config.accounts_dir, cls._get_config_filename(email)) - return cls._from_config_fp(config, config_fp) - - @classmethod - def _from_config_fp(cls, config, config_fp): - try: - acc_config = configobj.ConfigObj( - infile=config_fp, file_error=True, create_empty=False) - except IOError: - raise errors.Error( - "Account for %s does not exist" % os.path.basename(config_fp)) - - if os.path.basename(config_fp) != "default": - email = os.path.basename(config_fp) - else: - email = None - phone = acc_config["phone"] if acc_config["phone"] != "None" else None - - with open(acc_config["key"]) as key_file: - key = le_util.Key(acc_config["key"], key_file.read()) - - if "RegistrationResource" in acc_config: - acc_config_rr = acc_config["RegistrationResource"] - regr = messages.RegistrationResource( - uri=acc_config_rr["uri"], - new_authzr_uri=acc_config_rr["new_authzr_uri"], - terms_of_service=acc_config_rr["terms_of_service"], - body=messages.Registration.from_json(acc_config_rr["body"])) - else: - regr = None - - return cls(config, key, email, phone, regr) - - @classmethod - def get_accounts(cls, config): - """Return all current accounts. - - :param config: Configuration - :type config: :class:`letsencrypt.interfaces.IConfig` + :ivar datetime.datetime creation_dt: Creation date and time (UTC). + :ivar str creation_host: FQDN of host, where account has been created. """ + creation_dt = acme_fields.RFC3339Field("creation_dt") + creation_host = jose.Field("creation_host") + + def __init__(self, regr, key, meta=None): + self.key = key + self.regr = regr + self.meta = self.Meta( + # pyrfc3339 drops microseconds, make sure __eq__ is sane + creation_dt=datetime.datetime.now( + tz=pytz.UTC).replace(microsecond=0), + creation_host=socket.getfqdn()) if meta is None else meta + + @property + def id(self): # pylint: disable=invalid-name + """Globally unique account identifier. + + Implementation note: Email? Multiple accounts can have the same + email address. Registration URI? Assigned by the server, not + guaranteed to be stable over time, nor cannonical URI can be + generated. One could use the account key (fingerprint), as ACME + protocol doesn't allow it to be updated... + + """ + return self.slug + + @property + def slug(self): + """Short account identification string, useful for UI.""" + return "{1}@{0}".format(pyrfc3339.generate( + self.meta.creation_dt), self.meta.creation_host) + + def __repr__(self): + return "<{0}({1})>".format(self.__class__.__name__, self.id) + + def __eq__(self, other): + return (isinstance(other, self.__class__) and + self.key == other.key and self.regr == other.regr and + self.meta == other.meta) + + +def report_new_account(acc, config): + """Informs the user about their new Let's Encrypt account.""" + reporter = zope.component.queryUtility(interfaces.IReporter) + if reporter is None: + return + reporter.add_message( + "Your account credentials have been saved in your Let's Encrypt " + "configuration directory at {0}. You should make a secure backup " + "of this folder now. This configuration directory will also " + "contain certificates and private keys obtained by Let's Encrypt " + "so making regular backups of this folder is ideal.".format( + config.config_dir), + reporter.MEDIUM_PRIORITY, True) + + assert acc.regr.body.recovery_token is not None + recovery_msg = ("If you lose your account credentials, you can recover " + "them using the token \"{0}\". You must write that down " + "and put it in a safe place.".format( + acc.regr.body.recovery_token)) + if acc.regr.body.email is not None: + recovery_msg += (" Another recovery method will be e-mails sent to " + "{0}.".format(acc.regr.body.email)) + reporter.add_message(recovery_msg, reporter.HIGH_PRIORITY, True) + + +class AccountMemoryStorage(interfaces.AccountStorage): + """In-memory account strage.""" + + def __init__(self, initial_accounts=None): + self.accounts = initial_accounts if initial_accounts is not None else {} + + def find_all(self): + return self.accounts.values() + + def save(self, account): + if account.id in self.accounts: + logger.debug("Overwriting account: %s", account.id) + self.accounts[account.id] = account + + def load(self, account_id): try: - filenames = os.listdir(config.accounts_dir) + return self.accounts[account_id] + except KeyError: + raise errors.AccountNotFound(account_id) + + +class AccountFileStorage(interfaces.AccountStorage): + """Accounts file storage. + + :ivar .IConfig config: Client configuration + + """ + def __init__(self, config): + le_util.make_or_verify_dir(config.accounts_dir, 0o700, os.geteuid()) + self.config = config + + def _account_dir_path(self, account_id): + return os.path.join(self.config.accounts_dir, account_id) + + @classmethod + def _regr_path(cls, account_dir_path): + return os.path.join(account_dir_path, "regr.json") + + @classmethod + def _key_path(cls, account_dir_path): + return os.path.join(account_dir_path, "private_key.json") + + @classmethod + def _metadata_path(cls, account_dir_path): + return os.path.join(account_dir_path, "meta.json") + + def find_all(self): + try: + candidates = os.listdir(self.config.accounts_dir) except OSError: return [] accounts = [] - for name in filenames: - # Not some directory ie. keys - config_fp = os.path.join(config.accounts_dir, name) - if os.path.isfile(config_fp): - accounts.append(cls._from_config_fp(config, config_fp)) - + for account_id in candidates: + try: + accounts.append(self.load(account_id)) + except errors.AccountStorageError: + logger.debug("Account loading problem", exc_info=True) return accounts - @classmethod - def from_prompts(cls, config): - """Generate an account from prompted user input. + def load(self, account_id): + account_dir_path = self._account_dir_path(account_id) + if not os.path.isdir(account_dir_path): + raise errors.AccountNotFound( + "Account at %s does not exist" % account_dir_path) - :param config: Configuration - :type config: :class:`letsencrypt.interfaces.IConfig` + try: + with open(self._regr_path(account_dir_path)) as regr_file: + regr = messages.RegistrationResource.json_loads(regr_file.read()) + with open(self._key_path(account_dir_path)) as key_file: + key = jose.JWK.json_loads(key_file.read()) + with open(self._metadata_path(account_dir_path)) as metadata_file: + meta = Account.Meta.json_loads(metadata_file.read()) + except IOError as error: + raise errors.AccountStorageError(error) - :returns: Account or None - :rtype: :class:`letsencrypt.account.Account` + acc = Account(regr, key, meta) + assert acc.id == account_id, ( + "Account ids mismatch (expected: {0}, found: {1}".format( + account_id, acc.id)) + return acc - """ - while True: - code, email = zope.component.getUtility(interfaces.IDisplay).input( - "Enter email address") - - if code == display_util.OK: - try: - return cls.from_email(config, email) - except errors.Error: - continue - else: - return None - - @classmethod - def from_email(cls, config, email): - """Generate a new account from an email address. - - :param config: Configuration - :type config: :class:`letsencrypt.interfaces.IConfig` - - :param str email: Email address - - :raises .errors.Error: If invalid email address is given. - - """ - if not email or cls.safe_email(email): - email = email if email else None - - le_util.make_or_verify_dir( - config.account_keys_dir, 0o700, os.geteuid()) - key = crypto_util.init_save_key( - config.rsa_key_size, config.account_keys_dir, - cls._get_config_filename(email)) - return cls(config, key, email) - - raise errors.Error("Invalid email address.") - - @classmethod - def safe_email(cls, email): - """Scrub email address before using it.""" - if cls.EMAIL_REGEX.match(email): - return not email.startswith(".") and ".." not in email - else: - logger.warn("Invalid email address: %s.", email) - return False + def save(self, account): + account_dir_path = self._account_dir_path(account.id) + le_util.make_or_verify_dir(account_dir_path, 0o700, os.geteuid()) + try: + with open(self._regr_path(account_dir_path), "w") as regr_file: + regr_file.write(account.regr.json_dumps()) + with open(self._key_path(account_dir_path), "w") as key_file: + key_file.write(account.key.json_dumps()) + with open(self._metadata_path(account_dir_path), "w") as metadata_file: + metadata_file.write(account.meta.json_dumps()) + except IOError as error: + raise errors.AccountStorageError(error) diff --git a/letsencrypt/achallenges.py b/letsencrypt/achallenges.py index 0be626b35..a81ae05a2 100644 --- a/letsencrypt/achallenges.py +++ b/letsencrypt/achallenges.py @@ -57,7 +57,7 @@ class DVSNI(AnnotatedChallenge): """ response = challenges.DVSNIResponse(s=s) - cert_pem = crypto_util.make_ss_cert(self.key.pem, [ + cert_pem = crypto_util.make_ss_cert(self.key, [ self.domain, self.nonce_domain, response.z_domain(self.challb)]) return cert_pem, response diff --git a/letsencrypt/auth_handler.py b/letsencrypt/auth_handler.py index 2929166c2..bd6e89cc3 100644 --- a/letsencrypt/auth_handler.py +++ b/letsencrypt/auth_handler.py @@ -28,9 +28,7 @@ class AuthHandler(object): :class:`~acme.challenges.ContinuityChallenge` types :type cont_auth: :class:`letsencrypt.interfaces.IAuthenticator` - :ivar network: Network object for sending and receiving authorization - messages - :type network: :class:`letsencrypt.network.Network` + :ivar acme.client.Client acme: ACME client API. :ivar account: Client's Account :type account: :class:`letsencrypt.account.Account` @@ -43,10 +41,10 @@ class AuthHandler(object): form of :class:`letsencrypt.achallenges.AnnotatedChallenge` """ - def __init__(self, dv_auth, cont_auth, network, account): + def __init__(self, dv_auth, cont_auth, acme, account): self.dv_auth = dv_auth self.cont_auth = cont_auth - self.network = network + self.acme = acme self.account = account self.authzr = dict() @@ -71,8 +69,8 @@ class AuthHandler(object): """ for domain in domains: - self.authzr[domain] = self.network.request_domain_challenges( - domain, self.account.new_authzr_uri) + self.authzr[domain] = self.acme.request_domain_challenges( + domain, self.account.regr.new_authzr_uri) self._choose_challenges(domains) @@ -157,7 +155,7 @@ class AuthHandler(object): for achall, resp in itertools.izip(achalls, resps): # Don't send challenges for None and False authenticator responses if resp: - self.network.answer_challenge(achall.challb, resp) + self.acme.answer_challenge(achall.challb, resp) # TODO: answer_challenge returns challr, with URI, # that can be used in _find_updated_challr # comparisons... @@ -211,7 +209,7 @@ class AuthHandler(object): completed = [] failed = [] - self.authzr[domain], _ = self.network.poll(self.authzr[domain]) + self.authzr[domain], _ = self.acme.poll(self.authzr[domain]) if self.authzr[domain].body.status == messages.STATUS_VALID: return achalls, [] diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index 427365573..0797f23b4 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -6,6 +6,7 @@ import functools import logging import logging.handlers import os +import pkg_resources import sys import time import traceback @@ -76,23 +77,6 @@ More detailed help: """ -def _account_init(args, config): - # Prepare for init of Client - if args.email is None: - return client.determine_account(config) - else: - try: - # The way to get the default would be args.email = "" - # First try existing account - return account.Account.from_existing_account(config, args.email) - except errors.Error: - try: - # Try to make an account based on the email address - return account.Account.from_email(config, args.email) - except errors.Error: - return None - - def _find_domains(args, installer): if args.domains is None: domains = display_ops.choose_names(installer) @@ -106,30 +90,77 @@ def _find_domains(args, installer): return domains -def _init_acme(config, acc, authenticator, installer): - acme = client.Client(config, acc, authenticator, installer) +def _determine_account(args, config): + """Determine which account to use. - # Validate the key and csr - client.validate_key_csr(acc.key) + In order to make the renewer (configuration de/serialization) happy, + if ``args.account`` is ``None``, it will be updated based on the + user input. Same for ``args.email``. + + :param argparse.Namespace args: CLI arguments + :param letsencrypt.interface.IConfig config: Configuration object + :param .AccountStorage account_storage: Account storage. + + :returns: Account and optionally ACME client API (biproduct of new + registration). + :rtype: `tuple` of `letsencrypt.account.Account` and + `acme.client.Client` + + """ + account_storage = account.AccountFileStorage(config) + acme = None + + if args.account is not None: + acc = account_storage.load(args.account) + else: + accounts = account_storage.find_all() + if len(accounts) > 1: + acc = display_ops.choose_account(accounts) + elif len(accounts) == 1: + acc = accounts[0] + else: # no account registered yet + if args.email is None: + args.email = display_ops.get_email() + if not args.email: # get_email might return "" + args.email = None + + def _tos_cb(regr): + if args.tos: + return True + msg = ("Please read the Terms of Service at {0}. You " + "must agree in order to register with the ACME " + "server at {1}".format( + regr.terms_of_service, config.server)) + return zope.component.getUtility(interfaces.IDisplay).yesno( + msg, "Agree", "Cancel") - if authenticator is not None: - if acc.regr is None: try: - acme.register() + acc, acme = client.register( + config, account_storage, tos_cb=_tos_cb) except errors.Error as error: - logger.debug(error) - raise errors.Error("Unable to register an account with ACME " - "server") + logger.debug(error, exc_info=True) + raise errors.Error( + "Unable to register an account with ACME server") - return acme + args.account = acc.id + return acc, acme + + +def _init_le_client(args, config, authenticator, installer): + if authenticator is not None: + # if authenticator was given, then we will need account... + acc, acme = _determine_account(args, config) + logger.debug("Picked account: %r", acc) + # XXX + #crypto_util.validate_key_csr(acc.key) + else: + acc, acme = None, None + + return client.Client(config, acc, authenticator, installer, acme=acme) def run(args, config, plugins): """Obtain a certificate and install.""" - acc = _account_init(args, config) - if acc is None: - return None - if args.configurator is not None and (args.installer is not None or args.authenticator is not None): return ("Either --configurator or --authenticator/--installer" @@ -150,14 +181,15 @@ def run(args, config, plugins): return "Configurator could not be determined" domains = _find_domains(args, installer) - # TODO: Handle errors from _init_acme? - acme = _init_acme(config, acc, authenticator, installer) - lineage = acme.obtain_and_enroll_certificate( + # TODO: Handle errors from _init_le_client? + le_client = _init_le_client(args, config, authenticator, installer) + lineage = le_client.obtain_and_enroll_certificate( domains, authenticator, installer, plugins) if not lineage: return "Certificate could not be obtained" - acme.deploy_certificate(domains, lineage.privkey, lineage.cert, lineage.chain) - acme.enhance_config(domains, args.redirect) + le_client.deploy_certificate( + domains, lineage.privkey, lineage.cert, lineage.chain) + le_client.enhance_config(domains, args.redirect) def auth(args, config, plugins): @@ -169,10 +201,6 @@ def auth(args, config, plugins): # 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 - authenticator = display_ops.pick_authenticator( config, args.authenticator, plugins) if authenticator is None: @@ -183,16 +211,17 @@ def auth(args, config, plugins): else: installer = None - # TODO: Handle errors from _init_acme? - acme = _init_acme(config, acc, authenticator, installer) + # TODO: Handle errors from _init_le_client? + le_client = _init_le_client(args, config, authenticator, installer) if args.csr is not None: - certr, chain = acme.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")) - acme.save_certificate(certr, chain, args.cert_path, args.chain_path) + le_client.save_certificate( + certr, chain, args.cert_path, args.chain_path) else: domains = _find_domains(args, installer) - if not acme.obtain_and_enroll_certificate( + if not le_client.obtain_and_enroll_certificate( domains, authenticator, installer, plugins): return "Certificate could not be obtained" @@ -200,18 +229,16 @@ def auth(args, config, plugins): def install(args, config, plugins): """Install a previously obtained cert in a server.""" # XXX: Update for renewer/RenewableCert - acc = _account_init(args, config) - if acc is None: - return None - installer = display_ops.pick_installer(config, args.installer, plugins) if installer is None: return "Installer could not be determined" domains = _find_domains(args, installer) - acme = _init_acme(config, acc, authenticator=None, installer=installer) + le_client = _init_le_client( + args, config, authenticator=None, installer=installer) assert args.cert_path is not None # required=True in the subparser - acme.deploy_certificate(domains, args.key_path, args.cert_path, args.chain_path) - acme.enhance_config(domains, args.redirect) + le_client.deploy_certificate( + domains, args.key_path, args.cert_path, args.chain_path) + le_client.enhance_config(domains, args.redirect) def revoke(args, unused_config, unused_plugins): @@ -468,8 +495,14 @@ def create_parser(plugins, args): "automation", "--no-confirm", dest="no_confirm", action="store_true", help="Turn off confirmation screens, currently used for --revoke") helpful.add( - "automation", "--agree-eula", "-e", dest="tos", action="store_true", + "automation", "--agree-eula", dest="eula", action="store_true", + help="Agree to the Let's Encrypt Developer Preview EULA") + helpful.add( + "automation", "--agree-tos", dest="tos", action="store_true", help="Agree to the Let's Encrypt Subscriber Agreement") + helpful.add( + "automation", "--account", metavar="ACCOUNT_ID", + help="Account ID to use") helpful.add_group( "testing", description="The following flags are meant for " @@ -724,6 +757,13 @@ def main(cli_args=sys.argv[1:]): zope.component.provideUtility(report) atexit.register(report.atexit_print_messages) + # TODO: remove developer EULA prompt for the launch + if not config.eula: + eula = pkg_resources.resource_string("letsencrypt", "EULA") + if not zope.component.getUtility(interfaces.IDisplay).yesno( + eula, "Agree", "Cancel"): + raise errors.Error("Must agree to TOS") + if not os.geteuid() == 0: logger.warning( "Root (sudo) is required to run most of letsencrypt functionality.") diff --git a/letsencrypt/client.py b/letsencrypt/client.py index 65659effa..a513ff797 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -1,13 +1,15 @@ -"""ACME protocol client class and helper functions.""" +"""Let's Encrypt client API.""" import logging import os -import pkg_resources +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import rsa import OpenSSL import zope.component +from acme import client as acme_client from acme import jose -from acme.jose import jwk +from acme import messages from letsencrypt import account from letsencrypt import auth_handler @@ -18,7 +20,6 @@ from letsencrypt import crypto_util from letsencrypt import errors from letsencrypt import interfaces from letsencrypt import le_util -from letsencrypt import network from letsencrypt import reverter from letsencrypt import revoker from letsencrypt import storage @@ -30,48 +31,107 @@ from letsencrypt.display import enhancements logger = logging.getLogger(__name__) +def _acme_from_config_key(config, key): + # TODO: Allow for other alg types besides RS256 + return acme_client.Client(new_reg_uri=config.server, key=key, + verify_ssl=(not config.no_verify_ssl)) + + +def register(config, account_storage, tos_cb=None): + """Register new account with an ACME CA. + + This function takes care of generating fresh private key, + registering the account, optionally accepting CA Terms of Service + and finally saving the account. It should be called prior to + initialization of `Client`, unless account has already been created. + + :param .IConfig config: Client configuration. + + :param .AccountStorage account_storage: Account storage where newly + registered account will be saved to. Save happens only after TOS + acceptance step, so any account private keys or + `.RegistrationResource` will not be persisted if `tos_cb` + returns ``False``. + + :param tos_cb: If ACME CA requires the user to accept a Terms of + Service before registering account, client action is + necessary. For example, a CLI tool would prompt the user + acceptance. `tos_cb` must be a callable that should accept + `.RegistrationResource` and return a `bool`: ``True`` iff the + Terms of Service present in the contained + `.Registration.terms_of_service` is accepted by the client, and + ``False`` otherwise. ``tos_cb`` will be called only if the + client acction is necessary, i.e. when ``terms_of_service is not + None``. This argument is optional, if not supplied it will + default to automatic acceptance! + + :raises letsencrypt.errors.Error: In case of any client problems, in + particular registration failure, or unaccepted Terms of Service. + :raises acme.errors.Error: In case of any protocol problems. + + :returns: Newly registered and saved account, as well as protocol + API handle (should be used in `Client` initialization). + :rtype: `tuple` of `.Account` and `acme.client.Client` + + """ + # Log non-standard actions, potentially wrong API calls + if account_storage.find_all(): + logger.info("There are already existing accounts for %s", config.server) + if config.email is None: + logger.warn("Registering without email!") + + # Each new registration shall use a fresh new key + key = jose.JWKRSA(key=jose.ComparableRSAKey( + rsa.generate_private_key( + public_exponent=65537, + key_size=config.rsa_key_size, + backend=default_backend()))) + acme = _acme_from_config_key(config, key) + # TODO: add phone? + regr = acme.register(messages.Registration.from_data(email=config.email)) + + if regr.terms_of_service is not None: + if tos_cb is not None and not tos_cb(regr): + raise errors.Error( + "Registration cannot proceed without accepting " + "Terms of Service.") + regr = acme.agree_to_tos(regr) + + acc = account.Account(regr, key) + account.report_new_account(acc, config) + account_storage.save(acc) + return acc, acme + + class Client(object): """ACME protocol client. - :ivar network: Network object for sending and receiving messages - :type network: :class:`letsencrypt.network.Network` - - :ivar account: Account object used for registration - :type account: :class:`letsencrypt.account.Account` - - :ivar auth_handler: Object that supports the IAuthenticator interface. - auth_handler contains both a dv_authenticator and a - continuity_authenticator - :type auth_handler: :class:`letsencrypt.auth_handler.AuthHandler` - - :ivar installer: Object supporting the IInstaller interface. - :type installer: :class:`letsencrypt.interfaces.IInstaller` - - :ivar config: Configuration. - :type config: :class:`~letsencrypt.interfaces.IConfig` + :ivar .IConfig config: Client configuration. + :ivar .Account account: Account registered with `register`. + :ivar .AuthHandler auth_handler: Authorizations handler that will + dispatch DV and Continuity challenges to appropriate + authenticators (providing `.IAuthenticator` interface). + :ivar .IInstaller installer: Installer. + :ivar acme.client.Client acme: Optional ACME client API handle. + You might already have one from `register`. """ - def __init__(self, config, account_, dv_auth, installer): + def __init__(self, config, account_, dv_auth, installer, acme=None): """Initialize a client. - :param dv_auth: IAuthenticator that can solve the - :const:`letsencrypt.constants.DV_CHALLENGES`. - The :meth:`~letsencrypt.interfaces.IAuthenticator.prepare` - must have already been run. - :type dv_auth: :class:`letsencrypt.interfaces.IAuthenticator` + :param .IAuthenticator dv_auth: Prepared (`.IAuthenticator.prepare`) + authenticator that can solve the `.constants.DV_CHALLENGES`. """ + self.config = config self.account = account_ - self.installer = installer - # TODO: Allow for other alg types besides RS256 - self.network = network.Network( - config.server, jwk.JWKRSA.load(self.account.key.pem), - verify_ssl=(not config.no_verify_ssl)) - - self.config = config + # Initialize ACME if account is provided + if acme is None and self.account is not None: + acme = _acme_from_config_key(config, self.account.key) + self.acme = acme # TODO: Check if self.config.enroll_autorenew is None. If # so, set it based to the default: figure out if dv_auth is @@ -81,53 +141,10 @@ class Client(object): cont_auth = continuity_auth.ContinuityAuthenticator(config, installer) self.auth_handler = auth_handler.AuthHandler( - dv_auth, cont_auth, self.network, self.account) + dv_auth, cont_auth, self.acme, self.account) else: self.auth_handler = None - def register(self): - """New Registration with the ACME server.""" - self.account = self.network.register_from_account(self.account) - if self.account.terms_of_service is not None: - if not self.config.tos: - # TODO: Replace with self.account.terms_of_service - eula = pkg_resources.resource_string("letsencrypt", "EULA") - agree = zope.component.getUtility(interfaces.IDisplay).yesno( - eula, "Agree", "Cancel") - else: - agree = True - - if agree: - self.account.regr = self.network.agree_to_tos(self.account.regr) - else: - # What is the proper response here... - raise errors.Error("Must agree to TOS") - - self.account.save() - self._report_new_account() - - def _report_new_account(self): - """Informs the user about their new Let's Encrypt account.""" - reporter = zope.component.getUtility(interfaces.IReporter) - reporter.add_message( - "Your account credentials have been saved in your Let's Encrypt " - "configuration directory at {0}. You should make a secure backup " - "of this folder now. This configuration directory will also " - "contain certificates and private keys obtained by Let's Encrypt " - "so making regular backups of this folder is ideal.".format( - self.config.config_dir), - reporter.MEDIUM_PRIORITY, True) - - assert self.account.recovery_token is not None - recovery_msg = ("If you lose your account credentials, you can recover " - "them using the token \"{0}\". You must write that down " - "and put it in a safe place.".format( - self.account.recovery_token)) - if self.account.email is not None: - recovery_msg += (" Another recovery method will be e-mails sent to " - "{0}.".format(self.account.email)) - reporter.add_message(recovery_msg, reporter.HIGH_PRIORITY, True) - def _obtain_certificate(self, domains, csr): """Obtain certificate. @@ -155,11 +172,11 @@ class Client(object): logger.debug("CSR: %s, domains: %s", csr, domains) authzr = self.auth_handler.get_authorizations(domains) - certr = self.network.request_issuance( + certr = self.acme.request_issuance( jose.ComparableX509(OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_ASN1, csr.data)), authzr) - return certr, self.network.fetch_chain(certr) + return certr, self.acme.fetch_chain(certr) def obtain_certificate_from_csr(self, csr): """Obtain certficiate from CSR. @@ -451,28 +468,6 @@ def validate_key_csr(privkey, csr=None): raise errors.Error("The key and CSR do not match") -def determine_account(config): - """Determine which account to use. - - Will create an account if necessary. - - :param config: Configuration object - :type config: :class:`letsencrypt.interfaces.IConfig` - - :returns: Account - :rtype: :class:`letsencrypt.account.Account` - - """ - accounts = account.Account.get_accounts(config) - - if len(accounts) == 1: - return accounts[0] - elif len(accounts) > 1: - return display_ops.choose_account(accounts) - - return account.Account.from_prompts(config) - - def rollback(default_installer, checkpoints, config, plugins): """Revert configuration the specified number of checkpoints. diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index 2a9e87ade..d68b46e52 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -18,7 +18,6 @@ class NamespaceConfig(object): paths defined in :py:mod:`letsencrypt.constants`: - `accounts_dir` - - `account_keys_dir` - `cert_dir` - `cert_key_backup` - `in_progress_dir` @@ -51,10 +50,6 @@ class NamespaceConfig(object): return os.path.join( self.namespace.config_dir, constants.ACCOUNTS_DIR, self.server_path) - @property - def account_keys_dir(self): #pylint: disable=missing-docstring - return os.path.join(self.accounts_dir, constants.ACCOUNT_KEYS_DIR) - @property def backup_dir(self): # pylint: disable=missing-docstring return os.path.join(self.namespace.work_dir, constants.BACKUP_DIR) diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 4b0a215e1..07d1965fb 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -65,9 +65,6 @@ CONFIG_DIRS_MODE = 0o755 ACCOUNTS_DIR = "accounts" """Directory where all accounts are saved.""" -ACCOUNT_KEYS_DIR = "keys" -"""Directory where account keys are saved. Relative to `ACCOUNTS_DIR`.""" - BACKUP_DIR = "backups" """Directory (relative to `IConfig.work_dir`) where backups are kept.""" diff --git a/letsencrypt/crypto_util.py b/letsencrypt/crypto_util.py index 933f97362..edfd2eccf 100644 --- a/letsencrypt/crypto_util.py +++ b/letsencrypt/crypto_util.py @@ -8,8 +8,11 @@ import datetime import logging import os +from cryptography.hazmat.primitives import serialization import OpenSSL +from acme import jose + from letsencrypt import errors from letsencrypt import le_util @@ -212,15 +215,21 @@ def pyopenssl_load_certificate(data): return _pyopenssl_load(data, OpenSSL.crypto.load_certificate) -def make_ss_cert(key_str, domains, not_before=None, +def make_ss_cert(key, domains, not_before=None, validity=(7 * 24 * 60 * 60)): """Returns new self-signed cert in PEM form. - Uses key_str and contains all domains. + Uses key and contains all domains. """ + if isinstance(key, jose.JWK): + key = key.key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption()) + assert domains, "Must provide one or more hostnames for the cert." - pkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, key_str) + pkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, key) cert = OpenSSL.crypto.X509() cert.set_serial_number(1337) cert.set_version(2) diff --git a/letsencrypt/display/ops.py b/letsencrypt/display/ops.py index 71d2e80f6..afb7d6688 100644 --- a/letsencrypt/display/ops.py +++ b/letsencrypt/display/ops.py @@ -5,6 +5,7 @@ import os import zope.component from letsencrypt import interfaces +from letsencrypt import le_util from letsencrypt.display import util as display_util @@ -112,6 +113,24 @@ def pick_configurator( (interfaces.IAuthenticator, interfaces.IInstaller)) +def get_email(): + """Prompt for valid email address. + + :returns: Email or ``None`` if cancelled by user. + :rtype: str + + """ + while True: + code, email = zope.component.getUtility(interfaces.IDisplay).input( + "Enter email address") + + if code == display_util.OK: + if le_util.safe_email(email): + return email + else: + return None + + def choose_account(accounts): """Choose an account. @@ -120,11 +139,7 @@ def choose_account(accounts): """ # Note this will get more complicated once we start recording authorizations - labels = [ - "%s | %s" % (acc.email.ljust(display_util.WIDTH - 39), - acc.phone if acc.phone is not None else "") - for acc in accounts - ] + labels = [acc.slug for acc in accounts] code, index = util(interfaces.IDisplay).menu( "Please choose an account", labels) diff --git a/letsencrypt/errors.py b/letsencrypt/errors.py index 59a934b6d..e1cae19c7 100644 --- a/letsencrypt/errors.py +++ b/letsencrypt/errors.py @@ -5,6 +5,14 @@ class Error(Exception): """Generic Let's Encrypt client error.""" +class AccountStorageError(Error): + """Generic `.AccountStorage` error.""" + + +class AccountNotFound(AccountStorageError): + """Account not found error.""" + + class ReverterError(Error): """Let's Encrypt Reverter error.""" diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index 4b93757c8..b07e64894 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -1,10 +1,46 @@ """Let's Encrypt client interfaces.""" +import abc import zope.interface # pylint: disable=no-self-argument,no-method-argument,no-init,inherit-non-class # pylint: disable=too-few-public-methods +class AccountStorage(object): + """Accounts storage interface.""" + + __metaclass__ = abc.ABCMeta + + @abc.abstractmethod + def find_all(self): # pragma: no cover + """Find all accounts. + + :returns: All found accounts. + :rtype: list + + """ + raise NotImplementedError() + + @abc.abstractmethod + def load(self, account_id): # pragma: no cover + """Load an account by its id. + + :raises .AccountNotFound: if account could not be found + :raises .AccountStorageError: if account could not be loaded + + """ + raise NotImplementedError() + + @abc.abstractmethod + def save(self, account): # pragma: no cover + """Save account. + + :raises .AccountStorageError: if account could not be saved + + """ + raise NotImplementedError() + + class IPluginFactory(zope.interface.Interface): """IPlugin factory. @@ -160,8 +196,6 @@ class IConfig(zope.interface.Interface): accounts_dir = zope.interface.Attribute( "Directory where all account information is stored.") - account_keys_dir = zope.interface.Attribute( - "Directory where all account keys are stored.") backup_dir = zope.interface.Attribute("Configuration backups directory.") cert_dir = zope.interface.Attribute( "Directory where newly generated Certificate Signing Requests " diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index 0f8207b7a..583754ab5 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -1,12 +1,17 @@ """Utilities for all Let's Encrypt.""" import collections import errno +import logging import os +import re import stat from letsencrypt import errors +logger = logging.getLogger(__name__) + + Key = collections.namedtuple("Key", "file pem") # Note: form is the type of data, "pem" or "der" CSR = collections.namedtuple("CSR", "file data form") @@ -118,3 +123,16 @@ def safely_remove(path): except OSError as err: if err.errno != errno.ENOENT: raise + + +# Just make sure we don't get pwned... Make sure that it also doesn't +# start with a period or have two consecutive periods <- this needs to +# be done in addition to the regex +EMAIL_REGEX = re.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+$") +def safe_email(email): + """Scrub email address before using it.""" + if EMAIL_REGEX.match(email) is not None: + return not email.startswith(".") and ".." not in email + else: + logger.warn("Invalid email address: %s.", email) + return False diff --git a/letsencrypt/network.py b/letsencrypt/network.py deleted file mode 100644 index a890c94e2..000000000 --- a/letsencrypt/network.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Networking for ACME protocol.""" -from acme import client -from acme import messages - - -class Network(client.Client): - """ACME networking.""" - - def register_from_account(self, account): - """Register with server. - - .. todo:: this should probably not be a part of network... - - :param account: Account - :type account: :class:`letsencrypt.account.Account` - - :returns: Updated account - :rtype: :class:`letsencrypt.account.Account` - - """ - account.regr = self.register(messages.Registration.from_data( - email=account.email, phone=account.phone)) - return account diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 1c5ccbefd..1d77636aa 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -4,6 +4,7 @@ import pkg_resources import shutil import tempfile +from cryptography.hazmat.primitives import serialization import zope.interface from acme.jose import util as jose_util @@ -157,6 +158,11 @@ class Dvsni(object): return os.path.join( self.configurator.config.work_dir, achall.nonce_domain + ".crt") + def get_key_path(self, achall): + """Get standardized path to challenge key.""" + return os.path.join( + self.configurator.config.work_dir, achall.nonce_domain + '.pem') + def _setup_challenge_cert(self, achall, s=None): # pylint: disable=invalid-name """Generate and write out challenge certificate.""" @@ -170,6 +176,16 @@ class Dvsni(object): with open(cert_path, "w") as cert_chall_fd: cert_chall_fd.write(cert_pem) + key_path = self.get_key_path(achall) + key_pem = achall.key.key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption()) + # XXX: key_file chmods! (SEC) + with open(key_path, 'w') as key_file: + key_file.write(key_pem) + self.configurator.reverter.register_file_creation(True, key_path) + return response diff --git a/letsencrypt/plugins/common_test.py b/letsencrypt/plugins/common_test.py index c81d3939f..a692efc9c 100644 --- a/letsencrypt/plugins/common_test.py +++ b/letsencrypt/plugins/common_test.py @@ -165,7 +165,9 @@ class DvsniTest(unittest.TestCase): self.assertTrue(m_open.called) self.assertEqual( m_open.call_args[0], (self.sni.get_cert_file(achall), "w")) - self.assertEqual(m_open().write.call_args[0][0], "pem") + self.assertEqual(m_open().write.mock_calls[0][1][0], "pem") + self.assertEqual(m_open().write.mock_calls[1][1][0], + achall.key.key.private_bytes()) if __name__ == "__main__": diff --git a/letsencrypt/plugins/standalone/authenticator.py b/letsencrypt/plugins/standalone/authenticator.py index 35b579eea..d9a0e2d19 100644 --- a/letsencrypt/plugins/standalone/authenticator.py +++ b/letsencrypt/plugins/standalone/authenticator.py @@ -6,6 +6,7 @@ import socket import sys import time +from cryptography.hazmat.primitives import serialization import OpenSSL import zope.component import zope.interface @@ -214,7 +215,10 @@ class StandaloneAuthenticator(common.Plugin): # Signal that we've successfully bound TCP port os.kill(self.parent_pid, signal.SIGIO) self.private_key = OpenSSL.crypto.load_privatekey( - OpenSSL.crypto.FILETYPE_PEM, key.pem) + OpenSSL.crypto.FILETYPE_PEM, key.key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption())) while True: self.connection, _ = self.sock.accept() diff --git a/letsencrypt/plugins/standalone/tests/authenticator_test.py b/letsencrypt/plugins/standalone/tests/authenticator_test.py index e98616f84..422dc0549 100644 --- a/letsencrypt/plugins/standalone/tests/authenticator_test.py +++ b/letsencrypt/plugins/standalone/tests/authenticator_test.py @@ -6,21 +6,27 @@ import signal import socket import unittest +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization import mock import OpenSSL from acme import challenges +from acme import jose from letsencrypt import achallenges -from letsencrypt import le_util from letsencrypt.tests import acme_util -KEY = le_util.Key("foo", pkg_resources.resource_string( - "acme.jose", os.path.join("testdata", "rsa512_key.pem"))) +KEY_PATH = pkg_resources.resource_filename( + "acme.jose", os.path.join("testdata", "rsa512_key.pem")) +KEY_DATA = pkg_resources.resource_string( + "acme.jose", os.path.join("testdata", "rsa512_key.pem")) +KEY = jose.JWKRSA(key=jose.ComparableRSAKey(serialization.load_pem_private_key( + KEY_DATA, password=None, backend=default_backend()))) PRIVATE_KEY = OpenSSL.crypto.load_privatekey( - OpenSSL.crypto.FILETYPE_PEM, KEY.pem) + OpenSSL.crypto.FILETYPE_PEM, KEY_DATA) CONFIG = mock.Mock(dvsni_port=5001) diff --git a/letsencrypt/renewer.py b/letsencrypt/renewer.py index 3ccfc1b58..bc5277333 100644 --- a/letsencrypt/renewer.py +++ b/letsencrypt/renewer.py @@ -15,6 +15,7 @@ import configobj import OpenSSL import zope.component +from letsencrypt import account from letsencrypt import configuration from letsencrypt import cli from letsencrypt import client @@ -76,15 +77,13 @@ def renew(cert, old_version): authenticator = authenticator.init(config) authenticator.prepare() - account = client.determine_account(config) - # TODO: are there other ways to get the right account object, e.g. - # based on the email parameter that might be present in - # renewalparams? + acc = account.AccountFileStorage(config).load( + account_id=renewalparams["account"]) - our_client = client.Client(config, account, authenticator, None) + le_client = client.Client(config, acc, authenticator, None) with open(cert.version("cert", old_version)) as f: sans = crypto_util.get_sans_from_cert(f.read()) - new_certr, new_chain, new_key, _ = our_client.obtain_certificate(sans) + new_certr, new_chain, new_key, _ = le_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 diff --git a/letsencrypt/revoker.py b/letsencrypt/revoker.py index 7597ff2ce..426237661 100644 --- a/letsencrypt/revoker.py +++ b/letsencrypt/revoker.py @@ -15,12 +15,12 @@ import tempfile import OpenSSL +from acme import client as acme_client from acme.jose import util as jose_util from letsencrypt import crypto_util from letsencrypt import errors from letsencrypt import le_util -from letsencrypt import network from letsencrypt.display import util as display_util from letsencrypt.display import revocation @@ -34,8 +34,7 @@ class Revoker(object): .. todo:: Add a method to specify your own certificate for revocation - CLI - :ivar network: Network object - :type network: :class:`letsencrypt.network` + :ivar .acme.client.Client acme: ACME client :ivar installer: Installer object :type installer: :class:`~letsencrypt.interfaces.IInstaller` @@ -48,7 +47,7 @@ class Revoker(object): """ def __init__(self, installer, config, no_confirm=False): # XXX - self.network = network.Network(new_reg_uri=None, key=None, alg=None) + self.acme = acme_client.Client(new_reg_uri=None, key=None, alg=None) self.installer = installer self.config = config @@ -263,7 +262,7 @@ class Revoker(object): raise errors.RevokerError( "Corrupted backup key file: %s" % cert.backup_key_path) - return self.network.revoke(cert=None) # XXX + return self.acme.revoke(cert=None) # XXX def _remove_certs_keys(self, cert_list): # pylint: disable=no-self-use """Remove certificate and key. diff --git a/letsencrypt/tests/account_test.py b/letsencrypt/tests/account_test.py index 6b9fafe31..174d3614c 100644 --- a/letsencrypt/tests/account_test.py +++ b/letsencrypt/tests/account_test.py @@ -1,208 +1,173 @@ """Tests for letsencrypt.account.""" -import logging -import mock +import datetime import os import pkg_resources import shutil import tempfile import unittest +import mock +import pytz + +from acme import jose from acme import messages -from letsencrypt import configuration from letsencrypt import errors -from letsencrypt import le_util -from letsencrypt.display import util as display_util + +KEY = jose.JWKRSA.load(pkg_resources.resource_string( + __name__, os.path.join("testdata", "rsa512_key.pem"))) class AccountTest(unittest.TestCase): - """Tests letsencrypt.account.Account.""" + """Tests for letsencrypt.account.Account.""" def setUp(self): from letsencrypt.account import Account + self.regr = mock.MagicMock() + self.meta = Account.Meta( + creation_host="test.letsencrypt.org", + creation_dt=datetime.datetime( + 2015, 7, 4, 14, 4, 10, tzinfo=pytz.UTC)) + self.acc = Account(self.regr, KEY, self.meta) - logging.disable(logging.CRITICAL) + with mock.patch("letsencrypt.account.socket") as mock_socket: + mock_socket.getfqdn.return_value = "test.letsencrypt.org" + with mock.patch("letsencrypt.account.datetime") as mock_dt: + mock_dt.datetime.now.return_value = self.meta.creation_dt + self.acc_no_meta = Account(self.regr, KEY) - self.accounts_dir = tempfile.mkdtemp("accounts") - self.account_keys_dir = os.path.join(self.accounts_dir, "keys") - os.makedirs(self.account_keys_dir, 0o700) + def test_init(self): + self.assertEqual(self.regr, self.acc.regr) + self.assertEqual(KEY, self.acc.key) + self.assertEqual(self.meta, self.acc_no_meta.meta) + def test_id(self): + self.assertEqual( + self.acc.id, "test.letsencrypt.org@2015-07-04T14:04:10Z") + + def test_slug(self): + self.assertEqual( + self.acc.slug, "test.letsencrypt.org@2015-07-04T14:04:10Z") + + def test_repr(self): + self.assertEqual( + repr(self.acc), + "") + + +class ReportNewAccountTest(unittest.TestCase): + """Tests for letsencrypt.account.report_new_account.""" + + def setUp(self): + self.config = mock.MagicMock(config_dir='/etc/letsencrypt') + reg = messages.Registration.from_data(email="rhino@jungle.io") + reg = reg.update(recovery_token="ECCENTRIC INVISIBILITY RHINOCEROS") + self.acc = mock.MagicMock(regr=messages.RegistrationResource( + uri=None, new_authzr_uri=None, body=reg)) + + def _call(self): + from letsencrypt.account import report_new_account + report_new_account(self.acc, self.config) + + @mock.patch("letsencrypt.client.zope.component.queryUtility") + def test_no_reporter(self, mock_zope): + mock_zope.return_value = None + self._call() + + @mock.patch("letsencrypt.client.zope.component.queryUtility") + def test_it(self, mock_zope): + self._call() + call_list = mock_zope().add_message.call_args_list + self.assertTrue(self.config.config_dir in call_list[0][0][0]) + self.assertTrue(self.acc.regr.body.recovery_token in call_list[1][0][0]) + self.assertTrue(self.acc.regr.body.email in call_list[1][0][0]) + + +class AccountMemoryStorageTest(unittest.TestCase): + """Tests for letsencrypt.account.AccountMemoryStorage.""" + + def setUp(self): + from letsencrypt.account import AccountMemoryStorage + self.storage = AccountMemoryStorage() + + def test_it(self): + account = mock.Mock(id="x") + self.assertEqual([], self.storage.find_all()) + self.assertRaises(errors.AccountNotFound, self.storage.load, "x") + self.storage.save(account) + self.assertEqual([account], self.storage.find_all()) + self.assertEqual(account, self.storage.load("x")) + + +class AccountFileStorageTest(unittest.TestCase): + """Tests for letsencrypt.account.AccountFileStorage.""" + + def setUp(self): + self.tmp = tempfile.mkdtemp() self.config = mock.MagicMock( - spec=configuration.NamespaceConfig, accounts_dir=self.accounts_dir, - account_keys_dir=self.account_keys_dir, rsa_key_size=2048, - server="letsencrypt-demo.org") + accounts_dir=os.path.join(self.tmp, "accounts")) + from letsencrypt.account import AccountFileStorage + self.storage = AccountFileStorage(self.config) - key_file = pkg_resources.resource_filename( - "acme.jose", os.path.join("testdata", "rsa512_key.pem")) - key_pem = pkg_resources.resource_string( - "acme.jose", os.path.join("testdata", "rsa512_key.pem")) - - self.key = le_util.Key(key_file, key_pem) - self.email = "client@letsencrypt.org" - self.regr = messages.RegistrationResource( - uri="uri", - new_authzr_uri="new_authzr_uri", - terms_of_service="terms_of_service", - body=messages.Registration( - recovery_token="recovery_token", agreement="agreement") - ) - - self.test_account = Account( - self.config, self.key, self.email, None, self.regr) + from letsencrypt.account import Account + self.acc = Account( + regr=messages.RegistrationResource( + uri=None, new_authzr_uri=None, body=messages.Registration()), + key=KEY) def tearDown(self): - shutil.rmtree(self.accounts_dir) - logging.disable(logging.NOTSET) + shutil.rmtree(self.tmp) - @mock.patch("letsencrypt.account.zope.component.getUtility") - @mock.patch("letsencrypt.account.crypto_util.init_save_key") - def test_prompts(self, mock_key, mock_util): - from letsencrypt.account import Account + def test_init_creates_dir(self): + self.assertTrue(os.path.isdir(self.config.accounts_dir)) - mock_util().input.return_value = (display_util.OK, self.email) - mock_key.return_value = self.key + def test_save_and_restore(self): + self.storage.save(self.acc) + account_path = os.path.join(self.config.accounts_dir, self.acc.id) + self.assertTrue(os.path.exists(account_path)) + for file_name in "regr.json", "meta.json", "private_key.json": + self.assertTrue(os.path.exists( + os.path.join(account_path, file_name))) - acc = Account.from_prompts(self.config) - self.assertEqual(acc.email, self.email) - self.assertEqual(acc.key, self.key) - self.assertEqual(acc.config, self.config) + # restore + self.assertEqual(self.acc, self.storage.load(self.acc.id)) - @mock.patch("letsencrypt.account.zope.component.getUtility") - @mock.patch("letsencrypt.account.Account.from_email") - def test_prompts_bad_email(self, mock_from_email, mock_util): - from letsencrypt.account import Account + def test_find_all(self): + self.storage.save(self.acc) + self.assertEqual([self.acc], self.storage.find_all()) - mock_from_email.side_effect = (errors.Error, "acc") - mock_util().input.return_value = (display_util.OK, self.email) + def test_find_all_none_empty_list(self): + self.assertEqual([], self.storage.find_all()) - self.assertEqual(Account.from_prompts(self.config), "acc") + def test_find_all_accounts_dir_absent(self): + os.rmdir(self.config.accounts_dir) + self.assertEqual([], self.storage.find_all()) + def test_find_all_load_skips(self): + self.storage.load = mock.MagicMock( + side_effect=["x", errors.AccountStorageError, "z"]) + with mock.patch("letsencrypt.account.os.listdir") as mock_listdir: + mock_listdir.return_value = ["x", "y", "z"] + self.assertEqual(["x", "z"], self.storage.find_all()) - @mock.patch("letsencrypt.account.zope.component.getUtility") - @mock.patch("letsencrypt.account.crypto_util.init_save_key") - def test_prompts_empty_email(self, mock_key, mock_util): - from letsencrypt.account import Account + def test_load_non_existent_raises_error(self): + self.assertRaises(errors.AccountNotFound, self.storage.load, "missing") - mock_util().input.return_value = (display_util.OK, "") - acc = Account.from_prompts(self.config) - self.assertTrue(acc.email is None) - # _get_config_filename | pylint: disable=protected-access - mock_key.assert_called_once_with( - mock.ANY, mock.ANY, acc._get_config_filename(None)) + def test_load_ioerror(self): + self.storage.save(self.acc) + mock_open = mock.mock_open() + mock_open.side_effect = IOError + with mock.patch("__builtin__.open", mock_open): + self.assertRaises( + errors.AccountStorageError, self.storage.load, self.acc.id) - @mock.patch("letsencrypt.account.zope.component.getUtility") - def test_prompts_cancel(self, mock_util): - from letsencrypt.account import Account - - mock_util().input.return_value = (display_util.CANCEL, "") - - self.assertTrue(Account.from_prompts(self.config) is None) - - def test_from_email(self): - from letsencrypt.account import Account - - self.assertRaises( - errors.Error, Account.from_email, self.config, "not_valid...email") - - def test_save_from_existing_account(self): - from letsencrypt.account import Account - - self.test_account.save() - acc = Account.from_existing_account(self.config, self.email) - - self.assertEqual(acc.key, self.test_account.key) - self.assertEqual(acc.email, self.test_account.email) - self.assertEqual(acc.phone, self.test_account.phone) - self.assertEqual(acc.regr, self.test_account.regr) - - def test_properties(self): - self.assertEqual(self.test_account.uri, "uri") - self.assertEqual(self.test_account.new_authzr_uri, "new_authzr_uri") - self.assertEqual(self.test_account.terms_of_service, "terms_of_service") - self.assertEqual(self.test_account.recovery_token, "recovery_token") - - def test_partial_properties(self): - from letsencrypt.account import Account - - partial = Account(self.config, self.key) - - self.assertTrue(partial.uri is None) - self.assertTrue(partial.new_authzr_uri is None) - self.assertTrue(partial.terms_of_service is None) - self.assertTrue(partial.recovery_token is None) - - def test_partial_account_default(self): - from letsencrypt.account import Account - - partial = Account(self.config, self.key) - partial.save() - - acc = Account.from_existing_account(self.config) - - self.assertEqual(partial.key, acc.key) - self.assertEqual(partial.email, acc.email) - self.assertEqual(partial.phone, acc.phone) - self.assertEqual(partial.regr, acc.regr) - - def test_get_accounts(self): - from letsencrypt.account import Account - - accs = Account.get_accounts(self.config) - self.assertFalse(accs) - - self.test_account.save() - accs = Account.get_accounts(self.config) - self.assertEqual(len(accs), 1) - self.assertEqual(accs[0].email, self.test_account.email) - - acc2 = Account(self.config, self.key, "testing_email@gmail.com") - acc2.save() - accs = Account.get_accounts(self.config) - self.assertEqual(len(accs), 2) - - def test_get_accounts_no_accounts(self): - from letsencrypt.account import Account - - self.assertEqual(Account.get_accounts( - mock.Mock(accounts_dir="non-existant")), []) - - def test_failed_existing_account(self): - from letsencrypt.account import Account - - self.assertRaises(errors.Error, Account.from_existing_account, - self.config, "non-existant@email.org") - -class SafeEmailTest(unittest.TestCase): - """Test safe_email.""" - def setUp(self): - logging.disable(logging.CRITICAL) - - def tearDown(self): - logging.disable(logging.NOTSET) - - @classmethod - def _call(cls, addr): - from letsencrypt.account import Account - return Account.safe_email(addr) - - def test_valid_emails(self): - addrs = [ - "letsencrypt@letsencrypt.org", - "tbd.ade@gmail.com", - "abc_def.jdk@hotmail.museum", - ] - for addr in addrs: - self.assertTrue(self._call(addr), "%s failed." % addr) - - def test_invalid_emails(self): - addrs = [ - "letsencrypt@letsencrypt..org", - ".tbd.ade@gmail.com", - "~/abc_def.jdk@hotmail.museum", - ] - for addr in addrs: - self.assertFalse(self._call(addr), "%s failed." % addr) + def test_save_ioerrors(self): + mock_open = mock.mock_open() + mock_open.side_effect = IOError # TODO: [None, None, IOError] + with mock.patch("__builtin__.open", mock_open): + self.assertRaises( + errors.AccountStorageError, self.storage.save, self.acc) if __name__ == "__main__": diff --git a/letsencrypt/tests/achallenges_test.py b/letsencrypt/tests/achallenges_test.py index b06398fe2..e6c154aae 100644 --- a/letsencrypt/tests/achallenges_test.py +++ b/letsencrypt/tests/achallenges_test.py @@ -3,12 +3,14 @@ import os import pkg_resources import unittest +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization import OpenSSL from acme import challenges +from acme import jose from letsencrypt import crypto_util -from letsencrypt import le_util from letsencrypt.tests import acme_util @@ -19,8 +21,11 @@ class DVSNITest(unittest.TestCase): self.chall = acme_util.chall_to_challb( challenges.DVSNI(r="r_value", nonce="12345ABCDE"), "pending") self.response = challenges.DVSNIResponse() - key = le_util.Key("path", pkg_resources.resource_string( - "acme.jose", os.path.join("testdata", "rsa512_key.pem"))) + key = jose.JWKRSA(key=jose.ComparableRSAKey( + serialization.load_pem_private_key( + pkg_resources.resource_string( + "acme.jose", os.path.join("testdata", "rsa512_key.pem")), + password=None, backend=default_backend()))) from letsencrypt.achallenges import DVSNI self.achall = DVSNI(challb=self.chall, domain="example.com", key=key) diff --git a/letsencrypt/tests/auth_handler_test.py b/letsencrypt/tests/auth_handler_test.py index 6a94baea7..1cb40d5d9 100644 --- a/letsencrypt/tests/auth_handler_test.py +++ b/letsencrypt/tests/auth_handler_test.py @@ -6,11 +6,11 @@ import unittest import mock from acme import challenges +from acme import client as acme_client from acme import messages from letsencrypt import errors from letsencrypt import le_util -from letsencrypt import network from letsencrypt.tests import acme_util @@ -86,7 +86,7 @@ class GetAuthorizationsTest(unittest.TestCase): self.mock_dv_auth.perform.side_effect = gen_auth_resp self.mock_account = mock.Mock(key=le_util.Key("file_path", "PEM")) - self.mock_net = mock.MagicMock(spec=network.Network) + self.mock_net = mock.MagicMock(spec=acme_client.Client) self.handler = AuthHandler( self.mock_dv_auth, self.mock_cont_auth, diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index cdc5826df..130da4217 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -8,6 +8,8 @@ import unittest import mock +from letsencrypt import account +from letsencrypt import configuration from letsencrypt import errors @@ -26,7 +28,8 @@ class CLITest(unittest.TestCase): def _call(self, args): from letsencrypt import cli args = ['--text', '--config-dir', self.config_dir, - '--work-dir', self.work_dir, '--logs-dir', self.logs_dir] + args + '--work-dir', self.work_dir, '--logs-dir', self.logs_dir, + '--agree-eula'] + args with mock.patch('letsencrypt.cli.sys.stdout') as stdout: with mock.patch('letsencrypt.cli.sys.stderr') as stderr: with mock.patch('letsencrypt.cli.client') as client: @@ -96,5 +99,68 @@ class CLITest(unittest.TestCase): traceback.format_exception_only(KeyboardInterrupt, interrupt))) +class DetermineAccountTest(unittest.TestCase): + """Tests for letsencrypt.cli._determine_account.""" + + def setUp(self): + self.args = mock.MagicMock(account=None, email=None) + self.config = configuration.NamespaceConfig(self.args) + self.accs = [mock.MagicMock(id="x"), mock.MagicMock(id="y")] + self.account_storage = account.AccountMemoryStorage() + + def _call(self): + # pylint: disable=protected-access + from letsencrypt.cli import _determine_account + with mock.patch("letsencrypt.cli.account.AccountFileStorage") as mock_storage: + mock_storage.return_value = self.account_storage + return _determine_account(self.args, self.config) + + def test_args_account_set(self): + self.account_storage.save(self.accs[1]) + self.args.account = self.accs[1].id + self.assertEqual((self.accs[1], None), self._call()) + self.assertEqual(self.accs[1].id, self.args.account) + self.assertTrue(self.args.email is None) + + def test_single_account(self): + self.account_storage.save(self.accs[0]) + self.assertEqual((self.accs[0], None), self._call()) + self.assertEqual(self.accs[0].id, self.args.account) + self.assertTrue(self.args.email is None) + + @mock.patch("letsencrypt.client.display_ops.choose_account") + def test_multiple_accounts(self, mock_choose_accounts): + for acc in self.accs: + self.account_storage.save(acc) + mock_choose_accounts.return_value = self.accs[1] + self.assertEqual((self.accs[1], None), self._call()) + self.assertEqual( + set(mock_choose_accounts.call_args[0][0]), set(self.accs)) + self.assertEqual(self.accs[1].id, self.args.account) + self.assertTrue(self.args.email is None) + + @mock.patch("letsencrypt.client.display_ops.get_email") + def test_no_accounts_no_email(self, mock_get_email): + mock_get_email.return_value = "foo@bar.baz" + + with mock.patch("letsencrypt.cli.client") as client: + client.register.return_value = ( + self.accs[0], mock.sentinel.acme) + self.assertEqual((self.accs[0], mock.sentinel.acme), self._call()) + client.register.assert_called_once_with( + self.config, self.account_storage, tos_cb=mock.ANY) + + self.assertEqual(self.accs[0].id, self.args.account) + self.assertEqual("foo@bar.baz", self.args.email) + + def test_no_accounts_email(self): + self.args.email = "other email" + with mock.patch("letsencrypt.cli.client") as client: + client.register.return_value = (self.accs[1], mock.sentinel.acme) + self._call() + self.assertEqual(self.accs[1].id, self.args.account) + self.assertEqual("other email", self.args.email) + + if __name__ == '__main__': unittest.main() # pragma: no cover diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index ea9b248d4..8f9eb1692 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -2,8 +2,6 @@ import os import unittest import pkg_resources -import shutil -import tempfile import configobj import OpenSSL @@ -13,6 +11,7 @@ from acme import jose from letsencrypt import account from letsencrypt import configuration +from letsencrypt import errors from letsencrypt import le_util @@ -22,6 +21,38 @@ CSR_SAN = pkg_resources.resource_string( __name__, os.path.join("testdata", "csr-san.der")) +class RegisterTest(unittest.TestCase): + """Tests for letsencrypt.client.register.""" + + def setUp(self): + self.config = mock.MagicMock(rsa_key_size=1024) + self.account_storage = account.AccountMemoryStorage() + self.tos_cb = mock.MagicMock() + + def _call(self): + from letsencrypt.client import register + return register(self.config, self.account_storage, self.tos_cb) + + def test_no_tos(self): + with mock.patch("letsencrypt.client.acme_client.Client") as mock_client: + mock_client.register().terms_of_service = "http://tos" + with mock.patch("letsencrypt.account.report_new_account"): + self.tos_cb.return_value = False + self.assertRaises(errors.Error, self._call) + + self.tos_cb.return_value = True + self._call() + + self.tos_cb = None + self._call() + + def test_it(self): + with mock.patch("letsencrypt.client.acme_client.Client"): + with mock.patch("letsencrypt.account." + "report_new_account"): + self._call() + + class ClientTest(unittest.TestCase): """Tests for letsencrypt.client.Client.""" @@ -32,29 +63,29 @@ class ClientTest(unittest.TestCase): self.account = mock.MagicMock(**{"key.pem": KEY}) from letsencrypt.client import Client - with mock.patch("letsencrypt.client.network.Network") as network: + with mock.patch("letsencrypt.client.acme_client.Client") as acme: self.client = Client( config=self.config, account_=self.account, dv_auth=None, installer=None) - self.network = network + self.acme = acme - def test_init_network_verify_ssl(self): - self.network.assert_called_once_with( - mock.ANY, mock.ANY, verify_ssl=True) + def test_init_acme_verify_ssl(self): + self.acme.assert_called_once_with( + new_reg_uri=mock.ANY, key=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 + self.acme().request_issuance.return_value = mock.sentinel.certr + self.acme().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( + self.acme.request_issuance.assert_callend_once_with( jose.ComparableX509(OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_ASN1, CSR_SAN)), self.client.auth_handler.get_authorizations()) - self.network().fetch_chain.assert_called_once_with(mock.sentinel.certr) + self.acme().fetch_chain.assert_called_once_with(mock.sentinel.certr) def test_obtain_certificate_from_csr(self): self._mock_obtain_certificate() @@ -83,18 +114,6 @@ class ClientTest(unittest.TestCase): 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 - self.account.recovery_token = "ECCENTRIC INVISIBILITY RHINOCEROS" - self.account.email = "rhino@jungle.io" - - self.client._report_new_account() - call_list = mock_zope().add_message.call_args_list - self.assertTrue(self.config.config_dir in call_list[0][0][0]) - self.assertTrue(self.account.recovery_token in call_list[1][0][0]) - self.assertTrue(self.account.email in call_list[1][0][0]) - @mock.patch("letsencrypt.client.zope.component.getUtility") def test_report_renewal_status(self, mock_zope): # pylint: disable=protected-access @@ -128,50 +147,6 @@ class ClientTest(unittest.TestCase): self.assertTrue(cert.cli_config.renewal_configs_dir in msg) -class DetermineAccountTest(unittest.TestCase): - """Tests for letsencrypt.client.determine_authenticator.""" - - def setUp(self): - self.accounts_dir = tempfile.mkdtemp("accounts") - account_keys_dir = os.path.join(self.accounts_dir, "keys") - os.makedirs(account_keys_dir, 0o700) - - self.config = mock.MagicMock( - spec=configuration.NamespaceConfig, accounts_dir=self.accounts_dir, - account_keys_dir=account_keys_dir, rsa_key_size=2048, - server="letsencrypt-demo.org") - - def tearDown(self): - shutil.rmtree(self.accounts_dir) - - @mock.patch("letsencrypt.account.Account.from_prompts") - @mock.patch("letsencrypt.client.display_ops.choose_account") - def test_determine_account(self, mock_op, mock_prompt): - """Test determine account""" - from letsencrypt import client - - key = le_util.Key(tempfile.mkstemp()[1], "pem") - test_acc = account.Account(self.config, key, "email1@gmail.com") - mock_op.return_value = test_acc - - # Test 0 - mock_prompt.return_value = None - self.assertTrue(client.determine_account(self.config) is None) - - # Test 1 - test_acc.save() - acc = client.determine_account(self.config) - self.assertEqual(acc.email, test_acc.email) - - # Test multiple - self.assertFalse(mock_op.called) - acc2 = account.Account(self.config, key) - acc2.save() - chosen_acc = client.determine_account(self.config) - self.assertTrue(mock_op.called) - self.assertTrue(chosen_acc.email, test_acc.email) - - class RollbackTest(unittest.TestCase): """Tests for letsencrypt.client.rollback.""" diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index faf7021be..82e82d520 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -31,7 +31,6 @@ class NamespaceConfigTest(unittest.TestCase): @mock.patch('letsencrypt.configuration.constants') def test_dynamic_dirs(self, constants): constants.ACCOUNTS_DIR = 'acc' - constants.ACCOUNT_KEYS_DIR = 'keys' constants.BACKUP_DIR = 'backups' constants.CERT_KEY_BACKUP_DIR = 'c/' constants.CERT_DIR = 'certs' @@ -42,9 +41,6 @@ class NamespaceConfigTest(unittest.TestCase): self.assertEqual( self.config.accounts_dir, '/tmp/config/acc/acme-server.org:443/new') - self.assertEqual( - self.config.account_keys_dir, - '/tmp/config/acc/acme-server.org:443/new/keys') self.assertEqual(self.config.backup_dir, '/tmp/foo/backups') self.assertEqual(self.config.cert_dir, '/tmp/config/certs') self.assertEqual( diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index 25be6bebc..aba711150 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -7,9 +7,10 @@ import unittest import mock import zope.component +from acme import messages + from letsencrypt import account from letsencrypt import interfaces -from letsencrypt import le_util from letsencrypt.display import util as display_util @@ -140,8 +141,40 @@ class ConveniencePickPluginTest(unittest.TestCase): interfaces.IAuthenticator, interfaces.IInstaller)) +class GetEmailTest(unittest.TestCase): + """Tests for letsencrypt.display.ops.get_email.""" + + def setUp(self): + mock_display = mock.MagicMock() + self.input = mock_display.input + zope.component.provideUtility(mock_display, interfaces.IDisplay) + + @classmethod + def _call(cls): + from letsencrypt.display.ops import get_email + return get_email() + + def test_cancel_none(self): + self.input.return_value = (display_util.CANCEL, "foo@bar.baz") + self.assertTrue(self._call() is None) + + def test_ok_safe(self): + self.input.return_value = (display_util.OK, "foo@bar.baz") + with mock.patch("letsencrypt.display.ops.le_util" + ".safe_email") as mock_safe_email: + mock_safe_email.return_value = True + self.assertTrue(self._call() is "foo@bar.baz") + + def test_ok_not_safe(self): + self.input.return_value = (display_util.OK, "foo@bar.baz") + with mock.patch("letsencrypt.display.ops.le_util" + ".safe_email") as mock_safe_email: + mock_safe_email.side_effect = [False, True] + self.assertTrue(self._call() is "foo@bar.baz") + + class ChooseAccountTest(unittest.TestCase): - """Test choose_account.""" + """Tests for letsencrypt.display.ops.choose_account.""" def setUp(self): zope.component.provideUtility(display_util.FileDisplay(sys.stdout)) @@ -153,13 +186,14 @@ class ChooseAccountTest(unittest.TestCase): accounts_dir=self.accounts_dir, account_keys_dir=self.account_keys_dir, server="letsencrypt-demo.org") - self.key = le_util.Key("keypath", "pem") + self.key = mock.MagicMock() - self.acc1 = account.Account(self.config, self.key, "email1@g.com") - self.acc2 = account.Account( - self.config, self.key, "email2@g.com", "phone") - self.acc1.save() - self.acc2.save() + self.acc1 = account.Account(messages.RegistrationResource( + uri=None, new_authzr_uri=None, body=messages.Registration.from_data( + email="email1@g.com")), self.key) + self.acc2 = account.Account(messages.RegistrationResource( + uri=None, new_authzr_uri=None, body=messages.Registration.from_data( + email="email2@g.com", phone="phone")), self.key) @classmethod def _call(cls, accounts): diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index 7ce619d95..5a6364e81 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -165,5 +165,32 @@ class UniqueLineageNameTest(unittest.TestCase): mock_fdopen.side_effect = err self.assertRaises(OSError, self._call, "wow") + +class SafeEmailTest(unittest.TestCase): + """Test safe_email.""" + @classmethod + def _call(cls, addr): + from letsencrypt.le_util import safe_email + return safe_email(addr) + + def test_valid_emails(self): + addrs = [ + "letsencrypt@letsencrypt.org", + "tbd.ade@gmail.com", + "abc_def.jdk@hotmail.museum", + ] + for addr in addrs: + self.assertTrue(self._call(addr), "%s failed." % addr) + + def test_invalid_emails(self): + addrs = [ + "letsencrypt@letsencrypt..org", + ".tbd.ade@gmail.com", + "~/abc_def.jdk@hotmail.museum", + ] + for addr in addrs: + self.assertFalse(self._call(addr), "%s failed." % addr) + + if __name__ == '__main__': unittest.main() # pragma: no cover diff --git a/letsencrypt/tests/network_test.py b/letsencrypt/tests/network_test.py deleted file mode 100644 index aed681871..000000000 --- a/letsencrypt/tests/network_test.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Tests for letsencrypt.network.""" -import shutil -import tempfile -import unittest - -import mock - -from acme import messages -from letsencrypt import account - - -class NetworkTest(unittest.TestCase): - """Tests for letsencrypt.network.Network.""" - - def setUp(self): - from letsencrypt.network import Network - self.net = Network( - new_reg_uri=None, key=None, alg=None, verify_ssl=None) - - self.config = mock.Mock(accounts_dir=tempfile.mkdtemp()) - - def tearDown(self): - shutil.rmtree(self.config.accounts_dir) - - def test_register_from_account(self): - self.net.register = mock.Mock() - acc = account.Account( - self.config, 'key', email='cert-admin@example.com', - phone='+12025551212') - - self.net.register_from_account(acc) - - self.net.register.assert_called_once() - self.assertEqual( - set(self.net.register.mock_calls[0][1][0].contact), - set(('mailto:cert-admin@example.com', 'tel:+12025551212'))) - - def test_register_from_account_partial_info(self): - self.net.register = mock.Mock() - acc = account.Account( - self.config, 'key', email='cert-admin@example.com') - acc2 = account.Account(self.config, 'key') - - self.net.register_from_account(acc) - self.net.register.assert_called_with(messages.Registration( - contact=('mailto:cert-admin@example.com',))) - - self.net.register_from_account(acc2) - self.net.register.assert_called_with(messages.Registration()) - - -if __name__ == '__main__': - unittest.main() # pragma: no cover diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index b2063efd6..3596201b3 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -556,9 +556,9 @@ class RenewableCertTests(unittest.TestCase): datetime.timedelta(intended[time])) @mock.patch("letsencrypt.renewer.plugins_disco") - @mock.patch("letsencrypt.client.determine_account") + @mock.patch("letsencrypt.account.AccountFileStorage") @mock.patch("letsencrypt.client.Client") - def test_renew(self, mock_c, mock_da, mock_pd): + def test_renew(self, mock_c, mock_acc_storage, mock_pd): from letsencrypt import renewer test_cert = pkg_resources.resource_string( @@ -580,6 +580,7 @@ class RenewableCertTests(unittest.TestCase): self.test_rc.configfile["renewalparams"]["server"] = "acme.example.com" self.test_rc.configfile["renewalparams"]["authenticator"] = "fake" self.test_rc.configfile["renewalparams"]["dvsni_port"] = "4430" + self.test_rc.configfile["renewalparams"]["account"] = "abcde" mock_auth = mock.MagicMock() mock_pd.PluginsRegistry.find_all.return_value = {"apache": mock_auth} # Fails because "fake" != "apache" @@ -594,7 +595,7 @@ class RenewableCertTests(unittest.TestCase): 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_acc_storage().load.assert_called_once_with(account_id="abcde") 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 diff --git a/letsencrypt/tests/revoker_test.py b/letsencrypt/tests/revoker_test.py index 24336d023..f90da2168 100644 --- a/letsencrypt/tests/revoker_test.py +++ b/letsencrypt/tests/revoker_test.py @@ -69,9 +69,9 @@ class RevokerTest(RevokerBase): def tearDown(self): shutil.rmtree(self.backup_dir) - @mock.patch("letsencrypt.network.Network.revoke") + @mock.patch("acme.client.Client.revoke") @mock.patch("letsencrypt.revoker.revocation") - def test_revoke_by_key_all(self, mock_display, mock_net): + def test_revoke_by_key_all(self, mock_display, mock_acme): mock_display().confirm_revocation.return_value = True self.revoker.revoke_from_key(self.key) @@ -81,7 +81,7 @@ class RevokerTest(RevokerBase): for i in xrange(2): self.assertFalse(self._backups_exist(self.certs[i].get_row())) - self.assertEqual(mock_net.call_count, 2) + self.assertEqual(mock_acme.call_count, 2) @mock.patch("letsencrypt.revoker.OpenSSL.crypto.load_privatekey") def test_revoke_by_invalid_keys(self, mock_load_privatekey): @@ -93,9 +93,9 @@ class RevokerTest(RevokerBase): self.assertRaises( errors.RevokerError, self.revoker.revoke_from_key, self.key) - @mock.patch("letsencrypt.network.Network.revoke") + @mock.patch("acme.client.Client.revoke") @mock.patch("letsencrypt.revoker.revocation") - def test_revoke_by_wrong_key(self, mock_display, mock_net): + def test_revoke_by_wrong_key(self, mock_display, mock_acme): mock_display().confirm_revocation.return_value = True key_path = pkg_resources.resource_filename( @@ -107,11 +107,11 @@ class RevokerTest(RevokerBase): # Nothing was removed self.assertEqual(len(self._get_rows()), 2) # No revocation went through - self.assertEqual(mock_net.call_count, 0) + self.assertEqual(mock_acme.call_count, 0) - @mock.patch("letsencrypt.network.Network.revoke") + @mock.patch("acme.client.Client.revoke") @mock.patch("letsencrypt.revoker.revocation") - def test_revoke_by_cert(self, mock_display, mock_net): + def test_revoke_by_cert(self, mock_display, mock_acme): mock_display().confirm_revocation.return_value = True self.revoker.revoke_from_cert(self.paths[1]) @@ -124,11 +124,11 @@ class RevokerTest(RevokerBase): self.assertTrue(self._backups_exist(row0)) self.assertFalse(self._backups_exist(row1)) - self.assertEqual(mock_net.call_count, 1) + self.assertEqual(mock_acme.call_count, 1) - @mock.patch("letsencrypt.network.Network.revoke") + @mock.patch("acme.client.Client.revoke") @mock.patch("letsencrypt.revoker.revocation") - def test_revoke_by_cert_not_found(self, mock_display, mock_net): + def test_revoke_by_cert_not_found(self, mock_display, mock_acme): mock_display().confirm_revocation.return_value = True self.revoker.revoke_from_cert(self.paths[0]) @@ -143,11 +143,11 @@ class RevokerTest(RevokerBase): self.assertTrue(self._backups_exist(row1)) self.assertFalse(self._backups_exist(row0)) - self.assertEqual(mock_net.call_count, 1) + self.assertEqual(mock_acme.call_count, 1) - @mock.patch("letsencrypt.network.Network.revoke") + @mock.patch("acme.client.Client.revoke") @mock.patch("letsencrypt.revoker.revocation") - def test_revoke_by_menu(self, mock_display, mock_net): + def test_revoke_by_menu(self, mock_display, mock_acme): mock_display().confirm_revocation.return_value = True mock_display.display_certs.side_effect = [ (display_util.HELP, 0), @@ -165,13 +165,13 @@ class RevokerTest(RevokerBase): self.assertFalse(self._backups_exist(row0)) self.assertTrue(self._backups_exist(row1)) - self.assertEqual(mock_net.call_count, 1) + self.assertEqual(mock_acme.call_count, 1) self.assertEqual(mock_display.more_info_cert.call_count, 1) @mock.patch("letsencrypt.revoker.logger") - @mock.patch("letsencrypt.network.Network.revoke") + @mock.patch("acme.client.Client.revoke") @mock.patch("letsencrypt.revoker.revocation") - def test_revoke_by_menu_delete_all(self, mock_display, mock_net, mock_log): + def test_revoke_by_menu_delete_all(self, mock_display, mock_acme, mock_log): mock_display().confirm_revocation.return_value = True mock_display.display_certs.return_value = (display_util.OK, 0) @@ -183,7 +183,7 @@ class RevokerTest(RevokerBase): for i in xrange(2): self.assertFalse(self._backups_exist(self.certs[i].get_row())) - self.assertEqual(mock_net.call_count, 2) + self.assertEqual(mock_acme.call_count, 2) # Info is called when there aren't any certs left... self.assertTrue(mock_log.info.called) diff --git a/letsencrypt_apache/dvsni.py b/letsencrypt_apache/dvsni.py index 2542b242f..e79fc847c 100644 --- a/letsencrypt_apache/dvsni.py +++ b/letsencrypt_apache/dvsni.py @@ -142,5 +142,6 @@ class ApacheDvsni(common.Dvsni): return self.VHOST_TEMPLATE.format( vhost=ips, server_name=achall.nonce_domain, ssl_options_conf_path=self.configurator.parser.loc["ssl_options"], - cert_path=self.get_cert_file(achall), key_path=achall.key.file, + cert_path=self.get_cert_file(achall), + key_path=self.get_key_path(achall), document_root=document_root).replace("\n", os.linesep) diff --git a/letsencrypt_nginx/dvsni.py b/letsencrypt_nginx/dvsni.py index 9e79a90c2..637f5a7c7 100644 --- a/letsencrypt_nginx/dvsni.py +++ b/letsencrypt_nginx/dvsni.py @@ -141,7 +141,7 @@ class NginxDvsni(common.Dvsni): ['error_log', os.path.join( self.configurator.config.work_dir, 'error.log')], ['ssl_certificate', self.get_cert_file(achall)], - ['ssl_certificate_key', achall.key.file], + ['ssl_certificate_key', self.get_key_path(achall)], [['location', '/'], [['root', document_root]]]]) return [['server'], block] diff --git a/tests/integration/_common.sh b/tests/integration/_common.sh index 0f26d3815..60e7d60ac 100755 --- a/tests/integration/_common.sh +++ b/tests/integration/_common.sh @@ -18,6 +18,7 @@ letsencrypt_test () { $store_flags \ --text \ --agree-eula \ + --agree-tos \ --email "" \ --debug \ -vvvvvvv \ From 1bc9e7cb6471d27fd2b238fcca221d5f3fb361de Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 9 Jul 2015 06:53:06 +0000 Subject: [PATCH 04/30] Registration: drop singular email/phone --- acme/messages.py | 24 ------------------------ acme/messages_test.py | 12 ------------ letsencrypt/account.py | 4 ++-- letsencrypt/tests/account_test.py | 3 ++- 4 files changed, 4 insertions(+), 39 deletions(-) diff --git a/acme/messages.py b/acme/messages.py index 4d82a7723..0749b5f49 100644 --- a/acme/messages.py +++ b/acme/messages.py @@ -180,30 +180,6 @@ class Registration(ResourceBody): """All emails found in the ``contact`` field.""" return self._filter_contact(self.email_prefix) - @property - def phone(self): - """Phone. - - Picks any phone from `phones` or ``None`` if not available. - - """ - try: - return self.phones[0] - except IndexError: - return None - - @property - def email(self): - """Email. - - Picks any email from `emails` or ``None`` if not available. - - """ - try: - return self.emails[0] - except IndexError: - return None - class RegistrationResource(ResourceWithURI): """Registration Resource. diff --git a/acme/messages_test.py b/acme/messages_test.py index 4f99b538d..d028a59c5 100644 --- a/acme/messages_test.py +++ b/acme/messages_test.py @@ -147,18 +147,6 @@ class RegistrationTest(unittest.TestCase): def test_emails(self): self.assertEqual(('admin@foo.com',), self.reg.emails) - def test_phone(self): - self.assertEqual('1234', self.reg.phone) - - def test_phone_none(self): - self.assertTrue(self.reg_none.phone is None) - - def test_email(self): - self.assertEqual('admin@foo.com', self.reg.email) - - def test_email_none(self): - self.assertTrue(self.reg_none.email is None) - def test_to_partial_json(self): self.assertEqual(self.jobj_to, self.reg.to_partial_json()) diff --git a/letsencrypt/account.py b/letsencrypt/account.py index 2f8eaab27..4289a190e 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -98,9 +98,9 @@ def report_new_account(acc, config): "them using the token \"{0}\". You must write that down " "and put it in a safe place.".format( acc.regr.body.recovery_token)) - if acc.regr.body.email is not None: + if acc.regr.body.emails: recovery_msg += (" Another recovery method will be e-mails sent to " - "{0}.".format(acc.regr.body.email)) + "{0}.".format(", ".join(acc.regr.body.emails))) reporter.add_message(recovery_msg, reporter.HIGH_PRIORITY, True) diff --git a/letsencrypt/tests/account_test.py b/letsencrypt/tests/account_test.py index 174d3614c..8c2464d34 100644 --- a/letsencrypt/tests/account_test.py +++ b/letsencrypt/tests/account_test.py @@ -81,7 +81,8 @@ class ReportNewAccountTest(unittest.TestCase): call_list = mock_zope().add_message.call_args_list self.assertTrue(self.config.config_dir in call_list[0][0][0]) self.assertTrue(self.acc.regr.body.recovery_token in call_list[1][0][0]) - self.assertTrue(self.acc.regr.body.email in call_list[1][0][0]) + self.assertTrue( + ", ".join(self.acc.regr.body.emails) in call_list[1][0][0]) class AccountMemoryStorageTest(unittest.TestCase): From 3e2d1c8abc3af99d8e33a50b90c1e0f9282a1d65 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 9 Jul 2015 08:19:54 +0000 Subject: [PATCH 05/30] get_cert_file -> get_cert_path. --- letsencrypt/plugins/common.py | 4 ++-- letsencrypt/plugins/common_test.py | 2 +- letsencrypt_apache/dvsni.py | 2 +- letsencrypt_nginx/dvsni.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 1d77636aa..cd148fa5e 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -145,7 +145,7 @@ class Dvsni(object): if idx is not None: self.indices.append(idx) - def get_cert_file(self, achall): + def get_cert_path(self, achall): """Returns standardized name for challenge certificate. :param achall: Annotated DVSNI challenge. @@ -166,7 +166,7 @@ class Dvsni(object): def _setup_challenge_cert(self, achall, s=None): # pylint: disable=invalid-name """Generate and write out challenge certificate.""" - cert_path = self.get_cert_file(achall) + cert_path = self.get_cert_path(achall) # Register the path before you write out the file self.configurator.reverter.register_file_creation(True, cert_path) diff --git a/letsencrypt/plugins/common_test.py b/letsencrypt/plugins/common_test.py index a692efc9c..340cfcf0d 100644 --- a/letsencrypt/plugins/common_test.py +++ b/letsencrypt/plugins/common_test.py @@ -164,7 +164,7 @@ class DvsniTest(unittest.TestCase): self.assertTrue(m_open.called) self.assertEqual( - m_open.call_args[0], (self.sni.get_cert_file(achall), "w")) + m_open.call_args[0], (self.sni.get_cert_path(achall), "w")) self.assertEqual(m_open().write.mock_calls[0][1][0], "pem") self.assertEqual(m_open().write.mock_calls[1][1][0], achall.key.key.private_bytes()) diff --git a/letsencrypt_apache/dvsni.py b/letsencrypt_apache/dvsni.py index e79fc847c..9aaaf85a6 100644 --- a/letsencrypt_apache/dvsni.py +++ b/letsencrypt_apache/dvsni.py @@ -142,6 +142,6 @@ class ApacheDvsni(common.Dvsni): return self.VHOST_TEMPLATE.format( vhost=ips, server_name=achall.nonce_domain, ssl_options_conf_path=self.configurator.parser.loc["ssl_options"], - cert_path=self.get_cert_file(achall), + cert_path=self.get_cert_path(achall), key_path=self.get_key_path(achall), document_root=document_root).replace("\n", os.linesep) diff --git a/letsencrypt_nginx/dvsni.py b/letsencrypt_nginx/dvsni.py index 637f5a7c7..bdf8d4487 100644 --- a/letsencrypt_nginx/dvsni.py +++ b/letsencrypt_nginx/dvsni.py @@ -140,7 +140,7 @@ class NginxDvsni(common.Dvsni): self.configurator.config.work_dir, 'access.log')], ['error_log', os.path.join( self.configurator.config.work_dir, 'error.log')], - ['ssl_certificate', self.get_cert_file(achall)], + ['ssl_certificate', self.get_cert_path(achall)], ['ssl_certificate_key', self.get_key_path(achall)], [['location', '/'], [['root', document_root]]]]) From 0d24f52f6ebfa627d0558dce8b9f45c42463b126 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 9 Jul 2015 10:55:51 +0000 Subject: [PATCH 06/30] Expose le_util.safe_open. --- letsencrypt/le_util.py | 28 +++++++++++++++++++++------- letsencrypt/tests/le_util_test.py | 26 +++++++++++++------------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/letsencrypt/le_util.py b/letsencrypt/le_util.py index 583754ab5..e525a333c 100644 --- a/letsencrypt/le_util.py +++ b/letsencrypt/le_util.py @@ -58,16 +58,30 @@ def check_permissions(filepath, mode, uid=0): return stat.S_IMODE(file_stat.st_mode) == mode and file_stat.st_uid == uid -def _safely_attempt_open(fname, mode): - file_d = os.open(fname, os.O_CREAT | os.O_EXCL | os.O_RDWR, mode) - return os.fdopen(file_d, "w"), fname +def safe_open(path, mode="w", chmod=None, buffering=None): + """Safely open a file. + + :param str path: Path to a file. + :param str mode: Same os `mode` for `open`. + :param int chmod: Same as `mode` for `os.open`, uses Python defaults + if ``None``. + :param int buffering: Same as `bufsize` for `os.fdopen`, uses Python + defaults if ``None``. + + """ + # pylint: disable=star-args + open_args = () if chmod is None else (chmod,) + fdopen_args = () if buffering is None else (buffering,) + return os.fdopen( + os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR, *open_args), + mode, *fdopen_args) def _unique_file(path, filename_pat, count, mode): while True: + current_path = os.path.join(path, filename_pat(count)) try: - return _safely_attempt_open( - os.path.join(path, filename_pat(count)), mode) + return safe_open(current_path, chmod=mode), current_path except OSError as err: # "File exists," is okay, try a different name. if err.errno != errno.EEXIST: @@ -105,9 +119,9 @@ def unique_lineage_name(path, filename, mode=0o777): specified location. """ + preferred_path = os.path.join(path, "%s.conf" % (filename)) try: - return _safely_attempt_open( - os.path.join(path, "%s.conf" % (filename)), mode=mode) + return safe_open(preferred_path, chmod=mode), preferred_path except OSError as err: if err.errno != errno.EEXIST: raise diff --git a/letsencrypt/tests/le_util_test.py b/letsencrypt/tests/le_util_test.py index 5a6364e81..1ecc1ea16 100644 --- a/letsencrypt/tests/le_util_test.py +++ b/letsencrypt/tests/le_util_test.py @@ -21,7 +21,7 @@ class MakeOrVerifyDirTest(unittest.TestCase): def setUp(self): self.root_path = tempfile.mkdtemp() - self.path = os.path.join(self.root_path, 'foo') + self.path = os.path.join(self.root_path, "foo") os.mkdir(self.path, 0o400) self.uid = os.getuid() @@ -34,7 +34,7 @@ class MakeOrVerifyDirTest(unittest.TestCase): return make_or_verify_dir(directory, mode, self.uid) def test_creates_dir_when_missing(self): - path = os.path.join(self.root_path, 'bar') + path = os.path.join(self.root_path, "bar") self._call(path, 0o650) self.assertTrue(os.path.isdir(path)) self.assertEqual(stat.S_IMODE(os.stat(path).st_mode), 0o650) @@ -47,9 +47,9 @@ class MakeOrVerifyDirTest(unittest.TestCase): self.assertRaises(errors.Error, self._call, self.path, 0o600) def test_reraises_os_error(self): - with mock.patch.object(os, 'makedirs') as makedirs: + with mock.patch.object(os, "makedirs") as makedirs: makedirs.side_effect = OSError() - self.assertRaises(OSError, self._call, 'bar', 12312312) + self.assertRaises(OSError, self._call, "bar", 12312312) class CheckPermissionsTest(unittest.TestCase): @@ -85,7 +85,7 @@ class UniqueFileTest(unittest.TestCase): def setUp(self): self.root_path = tempfile.mkdtemp() - self.default_name = os.path.join(self.root_path, 'foo.txt') + self.default_name = os.path.join(self.root_path, "foo.txt") def tearDown(self): shutil.rmtree(self.root_path, ignore_errors=True) @@ -96,9 +96,9 @@ class UniqueFileTest(unittest.TestCase): def test_returns_fd_for_writing(self): fd, name = self._call() - fd.write('bar') + fd.write("bar") fd.close() - self.assertEqual(open(name).read(), 'bar') + self.assertEqual(open(name).read(), "bar") def test_right_mode(self): self.assertEqual(0o700, os.stat(self._call(0o700)[1]).st_mode & 0o777) @@ -118,11 +118,11 @@ class UniqueFileTest(unittest.TestCase): self.assertEqual(os.path.dirname(name3), self.root_path) basename1 = os.path.basename(name2) - self.assertTrue(basename1.endswith('foo.txt')) + self.assertTrue(basename1.endswith("foo.txt")) basename2 = os.path.basename(name2) - self.assertTrue(basename2.endswith('foo.txt')) + self.assertTrue(basename2.endswith("foo.txt")) basename3 = os.path.basename(name3) - self.assertTrue(basename3.endswith('foo.txt')) + self.assertTrue(basename3.endswith("foo.txt")) class UniqueLineageNameTest(unittest.TestCase): @@ -139,9 +139,9 @@ class UniqueLineageNameTest(unittest.TestCase): return unique_lineage_name(self.root_path, filename, mode) def test_basic(self): - f, name = self._call("wow") + f, path = self._call("wow") self.assertTrue(isinstance(f, file)) - self.assertTrue(isinstance(name, str)) + self.assertEqual(os.path.join(self.root_path, "wow.conf"), path) def test_multiple(self): for _ in xrange(10): @@ -192,5 +192,5 @@ class SafeEmailTest(unittest.TestCase): self.assertFalse(self._call(addr), "%s failed." % addr) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() # pragma: no cover From 7aa749174bd404e6efde358d09e84594048c769b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 9 Jul 2015 11:17:22 +0000 Subject: [PATCH 07/30] Fix achall response key chmods security bug. --- letsencrypt/plugins/common.py | 6 +++--- letsencrypt/plugins/common_test.py | 26 +++++++++++++++----------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index cd148fa5e..460af1b15 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -11,6 +11,7 @@ from acme.jose import util as jose_util from letsencrypt import constants from letsencrypt import interfaces +from letsencrypt import le_util def option_namespace(name): @@ -173,7 +174,7 @@ class Dvsni(object): cert_pem, response = achall.gen_cert_and_response(s) # Write out challenge cert - with open(cert_path, "w") as cert_chall_fd: + with open(cert_path, "wb") as cert_chall_fd: cert_chall_fd.write(cert_pem) key_path = self.get_key_path(achall) @@ -181,8 +182,7 @@ class Dvsni(object): encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()) - # XXX: key_file chmods! (SEC) - with open(key_path, 'w') as key_file: + with le_util.safe_open(key_path, 'wb', chmod=0o400) as key_file: key_file.write(key_pem) self.configurator.reverter.register_file_creation(True, key_path) diff --git a/letsencrypt/plugins/common_test.py b/letsencrypt/plugins/common_test.py index 340cfcf0d..8688c36b1 100644 --- a/letsencrypt/plugins/common_test.py +++ b/letsencrypt/plugins/common_test.py @@ -150,24 +150,28 @@ class DvsniTest(unittest.TestCase): # open context managers more elegantly. It avoids dealing with # __enter__ and __exit__ calls. # http://www.voidspace.org.uk/python/mock/helpers.html#mock.mock_open - m_open = mock.mock_open() + mock_open, mock_safe_open = mock.mock_open(), mock.mock_open() response = challenges.DVSNIResponse(s="randomS1") achall = mock.MagicMock(nonce=self.achalls[0].nonce, nonce_domain=self.achalls[0].nonce_domain) achall.gen_cert_and_response.return_value = ("pem", response) - with mock.patch("letsencrypt.plugins.common.open", m_open, create=True): - # pylint: disable=protected-access - self.assertEqual(response, self.sni._setup_challenge_cert( - achall, "randomS1")) + with mock.patch("letsencrypt.plugins.common.open", + mock_open, create=True): + with mock.patch("letsencrypt.plugins.common.le_util.safe_open", + mock_safe_open): + # pylint: disable=protected-access + self.assertEqual(response, self.sni._setup_challenge_cert( + achall, "randomS1")) - self.assertTrue(m_open.called) - self.assertEqual( - m_open.call_args[0], (self.sni.get_cert_path(achall), "w")) - self.assertEqual(m_open().write.mock_calls[0][1][0], "pem") - self.assertEqual(m_open().write.mock_calls[1][1][0], - achall.key.key.private_bytes()) + # pylint: disable=no-member + mock_open.assert_called_once_with(self.sni.get_cert_path(achall), "wb") + mock_open.return_value.write.assert_called_once_with("pem") + mock_safe_open.assert_called_once_with( + self.sni.get_key_path(achall), "wb", chmod=0o400) + mock_safe_open.return_value.write.assert_called_once_with( + achall.key.key.private_bytes()) if __name__ == "__main__": From 5e450e879cef21cd72a8d394d7bf0b6c84ee9a93 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 9 Jul 2015 11:33:02 +0000 Subject: [PATCH 08/30] Save account private_key.json as 0o400. --- letsencrypt/account.py | 3 ++- letsencrypt/tests/account_test.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/letsencrypt/account.py b/letsencrypt/account.py index 4289a190e..12389ebfd 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -192,7 +192,8 @@ class AccountFileStorage(interfaces.AccountStorage): try: with open(self._regr_path(account_dir_path), "w") as regr_file: regr_file.write(account.regr.json_dumps()) - with open(self._key_path(account_dir_path), "w") as key_file: + with le_util.safe_open(self._key_path(account_dir_path), + "w", chmod=0o400) as key_file: key_file.write(account.key.json_dumps()) with open(self._metadata_path(account_dir_path), "w") as metadata_file: metadata_file.write(account.meta.json_dumps()) diff --git a/letsencrypt/tests/account_test.py b/letsencrypt/tests/account_test.py index 8c2464d34..bd34edc56 100644 --- a/letsencrypt/tests/account_test.py +++ b/letsencrypt/tests/account_test.py @@ -3,6 +3,7 @@ import datetime import os import pkg_resources import shutil +import stat import tempfile import unittest @@ -130,6 +131,8 @@ class AccountFileStorageTest(unittest.TestCase): for file_name in "regr.json", "meta.json", "private_key.json": self.assertTrue(os.path.exists( os.path.join(account_path, file_name))) + self.assertEqual("0400", oct(os.stat(os.path.join( + account_path, "private_key.json"))[stat.ST_MODE] & 0o777)) # restore self.assertEqual(self.acc, self.storage.load(self.acc.id)) From 35c21d4cf41467e2f7fe2df5564384de60fd5be3 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 9 Jul 2015 13:28:57 +0000 Subject: [PATCH 09/30] Enforce "resource" field in request objects. Corresponds to: - https://github.com/letsencrypt/boulder/pull/442 - https://github.com/letsencrypt/acme-spec/pull/156 --- acme/challenges.py | 12 ++++++++++-- acme/client.py | 9 +++++++-- acme/client_test.py | 15 +++++++++++---- acme/interfaces.py | 13 +++++++++++++ acme/messages.py | 25 ++++++++++++++++++------- 5 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 acme/interfaces.py diff --git a/acme/challenges.py b/acme/challenges.py index 45db23e72..8024728fa 100644 --- a/acme/challenges.py +++ b/acme/challenges.py @@ -7,6 +7,7 @@ import os import requests +from acme import interfaces from acme import jose from acme import other @@ -31,10 +32,17 @@ class DVChallenge(Challenge): # pylint: disable=abstract-method """Domain validation challenges.""" -class ChallengeResponse(jose.TypedJSONObjectWithFields): +class ChallengeResponse(interfaces.ClientRequestableResource, + jose.TypedJSONObjectWithFields): # _fields_to_partial_json | pylint: disable=abstract-method - """ACME challenge response.""" + """ACME challenge response. + + :ivar str mitm_resource: ACME resource identifier used in client + HTTPS requests in order to protect against MITM. + + """ TYPES = {} + resource_type = 'challenge' @classmethod def from_json(cls, jobj): diff --git a/acme/client.py b/acme/client.py index 064bd8cd2..33e4e4f7f 100644 --- a/acme/client.py +++ b/acme/client.py @@ -2,6 +2,7 @@ import datetime import heapq import httplib +import json import logging import time @@ -81,6 +82,8 @@ class Client(object): # pylint: disable=too-many-instance-attributes response = self.net.post(self.new_reg_uri, new_reg) assert response.status_code == httplib.CREATED # TODO: handle errors + # "Instance of 'Field' has no key/contact member" bug: + # pylint: disable=no-member regr = self._regr_from_response(response) if (regr.body.key != self.key.public_key() or regr.body.contact != new_reg.contact): @@ -443,11 +446,13 @@ class ClientNetwork(object): .. todo:: Implement ``acmePath``. - :param JSONDeSerializable obj: + :param .ClientRequestableResource obj: :rtype: `.JWS` """ - dumps = obj.json_dumps() + jobj = obj.to_json() + jobj['resource'] = obj.resource_type + dumps = json.dumps(jobj) logger.debug('Serialized JSON: %s', dumps) return jws.JWS.sign( payload=dumps, key=self.key, alg=self.alg, nonce=nonce).json_dumps() diff --git a/acme/client_test.py b/acme/client_test.py index bbed2ed87..ec4231093 100644 --- a/acme/client_test.py +++ b/acme/client_test.py @@ -1,6 +1,7 @@ """Tests for acme.client.""" import datetime import httplib +import json import os import pkg_resources import unittest @@ -74,6 +75,8 @@ class ClientTest(unittest.TestCase): cert_chain_uri='https://www.letsencrypt-demo.org/ca') def test_register(self): + # "Instance of 'Field' has no to_json/update member" bug: + # pylint: disable=no-member self.response.status_code = httplib.CREATED self.response.json.return_value = self.regr.body.to_json() self.response.headers['Location'] = self.regr.uri @@ -97,6 +100,8 @@ class ClientTest(unittest.TestCase): errors.ClientError, self.client.register, self.regr.body) def test_update_registration(self): + # "Instance of 'Field' has no to_json/update member" bug: + # pylint: disable=no-member self.response.headers['Location'] = self.regr.uri self.response.json.return_value = self.regr.body.to_json() self.assertEqual(self.regr, self.client.update_registration(self.regr)) @@ -367,20 +372,22 @@ class ClientNetworkTest(unittest.TestCase): self.assertTrue(self.net.verify_ssl is self.verify_ssl) def test_wrap_in_jws(self): - class MockJSONDeSerializable(jose.JSONDeSerializable): + class MockClientRequestableResource(jose.JSONDeSerializable): # pylint: disable=missing-docstring + resource_type = 'mock' def __init__(self, value): self.value = value def to_partial_json(self): - return self.value + return {'foo': self.value} @classmethod def from_json(cls, value): pass # pragma: no cover # pylint: disable=protected-access jws_dump = self.net._wrap_in_jws( - MockJSONDeSerializable('foo'), nonce='Tg') + MockClientRequestableResource('foo'), nonce='Tg') jws = acme_jws.JWS.json_loads(jws_dump) - self.assertEqual(jws.payload, '"foo"') + self.assertEqual(json.loads(jws.payload), + {'foo': 'foo', 'resource': 'mock'}) self.assertEqual(jws.signature.combined.nonce, 'Tg') # TODO: check that nonce is in protected header diff --git a/acme/interfaces.py b/acme/interfaces.py new file mode 100644 index 000000000..9899b1093 --- /dev/null +++ b/acme/interfaces.py @@ -0,0 +1,13 @@ +"""ACME interfaces.""" +from acme import jose + + +class ClientRequestableResource(jose.JSONDeSerializable): + """Resource that can be requested by client. + + :ivar str resource_type: ACME resource identifier used in client + HTTPS requests in order to protect against MITM. + + """ + # pylint: disable=abstract-method + resource_type = NotImplemented diff --git a/acme/messages.py b/acme/messages.py index 4d82a7723..1be2c5632 100644 --- a/acme/messages.py +++ b/acme/messages.py @@ -3,6 +3,7 @@ import urlparse from acme import challenges from acme import fields +from acme import interfaces from acme import jose @@ -117,7 +118,6 @@ class Identifier(jose.JSONObjectWithFields): class Resource(jose.JSONObjectWithFields): """ACME Resource. - :ivar str uri: Location of the resource. :ivar acme.messages.ResourceBody body: Resource body. """ @@ -137,13 +137,15 @@ class ResourceBody(jose.JSONObjectWithFields): """ACME Resource Body.""" -class Registration(ResourceBody): +class Registration(interfaces.ClientRequestableResource, ResourceBody): """Registration Resource Body. :ivar acme.jose.jwk.JWK key: Public key. :ivar tuple contact: Contact information following ACME spec """ + resource_type = 'new-regr' + # on new-reg key server ignores 'key' and populates it based on # JWS.signature.combined.jwk key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json) @@ -205,7 +207,8 @@ class Registration(ResourceBody): return None -class RegistrationResource(ResourceWithURI): +class RegistrationResource(interfaces.ClientRequestableResource, + ResourceWithURI): """Registration Resource. :ivar acme.messages.Registration body: @@ -213,6 +216,7 @@ class RegistrationResource(ResourceWithURI): :ivar str terms_of_service: URL for the CA TOS. """ + resource_type = 'reg' body = jose.Field('body', decoder=Registration.from_json) new_authzr_uri = jose.Field('new_authzr_uri') terms_of_service = jose.Field('terms_of_service', omitempty=True) @@ -274,7 +278,7 @@ class ChallengeResource(Resource): return self.body.uri # pylint: disable=no-member -class Authorization(ResourceBody): +class Authorization(interfaces.ClientRequestableResource, ResourceBody): """Authorization Resource Body. :ivar acme.messages.Identifier identifier: @@ -287,6 +291,7 @@ class Authorization(ResourceBody): :ivar datetime.datetime expires: """ + resource_type = 'new-authz' identifier = jose.Field('identifier', decoder=Identifier.from_json) challenges = jose.Field('challenges', omitempty=True) combinations = jose.Field('combinations', omitempty=True) @@ -320,7 +325,8 @@ class AuthorizationResource(ResourceWithURI): new_cert_uri = jose.Field('new_cert_uri') -class CertificateRequest(jose.JSONObjectWithFields): +class CertificateRequest(interfaces.ClientRequestableResource, + jose.JSONObjectWithFields): """ACME new-cert request. :ivar acme.jose.util.ComparableX509 csr: @@ -328,11 +334,13 @@ class CertificateRequest(jose.JSONObjectWithFields): :ivar tuple authorizations: `tuple` of URIs (`str`) """ + resource_type = 'new-cert' csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr) authorizations = jose.Field('authorizations', decoder=tuple) -class CertificateResource(ResourceWithURI): +class CertificateResource(interfaces.ClientRequestableResource, + ResourceWithURI): """Certificate Resource. :ivar acme.jose.util.ComparableX509 body: @@ -341,17 +349,20 @@ class CertificateResource(ResourceWithURI): :ivar tuple authzrs: `tuple` of `AuthorizationResource`. """ + resource_type = 'cert' cert_chain_uri = jose.Field('cert_chain_uri') authzrs = jose.Field('authzrs') -class Revocation(jose.JSONObjectWithFields): +class Revocation(interfaces.ClientRequestableResource, + jose.JSONObjectWithFields): """Revocation message. :ivar .ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` """ + resource_type = 'revoke-cert' certificate = jose.Field( 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) From 517c9bd736be5c1e493fdef19f1d4191d53d1df4 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Thu, 9 Jul 2015 19:04:41 +0000 Subject: [PATCH 10/30] Fix new-regr -> new-reg typo --- acme/messages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/messages.py b/acme/messages.py index 1be2c5632..5d7759dd2 100644 --- a/acme/messages.py +++ b/acme/messages.py @@ -144,7 +144,7 @@ class Registration(interfaces.ClientRequestableResource, ResourceBody): :ivar tuple contact: Contact information following ACME spec """ - resource_type = 'new-regr' + resource_type = 'new-reg' # on new-reg key server ignores 'key' and populates it based on # JWS.signature.combined.jwk From 581875bde3906f9f3ac47d04ebbadea21ffc9865 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 05:14:50 +0000 Subject: [PATCH 11/30] Account.id: use key md5 hexdigest. --- letsencrypt/account.py | 33 ++++++++++++++------------- letsencrypt/tests/account_test.py | 4 ++-- letsencrypt/tests/display/ops_test.py | 8 ++++++- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/letsencrypt/account.py b/letsencrypt/account.py index 12389ebfd..b2b00a32e 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -1,9 +1,11 @@ """Creates ACME accounts for server.""" import datetime +import hashlib import logging import os import socket +from cryptography.hazmat.primitives import serialization import pyrfc3339 import pytz import zope.component @@ -20,15 +22,13 @@ from letsencrypt import le_util logger = logging.getLogger(__name__) -class Account(object): +class Account(object): # pylint: disable=too-few-public-methods """ACME protocol registration. :ivar .RegistrationResource regr: Registration Resource :ivar .JWK key: Authorized Account Key :ivar .Meta: Account metadata - - .. note:: ``creation_dt`` and ``creation_host`` are useful in - cross-machine migration scenarios. + :ivar str id: Globally unique account identifier. """ @@ -38,6 +38,9 @@ class Account(object): :ivar datetime.datetime creation_dt: Creation date and time (UTC). :ivar str creation_host: FQDN of host, where account has been created. + .. note:: ``creation_dt`` and ``creation_host`` are useful in + cross-machine migration scenarios. + """ creation_dt = acme_fields.RFC3339Field("creation_dt") creation_host = jose.Field("creation_host") @@ -51,18 +54,16 @@ class Account(object): tz=pytz.UTC).replace(microsecond=0), creation_host=socket.getfqdn()) if meta is None else meta - @property - def id(self): # pylint: disable=invalid-name - """Globally unique account identifier. - - Implementation note: Email? Multiple accounts can have the same - email address. Registration URI? Assigned by the server, not - guaranteed to be stable over time, nor cannonical URI can be - generated. One could use the account key (fingerprint), as ACME - protocol doesn't allow it to be updated... - - """ - return self.slug + self.id = hashlib.md5( # pylint: disable=invalid-name + self.key.key.public_key().public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo) + ).hexdigest() + # Implementation note: Email? Multiple accounts can have the + # same email address. Registration URI? Assigned by the + # server, not guaranteed to be stable over time, nor + # cannonical URI can be generated. ACME protocol doesn't allow + # account key (and thus its fingerprint) to be updated... @property def slug(self): diff --git a/letsencrypt/tests/account_test.py b/letsencrypt/tests/account_test.py index bd34edc56..c0a3f1848 100644 --- a/letsencrypt/tests/account_test.py +++ b/letsencrypt/tests/account_test.py @@ -45,7 +45,7 @@ class AccountTest(unittest.TestCase): def test_id(self): self.assertEqual( - self.acc.id, "test.letsencrypt.org@2015-07-04T14:04:10Z") + self.acc.id, "2ba35a3bdf380ed76a5ac9e740568395") def test_slug(self): self.assertEqual( @@ -54,7 +54,7 @@ class AccountTest(unittest.TestCase): def test_repr(self): self.assertEqual( repr(self.acc), - "") + "") class ReportNewAccountTest(unittest.TestCase): diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index aba711150..6f4b2f317 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -1,5 +1,6 @@ """Test letsencrypt.display.ops.""" import os +import pkg_resources import sys import tempfile import unittest @@ -7,6 +8,7 @@ import unittest import mock import zope.component +from acme import jose from acme import messages from letsencrypt import account @@ -15,6 +17,10 @@ from letsencrypt import interfaces from letsencrypt.display import util as display_util +KEY = jose.JWKRSA.load(pkg_resources.resource_string( + "letsencrypt.tests", os.path.join("testdata", "rsa512_key.pem"))) + + class ChoosePluginTest(unittest.TestCase): """Tests for letsencrypt.display.ops.choose_plugin.""" @@ -186,7 +192,7 @@ class ChooseAccountTest(unittest.TestCase): accounts_dir=self.accounts_dir, account_keys_dir=self.account_keys_dir, server="letsencrypt-demo.org") - self.key = mock.MagicMock() + self.key = KEY self.acc1 = account.Account(messages.RegistrationResource( uri=None, new_authzr_uri=None, body=messages.Registration.from_data( From f24479ebfc510b66c369f6979fb35686d1f45b0e Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 05:17:24 +0000 Subject: [PATCH 12/30] 100% coverage for letsencrypt.account. --- letsencrypt/tests/account_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/letsencrypt/tests/account_test.py b/letsencrypt/tests/account_test.py index c0a3f1848..f325bc7ba 100644 --- a/letsencrypt/tests/account_test.py +++ b/letsencrypt/tests/account_test.py @@ -100,6 +100,8 @@ class AccountMemoryStorageTest(unittest.TestCase): self.storage.save(account) self.assertEqual([account], self.storage.find_all()) self.assertEqual(account, self.storage.load("x")) + self.storage.save(account) + self.assertEqual([account], self.storage.find_all()) class AccountFileStorageTest(unittest.TestCase): From f4d5ce1986a3fb02e06a44defd117a455c0a3b06 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 05:27:01 +0000 Subject: [PATCH 13/30] Include Account.id in the Account.slug. --- letsencrypt/account.py | 4 ++-- letsencrypt/tests/account_test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/letsencrypt/account.py b/letsencrypt/account.py index b2b00a32e..1af9013d3 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -68,8 +68,8 @@ class Account(object): # pylint: disable=too-few-public-methods @property def slug(self): """Short account identification string, useful for UI.""" - return "{1}@{0}".format(pyrfc3339.generate( - self.meta.creation_dt), self.meta.creation_host) + return "{1}@{0} ({2})".format(pyrfc3339.generate( + self.meta.creation_dt), self.meta.creation_host, self.id[:4]) def __repr__(self): return "<{0}({1})>".format(self.__class__.__name__, self.id) diff --git a/letsencrypt/tests/account_test.py b/letsencrypt/tests/account_test.py index f325bc7ba..c3cfcf6c7 100644 --- a/letsencrypt/tests/account_test.py +++ b/letsencrypt/tests/account_test.py @@ -49,7 +49,7 @@ class AccountTest(unittest.TestCase): def test_slug(self): self.assertEqual( - self.acc.slug, "test.letsencrypt.org@2015-07-04T14:04:10Z") + self.acc.slug, "test.letsencrypt.org@2015-07-04T14:04:10Z (2ba3)") def test_repr(self): self.assertEqual( From 30a02d448792758281451e029d3b9046449515fa Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 05:32:16 +0000 Subject: [PATCH 14/30] Accounts: raise AccountStorageError on id mismatch (instead of assertion). This allows find_all() to skip broken account, instead of failing with AssertionError. --- letsencrypt/account.py | 7 ++++--- letsencrypt/tests/account_test.py | 7 +++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/letsencrypt/account.py b/letsencrypt/account.py index 1af9013d3..e962c993d 100644 --- a/letsencrypt/account.py +++ b/letsencrypt/account.py @@ -182,9 +182,10 @@ class AccountFileStorage(interfaces.AccountStorage): raise errors.AccountStorageError(error) acc = Account(regr, key, meta) - assert acc.id == account_id, ( - "Account ids mismatch (expected: {0}, found: {1}".format( - account_id, acc.id)) + if acc.id != account_id: + raise errors.AccountStorageError( + "Account ids mismatch (expected: {0}, found: {1}".format( + account_id, acc.id)) return acc def save(self, account): diff --git a/letsencrypt/tests/account_test.py b/letsencrypt/tests/account_test.py index c3cfcf6c7..93da225af 100644 --- a/letsencrypt/tests/account_test.py +++ b/letsencrypt/tests/account_test.py @@ -160,6 +160,13 @@ class AccountFileStorageTest(unittest.TestCase): def test_load_non_existent_raises_error(self): self.assertRaises(errors.AccountNotFound, self.storage.load, "missing") + def test_load_id_mismatch_raises_error(self): + self.storage.save(self.acc) + shutil.move(os.path.join(self.config.accounts_dir, self.acc.id), + os.path.join(self.config.accounts_dir, "x" + self.acc.id)) + self.assertRaises(errors.AccountStorageError, self.storage.load, + "x" + self.acc.id) + def test_load_ioerror(self): self.storage.save(self.acc) mock_open = mock.mock_open() From 0c46f80fdd3c4ffaf77c0099a8957d6f219c6c30 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 05:44:21 +0000 Subject: [PATCH 15/30] assert_callend_once -> assert_called_once --- letsencrypt/tests/client_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 8f9eb1692..1036987a8 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -81,7 +81,7 @@ class ClientTest(unittest.TestCase): def _check_obtain_certificate(self): self.client.auth_handler.get_authorizations.assert_called_once_with( ["example.com", "www.example.com"]) - self.acme.request_issuance.assert_callend_once_with( + self.acme.request_issuance.assert_called_once_with( jose.ComparableX509(OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_ASN1, CSR_SAN)), self.client.auth_handler.get_authorizations()) From 56d8c60df69b20c0749ee4b67c41777ed351575f Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 06:24:06 +0000 Subject: [PATCH 16/30] Fix letsencrypt.tests.client_test. --- letsencrypt/tests/client_test.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 1036987a8..2d1f1d2fd 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -64,19 +64,20 @@ class ClientTest(unittest.TestCase): from letsencrypt.client import Client with mock.patch("letsencrypt.client.acme_client.Client") as acme: + self.acme_client = acme + self.acme = acme.return_value = mock.MagicMock() self.client = Client( config=self.config, account_=self.account, dv_auth=None, installer=None) - self.acme = acme def test_init_acme_verify_ssl(self): - self.acme.assert_called_once_with( + self.acme_client.assert_called_once_with( new_reg_uri=mock.ANY, key=mock.ANY, verify_ssl=True) def _mock_obtain_certificate(self): self.client.auth_handler = mock.MagicMock() - self.acme().request_issuance.return_value = mock.sentinel.certr - self.acme().fetch_chain.return_value = mock.sentinel.chain + self.acme.request_issuance.return_value = mock.sentinel.certr + self.acme.fetch_chain.return_value = mock.sentinel.chain def _check_obtain_certificate(self): self.client.auth_handler.get_authorizations.assert_called_once_with( @@ -85,7 +86,7 @@ class ClientTest(unittest.TestCase): jose.ComparableX509(OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_ASN1, CSR_SAN)), self.client.auth_handler.get_authorizations()) - self.acme().fetch_chain.assert_called_once_with(mock.sentinel.certr) + self.acme.fetch_chain.assert_called_once_with(mock.sentinel.certr) def test_obtain_certificate_from_csr(self): self._mock_obtain_certificate() From 15f443dcedc516e92a8df88a8dc560ec2503703b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 06:42:02 +0000 Subject: [PATCH 17/30] assert_called_once -> assertEqual(1, *.call_count) --- acme/client_test.py | 3 ++- letsencrypt/plugins/manual_test.py | 2 +- letsencrypt/tests/cli_test.py | 4 ++-- letsencrypt/tests/display/ops_test.py | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/acme/client_test.py b/acme/client_test.py index bbed2ed87..a524989e3 100644 --- a/acme/client_test.py +++ b/acme/client_test.py @@ -503,7 +503,8 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase): def test_head(self): self.assertEqual(self.response, self.net.head('url', 'foo', bar='baz')) - self.send_request.assert_called_once('HEAD', 'url', 'foo', bar='baz') + self.send_request.assert_called_once_with( + 'HEAD', 'url', 'foo', bar='baz') def test_get(self): self.assertEqual(self.checked_response, self.net.get( diff --git a/letsencrypt/plugins/manual_test.py b/letsencrypt/plugins/manual_test.py index a533bcc75..9daba668a 100644 --- a/letsencrypt/plugins/manual_test.py +++ b/letsencrypt/plugins/manual_test.py @@ -41,7 +41,7 @@ class ManualAuthenticatorTest(unittest.TestCase): resp = challenges.SimpleHTTPResponse(tls=False, path='Zm9v') self.assertEqual([resp], self.auth.perform(self.achalls)) - mock_raw_input.assert_called_once() + self.assertEqual(1, mock_raw_input.call_count) mock_verify.assert_called_with(self.achalls[0].challb, "foo.com", 4430) message = mock_stdout.write.mock_calls[0][1][0] diff --git a/letsencrypt/tests/cli_test.py b/letsencrypt/tests/cli_test.py index 130da4217..613c3189b 100644 --- a/letsencrypt/tests/cli_test.py +++ b/letsencrypt/tests/cli_test.py @@ -45,7 +45,7 @@ class CLITest(unittest.TestCase): def test_rollback(self): _, _, _, client = self._call(['rollback']) - client.rollback.assert_called_once() + self.assertEqual(1, client.rollback.call_count) _, _, _, client = self._call(['rollback', '--checkpoints', '123']) client.rollback.assert_called_once_with( @@ -53,7 +53,7 @@ class CLITest(unittest.TestCase): def test_config_changes(self): _, _, _, client = self._call(['config_changes']) - client.view_config_changes.assert_called_once() + self.assertEqual(1, client.view_config_changes.call_count) def test_plugins(self): flags = ['--init', '--prepare', '--authenticators', '--installers'] diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index 6f4b2f317..273687128 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -80,11 +80,11 @@ class PickPluginTest(unittest.TestCase): def test_default_provided(self): self.default = "foo" self._call() - self.reg.filter.assert_called_once() + self.assertEqual(1, self.reg.filter.call_count) def test_no_default(self): self._call() - self.reg.filter.assert_called_once() + self.assertEqual(1, self.reg.filter.call_count) def test_no_candidate(self): self.assertTrue(self._call() is None) From 1d35946b4e3c7c165bde39010e88bebb46871ddc Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 06:51:09 +0000 Subject: [PATCH 18/30] Fix PickPluginTest.test_no_defaults. --- letsencrypt/tests/display/ops_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index 273687128..59b8d9b5c 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -84,7 +84,7 @@ class PickPluginTest(unittest.TestCase): def test_no_default(self): self._call() - self.assertEqual(1, self.reg.filter.call_count) + self.assertEqual(1, self.reg.ifaces.call_count) def test_no_candidate(self): self.assertTrue(self._call() is None) From 4ebc20402b3ca492ac3a0ca2c07e98fd21e29699 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 07:21:07 +0000 Subject: [PATCH 19/30] Pin mock<1.1.0 (Python 2.6 support). --- setup.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 042c6e159..49ac5a6c0 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ acme_install_requires = [ # rsa_recover_prime_factors (>=0.8) 'cryptography>=0.8', #'letsencrypt' # TODO: uses testdata vectors - 'mock', + 'mock<1.1.0', # py26 'pyrfc3339', 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304) 'pyasn1', # urllib3 InsecurePlatformWarning (#304) @@ -54,7 +54,7 @@ letsencrypt_install_requires = [ 'ConfigArgParse', 'configobj', #'cryptography>=0.7', # load_pem_x509_certificate, version pin mismatch - 'mock', + 'mock<1.1.0', # py26 'parsedatetime', 'psutil>=2.1.0', # net_connections introduced in 2.1.0 # https://pyopenssl.readthedocs.org/en/latest/api/crypto.html#OpenSSL.crypto.X509Req.get_extensions @@ -68,7 +68,7 @@ letsencrypt_install_requires = [ letsencrypt_apache_install_requires = [ #'acme', #'letsencrypt', - 'mock', + 'mock<1.1.0', # py26 'python-augeas', 'zope.component', 'zope.interface', @@ -77,7 +77,7 @@ letsencrypt_nginx_install_requires = [ #'acme', #'letsencrypt', 'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary? - 'mock', + 'mock<1.1.0', # py26 'zope.interface', ] @@ -86,7 +86,7 @@ install_requires = [ 'cryptography>=0.8', 'ConfigArgParse', 'configobj', - 'mock', + 'mock<1.1.0', # py26 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304) 'parsedatetime', 'psutil>=2.1.0', # net_connections introduced in 2.1.0 From c57cd239c330a11a3f5d795fb6ac056578813355 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 07:25:29 +0000 Subject: [PATCH 20/30] Register key creation before writing out. --- letsencrypt/plugins/common.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/letsencrypt/plugins/common.py b/letsencrypt/plugins/common.py index 460af1b15..104e8d9c4 100644 --- a/letsencrypt/plugins/common.py +++ b/letsencrypt/plugins/common.py @@ -168,7 +168,9 @@ class Dvsni(object): # pylint: disable=invalid-name """Generate and write out challenge certificate.""" cert_path = self.get_cert_path(achall) + key_path = self.get_key_path(achall) # Register the path before you write out the file + self.configurator.reverter.register_file_creation(True, key_path) self.configurator.reverter.register_file_creation(True, cert_path) cert_pem, response = achall.gen_cert_and_response(s) @@ -177,14 +179,13 @@ class Dvsni(object): with open(cert_path, "wb") as cert_chall_fd: cert_chall_fd.write(cert_pem) - key_path = self.get_key_path(achall) + # Write out challenge key key_pem = achall.key.key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()) with le_util.safe_open(key_path, 'wb', chmod=0o400) as key_file: key_file.write(key_pem) - self.configurator.reverter.register_file_creation(True, key_path) return response From 19c73249cae7982358f17311ddd908dc81820874 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 11:01:43 +0000 Subject: [PATCH 21/30] Sort vectors: acme/testdata, separate acme and letsencrypt. --- MANIFEST.in | 2 +- acme/challenges_test.py | 4 ++-- acme/client_test.py | 6 +++--- acme/jose/json_util_test.py | 4 ++-- acme/jose/jwa_test.py | 2 +- acme/jose/jwk_test.py | 8 ++++---- acme/jose/jws_test.py | 6 +++--- acme/jose/util_test.py | 6 +++--- acme/jws_test.py | 2 +- acme/messages_test.py | 8 ++++---- acme/other_test.py | 2 +- acme/{jose => }/testdata/README | 0 acme/testdata/cert-san.pem | 14 ++++++++++++++ acme/{jose => }/testdata/cert.der | Bin acme/testdata/cert.pem | 13 +++++++++++++ acme/testdata/csr-san.pem | 10 ++++++++++ acme/{jose => }/testdata/csr.der | Bin acme/testdata/csr.pem | 10 ++++++++++ acme/testdata/dsa512_key.pem | 14 ++++++++++++++ acme/{jose => }/testdata/rsa1024_key.pem | 0 acme/{jose => }/testdata/rsa256_key.pem | 0 acme/{jose => }/testdata/rsa512_key.pem | 0 letsencrypt/plugins/common_test.py | 5 +++-- .../standalone/tests/authenticator_test.py | 4 ++-- letsencrypt/tests/account_test.py | 2 +- letsencrypt/tests/achallenges_test.py | 3 ++- letsencrypt/tests/crypto_util_test.py | 4 ++-- letsencrypt/tests/proof_of_possession_test.py | 4 ++-- letsencrypt/tests/renewer_test.py | 6 +++--- letsencrypt/tests/revoker_test.py | 2 +- letsencrypt/tests/testdata/cert.der | Bin 0 -> 377 bytes letsencrypt/tests/testdata/rsa256_key.pem | 6 ++++++ letsencrypt/tests/testdata/rsa512_key.pem | 14 +++++++------- letsencrypt/tests/testdata/rsa512_key_2.pem | 9 +++++++++ letsencrypt_apache/tests/util.py | 4 ++-- letsencrypt_nginx/tests/util.py | 4 ++-- setup.py | 1 - 37 files changed, 128 insertions(+), 51 deletions(-) rename acme/{jose => }/testdata/README (100%) create mode 100644 acme/testdata/cert-san.pem rename acme/{jose => }/testdata/cert.der (100%) create mode 100644 acme/testdata/cert.pem create mode 100644 acme/testdata/csr-san.pem rename acme/{jose => }/testdata/csr.der (100%) create mode 100644 acme/testdata/csr.pem create mode 100644 acme/testdata/dsa512_key.pem rename acme/{jose => }/testdata/rsa1024_key.pem (100%) rename acme/{jose => }/testdata/rsa256_key.pem (100%) rename acme/{jose => }/testdata/rsa512_key.pem (100%) create mode 100644 letsencrypt/tests/testdata/cert.der create mode 100644 letsencrypt/tests/testdata/rsa256_key.pem create mode 100644 letsencrypt/tests/testdata/rsa512_key_2.pem diff --git a/MANIFEST.in b/MANIFEST.in index 7ff0a4d0c..900a7ab80 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,7 +6,7 @@ include letsencrypt/EULA recursive-include letsencrypt/tests/testdata * recursive-include acme/schemata *.json -recursive-include acme/jose/testdata * +recursive-include acme/testdata * recursive-include letsencrypt_apache/tests/testdata * include letsencrypt_apache/options-ssl-apache.conf diff --git a/acme/challenges_test.py b/acme/challenges_test.py index 94c04388d..40a2455d5 100644 --- a/acme/challenges_test.py +++ b/acme/challenges_test.py @@ -16,10 +16,10 @@ from acme import other CERT = jose.ComparableX509(OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string( - 'letsencrypt.tests', os.path.join('testdata', 'cert.pem')))) + 'acme', os.path.join('testdata', 'cert.pem')))) KEY = serialization.load_pem_private_key( pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'rsa512_key.pem')), + 'acme', os.path.join('testdata', 'rsa512_key.pem')), password=None, backend=default_backend()) diff --git a/acme/client_test.py b/acme/client_test.py index 7e433c91b..abd5e7172 100644 --- a/acme/client_test.py +++ b/acme/client_test.py @@ -18,11 +18,11 @@ from acme import messages_test CERT_DER = pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'cert.der')) + 'acme', os.path.join('testdata', 'cert.der')) KEY = jose.JWKRSA.load(pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))) + 'acme', os.path.join('testdata', 'rsa512_key.pem'))) KEY2 = jose.JWKRSA.load(pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'rsa256_key.pem'))) + 'acme', os.path.join('testdata', 'rsa256_key.pem'))) class ClientTest(unittest.TestCase): diff --git a/acme/jose/json_util_test.py b/acme/jose/json_util_test.py index 9e493e80c..458d3b87c 100644 --- a/acme/jose/json_util_test.py +++ b/acme/jose/json_util_test.py @@ -14,10 +14,10 @@ from acme.jose import util CERT = util.ComparableX509(OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string( - 'letsencrypt.tests', os.path.join('testdata', 'cert.pem')))) + 'acme', os.path.join('testdata', 'cert.pem')))) CSR = util.ComparableX509(OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string( - 'letsencrypt.tests', os.path.join('testdata', 'csr.pem')))) + 'acme', os.path.join('testdata', 'csr.pem')))) class FieldTest(unittest.TestCase): diff --git a/acme/jose/jwa_test.py b/acme/jose/jwa_test.py index 147038788..898773716 100644 --- a/acme/jose/jwa_test.py +++ b/acme/jose/jwa_test.py @@ -12,7 +12,7 @@ from acme.jose import jwk_test RSA1024_KEY = serialization.load_pem_private_key( pkg_resources.resource_string( - __name__, os.path.join('testdata', 'rsa1024_key.pem')), + 'acme', os.path.join('testdata', 'rsa1024_key.pem')), password=None, backend=default_backend()) diff --git a/acme/jose/jwk_test.py b/acme/jose/jwk_test.py index 5be28ba17..2e317d3cc 100644 --- a/acme/jose/jwk_test.py +++ b/acme/jose/jwk_test.py @@ -11,14 +11,14 @@ from acme.jose import util DSA_PEM = pkg_resources.resource_string( - 'letsencrypt.tests', os.path.join('testdata', 'dsa512_key.pem')) + 'acme', os.path.join('testdata', 'dsa512_key.pem')) RSA256_KEY = serialization.load_pem_private_key( pkg_resources.resource_string( - __name__, os.path.join('testdata', 'rsa256_key.pem')), + 'acme', os.path.join('testdata', 'rsa256_key.pem')), password=None, backend=default_backend()) RSA512_KEY = serialization.load_pem_private_key( pkg_resources.resource_string( - __name__, os.path.join('testdata', 'rsa512_key.pem')), + 'acme', os.path.join('testdata', 'rsa512_key.pem')), password=None, backend=default_backend()) @@ -112,7 +112,7 @@ class JWKRSATest(unittest.TestCase): from acme.jose.jwk import JWKRSA self.assertEqual( self.private, JWKRSA.load(pkg_resources.resource_string( - __name__, os.path.join('testdata', 'rsa256_key.pem')))) + 'acme', os.path.join('testdata', 'rsa256_key.pem')))) def test_public_key(self): self.assertEqual(self.jwk256, self.private.public_key()) diff --git a/acme/jose/jws_test.py b/acme/jose/jws_test.py index 72b8b7b22..ecfb11be7 100644 --- a/acme/jose/jws_test.py +++ b/acme/jose/jws_test.py @@ -18,10 +18,10 @@ from acme.jose import util CERT = util.ComparableX509(OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string( - 'letsencrypt.tests', 'testdata/cert.pem'))) + 'acme', 'testdata/cert.pem'))) RSA512_KEY = serialization.load_pem_private_key( pkg_resources.resource_string( - __name__, os.path.join('testdata', 'rsa512_key.pem')), + 'acme', os.path.join('testdata', 'rsa512_key.pem')), password=None, backend=default_backend()) @@ -210,7 +210,7 @@ class CLITest(unittest.TestCase): def setUp(self): self.key_path = pkg_resources.resource_filename( - __name__, os.path.join('testdata', 'rsa512_key.pem')) + 'acme', os.path.join('testdata', 'rsa512_key.pem')) def test_unverified(self): from acme.jose.jws import CLI diff --git a/acme/jose/util_test.py b/acme/jose/util_test.py index f29b0792f..8d36de09b 100644 --- a/acme/jose/util_test.py +++ b/acme/jose/util_test.py @@ -17,7 +17,7 @@ class ComparableX509Test(unittest.TestCase): def _load(method, filename): # pylint: disable=missing-docstring return ComparableX509(method( OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string( - 'letsencrypt.tests', os.path.join('testdata', filename)))) + 'acme', os.path.join('testdata', filename)))) self.req1 = _load(OpenSSL.crypto.load_certificate_request, 'csr.pem') self.req2 = _load(OpenSSL.crypto.load_certificate_request, 'csr.pem') @@ -61,13 +61,13 @@ class ComparableRSAKeyTest(unittest.TestCase): def load_key(): # pylint: disable=missing-docstring return ComparableRSAKey(serialization.load_pem_private_key( pkg_resources.resource_string( - __name__, os.path.join('testdata', 'rsa256_key.pem')), + 'acme', os.path.join('testdata', 'rsa256_key.pem')), password=None, backend=backend)) self.key = load_key() self.key_same = load_key() self.key2 = ComparableRSAKey(serialization.load_pem_private_key( pkg_resources.resource_string( - __name__, os.path.join('testdata', 'rsa512_key.pem')), + 'acme', os.path.join('testdata', 'rsa512_key.pem')), password=None, backend=backend)) def test_getattr_proxy(self): diff --git a/acme/jws_test.py b/acme/jws_test.py index e65a3bd46..989a6697a 100644 --- a/acme/jws_test.py +++ b/acme/jws_test.py @@ -12,7 +12,7 @@ from acme import jose RSA512_KEY = serialization.load_pem_private_key( pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'rsa512_key.pem')), + 'acme', os.path.join('testdata', 'rsa512_key.pem')), password=None, backend=default_backend()) diff --git a/acme/messages_test.py b/acme/messages_test.py index d028a59c5..71bf25963 100644 --- a/acme/messages_test.py +++ b/acme/messages_test.py @@ -14,17 +14,17 @@ from acme import jose CERT = jose.ComparableX509(OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_ASN1, pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'cert.der')))) + 'acme', os.path.join('testdata', 'cert.der')))) CSR = jose.ComparableX509(OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_ASN1, pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'csr.der')))) + 'acme', os.path.join('testdata', 'csr.der')))) KEY = serialization.load_pem_private_key( pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'rsa512_key.pem')), + 'acme', os.path.join('testdata', 'rsa512_key.pem')), password=None, backend=default_backend()) CERT = jose.ComparableX509(OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_ASN1, pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'cert.der')))) + 'acme', os.path.join('testdata', 'cert.der')))) class ErrorTest(unittest.TestCase): diff --git a/acme/other_test.py b/acme/other_test.py index 64699038e..25b07bcde 100644 --- a/acme/other_test.py +++ b/acme/other_test.py @@ -11,7 +11,7 @@ from acme import jose KEY = serialization.load_pem_private_key( pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'rsa512_key.pem')), + 'acme', os.path.join('testdata', 'rsa512_key.pem')), password=None, backend=default_backend()) diff --git a/acme/jose/testdata/README b/acme/testdata/README similarity index 100% rename from acme/jose/testdata/README rename to acme/testdata/README diff --git a/acme/testdata/cert-san.pem b/acme/testdata/cert-san.pem new file mode 100644 index 000000000..dcb835994 --- /dev/null +++ b/acme/testdata/cert-san.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICFjCCAcCgAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx +ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM +IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 +YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG +A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix +KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS +BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR +7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c ++pVE6K+EdE/twuUCAwEAAaM2MDQwCQYDVR0TBAIwADAnBgNVHREEIDAeggtleGFt +cGxlLmNvbYIPd3d3LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA0EASuvNKFTF +nTJsvnSXn52f4BMZJJ2id/kW7+r+FJRm+L20gKQ1aqq8d3e/lzRUrv5SMf1TAOe7 +RDjyGMKy5ZgM2w== +-----END CERTIFICATE----- diff --git a/acme/jose/testdata/cert.der b/acme/testdata/cert.der similarity index 100% rename from acme/jose/testdata/cert.der rename to acme/testdata/cert.der diff --git a/acme/testdata/cert.pem b/acme/testdata/cert.pem new file mode 100644 index 000000000..96c55cbf4 --- /dev/null +++ b/acme/testdata/cert.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx +ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM +IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 +YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG +A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix +KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS +BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR +7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c ++pVE6K+EdE/twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksll +vr6zJepBH5fMndfk3XJp10jT6VE+14KNtjh02a56GoraAvJAT5/H67E8GvJ/ocNn +B/o= +-----END CERTIFICATE----- diff --git a/acme/testdata/csr-san.pem b/acme/testdata/csr-san.pem new file mode 100644 index 000000000..a7128e35c --- /dev/null +++ b/acme/testdata/csr-san.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw +EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy +c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG +9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f +p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN +AQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t +MA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy +tmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A== +-----END CERTIFICATE REQUEST----- diff --git a/acme/jose/testdata/csr.der b/acme/testdata/csr.der similarity index 100% rename from acme/jose/testdata/csr.der rename to acme/testdata/csr.der diff --git a/acme/testdata/csr.pem b/acme/testdata/csr.pem new file mode 100644 index 000000000..b6818e39d --- /dev/null +++ b/acme/testdata/csr.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw +EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy +c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG +9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f +p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoCkwJwYJKoZIhvcN +AQkOMRowGDAWBgNVHREEDzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAANB +AHJH/O6BtC9aGzEVCMGOZ7z9iIRHWSzr9x/bOzn7hLwsbXPAgO1QxEwL+X+4g20G +n9XBE1N9W6HCIEut2d8wACg= +-----END CERTIFICATE REQUEST----- diff --git a/acme/testdata/dsa512_key.pem b/acme/testdata/dsa512_key.pem new file mode 100644 index 000000000..78e164712 --- /dev/null +++ b/acme/testdata/dsa512_key.pem @@ -0,0 +1,14 @@ +-----BEGIN DSA PARAMETERS----- +MIGdAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqfn6GC +OixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSPAkEA +qfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xmrfvl +41pgNJpgu99YOYqPpS0g7A== +-----END DSA PARAMETERS----- +-----BEGIN DSA PRIVATE KEY----- +MIH5AgEAAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqf +n6GCOixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSP +AkEAqfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xm +rfvl41pgNJpgu99YOYqPpS0g7AJATQ2LUzjGQSM6UljcPY5I2OD9THkUR9kH2tth +zZd70UoI9btrVaTizgqYShuok94glSQNK0H92JgUk3scJPaAkAIVAMDn61h6vrCE +mNv063So6E+eYaIN +-----END DSA PRIVATE KEY----- diff --git a/acme/jose/testdata/rsa1024_key.pem b/acme/testdata/rsa1024_key.pem similarity index 100% rename from acme/jose/testdata/rsa1024_key.pem rename to acme/testdata/rsa1024_key.pem diff --git a/acme/jose/testdata/rsa256_key.pem b/acme/testdata/rsa256_key.pem similarity index 100% rename from acme/jose/testdata/rsa256_key.pem rename to acme/testdata/rsa256_key.pem diff --git a/acme/jose/testdata/rsa512_key.pem b/acme/testdata/rsa512_key.pem similarity index 100% rename from acme/jose/testdata/rsa512_key.pem rename to acme/testdata/rsa512_key.pem diff --git a/letsencrypt/plugins/common_test.py b/letsencrypt/plugins/common_test.py index 8688c36b1..b68ab8369 100644 --- a/letsencrypt/plugins/common_test.py +++ b/letsencrypt/plugins/common_test.py @@ -1,4 +1,5 @@ """Tests for letsencrypt.plugins.common.""" +import os import pkg_resources import unittest @@ -111,9 +112,9 @@ class DvsniTest(unittest.TestCase): """Tests for letsencrypt.plugins.common.DvsniTest.""" rsa256_file = pkg_resources.resource_filename( - "acme.jose", "testdata/rsa256_key.pem") + "letsencrypt.tests", os.path.join("testdata", "rsa256_key.pem")) rsa256_pem = pkg_resources.resource_string( - "acme.jose", "testdata/rsa256_key.pem") + "letsencrypt.tests", os.path.join("testdata", "rsa256_key.pem")) auth_key = le_util.Key(rsa256_file, rsa256_pem) achalls = [ diff --git a/letsencrypt/plugins/standalone/tests/authenticator_test.py b/letsencrypt/plugins/standalone/tests/authenticator_test.py index 422dc0549..45c485b5d 100644 --- a/letsencrypt/plugins/standalone/tests/authenticator_test.py +++ b/letsencrypt/plugins/standalone/tests/authenticator_test.py @@ -20,9 +20,9 @@ from letsencrypt.tests import acme_util KEY_PATH = pkg_resources.resource_filename( - "acme.jose", os.path.join("testdata", "rsa512_key.pem")) + "letsencrypt.tests", os.path.join("testdata", "rsa512_key.pem")) KEY_DATA = pkg_resources.resource_string( - "acme.jose", os.path.join("testdata", "rsa512_key.pem")) + "letsencrypt.tests", os.path.join("testdata", "rsa512_key.pem")) KEY = jose.JWKRSA(key=jose.ComparableRSAKey(serialization.load_pem_private_key( KEY_DATA, password=None, backend=default_backend()))) PRIVATE_KEY = OpenSSL.crypto.load_privatekey( diff --git a/letsencrypt/tests/account_test.py b/letsencrypt/tests/account_test.py index 93da225af..9a129dbe6 100644 --- a/letsencrypt/tests/account_test.py +++ b/letsencrypt/tests/account_test.py @@ -17,7 +17,7 @@ from letsencrypt import errors KEY = jose.JWKRSA.load(pkg_resources.resource_string( - __name__, os.path.join("testdata", "rsa512_key.pem"))) + "letsencrypt.tests", os.path.join("testdata", "rsa512_key_2.pem"))) class AccountTest(unittest.TestCase): diff --git a/letsencrypt/tests/achallenges_test.py b/letsencrypt/tests/achallenges_test.py index e6c154aae..4faa34d12 100644 --- a/letsencrypt/tests/achallenges_test.py +++ b/letsencrypt/tests/achallenges_test.py @@ -24,7 +24,8 @@ class DVSNITest(unittest.TestCase): key = jose.JWKRSA(key=jose.ComparableRSAKey( serialization.load_pem_private_key( pkg_resources.resource_string( - "acme.jose", os.path.join("testdata", "rsa512_key.pem")), + "letsencrypt.tests", os.path.join( + "testdata", "rsa512_key.pem")), password=None, backend=default_backend()))) from letsencrypt.achallenges import DVSNI diff --git a/letsencrypt/tests/crypto_util_test.py b/letsencrypt/tests/crypto_util_test.py index 06bdc4cd8..2e1e62797 100644 --- a/letsencrypt/tests/crypto_util_test.py +++ b/letsencrypt/tests/crypto_util_test.py @@ -11,9 +11,9 @@ import mock RSA256_KEY = pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'rsa256_key.pem')) + 'letsencrypt.tests', os.path.join('testdata', 'rsa256_key.pem')) RSA512_KEY = pkg_resources.resource_string( - 'acme.jose', os.path.join('testdata', 'rsa512_key.pem')) + 'letsencrypt.tests', os.path.join('testdata', 'rsa512_key.pem')) CERT = pkg_resources.resource_string( 'letsencrypt.tests', os.path.join('testdata', 'cert.pem')) SAN_CERT = pkg_resources.resource_string( diff --git a/letsencrypt/tests/proof_of_possession_test.py b/letsencrypt/tests/proof_of_possession_test.py index d91b8bdb6..ef0ffc21e 100644 --- a/letsencrypt/tests/proof_of_possession_test.py +++ b/letsencrypt/tests/proof_of_possession_test.py @@ -19,7 +19,7 @@ from letsencrypt.display import util as display_util BASE_PACKAGE = "letsencrypt.tests" CERT0_PATH = pkg_resources.resource_filename( - "acme.jose", os.path.join("testdata", "cert.der")) + BASE_PACKAGE, os.path.join("testdata", "cert.der")) CERT2_PATH = pkg_resources.resource_filename( BASE_PACKAGE, os.path.join("testdata", "dsa_cert.pem")) CERT2_KEY_PATH = pkg_resources.resource_filename( @@ -27,7 +27,7 @@ CERT2_KEY_PATH = pkg_resources.resource_filename( CERT3_PATH = pkg_resources.resource_filename( BASE_PACKAGE, os.path.join("testdata", "matching_cert.pem")) CERT3_KEY_PATH = pkg_resources.resource_filename( - BASE_PACKAGE, os.path.join("testdata", "rsa512_key.pem")) + BASE_PACKAGE, os.path.join("testdata", "rsa512_key_2.pem")) with open(CERT3_KEY_PATH) as cert3_file: CERT3_KEY = serialization.load_pem_private_key( cert3_file.read(), password=None, diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index 3596201b3..4ee7cc0e8 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -296,7 +296,7 @@ class RenewableCertTests(unittest.TestCase): def _test_notafterbefore(self, function, timestamp): test_cert = pkg_resources.resource_string( - "letsencrypt.tests", "testdata/cert.pem") + "letsencrypt.tests", os.path.join("testdata", "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: @@ -320,7 +320,7 @@ class RenewableCertTests(unittest.TestCase): """Test should_autodeploy() and should_autorenew() on the basis of expiry time windows.""" test_cert = pkg_resources.resource_string( - "letsencrypt.tests", "testdata/cert.pem") + "letsencrypt.tests", os.path.join("testdata", "cert.pem")) for kind in ALL_FOUR: where = getattr(self.test_rc, kind) os.symlink(os.path.join("..", "..", "archive", "example.org", @@ -562,7 +562,7 @@ class RenewableCertTests(unittest.TestCase): from letsencrypt import renewer test_cert = pkg_resources.resource_string( - "letsencrypt.tests", "testdata/cert-san.pem") + "letsencrypt.tests", os.path.join("testdata", "cert-san.pem")) for kind in ALL_FOUR: os.symlink(os.path.join("..", "..", "archive", "example.org", kind + "1.pem"), diff --git a/letsencrypt/tests/revoker_test.py b/letsencrypt/tests/revoker_test.py index f90da2168..92eeaf92d 100644 --- a/letsencrypt/tests/revoker_test.py +++ b/letsencrypt/tests/revoker_test.py @@ -99,7 +99,7 @@ class RevokerTest(RevokerBase): mock_display().confirm_revocation.return_value = True key_path = pkg_resources.resource_filename( - "acme.jose", os.path.join("testdata", "rsa256_key.pem")) + "letsencrypt.tests", os.path.join("testdata", "rsa256_key.pem")) wrong_key = le_util.Key(key_path, open(key_path).read()) self.revoker.revoke_from_key(wrong_key) diff --git a/letsencrypt/tests/testdata/cert.der b/letsencrypt/tests/testdata/cert.der new file mode 100644 index 0000000000000000000000000000000000000000..5f1018505d81a50ed3239d829533deac5fcc2085 GIT binary patch literal 377 zcmXqLVk|XiVw7LN%*4pV#L2Ms(6oH-+lDa)ylk9WZ60mkc^MhGSs4t(3`Got*qB3E zn0dHUD-v@Ha#Hn@^K%X4#CZ)(4a|&;3``6RjLoCKTyr=Vr#=)57+D#Zy%`KVm>e0_ zlooFZd@FxGb01z;s66b16iPJW%*ddSVYv$pLpwieafaMs>~58{VWGc zu1@bVkOxUCvq%_-HDFi315zN&!fL?G$oL;EIG7z7c)I@!HO%vwut#mfG=7{>z}O6i?Kd^cJYN9>LqE5%h;CwZnWA40OQJj AMgRZ+ literal 0 HcmV?d00001 diff --git a/letsencrypt/tests/testdata/rsa256_key.pem b/letsencrypt/tests/testdata/rsa256_key.pem new file mode 100644 index 000000000..659274d1d --- /dev/null +++ b/letsencrypt/tests/testdata/rsa256_key.pem @@ -0,0 +1,6 @@ +-----BEGIN RSA PRIVATE KEY----- +MIGrAgEAAiEAm2Fylv+Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEkCAwEAAQIh +AJT0BA/xD01dFCAXzSNyj9nfSZa3NpqzJZZn/eOm7vghAhEAzUVNZn4lLLBD1R6N +E8TKNQIRAMHHyn3O5JeY36lwKwkUlEUCEAliRauN0L0+QZuYjfJ9aJECEGx4dru3 +rTPCyighdqWNlHUCEQCiLjlwSRtWgmMBudCkVjzt +-----END RSA PRIVATE KEY----- diff --git a/letsencrypt/tests/testdata/rsa512_key.pem b/letsencrypt/tests/testdata/rsa512_key.pem index 709b6d8e3..610c8d315 100644 --- a/letsencrypt/tests/testdata/rsa512_key.pem +++ b/letsencrypt/tests/testdata/rsa512_key.pem @@ -1,9 +1,9 @@ -----BEGIN RSA PRIVATE KEY----- -MIIBOwIBAAJBAPS2EXFRNza/qpXnnBHF/CcFQ543htV+7nLAmrLrmTNHtPXJmLlM -8SJDIzv/ceAFXL110VzxFfi81lpH5E5c0TMCAwEAAQJBALmppYQ/JVARjWBcsEm/ -1/bXBJ127YLv4gQIY5baL4r6IdEE33OXMTTmD9wf+ajuq1eaH0htHkwhOvREu0sz -bskCIQD/Cg+xhEVLcwK3pFp3afPIhj1IPFiL3Uy/nqyMZ6O/RQIhAPWiDBofp7Cp -J4dGZs+hkRySq/IOeeRJlNK1Pq64nToXAiBZ7+te1100YSd5KT051SRB94zO13EG -SZESFduVW8rz3QIgK+tLiqg6TYYRQUi/PUTAM4GuKNuZw828RGiPyqHLywUCIQCd -pkZrNphL/y0D7HSbPIfZzD90M2V8tUjlK0BTqk1bHA== +MIIBOgIBAAJBAKx1c7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79 +vukFhN9HFoHZiUvOjm0c+pVE6K+EdE/twuUCAwEAAQJAMbrEnJCrQe8YqAbw1/Bn +elAzIamndfE3U8bTavf9sgFpS4HL83rhd6PDbvx81ucaJAT/5x048fM/nFl4fzAc +mQIhAOF/a9o3EIsDKEmUl+Z1OaOiUxDF3kqWSmALEsmvDhwXAiEAw8ljV5RO/rUp +Zu2YMDFq3MKpyyMgBIJ8CxmGRc6gCmMCIGRQzkcmhfqBrhOFwkmozrqIBRIKJIjj +8TRm2LXWZZ2DAiAqVO7PztdNpynugUy4jtbGKKjBrTSNBRGA7OHlUgm0dQIhALQq +6oGU29Vxlvt3k0vmiRKU4AVfLyNXIGtcWcNG46h/ -----END RSA PRIVATE KEY----- diff --git a/letsencrypt/tests/testdata/rsa512_key_2.pem b/letsencrypt/tests/testdata/rsa512_key_2.pem new file mode 100644 index 000000000..709b6d8e3 --- /dev/null +++ b/letsencrypt/tests/testdata/rsa512_key_2.pem @@ -0,0 +1,9 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIBOwIBAAJBAPS2EXFRNza/qpXnnBHF/CcFQ543htV+7nLAmrLrmTNHtPXJmLlM +8SJDIzv/ceAFXL110VzxFfi81lpH5E5c0TMCAwEAAQJBALmppYQ/JVARjWBcsEm/ +1/bXBJ127YLv4gQIY5baL4r6IdEE33OXMTTmD9wf+ajuq1eaH0htHkwhOvREu0sz +bskCIQD/Cg+xhEVLcwK3pFp3afPIhj1IPFiL3Uy/nqyMZ6O/RQIhAPWiDBofp7Cp +J4dGZs+hkRySq/IOeeRJlNK1Pq64nToXAiBZ7+te1100YSd5KT051SRB94zO13EG +SZESFduVW8rz3QIgK+tLiqg6TYYRQUi/PUTAM4GuKNuZw828RGiPyqHLywUCIQCd +pkZrNphL/y0D7HSbPIfZzD90M2V8tUjlK0BTqk1bHA== +-----END RSA PRIVATE KEY----- diff --git a/letsencrypt_apache/tests/util.py b/letsencrypt_apache/tests/util.py index 0769f6050..b7048035a 100644 --- a/letsencrypt_apache/tests/util.py +++ b/letsencrypt_apache/tests/util.py @@ -29,9 +29,9 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2") self.rsa256_file = pkg_resources.resource_filename( - "acme.jose", "testdata/rsa256_key.pem") + "letsencrypt.tests", os.path.join("testdata", "rsa256_key.pem")) self.rsa256_pem = pkg_resources.resource_string( - "acme.jose", "testdata/rsa256_key.pem") + "letsencrypt.tests", os.path.join("testdata", "rsa256_key.pem")) def get_apache_configurator( diff --git a/letsencrypt_nginx/tests/util.py b/letsencrypt_nginx/tests/util.py index 414a2f315..a7db398c6 100644 --- a/letsencrypt_nginx/tests/util.py +++ b/letsencrypt_nginx/tests/util.py @@ -26,9 +26,9 @@ class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods self.config_path = os.path.join(self.temp_dir, "etc_nginx") self.rsa256_file = pkg_resources.resource_filename( - "acme.jose", "testdata/rsa256_key.pem") + "letsencrypt.tests", os.path.join("testdata", "rsa256_key.pem")) self.rsa256_pem = pkg_resources.resource_string( - "acme.jose", "testdata/rsa256_key.pem") + "letsencrypt.tests", os.path.join("testdata", "rsa256_key.pem")) def get_data_filename(filename): diff --git a/setup.py b/setup.py index 49ac5a6c0..b2bd0e652 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,6 @@ acme_install_requires = [ # load_pem_private/public_key (>=0.6) # rsa_recover_prime_factors (>=0.8) 'cryptography>=0.8', - #'letsencrypt' # TODO: uses testdata vectors 'mock<1.1.0', # py26 'pyrfc3339', 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304) From 0e474436c4295c9d6697fc7ef735c27db5b862a0 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 12:26:51 +0000 Subject: [PATCH 22/30] Unified vector loading in acme. --- acme/challenges_test.py | 14 ++-------- acme/client_test.py | 12 +++----- acme/jose/json_util_test.py | 13 +++------ acme/jose/jwa_test.py | 26 +++++++----------- acme/jose/jwk_test.py | 34 ++++++++--------------- acme/jose/jws_test.py | 21 ++++---------- acme/jose/util_test.py | 42 ++++++++-------------------- acme/jws_test.py | 13 ++------- acme/messages_test.py | 22 +++------------ acme/other_test.py | 11 ++------ acme/test_util.py | 55 +++++++++++++++++++++++++++++++++++++ acme/testdata/README | 4 +++ 12 files changed, 119 insertions(+), 148 deletions(-) create mode 100644 acme/test_util.py diff --git a/acme/challenges_test.py b/acme/challenges_test.py index 40a2455d5..a1214c2f9 100644 --- a/acme/challenges_test.py +++ b/acme/challenges_test.py @@ -1,10 +1,6 @@ """Tests for acme.challenges.""" -import os -import pkg_resources import unittest -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization import mock import OpenSSL import requests @@ -12,15 +8,11 @@ import urlparse from acme import jose from acme import other +from acme import test_util -CERT = jose.ComparableX509(OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'cert.pem')))) -KEY = serialization.load_pem_private_key( - pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'rsa512_key.pem')), - password=None, backend=default_backend()) +CERT = test_util.load_cert('cert.pem') +KEY = test_util.load_rsa_private_key('rsa512_key.pem') class ChallengeResponseTest(unittest.TestCase): diff --git a/acme/client_test.py b/acme/client_test.py index abd5e7172..3e3380a16 100644 --- a/acme/client_test.py +++ b/acme/client_test.py @@ -2,8 +2,6 @@ import datetime import httplib import json -import os -import pkg_resources import unittest import mock @@ -15,14 +13,12 @@ from acme import jose from acme import jws as acme_jws from acme import messages from acme import messages_test +from acme import test_util -CERT_DER = pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'cert.der')) -KEY = jose.JWKRSA.load(pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'rsa512_key.pem'))) -KEY2 = jose.JWKRSA.load(pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'rsa256_key.pem'))) +CERT_DER = test_util.load_vector('cert.der') +KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) +KEY2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) class ClientTest(unittest.TestCase): diff --git a/acme/jose/json_util_test.py b/acme/jose/json_util_test.py index 458d3b87c..9e2a87858 100644 --- a/acme/jose/json_util_test.py +++ b/acme/jose/json_util_test.py @@ -1,23 +1,18 @@ """Tests for acme.jose.json_util.""" import itertools -import os -import pkg_resources import unittest import mock -import OpenSSL + +from acme import test_util from acme.jose import errors from acme.jose import interfaces from acme.jose import util -CERT = util.ComparableX509(OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'cert.pem')))) -CSR = util.ComparableX509(OpenSSL.crypto.load_certificate_request( - OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'csr.pem')))) +CERT = test_util.load_cert('cert.pem') +CSR = test_util.load_csr('csr.pem') class FieldTest(unittest.TestCase): diff --git a/acme/jose/jwa_test.py b/acme/jose/jwa_test.py index 898773716..1a3896f4a 100644 --- a/acme/jose/jwa_test.py +++ b/acme/jose/jwa_test.py @@ -1,19 +1,14 @@ """Tests for acme.jose.jwa.""" -import os -import pkg_resources import unittest -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization +from acme import test_util from acme.jose import errors -from acme.jose import jwk_test -RSA1024_KEY = serialization.load_pem_private_key( - pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'rsa1024_key.pem')), - password=None, backend=default_backend()) +RSA256_KEY = test_util.load_rsa_private_key('rsa256_key.pem') +RSA512_KEY = test_util.load_rsa_private_key('rsa512_key.pem') +RSA1024_KEY = test_util.load_rsa_private_key('rsa1024_key.pem') class JWASignatureTest(unittest.TestCase): @@ -76,13 +71,13 @@ class JWARSTest(unittest.TestCase): def test_sign_no_private_part(self): from acme.jose.jwa import RS256 self.assertRaises( - errors.Error, RS256.sign, jwk_test.RSA512_KEY.public_key(), 'foo') + errors.Error, RS256.sign, RSA512_KEY.public_key(), 'foo') def test_sign_key_too_small(self): from acme.jose.jwa import RS256 from acme.jose.jwa import PS256 - self.assertRaises(errors.Error, RS256.sign, jwk_test.RSA256_KEY, 'foo') - self.assertRaises(errors.Error, PS256.sign, jwk_test.RSA256_KEY, 'foo') + self.assertRaises(errors.Error, RS256.sign, RSA256_KEY, 'foo') + self.assertRaises(errors.Error, PS256.sign, RSA256_KEY, 'foo') def test_rs(self): from acme.jose.jwa import RS256 @@ -92,11 +87,10 @@ class JWARSTest(unittest.TestCase): '\xa4\x99\x1e\x19&\xd8\xc7\x99S\x97\xfc\x85\x0cOV\xe6\x07\x99' '\xd2\xb9.>}\xfd' ) - self.assertEqual(RS256.sign(jwk_test.RSA512_KEY, 'foo'), sig) - self.assertTrue(RS256.verify( - jwk_test.RSA512_KEY.public_key(), 'foo', sig)) + self.assertEqual(RS256.sign(RSA512_KEY, 'foo'), sig) + self.assertTrue(RS256.verify(RSA512_KEY.public_key(), 'foo', sig)) self.assertFalse(RS256.verify( - jwk_test.RSA512_KEY.public_key(), 'foo', sig + '!')) + RSA512_KEY.public_key(), 'foo', sig + '!')) def test_ps(self): from acme.jose.jwa import PS256 diff --git a/acme/jose/jwk_test.py b/acme/jose/jwk_test.py index 2e317d3cc..86674b726 100644 --- a/acme/jose/jwk_test.py +++ b/acme/jose/jwk_test.py @@ -1,25 +1,15 @@ """Tests for acme.jose.jwk.""" -import os -import pkg_resources import unittest -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization +from acme import test_util from acme.jose import errors from acme.jose import util -DSA_PEM = pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'dsa512_key.pem')) -RSA256_KEY = serialization.load_pem_private_key( - pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'rsa256_key.pem')), - password=None, backend=default_backend()) -RSA512_KEY = serialization.load_pem_private_key( - pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'rsa512_key.pem')), - password=None, backend=default_backend()) +DSA_PEM = test_util.load_vector('dsa512_key.pem') +RSA256_KEY = test_util.load_rsa_private_key('rsa256_key.pem') +RSA512_KEY = test_util.load_rsa_private_key('rsa512_key.pem') class JWKTest(unittest.TestCase): @@ -73,8 +63,8 @@ class JWKRSATest(unittest.TestCase): 'e': 'AQAB', 'n': 'm2Fylv-Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEk', } - self.jwk256_comparable = JWKRSA(key=util.ComparableRSAKey( - RSA256_KEY.public_key())) + # pylint: disable=protected-access + self.jwk256_not_comparable = JWKRSA(key=RSA256_KEY.public_key()._wrapped) self.jwk512 = JWKRSA(key=RSA512_KEY.public_key()) self.jwk512json = { 'kty': 'RSA', @@ -96,9 +86,10 @@ class JWKRSATest(unittest.TestCase): 'qi': 'oi45cEkbVoJjAbnQpFY87Q', }) - def test_init_comparable(self): - self.assertTrue(isinstance(self.jwk256.key, util.ComparableRSAKey)) - self.assertEqual(self.jwk256, self.jwk256_comparable) + def test_init_auto_comparable(self): + self.assertTrue(isinstance( + self.jwk256_not_comparable.key, util.ComparableRSAKey)) + self.assertEqual(self.jwk256, self.jwk256_not_comparable) def test_equals(self): self.assertEqual(self.jwk256, self.jwk256) @@ -110,9 +101,8 @@ class JWKRSATest(unittest.TestCase): def test_load(self): from acme.jose.jwk import JWKRSA - self.assertEqual( - self.private, JWKRSA.load(pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'rsa256_key.pem')))) + self.assertEqual(self.private, JWKRSA.load( + test_util.load_vector('rsa256_key.pem'))) def test_public_key(self): self.assertEqual(self.jwk256, self.private.public_key()) diff --git a/acme/jose/jws_test.py b/acme/jose/jws_test.py index ecfb11be7..7a3e8cb83 100644 --- a/acme/jose/jws_test.py +++ b/acme/jose/jws_test.py @@ -1,28 +1,20 @@ """Tests for acme.jose.jws.""" import base64 -import os -import pkg_resources import unittest -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization import mock import OpenSSL +from acme import test_util + from acme.jose import b64 from acme.jose import errors from acme.jose import jwa from acme.jose import jwk -from acme.jose import util -CERT = util.ComparableX509(OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string( - 'acme', 'testdata/cert.pem'))) -RSA512_KEY = serialization.load_pem_private_key( - pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'rsa512_key.pem')), - password=None, backend=default_backend()) +CERT = test_util.load_cert('cert.pem') +KEY = jwk.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) class MediaTypeTest(unittest.TestCase): @@ -112,7 +104,7 @@ class JWSTest(unittest.TestCase): """Tests for acme.jose.jws.JWS.""" def setUp(self): - self.privkey = jwk.JWKRSA(key=RSA512_KEY) + self.privkey = KEY self.pubkey = self.privkey.public_key() from acme.jose.jws import JWS @@ -209,8 +201,7 @@ class JWSTest(unittest.TestCase): class CLITest(unittest.TestCase): def setUp(self): - self.key_path = pkg_resources.resource_filename( - 'acme', os.path.join('testdata', 'rsa512_key.pem')) + self.key_path = test_util.vector_path('rsa512_key.pem') def test_unverified(self): from acme.jose.jws import CLI diff --git a/acme/jose/util_test.py b/acme/jose/util_test.py index 8d36de09b..1bde9ebd9 100644 --- a/acme/jose/util_test.py +++ b/acme/jose/util_test.py @@ -1,31 +1,22 @@ """Tests for acme.jose.util.""" import functools -import os -import pkg_resources import unittest -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization -import OpenSSL +from acme import test_util class ComparableX509Test(unittest.TestCase): """Tests for acme.jose.util.ComparableX509.""" def setUp(self): - from acme.jose.util import ComparableX509 - def _load(method, filename): # pylint: disable=missing-docstring - return ComparableX509(method( - OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string( - 'acme', os.path.join('testdata', filename)))) + # test_util.load_{csr,cert} return ComparableX509 + self.req1 = test_util.load_csr('csr.pem') + self.req2 = test_util.load_csr('csr.pem') + self.req_other = test_util.load_csr('csr-san.pem') - self.req1 = _load(OpenSSL.crypto.load_certificate_request, 'csr.pem') - self.req2 = _load(OpenSSL.crypto.load_certificate_request, 'csr.pem') - self.req_other = _load(OpenSSL.crypto.load_certificate_request, 'csr-san.pem') - - self.cert1 = _load(OpenSSL.crypto.load_certificate, 'cert.pem') - self.cert2 = _load(OpenSSL.crypto.load_certificate, 'cert.pem') - self.cert_other = _load(OpenSSL.crypto.load_certificate, 'cert-san.pem') + self.cert1 = test_util.load_cert('cert.pem') + self.cert2 = test_util.load_cert('cert.pem') + self.cert_other = test_util.load_cert('cert-san.pem') def test_eq(self): self.assertEqual(self.req1, self.req2) @@ -56,19 +47,10 @@ class ComparableRSAKeyTest(unittest.TestCase): """Tests for acme.jose.util.ComparableRSAKey.""" def setUp(self): - from acme.jose.util import ComparableRSAKey - backend = default_backend() - def load_key(): # pylint: disable=missing-docstring - return ComparableRSAKey(serialization.load_pem_private_key( - pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'rsa256_key.pem')), - password=None, backend=backend)) - self.key = load_key() - self.key_same = load_key() - self.key2 = ComparableRSAKey(serialization.load_pem_private_key( - pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'rsa512_key.pem')), - password=None, backend=backend)) + # test_utl.load_rsa_private_key return ComparableRSAKey + self.key = test_util.load_rsa_private_key('rsa256_key.pem') + self.key_same = test_util.load_rsa_private_key('rsa256_key.pem') + self.key2 = test_util.load_rsa_private_key('rsa512_key.pem') def test_getattr_proxy(self): self.assertEqual(256, self.key.key_size) diff --git a/acme/jws_test.py b/acme/jws_test.py index 989a6697a..07361581c 100644 --- a/acme/jws_test.py +++ b/acme/jws_test.py @@ -1,19 +1,12 @@ """Tests for acme.jws.""" -import os -import pkg_resources import unittest -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization - from acme import errors from acme import jose +from acme import test_util -RSA512_KEY = serialization.load_pem_private_key( - pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'rsa512_key.pem')), - password=None, backend=default_backend()) +KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) class HeaderTest(unittest.TestCase): @@ -46,7 +39,7 @@ class JWSTest(unittest.TestCase): """Tests for acme.jws.JWS.""" def setUp(self): - self.privkey = jose.JWKRSA(key=RSA512_KEY) + self.privkey = KEY self.pubkey = self.privkey.public_key() self.nonce = jose.b64encode('Nonce') diff --git a/acme/messages_test.py b/acme/messages_test.py index 71bf25963..2ed0dd669 100644 --- a/acme/messages_test.py +++ b/acme/messages_test.py @@ -1,30 +1,16 @@ """Tests for acme.messages.""" -import os -import pkg_resources import unittest -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization import mock -import OpenSSL from acme import challenges from acme import jose +from acme import test_util -CERT = jose.ComparableX509(OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_ASN1, pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'cert.der')))) -CSR = jose.ComparableX509(OpenSSL.crypto.load_certificate_request( - OpenSSL.crypto.FILETYPE_ASN1, pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'csr.der')))) -KEY = serialization.load_pem_private_key( - pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'rsa512_key.pem')), - password=None, backend=default_backend()) -CERT = jose.ComparableX509(OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_ASN1, pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'cert.der')))) +CERT = test_util.load_cert('cert.der') +CSR = test_util.load_csr('csr.der') +KEY = test_util.load_rsa_private_key('rsa512_key.pem') class ErrorTest(unittest.TestCase): diff --git a/acme/other_test.py b/acme/other_test.py index 25b07bcde..428fca81f 100644 --- a/acme/other_test.py +++ b/acme/other_test.py @@ -1,18 +1,11 @@ """Tests for acme.sig.""" -import os -import pkg_resources import unittest -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization - from acme import jose +from acme import test_util -KEY = serialization.load_pem_private_key( - pkg_resources.resource_string( - 'acme', os.path.join('testdata', 'rsa512_key.pem')), - password=None, backend=default_backend()) +KEY = test_util.load_rsa_private_key('rsa512_key.pem') class SignatureTest(unittest.TestCase): diff --git a/acme/test_util.py b/acme/test_util.py new file mode 100644 index 000000000..cec732625 --- /dev/null +++ b/acme/test_util.py @@ -0,0 +1,55 @@ +"""Test utilities. + +.. warning:: This module is not part of the public API. + +""" +import os +import pkg_resources + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +import OpenSSL + +from acme import jose + + +def vector_path(*names): + """Path to a test vector.""" + return pkg_resources.resource_filename( + __name__, os.path.join('testdata', *names)) + +def load_vector(*names): + """Load contents of a test vector.""" + # luckily, resource_string opens file in binary mode + return pkg_resources.resource_string( + __name__, os.path.join('testdata', *names)) + +def _guess_loader(filename, loader_pem, loader_der): + _, ext = os.path.splitext(filename) + if ext.lower() == '.pem': + return loader_pem + elif ext.lower() == '.der': + return loader_der + else: # pragma: no cover + raise ValueError("Loader could not be recognized based on extension") + +def load_cert(*names): + """Load certificate.""" + loader = _guess_loader( + names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1) + return jose.ComparableX509(OpenSSL.crypto.load_certificate( + loader, load_vector(*names))) + +def load_csr(*names): + """Load certificate request.""" + loader = _guess_loader( + names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1) + return jose.ComparableX509(OpenSSL.crypto.load_certificate_request( + loader, load_vector(*names))) + +def load_rsa_private_key(*names): + """Load RSA private key.""" + loader = _guess_loader(names[-1], serialization.load_pem_private_key, + serialization.load_der_private_key) + return jose.ComparableRSAKey(loader( + load_vector(*names), password=None, backend=default_backend())) diff --git a/acme/testdata/README b/acme/testdata/README index be3d8b2f7..11bca55e5 100644 --- a/acme/testdata/README +++ b/acme/testdata/README @@ -1,3 +1,7 @@ +In order for acme.test_util._guess_loader to work properly, make sure +to use appropriate extension for vector filenames: .pem for PEM and +.der for DER. + The following command has been used to generate test keys: for x in 256 512 1024; do openssl genrsa -out rsa${k}_key.pem $k; done From b0c72410bacc6c443b34dadbef179bb94fe6946f Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 13:40:51 +0000 Subject: [PATCH 23/30] Unified vector loading in letsencrypt. --- acme/test_util.py | 2 + letsencrypt/tests/account_test.py | 8 +- letsencrypt/tests/achallenges_test.py | 13 +-- letsencrypt/tests/acme_util.py | 12 +-- letsencrypt/tests/client_test.py | 10 +-- letsencrypt/tests/crypto_util_test.py | 79 ++++++++----------- letsencrypt/tests/display/ops_test.py | 6 +- letsencrypt/tests/display/revocation_test.py | 18 ++--- letsencrypt/tests/proof_of_possession_test.py | 26 ++---- letsencrypt/tests/renewer_test.py | 17 ++-- letsencrypt/tests/revoker_test.py | 25 ++---- letsencrypt/tests/test_util.py | 1 + setup.py | 2 +- tox.ini | 8 +- 14 files changed, 88 insertions(+), 139 deletions(-) create mode 120000 letsencrypt/tests/test_util.py diff --git a/acme/test_util.py b/acme/test_util.py index cec732625..85289a978 100644 --- a/acme/test_util.py +++ b/acme/test_util.py @@ -1,3 +1,5 @@ +# Symlinked in letsencrypt/tests/test_util.py, casues duplicate-code +# warning that cannot be disabled locally. """Test utilities. .. warning:: This module is not part of the public API. diff --git a/letsencrypt/tests/account_test.py b/letsencrypt/tests/account_test.py index 9a129dbe6..e19940fe8 100644 --- a/letsencrypt/tests/account_test.py +++ b/letsencrypt/tests/account_test.py @@ -1,7 +1,6 @@ """Tests for letsencrypt.account.""" import datetime import os -import pkg_resources import shutil import stat import tempfile @@ -15,9 +14,10 @@ from acme import messages from letsencrypt import errors +from letsencrypt.tests import test_util -KEY = jose.JWKRSA.load(pkg_resources.resource_string( - "letsencrypt.tests", os.path.join("testdata", "rsa512_key_2.pem"))) + +KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key_2.pem")) class AccountTest(unittest.TestCase): @@ -61,7 +61,7 @@ class ReportNewAccountTest(unittest.TestCase): """Tests for letsencrypt.account.report_new_account.""" def setUp(self): - self.config = mock.MagicMock(config_dir='/etc/letsencrypt') + self.config = mock.MagicMock(config_dir="/etc/letsencrypt") reg = messages.Registration.from_data(email="rhino@jungle.io") reg = reg.update(recovery_token="ECCENTRIC INVISIBILITY RHINOCEROS") self.acc = mock.MagicMock(regr=messages.RegistrationResource( diff --git a/letsencrypt/tests/achallenges_test.py b/letsencrypt/tests/achallenges_test.py index 4faa34d12..253cc20d7 100644 --- a/letsencrypt/tests/achallenges_test.py +++ b/letsencrypt/tests/achallenges_test.py @@ -1,17 +1,15 @@ """Tests for letsencrypt.achallenges.""" -import os -import pkg_resources import unittest -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization import OpenSSL from acme import challenges from acme import jose from letsencrypt import crypto_util + from letsencrypt.tests import acme_util +from letsencrypt.tests import test_util class DVSNITest(unittest.TestCase): @@ -21,12 +19,7 @@ class DVSNITest(unittest.TestCase): self.chall = acme_util.chall_to_challb( challenges.DVSNI(r="r_value", nonce="12345ABCDE"), "pending") self.response = challenges.DVSNIResponse() - key = jose.JWKRSA(key=jose.ComparableRSAKey( - serialization.load_pem_private_key( - pkg_resources.resource_string( - "letsencrypt.tests", os.path.join( - "testdata", "rsa512_key.pem")), - password=None, backend=default_backend()))) + key = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) from letsencrypt.achallenges import DVSNI self.achall = DVSNI(challb=self.chall, domain="example.com", key=key) diff --git a/letsencrypt/tests/acme_util.py b/letsencrypt/tests/acme_util.py index 8e19a9ca8..0f504fde3 100644 --- a/letsencrypt/tests/acme_util.py +++ b/letsencrypt/tests/acme_util.py @@ -1,21 +1,15 @@ """ACME utilities for testing.""" import datetime import itertools -import os -import pkg_resources - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization from acme import challenges from acme import jose from acme import messages +from letsencrypt.tests import test_util -KEY = serialization.load_pem_private_key( - pkg_resources.resource_string( - __name__, os.path.join('testdata', 'rsa512_key.pem')), - password=None, backend=default_backend()) + +KEY = test_util.load_rsa_private_key('rsa512_key.pem') # Challenges SIMPLE_HTTP = challenges.SimpleHTTP( diff --git a/letsencrypt/tests/client_test.py b/letsencrypt/tests/client_test.py index 2d1f1d2fd..b992089cc 100644 --- a/letsencrypt/tests/client_test.py +++ b/letsencrypt/tests/client_test.py @@ -1,7 +1,5 @@ """Tests for letsencrypt.client.""" -import os import unittest -import pkg_resources import configobj import OpenSSL @@ -14,11 +12,11 @@ from letsencrypt import configuration from letsencrypt import errors from letsencrypt import le_util +from letsencrypt.tests import test_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")) + +KEY = test_util.load_vector("rsa512_key.pem") +CSR_SAN = test_util.load_vector("csr-san.der") class RegisterTest(unittest.TestCase): diff --git a/letsencrypt/tests/crypto_util_test.py b/letsencrypt/tests/crypto_util_test.py index 2e1e62797..fd3b60c60 100644 --- a/letsencrypt/tests/crypto_util_test.py +++ b/letsencrypt/tests/crypto_util_test.py @@ -1,7 +1,5 @@ """Tests for letsencrypt.crypto_util.""" import logging -import os -import pkg_resources import shutil import tempfile import unittest @@ -9,15 +7,13 @@ import unittest import OpenSSL import mock +from letsencrypt.tests import test_util -RSA256_KEY = pkg_resources.resource_string( - 'letsencrypt.tests', os.path.join('testdata', 'rsa256_key.pem')) -RSA512_KEY = pkg_resources.resource_string( - 'letsencrypt.tests', os.path.join('testdata', 'rsa512_key.pem')) -CERT = pkg_resources.resource_string( - 'letsencrypt.tests', os.path.join('testdata', 'cert.pem')) -SAN_CERT = pkg_resources.resource_string( - 'letsencrypt.tests', os.path.join('testdata', 'cert-san.pem')) + +RSA256_KEY = test_util.load_vector('rsa256_key.pem') +RSA512_KEY = test_util.load_vector('rsa512_key.pem') +CERT = test_util.load_vector('cert.pem') +SAN_CERT = test_util.load_vector('cert-san.pem') class InitSaveKeyTest(unittest.TestCase): @@ -100,21 +96,17 @@ class ValidCSRTest(unittest.TestCase): from letsencrypt.crypto_util import valid_csr return valid_csr(csr) - def _call_testdata(self, name): - return self._call(pkg_resources.resource_string( - __name__, os.path.join('testdata', name))) - def test_valid_pem_true(self): - self.assertTrue(self._call_testdata('csr.pem')) + self.assertTrue(self._call(test_util.load_vector('csr.pem'))) def test_valid_pem_san_true(self): - self.assertTrue(self._call_testdata('csr-san.pem')) + self.assertTrue(self._call(test_util.load_vector('csr-san.pem'))) def test_valid_der_false(self): - self.assertFalse(self._call_testdata('csr.der')) + self.assertFalse(self._call(test_util.load_vector('csr.der'))) def test_valid_der_san_false(self): - self.assertFalse(self._call_testdata('csr-san.der')) + self.assertFalse(self._call(test_util.load_vector('csr-san.der'))) def test_empty_false(self): self.assertFalse(self._call('')) @@ -127,16 +119,17 @@ class CSRMatchesPubkeyTest(unittest.TestCase): """Tests for letsencrypt.crypto_util.csr_matches_pubkey.""" @classmethod - def _call_testdata(cls, name, privkey): + def _call(cls, *args, **kwargs): from letsencrypt.crypto_util import csr_matches_pubkey - return csr_matches_pubkey(pkg_resources.resource_string( - __name__, os.path.join('testdata', name)), privkey) + return csr_matches_pubkey(*args, **kwargs) def test_valid_true(self): - self.assertTrue(self._call_testdata('csr.pem', RSA512_KEY)) + self.assertTrue(self._call( + test_util.load_vector('csr.pem'), RSA512_KEY)) def test_invalid_false(self): - self.assertFalse(self._call_testdata('csr.pem', RSA256_KEY)) + self.assertFalse(self._call( + test_util.load_vector('csr.pem'), RSA256_KEY)) class MakeKeyTest(unittest.TestCase): # pylint: disable=too-few-public-methods @@ -185,50 +178,42 @@ class GetSANsFromCertTest(unittest.TestCase): return get_sans_from_cert(*args, **kwargs) def test_single(self): - self.assertEqual([], self._call(pkg_resources.resource_string( - __name__, os.path.join('testdata', 'cert.pem')))) + self.assertEqual([], self._call(test_util.load_vector('cert.pem'))) def test_san(self): self.assertEqual( ['example.com', 'www.example.com'], - self._call(pkg_resources.resource_string( - __name__, os.path.join('testdata', 'cert-san.pem')))) + self._call(test_util.load_vector('cert-san.pem'))) class GetSANsFromCSRTest(unittest.TestCase): """Tests for letsencrypt.crypto_util.get_sans_from_csr.""" - def test_extract_one_san(self): + + @classmethod + def _call(cls, *args, **kwargs): from letsencrypt.crypto_util import get_sans_from_csr - csr = pkg_resources.resource_string( - __name__, os.path.join('testdata', 'csr.pem')) - self.assertEqual(get_sans_from_csr(csr), ['example.com']) + return get_sans_from_csr(*args, **kwargs) + + def test_extract_one_san(self): + self.assertEqual(['example.com'], self._call( + test_util.load_vector('csr.pem'))) def test_extract_two_sans(self): - from letsencrypt.crypto_util import get_sans_from_csr - csr = pkg_resources.resource_string( - __name__, os.path.join('testdata', 'csr-san.pem')) - self.assertEqual(get_sans_from_csr(csr), ['example.com', - 'www.example.com']) + self.assertEqual(['example.com', 'www.example.com'], self._call( + test_util.load_vector('csr-san.pem'))) def test_extract_six_sans(self): - from letsencrypt.crypto_util import get_sans_from_csr - csr = pkg_resources.resource_string( - __name__, os.path.join('testdata', 'csr-6sans.pem')) - self.assertEqual(get_sans_from_csr(csr), + self.assertEqual(self._call(test_util.load_vector('csr-6sans.pem')), ["example.com", "example.org", "example.net", "example.info", "subdomain.example.com", "other.subdomain.example.com"]) def test_parse_non_csr(self): - from letsencrypt.crypto_util import get_sans_from_csr - self.assertRaises(OpenSSL.crypto.Error, get_sans_from_csr, - "hello there") + self.assertRaises(OpenSSL.crypto.Error, self._call, "hello there") def test_parse_no_sans(self): - from letsencrypt.crypto_util import get_sans_from_csr - csr = pkg_resources.resource_string( - __name__, os.path.join('testdata', 'csr-nosans.pem')) - self.assertEqual([], get_sans_from_csr(csr)) + self.assertEqual( + [], self._call(test_util.load_vector('csr-nosans.pem'))) if __name__ == '__main__': diff --git a/letsencrypt/tests/display/ops_test.py b/letsencrypt/tests/display/ops_test.py index 59b8d9b5c..3a0c627ce 100644 --- a/letsencrypt/tests/display/ops_test.py +++ b/letsencrypt/tests/display/ops_test.py @@ -1,6 +1,5 @@ """Test letsencrypt.display.ops.""" import os -import pkg_resources import sys import tempfile import unittest @@ -16,9 +15,10 @@ from letsencrypt import interfaces from letsencrypt.display import util as display_util +from letsencrypt.tests import test_util -KEY = jose.JWKRSA.load(pkg_resources.resource_string( - "letsencrypt.tests", os.path.join("testdata", "rsa512_key.pem"))) + +KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem")) class ChoosePluginTest(unittest.TestCase): diff --git a/letsencrypt/tests/display/revocation_test.py b/letsencrypt/tests/display/revocation_test.py index cb877499a..6e9763006 100644 --- a/letsencrypt/tests/display/revocation_test.py +++ b/letsencrypt/tests/display/revocation_test.py @@ -1,6 +1,4 @@ """Test :mod:`letsencrypt.display.revocation`.""" -import os -import pkg_resources import sys import unittest @@ -9,15 +7,14 @@ import zope.component from letsencrypt.display import util as display_util +from letsencrypt.tests import test_util + class DisplayCertsTest(unittest.TestCase): def setUp(self): from letsencrypt.revoker import Cert - base_package = "letsencrypt.tests" - self.cert0 = Cert(pkg_resources.resource_filename( - base_package, os.path.join("testdata", "cert.pem"))) - self.cert1 = Cert(pkg_resources.resource_filename( - base_package, os.path.join("testdata", "cert-san.pem"))) + self.cert0 = Cert(test_util.vector_path("cert.pem")) + self.cert1 = Cert(test_util.vector_path("cert-san.pem")) self.certs = [self.cert0, self.cert1] @@ -62,9 +59,7 @@ class MoreInfoCertTest(unittest.TestCase): class SuccessRevocationTest(unittest.TestCase): def setUp(self): from letsencrypt.revoker import Cert - base_package = "letsencrypt.tests" - self.cert = Cert(pkg_resources.resource_filename( - base_package, os.path.join("testdata", "cert.pem"))) + self.cert = Cert(test_util.vector_path("cert.pem")) @classmethod def _call(cls, cert): @@ -82,8 +77,7 @@ class SuccessRevocationTest(unittest.TestCase): class ConfirmRevocationTest(unittest.TestCase): def setUp(self): from letsencrypt.revoker import Cert - self.cert = Cert(pkg_resources.resource_filename( - "letsencrypt.tests", os.path.join("testdata", "cert.pem"))) + self.cert = Cert(test_util.vector_path("cert.pem")) @classmethod def _call(cls, cert): diff --git a/letsencrypt/tests/proof_of_possession_test.py b/letsencrypt/tests/proof_of_possession_test.py index ef0ffc21e..bfe3478d1 100644 --- a/letsencrypt/tests/proof_of_possession_test.py +++ b/letsencrypt/tests/proof_of_possession_test.py @@ -1,11 +1,8 @@ """Tests for letsencrypt.proof_of_possession.""" import os -import pkg_resources import tempfile import unittest -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import serialization import mock from acme import challenges @@ -16,22 +13,15 @@ from letsencrypt import achallenges from letsencrypt import proof_of_possession from letsencrypt.display import util as display_util +from letsencrypt.tests import test_util -BASE_PACKAGE = "letsencrypt.tests" -CERT0_PATH = pkg_resources.resource_filename( - BASE_PACKAGE, os.path.join("testdata", "cert.der")) -CERT2_PATH = pkg_resources.resource_filename( - BASE_PACKAGE, os.path.join("testdata", "dsa_cert.pem")) -CERT2_KEY_PATH = pkg_resources.resource_filename( - BASE_PACKAGE, os.path.join("testdata", "dsa512_key.pem")) -CERT3_PATH = pkg_resources.resource_filename( - BASE_PACKAGE, os.path.join("testdata", "matching_cert.pem")) -CERT3_KEY_PATH = pkg_resources.resource_filename( - BASE_PACKAGE, os.path.join("testdata", "rsa512_key_2.pem")) -with open(CERT3_KEY_PATH) as cert3_file: - CERT3_KEY = serialization.load_pem_private_key( - cert3_file.read(), password=None, - backend=default_backend()).public_key() + +CERT0_PATH = test_util.vector_path("cert.der") +CERT2_PATH = test_util.vector_path("dsa_cert.pem") +CERT2_KEY_PATH = test_util.vector_path("dsa512_key.pem") +CERT3_PATH = test_util.vector_path("matching_cert.pem") +CERT3_KEY_PATH = test_util.vector_path("rsa512_key_2.pem") +CERT3_KEY = test_util.load_rsa_private_key("rsa512_key_2.pem").public_key() class ProofOfPossessionTest(unittest.TestCase): diff --git a/letsencrypt/tests/renewer_test.py b/letsencrypt/tests/renewer_test.py index 4ee7cc0e8..65bfce314 100644 --- a/letsencrypt/tests/renewer_test.py +++ b/letsencrypt/tests/renewer_test.py @@ -2,22 +2,20 @@ import datetime import os import tempfile -import pkg_resources import shutil import unittest import configobj import mock -import OpenSSL import pytz from letsencrypt import configuration from letsencrypt.storage import ALL_FOUR +from letsencrypt.tests import test_util -CERT = OpenSSL.crypto.load_certificate( - OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string( - 'letsencrypt.tests', os.path.join('testdata', 'cert.pem'))) + +CERT = test_util.load_cert('cert.pem') def unlink_all(rc_object): @@ -295,8 +293,7 @@ class RenewableCertTests(unittest.TestCase): self.assertFalse(self.test_rc.has_pending_deployment()) def _test_notafterbefore(self, function, timestamp): - test_cert = pkg_resources.resource_string( - "letsencrypt.tests", os.path.join("testdata", "cert.pem")) + 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: @@ -319,8 +316,7 @@ class RenewableCertTests(unittest.TestCase): def test_time_interval_judgments(self, mock_datetime): """Test should_autodeploy() and should_autorenew() on the basis of expiry time windows.""" - test_cert = pkg_resources.resource_string( - "letsencrypt.tests", os.path.join("testdata", "cert.pem")) + test_cert = test_util.load_vector("cert.pem") for kind in ALL_FOUR: where = getattr(self.test_rc, kind) os.symlink(os.path.join("..", "..", "archive", "example.org", @@ -561,8 +557,7 @@ class RenewableCertTests(unittest.TestCase): def test_renew(self, mock_c, mock_acc_storage, mock_pd): from letsencrypt import renewer - test_cert = pkg_resources.resource_string( - "letsencrypt.tests", os.path.join("testdata", "cert-san.pem")) + test_cert = test_util.load_vector("cert-san.pem") for kind in ALL_FOUR: os.symlink(os.path.join("..", "..", "archive", "example.org", kind + "1.pem"), diff --git a/letsencrypt/tests/revoker_test.py b/letsencrypt/tests/revoker_test.py index 92eeaf92d..87dab4eb8 100644 --- a/letsencrypt/tests/revoker_test.py +++ b/letsencrypt/tests/revoker_test.py @@ -1,7 +1,6 @@ """Test letsencrypt.revoker.""" import csv import os -import pkg_resources import shutil import tempfile import unittest @@ -13,10 +12,11 @@ from letsencrypt import errors from letsencrypt import le_util from letsencrypt.display import util as display_util +from letsencrypt.tests import test_util + KEY = OpenSSL.crypto.load_privatekey( - OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string( - __name__, os.path.join("testdata", "rsa512_key.pem"))) + OpenSSL.crypto.FILETYPE_PEM, test_util.load_vector("rsa512_key.pem")) class RevokerBase(unittest.TestCase): # pylint: disable=too-few-public-methods @@ -98,8 +98,7 @@ class RevokerTest(RevokerBase): def test_revoke_by_wrong_key(self, mock_display, mock_acme): mock_display().confirm_revocation.return_value = True - key_path = pkg_resources.resource_filename( - "letsencrypt.tests", os.path.join("testdata", "rsa256_key.pem")) + key_path = test_util.vector_path("rsa256_key.pem") wrong_key = le_util.Key(key_path, open(key_path).read()) self.revoker.revoke_from_key(wrong_key) @@ -395,22 +394,14 @@ class CertTest(unittest.TestCase): def create_revoker_certs(): """Create a few revoker.Cert objects.""" + cert0_path = test_util.vector_path("cert.pem") + cert1_path = test_util.vector_path("cert-san.pem") + key_path = test_util.vector_path("rsa512_key.pem") + from letsencrypt.revoker import Cert - - base_package = "letsencrypt.tests" - - cert0_path = pkg_resources.resource_filename( - base_package, os.path.join("testdata", "cert.pem")) - - cert1_path = pkg_resources.resource_filename( - base_package, os.path.join("testdata", "cert-san.pem")) - cert0 = Cert(cert0_path) cert1 = Cert(cert1_path) - key_path = pkg_resources.resource_filename( - base_package, os.path.join("testdata", "rsa512_key.pem")) - return [cert0_path, cert1_path], [cert0, cert1], key_path diff --git a/letsencrypt/tests/test_util.py b/letsencrypt/tests/test_util.py new file mode 120000 index 000000000..d46ad3bd4 --- /dev/null +++ b/letsencrypt/tests/test_util.py @@ -0,0 +1 @@ +../../acme/test_util.py \ No newline at end of file diff --git a/setup.py b/setup.py index b2bd0e652..0c74b296c 100644 --- a/setup.py +++ b/setup.py @@ -187,6 +187,6 @@ setup( ], }, - zip_safe=False, + zip_safe=False, # letsencrypt/tests/test_util.py is a symlink! include_package_data=True, ) diff --git a/tox.ini b/tox.ini index aed60f454..2f20e5799 100644 --- a/tox.ini +++ b/tox.ini @@ -28,6 +28,12 @@ commands = [testenv:lint] # recent versions of pylint do not support Python 2.6 (#97, #187) basepython = python2.7 +# separating into multiple invocations disables cross package +# duplicate code checking; if one of the commands fails, others will +# continue, but tox return code will reflect previous error commands = pip install -r requirements.txt -e .[dev] - pylint --rcfile=.pylintrc letsencrypt acme letsencrypt_apache letsencrypt_nginx + pylint --rcfile=.pylintrc letsencrypt + pylint --rcfile=.pylintrc acme + pylint --rcfile=.pylintrc letsencrypt_apache + pylint --rcfile=.pylintrc letsencrypt_nginx From 2f9cd6880769d345c1c5afff800e6ea794e33e45 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 14:14:52 +0000 Subject: [PATCH 24/30] Move acme and plugins to respective subdirectories. for x in acme letsencrypt_nginx letsencrypt_apache; do git mv $x _$x; mkdir $x; git mv _$x $x/$x; done --- acme/{ => acme}/__init__.py | 0 acme/{ => acme}/challenges.py | 0 acme/{ => acme}/challenges_test.py | 0 acme/{ => acme}/client.py | 0 acme/{ => acme}/client_test.py | 0 acme/{ => acme}/errors.py | 0 acme/{ => acme}/errors_test.py | 0 acme/{ => acme}/fields.py | 0 acme/{ => acme}/fields_test.py | 0 acme/{ => acme}/interfaces.py | 0 acme/{ => acme}/jose/__init__.py | 0 acme/{ => acme}/jose/b64.py | 0 acme/{ => acme}/jose/b64_test.py | 0 acme/{ => acme}/jose/errors.py | 0 acme/{ => acme}/jose/errors_test.py | 0 acme/{ => acme}/jose/interfaces.py | 0 acme/{ => acme}/jose/interfaces_test.py | 0 acme/{ => acme}/jose/json_util.py | 0 acme/{ => acme}/jose/json_util_test.py | 0 acme/{ => acme}/jose/jwa.py | 0 acme/{ => acme}/jose/jwa_test.py | 0 acme/{ => acme}/jose/jwk.py | 0 acme/{ => acme}/jose/jwk_test.py | 0 acme/{ => acme}/jose/jws.py | 0 acme/{ => acme}/jose/jws_test.py | 0 acme/{ => acme}/jose/util.py | 0 acme/{ => acme}/jose/util_test.py | 0 acme/{ => acme}/jws.py | 0 acme/{ => acme}/jws_test.py | 0 acme/{ => acme}/messages.py | 0 acme/{ => acme}/messages_test.py | 0 acme/{ => acme}/other.py | 0 acme/{ => acme}/other_test.py | 0 acme/{ => acme}/test_util.py | 0 acme/{ => acme}/testdata/README | 0 acme/{ => acme}/testdata/cert-san.pem | 0 acme/{ => acme}/testdata/cert.der | Bin acme/{ => acme}/testdata/cert.pem | 0 acme/{ => acme}/testdata/csr-san.pem | 0 acme/{ => acme}/testdata/csr.der | Bin acme/{ => acme}/testdata/csr.pem | 0 acme/{ => acme}/testdata/dsa512_key.pem | 0 acme/{ => acme}/testdata/rsa1024_key.pem | 0 acme/{ => acme}/testdata/rsa256_key.pem | 0 acme/{ => acme}/testdata/rsa512_key.pem | 0 .../{ => letsencrypt_apache}/__init__.py | 0 .../{ => letsencrypt_apache}/augeas_configurator.py | 0 .../{ => letsencrypt_apache}/configurator.py | 0 .../{ => letsencrypt_apache}/constants.py | 0 .../{ => letsencrypt_apache}/display_ops.py | 0 .../{ => letsencrypt_apache}/dvsni.py | 0 letsencrypt_apache/{ => letsencrypt_apache}/obj.py | 0 .../options-ssl-apache.conf | 0 .../{ => letsencrypt_apache}/parser.py | 0 .../{ => letsencrypt_apache}/tests/__init__.py | 0 .../tests/configurator_test.py | 0 .../tests/display_ops_test.py | 0 .../{ => letsencrypt_apache}/tests/dvsni_test.py | 0 .../{ => letsencrypt_apache}/tests/obj_test.py | 0 .../{ => letsencrypt_apache}/tests/parser_test.py | 0 .../default_vhost/apache2/apache2.conf | 0 .../conf-available/other-vhosts-access-log.conf | 0 .../apache2/conf-available/security.conf | 0 .../apache2/conf-available/serve-cgi-bin.conf | 0 .../conf-enabled/other-vhosts-access-log.conf | 0 .../apache2/conf-enabled/security.conf | 0 .../apache2/conf-enabled/serve-cgi-bin.conf | 0 .../debian_apache_2_4/default_vhost/apache2/envvars | 0 .../default_vhost/apache2/mods-available/ssl.conf | 0 .../default_vhost/apache2/mods-available/ssl.load | 0 .../default_vhost/apache2/ports.conf | 0 .../apache2/sites-available/000-default.conf | 0 .../apache2/sites-available/default-ssl.conf | 0 .../apache2/sites-enabled/000-default.conf | 0 .../testdata/debian_apache_2_4/default_vhost/sites | 0 .../two_vhost_80/apache2/apache2.conf | 0 .../conf-available/other-vhosts-access-log.conf | 0 .../apache2/conf-available/security.conf | 0 .../apache2/conf-available/serve-cgi-bin.conf | 0 .../conf-enabled/other-vhosts-access-log.conf | 0 .../two_vhost_80/apache2/conf-enabled/security.conf | 0 .../apache2/conf-enabled/serve-cgi-bin.conf | 0 .../debian_apache_2_4/two_vhost_80/apache2/envvars | 0 .../two_vhost_80/apache2/mods-available/ssl.conf | 0 .../two_vhost_80/apache2/mods-available/ssl.load | 0 .../two_vhost_80/apache2/ports.conf | 0 .../apache2/sites-available/000-default.conf | 0 .../apache2/sites-available/default-ssl.conf | 0 .../apache2/sites-available/encryption-example.conf | 0 .../apache2/sites-available/letsencrypt.conf | 0 .../apache2/sites-enabled/000-default.conf | 0 .../apache2/sites-enabled/encryption-example.conf | 0 .../apache2/sites-enabled/letsencrypt.conf | 0 .../testdata/debian_apache_2_4/two_vhost_80/sites | 0 .../{ => letsencrypt_apache}/tests/util.py | 0 .../{ => letsencrypt_nginx}/__init__.py | 0 .../{ => letsencrypt_nginx}/configurator.py | 0 .../{ => letsencrypt_nginx}/constants.py | 0 letsencrypt_nginx/{ => letsencrypt_nginx}/dvsni.py | 0 .../{ => letsencrypt_nginx}/nginxparser.py | 0 letsencrypt_nginx/{ => letsencrypt_nginx}/obj.py | 0 .../{ => letsencrypt_nginx}/options-ssl-nginx.conf | 0 letsencrypt_nginx/{ => letsencrypt_nginx}/parser.py | 0 .../{ => letsencrypt_nginx}/tests/__init__.py | 0 .../tests/configurator_test.py | 0 .../{ => letsencrypt_nginx}/tests/dvsni_test.py | 0 .../tests/nginxparser_test.py | 0 .../{ => letsencrypt_nginx}/tests/obj_test.py | 0 .../{ => letsencrypt_nginx}/tests/parser_test.py | 0 .../tests/testdata/etc_nginx/edge_cases.conf | 0 .../tests/testdata/etc_nginx/foo.conf | 0 .../tests/testdata/etc_nginx/mime.types | 0 .../tests/testdata/etc_nginx/nginx.conf | 0 .../tests/testdata/etc_nginx/nginx.new.conf | 0 .../tests/testdata/etc_nginx/server.conf | 0 .../tests/testdata/etc_nginx/sites-enabled/default | 0 .../testdata/etc_nginx/sites-enabled/example.com | 0 .../default_vhost/nginx/fastcgi_params | 0 .../ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf | 0 .../ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win | 0 .../default_vhost/nginx/mime.types | 0 .../default_vhost/nginx/naxsi-ui.conf.1.4.1 | 0 .../default_vhost/nginx/naxsi.rules | 0 .../default_vhost/nginx/naxsi_core.rules | 0 .../default_vhost/nginx/nginx.conf | 0 .../default_vhost/nginx/proxy_params | 0 .../default_vhost/nginx/scgi_params | 0 .../default_vhost/nginx/sites-available/default | 0 .../default_vhost/nginx/sites-enabled/default | 0 .../default_vhost/nginx/uwsgi_params | 0 .../ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf | 0 .../{ => letsencrypt_nginx}/tests/util.py | 0 132 files changed, 0 insertions(+), 0 deletions(-) rename acme/{ => acme}/__init__.py (100%) rename acme/{ => acme}/challenges.py (100%) rename acme/{ => acme}/challenges_test.py (100%) rename acme/{ => acme}/client.py (100%) rename acme/{ => acme}/client_test.py (100%) rename acme/{ => acme}/errors.py (100%) rename acme/{ => acme}/errors_test.py (100%) rename acme/{ => acme}/fields.py (100%) rename acme/{ => acme}/fields_test.py (100%) rename acme/{ => acme}/interfaces.py (100%) rename acme/{ => acme}/jose/__init__.py (100%) rename acme/{ => acme}/jose/b64.py (100%) rename acme/{ => acme}/jose/b64_test.py (100%) rename acme/{ => acme}/jose/errors.py (100%) rename acme/{ => acme}/jose/errors_test.py (100%) rename acme/{ => acme}/jose/interfaces.py (100%) rename acme/{ => acme}/jose/interfaces_test.py (100%) rename acme/{ => acme}/jose/json_util.py (100%) rename acme/{ => acme}/jose/json_util_test.py (100%) rename acme/{ => acme}/jose/jwa.py (100%) rename acme/{ => acme}/jose/jwa_test.py (100%) rename acme/{ => acme}/jose/jwk.py (100%) rename acme/{ => acme}/jose/jwk_test.py (100%) rename acme/{ => acme}/jose/jws.py (100%) rename acme/{ => acme}/jose/jws_test.py (100%) rename acme/{ => acme}/jose/util.py (100%) rename acme/{ => acme}/jose/util_test.py (100%) rename acme/{ => acme}/jws.py (100%) rename acme/{ => acme}/jws_test.py (100%) rename acme/{ => acme}/messages.py (100%) rename acme/{ => acme}/messages_test.py (100%) rename acme/{ => acme}/other.py (100%) rename acme/{ => acme}/other_test.py (100%) rename acme/{ => acme}/test_util.py (100%) rename acme/{ => acme}/testdata/README (100%) rename acme/{ => acme}/testdata/cert-san.pem (100%) rename acme/{ => acme}/testdata/cert.der (100%) rename acme/{ => acme}/testdata/cert.pem (100%) rename acme/{ => acme}/testdata/csr-san.pem (100%) rename acme/{ => acme}/testdata/csr.der (100%) rename acme/{ => acme}/testdata/csr.pem (100%) rename acme/{ => acme}/testdata/dsa512_key.pem (100%) rename acme/{ => acme}/testdata/rsa1024_key.pem (100%) rename acme/{ => acme}/testdata/rsa256_key.pem (100%) rename acme/{ => acme}/testdata/rsa512_key.pem (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/__init__.py (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/augeas_configurator.py (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/configurator.py (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/constants.py (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/display_ops.py (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/dvsni.py (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/obj.py (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/options-ssl-apache.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/parser.py (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/__init__.py (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/configurator_test.py (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/display_ops_test.py (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/dvsni_test.py (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/obj_test.py (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/parser_test.py (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/apache2.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/other-vhosts-access-log.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/security.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/serve-cgi-bin.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/other-vhosts-access-log.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/security.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/serve-cgi-bin.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/envvars (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.load (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/ports.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/000-default.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/default-ssl.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-enabled/000-default.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/default_vhost/sites (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/apache2.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/other-vhosts-access-log.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/security.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/serve-cgi-bin.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/other-vhosts-access-log.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/security.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/serve-cgi-bin.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/envvars (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.load (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/ports.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/000-default.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/default-ssl.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/encryption-example.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/000-default.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/encryption-example.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/letsencrypt.conf (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/testdata/debian_apache_2_4/two_vhost_80/sites (100%) rename letsencrypt_apache/{ => letsencrypt_apache}/tests/util.py (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/__init__.py (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/configurator.py (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/constants.py (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/dvsni.py (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/nginxparser.py (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/obj.py (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/options-ssl-nginx.conf (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/parser.py (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/__init__.py (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/configurator_test.py (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/dvsni_test.py (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/nginxparser_test.py (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/obj_test.py (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/parser_test.py (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/edge_cases.conf (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/foo.conf (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/mime.types (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/nginx.conf (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/nginx.new.conf (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/server.conf (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/sites-enabled/default (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/sites-enabled/example.com (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf (100%) rename letsencrypt_nginx/{ => letsencrypt_nginx}/tests/util.py (100%) diff --git a/acme/__init__.py b/acme/acme/__init__.py similarity index 100% rename from acme/__init__.py rename to acme/acme/__init__.py diff --git a/acme/challenges.py b/acme/acme/challenges.py similarity index 100% rename from acme/challenges.py rename to acme/acme/challenges.py diff --git a/acme/challenges_test.py b/acme/acme/challenges_test.py similarity index 100% rename from acme/challenges_test.py rename to acme/acme/challenges_test.py diff --git a/acme/client.py b/acme/acme/client.py similarity index 100% rename from acme/client.py rename to acme/acme/client.py diff --git a/acme/client_test.py b/acme/acme/client_test.py similarity index 100% rename from acme/client_test.py rename to acme/acme/client_test.py diff --git a/acme/errors.py b/acme/acme/errors.py similarity index 100% rename from acme/errors.py rename to acme/acme/errors.py diff --git a/acme/errors_test.py b/acme/acme/errors_test.py similarity index 100% rename from acme/errors_test.py rename to acme/acme/errors_test.py diff --git a/acme/fields.py b/acme/acme/fields.py similarity index 100% rename from acme/fields.py rename to acme/acme/fields.py diff --git a/acme/fields_test.py b/acme/acme/fields_test.py similarity index 100% rename from acme/fields_test.py rename to acme/acme/fields_test.py diff --git a/acme/interfaces.py b/acme/acme/interfaces.py similarity index 100% rename from acme/interfaces.py rename to acme/acme/interfaces.py diff --git a/acme/jose/__init__.py b/acme/acme/jose/__init__.py similarity index 100% rename from acme/jose/__init__.py rename to acme/acme/jose/__init__.py diff --git a/acme/jose/b64.py b/acme/acme/jose/b64.py similarity index 100% rename from acme/jose/b64.py rename to acme/acme/jose/b64.py diff --git a/acme/jose/b64_test.py b/acme/acme/jose/b64_test.py similarity index 100% rename from acme/jose/b64_test.py rename to acme/acme/jose/b64_test.py diff --git a/acme/jose/errors.py b/acme/acme/jose/errors.py similarity index 100% rename from acme/jose/errors.py rename to acme/acme/jose/errors.py diff --git a/acme/jose/errors_test.py b/acme/acme/jose/errors_test.py similarity index 100% rename from acme/jose/errors_test.py rename to acme/acme/jose/errors_test.py diff --git a/acme/jose/interfaces.py b/acme/acme/jose/interfaces.py similarity index 100% rename from acme/jose/interfaces.py rename to acme/acme/jose/interfaces.py diff --git a/acme/jose/interfaces_test.py b/acme/acme/jose/interfaces_test.py similarity index 100% rename from acme/jose/interfaces_test.py rename to acme/acme/jose/interfaces_test.py diff --git a/acme/jose/json_util.py b/acme/acme/jose/json_util.py similarity index 100% rename from acme/jose/json_util.py rename to acme/acme/jose/json_util.py diff --git a/acme/jose/json_util_test.py b/acme/acme/jose/json_util_test.py similarity index 100% rename from acme/jose/json_util_test.py rename to acme/acme/jose/json_util_test.py diff --git a/acme/jose/jwa.py b/acme/acme/jose/jwa.py similarity index 100% rename from acme/jose/jwa.py rename to acme/acme/jose/jwa.py diff --git a/acme/jose/jwa_test.py b/acme/acme/jose/jwa_test.py similarity index 100% rename from acme/jose/jwa_test.py rename to acme/acme/jose/jwa_test.py diff --git a/acme/jose/jwk.py b/acme/acme/jose/jwk.py similarity index 100% rename from acme/jose/jwk.py rename to acme/acme/jose/jwk.py diff --git a/acme/jose/jwk_test.py b/acme/acme/jose/jwk_test.py similarity index 100% rename from acme/jose/jwk_test.py rename to acme/acme/jose/jwk_test.py diff --git a/acme/jose/jws.py b/acme/acme/jose/jws.py similarity index 100% rename from acme/jose/jws.py rename to acme/acme/jose/jws.py diff --git a/acme/jose/jws_test.py b/acme/acme/jose/jws_test.py similarity index 100% rename from acme/jose/jws_test.py rename to acme/acme/jose/jws_test.py diff --git a/acme/jose/util.py b/acme/acme/jose/util.py similarity index 100% rename from acme/jose/util.py rename to acme/acme/jose/util.py diff --git a/acme/jose/util_test.py b/acme/acme/jose/util_test.py similarity index 100% rename from acme/jose/util_test.py rename to acme/acme/jose/util_test.py diff --git a/acme/jws.py b/acme/acme/jws.py similarity index 100% rename from acme/jws.py rename to acme/acme/jws.py diff --git a/acme/jws_test.py b/acme/acme/jws_test.py similarity index 100% rename from acme/jws_test.py rename to acme/acme/jws_test.py diff --git a/acme/messages.py b/acme/acme/messages.py similarity index 100% rename from acme/messages.py rename to acme/acme/messages.py diff --git a/acme/messages_test.py b/acme/acme/messages_test.py similarity index 100% rename from acme/messages_test.py rename to acme/acme/messages_test.py diff --git a/acme/other.py b/acme/acme/other.py similarity index 100% rename from acme/other.py rename to acme/acme/other.py diff --git a/acme/other_test.py b/acme/acme/other_test.py similarity index 100% rename from acme/other_test.py rename to acme/acme/other_test.py diff --git a/acme/test_util.py b/acme/acme/test_util.py similarity index 100% rename from acme/test_util.py rename to acme/acme/test_util.py diff --git a/acme/testdata/README b/acme/acme/testdata/README similarity index 100% rename from acme/testdata/README rename to acme/acme/testdata/README diff --git a/acme/testdata/cert-san.pem b/acme/acme/testdata/cert-san.pem similarity index 100% rename from acme/testdata/cert-san.pem rename to acme/acme/testdata/cert-san.pem diff --git a/acme/testdata/cert.der b/acme/acme/testdata/cert.der similarity index 100% rename from acme/testdata/cert.der rename to acme/acme/testdata/cert.der diff --git a/acme/testdata/cert.pem b/acme/acme/testdata/cert.pem similarity index 100% rename from acme/testdata/cert.pem rename to acme/acme/testdata/cert.pem diff --git a/acme/testdata/csr-san.pem b/acme/acme/testdata/csr-san.pem similarity index 100% rename from acme/testdata/csr-san.pem rename to acme/acme/testdata/csr-san.pem diff --git a/acme/testdata/csr.der b/acme/acme/testdata/csr.der similarity index 100% rename from acme/testdata/csr.der rename to acme/acme/testdata/csr.der diff --git a/acme/testdata/csr.pem b/acme/acme/testdata/csr.pem similarity index 100% rename from acme/testdata/csr.pem rename to acme/acme/testdata/csr.pem diff --git a/acme/testdata/dsa512_key.pem b/acme/acme/testdata/dsa512_key.pem similarity index 100% rename from acme/testdata/dsa512_key.pem rename to acme/acme/testdata/dsa512_key.pem diff --git a/acme/testdata/rsa1024_key.pem b/acme/acme/testdata/rsa1024_key.pem similarity index 100% rename from acme/testdata/rsa1024_key.pem rename to acme/acme/testdata/rsa1024_key.pem diff --git a/acme/testdata/rsa256_key.pem b/acme/acme/testdata/rsa256_key.pem similarity index 100% rename from acme/testdata/rsa256_key.pem rename to acme/acme/testdata/rsa256_key.pem diff --git a/acme/testdata/rsa512_key.pem b/acme/acme/testdata/rsa512_key.pem similarity index 100% rename from acme/testdata/rsa512_key.pem rename to acme/acme/testdata/rsa512_key.pem diff --git a/letsencrypt_apache/__init__.py b/letsencrypt_apache/letsencrypt_apache/__init__.py similarity index 100% rename from letsencrypt_apache/__init__.py rename to letsencrypt_apache/letsencrypt_apache/__init__.py diff --git a/letsencrypt_apache/augeas_configurator.py b/letsencrypt_apache/letsencrypt_apache/augeas_configurator.py similarity index 100% rename from letsencrypt_apache/augeas_configurator.py rename to letsencrypt_apache/letsencrypt_apache/augeas_configurator.py diff --git a/letsencrypt_apache/configurator.py b/letsencrypt_apache/letsencrypt_apache/configurator.py similarity index 100% rename from letsencrypt_apache/configurator.py rename to letsencrypt_apache/letsencrypt_apache/configurator.py diff --git a/letsencrypt_apache/constants.py b/letsencrypt_apache/letsencrypt_apache/constants.py similarity index 100% rename from letsencrypt_apache/constants.py rename to letsencrypt_apache/letsencrypt_apache/constants.py diff --git a/letsencrypt_apache/display_ops.py b/letsencrypt_apache/letsencrypt_apache/display_ops.py similarity index 100% rename from letsencrypt_apache/display_ops.py rename to letsencrypt_apache/letsencrypt_apache/display_ops.py diff --git a/letsencrypt_apache/dvsni.py b/letsencrypt_apache/letsencrypt_apache/dvsni.py similarity index 100% rename from letsencrypt_apache/dvsni.py rename to letsencrypt_apache/letsencrypt_apache/dvsni.py diff --git a/letsencrypt_apache/obj.py b/letsencrypt_apache/letsencrypt_apache/obj.py similarity index 100% rename from letsencrypt_apache/obj.py rename to letsencrypt_apache/letsencrypt_apache/obj.py diff --git a/letsencrypt_apache/options-ssl-apache.conf b/letsencrypt_apache/letsencrypt_apache/options-ssl-apache.conf similarity index 100% rename from letsencrypt_apache/options-ssl-apache.conf rename to letsencrypt_apache/letsencrypt_apache/options-ssl-apache.conf diff --git a/letsencrypt_apache/parser.py b/letsencrypt_apache/letsencrypt_apache/parser.py similarity index 100% rename from letsencrypt_apache/parser.py rename to letsencrypt_apache/letsencrypt_apache/parser.py diff --git a/letsencrypt_apache/tests/__init__.py b/letsencrypt_apache/letsencrypt_apache/tests/__init__.py similarity index 100% rename from letsencrypt_apache/tests/__init__.py rename to letsencrypt_apache/letsencrypt_apache/tests/__init__.py diff --git a/letsencrypt_apache/tests/configurator_test.py b/letsencrypt_apache/letsencrypt_apache/tests/configurator_test.py similarity index 100% rename from letsencrypt_apache/tests/configurator_test.py rename to letsencrypt_apache/letsencrypt_apache/tests/configurator_test.py diff --git a/letsencrypt_apache/tests/display_ops_test.py b/letsencrypt_apache/letsencrypt_apache/tests/display_ops_test.py similarity index 100% rename from letsencrypt_apache/tests/display_ops_test.py rename to letsencrypt_apache/letsencrypt_apache/tests/display_ops_test.py diff --git a/letsencrypt_apache/tests/dvsni_test.py b/letsencrypt_apache/letsencrypt_apache/tests/dvsni_test.py similarity index 100% rename from letsencrypt_apache/tests/dvsni_test.py rename to letsencrypt_apache/letsencrypt_apache/tests/dvsni_test.py diff --git a/letsencrypt_apache/tests/obj_test.py b/letsencrypt_apache/letsencrypt_apache/tests/obj_test.py similarity index 100% rename from letsencrypt_apache/tests/obj_test.py rename to letsencrypt_apache/letsencrypt_apache/tests/obj_test.py diff --git a/letsencrypt_apache/tests/parser_test.py b/letsencrypt_apache/letsencrypt_apache/tests/parser_test.py similarity index 100% rename from letsencrypt_apache/tests/parser_test.py rename to letsencrypt_apache/letsencrypt_apache/tests/parser_test.py diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/apache2.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/apache2.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/apache2.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/apache2.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/other-vhosts-access-log.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/other-vhosts-access-log.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/other-vhosts-access-log.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/other-vhosts-access-log.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/security.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/security.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/security.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/security.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/serve-cgi-bin.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/serve-cgi-bin.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/serve-cgi-bin.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/serve-cgi-bin.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/other-vhosts-access-log.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/other-vhosts-access-log.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/other-vhosts-access-log.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/other-vhosts-access-log.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/security.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/security.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/security.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/security.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/serve-cgi-bin.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/serve-cgi-bin.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/serve-cgi-bin.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-enabled/serve-cgi-bin.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/envvars b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/envvars similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/envvars rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/envvars diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.load b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.load similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.load rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.load diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/ports.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/ports.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/ports.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/ports.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/000-default.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/000-default.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/000-default.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/000-default.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/default-ssl.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/default-ssl.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/default-ssl.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/default-ssl.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-enabled/000-default.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-enabled/000-default.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-enabled/000-default.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-enabled/000-default.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/sites b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/sites similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/sites rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/default_vhost/sites diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/apache2.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/apache2.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/apache2.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/apache2.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/other-vhosts-access-log.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/other-vhosts-access-log.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/other-vhosts-access-log.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/other-vhosts-access-log.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/security.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/security.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/security.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/security.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/serve-cgi-bin.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/serve-cgi-bin.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/serve-cgi-bin.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-available/serve-cgi-bin.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/other-vhosts-access-log.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/other-vhosts-access-log.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/other-vhosts-access-log.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/other-vhosts-access-log.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/security.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/security.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/security.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/security.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/serve-cgi-bin.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/serve-cgi-bin.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/serve-cgi-bin.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/conf-enabled/serve-cgi-bin.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/envvars b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/envvars similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/envvars rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/envvars diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.load b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.load similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.load rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/mods-available/ssl.load diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/ports.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/ports.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/ports.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/ports.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/000-default.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/000-default.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/000-default.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/000-default.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/default-ssl.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/default-ssl.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/default-ssl.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/default-ssl.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/encryption-example.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/encryption-example.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/encryption-example.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/encryption-example.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-available/letsencrypt.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/000-default.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/000-default.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/000-default.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/000-default.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/encryption-example.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/encryption-example.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/encryption-example.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/encryption-example.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/letsencrypt.conf b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/letsencrypt.conf similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/letsencrypt.conf rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/apache2/sites-enabled/letsencrypt.conf diff --git a/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/sites b/letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/sites similarity index 100% rename from letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/sites rename to letsencrypt_apache/letsencrypt_apache/tests/testdata/debian_apache_2_4/two_vhost_80/sites diff --git a/letsencrypt_apache/tests/util.py b/letsencrypt_apache/letsencrypt_apache/tests/util.py similarity index 100% rename from letsencrypt_apache/tests/util.py rename to letsencrypt_apache/letsencrypt_apache/tests/util.py diff --git a/letsencrypt_nginx/__init__.py b/letsencrypt_nginx/letsencrypt_nginx/__init__.py similarity index 100% rename from letsencrypt_nginx/__init__.py rename to letsencrypt_nginx/letsencrypt_nginx/__init__.py diff --git a/letsencrypt_nginx/configurator.py b/letsencrypt_nginx/letsencrypt_nginx/configurator.py similarity index 100% rename from letsencrypt_nginx/configurator.py rename to letsencrypt_nginx/letsencrypt_nginx/configurator.py diff --git a/letsencrypt_nginx/constants.py b/letsencrypt_nginx/letsencrypt_nginx/constants.py similarity index 100% rename from letsencrypt_nginx/constants.py rename to letsencrypt_nginx/letsencrypt_nginx/constants.py diff --git a/letsencrypt_nginx/dvsni.py b/letsencrypt_nginx/letsencrypt_nginx/dvsni.py similarity index 100% rename from letsencrypt_nginx/dvsni.py rename to letsencrypt_nginx/letsencrypt_nginx/dvsni.py diff --git a/letsencrypt_nginx/nginxparser.py b/letsencrypt_nginx/letsencrypt_nginx/nginxparser.py similarity index 100% rename from letsencrypt_nginx/nginxparser.py rename to letsencrypt_nginx/letsencrypt_nginx/nginxparser.py diff --git a/letsencrypt_nginx/obj.py b/letsencrypt_nginx/letsencrypt_nginx/obj.py similarity index 100% rename from letsencrypt_nginx/obj.py rename to letsencrypt_nginx/letsencrypt_nginx/obj.py diff --git a/letsencrypt_nginx/options-ssl-nginx.conf b/letsencrypt_nginx/letsencrypt_nginx/options-ssl-nginx.conf similarity index 100% rename from letsencrypt_nginx/options-ssl-nginx.conf rename to letsencrypt_nginx/letsencrypt_nginx/options-ssl-nginx.conf diff --git a/letsencrypt_nginx/parser.py b/letsencrypt_nginx/letsencrypt_nginx/parser.py similarity index 100% rename from letsencrypt_nginx/parser.py rename to letsencrypt_nginx/letsencrypt_nginx/parser.py diff --git a/letsencrypt_nginx/tests/__init__.py b/letsencrypt_nginx/letsencrypt_nginx/tests/__init__.py similarity index 100% rename from letsencrypt_nginx/tests/__init__.py rename to letsencrypt_nginx/letsencrypt_nginx/tests/__init__.py diff --git a/letsencrypt_nginx/tests/configurator_test.py b/letsencrypt_nginx/letsencrypt_nginx/tests/configurator_test.py similarity index 100% rename from letsencrypt_nginx/tests/configurator_test.py rename to letsencrypt_nginx/letsencrypt_nginx/tests/configurator_test.py diff --git a/letsencrypt_nginx/tests/dvsni_test.py b/letsencrypt_nginx/letsencrypt_nginx/tests/dvsni_test.py similarity index 100% rename from letsencrypt_nginx/tests/dvsni_test.py rename to letsencrypt_nginx/letsencrypt_nginx/tests/dvsni_test.py diff --git a/letsencrypt_nginx/tests/nginxparser_test.py b/letsencrypt_nginx/letsencrypt_nginx/tests/nginxparser_test.py similarity index 100% rename from letsencrypt_nginx/tests/nginxparser_test.py rename to letsencrypt_nginx/letsencrypt_nginx/tests/nginxparser_test.py diff --git a/letsencrypt_nginx/tests/obj_test.py b/letsencrypt_nginx/letsencrypt_nginx/tests/obj_test.py similarity index 100% rename from letsencrypt_nginx/tests/obj_test.py rename to letsencrypt_nginx/letsencrypt_nginx/tests/obj_test.py diff --git a/letsencrypt_nginx/tests/parser_test.py b/letsencrypt_nginx/letsencrypt_nginx/tests/parser_test.py similarity index 100% rename from letsencrypt_nginx/tests/parser_test.py rename to letsencrypt_nginx/letsencrypt_nginx/tests/parser_test.py diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/edge_cases.conf b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/edge_cases.conf similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/edge_cases.conf rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/edge_cases.conf diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/foo.conf b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/foo.conf similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/foo.conf rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/foo.conf diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/mime.types b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/mime.types similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/mime.types rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/mime.types diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/nginx.conf b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/nginx.conf similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/nginx.conf rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/nginx.conf diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/nginx.new.conf b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/nginx.new.conf similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/nginx.new.conf rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/nginx.new.conf diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/server.conf b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/server.conf similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/server.conf rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/server.conf diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/sites-enabled/default b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/sites-enabled/default similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/sites-enabled/default rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/sites-enabled/default diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/sites-enabled/example.com b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/sites-enabled/example.com similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/sites-enabled/example.com rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/sites-enabled/example.com diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1 diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-enabled/default diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params diff --git a/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf b/letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf similarity index 100% rename from letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf rename to letsencrypt_nginx/letsencrypt_nginx/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf diff --git a/letsencrypt_nginx/tests/util.py b/letsencrypt_nginx/letsencrypt_nginx/tests/util.py similarity index 100% rename from letsencrypt_nginx/tests/util.py rename to letsencrypt_nginx/letsencrypt_nginx/tests/util.py From b9df69af9f1f883daa03bc715200f6e866fd7cd5 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 16:34:08 +0000 Subject: [PATCH 25/30] Basic dev/test setup for separate package subdirectories. --- Dockerfile | 6 ++- MANIFEST.in | 9 ----- Vagrantfile | 2 +- acme/MANIFEST.in | 1 + acme/setup.py | 29 ++++++++++++++ docs/contributing.rst | 2 +- docs/using.rst | 2 +- letsencrypt/plugins/disco.py | 6 ++- letsencrypt/tests/test_util.py | 2 +- letsencrypt_apache/MANIFEST.in | 2 + letsencrypt_apache/setup.py | 23 +++++++++++ letsencrypt_nginx/MANIFEST.in | 2 + letsencrypt_nginx/setup.py | 22 ++++++++++ setup.py | 73 +--------------------------------- tox.ini | 15 ++++--- 15 files changed, 104 insertions(+), 92 deletions(-) create mode 100644 acme/MANIFEST.in create mode 100644 acme/setup.py create mode 100644 letsencrypt_apache/MANIFEST.in create mode 100644 letsencrypt_apache/setup.py create mode 100644 letsencrypt_nginx/MANIFEST.in create mode 100644 letsencrypt_nginx/setup.py diff --git a/Dockerfile b/Dockerfile index 479aa4e85..a045cf2ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,7 +50,11 @@ COPY letsencrypt_nginx /opt/letsencrypt/src/letsencrypt_nginx/ # requirements.txt not installed! RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv && \ - /opt/letsencrypt/venv/bin/pip install -e /opt/letsencrypt/src + /opt/letsencrypt/venv/bin/pip install \ + -e /opt/letsencrypt/src/acme \ + -e /opt/letsencrypt/src \ + -e /opt/letsencrypt/src/letsencrypt_apache \ + -e /opt/letsencrypt/src/letsencrypt_nginx # install in editable mode (-e) to save space: it's not possible to # "rm -rf /opt/letsencrypt/src" (it's stays in the underlaying image); diff --git a/MANIFEST.in b/MANIFEST.in index 900a7ab80..08b67ef7a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,12 +4,3 @@ include CONTRIBUTING.md include linter_plugin.py include letsencrypt/EULA recursive-include letsencrypt/tests/testdata * - -recursive-include acme/schemata *.json -recursive-include acme/testdata * - -recursive-include letsencrypt_apache/tests/testdata * -include letsencrypt_apache/options-ssl-apache.conf - -recursive-include letsencrypt_nginx/tests/testdata * -include letsencrypt_nginx/options-ssl-nginx.conf diff --git a/Vagrantfile b/Vagrantfile index 1d3b48f06..0659e1ee2 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -10,7 +10,7 @@ cd /vagrant sudo ./bootstrap/ubuntu.sh if [ ! -d "venv" ]; then virtualenv --no-site-packages -p python2 venv - ./venv/bin/pip install -r requirements.txt -e .[dev,docs,testing] + ./venv/bin/pip install -r requirements.txt -e acme -e .[dev,docs,testing] -e letsencrypt_apache -e letsencrypt_nginx fi SETUP_SCRIPT diff --git a/acme/MANIFEST.in b/acme/MANIFEST.in new file mode 100644 index 000000000..f3444f746 --- /dev/null +++ b/acme/MANIFEST.in @@ -0,0 +1 @@ +recursive-include acme/testdata * diff --git a/acme/setup.py b/acme/setup.py new file mode 100644 index 000000000..d83131d2a --- /dev/null +++ b/acme/setup.py @@ -0,0 +1,29 @@ +from setuptools import setup +from setuptools import find_packages + + +install_requires = [ + 'argparse', + # load_pem_private/public_key (>=0.6) + # rsa_recover_prime_factors (>=0.8) + 'cryptography>=0.8', + 'mock<1.1.0', # py26 + 'pyrfc3339', + 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304) + 'pyasn1', # urllib3 InsecurePlatformWarning (#304) + 'PyOpenSSL', + 'pytz', + 'requests', + 'werkzeug', +] + +setup( + name='acme', + packages=find_packages(), + install_requires=install_requires, + entry_points={ + 'console_scripts': [ + 'jws = acme.jose.jws:CLI.run', + ], + }, +) diff --git a/docs/contributing.rst b/docs/contributing.rst index b415390cf..c2120f1e1 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -26,7 +26,7 @@ Install the development packages: .. code-block:: shell - pip install -r requirements.txt -e .[dev,docs,testing] + pip install -r requirements.txt -e acme -e .[dev,docs,testing] -e letsencrypt_apache -e letsencrypt_nginx .. note:: `-e` (short for `--editable`) turns on *editable mode* in which any source code changes in the current working diff --git a/docs/using.rst b/docs/using.rst index 026c34aa2..0d9ffcc73 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -108,7 +108,7 @@ Installation .. code-block:: shell virtualenv --no-site-packages -p python2 venv - ./venv/bin/pip install -r requirements.txt . + ./venv/bin/pip install -r requirements.txt acme . letsencrypt_apache letsencrypt_nginx .. warning:: Please do **not** use ``python setup.py install``. Please do **not** attempt the installation commands as diff --git a/letsencrypt/plugins/disco.py b/letsencrypt/plugins/disco.py index e2e2d4c54..51d251126 100644 --- a/letsencrypt/plugins/disco.py +++ b/letsencrypt/plugins/disco.py @@ -16,7 +16,11 @@ logger = logging.getLogger(__name__) class PluginEntryPoint(object): """Plugin entry point.""" - PREFIX_FREE_DISTRIBUTIONS = ["letsencrypt"] + PREFIX_FREE_DISTRIBUTIONS = [ + "letsencrypt", + "letsencrypt-apache", + "letsencrypt-nginx", + ] """Distributions for which prefix will be omitted.""" # this object is mutable, don't allow it to be hashed! diff --git a/letsencrypt/tests/test_util.py b/letsencrypt/tests/test_util.py index d46ad3bd4..80d26cbe8 120000 --- a/letsencrypt/tests/test_util.py +++ b/letsencrypt/tests/test_util.py @@ -1 +1 @@ -../../acme/test_util.py \ No newline at end of file +../../acme/acme/test_util.py \ No newline at end of file diff --git a/letsencrypt_apache/MANIFEST.in b/letsencrypt_apache/MANIFEST.in new file mode 100644 index 000000000..aac2bfb36 --- /dev/null +++ b/letsencrypt_apache/MANIFEST.in @@ -0,0 +1,2 @@ +recursive-include letsencrypt_apache/tests/testdata * +include letsencrypt_apache/options-ssl-apache.conf diff --git a/letsencrypt_apache/setup.py b/letsencrypt_apache/setup.py new file mode 100644 index 000000000..fac5b6b88 --- /dev/null +++ b/letsencrypt_apache/setup.py @@ -0,0 +1,23 @@ +from setuptools import setup +from setuptools import find_packages + + +install_requires = [ + 'acme', + 'letsencrypt', + 'mock<1.1.0', # py26 + 'python-augeas', + 'zope.component', + 'zope.interface', +] + +setup( + name='letsencrypt-apache', + packages=find_packages(), + install_requires=install_requires, + entry_points={ + 'letsencrypt.plugins': [ + 'apache = letsencrypt_apache.configurator:ApacheConfigurator', + ], + }, +) diff --git a/letsencrypt_nginx/MANIFEST.in b/letsencrypt_nginx/MANIFEST.in new file mode 100644 index 000000000..94f85e40f --- /dev/null +++ b/letsencrypt_nginx/MANIFEST.in @@ -0,0 +1,2 @@ +recursive-include letsencrypt_nginx/tests/testdata * +include letsencrypt_nginx/options-ssl-nginx.conf diff --git a/letsencrypt_nginx/setup.py b/letsencrypt_nginx/setup.py new file mode 100644 index 000000000..bd8e8976d --- /dev/null +++ b/letsencrypt_nginx/setup.py @@ -0,0 +1,22 @@ +from setuptools import setup +from setuptools import find_packages + + +install_requires = [ + 'acme', + 'letsencrypt', + 'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary? + 'mock<1.1.0', # py26 + 'zope.interface', +] + +setup( + name='letsencrypt-nginx', + packages=find_packages(), + install_requires=install_requires, + entry_points={ + 'letsencrypt.plugins': [ + 'nginx = letsencrypt_nginx.configurator:NginxConfigurator', + ], + }, +) diff --git a/setup.py b/setup.py index 0c74b296c..1e0d58a70 100644 --- a/setup.py +++ b/setup.py @@ -28,88 +28,24 @@ meta = dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", read_file(init_fn))) readme = read_file(os.path.join(here, 'README.rst')) changes = read_file(os.path.join(here, 'CHANGES.rst')) -# #358: acme, letsencrypt, letsencrypt_apache, letsencrypt_nginx, etc. -# shall be distributed separately. Please make sure to keep the -# dependecy lists up to date: this is being somewhat checked below -# using an assert statement! Separate lists are helpful for OS package -# maintainers. and will make the future migration a lot easier. -acme_install_requires = [ - 'argparse', - # load_pem_private/public_key (>=0.6) - # rsa_recover_prime_factors (>=0.8) - 'cryptography>=0.8', - 'mock<1.1.0', # py26 - 'pyrfc3339', - 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304) - 'pyasn1', # urllib3 InsecurePlatformWarning (#304) - #'PyOpenSSL', # version pin would cause mismatch - 'pytz', - 'requests', - 'werkzeug', -] -letsencrypt_install_requires = [ - #'acme', - 'argparse', - 'ConfigArgParse', - 'configobj', - #'cryptography>=0.7', # load_pem_x509_certificate, version pin mismatch - 'mock<1.1.0', # py26 - 'parsedatetime', - 'psutil>=2.1.0', # net_connections introduced in 2.1.0 - # https://pyopenssl.readthedocs.org/en/latest/api/crypto.html#OpenSSL.crypto.X509Req.get_extensions - 'PyOpenSSL>=0.15', - 'pyrfc3339', - 'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280 - 'pytz', - 'zope.component', - 'zope.interface', -] -letsencrypt_apache_install_requires = [ - #'acme', - #'letsencrypt', - 'mock<1.1.0', # py26 - 'python-augeas', - 'zope.component', - 'zope.interface', -] -letsencrypt_nginx_install_requires = [ - #'acme', - #'letsencrypt', - 'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary? - 'mock<1.1.0', # py26 - 'zope.interface', -] - install_requires = [ + 'acme', 'argparse', - 'cryptography>=0.8', 'ConfigArgParse', 'configobj', + 'cryptography>=0.7', # load_pem_x509_certificate 'mock<1.1.0', # py26 - 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304) 'parsedatetime', 'psutil>=2.1.0', # net_connections introduced in 2.1.0 - 'pyasn1', # urllib3 InsecurePlatformWarning (#304) # https://pyopenssl.readthedocs.org/en/latest/api/crypto.html#OpenSSL.crypto.X509Req.get_extensions 'PyOpenSSL>=0.15', - 'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary? 'pyrfc3339', - 'python-augeas', 'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280 'pytz', - 'requests', - 'werkzeug', 'zope.component', 'zope.interface', ] -assert set(install_requires) == set.union(*(set(ireq) for ireq in ( - acme_install_requires, - letsencrypt_install_requires, - letsencrypt_apache_install_requires, - letsencrypt_nginx_install_requires -))), "*install_requires don't match up!" - dev_extras = [ # Pin astroid==1.3.5, pylint==1.4.2 as a workaround for #289 'astroid==1.3.5', @@ -172,7 +108,6 @@ setup( 'console_scripts': [ 'letsencrypt = letsencrypt.cli:main', 'letsencrypt-renewer = letsencrypt.renewer:main', - 'jws = letsencrypt.acme.jose.jws:CLI.run', ], 'letsencrypt.plugins': [ 'manual = letsencrypt.plugins.manual:ManualAuthenticator', @@ -180,10 +115,6 @@ setup( 'null = letsencrypt.plugins.null:Installer', 'standalone = letsencrypt.plugins.standalone.authenticator' ':StandaloneAuthenticator', - - # to be moved to separate pypi packages - 'apache = letsencrypt_apache.configurator:ApacheConfigurator', - 'nginx = letsencrypt_nginx.configurator:NginxConfigurator', ], }, diff --git a/tox.ini b/tox.ini index 2f20e5799..71eb950d1 100644 --- a/tox.ini +++ b/tox.ini @@ -3,11 +3,14 @@ # "tox" from this directory. [tox] +# acme and letsencrypt are not yet on pypi, so when Tox invokes +# "install *.zip", it will not find deps +skipsdist = true envlist = py26,py27,cover,lint [testenv] commands = - pip install -r requirements.txt -e .[testing] + pip install -r requirements.txt -e acme -e .[testing] -e letsencrypt_apache -e letsencrypt_nginx # -q does not suppress errors python setup.py test -q python setup.py test -q -s acme @@ -22,7 +25,7 @@ setenv = [testenv:cover] basepython = python2.7 commands = - pip install -r requirements.txt -e .[testing] + pip install -r requirements.txt -e acme -e .[testing] -e letsencrypt_apache -e letsencrypt_nginx ./tox.cover.sh [testenv:lint] @@ -32,8 +35,8 @@ basepython = python2.7 # duplicate code checking; if one of the commands fails, others will # continue, but tox return code will reflect previous error commands = - pip install -r requirements.txt -e .[dev] + pip install -r requirements.txt -e acme -e .[dev] -e letsencrypt_apache -e letsencrypt_nginx pylint --rcfile=.pylintrc letsencrypt - pylint --rcfile=.pylintrc acme - pylint --rcfile=.pylintrc letsencrypt_apache - pylint --rcfile=.pylintrc letsencrypt_nginx + pylint --rcfile=.pylintrc acme/acme + pylint --rcfile=.pylintrc letsencrypt_apache/letsencrypt_apache + pylint --rcfile=.pylintrc letsencrypt_nginx/letsencrypt_nginx From 2e9cf9a5d5c567c07f0f0510474b513976d6e4ad Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 16:39:05 +0000 Subject: [PATCH 26/30] Include requirements.txt in sdist --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 08b67ef7a..530044212 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ +include requirements.txt include README.rst include CHANGES.rst include CONTRIBUTING.md From db1e078c0602f0b691379d7c8bb31076e11b77b2 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 16:39:19 +0000 Subject: [PATCH 27/30] Shared tox envdir --- .dockerignore | 2 +- .gitignore | 2 +- tox.ini | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.dockerignore b/.dockerignore index 2ce8a8209..203d8000f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,6 @@ # test docker on their git working directories. .git -.tox +tox.cover venv docs diff --git a/.gitignore b/.gitignore index 54670e6da..2441f1a74 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ build/ dist/ /venv/ -/.tox/ +/tox.venv/ letsencrypt.log # coverage diff --git a/tox.ini b/tox.ini index 71eb950d1..031f3af4f 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,10 @@ skipsdist = true envlist = py26,py27,cover,lint [testenv] +# share one venv across testenvs, instead of multiple +# .tox/{py26,py27,cover,lint}; but do NOT set envdir to +# {toxinidir}/venv as it will destroy existing dev venv +envdir = {toxinidir}/tox.venv commands = pip install -r requirements.txt -e acme -e .[testing] -e letsencrypt_apache -e letsencrypt_nginx # -q does not suppress errors From 2ae6ac2bfdcf628efcbb9a6d14143c2d1b6b253e Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 17:13:25 +0000 Subject: [PATCH 28/30] Travis: tox-venv activation path adjustment --- .dockerignore | 2 +- .travis.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.dockerignore b/.dockerignore index 203d8000f..0bdb9e62d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,5 @@ # this file uses slightly different syntax than .gitignore, -# e.g. ".tox/" will not ignore .tox directory +# e.g. "tox.cover/" will not ignore tox.cover directory # well, official docker build should be done on clean git checkout # anyway, so .tox should be empty... But I'm sure people will try to diff --git a/.travis.yml b/.travis.yml index a7b03d20a..a9769ed78 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ env: install: "travis_retry pip install tox coveralls" before_script: '[ "${TOXENV:0:2}" != "py" ] || ./tests/boulder-start.sh' # TODO: eliminate substring slice bashism -script: 'travis_retry tox && ([ "${TOXENV:0:2}" != "py" ] || (source .tox/$TOXENV/bin/activate && ./tests/boulder-integration.sh))' +script: 'travis_retry tox && ([ "${TOXENV:0:2}" != "py" ] || (source tox.venv/$TOXENV/bin/activate && ./tests/boulder-integration.sh))' after_success: '[ "$TOXENV" == "cover" ] && coveralls' From ff8925d92fc5a4146cd1704f9563653789df11c1 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 17:22:52 +0000 Subject: [PATCH 29/30] Update sys.path in docs/conf.py for Sphinx to find subdir packages. --- docs/conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index c285c19e5..62e846a33 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -36,6 +36,8 @@ with codecs.open(init_fn, encoding='utf8') as fd: # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath(os.path.join(here, '..'))) +for pkg in 'acme', 'letsencrypt_apache', 'letsencrypt_nginx': + sys.path.insert(0, os.path.abspath(os.path.join(here, '..', pkg))) # -- General configuration ------------------------------------------------ From 0b4a85c145326037f2bd7da60b35eb5d97badd17 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Fri, 10 Jul 2015 17:29:28 +0000 Subject: [PATCH 30/30] Once again fix travis/tox venv path. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a9769ed78..31348f066 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ env: install: "travis_retry pip install tox coveralls" before_script: '[ "${TOXENV:0:2}" != "py" ] || ./tests/boulder-start.sh' # TODO: eliminate substring slice bashism -script: 'travis_retry tox && ([ "${TOXENV:0:2}" != "py" ] || (source tox.venv/$TOXENV/bin/activate && ./tests/boulder-integration.sh))' +script: 'travis_retry tox && ([ "${TOXENV:0:2}" != "py" ] || (source tox.venv/bin/activate && ./tests/boulder-integration.sh))' after_success: '[ "$TOXENV" == "cover" ] && coveralls'