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

View file

@ -1,3 +1,4 @@
import logging
import os
import re
@ -11,7 +12,6 @@ from letsencrypt.client import errors
from letsencrypt.client import interfaces
from letsencrypt.client import le_util
from letsencrypt.client.display import ops as display_ops
from letsencrypt.client.display import util as display_util
@ -142,32 +142,43 @@ class Account(object):
return cls(config, key, email, phone, regr)
@classmethod
def choose_account(cls, config):
"""Choose one of the available accounts."""
def get_accounts(cls, config):
"""Return all current accounts.
:param config: Configuration
:type config: :class:`letsencrypt.client.interfaces.IConfig`
"""
try:
filenames = os.listdir(config.accounts_dir)
except OSError:
return []
accounts = []
filenames = os.listdir(config.accounts_dir)
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))
if len(accounts) == 1:
return accounts[0]
elif len(accounts) > 1:
return display_ops.choose_account(accounts)
else:
return None
return accounts
@classmethod
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(
"Enter email address")
"Enter email address (optional)")
if code == display_util.OK:
email = email if email != "" else None
print config.account_keys_dir
le_util.make_or_verify_dir(
config.account_keys_dir, 0o700, os.geteuid())
key = crypto_util.init_save_key(
@ -181,4 +192,5 @@ class Account(object):
"""Scrub email address before using it."""
if re.match(cls.EMAIL_REGEX, email):
return bool(not email.startswith(".") and ".." not in email)
logging.warn("Invalid email address: using default address.")
return False

View file

@ -26,8 +26,8 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
messages
:type network: :class:`letsencrypt.client.network2.Network`
:ivar authkey: Authorized Keys for domains.
:type authkey: :class:`letsencrypt.client.le_util.Key`
:ivar account: Client's Account
:type account: :class:`letsencrypt.client.account.Account`
:ivar dict authzr: ACME Authorization Resource dict where keys are domains.
: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`
"""
def __init__(self, dv_auth, cont_auth, network, authkey):
def __init__(self, dv_auth, cont_auth, network, account):
self.dv_auth = dv_auth
self.cont_auth = cont_auth
self.network = network
self.authkey = authkey
self.account = account
self.authzr = dict()
# 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):
logging.info(" DVSNI challenge for %s.", domain)
achall = achallenges.DVSNI(
challb=challb, domain=domain, key=self.authkey)
challb=challb, domain=domain, key=self.account.key)
elif isinstance(chall, challenges.SimpleHTTPS):
logging.info(" SimpleHTTPS challenge for %s.", domain)
achall = achallenges.SimpleHTTPS(
challb=challb, domain=domain, key=self.authkey)
challb=challb, domain=domain, key=self.account.key)
elif isinstance(chall, challenges.DNS):
logging.info(" DNS challenge for %s.", domain)
achall = achallenges.DNS(challb=challb, domain=domain)

View file

@ -1,7 +1,6 @@
"""ACME protocol client class and helper functions."""
import logging
import os
import sys
import M2Crypto
import zope.component
@ -9,6 +8,7 @@ import zope.component
from letsencrypt.acme import jose
from letsencrypt.acme.jose import jwk
from letsencrypt.client import account
from letsencrypt.client import auth_handler
from letsencrypt.client import continuity_auth
from letsencrypt.client import crypto_util
@ -30,11 +30,8 @@ class Client(object):
:ivar network: Network object for sending and receiving messages
: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
:type account: :class:`letsencrypt.client.registration.Registration`
:type account: :class:`letsencrypt.client.account.Account`
:ivar auth_handler: Object that supports the IAuthenticator interface.
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.
:param dv_auth: IAuthenticator that can solve the
@ -59,37 +56,39 @@ class Client(object):
:type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
"""
self.authkey = authkey
self.account = None
self.account = account
self.installer = installer
# TODO: Allow for other alg types besides RS256
self.network = network2.Network(
"https://%s/acme/new-reg" % config.server,
jwk.JWKRSA.load(authkey.pem))
jwk.JWKRSA.load(account.key.pem))
self.config = config
if dv_auth is not None:
cont_auth = continuity_auth.ContinuityAuthenticator(config)
self.auth_handler = auth_handler.AuthHandler(
dv_auth, cont_auth, self.network, self.authkey)
dv_auth, cont_auth, self.network, self.account)
else:
self.auth_handler = None
def register(self, network, store=True):
"""New Registration with the ACME server.
:param bool store: Whether to store the registration information
"""
def register(self, save=True):
"""New Registration with the ACME server."""
self.account = self.network.register_from_account(self.account)
if self.account.regr.terms_of_service or self.config.tos:
agree = zope.component.getUtility(interfaces.IDisplay).yesno(
self.account.regr.terms_of_service, "Agree", "Cancel")
if self.account.terms_of_service:
if not self.config.tos:
agree = zope.component.getUtility(interfaces.IDisplay).yesno(
self.account.terms_of_service, "Agree", "Cancel")
else:
agree = True
if agree:
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):
"""Obtains a certificate from the ACME server.
@ -111,14 +110,14 @@ class Client(object):
"not set.")
logging.warning(msg)
raise errors.LetsEncryptClientError(msg)
if self.regr is None:
if self.account.regr is None:
raise errors.LetsEncryptClientError(
"Please register with the ACME server first.")
# Perform Challenges/Get Authorizations
if self.regr.new_authzr_uri:
if self.account.new_authzr_uri:
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...
# and it isn't standardized... ugh - acme-spec #93
else:
@ -129,7 +128,7 @@ class Client(object):
# Create CSR from names
if csr is None:
csr = crypto_util.init_save_csr(
self.authkey, domains, self.config.cert_dir)
self.account.key, domains, self.config.cert_dir)
# Retrieve certificate
certr = self.network.request_issuance(
@ -142,7 +141,7 @@ class Client(object):
certr, self.config.cert_path, self.config.chain_path)
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
@ -379,6 +378,28 @@ def determine_authenticator(all_auths, config):
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):
"""Returns a valid installer if one exists.

View file

@ -187,10 +187,18 @@ class Network(object):
return regr
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 = (
"mailto:" + self.email if self.email is not None else None,
"tel:" + self.phone if self.phone is not None else None
"mailto:" + account.email if account.email 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)

View file

@ -93,7 +93,6 @@ class AccountTest(unittest.TestCase):
self.assertTrue(partial.terms_of_service is None)
self.assertTrue(partial.recovery_token is None)
def test_partial_account_default(self):
partial = account.Account(self.config, self.key)
partial.save()
@ -105,25 +104,19 @@ class AccountTest(unittest.TestCase):
self.assertEqual(partial.phone, acc.phone)
self.assertEqual(partial.regr, acc.regr)
@mock.patch("letsencrypt.client.account.display_ops.choose_account")
def test_choose_account(self, mock_op):
mock_op.return_value = self.test_account
def test_get_accounts(self):
accs = account.Account.get_accounts(self.config)
self.assertFalse(accs)
# Test 0
self.assertTrue(account.Account.choose_account(self.config) is None)
# Test 1
self.test_account.save()
acc = account.Account.choose_account(self.config)
self.assertEqual(acc.email, self.test_account.email)
accs = account.Account.get_accounts(self.config)
self.assertEqual(len(accs), 1)
self.assertEqual(accs[0].email, self.test_account.email)
# Test multiple
self.assertFalse(mock_op.called)
acc2 = account.Account(self.config, self.key)
acc2 = account.Account(self.config, self.key, "testing_email@gmail.com")
acc2.save()
test_acc = account.Account.choose_account(self.config)
self.assertTrue(mock_op.called)
self.assertTrue(test_acc.email, self.test_account.email)
accs = account.Account.get_accounts(self.config)
self.assertEqual(len(accs), 2)
def _read_out_config(self, filep):
print open(os.path.join(self.accounts_dir, filep)).read()

View file

@ -1,10 +1,56 @@
"""letsencrypt.client.client.py tests."""
import os
import unittest
import shutil
import tempfile
import mock
from letsencrypt.client import account
from letsencrypt.client import configuration
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):

View file

@ -72,8 +72,9 @@ class ChooseAccountTest(unittest.TestCase):
server="letsencrypt-demo.org")
self.key = le_util.Key("keypath", "pem")
self.acc1 = account.Account(self.config, self.key, "email1")
self.acc2 = account.Account(self.config, self.key, "email2", "phone")
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()
@ -84,6 +85,7 @@ class ChooseAccountTest(unittest.TestCase):
@mock.patch("letsencrypt.client.display.ops.util")
def test_one(self, mock_util):
print self.acc1
mock_util().menu.return_value = (display_util.OK, 0)
self.assertEqual(self._call([self.acc1]), self.acc1)

View file

@ -196,14 +196,16 @@ def main(): # pylint: disable=too-many-branches, too-many-statements
# Prepare for init of Client
if args.authkey is None:
authkey = crypto_util.init_save_key(args.rsa_key_size, config.key_dir)
account = client.determine_account(config)
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
client.validate_key_csr(authkey)
client.validate_key_csr(account.key)
# This more closely mimics the capabilities of the CLI
# 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()
cert_file, chain_file = acme.obtain_certificate(doms)
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:
acme.enhance_config(doms, args.redirect)