mirror of
https://github.com/certbot/certbot.git
synced 2026-08-03 03:41:53 +02:00
cleanup challenge infrastructure
This commit is contained in:
@@ -30,9 +30,10 @@ IN_PROGRESS_DIR = os.path.join(BACKUP_DIR, "IN_PROGRESS/")
|
|||||||
"""Directory used before a permanent checkpoint is finalized"""
|
"""Directory used before a permanent checkpoint is finalized"""
|
||||||
|
|
||||||
CERT_KEY_BACKUP = os.path.join(WORK_DIR, "keys-certs/")
|
CERT_KEY_BACKUP = os.path.join(WORK_DIR, "keys-certs/")
|
||||||
"""Directory where all certificates/keys are stored.
|
"""Directory where all certificates/keys are stored. Used for easy revocation"""
|
||||||
|
|
||||||
Used for easy revocation"""
|
REV_TOKENS_DIR = os.path.join(WORK_DIR, "revocation_tokens/")
|
||||||
|
"""Directory where all revocation tokens are saved."""
|
||||||
|
|
||||||
KEY_DIR = os.path.join(SERVER_ROOT, "ssl/")
|
KEY_DIR = os.path.join(SERVER_ROOT, "ssl/")
|
||||||
"""Where all keys should be stored"""
|
"""Where all keys should be stored"""
|
||||||
@@ -56,15 +57,15 @@ CHAIN_PATH = CERT_DIR + "chain-letsencrypt.pem"
|
|||||||
INVALID_EXT = ".acme.invalid"
|
INVALID_EXT = ".acme.invalid"
|
||||||
"""Invalid Extension"""
|
"""Invalid Extension"""
|
||||||
|
|
||||||
# Challenge Information
|
|
||||||
#CHALLENGE_PREFERENCES = ["dvsni", "recoveryToken"]
|
|
||||||
"""Challenge Preferences Dict for currently supported challenges"""
|
|
||||||
|
|
||||||
EXCLUSIVE_CHALLENGES = [frozenset(["dvsni", "simpleHttps"])]
|
EXCLUSIVE_CHALLENGES = [frozenset(["dvsni", "simpleHttps"])]
|
||||||
"""Mutually Exclusive Challenges - only solve 1"""
|
"""Mutually Exclusive Challenges - only solve 1"""
|
||||||
|
|
||||||
CONFIG_CHALLENGES = frozenset(["dvsni", "simpleHttps"])
|
AUTH_CHALLENGES = frozenset(["dvsni", "simpleHttps", "dns"])
|
||||||
"""These are challenges that must be solved by a Configurator object"""
|
"""These are challenges that must be solved by an Authenticator object"""
|
||||||
|
|
||||||
|
CLIENT_CHALLENGES = frozenset(
|
||||||
|
["recoveryToken", "recoveryContact", "proofOfPossession"])
|
||||||
|
"""These are challenges that are handled by client.py"""
|
||||||
|
|
||||||
# Challenge Constants
|
# Challenge Constants
|
||||||
S_SIZE = 32
|
S_SIZE = 32
|
||||||
|
|||||||
@@ -811,7 +811,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||||||
"""
|
"""
|
||||||
enabled_dir = os.path.join(self.parser.root, "sites-enabled/")
|
enabled_dir = os.path.join(self.parser.root, "sites-enabled/")
|
||||||
for entry in os.listdir(enabled_dir):
|
for entry in os.listdir(enabled_dir):
|
||||||
if os.path.realpath(enabled_dir + entry) == avail_fp:
|
if os.path.realpath(os.path.join(enabled_dir, entry)) == avail_fp:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@@ -926,7 +926,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||||||
###########################################################################
|
###########################################################################
|
||||||
# Challenges Section
|
# Challenges Section
|
||||||
###########################################################################
|
###########################################################################
|
||||||
def get_chall_pref(self): # pylint: disable=no-self-use
|
# pylint: disable=no-self-use, unused-argument
|
||||||
|
def get_chall_pref(self, domain):
|
||||||
"""Return list of challenge preferences."""
|
"""Return list of challenge preferences."""
|
||||||
|
|
||||||
return ["dvsni"]
|
return ["dvsni"]
|
||||||
@@ -948,10 +949,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||||||
"""
|
"""
|
||||||
self.chall_out += len(chall_list)
|
self.chall_out += len(chall_list)
|
||||||
responses = [None] * len(chall_list)
|
responses = [None] * len(chall_list)
|
||||||
apache_dvsni = dvsni.ApacheDVSNI(self)
|
apache_dvsni = dvsni.ApacheDvsni(self)
|
||||||
|
|
||||||
for i, chall in enumerate(chall_list):
|
for i, chall in enumerate(chall_list):
|
||||||
if isinstance(chall, challenge_util.DVSNI_Chall):
|
if isinstance(chall, challenge_util.DvsniChall):
|
||||||
apache_dvsni.add_chall(chall, i)
|
apache_dvsni.add_chall(chall, i)
|
||||||
|
|
||||||
sni_response = apache_dvsni.perform()
|
sni_response = apache_dvsni.perform()
|
||||||
|
|||||||
@@ -7,18 +7,19 @@ from letsencrypt.client import CONFIG
|
|||||||
|
|
||||||
from letsencrypt.client.apache import parser
|
from letsencrypt.client.apache import parser
|
||||||
|
|
||||||
class ApacheDVSNI(object):
|
|
||||||
|
class ApacheDvsni(object):
|
||||||
"""Class performs DVSNI challenges within the Apache configurator.
|
"""Class performs DVSNI challenges within the Apache configurator.
|
||||||
|
|
||||||
:ivar config: ApacheConfigurator object
|
:ivar config: ApacheConfigurator object
|
||||||
:type config: :class:`letsencrypt.client.apache.configurator`
|
:type config: :class:`letsencrypt.client.apache.configurator`
|
||||||
|
|
||||||
:ivar dvsni_chall: Data required for challenges.
|
:ivar dvsni_chall: Data required for challenges.
|
||||||
where DVSNI_Chall tuples have the following fields
|
where DvsniChall tuples have the following fields
|
||||||
`domain` (`str`), `r_b64` (base64 `str`), `nonce` (hex `str`)
|
`domain` (`str`), `r_b64` (base64 `str`), `nonce` (hex `str`)
|
||||||
`key` (:class:`letsencrypt.client.client.Client.Key`)
|
`key` (:class:`letsencrypt.client.client.Client.Key`)
|
||||||
:type dvsni_chall: `list` of
|
:type dvsni_chall: `list` of
|
||||||
:class:`letsencrypt.client.challenge_util.DVSNI_Chall`
|
:class:`letsencrypt.client.challenge_util.DvsniChall`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
@@ -33,7 +34,7 @@ class ApacheDVSNI(object):
|
|||||||
"""Add challenge to DVSNI object to perform at once.
|
"""Add challenge to DVSNI object to perform at once.
|
||||||
|
|
||||||
:param chall: DVSNI challenge info
|
:param chall: DVSNI challenge info
|
||||||
:type chall: :class:`letsencrypt.client.challenge_util.DVSNI_Chall`
|
:type chall: :class:`letsencrypt.client.challenge_util.DvsniChall`
|
||||||
|
|
||||||
:param int idx: index to challenge in a larger array
|
:param int idx: index to challenge in a larger array
|
||||||
|
|
||||||
@@ -91,19 +92,6 @@ class ApacheDVSNI(object):
|
|||||||
|
|
||||||
return responses
|
return responses
|
||||||
|
|
||||||
# def chall_complete(self, chall):
|
|
||||||
# """Used by Authenticator to notify the DVSNI challenge.
|
|
||||||
|
|
||||||
# :param chall: Challenge info
|
|
||||||
# :type chall: :class:`letsencrypt.client.client.Client.DVSNI_Chall`
|
|
||||||
|
|
||||||
# """
|
|
||||||
# self.completed += 1
|
|
||||||
# if self.completed < len(self.dvsni_chall):
|
|
||||||
# return False
|
|
||||||
# return True
|
|
||||||
|
|
||||||
# TODO: Variable names
|
|
||||||
def mod_config(self, ll_addrs):
|
def mod_config(self, ll_addrs):
|
||||||
"""Modifies Apache config files to include challenge vhosts.
|
"""Modifies Apache config files to include challenge vhosts.
|
||||||
|
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ class ApacheParser(object):
|
|||||||
:rtype: str
|
:rtype: str
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# Checkout fnmatch.py in venv/local/lib/python2.7/fnmatch.py
|
||||||
regex = ""
|
regex = ""
|
||||||
for letter in clean_fn_match:
|
for letter in clean_fn_match:
|
||||||
if letter == '.':
|
if letter == '.':
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ def _find_dumb_path(challenges, preferences):
|
|||||||
|
|
||||||
|
|
||||||
def is_preferred(offered_challenge_type, path):
|
def is_preferred(offered_challenge_type, path):
|
||||||
|
"""Return whether or not the challenge is preferred in path."""
|
||||||
for _, challenge_type in path:
|
for _, challenge_type in path:
|
||||||
for mutually_exclusive in CONFIG.EXCLUSIVE_CHALLENGES:
|
for mutually_exclusive in CONFIG.EXCLUSIVE_CHALLENGES:
|
||||||
# Second part is in case we eventually allow multiple names
|
# Second part is in case we eventually allow multiple names
|
||||||
|
|||||||
@@ -8,8 +8,17 @@ from letsencrypt.client import CONFIG
|
|||||||
from letsencrypt.client import crypto_util
|
from letsencrypt.client import crypto_util
|
||||||
from letsencrypt.client import le_util
|
from letsencrypt.client import le_util
|
||||||
|
|
||||||
|
# Authenticator Challenges
|
||||||
|
DvsniChall = collections.namedtuple("DvsniChall", "domain, r_b64, nonce, key")
|
||||||
|
SimpleHttpsChall = collections.namedtuple(
|
||||||
|
"SimpleHttpsChall", "domain, token, key")
|
||||||
|
DnsChall = collections.namedtuple("DnsChall", "domain, token, key")
|
||||||
|
|
||||||
DVSNI_Chall = collections.namedtuple("DVSNI_Chall", "domain, r_b64, nonce, key")
|
# Client Challenges
|
||||||
|
RecContactChall = collections.namedtuple(
|
||||||
|
"RecContactChall", "domain, a_url, s_url, contact")
|
||||||
|
RecTokenChall = collections.namedtuple("RecTokenChall", "domain")
|
||||||
|
PopChall = collections.namedtuple("PopChall", "domain, alg, nonce, hints")
|
||||||
|
|
||||||
|
|
||||||
# DVSNI Challenge functions
|
# DVSNI Challenge functions
|
||||||
|
|||||||
+129
-42
@@ -20,6 +20,7 @@ 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 import network
|
from letsencrypt.client import network
|
||||||
|
from letsencrypt.client import recovery_token
|
||||||
|
|
||||||
|
|
||||||
# it's weird to point to chocolate servers via raw IPv6 addresses, and
|
# it's weird to point to chocolate servers via raw IPv6 addresses, and
|
||||||
@@ -40,12 +41,15 @@ class Client(object):
|
|||||||
:type authkey: :class:`letsencrypt.client.client.Client.Key`
|
:type authkey: :class:`letsencrypt.client.client.Client.Key`
|
||||||
|
|
||||||
:ivar auth: Object that supports the IAuthenticator interface.
|
:ivar auth: Object that supports the IAuthenticator interface.
|
||||||
|
`auth` is used specifically for CONFIG.AUTH_CHALLENGES
|
||||||
:type auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
|
:type auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
|
||||||
|
|
||||||
:ivar installer: Object supporting the IInstaller interface.
|
:ivar installer: Object supporting the IInstaller interface.
|
||||||
:type installer: :class:`letsencrypt.client.interfaces.IInstraller`
|
:type installer: :class:`letsencrypt.client.interfaces.IInstraller`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
zope.interface.implements(interfaces.IAuthenticator)
|
||||||
|
|
||||||
Key = collections.namedtuple("Key", "file pem")
|
Key = collections.namedtuple("Key", "file pem")
|
||||||
CSR = collections.namedtuple("CSR", "file data form")
|
CSR = collections.namedtuple("CSR", "file data form")
|
||||||
|
|
||||||
@@ -60,14 +64,7 @@ class Client(object):
|
|||||||
self.auth = auth
|
self.auth = auth
|
||||||
self.installer = installer
|
self.installer = installer
|
||||||
|
|
||||||
# Client challenges and Authenticator challenges should be separate
|
self.rec_token = recovery_token.RecoveryToken(server)
|
||||||
# and really should not be conflicting along the same path.
|
|
||||||
# I have chosen to make client challenges preferred
|
|
||||||
# as the client challenges should be able to be completely handled
|
|
||||||
# by this module and does not require outside config changes.
|
|
||||||
# (which may be costly)
|
|
||||||
self.preferences = ["recoveryToken"]
|
|
||||||
self.preferences.extend(auth.get_chall_pref())
|
|
||||||
|
|
||||||
def obtain_certificate(self, csr,
|
def obtain_certificate(self, csr,
|
||||||
cert_path=CONFIG.CERT_PATH,
|
cert_path=CONFIG.CERT_PATH,
|
||||||
@@ -104,12 +101,9 @@ class Client(object):
|
|||||||
while i < len(responses):
|
while i < len(responses):
|
||||||
# Get Authorization
|
# Get Authorization
|
||||||
if responses[i] is not None:
|
if responses[i] is not None:
|
||||||
print "client chall_msgs:", challenge_msgs[i]
|
|
||||||
print "client responses:", responses[i]
|
|
||||||
print "client auth_c:", auth_c[i]
|
|
||||||
print "client client_c:", client_c[i]
|
|
||||||
self.acme_authorization(
|
self.acme_authorization(
|
||||||
challenge_msgs[i], auth_c[i], client_c[i], responses[i])
|
challenge_msgs[i], self.names[i],
|
||||||
|
auth_c[i], client_c[i], responses[i])
|
||||||
# Received authorization, remove challenge from list
|
# Received authorization, remove challenge from list
|
||||||
# We have also cleaned up challenges... keep index
|
# We have also cleaned up challenges... keep index
|
||||||
# in sync
|
# in sync
|
||||||
@@ -146,13 +140,15 @@ class Client(object):
|
|||||||
return self.network.send_and_receive_expected(
|
return self.network.send_and_receive_expected(
|
||||||
acme.challenge_request(domain), "challenge")
|
acme.challenge_request(domain), "challenge")
|
||||||
|
|
||||||
def acme_authorization(self, challenge_msg, auth_c, client_c, responses):
|
def acme_authorization(
|
||||||
|
self, challenge_msg, domain, auth_c, client_c, responses):
|
||||||
"""Handle ACME "authorization" phase.
|
"""Handle ACME "authorization" phase.
|
||||||
|
|
||||||
:param dict challenge_msg: ACME "challenge" message.
|
:param dict challenge_msg: ACME "challenge" message.
|
||||||
|
:param str domain: domain that is requesting authorization
|
||||||
:param chal_objs: TODO - this will be a new object...
|
:param list auth_c: auth challenges
|
||||||
:param responses: TODO
|
:param list client_c: client challenges
|
||||||
|
:param list responses: Responses to all challenges in challenge_msg
|
||||||
|
|
||||||
:returns: ACME "authorization" message.
|
:returns: ACME "authorization" message.
|
||||||
:rtype: dict
|
:rtype: dict
|
||||||
@@ -161,7 +157,7 @@ class Client(object):
|
|||||||
try:
|
try:
|
||||||
return self.network.send_and_receive_expected(
|
return self.network.send_and_receive_expected(
|
||||||
acme.authorization_request(
|
acme.authorization_request(
|
||||||
challenge_msg["sessionID"], self.names[0],
|
challenge_msg["sessionID"], domain,
|
||||||
challenge_msg["nonce"], responses, self.authkey.pem),
|
challenge_msg["nonce"], responses, self.authkey.pem),
|
||||||
"authorization")
|
"authorization")
|
||||||
except errors.LetsEncryptClientError as err:
|
except errors.LetsEncryptClientError as err:
|
||||||
@@ -322,10 +318,20 @@ class Client(object):
|
|||||||
auth_idx = []
|
auth_idx = []
|
||||||
client_idx = []
|
client_idx = []
|
||||||
|
|
||||||
|
# Client challenges and Authenticator challenges should be separate
|
||||||
|
# and really should not be conflicting along the same path.
|
||||||
|
# I have chosen to make client challenges preferred
|
||||||
|
# as the client challenges should be able to be completely handled
|
||||||
|
# by this module and does not require outside config changes.
|
||||||
|
# (which may be costly)
|
||||||
|
|
||||||
for i, msg in enumerate(challenge_msgs):
|
for i, msg in enumerate(challenge_msgs):
|
||||||
|
prefs = self.get_chall_pref(self.names[i])
|
||||||
|
prefs.extend(self.auth.get_chall_pref(self.names[i]))
|
||||||
|
|
||||||
paths.append(challenge.gen_challenge_path(
|
paths.append(challenge.gen_challenge_path(
|
||||||
msg["challenges"],
|
msg["challenges"],
|
||||||
self.preferences,
|
prefs,
|
||||||
msg.get("combinations", [])))
|
msg.get("combinations", [])))
|
||||||
|
|
||||||
logging.info("Performing the following challenges:")
|
logging.info("Performing the following challenges:")
|
||||||
@@ -340,20 +346,16 @@ class Client(object):
|
|||||||
|
|
||||||
responses.append(["null"] * len(msg["challenges"]))
|
responses.append(["null"] * len(msg["challenges"]))
|
||||||
|
|
||||||
# Do client centric challenges here...
|
# Flatten list for client authenticator functions
|
||||||
# Since this isn't implemented yet...
|
client_resp = self.perform(
|
||||||
# Client challenge responses should be cached...
|
[chall for sublist in client_chall for chall in sublist])
|
||||||
# The client should be able to solve all challenges the first time
|
self._assign_responses(client_resp, client_idx, responses)
|
||||||
assert not client_i
|
|
||||||
# Flatten list for authenticator
|
# Flatten list for auth authenticator
|
||||||
auth_resp = self.auth.perform(
|
auth_resp = self.auth.perform(
|
||||||
[chall for sublist in auth_chall for chall in sublist])
|
[chall for sublist in auth_chall for chall in sublist])
|
||||||
self._assign_responses(auth_resp, auth_idx, responses)
|
self._assign_responses(auth_resp, auth_idx, responses)
|
||||||
|
|
||||||
print 'auth_resp:', auth_resp
|
|
||||||
print 'auth_idx:', auth_idx
|
|
||||||
print 'auth_responses:', responses
|
|
||||||
|
|
||||||
for i in range(len(paths)):
|
for i in range(len(paths)):
|
||||||
# If challenges failed to complete... zero them out
|
# If challenges failed to complete... zero them out
|
||||||
if not self._path_satisfied(responses[i], paths[i]):
|
if not self._path_satisfied(responses[i], paths[i]):
|
||||||
@@ -476,9 +478,17 @@ class Client(object):
|
|||||||
|
|
||||||
:param list path: List of indices from `challenges`.
|
:param list path: List of indices from `challenges`.
|
||||||
|
|
||||||
:returns: A pair of TODO
|
:returns: auth_chall, list of `collections.namedtuples`
|
||||||
|
auth_satisfies, list of indices, each associated auth_chall
|
||||||
|
satisfieswithin the challenge_msg
|
||||||
|
client_chall, list of `collections.namedtuples`
|
||||||
|
client_satisfies, list of indices each associated client_chall
|
||||||
|
satisfies within the challenge_msg
|
||||||
:rtype: tuple
|
:rtype: tuple
|
||||||
|
|
||||||
|
:raises errors.LetsEncryptClientError: If Challenge type is not
|
||||||
|
recognized
|
||||||
|
|
||||||
"""
|
"""
|
||||||
auth_chall = []
|
auth_chall = []
|
||||||
# Since a single invocation of SNI challenge can satisfy multiple
|
# Since a single invocation of SNI challenge can satisfy multiple
|
||||||
@@ -487,29 +497,106 @@ class Client(object):
|
|||||||
|
|
||||||
client_chall = []
|
client_chall = []
|
||||||
client_satisfies = []
|
client_satisfies = []
|
||||||
|
domain = str(domain)
|
||||||
|
|
||||||
for index in path:
|
for index in path:
|
||||||
chall = challenges[index]
|
chall = challenges[index]
|
||||||
|
|
||||||
if chall["type"] == "dvsni":
|
# Authenticator Challenges
|
||||||
logging.info(" DVSNI challenge for name %s.", domain)
|
if chall["type"] in CONFIG.AUTH_CHALLENGES:
|
||||||
|
auth_chall.append(self._construct_auth_chall(chall, domain))
|
||||||
auth_satisfies.append(index)
|
auth_satisfies.append(index)
|
||||||
auth_chall.append(challenge_util.DVSNI_Chall(
|
|
||||||
str(domain), str(chall["r"]),
|
|
||||||
str(chall["nonce"]), self.authkey))
|
|
||||||
|
|
||||||
elif chall["type"] == "recoveryToken":
|
# Client Challenges
|
||||||
logging.info(" Recovery Token Challenge for name: %s.", domain)
|
elif chall["type"] in CONFIG.CLIENT_CHALLENGES:
|
||||||
|
client_chall.append(self._construct_client_chall(chall, domain))
|
||||||
client_satisfies.append(index)
|
client_satisfies.append(index)
|
||||||
client_chall.append({
|
|
||||||
type: "recoveryToken",
|
|
||||||
})
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logging.fatal("Challenge not currently supported")
|
raise errors.LetsEncryptClientError(
|
||||||
sys.exit(82)
|
"Received unrecognized challenge of type: "
|
||||||
|
"%s" % chall["type"])
|
||||||
|
|
||||||
return auth_chall, auth_satisfies, client_chall, client_satisfies
|
return auth_chall, auth_satisfies, client_chall, client_satisfies
|
||||||
|
|
||||||
|
def _construct_auth_chall(self, chall, domain):
|
||||||
|
"""Construct Auth Type Challenges.
|
||||||
|
|
||||||
|
:param dict chall: Single challenge
|
||||||
|
|
||||||
|
:returns: challenge_util named tuple Chall object
|
||||||
|
:rtype: `collections.namedtuple`
|
||||||
|
|
||||||
|
:raises errors.LetsEncryptClientError: If unimplemented challenge exists
|
||||||
|
|
||||||
|
"""
|
||||||
|
if chall["type"] == "dvsni":
|
||||||
|
logging.info(" DVSNI challenge for name %s.", domain)
|
||||||
|
return challenge_util.DvsniChall(
|
||||||
|
domain, str(chall["r"]), str(chall["nonce"]), self.authkey)
|
||||||
|
|
||||||
|
elif chall["type"] == "simpleHttps":
|
||||||
|
logging.info(" SimpleHTTPS challenge for name %s.", domain)
|
||||||
|
return challenge_util.SimpleHttpsChall(
|
||||||
|
domain, str(chall["token"]), self.authkey)
|
||||||
|
|
||||||
|
elif chall["type"] == "dns":
|
||||||
|
logging.info(" DNS challenge for name %s.", domain)
|
||||||
|
return challenge_util.DnsChall(
|
||||||
|
domain, str(chall["token"]), self.authkey)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise errors.LetsEncryptClientError(
|
||||||
|
"Unimplemented Auth Challenge: %s" % chall["type"])
|
||||||
|
|
||||||
|
def _construct_client_chall(self, chall, domain):
|
||||||
|
"""Construct Client Type Challenges.
|
||||||
|
|
||||||
|
:param dict chall: Single challenge
|
||||||
|
|
||||||
|
:returns: challenge_util named tuple Chall object
|
||||||
|
:rtype: `collections.namedtuple`
|
||||||
|
|
||||||
|
:raises errors.LetsEncryptClientError: If unimplemented challenge exists
|
||||||
|
|
||||||
|
"""
|
||||||
|
if chall["type"] == "recoveryToken":
|
||||||
|
logging.info(" Recovery Token Challenge for name: %s.", domain)
|
||||||
|
return challenge_util.RecTokenChall(domain)
|
||||||
|
|
||||||
|
elif chall["type"] == "recoveryContact":
|
||||||
|
logging.info(" Recovery Contact Challenge for name: %s.", domain)
|
||||||
|
return challenge_util.RecContactChall(
|
||||||
|
domain,
|
||||||
|
chall.get("activationURL", None),
|
||||||
|
chall.get("successURL", None),
|
||||||
|
chall.get("contact", None))
|
||||||
|
|
||||||
|
elif chall["type"] == "proofOfPossession":
|
||||||
|
logging.info(" Proof-of-Possession Challenge for name: "
|
||||||
|
"%s", domain)
|
||||||
|
return challenge_util.PopChall(
|
||||||
|
domain, chall["alg"], chall["nonce"], chall["hints"])
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise errors.LetsEncryptClientError(
|
||||||
|
"Unimplemented Client Challenge: %s" % chall["type"])
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def get_chall_pref(self, domain):
|
||||||
|
"""Return list of challenge preferences."""
|
||||||
|
return ["recoveryToken"]
|
||||||
|
|
||||||
|
def perform(self, chall_list):
|
||||||
|
"""Perform client specific challenges."""
|
||||||
|
responses = []
|
||||||
|
for chall in chall_list:
|
||||||
|
if isinstance(chall, challenge_util.RecTokenChall):
|
||||||
|
responses.append(self.rec_token.perform(chall))
|
||||||
|
else:
|
||||||
|
raise errors.LetsEncryptClientError("Unexpected Challenge")
|
||||||
|
return responses
|
||||||
|
|
||||||
|
|
||||||
def validate_key_csr(privkey, csr):
|
def validate_key_csr(privkey, csr):
|
||||||
"""Validate CSR and key files.
|
"""Validate CSR and key files.
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ class IAuthenticator(zope.interface.Interface):
|
|||||||
ability to perform challenges and attain a certificate.
|
ability to perform challenges and attain a certificate.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def get_chall_pref():
|
def get_chall_pref(domain):
|
||||||
"""Return list of challenge preferences.
|
"""Return list of challenge preferences.
|
||||||
|
|
||||||
|
:param str domain: Domain for which challenge preferences are sought.
|
||||||
|
|
||||||
:returns: list of strings with the most preferred challenges first.
|
:returns: list of strings with the most preferred challenges first.
|
||||||
:rtype: list
|
:rtype: list
|
||||||
|
|
||||||
@@ -22,7 +24,7 @@ class IAuthenticator(zope.interface.Interface):
|
|||||||
"""Perform the given challenge.
|
"""Perform the given challenge.
|
||||||
|
|
||||||
:param list chall_list: List of challenge types defined in client.py
|
:param list chall_list: List of challenge types defined in client.py
|
||||||
|
|
||||||
:returns: List of responses
|
:returns: List of responses
|
||||||
If the challenge cant be completed...
|
If the challenge cant be completed...
|
||||||
None - Authenticator can perform challenge, but can't at this time
|
None - Authenticator can perform challenge, but can't at this time
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"""Recovery Token Identifier Validation Challenge."""
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
|
||||||
|
import zope.component
|
||||||
|
# import zope.interface
|
||||||
|
|
||||||
|
from letsencrypt.client import CONFIG
|
||||||
|
from letsencrypt.client import le_util
|
||||||
|
from letsencrypt.client import interfaces
|
||||||
|
|
||||||
|
|
||||||
|
class RecoveryToken(object):
|
||||||
|
"""Recovery Token Identifier Validation Challenge.
|
||||||
|
|
||||||
|
Based on draft-barnes-acme, section 6.4.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# zope.interface.implements(interfaces.IChallenge)
|
||||||
|
|
||||||
|
def __init__(self, server, direc=CONFIG.REV_TOKENS_DIR):
|
||||||
|
# super(RecoveryToken, self).__init__()
|
||||||
|
self.token_dir = os.path.join(direc, server)
|
||||||
|
|
||||||
|
def perform(self, chall):
|
||||||
|
"""Perform the Recovery Token Challenge.
|
||||||
|
|
||||||
|
:param chall: Recovery Token Challenge
|
||||||
|
:type chall: :class:`letsencrypt.client.challenge_util.RecTokenChall`
|
||||||
|
|
||||||
|
:returns: response
|
||||||
|
:rtype: dict
|
||||||
|
|
||||||
|
"""
|
||||||
|
token_fp = os.path.join(self.token_dir, chall.domain)
|
||||||
|
if os.path.isfile(token_fp):
|
||||||
|
with open(token_fp) as token_fd:
|
||||||
|
return self.generate_response(token_fd.read())
|
||||||
|
|
||||||
|
cancel, token = zope.component.getUtility(
|
||||||
|
interfaces.IDisplay).generic_input(
|
||||||
|
"%s - Input Recovery Token: " % chall.domain)
|
||||||
|
if cancel != 1:
|
||||||
|
return self.generate_response(token)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def cleanup(self, chall):
|
||||||
|
"""Cleanup the saved recovery token if it exists.
|
||||||
|
|
||||||
|
:param chall: Recovery Token Challenge
|
||||||
|
:type chall: :class:`letsencrypt.client.challenge_util.RecTokenChall`
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
os.remove(os.path.join(self.token_dir, chall.domain))
|
||||||
|
except OSError as err:
|
||||||
|
if err.errno != errno.ENOENT:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def generate_response(self, token): # pylint: disable=no-self-use
|
||||||
|
"""Generate json response."""
|
||||||
|
return {
|
||||||
|
"type": "recoveryToken",
|
||||||
|
"token": token,
|
||||||
|
}
|
||||||
|
|
||||||
|
def requires_human(self, domain):
|
||||||
|
"""Indicates whether or not domain can be auto solved."""
|
||||||
|
return not os.path.isfile(os.path.join(self.token_dir, domain))
|
||||||
|
|
||||||
|
def store_token(self, domain, token):
|
||||||
|
"""Store token for later automatic use.
|
||||||
|
|
||||||
|
:param str domain: domain associated with the token
|
||||||
|
:param str token: token from authorization
|
||||||
|
|
||||||
|
"""
|
||||||
|
le_util.make_or_verify_dir(self.token_dir, 0o700, os.geteuid())
|
||||||
|
|
||||||
|
with open(os.path.join(self.token_dir, domain), 'w') as token_fd:
|
||||||
|
token_fd.write(str(token))
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
"""Recovery Token Identifier Validation Challenge.
|
|
||||||
|
|
||||||
.. note:: This challenge has not been implemented into the project yet
|
|
||||||
|
|
||||||
"""
|
|
||||||
import zope.component
|
|
||||||
import zope.interface
|
|
||||||
|
|
||||||
from letsencrypt.client import interfaces
|
|
||||||
|
|
||||||
|
|
||||||
class RecoveryToken(object):
|
|
||||||
"""Recovery Token Identifier Validation Challenge.
|
|
||||||
|
|
||||||
Based on draft-barnes-acme, section 6.4.
|
|
||||||
|
|
||||||
"""
|
|
||||||
zope.interface.implements(interfaces.IChallenge)
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(RecoveryToken, self).__init__()
|
|
||||||
self.token = ""
|
|
||||||
|
|
||||||
def perform(self, quiet=True):
|
|
||||||
cancel, self.token = zope.component.getUtility(
|
|
||||||
interfaces.IDisplay).generic_input(
|
|
||||||
"Please Input Recovery Token: ")
|
|
||||||
return cancel != 1
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def generate_response(self):
|
|
||||||
return {
|
|
||||||
"type": "recoveryToken",
|
|
||||||
"token": self.token,
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
"""Class helps construct valid ACME messages for testing."""
|
||||||
|
from letsencrypt.client import CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
CHALLENGES = {
|
||||||
|
"simpleHttps":
|
||||||
|
{
|
||||||
|
"type": "simpleHttps",
|
||||||
|
"token": "evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA"
|
||||||
|
},
|
||||||
|
"dvsni":
|
||||||
|
{
|
||||||
|
"type": "dvsni",
|
||||||
|
"r": "Tyq0La3slT7tqQ0wlOiXnCY2vyez7Zo5blgPJ1xt5xI",
|
||||||
|
"nonce": "a82d5ff8ef740d12881f6d3c2277ab2e"
|
||||||
|
},
|
||||||
|
"dns":
|
||||||
|
{
|
||||||
|
"type": "dns",
|
||||||
|
"token": "17817c66b60ce2e4012dfad92657527a"
|
||||||
|
},
|
||||||
|
"recoveryContact":
|
||||||
|
{
|
||||||
|
"type": "recoveryContact",
|
||||||
|
"activationURL": "https://example.ca/sendrecovery/a5bd99383fb0",
|
||||||
|
"successURL": "https://example.ca/confirmrecovery/bb1b9928932",
|
||||||
|
"contact": "c********n@example.com"
|
||||||
|
},
|
||||||
|
"recoveryTokent":
|
||||||
|
{
|
||||||
|
"type": "recoveryToken"
|
||||||
|
},
|
||||||
|
"proofOfPossession":
|
||||||
|
{
|
||||||
|
"type": "proofOfPossession",
|
||||||
|
"alg": "RS256",
|
||||||
|
"nonce": "eET5udtV7aoX8Xl8gYiZIA",
|
||||||
|
"hints": {
|
||||||
|
"jwk": {
|
||||||
|
"kty": "RSA",
|
||||||
|
"e": "AQAB",
|
||||||
|
"n": "KxITJ0rNlfDMAtfDr8eAw...fSSoehDFNZKQKzTZPtQ"
|
||||||
|
},
|
||||||
|
"certFingerprints": [
|
||||||
|
"93416768eb85e33adc4277f4c9acd63e7418fcfe",
|
||||||
|
"16d95b7b63f1972b980b14c20291f3c0d1855d95",
|
||||||
|
"48b46570d9fc6358108af43ad1649484def0debf"
|
||||||
|
],
|
||||||
|
"subjectKeyIdentifiers":
|
||||||
|
["d0083162dcc4c8a23ecb8aecbd86120e56fd24e5"],
|
||||||
|
"serialNumbers": [34234239832, 23993939911, 17],
|
||||||
|
"issuers": [
|
||||||
|
"C=US, O=SuperT LLC, CN=SuperTrustworthy Public CA",
|
||||||
|
"O=LessTrustworthy CA Inc, CN=LessTrustworthy But StillSecure"
|
||||||
|
],
|
||||||
|
"authorizedFor": ["www.example.com", "example.net"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_challenges():
|
||||||
|
"""Returns all auth challenges."""
|
||||||
|
return [chall for typ, chall in CHALLENGES.iteritems()
|
||||||
|
if typ in CONFIG.AUTH_CHALLENGES]
|
||||||
|
|
||||||
|
|
||||||
|
def get_client_challenges():
|
||||||
|
"""Returns all client challenges."""
|
||||||
|
return [chall for typ, chall in CHALLENGES.iteritems()
|
||||||
|
if typ in CONFIG.CLIENT_CHALLENGES]
|
||||||
|
|
||||||
|
|
||||||
|
def get_challenges():
|
||||||
|
"""Returns all challenges."""
|
||||||
|
return [chall for chall in CHALLENGES.itervalues()]
|
||||||
|
|
||||||
|
|
||||||
|
def gen_combos(challs):
|
||||||
|
"""Generate natural combinations for challs."""
|
||||||
|
dv_chall = []
|
||||||
|
renewal_chall = []
|
||||||
|
combos = []
|
||||||
|
|
||||||
|
for i, chall in enumerate(challs):
|
||||||
|
if chall["type"] in CONFIG.AUTH_CHALLENGES:
|
||||||
|
dv_chall.append(i)
|
||||||
|
else:
|
||||||
|
renewal_chall.append(i)
|
||||||
|
|
||||||
|
# Gen combos for 1 of each type
|
||||||
|
for i in range(len(dv_chall)):
|
||||||
|
for j in range(len(renewal_chall)):
|
||||||
|
combos.append([i, j])
|
||||||
|
|
||||||
|
return combos
|
||||||
|
|
||||||
|
|
||||||
|
def get_chall_msg(iden, nonce, challenges, combos=None):
|
||||||
|
"""Produce an ACME challenge message."""
|
||||||
|
chall_msg = {
|
||||||
|
"type": "challenge",
|
||||||
|
"sessionID": iden,
|
||||||
|
"nonce": nonce,
|
||||||
|
"challenges": challenges
|
||||||
|
}
|
||||||
|
|
||||||
|
if combos is None:
|
||||||
|
return chall_msg
|
||||||
|
|
||||||
|
chall_msg["combinations"] = combos
|
||||||
|
return chall_msg
|
||||||
@@ -162,8 +162,11 @@ class TwoVhost80Test(unittest.TestCase):
|
|||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
errors.LetsEncryptConfiguratorError, self.config.get_version)
|
errors.LetsEncryptConfiguratorError, self.config.get_version)
|
||||||
|
|
||||||
@mock.patch("letsencrypt.client.apache.dvsni")
|
@mock.patch("letsencrypt.client.apache.configurator."
|
||||||
def test_perform(self, mock_dvsni, mock_restart):
|
"dvsni.ApacheDvsni.perform")
|
||||||
|
@mock.patch("letsencrypt.client.apache.configurator."
|
||||||
|
"ApacheConfigurator.restart")
|
||||||
|
def test_perform(self, mock_restart, mock_dvsni_perform):
|
||||||
# Only tests functionality specific to configurator.perform
|
# Only tests functionality specific to configurator.perform
|
||||||
# Note: As more challenges are offered this will have to be expanded
|
# Note: As more challenges are offered this will have to be expanded
|
||||||
rsa256_file = pkg_resources.resource_filename(
|
rsa256_file = pkg_resources.resource_filename(
|
||||||
@@ -172,12 +175,12 @@ class TwoVhost80Test(unittest.TestCase):
|
|||||||
__name__, 'testdata/rsa256_key.pem')
|
__name__, 'testdata/rsa256_key.pem')
|
||||||
|
|
||||||
auth_key = client.Client.Key(rsa256_file, rsa256_pem)
|
auth_key = client.Client.Key(rsa256_file, rsa256_pem)
|
||||||
chall1 = challenge_util.DVSNI_Chall(
|
chall1 = challenge_util.DvsniChall(
|
||||||
"encryption-example.demo",
|
"encryption-example.demo",
|
||||||
"jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q",
|
"jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q",
|
||||||
"37bc5eb75d3e00a19b4f6355845e5a18",
|
"37bc5eb75d3e00a19b4f6355845e5a18",
|
||||||
auth_key)
|
auth_key)
|
||||||
chall2 = challenge_util.DVSNI_Chall(
|
chall2 = challenge_util.DvsniChall(
|
||||||
"letsencrypt.demo",
|
"letsencrypt.demo",
|
||||||
"uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU",
|
"uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU",
|
||||||
"59ed014cac95f77057b1d7a1b2c596ba",
|
"59ed014cac95f77057b1d7a1b2c596ba",
|
||||||
@@ -188,11 +191,13 @@ class TwoVhost80Test(unittest.TestCase):
|
|||||||
{"type": "dvsni", "s": "randomS2"}
|
{"type": "dvsni", "s": "randomS2"}
|
||||||
]
|
]
|
||||||
|
|
||||||
mock_dvsni().perform.return_value = dvsni_ret_val
|
mock_dvsni_perform.return_value = dvsni_ret_val
|
||||||
responses = self.config.perform([chall1, chall2])
|
responses = self.config.perform([chall1, chall2])
|
||||||
|
|
||||||
self.assertEqual(mock_dvsni.perform.call_count, 1)
|
self.assertEqual(mock_dvsni_perform.call_count, 1)
|
||||||
self.assertEqual(responses, dvsni_ret_val)
|
self.assertEqual(responses, dvsni_ret_val)
|
||||||
|
|
||||||
|
self.assertEqual(mock_restart.call_count, 1)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
"""Test for letsencrypt.client.apache.dvsni."""
|
||||||
|
import os
|
||||||
|
import pkg_resources
|
||||||
|
import unittest
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
import mock
|
||||||
|
import zope.component
|
||||||
|
|
||||||
|
from letsencrypt.client import challenge_util
|
||||||
|
from letsencrypt.client import client
|
||||||
|
from letsencrypt.client import CONFIG
|
||||||
|
from letsencrypt.client import display
|
||||||
|
|
||||||
|
from letsencrypt.client.apache import obj
|
||||||
|
|
||||||
|
from letsencrypt.client.tests import config_util
|
||||||
|
|
||||||
|
|
||||||
|
class DvsniPerformTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
from letsencrypt.client.apache import dvsni
|
||||||
|
zope.component.provideUtility(display.NcursesDisplay())
|
||||||
|
|
||||||
|
self.temp_dir, self.config_dir, self.work_dir = config_util.dir_setup(
|
||||||
|
"debian_apache_2_4/two_vhost_80")
|
||||||
|
|
||||||
|
self.ssl_options = config_util.setup_apache_ssl_options(self.config_dir)
|
||||||
|
|
||||||
|
# Final slash is currently important
|
||||||
|
self.config_path = os.path.join(
|
||||||
|
self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2/")
|
||||||
|
|
||||||
|
config = config_util.get_apache_configurator(
|
||||||
|
self.config_path, self.config_dir, self.work_dir, self.ssl_options)
|
||||||
|
|
||||||
|
self.sni = dvsni.ApacheDvsni(config)
|
||||||
|
|
||||||
|
rsa256_file = pkg_resources.resource_filename(
|
||||||
|
__name__, 'testdata/rsa256_key.pem')
|
||||||
|
rsa256_pem = pkg_resources.resource_string(
|
||||||
|
__name__, 'testdata/rsa256_key.pem')
|
||||||
|
|
||||||
|
auth_key = client.Client.Key(rsa256_file, rsa256_pem)
|
||||||
|
self.chall1 = challenge_util.DvsniChall(
|
||||||
|
"encryption-example.demo",
|
||||||
|
"jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q",
|
||||||
|
"37bc5eb75d3e00a19b4f6355845e5a18",
|
||||||
|
auth_key)
|
||||||
|
self.chall2 = challenge_util.DvsniChall(
|
||||||
|
"letsencrypt.demo",
|
||||||
|
"uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU",
|
||||||
|
"59ed014cac95f77057b1d7a1b2c596ba",
|
||||||
|
auth_key)
|
||||||
|
|
||||||
|
self.sni.add_chall(self.chall1)
|
||||||
|
self.sni.add_chall(self.chall2)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
shutil.rmtree(self.temp_dir)
|
||||||
|
shutil.rmtree(self.config_dir)
|
||||||
|
shutil.rmtree(self.work_dir)
|
||||||
|
|
||||||
|
@mock.patch("letsencrypt.client.apache.configurator."
|
||||||
|
"ApacheConfigurator.restart")
|
||||||
|
@mock.patch("letsencrypt.client.challenge_util.dvsni_gen_cert")
|
||||||
|
def test_perform(self, mock_dvsni_gen_cert, mock_restart):
|
||||||
|
mock_dvsni_gen_cert.side_effect = ["randomS1", "randomS2"]
|
||||||
|
responses = self.sni.perform()
|
||||||
|
|
||||||
|
self.assertEqual(mock_dvsni_gen_cert.call_count, 2)
|
||||||
|
calls = mock_dvsni_gen_cert.call_args_list
|
||||||
|
expected_call_list = [
|
||||||
|
(self.sni.get_cert_file(self.chall1.nonce), self.chall1.domain,
|
||||||
|
self.chall1.r_b64, self.chall1.nonce, self.chall1.key),
|
||||||
|
(self.sni.get_cert_file(self.chall2.nonce), self.chall2.domain,
|
||||||
|
self.chall2.r_b64, self.chall2.nonce, self.chall2.key)
|
||||||
|
]
|
||||||
|
|
||||||
|
for i in range(len(expected_call_list)):
|
||||||
|
for j in range(len(expected_call_list[0])):
|
||||||
|
self.assertEqual(calls[i][0][j], expected_call_list[i][j])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
len(self.sni.config.parser.find_dir(
|
||||||
|
"Include", self.sni.challenge_conf)),
|
||||||
|
1)
|
||||||
|
self.assertEqual(len(responses), 2)
|
||||||
|
self.assertEqual(responses[0]["s"], "randomS1")
|
||||||
|
self.assertEqual(responses[1]["s"], "randomS2")
|
||||||
|
|
||||||
|
def test_mod_config(self):
|
||||||
|
v_addr1 = [obj.Addr(("1.2.3.4", "443")), obj.Addr(("5.6.7.8", "443"))]
|
||||||
|
v_addr2 = [obj.Addr(("127.0.0.1", "443"))]
|
||||||
|
ll_addr = []
|
||||||
|
ll_addr.append(v_addr1)
|
||||||
|
ll_addr.append(v_addr2)
|
||||||
|
self.sni.mod_config(ll_addr)
|
||||||
|
self.sni.config.save()
|
||||||
|
|
||||||
|
self.sni.config.parser.find_dir("Include", self.sni.challenge_conf)
|
||||||
|
vh_match = self.sni.config.aug.match(
|
||||||
|
"/files" + self.sni.challenge_conf + "//VirtualHost")
|
||||||
|
|
||||||
|
vhs = []
|
||||||
|
for match in vh_match:
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
vhs.append(self.sni.config._create_vhost(match))
|
||||||
|
self.assertEqual(len(vhs), 2)
|
||||||
|
for vhost in vhs:
|
||||||
|
if vhost.addrs == set(v_addr1):
|
||||||
|
self.assertEqual(
|
||||||
|
vhost.names,
|
||||||
|
set([str(self.chall1.nonce + CONFIG.INVALID_EXT)]))
|
||||||
|
else:
|
||||||
|
self.assertEqual(vhost.addrs, set(v_addr2))
|
||||||
|
self.assertEqual(
|
||||||
|
vhost.names,
|
||||||
|
set([str(self.chall2.nonce + CONFIG.INVALID_EXT)]))
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
"""Test client.py."""
|
||||||
|
import unittest
|
||||||
|
import mock
|
||||||
|
import pkg_resources
|
||||||
|
|
||||||
|
from letsencrypt.client.tests import acme_util
|
||||||
|
|
||||||
|
|
||||||
|
class VerifyIdentityTest(unittest.TestCase):
|
||||||
|
"""verify_identities test."""
|
||||||
|
def setUp(self):
|
||||||
|
from letsencrypt.client.client import Client
|
||||||
|
from letsencrypt.client import CONFIG
|
||||||
|
|
||||||
|
rsa256_file = pkg_resources.resource_filename(
|
||||||
|
__name__, 'testdata/rsa256_key.pem')
|
||||||
|
rsa256_pem = pkg_resources.resource_string(
|
||||||
|
__name__, 'testdata/rsa256_key.pem')
|
||||||
|
|
||||||
|
auth_key = Client.Key(rsa256_file, rsa256_pem)
|
||||||
|
|
||||||
|
self.mock_auth = mock.MagicMock(name='ApacheConfigurator')
|
||||||
|
self.mock_auth.get_chall_pref.return_value = ["dvsni"]
|
||||||
|
self.mock_auth.perform.side_effect = gen_auth_resp
|
||||||
|
|
||||||
|
self.client = Client(
|
||||||
|
CONFIG.ACME_SERVER, ["0", "1", "2", "3", "4"],
|
||||||
|
auth_key, self.mock_auth, None)
|
||||||
|
self.client.perform = mock.MagicMock(
|
||||||
|
name='perform', side_effect=gen_auth_resp)
|
||||||
|
|
||||||
|
def test_name1_dvsni1(self):
|
||||||
|
self.client.names = ["0"]
|
||||||
|
challenge = [acme_util.CHALLENGES["dvsni"]]
|
||||||
|
msgs = [acme_util.get_chall_msg("0", "nonce0", challenge)]
|
||||||
|
|
||||||
|
responses, auth_c, client_c = self.client.verify_identities(msgs)
|
||||||
|
|
||||||
|
self.assertEqual(len(responses), 1)
|
||||||
|
self.assertEqual(len(responses[0]), 1)
|
||||||
|
|
||||||
|
self.assertEqual("DvsniChall0", responses[0][0])
|
||||||
|
self.assertEqual(len(auth_c), 1)
|
||||||
|
self.assertEqual(len(client_c), 1)
|
||||||
|
self.assertEqual(len(auth_c[0]), 1)
|
||||||
|
self.assertEqual(len(client_c[0]), 0)
|
||||||
|
|
||||||
|
def test_name5_dvsni5(self):
|
||||||
|
challenge = [acme_util.CHALLENGES["dvsni"]]
|
||||||
|
msgs = []
|
||||||
|
for i in range(5):
|
||||||
|
msgs.append(
|
||||||
|
acme_util.get_chall_msg(str(i), "nonce%d" % i, challenge))
|
||||||
|
|
||||||
|
responses, auth_c, client_c = self.client.verify_identities(msgs)
|
||||||
|
|
||||||
|
self.assertEqual(len(responses), 5)
|
||||||
|
self.assertEqual(len(auth_c), 5)
|
||||||
|
self.assertEqual(len(client_c), 5)
|
||||||
|
# Each message contains 1 auth, 0 client
|
||||||
|
for i in range(5):
|
||||||
|
self.assertEqual(len(responses[i]), 1)
|
||||||
|
self.assertEqual(responses[i][0], "DvsniChall%d" % i)
|
||||||
|
self.assertEqual(len(auth_c[i]), 1)
|
||||||
|
self.assertEqual(len(client_c[i]), 0)
|
||||||
|
self.assertEqual(type(auth_c[i][0]).__name__, "DvsniChall")
|
||||||
|
|
||||||
|
@mock.patch("letsencrypt.client.client."
|
||||||
|
"challenge.gen_challenge_path")
|
||||||
|
def test_name1_auth(self, mock_chall_path):
|
||||||
|
self.client.names = ["0"]
|
||||||
|
|
||||||
|
challenges = acme_util.get_auth_challenges()
|
||||||
|
combos = acme_util.gen_combos(challenges)
|
||||||
|
msgs = [acme_util.get_chall_msg("0", "nonce0", challenges, combos)]
|
||||||
|
|
||||||
|
path = gen_path(["simpleHttps"], challenges)
|
||||||
|
mock_chall_path.return_value = path
|
||||||
|
|
||||||
|
responses, auth_c, client_c = self.client.verify_identities(msgs)
|
||||||
|
|
||||||
|
self.assertEqual(len(responses), 1)
|
||||||
|
self.assertEqual(len(responses[0]), len(challenges))
|
||||||
|
self.assertEqual(len(auth_c), 1)
|
||||||
|
self.assertEqual(len(client_c), 1)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
responses[0],
|
||||||
|
self._get_exp_response("0", path, challenges))
|
||||||
|
|
||||||
|
self.assertEqual(len(auth_c[0]), 1)
|
||||||
|
self.assertEqual(len(client_c[0]), 0)
|
||||||
|
self.assertEqual(type(auth_c[0][0]).__name__, "SimpleHttpsChall")
|
||||||
|
|
||||||
|
@mock.patch("letsencrypt.client.client."
|
||||||
|
"challenge.gen_challenge_path")
|
||||||
|
def test_name1_all(self, mock_chall_path):
|
||||||
|
self.client.names = ["0"]
|
||||||
|
|
||||||
|
challenges = acme_util.get_challenges()
|
||||||
|
combos = acme_util.gen_combos(challenges)
|
||||||
|
msgs = [acme_util.get_chall_msg("0", "nonce0", challenges, combos)]
|
||||||
|
|
||||||
|
path = gen_path(["simpleHttps", "recoveryToken"], challenges)
|
||||||
|
mock_chall_path.return_value = path
|
||||||
|
|
||||||
|
responses, auth_c, client_c = self.client.verify_identities(msgs)
|
||||||
|
|
||||||
|
self.assertEqual(len(responses), 1)
|
||||||
|
self.assertEqual(len(responses[0]), len(challenges))
|
||||||
|
self.assertEqual(len(auth_c), 1)
|
||||||
|
self.assertEqual(len(client_c), 1)
|
||||||
|
self.assertEqual(len(auth_c[0]), 1)
|
||||||
|
self.assertEqual(len(client_c[0]), 1)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
responses[0],
|
||||||
|
self._get_exp_response("0", path, challenges))
|
||||||
|
self.assertEqual(type(auth_c[0][0]).__name__, "SimpleHttpsChall")
|
||||||
|
self.assertEqual(type(client_c[0][0]).__name__, "RecTokenChall")
|
||||||
|
|
||||||
|
@mock.patch("letsencrypt.client.client."
|
||||||
|
"challenge.gen_challenge_path")
|
||||||
|
def test_name5_all(self, mock_chall_path):
|
||||||
|
challenges = acme_util.get_challenges()
|
||||||
|
combos = acme_util.gen_combos(challenges)
|
||||||
|
msgs = []
|
||||||
|
for i in range(5):
|
||||||
|
msgs.append(
|
||||||
|
acme_util.get_chall_msg(
|
||||||
|
str(i), "nonce%d" % i, challenges, combos))
|
||||||
|
|
||||||
|
path = gen_path(["dvsni", "recoveryContact"], challenges)
|
||||||
|
mock_chall_path.return_value = path
|
||||||
|
|
||||||
|
responses, auth_c, client_c = self.client.verify_identities(msgs)
|
||||||
|
|
||||||
|
self.assertEqual(len(responses), 5)
|
||||||
|
for i in range(5):
|
||||||
|
self.assertEqual(len(responses[i]), len(challenges))
|
||||||
|
self.assertEqual(len(auth_c), 5)
|
||||||
|
self.assertEqual(len(client_c), 5)
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
self.assertEqual(
|
||||||
|
responses[i], self._get_exp_response(i, path, challenges))
|
||||||
|
self.assertEqual(len(auth_c[0]), 1)
|
||||||
|
self.assertEqual(len(client_c[0]), 1)
|
||||||
|
|
||||||
|
self.assertEqual(type(auth_c[i][0]).__name__, "DvsniChall")
|
||||||
|
self.assertEqual(type(client_c[i][0]).__name__, "RecContactChall")
|
||||||
|
|
||||||
|
@mock.patch("letsencrypt.client.client."
|
||||||
|
"challenge.gen_challenge_path")
|
||||||
|
def test_name5_mix(self, mock_chall_path):
|
||||||
|
paths = []
|
||||||
|
msgs = []
|
||||||
|
chosen_chall = [["dns"],
|
||||||
|
["dvsni"],
|
||||||
|
["simpleHttps", "proofOfPossession"],
|
||||||
|
["simpleHttps"],
|
||||||
|
["dns", "recoveryToken"]]
|
||||||
|
challenge_list = [acme_util.get_auth_challenges(),
|
||||||
|
[acme_util.CHALLENGES["dvsni"]],
|
||||||
|
acme_util.get_challenges(),
|
||||||
|
acme_util.get_auth_challenges(),
|
||||||
|
acme_util.get_challenges()]
|
||||||
|
|
||||||
|
# Combos doesn't matter since I am overriding the gen_path function
|
||||||
|
for i in range(5):
|
||||||
|
paths.append(gen_path(chosen_chall[i], challenge_list[i]))
|
||||||
|
msgs.append(
|
||||||
|
acme_util.get_chall_msg(
|
||||||
|
str(i), "nonce%d" % i, challenge_list[i]))
|
||||||
|
|
||||||
|
mock_chall_path.side_effect = paths
|
||||||
|
|
||||||
|
responses, auth_c, client_c = self.client.verify_identities(msgs)
|
||||||
|
|
||||||
|
self.assertEqual(len(responses), 5)
|
||||||
|
self.assertEqual(len(auth_c), 5)
|
||||||
|
self.assertEqual(len(client_c), 5)
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
resp = self._get_exp_response(i, paths[i], challenge_list[i])
|
||||||
|
self.assertEqual(responses[i], resp)
|
||||||
|
self.assertEqual(len(auth_c[i]), 1)
|
||||||
|
self.assertEqual(len(client_c[i]), len(chosen_chall[i]) - 1)
|
||||||
|
|
||||||
|
self.assertEqual(type(auth_c[0][0]).__name__, "DnsChall")
|
||||||
|
self.assertEqual(type(auth_c[1][0]).__name__, "DvsniChall")
|
||||||
|
self.assertEqual(type(auth_c[2][0]).__name__, "SimpleHttpsChall")
|
||||||
|
self.assertEqual(type(auth_c[3][0]).__name__, "SimpleHttpsChall")
|
||||||
|
self.assertEqual(type(auth_c[4][0]).__name__, "DnsChall")
|
||||||
|
|
||||||
|
self.assertEqual(type(client_c[2][0]).__name__, "PopChall")
|
||||||
|
self.assertEqual(type(client_c[4][0]).__name__, "RecTokenChall")
|
||||||
|
|
||||||
|
def _get_exp_response(self, domain, path, challenges):
|
||||||
|
exp_resp = ["null"] * len(challenges)
|
||||||
|
for i in path:
|
||||||
|
exp_resp[i] = translate[challenges[i]["type"]] + str(domain)
|
||||||
|
|
||||||
|
return exp_resp
|
||||||
|
|
||||||
|
|
||||||
|
class ClientPerformTest(unittest.TestCase):
|
||||||
|
"""Test client perform function."""
|
||||||
|
def setUp(self):
|
||||||
|
from letsencrypt.client.client import Client
|
||||||
|
from letsencrypt.client import CONFIG
|
||||||
|
|
||||||
|
rsa256_file = pkg_resources.resource_filename(
|
||||||
|
__name__, 'testdata/rsa256_key.pem')
|
||||||
|
rsa256_pem = pkg_resources.resource_string(
|
||||||
|
__name__, 'testdata/rsa256_key.pem')
|
||||||
|
|
||||||
|
auth_key = Client.Key(rsa256_file, rsa256_pem)
|
||||||
|
|
||||||
|
self.client = Client(
|
||||||
|
CONFIG.ACME_SERVER, ["example.com"], auth_key, None, None)
|
||||||
|
self.client.rec_token.perform = mock.MagicMock(
|
||||||
|
name="rec_token_perform", side_effect=gen_client_resp)
|
||||||
|
|
||||||
|
def test_rec_token1(self):
|
||||||
|
from letsencrypt.client.challenge_util import RecTokenChall
|
||||||
|
token = RecTokenChall("0")
|
||||||
|
|
||||||
|
responses = self.client.perform([token])
|
||||||
|
|
||||||
|
self.assertEqual(responses, ["RecTokenChall0"])
|
||||||
|
|
||||||
|
def test_rec_token5(self):
|
||||||
|
from letsencrypt.client.challenge_util import RecTokenChall
|
||||||
|
tokens = []
|
||||||
|
for i in range(5):
|
||||||
|
tokens.append(RecTokenChall(str(i)))
|
||||||
|
|
||||||
|
responses = self.client.perform(tokens)
|
||||||
|
|
||||||
|
self.assertEqual(len(responses), 5)
|
||||||
|
for i in range(5):
|
||||||
|
self.assertEqual(responses[i], "RecTokenChall%d" % i)
|
||||||
|
|
||||||
|
def test_unexpected(self):
|
||||||
|
from letsencrypt.client.challenge_util import DvsniChall
|
||||||
|
from letsencrypt.client.errors import LetsEncryptClientError
|
||||||
|
unexpected = DvsniChall("0", "rb64", "123", "invalid_key")
|
||||||
|
|
||||||
|
self.assertRaises(
|
||||||
|
LetsEncryptClientError, self.client.perform, [unexpected])
|
||||||
|
|
||||||
|
|
||||||
|
translate = {"dvsni": "DvsniChall",
|
||||||
|
"simpleHttps": "SimpleHttpsChall",
|
||||||
|
"dns": "DnsChall",
|
||||||
|
"recoveryToken": "RecTokenChall",
|
||||||
|
"recoveryContact": "RecContactChall",
|
||||||
|
"proofOfPossession": "PopChall"}
|
||||||
|
|
||||||
|
|
||||||
|
def gen_auth_resp(chall_list):
|
||||||
|
return ["%s%s" % (type(chall).__name__, chall.domain)
|
||||||
|
for chall in chall_list]
|
||||||
|
|
||||||
|
|
||||||
|
def gen_client_resp(chall):
|
||||||
|
return "%s%s" % (type(chall).__name__, chall.domain)
|
||||||
|
|
||||||
|
|
||||||
|
def gen_path(str_list, challenges):
|
||||||
|
path = []
|
||||||
|
for i, chall in enumerate(challenges):
|
||||||
|
for str_chall in str_list:
|
||||||
|
if chall["type"] == str_chall:
|
||||||
|
path.append(i)
|
||||||
|
continue
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""Tests for recovery_token.py."""
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
|
||||||
|
class RecoveryTokenTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
from letsencrypt.client.recovery_token import RecoveryToken
|
||||||
|
server = "demo_server"
|
||||||
|
self.base_dir = tempfile.mkdtemp("tokens")
|
||||||
|
self.token_dir = os.path.join(self.base_dir, server)
|
||||||
|
self.rec_token = RecoveryToken(server, self.base_dir)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
shutil.rmtree(self.base_dir)
|
||||||
|
|
||||||
|
def test_store_token(self):
|
||||||
|
self.rec_token.store_token("example.com", 111)
|
||||||
|
path = os.path.join(self.token_dir, "example.com")
|
||||||
|
self.assertTrue(os.path.isfile(path))
|
||||||
|
with open(path) as token_fd:
|
||||||
|
self.assertEqual(token_fd.read(), "111")
|
||||||
|
|
||||||
|
def test_requires_human(self):
|
||||||
|
self.rec_token.store_token("example2.com", 222)
|
||||||
|
self.assertFalse(self.rec_token.requires_human("example2.com"))
|
||||||
|
self.assertTrue(self.rec_token.requires_human("example3.com"))
|
||||||
|
|
||||||
|
def test_cleanup(self):
|
||||||
|
from letsencrypt.client.challenge_util import RecTokenChall
|
||||||
|
self.rec_token.store_token("example3.com", 333)
|
||||||
|
self.assertFalse(self.rec_token.requires_human("example3.com"))
|
||||||
|
|
||||||
|
self.rec_token.cleanup(RecTokenChall("example3.com"))
|
||||||
|
self.assertTrue(self.rec_token.requires_human("example3.com"))
|
||||||
|
|
||||||
|
# Shouldn't throw an error
|
||||||
|
self.rec_token.cleanup(RecTokenChall("example4.com"))
|
||||||
|
|
||||||
|
def test_perform_stored(self):
|
||||||
|
from letsencrypt.client.challenge_util import RecTokenChall
|
||||||
|
self.rec_token.store_token("example4.com", 444)
|
||||||
|
response = self.rec_token.perform(RecTokenChall("example4.com"))
|
||||||
|
|
||||||
|
self.assertEqual(response, {"type": "recoveryToken", "token": "444"})
|
||||||
|
|
||||||
|
@mock.patch("letsencrypt.client.recovery_token.zope.component.getUtility")
|
||||||
|
def test_perform_not_stored(self, mock_input):
|
||||||
|
from letsencrypt.client.challenge_util import RecTokenChall
|
||||||
|
|
||||||
|
mock_input().generic_input.side_effect = [(0, "555"), (1, "000")]
|
||||||
|
response = self.rec_token.perform(RecTokenChall("example5.com"))
|
||||||
|
self.assertEqual(response, {"type": "recoveryToken", "token": "555"})
|
||||||
|
|
||||||
|
response = self.rec_token.perform(RecTokenChall("example6.com"))
|
||||||
|
self.assertEqual(response, None)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -14,7 +14,7 @@ commands =
|
|||||||
[testenv:cover]
|
[testenv:cover]
|
||||||
commands =
|
commands =
|
||||||
python setup.py dev
|
python setup.py dev
|
||||||
python setup.py nosetests --with-coverage --cover-min-percentage=47
|
python setup.py nosetests --with-coverage --cover-min-percentage=60
|
||||||
|
|
||||||
[testenv:lint]
|
[testenv:lint]
|
||||||
commands =
|
commands =
|
||||||
|
|||||||
Reference in New Issue
Block a user