mirror of
https://github.com/certbot/certbot.git
synced 2026-06-06 07:12:54 -04:00
towards accounts
This commit is contained in:
parent
31915c5a01
commit
214c0e9355
11 changed files with 266 additions and 95 deletions
115
letsencrypt/client/account.py
Normal file
115
letsencrypt/client/account.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
import configobj
|
||||
import zope.component
|
||||
|
||||
from letsencrypt.acme import messages2
|
||||
|
||||
from letsencrypt.client import crypto_util
|
||||
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
|
||||
|
||||
|
||||
class Account(object):
|
||||
"""ACME protocol registration.
|
||||
|
||||
:ivar config: Client configuration object
|
||||
:type config: :class:`~letsencrypt.client.interfaces.IConfig`
|
||||
:ivar key: Account/Authorized Key
|
||||
:type key: :class:`~letsencrypt.client.le_util.Key`
|
||||
|
||||
:ivar str email: Client's email address
|
||||
:ivar str phone: Client's phone number
|
||||
|
||||
:ivar bool save: Whether or not to save the account information
|
||||
|
||||
:ivar regr: Registration Resource
|
||||
:type regr: :class:`~letsencrypt.acme.messages2.RegistrationResource`
|
||||
|
||||
"""
|
||||
def __init__(self, config, key, email=None, phone=None, regr=None):
|
||||
self.key = key
|
||||
self.config = config
|
||||
self.email = email
|
||||
self.phone = phone
|
||||
|
||||
self.regr = regr
|
||||
|
||||
def save(self):
|
||||
# account_dir = le_util.make_or_verify_dir(
|
||||
# os.path.join(self.config.config_dir, "accounts"))
|
||||
# account_key_dir = le_util.make_or_verify_dir(
|
||||
# os.path.join(account_dir, "keys"), 0o700)
|
||||
|
||||
acc_config = configobj.ConfigObj()
|
||||
# acc_config.filename = os.path.join(
|
||||
# account_dir, self._get_config_filename())
|
||||
acc_config.filename = sys.stdout
|
||||
|
||||
acc_config.initial_comment = [
|
||||
"Account information for %s under %s" % (
|
||||
self._get_config_filename(self.email), self.config.server)]
|
||||
acc_config["key"] = self.key.path
|
||||
acc_config["phone"] = self.phone
|
||||
|
||||
regr_json = self.regr.to_json()
|
||||
regr_dict = json.loads(regr_json)
|
||||
|
||||
acc_config["regr"] = regr_dict
|
||||
acc_config.write()
|
||||
|
||||
@classmethod
|
||||
def _get_config_filename(self, email):
|
||||
return email if email is not None else "default"
|
||||
|
||||
@classmethod
|
||||
def from_existing_account(cls, config, email=None):
|
||||
accounts_dir = os.path.join(
|
||||
config.config_dir, "accounts", config.server)
|
||||
config_fp = os.path.join(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.LetsEncryptClientError(
|
||||
"Account for %s does not exist" % os.path.basename(config_fp))
|
||||
json_regr = json.dumps(acc_config["regr"])
|
||||
return cls(config, acc_config["key"], acc_config["email"],
|
||||
acc_config["phone"],
|
||||
messages2.RegistrationResource.from_json(json_regr))
|
||||
|
||||
@classmethod
|
||||
def choose_account(cls, config):
|
||||
"""Choose one of the available accounts."""
|
||||
accounts = []
|
||||
accounts_dir = os.path.join(config.config_dir, "accounts")
|
||||
filenames = os.listdir(accounts_dir)
|
||||
for name in filenames:
|
||||
# Not some directory ie. keys
|
||||
config_fp = os.path.join(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
|
||||
|
||||
@classmethod
|
||||
def from_prompts(cls, config):
|
||||
email = zope.component.getUtility(interfaces.IDisplay).input(
|
||||
"Enter email address")
|
||||
key_dir = os.path.join(config.config_dir, "accounts", config.server, "keys")
|
||||
key = crypto_util.init_save_key(2048, config.accounts_dir, email)
|
||||
return cls(config, email, key)
|
||||
|
|
@ -172,8 +172,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
|||
raise errors.AuthorizationError(
|
||||
"Failed Authorization procedure for %s" % domain)
|
||||
|
||||
self._cleanup_challenges(comp_challs)
|
||||
self._cleanup_challenges(failed_challs)
|
||||
self._cleanup_challenges(comp_challs+failed_challs)
|
||||
|
||||
dom_to_check -= comp_domains
|
||||
comp_domains.clear()
|
||||
|
|
@ -191,6 +190,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
|||
# challenges will be determined here...
|
||||
for achall in achalls:
|
||||
status = self._get_chall_status(self.authzr[domain], achall)
|
||||
print "Status:", status
|
||||
# This does nothing for challenges that have yet to be decided yet.
|
||||
if status == messages2.STATUS_VALID:
|
||||
completed.append(achall)
|
||||
|
|
@ -209,6 +209,8 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
|||
for authzr_chall in authzr:
|
||||
if type(authzr_chall) is type(chall):
|
||||
return chall.status
|
||||
raise errors.AuthorizationError(
|
||||
"Target challenge not found in authorization resource")
|
||||
|
||||
def _get_chall_pref(self, domain):
|
||||
"""Return list of challenge preferences.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import os
|
|||
import sys
|
||||
|
||||
import M2Crypto
|
||||
import zope.component
|
||||
|
||||
from letsencrypt.acme import jose
|
||||
from letsencrypt.acme.jose import jwk
|
||||
|
|
@ -12,6 +13,7 @@ from letsencrypt.client import auth_handler
|
|||
from letsencrypt.client import continuity_auth
|
||||
from letsencrypt.client import crypto_util
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client import le_util
|
||||
from letsencrypt.client import network2
|
||||
from letsencrypt.client import reverter
|
||||
|
|
@ -31,8 +33,8 @@ class Client(object):
|
|||
:ivar authkey: Authorization Key
|
||||
:type authkey: :class:`letsencrypt.client.le_util.Key`
|
||||
|
||||
:ivar reg: Registration Resource
|
||||
:type reg: :class:`letsencrypt.acme.messages2.RegistrationResource`
|
||||
:ivar account: Account object used for registration
|
||||
:type account: :class:`letsencrypt.client.registration.Registration`
|
||||
|
||||
:ivar auth_handler: Object that supports the IAuthenticator interface.
|
||||
auth_handler contains both a dv_authenticator and a
|
||||
|
|
@ -58,7 +60,7 @@ class Client(object):
|
|||
|
||||
"""
|
||||
self.authkey = authkey
|
||||
self.regr = None
|
||||
self.account = None
|
||||
self.installer = installer
|
||||
|
||||
# TODO: Allow for other alg types besides RS256
|
||||
|
|
@ -75,34 +77,19 @@ class Client(object):
|
|||
else:
|
||||
self.auth_handler = None
|
||||
|
||||
def register(self, email=None, phone=None):
|
||||
def register(self, network, store=True):
|
||||
"""New Registration with the ACME server.
|
||||
|
||||
:param str email: User's email address
|
||||
:param str phone: User's phone number
|
||||
:param bool store: Whether to store the registration information
|
||||
|
||||
"""
|
||||
# TODO: properly format/scrub phone number
|
||||
details = (
|
||||
"mailto:" + email if email is not None else None,
|
||||
"tel:" + phone if phone is not None else None
|
||||
)
|
||||
contact_tuple = tuple(detail for detail in details if detail is not None)
|
||||
|
||||
# TODO: Replace with real info once through testing.
|
||||
if not contact_tuple:
|
||||
contact_tuple = ("mailto:letsencrypt-client@letsencrypt.org",
|
||||
"tel:+12025551212")
|
||||
self.regr = self.network.register(contact=contact_tuple)
|
||||
|
||||
# If terms of service exist... we need to sign it.
|
||||
# TODO: Replace the `preview EULA` with this...
|
||||
if self.regr.terms_of_service:
|
||||
self.network.agree_to_tos(self.regr)
|
||||
|
||||
def set_regr(self, regr):
|
||||
"""Set a preexisting registration resource."""
|
||||
self.regr = regr
|
||||
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 agree:
|
||||
self.account.regr = self.network.agree_to_tos(self.account.regr)
|
||||
# TODO: Handle case where user doesn't agree
|
||||
|
||||
def obtain_certificate(self, domains, csr=None):
|
||||
"""Obtains a certificate from the ACME server.
|
||||
|
|
@ -141,7 +128,8 @@ class Client(object):
|
|||
|
||||
# Create CSR from names
|
||||
if csr is None:
|
||||
csr = init_csr(self.authkey, domains, self.config.cert_dir)
|
||||
csr = crypto_util.init_save_csr(
|
||||
self.authkey, domains, self.config.cert_dir)
|
||||
|
||||
# Retrieve certificate
|
||||
certr = self.network.request_issuance(
|
||||
|
|
@ -323,60 +311,6 @@ def validate_key_csr(privkey, csr=None):
|
|||
"The key and CSR do not match")
|
||||
|
||||
|
||||
def init_key(key_size, key_dir):
|
||||
"""Initializes privkey.
|
||||
|
||||
Inits key and CSR using provided files or generating new files
|
||||
if necessary. Both will be saved in PEM format on the
|
||||
filesystem. The CSR is placed into DER format to allow
|
||||
the namedtuple to easily work with the protocol.
|
||||
|
||||
:param str key_dir: Key save directory.
|
||||
|
||||
"""
|
||||
try:
|
||||
key_pem = crypto_util.make_key(key_size)
|
||||
except ValueError as err:
|
||||
logging.fatal(str(err))
|
||||
sys.exit(1)
|
||||
|
||||
# Save file
|
||||
le_util.make_or_verify_dir(key_dir, 0o700)
|
||||
key_f, key_filename = le_util.unique_file(
|
||||
os.path.join(key_dir, "key-letsencrypt.pem"), 0o600)
|
||||
key_f.write(key_pem)
|
||||
key_f.close()
|
||||
|
||||
logging.info("Generating key (%d bits): %s", key_size, key_filename)
|
||||
|
||||
return le_util.Key(key_filename, key_pem)
|
||||
|
||||
|
||||
def init_csr(privkey, names, cert_dir):
|
||||
"""Initialize a CSR with the given private key.
|
||||
|
||||
:param privkey: Key to include in the CSR
|
||||
:type privkey: :class:`letsencrypt.client.le_util.Key`
|
||||
|
||||
:param set names: `str` names to include in the CSR
|
||||
|
||||
:param str cert_dir: Certificate save directory.
|
||||
|
||||
"""
|
||||
csr_pem, csr_der = crypto_util.make_csr(privkey.pem, names)
|
||||
|
||||
# Save CSR
|
||||
le_util.make_or_verify_dir(cert_dir, 0o755)
|
||||
csr_f, csr_filename = le_util.unique_file(
|
||||
os.path.join(cert_dir, "csr-letsencrypt.pem"), 0o644)
|
||||
csr_f.write(csr_pem)
|
||||
csr_f.close()
|
||||
|
||||
logging.info("Creating CSR: %s", csr_filename)
|
||||
|
||||
return le_util.CSR(csr_filename, csr_der, "der")
|
||||
|
||||
|
||||
def list_available_authenticators(avail_auths):
|
||||
"""Return a pretty-printed list of authenticators.
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,16 @@ class NamespaceConfig(object):
|
|||
self.namespace.work_dir, constants.CERT_KEY_BACKUP_DIR,
|
||||
self.namespace.server.partition(":")[0])
|
||||
|
||||
@property
|
||||
def accounts_dir(self): #pylint: disable=missing-docstring
|
||||
return os.path.join(
|
||||
self.namespace.config_dir, "accounts", self.namespace.server)
|
||||
|
||||
@property
|
||||
def account_keys_dir(self): #pylint: disable=missing-docstring
|
||||
return os.path.join(self.namespace.config_dir, "accounts",
|
||||
self.namespace.server, "keys")
|
||||
|
||||
# TODO: This should probably include the server name
|
||||
@property
|
||||
def rec_token_dir(self): # pylint: disable=missing-docstring
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
is capable of handling the signatures.
|
||||
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
import Crypto.Hash.SHA256
|
||||
|
|
@ -12,7 +14,69 @@ import Crypto.Signature.PKCS1_v1_5
|
|||
|
||||
import M2Crypto
|
||||
|
||||
from letsencrypt.client import le_util
|
||||
|
||||
|
||||
# High level functions
|
||||
def init_save_key(key_size, key_dir, keyname="key-letsencrypt.pem"):
|
||||
"""Initializes and saves a privkey.
|
||||
|
||||
Inits key and saves it in PEM format on the filesystem.
|
||||
|
||||
.. note:: keyname is the attempted filename, it may be different if a file
|
||||
already exists at the path.
|
||||
|
||||
:param int key_size: RSA key size in bits
|
||||
:param str key_dir: Key save directory.
|
||||
:param str keyname: Filename of key
|
||||
|
||||
:raises ValueError: If unable to generate the key given key_size.
|
||||
|
||||
"""
|
||||
try:
|
||||
key_pem = make_key(key_size)
|
||||
except ValueError as err:
|
||||
logging.fatal(str(err))
|
||||
raise err
|
||||
|
||||
# Save file
|
||||
le_util.make_or_verify_dir(key_dir, 0o700)
|
||||
key_f, key_path = le_util.unique_file(
|
||||
os.path.join(key_dir, keyname), 0o600)
|
||||
key_f.write(key_pem)
|
||||
key_f.close()
|
||||
|
||||
logging.info("Generating key (%d bits): %s", key_size, key_path)
|
||||
|
||||
return le_util.Key(key_path, key_pem)
|
||||
|
||||
|
||||
def init_save_csr(privkey, names, cert_dir):
|
||||
"""Initialize a CSR with the given private key.
|
||||
|
||||
:param privkey: Key to include in the CSR
|
||||
:type privkey: :class:`letsencrypt.client.le_util.Key`
|
||||
|
||||
:param set names: `str` names to include in the CSR
|
||||
|
||||
:param str cert_dir: Certificate save directory.
|
||||
|
||||
"""
|
||||
csr_pem, csr_der = make_csr(privkey.pem, names)
|
||||
|
||||
# Save CSR
|
||||
le_util.make_or_verify_dir(cert_dir, 0o755)
|
||||
csr_f, csr_filename = le_util.unique_file(
|
||||
os.path.join(cert_dir, "csr-letsencrypt.pem"), 0o644)
|
||||
csr_f.write(csr_pem)
|
||||
csr_f.close()
|
||||
|
||||
logging.info("Creating CSR: %s", csr_filename)
|
||||
|
||||
return le_util.CSR(csr_filename, csr_der, "der")
|
||||
|
||||
|
||||
# Lower level functions
|
||||
def make_csr(key_str, domains):
|
||||
"""Generate a CSR.
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,26 @@ def choose_authenticator(auths, errs):
|
|||
else:
|
||||
return
|
||||
|
||||
def choose_account(accounts):
|
||||
"""Choose an account.
|
||||
|
||||
:param list accounts: where each is of type
|
||||
:class:`~letsencrypt.client.account.Account`
|
||||
|
||||
"""
|
||||
# Note this will get more complicated once we start recording authorizations
|
||||
|
||||
labels = [
|
||||
"%s | %s" % (acc.email.ljust(display_util.WIDTH - 39), acc.phone)
|
||||
for acc in accounts
|
||||
]
|
||||
|
||||
code, index = util(interfaces.IDisplay).menu(
|
||||
"Please choose an account", labels)
|
||||
if code == display_util.OK:
|
||||
return accounts[index]
|
||||
else:
|
||||
return None
|
||||
|
||||
def choose_names(installer):
|
||||
"""Display screen to select domains to validate.
|
||||
|
|
|
|||
|
|
@ -186,6 +186,19 @@ class Network(object):
|
|||
|
||||
return regr
|
||||
|
||||
def register_from_account(self, account):
|
||||
# TODO: properly format/scrub phone number and email
|
||||
details = (
|
||||
"mailto:" + self.email if self.email is not None else None,
|
||||
"tel:" + self.phone if self.phone is not None else None
|
||||
)
|
||||
|
||||
contact_tuple = tuple(det for det in details if det is not None)
|
||||
|
||||
account.regr = self.register(contact=contact_tuple)
|
||||
|
||||
return account
|
||||
|
||||
def update_registration(self, regr):
|
||||
"""Update registration.
|
||||
|
||||
|
|
|
|||
|
|
@ -1006,15 +1006,16 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
apache_dvsni.add_chall(achall, i)
|
||||
|
||||
sni_response = apache_dvsni.perform()
|
||||
# Must restart in order to activate the challenges.
|
||||
# Handled here because we may be able to load up other challenge types
|
||||
self.restart()
|
||||
if sni_response:
|
||||
# Must restart in order to activate the challenges.
|
||||
# Handled here because we may be able to load up other challenge types
|
||||
self.restart()
|
||||
|
||||
# Go through all of the challenges and assign them to the proper place
|
||||
# in the responses return value. All responses must be in the same order
|
||||
# as the original challenges.
|
||||
for i, resp in enumerate(sni_response):
|
||||
responses[apache_dvsni.indices[i]] = resp
|
||||
# Go through all of the challenges and assign them to the proper
|
||||
# place in the responses return value. All responses must be in the
|
||||
# same order as the original challenges.
|
||||
for i, resp in enumerate(sni_response):
|
||||
responses[apache_dvsni.indices[i]] = resp
|
||||
|
||||
return responses
|
||||
|
||||
|
|
|
|||
10
letsencrypt/client/tests/account_test.py
Normal file
10
letsencrypt/client/tests/account_test.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import mock
|
||||
|
||||
from letsencrypt.client import account
|
||||
from letsencrypt.client import configuration
|
||||
|
||||
|
||||
mock_config = mock.MagicMock(spec=configuration.NamespaceConfig)
|
||||
acc = account.Account.from_prompts(mock_config)
|
||||
|
||||
acc.save()
|
||||
|
|
@ -18,6 +18,7 @@ import letsencrypt
|
|||
|
||||
from letsencrypt.client import configuration
|
||||
from letsencrypt.client import client
|
||||
from letsencrypt.client import crypto_util
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client import le_util
|
||||
|
|
@ -93,7 +94,7 @@ def create_parser():
|
|||
add("--no-confirm", dest="no_confirm", action="store_true",
|
||||
help="Turn off confirmation screens, currently used for --revoke")
|
||||
|
||||
add("-e", "--agree-tos", dest="eula", action="store_true",
|
||||
add("-e", "--agree-tos", dest="tos", action="store_true",
|
||||
help="Skip the end user license agreement screen.")
|
||||
add("-t", "--text", dest="use_curses", action="store_false",
|
||||
help="Use the text output instead of the curses UI.")
|
||||
|
|
@ -163,7 +164,7 @@ def main(): # pylint: disable=too-many-branches, too-many-statements
|
|||
client.rollback(args.rollback, config)
|
||||
sys.exit()
|
||||
|
||||
if not args.eula:
|
||||
if not args.tos:
|
||||
display_eula()
|
||||
|
||||
all_auths = init_auths(config)
|
||||
|
|
@ -195,7 +196,7 @@ def main(): # pylint: disable=too-many-branches, too-many-statements
|
|||
|
||||
# Prepare for init of Client
|
||||
if args.authkey is None:
|
||||
authkey = client.init_key(args.rsa_key_size, config.key_dir)
|
||||
authkey = crypto_util.init_save_key(args.rsa_key_size, config.key_dir)
|
||||
else:
|
||||
authkey = le_util.Key(args.authkey[0], args.authkey[1])
|
||||
|
||||
|
|
|
|||
1
setup.py
1
setup.py
|
|
@ -29,6 +29,7 @@ changes = read_file(os.path.join(here, 'CHANGES.rst'))
|
|||
install_requires = [
|
||||
'argparse',
|
||||
'ConfArgParse',
|
||||
'configobj',
|
||||
'jsonschema',
|
||||
'mock',
|
||||
'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304)
|
||||
|
|
|
|||
Loading…
Reference in a new issue