account integration

This commit is contained in:
James Kasten
2015-04-17 03:40:22 -07:00
parent 3d9d0627d7
commit ab616a598f
8 changed files with 154 additions and 70 deletions
+25 -13
View File
@@ -1,3 +1,4 @@
import logging
import os import os
import re import re
@@ -11,7 +12,6 @@ from letsencrypt.client import errors
from letsencrypt.client import interfaces from letsencrypt.client import interfaces
from letsencrypt.client import le_util from letsencrypt.client import le_util
from letsencrypt.client.display import ops as display_ops
from letsencrypt.client.display import util as display_util from letsencrypt.client.display import util as display_util
@@ -142,32 +142,43 @@ class Account(object):
return cls(config, key, email, phone, regr) return cls(config, key, email, phone, regr)
@classmethod @classmethod
def choose_account(cls, config): def get_accounts(cls, config):
"""Choose one of the available accounts.""" """Return all current accounts.
accounts = []
:param config: Configuration
:type config: :class:`letsencrypt.client.interfaces.IConfig`
"""
try:
filenames = os.listdir(config.accounts_dir) filenames = os.listdir(config.accounts_dir)
except OSError:
return []
accounts = []
for name in filenames: for name in filenames:
# Not some directory ie. keys # Not some directory ie. keys
config_fp = os.path.join(config.accounts_dir, name) config_fp = os.path.join(config.accounts_dir, name)
if os.path.isfile(config_fp): if os.path.isfile(config_fp):
accounts.append(cls._from_config_fp(config, config_fp)) accounts.append(cls._from_config_fp(config, config_fp))
if len(accounts) == 1: return accounts
return accounts[0]
elif len(accounts) > 1:
return display_ops.choose_account(accounts)
else:
return None
@classmethod @classmethod
def from_prompts(cls, config): def from_prompts(cls, config):
"""Generate an account from prompted user input.""" """Generate an account from prompted user input.
:param config: Configuration
:type config: :class:`letsencrypt.client.interfaces.IConfig`
:returns: Account or None
:rtype: :class:`letsencrypt.client.account.Account`
"""
code, email = zope.component.getUtility(interfaces.IDisplay).input( code, email = zope.component.getUtility(interfaces.IDisplay).input(
"Enter email address") "Enter email address (optional)")
if code == display_util.OK: if code == display_util.OK:
email = email if email != "" else None email = email if email != "" else None
print config.account_keys_dir
le_util.make_or_verify_dir( le_util.make_or_verify_dir(
config.account_keys_dir, 0o700, os.geteuid()) config.account_keys_dir, 0o700, os.geteuid())
key = crypto_util.init_save_key( key = crypto_util.init_save_key(
@@ -181,4 +192,5 @@ class Account(object):
"""Scrub email address before using it.""" """Scrub email address before using it."""
if re.match(cls.EMAIL_REGEX, email): if re.match(cls.EMAIL_REGEX, email):
return bool(not email.startswith(".") and ".." not in email) return bool(not email.startswith(".") and ".." not in email)
logging.warn("Invalid email address: using default address.")
return False return False
+6 -6
View File
@@ -26,8 +26,8 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
messages messages
:type network: :class:`letsencrypt.client.network2.Network` :type network: :class:`letsencrypt.client.network2.Network`
:ivar authkey: Authorized Keys for domains. :ivar account: Client's Account
:type authkey: :class:`letsencrypt.client.le_util.Key` :type account: :class:`letsencrypt.client.account.Account`
:ivar dict authzr: ACME Authorization Resource dict where keys are domains. :ivar dict authzr: ACME Authorization Resource dict where keys are domains.
:ivar list dv_c: DV challenges in the form of :ivar list dv_c: DV challenges in the form of
@@ -36,12 +36,12 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
form of :class:`letsencrypt.client.achallenges.AnnotatedChallenge` form of :class:`letsencrypt.client.achallenges.AnnotatedChallenge`
""" """
def __init__(self, dv_auth, cont_auth, network, authkey): def __init__(self, dv_auth, cont_auth, network, account):
self.dv_auth = dv_auth self.dv_auth = dv_auth
self.cont_auth = cont_auth self.cont_auth = cont_auth
self.network = network self.network = network
self.authkey = authkey self.account = account
self.authzr = dict() self.authzr = dict()
# List must be used to keep responses straight. # List must be used to keep responses straight.
@@ -275,11 +275,11 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
if isinstance(chall, challenges.DVSNI): if isinstance(chall, challenges.DVSNI):
logging.info(" DVSNI challenge for %s.", domain) logging.info(" DVSNI challenge for %s.", domain)
achall = achallenges.DVSNI( achall = achallenges.DVSNI(
challb=challb, domain=domain, key=self.authkey) challb=challb, domain=domain, key=self.account.key)
elif isinstance(chall, challenges.SimpleHTTPS): elif isinstance(chall, challenges.SimpleHTTPS):
logging.info(" SimpleHTTPS challenge for %s.", domain) logging.info(" SimpleHTTPS challenge for %s.", domain)
achall = achallenges.SimpleHTTPS( achall = achallenges.SimpleHTTPS(
challb=challb, domain=domain, key=self.authkey) challb=challb, domain=domain, key=self.account.key)
elif isinstance(chall, challenges.DNS): elif isinstance(chall, challenges.DNS):
logging.info(" DNS challenge for %s.", domain) logging.info(" DNS challenge for %s.", domain)
achall = achallenges.DNS(challb=challb, domain=domain) achall = achallenges.DNS(challb=challb, domain=domain)
+44 -23
View File
@@ -1,7 +1,6 @@
"""ACME protocol client class and helper functions.""" """ACME protocol client class and helper functions."""
import logging import logging
import os import os
import sys
import M2Crypto import M2Crypto
import zope.component import zope.component
@@ -9,6 +8,7 @@ import zope.component
from letsencrypt.acme import jose from letsencrypt.acme import jose
from letsencrypt.acme.jose import jwk from letsencrypt.acme.jose import jwk
from letsencrypt.client import account
from letsencrypt.client import auth_handler from letsencrypt.client import auth_handler
from letsencrypt.client import continuity_auth from letsencrypt.client import continuity_auth
from letsencrypt.client import crypto_util from letsencrypt.client import crypto_util
@@ -30,11 +30,8 @@ class Client(object):
:ivar network: Network object for sending and receiving messages :ivar network: Network object for sending and receiving messages
:type network: :class:`letsencrypt.client.network2.Network` :type network: :class:`letsencrypt.client.network2.Network`
:ivar authkey: Authorization Key
:type authkey: :class:`letsencrypt.client.le_util.Key`
:ivar account: Account object used for registration :ivar account: Account object used for registration
:type account: :class:`letsencrypt.client.registration.Registration` :type account: :class:`letsencrypt.client.account.Account`
:ivar auth_handler: Object that supports the IAuthenticator interface. :ivar auth_handler: Object that supports the IAuthenticator interface.
auth_handler contains both a dv_authenticator and a auth_handler contains both a dv_authenticator and a
@@ -49,7 +46,7 @@ class Client(object):
""" """
def __init__(self, config, authkey, dv_auth, installer): def __init__(self, config, account, dv_auth, installer):
"""Initialize a client. """Initialize a client.
:param dv_auth: IAuthenticator that can solve the :param dv_auth: IAuthenticator that can solve the
@@ -59,38 +56,40 @@ class Client(object):
:type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator` :type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
""" """
self.authkey = authkey self.account = account
self.account = None
self.installer = installer self.installer = installer
# TODO: Allow for other alg types besides RS256 # TODO: Allow for other alg types besides RS256
self.network = network2.Network( self.network = network2.Network(
"https://%s/acme/new-reg" % config.server, "https://%s/acme/new-reg" % config.server,
jwk.JWKRSA.load(authkey.pem)) jwk.JWKRSA.load(account.key.pem))
self.config = config self.config = config
if dv_auth is not None: if dv_auth is not None:
cont_auth = continuity_auth.ContinuityAuthenticator(config) cont_auth = continuity_auth.ContinuityAuthenticator(config)
self.auth_handler = auth_handler.AuthHandler( self.auth_handler = auth_handler.AuthHandler(
dv_auth, cont_auth, self.network, self.authkey) dv_auth, cont_auth, self.network, self.account)
else: else:
self.auth_handler = None self.auth_handler = None
def register(self, network, store=True): def register(self, save=True):
"""New Registration with the ACME server. """New Registration with the ACME server."""
:param bool store: Whether to store the registration information
"""
self.account = self.network.register_from_account(self.account) self.account = self.network.register_from_account(self.account)
if self.account.regr.terms_of_service or self.config.tos: if self.account.terms_of_service:
if not self.config.tos:
agree = zope.component.getUtility(interfaces.IDisplay).yesno( agree = zope.component.getUtility(interfaces.IDisplay).yesno(
self.account.regr.terms_of_service, "Agree", "Cancel") self.account.terms_of_service, "Agree", "Cancel")
else:
agree = True
if agree: if agree:
self.account.regr = self.network.agree_to_tos(self.account.regr) self.account.regr = self.network.agree_to_tos(self.account.regr)
# TODO: Handle case where user doesn't agree # TODO: Handle case where user doesn't agree
self.account.save()
def obtain_certificate(self, domains, csr=None): def obtain_certificate(self, domains, csr=None):
"""Obtains a certificate from the ACME server. """Obtains a certificate from the ACME server.
@@ -111,14 +110,14 @@ class Client(object):
"not set.") "not set.")
logging.warning(msg) logging.warning(msg)
raise errors.LetsEncryptClientError(msg) raise errors.LetsEncryptClientError(msg)
if self.regr is None: if self.account.regr is None:
raise errors.LetsEncryptClientError( raise errors.LetsEncryptClientError(
"Please register with the ACME server first.") "Please register with the ACME server first.")
# Perform Challenges/Get Authorizations # Perform Challenges/Get Authorizations
if self.regr.new_authzr_uri: if self.account.new_authzr_uri:
authzr = self.auth_handler.get_authorizations( authzr = self.auth_handler.get_authorizations(
domains, self.regr.new_authzr_uri) domains, self.account.new_authzr_uri)
# This isn't required to be in the registration resource... # This isn't required to be in the registration resource...
# and it isn't standardized... ugh - acme-spec #93 # and it isn't standardized... ugh - acme-spec #93
else: else:
@@ -129,7 +128,7 @@ class Client(object):
# Create CSR from names # Create CSR from names
if csr is None: if csr is None:
csr = crypto_util.init_save_csr( csr = crypto_util.init_save_csr(
self.authkey, domains, self.config.cert_dir) self.account.key, domains, self.config.cert_dir)
# Retrieve certificate # Retrieve certificate
certr = self.network.request_issuance( certr = self.network.request_issuance(
@@ -142,7 +141,7 @@ class Client(object):
certr, self.config.cert_path, self.config.chain_path) certr, self.config.cert_path, self.config.chain_path)
revoker.Revoker.store_cert_key( revoker.Revoker.store_cert_key(
cert_file, self.authkey.file, self.config) cert_file, self.account.key.file, self.config)
return cert_file, chain_file return cert_file, chain_file
@@ -379,6 +378,28 @@ def determine_authenticator(all_auths, config):
return auth return auth
def determine_account(config):
"""Determine which account to use.
Will create an account if necessary.
:param config: Configuration object
:type config: :class:`letsencrypt.client.interfaces.IConfig`
:returns: Account
:rtype: :class:`letsencrypt.client.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 determine_installer(config): def determine_installer(config):
"""Returns a valid installer if one exists. """Returns a valid installer if one exists.
+11 -3
View File
@@ -187,10 +187,18 @@ class Network(object):
return regr return regr
def register_from_account(self, account): def register_from_account(self, account):
# TODO: properly format/scrub phone number and email """Register with server.
:param account: Account
:type account: :class:`letsencrypt.client.account.Account`
:returns: Updated account
:rtype: :class:`letsencrypt.client.account.Account`
"""
details = ( details = (
"mailto:" + self.email if self.email is not None else None, "mailto:" + account.email if account.email is not None else None,
"tel:" + self.phone if self.phone is not None else None "tel:" + account.phone if account.phone is not None else None
) )
contact_tuple = tuple(det for det in details if det is not None) contact_tuple = tuple(det for det in details if det is not None)
+9 -16
View File
@@ -93,7 +93,6 @@ class AccountTest(unittest.TestCase):
self.assertTrue(partial.terms_of_service is None) self.assertTrue(partial.terms_of_service is None)
self.assertTrue(partial.recovery_token is None) self.assertTrue(partial.recovery_token is None)
def test_partial_account_default(self): def test_partial_account_default(self):
partial = account.Account(self.config, self.key) partial = account.Account(self.config, self.key)
partial.save() partial.save()
@@ -105,25 +104,19 @@ class AccountTest(unittest.TestCase):
self.assertEqual(partial.phone, acc.phone) self.assertEqual(partial.phone, acc.phone)
self.assertEqual(partial.regr, acc.regr) self.assertEqual(partial.regr, acc.regr)
@mock.patch("letsencrypt.client.account.display_ops.choose_account") def test_get_accounts(self):
def test_choose_account(self, mock_op): accs = account.Account.get_accounts(self.config)
mock_op.return_value = self.test_account self.assertFalse(accs)
# Test 0
self.assertTrue(account.Account.choose_account(self.config) is None)
# Test 1
self.test_account.save() self.test_account.save()
acc = account.Account.choose_account(self.config) accs = account.Account.get_accounts(self.config)
self.assertEqual(acc.email, self.test_account.email) self.assertEqual(len(accs), 1)
self.assertEqual(accs[0].email, self.test_account.email)
# Test multiple acc2 = account.Account(self.config, self.key, "testing_email@gmail.com")
self.assertFalse(mock_op.called)
acc2 = account.Account(self.config, self.key)
acc2.save() acc2.save()
test_acc = account.Account.choose_account(self.config) accs = account.Account.get_accounts(self.config)
self.assertTrue(mock_op.called) self.assertEqual(len(accs), 2)
self.assertTrue(test_acc.email, self.test_account.email)
def _read_out_config(self, filep): def _read_out_config(self, filep):
print open(os.path.join(self.accounts_dir, filep)).read() print open(os.path.join(self.accounts_dir, filep)).read()
+46
View File
@@ -1,10 +1,56 @@
"""letsencrypt.client.client.py tests.""" """letsencrypt.client.client.py tests."""
import os
import unittest import unittest
import shutil
import tempfile
import mock import mock
from letsencrypt.client import account
from letsencrypt.client import configuration from letsencrypt.client import configuration
from letsencrypt.client import errors from letsencrypt.client import errors
from letsencrypt.client import le_util
class DetermineAccountTest(unittest.TestCase):
def setUp(self):
self.accounts_dir = tempfile.mkdtemp("accounts")
self.account_keys_dir = os.path.join(self.accounts_dir, "keys")
os.makedirs(self.account_keys_dir, 0o700)
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")
def tearDown(self):
shutil.rmtree(self.accounts_dir)
@mock.patch("letsencrypt.client.client.account.Account.from_prompts")
@mock.patch("letsencrypt.client.client.display_ops.choose_account")
def determine_account(self, mock_op, mock_prompt):
from letsencrypt.client import client
key = le_util.Key("file", "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, self.key)
acc2.save()
chosen_acc = client.determine_account(self.config)
self.assertTrue(mock_op.called)
self.assertTrue(chosen_acc.email, test_acc.email)
class DetermineAuthenticatorTest(unittest.TestCase): class DetermineAuthenticatorTest(unittest.TestCase):
+4 -2
View File
@@ -72,8 +72,9 @@ class ChooseAccountTest(unittest.TestCase):
server="letsencrypt-demo.org") server="letsencrypt-demo.org")
self.key = le_util.Key("keypath", "pem") self.key = le_util.Key("keypath", "pem")
self.acc1 = account.Account(self.config, self.key, "email1") self.acc1 = account.Account(self.config, self.key, "email1@g.com")
self.acc2 = account.Account(self.config, self.key, "email2", "phone") self.acc2 = account.Account(
self.config, self.key, "email2@g.com", "phone")
self.acc1.save() self.acc1.save()
self.acc2.save() self.acc2.save()
@@ -84,6 +85,7 @@ class ChooseAccountTest(unittest.TestCase):
@mock.patch("letsencrypt.client.display.ops.util") @mock.patch("letsencrypt.client.display.ops.util")
def test_one(self, mock_util): def test_one(self, mock_util):
print self.acc1
mock_util().menu.return_value = (display_util.OK, 0) mock_util().menu.return_value = (display_util.OK, 0)
self.assertEqual(self._call([self.acc1]), self.acc1) self.assertEqual(self._call([self.acc1]), self.acc1)
+7 -5
View File
@@ -196,14 +196,16 @@ def main(): # pylint: disable=too-many-branches, too-many-statements
# Prepare for init of Client # Prepare for init of Client
if args.authkey is None: if args.authkey is None:
authkey = crypto_util.init_save_key(args.rsa_key_size, config.key_dir) account = client.determine_account(config)
else: else:
authkey = le_util.Key(args.authkey[0], args.authkey[1]) # TODO: Figure out what to do with this
# le_util.Key(args.authkey[0], args.authkey[1])
account = client.determine_account(config)
acme = client.Client(config, authkey, auth, installer) acme = client.Client(config, account, auth, installer)
# Validate the key and csr # Validate the key and csr
client.validate_key_csr(authkey) client.validate_key_csr(account.key)
# This more closely mimics the capabilities of the CLI # This more closely mimics the capabilities of the CLI
# It should be possible for reconfig only, install-only, no-install # It should be possible for reconfig only, install-only, no-install
@@ -214,7 +216,7 @@ def main(): # pylint: disable=too-many-branches, too-many-statements
acme.register() acme.register()
cert_file, chain_file = acme.obtain_certificate(doms) cert_file, chain_file = acme.obtain_certificate(doms)
if installer is not None and cert_file is not None: if installer is not None and cert_file is not None:
acme.deploy_certificate(doms, authkey, cert_file, chain_file) acme.deploy_certificate(doms, account.key, cert_file, chain_file)
if installer is not None: if installer is not None:
acme.enhance_config(doms, args.redirect) acme.enhance_config(doms, args.redirect)