mirror of
https://github.com/certbot/certbot.git
synced 2026-06-08 08:12:15 -04:00
account integration
This commit is contained in:
parent
3d9d0627d7
commit
ab616a598f
8 changed files with 154 additions and 70 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue