mirror of
https://github.com/certbot/certbot.git
synced 2026-07-28 00:35:50 +02:00
Merge branch 'dvsni_cleanup'
This commit is contained in:
@@ -10,6 +10,12 @@
|
||||
.. automodule:: letsencrypt.client.apache.configurator
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt.client.apache.dvsni`
|
||||
=============================================
|
||||
|
||||
.. automodule:: letsencrypt.client.apache.dvsni
|
||||
:members:
|
||||
|
||||
:mod:`letsencrypt.client.apache.obj`
|
||||
====================================
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
:mod:`letsencrypt.client.auth_handler`
|
||||
--------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.auth_handler
|
||||
:members:
|
||||
@@ -0,0 +1,5 @@
|
||||
:mod:`letsencrypt.client.client_authenticator`
|
||||
--------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.client_authenticator
|
||||
:members:
|
||||
@@ -0,0 +1,5 @@
|
||||
:mod:`letsencrypt.client.recovery_token`
|
||||
--------------------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.recovery_token
|
||||
:members:
|
||||
@@ -1,5 +0,0 @@
|
||||
:mod:`letsencrypt.client.recovery_token_challenge`
|
||||
--------------------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.recovery_token_challenge
|
||||
:members:
|
||||
@@ -30,9 +30,10 @@ IN_PROGRESS_DIR = os.path.join(BACKUP_DIR, "IN_PROGRESS/")
|
||||
"""Directory used before a permanent checkpoint is finalized"""
|
||||
|
||||
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/")
|
||||
"""Where all keys should be stored"""
|
||||
@@ -47,9 +48,6 @@ OPTIONS_SSL_CONF = os.path.join(CONFIG_DIR, "options-ssl.conf")
|
||||
LE_VHOST_EXT = "-le-ssl.conf"
|
||||
"""Let's Encrypt SSL vhost configuration extension"""
|
||||
|
||||
APACHE_CHALLENGE_CONF = os.path.join(CONFIG_DIR, "le_dvsni_cert_challenge.conf")
|
||||
"""Temporary file for challenge virtual hosts"""
|
||||
|
||||
CERT_PATH = CERT_DIR + "cert-letsencrypt.pem"
|
||||
"""Let's Encrypt cert file."""
|
||||
|
||||
@@ -59,15 +57,15 @@ CHAIN_PATH = CERT_DIR + "chain-letsencrypt.pem"
|
||||
INVALID_EXT = ".acme.invalid"
|
||||
"""Invalid Extension"""
|
||||
|
||||
# Challenge Information
|
||||
CHALLENGE_PREFERENCES = ["dvsni", "recoveryToken"]
|
||||
"""Challenge Preferences Dict for currently supported challenges"""
|
||||
|
||||
EXCLUSIVE_CHALLENGES = [frozenset(["dvsni", "simpleHttps"])]
|
||||
"""Mutually Exclusive Challenges - only solve 1"""
|
||||
|
||||
CONFIG_CHALLENGES = frozenset(["dvsni", "simpleHttps"])
|
||||
"""These are challenges that must be solved by a Configurator object"""
|
||||
DV_CHALLENGES = frozenset(["dvsni", "simpleHttps", "dns"])
|
||||
"""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
|
||||
S_SIZE = 32
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"""Apache Configuration based off of Augeas Configurator."""
|
||||
import logging
|
||||
import os
|
||||
import pkg_resources
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -17,9 +15,11 @@ from letsencrypt.client import errors
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client import le_util
|
||||
|
||||
from letsencrypt.client.apache import dvsni
|
||||
from letsencrypt.client.apache import obj
|
||||
from letsencrypt.client.apache import parser
|
||||
|
||||
|
||||
# TODO: Augeas sections ie. <VirtualHost>, <IfModule> beginning and closing
|
||||
# tags need to be the same case, otherwise Augeas doesn't recognize them.
|
||||
# This is not able to be completely remedied by regular expressions because
|
||||
@@ -117,6 +117,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
self.vhosts = self.get_virtual_hosts()
|
||||
# Add name_server association dict
|
||||
self.assoc = dict()
|
||||
# Add number of outstanding challenges
|
||||
self.chall_out = 0
|
||||
|
||||
# Enable mod_ssl if it isn't already enabled
|
||||
# This is Let's Encrypt... we enable mod_ssl on initialization :)
|
||||
@@ -125,11 +127,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
# on initialization
|
||||
self._prepare_server_https()
|
||||
|
||||
# Note: initialization doesn't check to see if the config is correct
|
||||
# by Apache's standards. This should be done by the client (client.py)
|
||||
# if it is desired. There may be instances where correct configuration
|
||||
# isn't required on startup.
|
||||
|
||||
def deploy_cert(self, vhost, cert, key, cert_chain=None):
|
||||
"""Deploys certificate to specified virtual host.
|
||||
|
||||
@@ -804,7 +801,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
def is_site_enabled(self, avail_fp):
|
||||
"""Checks to see if the given site is enabled.
|
||||
|
||||
.. todo:: fix hardcoded sites-enabled
|
||||
.. todo:: fix hardcoded sites-enabled, check os.path.samefile
|
||||
|
||||
:param str avail_fp: Complete file path of available site
|
||||
|
||||
@@ -814,7 +811,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
"""
|
||||
enabled_dir = os.path.join(self.parser.root, "sites-enabled/")
|
||||
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 False
|
||||
@@ -929,190 +926,60 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
###########################################################################
|
||||
# Challenges Section
|
||||
###########################################################################
|
||||
# pylint: disable=no-self-use, unused-argument
|
||||
def get_chall_pref(self, domain):
|
||||
"""Return list of challenge preferences."""
|
||||
|
||||
# TODO: Change list_sni_tuple to namedtuple. Also include key within tuple.
|
||||
# This allows the keys to be different for each SNI challenge
|
||||
return ["dvsni"]
|
||||
|
||||
def perform(self, chall_dict):
|
||||
def perform(self, chall_list):
|
||||
"""Perform the configuration related challenge.
|
||||
|
||||
:param dict chall_dict: Dictionary representing a challenge.
|
||||
This function currently assumes all challenges will be fulfilled.
|
||||
If this turns out not to be the case in the future. Cleanup and
|
||||
outstanding challenges will have to be designed better.
|
||||
|
||||
:param list chall_list: List of challenges to be
|
||||
fulfilled by configurator.
|
||||
|
||||
:returns: list of responses. All responses are returned in the same
|
||||
order as received by the perform function. A None response
|
||||
indicates the challenge was not perfromed.
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
self.chall_out += len(chall_list)
|
||||
responses = [None] * len(chall_list)
|
||||
apache_dvsni = dvsni.ApacheDvsni(self)
|
||||
|
||||
if chall_dict.get("type", "") == 'dvsni':
|
||||
return self.dvsni_perform(chall_dict)
|
||||
return None
|
||||
for i, chall in enumerate(chall_list):
|
||||
if isinstance(chall, challenge_util.DvsniChall):
|
||||
# Currently also have dvsni hold associated index
|
||||
# of the challenge. This helps to put all of the responses back
|
||||
# together when they are all complete.
|
||||
apache_dvsni.add_chall(chall, i)
|
||||
|
||||
def dvsni_perform(self, chall_dict):
|
||||
"""Perform a DVSNI challenge.
|
||||
|
||||
`chall_dict` composed of:
|
||||
|
||||
list_sni_tuple:
|
||||
List of tuples with form `(name, r, nonce)`, where
|
||||
`name` (`str`), `r` (base64 `str`), `nonce` (hex `str`)
|
||||
|
||||
dvsni_key:
|
||||
DVSNI key (:class:`letsencrypt.client.client.Client.Key`)
|
||||
|
||||
:param dict chall_dict: dvsni challenge - see documentation
|
||||
|
||||
"""
|
||||
# Save any changes to the configuration as a precaution
|
||||
# About to make temporary changes to the config
|
||||
self.save()
|
||||
|
||||
# Do weak validation that challenge is of expected type
|
||||
if not ("list_sni_tuple" in chall_dict and "dvsni_key" in chall_dict):
|
||||
logging.fatal("Incorrect parameter given to Apache DVSNI challenge")
|
||||
logging.fatal("Chall dict: %s", chall_dict)
|
||||
sys.exit(1)
|
||||
|
||||
addresses = []
|
||||
default_addr = "*:443"
|
||||
for tup in chall_dict["list_sni_tuple"]:
|
||||
vhost = self.choose_virtual_host(tup[0])
|
||||
if vhost is None:
|
||||
logging.error(
|
||||
"No vhost exists with servername or alias of: %s", tup[0])
|
||||
logging.error("No _default_:443 vhost exists")
|
||||
logging.error("Please specify servernames in the Apache config")
|
||||
return None
|
||||
|
||||
# TODO - @jdkasten review this code to make sure it makes sense
|
||||
self.make_server_sni_ready(vhost, default_addr)
|
||||
|
||||
for addr in vhost.addrs:
|
||||
if "_default_" == addr.get_addr():
|
||||
addresses.append([default_addr])
|
||||
break
|
||||
else:
|
||||
addresses.append(list(vhost.addrs))
|
||||
|
||||
responses = []
|
||||
|
||||
# Create all of the challenge certs
|
||||
for tup in chall_dict["list_sni_tuple"]:
|
||||
cert_path = self.dvsni_get_cert_file(tup[2])
|
||||
self.register_file_creation(cert_path)
|
||||
s_b64 = challenge_util.dvsni_gen_cert(
|
||||
cert_path, tup[0], tup[1], tup[2], chall_dict["dvsni_key"])
|
||||
|
||||
responses.append({"type": "dvsni", "s": s_b64})
|
||||
|
||||
# Setup the configuration
|
||||
self.dvsni_mod_config(chall_dict["list_sni_tuple"],
|
||||
chall_dict["dvsni_key"],
|
||||
addresses)
|
||||
|
||||
# Save reversible changes and restart the server
|
||||
self.save("SNI Challenge", True)
|
||||
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()
|
||||
|
||||
# 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
|
||||
|
||||
def cleanup(self):
|
||||
def cleanup(self, chall_list):
|
||||
"""Revert all challenges."""
|
||||
self.chall_out -= len(chall_list)
|
||||
|
||||
self.revert_challenge_config()
|
||||
self.restart()
|
||||
|
||||
# TODO: Variable names
|
||||
def dvsni_mod_config(self, list_sni_tuple, dvsni_key,
|
||||
ll_addrs):
|
||||
"""Modifies Apache config files to include challenge vhosts.
|
||||
|
||||
Result: Apache config includes virtual servers for issued challs
|
||||
|
||||
:param list list_sni_tuple: list of tuples with the form
|
||||
`(addr, y, nonce)`, where `addr` is `str`, `y` is `bytearray`,
|
||||
and nonce is hex `str`
|
||||
|
||||
:param dvsni_key: DVSNI key
|
||||
:type dvsni_key: :class:`letsencrypt.client.client.Client.Key`
|
||||
|
||||
:param list ll_addrs: list of list of
|
||||
:class:`letsencrypt.client.apache.obj.Addr` to apply
|
||||
|
||||
"""
|
||||
# WARNING: THIS IS A POTENTIAL SECURITY VULNERABILITY
|
||||
# THIS SHOULD BE HANDLED BY THE PACKAGE MANAGER
|
||||
# AND TAKEN OUT BEFORE RELEASE, INSTEAD
|
||||
# SHOWING A NICE ERROR MESSAGE ABOUT THE PROBLEM
|
||||
|
||||
# Check to make sure options-ssl.conf is installed
|
||||
# pylint: disable=no-member
|
||||
if not os.path.isfile(CONFIG.OPTIONS_SSL_CONF):
|
||||
dist_conf = pkg_resources.resource_filename(
|
||||
__name__, os.path.basename(CONFIG.OPTIONS_SSL_CONF))
|
||||
shutil.copyfile(dist_conf, CONFIG.OPTIONS_SSL_CONF)
|
||||
|
||||
# TODO: Use ip address of existing vhost instead of relying on FQDN
|
||||
config_text = "<IfModule mod_ssl.c>\n"
|
||||
for idx, lis in enumerate(ll_addrs):
|
||||
config_text += self.get_config_text(
|
||||
list_sni_tuple[idx][2], lis, dvsni_key.file)
|
||||
config_text += "</IfModule>\n"
|
||||
|
||||
self.dvsni_conf_include_check(self.parser.loc["default"])
|
||||
self.register_file_creation(True, CONFIG.APACHE_CHALLENGE_CONF)
|
||||
|
||||
with open(CONFIG.APACHE_CHALLENGE_CONF, 'w') as new_conf:
|
||||
new_conf.write(config_text)
|
||||
|
||||
def dvsni_conf_include_check(self, main_config):
|
||||
"""Adds DVSNI challenge conf file into configuration.
|
||||
|
||||
Adds DVSNI challenge include file if it does not already exist
|
||||
within mainConfig
|
||||
|
||||
:param str main_config: file path to main user apache config file
|
||||
|
||||
"""
|
||||
if len(self.parser.find_dir(
|
||||
parser.case_i("Include"), CONFIG.APACHE_CHALLENGE_CONF)) == 0:
|
||||
# print "Including challenge virtual host(s)"
|
||||
self.parser.add_dir(parser.get_aug_path(main_config),
|
||||
"Include", CONFIG.APACHE_CHALLENGE_CONF)
|
||||
|
||||
def get_config_text(self, nonce, ip_addrs, dvsni_key_file):
|
||||
"""Chocolate virtual server configuration text
|
||||
|
||||
:param str nonce: hex form of nonce
|
||||
:param list ip_addrs: addresses of challenged domain
|
||||
:class:`list` of type :class:`letsencrypt.client.apache.obj.Addr`
|
||||
:param str dvsni_key_file: Path to key file
|
||||
|
||||
:returns: virtual host configuration text
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
ips = " ".join(str(i) for i in ip_addrs)
|
||||
return ("<VirtualHost " + ips + ">\n"
|
||||
"ServerName " + nonce + CONFIG.INVALID_EXT + "\n"
|
||||
"UseCanonicalName on\n"
|
||||
"SSLStrictSNIVHostCheck on\n"
|
||||
"\n"
|
||||
"LimitRequestBody 1048576\n"
|
||||
"\n"
|
||||
"Include " + self.parser.loc["ssl_options"] + "\n"
|
||||
"SSLCertificateFile " + self.dvsni_get_cert_file(nonce) + "\n"
|
||||
"SSLCertificateKeyFile " + dvsni_key_file + "\n"
|
||||
"\n"
|
||||
"DocumentRoot " + self.direc["config"] + "challenge_page/\n"
|
||||
"</VirtualHost>\n\n")
|
||||
|
||||
def dvsni_get_cert_file(self, nonce):
|
||||
"""Returns standardized name for challenge certificate.
|
||||
|
||||
:param str nonce: hex form of nonce
|
||||
|
||||
:returns: certificate file name
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
return self.direc["work"] + nonce + ".crt"
|
||||
# If all of the challenges have been finished, clean up everything
|
||||
if self.chall_out <= 0:
|
||||
self.revert_challenge_config()
|
||||
self.restart()
|
||||
|
||||
|
||||
def enable_mod(mod_name):
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
"""ApacheDVSNI"""
|
||||
import logging
|
||||
import os
|
||||
|
||||
from letsencrypt.client import challenge_util
|
||||
from letsencrypt.client import CONFIG
|
||||
|
||||
from letsencrypt.client.apache import parser
|
||||
|
||||
|
||||
class ApacheDvsni(object):
|
||||
"""Class performs DVSNI challenges within the Apache configurator.
|
||||
|
||||
:ivar config: ApacheConfigurator object
|
||||
:type config: :class:`letsencrypt.client.apache.configurator`
|
||||
|
||||
:ivar dvsni_chall: Data required for challenges.
|
||||
where DvsniChall tuples have the following fields
|
||||
`domain` (`str`), `r_b64` (base64 `str`), `nonce` (hex `str`)
|
||||
`key` (:class:`letsencrypt.client.client.Client.Key`)
|
||||
:type dvsni_chall: `list` of
|
||||
:class:`letsencrypt.client.challenge_util.DvsniChall`
|
||||
|
||||
:param list indicies: Meant to hold indices of challenges in a
|
||||
larger array. ApacheDvsni is capable of solving many challenges
|
||||
at once which causes an indexing issue within ApacheConfigurator
|
||||
who must return all responses in order. Imagine ApacheConfigurator
|
||||
maintaining state about where all of the SimpleHttps Challenges,
|
||||
Dvsni Challenges belong in the response array. This is an optional
|
||||
utility.
|
||||
|
||||
:param str challenge_conf: location of the challenge config file
|
||||
|
||||
"""
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.dvsni_chall = []
|
||||
self.indices = []
|
||||
self.challenge_conf = os.path.join(
|
||||
config.direc["config"], "le_dvsni_cert_challenge.conf")
|
||||
# self.completed = 0
|
||||
|
||||
def add_chall(self, chall, idx=None):
|
||||
"""Add challenge to DVSNI object to perform at once.
|
||||
|
||||
:param chall: DVSNI challenge info
|
||||
:type chall: :class:`letsencrypt.client.challenge_util.DvsniChall`
|
||||
|
||||
:param int idx: index to challenge in a larger array
|
||||
|
||||
"""
|
||||
self.dvsni_chall.append(chall)
|
||||
if idx is not None:
|
||||
self.indices.append(idx)
|
||||
|
||||
def perform(self):
|
||||
"""Peform a DVSNI challenge."""
|
||||
if not self.dvsni_chall:
|
||||
return None
|
||||
# Save any changes to the configuration as a precaution
|
||||
# About to make temporary changes to the config
|
||||
self.config.save()
|
||||
|
||||
addresses = []
|
||||
default_addr = "*:443"
|
||||
for chall in self.dvsni_chall:
|
||||
vhost = self.config.choose_virtual_host(chall.domain)
|
||||
if vhost is None:
|
||||
logging.error(
|
||||
"No vhost exists with servername or alias of: %s",
|
||||
chall.domain)
|
||||
logging.error("No _default_:443 vhost exists")
|
||||
logging.error("Please specify servernames in the Apache config")
|
||||
return None
|
||||
|
||||
# TODO - @jdkasten review this code to make sure it makes sense
|
||||
self.config.make_server_sni_ready(vhost, default_addr)
|
||||
|
||||
for addr in vhost.addrs:
|
||||
if "_default_" == addr.get_addr():
|
||||
addresses.append([default_addr])
|
||||
break
|
||||
else:
|
||||
addresses.append(list(vhost.addrs))
|
||||
|
||||
responses = []
|
||||
|
||||
# Create all of the challenge certs
|
||||
for chall in self.dvsni_chall:
|
||||
cert_path = self.get_cert_file(chall.nonce)
|
||||
self.config.register_file_creation(cert_path)
|
||||
s_b64 = challenge_util.dvsni_gen_cert(
|
||||
cert_path, chall.domain, chall.r_b64, chall.nonce, chall.key)
|
||||
|
||||
responses.append({"type": "dvsni", "s": s_b64})
|
||||
|
||||
# Setup the configuration
|
||||
self._mod_config(addresses)
|
||||
|
||||
# Save reversible changes
|
||||
self.config.save("SNI Challenge", True)
|
||||
|
||||
return responses
|
||||
|
||||
def _mod_config(self, ll_addrs):
|
||||
"""Modifies Apache config files to include challenge vhosts.
|
||||
|
||||
Result: Apache config includes virtual servers for issued challs
|
||||
|
||||
:param list ll_addrs: list of list of
|
||||
:class:`letsencrypt.client.apache.obj.Addr` to apply
|
||||
|
||||
"""
|
||||
# WARNING: THIS IS A POTENTIAL SECURITY VULNERABILITY
|
||||
# THIS SHOULD BE HANDLED BY THE PACKAGE MANAGER
|
||||
# AND TAKEN OUT BEFORE RELEASE, INSTEAD
|
||||
# SHOWING A NICE ERROR MESSAGE ABOUT THE PROBLEM
|
||||
|
||||
# Check to make sure options-ssl.conf is installed
|
||||
# pylint: disable=no-member
|
||||
# if not os.path.isfile(CONFIG.OPTIONS_SSL_CONF):
|
||||
# dist_conf = pkg_resources.resource_filename(
|
||||
# __name__, os.path.basename(CONFIG.OPTIONS_SSL_CONF))
|
||||
# shutil.copyfile(dist_conf, CONFIG.OPTIONS_SSL_CONF)
|
||||
|
||||
# TODO: Use ip address of existing vhost instead of relying on FQDN
|
||||
config_text = "<IfModule mod_ssl.c>\n"
|
||||
for idx, lis in enumerate(ll_addrs):
|
||||
config_text += self._get_config_text(
|
||||
self.dvsni_chall[idx].nonce, lis,
|
||||
self.dvsni_chall[idx].key.file)
|
||||
config_text += "</IfModule>\n"
|
||||
|
||||
self._conf_include_check(self.config.parser.loc["default"])
|
||||
self.config.register_file_creation(True, self.challenge_conf)
|
||||
|
||||
with open(self.challenge_conf, 'w') as new_conf:
|
||||
new_conf.write(config_text)
|
||||
|
||||
def _conf_include_check(self, main_config):
|
||||
"""Adds DVSNI challenge conf file into configuration.
|
||||
|
||||
Adds DVSNI challenge include file if it does not already exist
|
||||
within mainConfig
|
||||
|
||||
:param str main_config: file path to main user apache config file
|
||||
|
||||
"""
|
||||
if len(self.config.parser.find_dir(
|
||||
parser.case_i("Include"), self.challenge_conf)) == 0:
|
||||
# print "Including challenge virtual host(s)"
|
||||
self.config.parser.add_dir(parser.get_aug_path(main_config),
|
||||
"Include", self.challenge_conf)
|
||||
|
||||
def _get_config_text(self, nonce, ip_addrs, dvsni_key_file):
|
||||
"""Chocolate virtual server configuration text
|
||||
|
||||
:param str nonce: hex form of nonce
|
||||
:param list ip_addrs: addresses of challenged domain
|
||||
:class:`list` of type :class:`letsencrypt.client.apache.obj.Addr`
|
||||
:param str dvsni_key_file: Path to key file
|
||||
|
||||
:returns: virtual host configuration text
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
ips = " ".join(str(i) for i in ip_addrs)
|
||||
return ("<VirtualHost " + ips + ">\n"
|
||||
"ServerName " + nonce + CONFIG.INVALID_EXT + "\n"
|
||||
"UseCanonicalName on\n"
|
||||
"SSLStrictSNIVHostCheck on\n"
|
||||
"\n"
|
||||
"LimitRequestBody 1048576\n"
|
||||
"\n"
|
||||
"Include " + self.config.parser.loc["ssl_options"] + "\n"
|
||||
"SSLCertificateFile " + self.get_cert_file(nonce) + "\n"
|
||||
"SSLCertificateKeyFile " + dvsni_key_file + "\n"
|
||||
"\n"
|
||||
"DocumentRoot " + self.config.direc["config"] + "dvsni_page/\n"
|
||||
"</VirtualHost>\n\n")
|
||||
|
||||
def get_cert_file(self, nonce):
|
||||
"""Returns standardized name for challenge certificate.
|
||||
|
||||
:param str nonce: hex form of nonce
|
||||
|
||||
:returns: certificate file name
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
return self.config.direc["work"] + nonce + ".crt"
|
||||
@@ -225,6 +225,7 @@ class ApacheParser(object):
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
# Checkout fnmatch.py in venv/local/lib/python2.7/fnmatch.py
|
||||
regex = ""
|
||||
for letter in clean_fn_match:
|
||||
if letter == '.':
|
||||
|
||||
@@ -342,7 +342,7 @@ class AugeasConfigurator(object):
|
||||
else:
|
||||
cp_dir = self.direc["progress"]
|
||||
|
||||
le_util.make_or_verify_dir(cp_dir)
|
||||
le_util.make_or_verify_dir(cp_dir, 0o755, os.geteuid())
|
||||
try:
|
||||
with open(os.path.join(cp_dir, "NEW_FILES"), 'a') as new_fd:
|
||||
for file_path in files:
|
||||
@@ -399,7 +399,7 @@ class AugeasConfigurator(object):
|
||||
else:
|
||||
logging.warn(
|
||||
"File: %s - Could not be found to be deleted\n"
|
||||
"Program was probably shut down unexpectedly, ")
|
||||
"LE probably shut down unexpectedly", path)
|
||||
except (IOError, OSError):
|
||||
logging.fatal(
|
||||
"Unable to remove filepaths contained within %s", file_list)
|
||||
|
||||
@@ -0,0 +1,436 @@
|
||||
"""ACME AuthHandler."""
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from letsencrypt.client import acme
|
||||
from letsencrypt.client import CONFIG
|
||||
from letsencrypt.client import challenge_util
|
||||
from letsencrypt.client import errors
|
||||
|
||||
|
||||
class AuthHandler(object):
|
||||
"""ACME Authorization Handler for a client.
|
||||
|
||||
:ivar dv_auth: Authenticator capable of solving CONFIG.DV_CHALLENGES
|
||||
:type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
|
||||
|
||||
:ivar client_auth: Authenticator capable of solving CONFIG.CLIENT_CHALLENGES
|
||||
:type client_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
|
||||
|
||||
:ivar network: Network object for sending and receiving authorization
|
||||
messages
|
||||
:type network: :class:`letsencrypt.client.network.Network`
|
||||
|
||||
:ivar list domains: list of str domains to get authorization
|
||||
:ivar dict authkey: Authorized Keys for each domain.
|
||||
values are of type :class:`letsencrypt.client.client.Client.Key`
|
||||
:ivar dict responses: keys: domain, values: list of dict responses
|
||||
:ivar dict msgs: ACME Challenge messages with domain as a key
|
||||
:ivar dict paths: optimal path for authorization. eg. paths[domain]
|
||||
:ivar dict dv_c: Keys - domain, Values are DV challenges in the form of
|
||||
:class:`letsencrypt.client.challenge_util.IndexedChall`
|
||||
:ivar dict client_c: Keys - domain, Values are Client challenges in the form
|
||||
of :class:`letsencrypt.client.challenge_util.IndexedChall`
|
||||
|
||||
"""
|
||||
def __init__(self, dv_auth, client_auth, network):
|
||||
self.dv_auth = dv_auth
|
||||
self.client_auth = client_auth
|
||||
self.network = network
|
||||
|
||||
self.domains = []
|
||||
self.authkey = dict()
|
||||
self.responses = dict()
|
||||
self.msgs = dict()
|
||||
self.paths = dict()
|
||||
|
||||
self.dv_c = dict()
|
||||
self.client_c = dict()
|
||||
|
||||
def add_chall_msg(self, domain, msg, authkey):
|
||||
"""Add a challenge message to the AuthHandler.
|
||||
|
||||
:param str domain: domain for authorization
|
||||
:param dict msg: ACME challenge message
|
||||
|
||||
:param authkey: authorized key for the challenge
|
||||
:type authkey: :class:`letsencrypt.client.client.Client.Key`
|
||||
|
||||
"""
|
||||
if domain in self.domains:
|
||||
raise errors.LetsEncryptAuthHandlerError(
|
||||
"Multiple ACMEChallengeMessages for the same domain "
|
||||
"is not supported.")
|
||||
self.domains.append(domain)
|
||||
self.responses[domain] = ["null"] * len(msg["challenges"])
|
||||
self.msgs[domain] = msg
|
||||
self.authkey[domain] = authkey
|
||||
|
||||
def get_authorizations(self):
|
||||
"""Retreive all authorizations for challenges.
|
||||
|
||||
:raises LetsEncryptAuthHandlerError: If unable to retrieve all
|
||||
authorizations
|
||||
|
||||
"""
|
||||
progress = True
|
||||
while self.msgs and progress:
|
||||
progress = False
|
||||
self._satisfy_challenges()
|
||||
|
||||
delete_list = []
|
||||
|
||||
for dom in self.domains:
|
||||
if self._path_satisfied(dom):
|
||||
self.acme_authorization(dom)
|
||||
delete_list.append(dom)
|
||||
|
||||
# This avoids modifying while iterating over the list
|
||||
if delete_list:
|
||||
self._cleanup_state(delete_list)
|
||||
progress = True
|
||||
|
||||
if not progress:
|
||||
raise errors.LetsEncryptAuthHandlerError(
|
||||
"Unable to solve challenges for requested names.")
|
||||
|
||||
def acme_authorization(self, domain):
|
||||
"""Handle ACME "authorization" phase.
|
||||
|
||||
:param str domain: domain that is requesting authorization
|
||||
|
||||
:returns: ACME "authorization" message.
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
try:
|
||||
return self.network.send_and_receive_expected(
|
||||
acme.authorization_request(
|
||||
self.msgs[domain]["sessionID"],
|
||||
domain,
|
||||
self.msgs[domain]["nonce"],
|
||||
self.responses[domain],
|
||||
self.authkey[domain].pem),
|
||||
"authorization")
|
||||
except errors.LetsEncryptClientError as err:
|
||||
logging.fatal(str(err))
|
||||
logging.fatal(
|
||||
"Failed Authorization procedure - cleaning up challenges")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
self._cleanup_challenges(domain)
|
||||
|
||||
def _satisfy_challenges(self):
|
||||
"""Attempt to satisfy all saved challenge messages."""
|
||||
logging.info("Performing the following challenges:")
|
||||
for dom in self.domains:
|
||||
self.paths[dom] = gen_challenge_path(
|
||||
self.msgs[dom]["challenges"],
|
||||
self._get_chall_pref(dom),
|
||||
self.msgs[dom].get("combinations", None))
|
||||
|
||||
self.dv_c[dom], self.client_c[dom] = self._challenge_factory(
|
||||
dom, self.paths[dom])
|
||||
|
||||
# Flatten challs for authenticator functions and remove index
|
||||
# Order is important here as we will not expose the outside
|
||||
# Authenticator to our own indices.
|
||||
flat_client = []
|
||||
flat_auth = []
|
||||
for dom in self.domains:
|
||||
flat_client.extend(ichall.chall for ichall in self.client_c[dom])
|
||||
flat_auth.extend(ichall.chall for ichall in self.dv_c[dom])
|
||||
|
||||
client_resp = self.client_auth.perform(flat_client)
|
||||
dv_resp = self.dv_auth.perform(flat_auth)
|
||||
|
||||
logging.info("Ready for verification...")
|
||||
|
||||
# Assemble Responses
|
||||
self._assign_responses(client_resp, self.client_c)
|
||||
self._assign_responses(dv_resp, self.dv_c)
|
||||
|
||||
def _assign_responses(self, flat_list, ichall_dict):
|
||||
"""Assign responses from flat_list back to the IndexedChall dicts.
|
||||
|
||||
:param list flat_list: flat_list of responses from an IAuthenticator
|
||||
:param dict ichall_dict: Master dict mapping all domains to a list of
|
||||
their associated 'client' and 'dv' IndexedChallenges, or their
|
||||
:class:`letsencrypt.client.challenge_util.IndexedChall` list
|
||||
|
||||
"""
|
||||
flat_index = 0
|
||||
for dom in self.domains:
|
||||
for ichall in ichall_dict[dom]:
|
||||
self.responses[dom][ichall.index] = flat_list[flat_index]
|
||||
flat_index += 1
|
||||
|
||||
def _path_satisfied(self, dom):
|
||||
"""Returns whether a path has been completely satisfied."""
|
||||
return all(
|
||||
None != self.responses[dom][i] and "null" != self.responses[dom][i]
|
||||
for i in self.paths[dom])
|
||||
|
||||
def _get_chall_pref(self, domain):
|
||||
"""Return list of challenge preferences.
|
||||
|
||||
:param str domain: domain for which you are requesting preferences
|
||||
|
||||
"""
|
||||
chall_prefs = []
|
||||
chall_prefs.extend(self.client_auth.get_chall_pref(domain))
|
||||
chall_prefs.extend(self.dv_auth.get_chall_pref(domain))
|
||||
return chall_prefs
|
||||
|
||||
def _cleanup_challenges(self, domain):
|
||||
"""Cleanup configuration challenges
|
||||
|
||||
:param str domain: domain for which to clean up challenges
|
||||
|
||||
"""
|
||||
logging.info("Cleaning up challenges...")
|
||||
self.dv_auth.cleanup(self.dv_c[domain])
|
||||
self.client_auth.cleanup(self.client_c[domain])
|
||||
|
||||
def _cleanup_state(self, delete_list):
|
||||
"""Cleanup state after an authorization is received.
|
||||
|
||||
:param list delete_list: list of domains in str form
|
||||
|
||||
"""
|
||||
for domain in delete_list:
|
||||
del self.msgs[domain]
|
||||
del self.responses[domain]
|
||||
del self.paths[domain]
|
||||
|
||||
del self.authkey[domain]
|
||||
|
||||
del self.client_c[domain]
|
||||
del self.dv_c[domain]
|
||||
|
||||
self.domains.remove(domain)
|
||||
|
||||
def _challenge_factory(self, domain, path):
|
||||
"""Construct Namedtuple Challenges
|
||||
|
||||
:param str domain: domain of the enrollee
|
||||
|
||||
:param list path: List of indices from `challenges`.
|
||||
|
||||
:returns: dv_chall, list of
|
||||
:class:`letsencrypt.client.challenge_util.IndexedChall`
|
||||
client_chall, list of
|
||||
:class:`letsencrypt.client.challenge_util.IndexedChall`
|
||||
:rtype: tuple
|
||||
|
||||
:raises errors.LetsEncryptClientError: If Challenge type is not
|
||||
recognized
|
||||
|
||||
"""
|
||||
challenges = self.msgs[domain]["challenges"]
|
||||
|
||||
dv_chall = []
|
||||
client_chall = []
|
||||
|
||||
for index in self.paths[domain]:
|
||||
chall = challenges[index]
|
||||
|
||||
# Authenticator Challenges
|
||||
if chall["type"] in CONFIG.DV_CHALLENGES:
|
||||
dv_chall.append(challenge_util.IndexedChall(
|
||||
self._construct_dv_chall(chall, domain), index))
|
||||
|
||||
# Client Challenges
|
||||
elif chall["type"] in CONFIG.CLIENT_CHALLENGES:
|
||||
client_chall.append(challenge_util.IndexedChall(
|
||||
self._construct_client_chall(chall, domain), index))
|
||||
|
||||
else:
|
||||
raise errors.LetsEncryptClientError(
|
||||
"Received unrecognized challenge of type: "
|
||||
"%s" % chall["type"])
|
||||
|
||||
return dv_chall, client_chall
|
||||
|
||||
def _construct_dv_chall(self, chall, domain):
|
||||
"""Construct Auth Type Challenges.
|
||||
|
||||
:param dict chall: Single challenge
|
||||
:param str domain: challenge's domain
|
||||
|
||||
: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[domain])
|
||||
|
||||
elif chall["type"] == "simpleHttps":
|
||||
logging.info(" SimpleHTTPS challenge for name %s.", domain)
|
||||
return challenge_util.SimpleHttpsChall(
|
||||
domain, str(chall["token"]), self.authkey[domain])
|
||||
|
||||
elif chall["type"] == "dns":
|
||||
logging.info(" DNS challenge for name %s.", domain)
|
||||
return challenge_util.DnsChall(
|
||||
domain, str(chall["token"]), self.authkey[domain])
|
||||
|
||||
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
|
||||
:param str domain: challenge's domain
|
||||
|
||||
: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"])
|
||||
|
||||
def gen_challenge_path(challenges, preferences, combos=None):
|
||||
"""Generate a plan to get authority over the identity.
|
||||
|
||||
.. todo:: Make sure that the challenges are feasible...
|
||||
Example: Do you have the recovery key?
|
||||
|
||||
:param list challenges: A list of challenges from ACME "challenge"
|
||||
server message to be fulfilled by the client in order to prove
|
||||
possession of the identifier.
|
||||
|
||||
:param list preferences: List of challenge preferences for domain
|
||||
|
||||
:param combos: A collection of sets of challenges from ACME
|
||||
"challenge" server message ("combinations"), each of which would
|
||||
be sufficient to prove possession of the identifier.
|
||||
:type combos: list or None
|
||||
|
||||
:returns: List of indices from `challenges`.
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
if combos:
|
||||
return _find_smart_path(challenges, preferences, combos)
|
||||
else:
|
||||
return _find_dumb_path(challenges, preferences)
|
||||
|
||||
|
||||
def _find_smart_path(challenges, preferences, combos):
|
||||
"""Find challenge path with server hints.
|
||||
|
||||
Can be called if combinations is included. Function uses a simple
|
||||
ranking system to choose the combo with the lowest cost.
|
||||
|
||||
:param list challenges: A list of challenges from ACME "challenge"
|
||||
server message to be fulfilled by the client in order to prove
|
||||
possession of the identifier.
|
||||
|
||||
:param combos: A collection of sets of challenges from ACME
|
||||
"challenge" server message ("combinations"), each of which would
|
||||
be sufficient to prove possession of the identifier.
|
||||
:type combos: list or None
|
||||
|
||||
:returns: List of indices from `challenges`.
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
chall_cost = {}
|
||||
max_cost = 0
|
||||
for i, chall in enumerate(preferences):
|
||||
chall_cost[chall] = i
|
||||
max_cost += i
|
||||
|
||||
best_combo = []
|
||||
# Set above completing all of the available challenges
|
||||
best_combo_cost = max_cost + 1
|
||||
|
||||
combo_total = 0
|
||||
for combo in combos:
|
||||
for challenge_index in combo:
|
||||
combo_total += chall_cost.get(challenges[
|
||||
challenge_index]["type"], max_cost)
|
||||
if combo_total < best_combo_cost:
|
||||
best_combo = combo
|
||||
best_combo_cost = combo_total
|
||||
combo_total = 0
|
||||
|
||||
if not best_combo:
|
||||
logging.fatal("Client does not support any combination of "
|
||||
"challenges to satisfy ACME server")
|
||||
sys.exit(22)
|
||||
|
||||
return best_combo
|
||||
|
||||
def _find_dumb_path(challenges, preferences):
|
||||
"""Find challenge path without server hints.
|
||||
|
||||
Should be called if the combinations hint is not included by the
|
||||
server. This function returns the best path that does not contain
|
||||
multiple mutually exclusive challenges.
|
||||
|
||||
:param list challenges: A list of challenges from ACME "challenge"
|
||||
server message to be fulfilled by the client in order to prove
|
||||
possession of the identifier.
|
||||
|
||||
:param list preferences: A list of preferences representing the
|
||||
challenge type found within the ACME spec. Each challenge type
|
||||
can only be listed once.
|
||||
|
||||
:returns: List of indices from `challenges`.
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
# Add logic for a crappy server
|
||||
# Choose a DV
|
||||
path = []
|
||||
assert len(preferences) == len(set(preferences))
|
||||
for pref_c in preferences:
|
||||
for i, offered_challenge in enumerate(challenges):
|
||||
if (pref_c == offered_challenge["type"] and
|
||||
is_preferred(offered_challenge["type"], path)):
|
||||
path.append((i, offered_challenge["type"]))
|
||||
|
||||
return [i for (i, _) in path]
|
||||
|
||||
def is_preferred(offered_challenge_type, path):
|
||||
"""Return whether or not the challenge is preferred in path."""
|
||||
for _, challenge_type in path:
|
||||
for mutually_exclusive in CONFIG.EXCLUSIVE_CHALLENGES:
|
||||
# Second part is in case we eventually allow multiple names
|
||||
# to be challenges at the same time
|
||||
if (challenge_type in mutually_exclusive and
|
||||
offered_challenge_type in mutually_exclusive and
|
||||
challenge_type != offered_challenge_type):
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -1,117 +0,0 @@
|
||||
"""ACME challenge."""
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from letsencrypt.client import CONFIG
|
||||
|
||||
|
||||
def gen_challenge_path(challenges, combos=None):
|
||||
"""Generate a plan to get authority over the identity.
|
||||
|
||||
.. todo:: Make sure that the challenges are feasible...
|
||||
Example: Do you have the recovery key?
|
||||
|
||||
:param list challenges: A list of challenges from ACME "challenge"
|
||||
server message to be fulfilled by the client in order to prove
|
||||
possession of the identifier.
|
||||
|
||||
:param combos: A collection of sets of challenges from ACME
|
||||
"challenge" server message ("combinations"), each of which would
|
||||
be sufficient to prove possession of the identifier.
|
||||
:type combos: list or None
|
||||
|
||||
:returns: List of indices from `challenges`.
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
if combos:
|
||||
return _find_smart_path(challenges, combos)
|
||||
else:
|
||||
return _find_dumb_path(challenges)
|
||||
|
||||
|
||||
def _find_smart_path(challenges, combos):
|
||||
"""Find challenge path with server hints.
|
||||
|
||||
Can be called if combinations is included. Function uses a simple
|
||||
ranking system to choose the combo with the lowest cost.
|
||||
|
||||
:param list challenges: A list of challenges from ACME "challenge"
|
||||
server message to be fulfilled by the client in order to prove
|
||||
possession of the identifier.
|
||||
|
||||
:param combos: A collection of sets of challenges from ACME
|
||||
"challenge" server message ("combinations"), each of which would
|
||||
be sufficient to prove possession of the identifier.
|
||||
:type combos: list or None
|
||||
|
||||
:returns: List of indices from `challenges`.
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
chall_cost = {}
|
||||
max_cost = 0
|
||||
for i, chall in enumerate(CONFIG.CHALLENGE_PREFERENCES):
|
||||
chall_cost[chall] = i
|
||||
max_cost += i
|
||||
|
||||
best_combo = []
|
||||
# Set above completing all of the available challenges
|
||||
best_combo_cost = max_cost + 1
|
||||
|
||||
combo_total = 0
|
||||
for combo in combos:
|
||||
for challenge_index in combo:
|
||||
combo_total += chall_cost.get(challenges[
|
||||
challenge_index]["type"], max_cost)
|
||||
if combo_total < best_combo_cost:
|
||||
best_combo = combo
|
||||
best_combo_cost = combo_total
|
||||
combo_total = 0
|
||||
|
||||
if not best_combo:
|
||||
logging.fatal("Client does not support any combination of "
|
||||
"challenges to satisfy ACME server")
|
||||
sys.exit(22)
|
||||
|
||||
return best_combo
|
||||
|
||||
|
||||
def _find_dumb_path(challenges):
|
||||
"""Find challenge path without server hints.
|
||||
|
||||
Should be called if the combinations hint is not included by the
|
||||
server. This function returns the best path that does not contain
|
||||
multiple mutually exclusive challenges.
|
||||
|
||||
:param list challenges: A list of challenges from ACME "challenge"
|
||||
server message to be fulfilled by the client in order to prove
|
||||
possession of the identifier.
|
||||
|
||||
:returns: List of indices from `challenges`.
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
# Add logic for a crappy server
|
||||
# Choose a DV
|
||||
path = []
|
||||
for pref_c in CONFIG.CHALLENGE_PREFERENCES:
|
||||
for i, offered_challenge in enumerate(challenges):
|
||||
if (pref_c == offered_challenge["type"] and
|
||||
is_preferred(offered_challenge["type"], path)):
|
||||
path.append((i, offered_challenge["type"]))
|
||||
|
||||
return [i for (i, _) in path]
|
||||
|
||||
|
||||
def is_preferred(offered_challenge_type, path):
|
||||
for _, challenge_type in path:
|
||||
for mutually_exclusive in CONFIG.EXCLUSIVE_CHALLENGES:
|
||||
# Second part is in case we eventually allow multiple names
|
||||
# to be challenges at the same time
|
||||
if (challenge_type in mutually_exclusive and
|
||||
offered_challenge_type in mutually_exclusive and
|
||||
challenge_type != offered_challenge_type):
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Challenge specific utility functions."""
|
||||
import collections
|
||||
import hashlib
|
||||
|
||||
from Crypto import Random
|
||||
@@ -7,6 +8,21 @@ from letsencrypt.client import CONFIG
|
||||
from letsencrypt.client import crypto_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")
|
||||
|
||||
# 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")
|
||||
|
||||
# Helper Challenge Wrapper - Can be used to maintain the proper position of
|
||||
# the response within a larger challenge list
|
||||
IndexedChall = collections.namedtuple("IndexedChall", "chall, index")
|
||||
|
||||
# DVSNI Challenge functions
|
||||
def dvsni_gen_cert(filepath, name, r_b64, nonce, key):
|
||||
|
||||
+36
-172
@@ -12,7 +12,8 @@ import M2Crypto
|
||||
import zope.component
|
||||
|
||||
from letsencrypt.client import acme
|
||||
from letsencrypt.client import challenge
|
||||
from letsencrypt.client import auth_handler
|
||||
from letsencrypt.client import client_authenticator
|
||||
from letsencrypt.client import CONFIG
|
||||
from letsencrypt.client import crypto_util
|
||||
from letsencrypt.client import errors
|
||||
@@ -38,27 +39,40 @@ class Client(object):
|
||||
:ivar authkey: Authorization Key
|
||||
:type authkey: :class:`letsencrypt.client.client.Client.Key`
|
||||
|
||||
:ivar auth: Object that supports the IAuthenticator interface.
|
||||
:type auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
|
||||
:ivar auth_handler: Object that supports the IAuthenticator interface.
|
||||
auth_handler contains both a dv_authenticator and a client_authenticator
|
||||
:type auth_handler: :class:`letsencrypt.client.auth_handler.AuthHandler`
|
||||
|
||||
:ivar installer: Object supporting the IInstaller interface.
|
||||
:type installer: :class:`letsencrypt.client.interfaces.IInstaller`
|
||||
|
||||
"""
|
||||
zope.interface.implements(interfaces.IAuthenticator)
|
||||
|
||||
Key = collections.namedtuple("Key", "file pem")
|
||||
CSR = collections.namedtuple("CSR", "file data form")
|
||||
|
||||
def __init__(self, server, names, authkey, auth, installer):
|
||||
"""Initialize a client."""
|
||||
def __init__(self, server, names, authkey, dv_auth, installer):
|
||||
"""Initialize a client.
|
||||
|
||||
:param str server: CA server to contact
|
||||
:param dv_auth: IAuthenticator Interface that can solve the
|
||||
CONFIG.DV_CHALLENGES
|
||||
:type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
|
||||
|
||||
"""
|
||||
self.network = network.Network(server)
|
||||
self.names = names
|
||||
self.authkey = authkey
|
||||
|
||||
sanity_check_names([server] + names)
|
||||
|
||||
self.auth = auth
|
||||
self.installer = installer
|
||||
|
||||
client_auth = client_authenticator.ClientAuthenticator(server)
|
||||
self.auth_handler = auth_handler.AuthHandler(
|
||||
dv_auth, client_auth, self.network)
|
||||
|
||||
def obtain_certificate(self, csr,
|
||||
cert_path=CONFIG.CERT_PATH,
|
||||
chain_path=CONFIG.CHAIN_PATH):
|
||||
@@ -76,13 +90,12 @@ class Client(object):
|
||||
|
||||
"""
|
||||
# Request Challenges
|
||||
challenge_msg = self.acme_challenge()
|
||||
for name in self.names:
|
||||
self.auth_handler.add_chall_msg(
|
||||
name, self.acme_challenge(name), self.authkey)
|
||||
|
||||
# Perform Challenges
|
||||
responses, challenge_objs = self.verify_identity(challenge_msg)
|
||||
|
||||
# Get Authorization
|
||||
self.acme_authorization(challenge_msg, challenge_objs, responses)
|
||||
# Perform Challenges/Get Authorizations
|
||||
self.auth_handler.get_authorizations()
|
||||
|
||||
# Retrieve certificate
|
||||
certificate_dict = self.acme_certificate(csr.data)
|
||||
@@ -95,43 +108,15 @@ class Client(object):
|
||||
|
||||
return cert_file, chain_file
|
||||
|
||||
def acme_challenge(self):
|
||||
def acme_challenge(self, domain):
|
||||
"""Handle ACME "challenge" phase.
|
||||
|
||||
.. todo:: Handle more than one domain name in self.names
|
||||
|
||||
:returns: ACME "challenge" message.
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
return self.network.send_and_receive_expected(
|
||||
acme.challenge_request(self.names[0]), "challenge")
|
||||
|
||||
def acme_authorization(self, challenge_msg, chal_objs, responses):
|
||||
"""Handle ACME "authorization" phase.
|
||||
|
||||
:param dict challenge_msg: ACME "challenge" message.
|
||||
|
||||
:param chal_objs: TODO - this will be a new object...
|
||||
:param responses: TODO
|
||||
|
||||
:returns: ACME "authorization" message.
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
try:
|
||||
return self.network.send_and_receive_expected(
|
||||
acme.authorization_request(
|
||||
challenge_msg["sessionID"], self.names[0],
|
||||
challenge_msg["nonce"], responses, self.authkey.pem),
|
||||
"authorization")
|
||||
except errors.LetsEncryptClientError as err:
|
||||
logging.fatal(str(err))
|
||||
logging.fatal(
|
||||
"Failed Authorization procedure - cleaning up challenges")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
self.cleanup_challenges(chal_objs)
|
||||
acme.challenge_request(domain), "challenge")
|
||||
|
||||
def acme_certificate(self, csr_der):
|
||||
"""Handle ACME "certificate" phase.
|
||||
@@ -242,78 +227,6 @@ class Client(object):
|
||||
# # TODO enable OCSP Stapling
|
||||
# continue
|
||||
|
||||
def cleanup_challenges(self, challenges):
|
||||
"""Cleanup configuration challenges
|
||||
|
||||
:param dict challenges: challenges from a challenge message
|
||||
|
||||
"""
|
||||
logging.info("Cleaning up challenges...")
|
||||
for chall in challenges:
|
||||
if chall["type"] in CONFIG.CONFIG_CHALLENGES:
|
||||
self.auth.cleanup()
|
||||
else:
|
||||
# Handle other cleanup if needed
|
||||
pass
|
||||
|
||||
def verify_identity(self, challenge_msg):
|
||||
"""Verify identity.
|
||||
|
||||
:param dict challenge_msg: ACME "challenge" message.
|
||||
|
||||
:returns: TODO
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
path = challenge.gen_challenge_path(
|
||||
challenge_msg["challenges"], challenge_msg.get("combinations", []))
|
||||
|
||||
logging.info("Performing the following challenges:")
|
||||
|
||||
# Every indices element is a list of integers referring to which
|
||||
# challenges in the master list the challenge object satisfies
|
||||
# Single Challenge objects that can satisfy multiple server challenges
|
||||
# mess up the order of the challenges, thus requiring the indices
|
||||
challenge_objs, indices = self.challenge_factory(
|
||||
self.names[0], challenge_msg["challenges"], path)
|
||||
|
||||
responses = ["null"] * len(challenge_msg["challenges"])
|
||||
|
||||
# Perform challenges
|
||||
for i, c_obj in enumerate(challenge_objs):
|
||||
resp = "null"
|
||||
if c_obj["type"] in CONFIG.CONFIG_CHALLENGES:
|
||||
resp = self.auth.perform(c_obj)
|
||||
else:
|
||||
# Handle RecoveryToken type challenges
|
||||
pass
|
||||
|
||||
self._assign_responses(resp, indices[i], responses)
|
||||
|
||||
logging.info(
|
||||
"Configured Apache for challenges; waiting for verification...")
|
||||
|
||||
return responses, challenge_objs
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def _assign_responses(self, resp, index_list, responses):
|
||||
"""Assign chall_response to appropriate places in response list.
|
||||
|
||||
:param resp: responses from a challenge
|
||||
:type resp: list of dicts or dict
|
||||
|
||||
:param list index_list: respective challenges resp satisfies
|
||||
:param list responses: master list of responses
|
||||
|
||||
"""
|
||||
if isinstance(resp, list):
|
||||
assert len(resp) == len(index_list)
|
||||
for j, index in enumerate(index_list):
|
||||
responses[index] = resp[j]
|
||||
else:
|
||||
for index in index_list:
|
||||
responses[index] = resp
|
||||
|
||||
def store_cert_key(self, cert_file, encrypt=False):
|
||||
"""Store certificate key.
|
||||
|
||||
@@ -391,67 +304,18 @@ class Client(object):
|
||||
vhost.add(host)
|
||||
return vhost
|
||||
|
||||
def challenge_factory(self, name, challenges, path):
|
||||
"""
|
||||
|
||||
:param name: TODO
|
||||
|
||||
:param list challenges: A list of challenges from ACME "challenge"
|
||||
server message to be fulfilled by the client in order to prove
|
||||
possession of the identifier.
|
||||
|
||||
:param list path: List of indices from `challenges`.
|
||||
|
||||
:returns: A pair of TODO
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
sni_todo = []
|
||||
# Since a single invocation of SNI challenge can satisfy multiple
|
||||
# challenges. We must keep track of all the challenges it satisfies
|
||||
sni_satisfies = []
|
||||
|
||||
challenge_objs = []
|
||||
challenge_obj_indices = []
|
||||
for index in path:
|
||||
chall = challenges[index]
|
||||
|
||||
if chall["type"] == "dvsni":
|
||||
logging.info(" DVSNI challenge for name %s.", name)
|
||||
sni_satisfies.append(index)
|
||||
sni_todo.append((str(name), str(chall["r"]),
|
||||
str(chall["nonce"])))
|
||||
|
||||
elif chall["type"] == "recoveryToken":
|
||||
logging.info("\tRecovery Token Challenge for name: %s.", name)
|
||||
challenge_obj_indices.append(index)
|
||||
challenge_objs.append({
|
||||
type: "recoveryToken",
|
||||
})
|
||||
|
||||
else:
|
||||
logging.fatal("Challenge not currently supported")
|
||||
sys.exit(82)
|
||||
|
||||
if sni_todo:
|
||||
# SNI_Challenge can satisfy many sni challenges at once so only
|
||||
# one "challenge object" is issued for all sni_challenges
|
||||
challenge_objs.append({
|
||||
"type": "dvsni",
|
||||
"list_sni_tuple": sni_todo,
|
||||
"dvsni_key": self.authkey,
|
||||
})
|
||||
challenge_obj_indices.append(sni_satisfies)
|
||||
logging.debug(sni_todo)
|
||||
|
||||
return challenge_objs, challenge_obj_indices
|
||||
|
||||
|
||||
def validate_key_csr(privkey, csr, names):
|
||||
def validate_key_csr(privkey, csr):
|
||||
"""Validate CSR and key files.
|
||||
|
||||
Verifies that the client key and csr arguments are valid and
|
||||
correspond to one another.
|
||||
Verifies that the client key and csr arguments are valid and correspond to
|
||||
one another. This does not currently check the names in the CSR.
|
||||
|
||||
:param privkey: Key associated with CSR
|
||||
:type privkey: :class:`letsencrypt.client.client.Client.Key`
|
||||
|
||||
:param csr: CSR
|
||||
:type csr: :class:`letsencrypt.client.client.Client.CSR`
|
||||
|
||||
:raises LetsEncryptClientError: if validation fails
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Client Authenticator"""
|
||||
import zope.interface
|
||||
|
||||
from letsencrypt.client import challenge_util
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client import interfaces
|
||||
from letsencrypt.client import recovery_token
|
||||
|
||||
class ClientAuthenticator(object):
|
||||
"""IAuthenticator for CONFIG.CLIENT_CHALLENGES.
|
||||
|
||||
:ivar rec_token: Performs "recoveryToken" challenges
|
||||
:type rec_token: :class:`letsencrypt.client.recovery_token.RecoveryToken`
|
||||
|
||||
"""
|
||||
zope.interface.implements(interfaces.IAuthenticator)
|
||||
|
||||
# This will have an installer soon for get_key/cert purposes
|
||||
def __init__(self, server):
|
||||
"""Initialize Client Authenticator.
|
||||
|
||||
:param str server: ACME CA Server
|
||||
|
||||
"""
|
||||
self.rec_token = recovery_token.RecoveryToken(server)
|
||||
|
||||
# pylint: disable=unused-argument,no-self-use
|
||||
def get_chall_pref(self, domain):
|
||||
"""Return list of challenge preferences."""
|
||||
return ["recoveryToken"]
|
||||
|
||||
def perform(self, chall_list):
|
||||
"""Perform client specific challenges for IAuthenticator"""
|
||||
responses = []
|
||||
for chall in chall_list:
|
||||
if isinstance(chall, challenge_util.RecTokenChall):
|
||||
responses.append(self.rec_token.perform(chall))
|
||||
else:
|
||||
raise errors.LetsEncryptClientAuthError("Unexpected Challenge")
|
||||
return responses
|
||||
|
||||
def cleanup(self, chall_list):
|
||||
"""Cleanup call for IAuthenticator."""
|
||||
for chall in chall_list:
|
||||
if isinstance(chall, challenge_util.RecTokenChall):
|
||||
self.rec_token.cleanup(chall)
|
||||
else:
|
||||
raise errors.LetsEncryptClientAuthError("Unexpected Challenge")
|
||||
@@ -5,8 +5,16 @@ class LetsEncryptClientError(Exception):
|
||||
"""Generic Let's Encrypt client error."""
|
||||
|
||||
|
||||
class LetsEncryptAuthHandlerError(LetsEncryptClientError):
|
||||
"""Let's Encrypt Auth Handler error."""
|
||||
|
||||
|
||||
class LetsEncryptClientAuthError(LetsEncryptAuthHandlerError):
|
||||
"""Let's Encrypt Client Authenticator Error."""
|
||||
|
||||
|
||||
class LetsEncryptConfiguratorError(LetsEncryptClientError):
|
||||
"""Let's Encrypt configurator error."""
|
||||
"""Let's Encrypt Configurator error."""
|
||||
|
||||
|
||||
class LetsEncryptDvsniError(LetsEncryptConfiguratorError):
|
||||
|
||||
@@ -11,22 +11,36 @@ class IAuthenticator(zope.interface.Interface):
|
||||
ability to perform challenges and attain a certificate.
|
||||
|
||||
"""
|
||||
def perform(chall_dict):
|
||||
"""Perform the given challenge"""
|
||||
def get_chall_pref(domain):
|
||||
"""Return list of challenge preferences.
|
||||
|
||||
def cleanup():
|
||||
:param str domain: Domain for which challenge preferences are sought.
|
||||
|
||||
:returns: list of strings with the most preferred challenges first.
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
def perform(chall_list):
|
||||
"""Perform the given challenge.
|
||||
|
||||
:param list chall_list: List of challenge types defined in client.py
|
||||
|
||||
:returns: List of responses
|
||||
If the challenge cant be completed...
|
||||
None - Authenticator can perform challenge, but can't at this time
|
||||
False - Authenticator will never be able to perform (error)
|
||||
:rtype: `list` of dicts
|
||||
|
||||
"""
|
||||
def cleanup(chall_list):
|
||||
"""Revert changes and shutdown after challenges complete."""
|
||||
|
||||
|
||||
class IChallenge(zope.interface.Interface):
|
||||
"""Let's Encrypt challenge."""
|
||||
|
||||
def perform(quiet=True):
|
||||
"""Perform the challenge.
|
||||
|
||||
:param bool quiet: TODO
|
||||
|
||||
"""
|
||||
def perform():
|
||||
"""Perform the challenge."""
|
||||
|
||||
def generate_response():
|
||||
"""Generate response."""
|
||||
|
||||
@@ -62,9 +62,9 @@ def unique_file(default_name, mode=0o777):
|
||||
f_parsed = os.path.splitext(default_name)
|
||||
while 1:
|
||||
try:
|
||||
fd = os.open(
|
||||
file_d = os.open(
|
||||
default_name, os.O_CREAT | os.O_EXCL | os.O_RDWR, mode)
|
||||
return os.fdopen(fd, 'w'), default_name
|
||||
return os.fdopen(file_d, 'w'), default_name
|
||||
except OSError:
|
||||
pass
|
||||
default_name = f_parsed[0] + '_' + str(count) + f_parsed[1]
|
||||
|
||||
@@ -11,6 +11,9 @@ from letsencrypt.client import acme
|
||||
from letsencrypt.client import errors
|
||||
|
||||
|
||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
|
||||
|
||||
class Network(object):
|
||||
"""Class for communicating with ACME servers.
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""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.
|
||||
|
||||
"""
|
||||
def __init__(self, server, direc=CONFIG.REV_TOKENS_DIR):
|
||||
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_dv_challenges():
|
||||
"""Returns all auth challenges."""
|
||||
return [chall for typ, chall in CHALLENGES.iteritems()
|
||||
if typ in CONFIG.DV_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.DV_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
|
||||
@@ -0,0 +1 @@
|
||||
"""Let's Encrypt Apache Tests"""
|
||||
+1
-1
@@ -17,7 +17,7 @@ def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"):
|
||||
work_dir = tempfile.mkdtemp("work")
|
||||
|
||||
test_configs = pkg_resources.resource_filename(
|
||||
__name__, "testdata/%s" % test_dir)
|
||||
"letsencrypt.client.tests", "testdata/%s" % test_dir)
|
||||
|
||||
shutil.copytree(
|
||||
test_configs, os.path.join(temp_dir, test_dir), symlinks=True)
|
||||
+43
-4
@@ -1,5 +1,6 @@
|
||||
"""Test for letsencrypt.client.apache_configurator."""
|
||||
"""Test for letsencrypt.client.apache.configurator."""
|
||||
import os
|
||||
import pkg_resources
|
||||
import re
|
||||
import shutil
|
||||
import unittest
|
||||
@@ -7,21 +8,22 @@ import unittest
|
||||
import mock
|
||||
import zope.component
|
||||
|
||||
from letsencrypt.client import display
|
||||
from letsencrypt.client import challenge_util
|
||||
from letsencrypt.client import client
|
||||
from letsencrypt.client import errors
|
||||
|
||||
from letsencrypt.client.apache import configurator
|
||||
from letsencrypt.client.apache import obj
|
||||
from letsencrypt.client.apache import parser
|
||||
|
||||
from letsencrypt.client.tests import config_util
|
||||
from letsencrypt.client.tests.apache import config_util
|
||||
|
||||
|
||||
class TwoVhost80Test(unittest.TestCase):
|
||||
"""Test two standard well configured HTTP vhosts."""
|
||||
|
||||
def setUp(self):
|
||||
zope.component.provideUtility(display.NcursesDisplay())
|
||||
#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")
|
||||
@@ -159,5 +161,42 @@ class TwoVhost80Test(unittest.TestCase):
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptConfiguratorError, self.config.get_version)
|
||||
|
||||
@mock.patch("letsencrypt.client.apache.configurator."
|
||||
"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
|
||||
# Note: As more challenges are offered this will have to be expanded
|
||||
rsa256_file = pkg_resources.resource_filename(
|
||||
"letsencrypt.client.tests", 'testdata/rsa256_key.pem')
|
||||
rsa256_pem = pkg_resources.resource_string(
|
||||
"letsencrypt.client.tests", 'testdata/rsa256_key.pem')
|
||||
|
||||
auth_key = client.Client.Key(rsa256_file, rsa256_pem)
|
||||
chall1 = challenge_util.DvsniChall(
|
||||
"encryption-example.demo",
|
||||
"jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q",
|
||||
"37bc5eb75d3e00a19b4f6355845e5a18",
|
||||
auth_key)
|
||||
chall2 = challenge_util.DvsniChall(
|
||||
"letsencrypt.demo",
|
||||
"uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU",
|
||||
"59ed014cac95f77057b1d7a1b2c596ba",
|
||||
auth_key)
|
||||
|
||||
dvsni_ret_val = [
|
||||
{"type": "dvsni", "s": "randomS1"},
|
||||
{"type": "dvsni", "s": "randomS2"}
|
||||
]
|
||||
|
||||
mock_dvsni_perform.return_value = dvsni_ret_val
|
||||
responses = self.config.perform([chall1, chall2])
|
||||
|
||||
self.assertEqual(mock_dvsni_perform.call_count, 1)
|
||||
self.assertEqual(responses, dvsni_ret_val)
|
||||
|
||||
self.assertEqual(mock_restart.call_count, 1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,152 @@
|
||||
"""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.tests.apache 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(
|
||||
"letsencrypt.client.tests", 'testdata/rsa256_key.pem')
|
||||
rsa256_pem = pkg_resources.resource_string(
|
||||
"letsencrypt.client.tests", 'testdata/rsa256_key.pem')
|
||||
|
||||
auth_key = client.Client.Key(rsa256_file, rsa256_pem)
|
||||
self.challs = []
|
||||
self.challs.append(challenge_util.DvsniChall(
|
||||
"encryption-example.demo",
|
||||
"jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q",
|
||||
"37bc5eb75d3e00a19b4f6355845e5a18",
|
||||
auth_key))
|
||||
self.challs.append(challenge_util.DvsniChall(
|
||||
"letsencrypt.demo",
|
||||
"uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU",
|
||||
"59ed014cac95f77057b1d7a1b2c596ba",
|
||||
auth_key))
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
shutil.rmtree(self.config_dir)
|
||||
shutil.rmtree(self.work_dir)
|
||||
|
||||
def test_perform0(self):
|
||||
resp = self.sni.perform()
|
||||
self.assertIs(resp, None)
|
||||
|
||||
@mock.patch("letsencrypt.client.apache.configurator."
|
||||
"ApacheConfigurator.restart")
|
||||
@mock.patch("letsencrypt.client.challenge_util.dvsni_gen_cert")
|
||||
def test_perform1(self, mock_dvsni_gen_cert, mock_restart):
|
||||
chall = self.challs[0]
|
||||
self.sni.add_chall(chall)
|
||||
mock_dvsni_gen_cert.return_value = "randomS1"
|
||||
responses = self.sni.perform()
|
||||
|
||||
self.assertEqual(mock_dvsni_gen_cert.call_count, 1)
|
||||
calls = mock_dvsni_gen_cert.call_args_list
|
||||
expected_call_list = [
|
||||
(self.sni.get_cert_file(chall.nonce), chall.domain,
|
||||
chall.r_b64, chall.nonce, chall.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), 1)
|
||||
self.assertEqual(responses[0]["s"], "randomS1")
|
||||
|
||||
@mock.patch("letsencrypt.client.apache.configurator."
|
||||
"ApacheConfigurator.restart")
|
||||
@mock.patch("letsencrypt.client.challenge_util.dvsni_gen_cert")
|
||||
def test_perform2(self, mock_dvsni_gen_cert, mock_restart):
|
||||
for chall in self.challs:
|
||||
self.sni.add_chall(chall)
|
||||
|
||||
mock_dvsni_gen_cert.side_effect = ["randomS0", "randomS1"]
|
||||
responses = self.sni.perform()
|
||||
|
||||
self.assertEqual(mock_dvsni_gen_cert.call_count, 2)
|
||||
calls = mock_dvsni_gen_cert.call_args_list
|
||||
expected_call_list = []
|
||||
|
||||
for chall in self.challs:
|
||||
expected_call_list.append(
|
||||
(self.sni.get_cert_file(chall.nonce), chall.domain,
|
||||
chall.r_b64, chall.nonce, chall.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)
|
||||
for i in range(2):
|
||||
self.assertEqual(responses[i]["s"], "randomS%d" % i)
|
||||
|
||||
def test_mod_config(self):
|
||||
from letsencrypt.client.apache.obj import Addr
|
||||
for chall in self.challs:
|
||||
self.sni.add_chall(chall)
|
||||
v_addr1 = [Addr(("1.2.3.4", "443")), Addr(("5.6.7.8", "443"))]
|
||||
v_addr2 = [Addr(("127.0.0.1", "443"))]
|
||||
ll_addr = []
|
||||
ll_addr.append(v_addr1)
|
||||
ll_addr.append(v_addr2)
|
||||
self.sni._mod_config(ll_addr) # pylint: disable=protected-access
|
||||
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.challs[0].nonce + CONFIG.INVALID_EXT)]))
|
||||
else:
|
||||
self.assertEqual(vhost.addrs, set(v_addr2))
|
||||
self.assertEqual(
|
||||
vhost.names,
|
||||
set([str(self.challs[1].nonce + CONFIG.INVALID_EXT)]))
|
||||
+16
-12
@@ -1,14 +1,13 @@
|
||||
"""Test the helper objects in apache.obj.py."""
|
||||
import unittest
|
||||
|
||||
from letsencrypt.client.apache import obj
|
||||
|
||||
|
||||
class AddrTest(unittest.TestCase):
|
||||
"""Test the Addr class."""
|
||||
def setUp(self):
|
||||
self.addr1 = obj.Addr.fromstring("192.168.1.1")
|
||||
self.addr2 = obj.Addr.fromstring("192.168.1.1:*")
|
||||
self.addr3 = obj.Addr.fromstring("192.168.1.1:80")
|
||||
from letsencrypt.client.apache.obj import Addr
|
||||
self.addr1 = Addr.fromstring("192.168.1.1")
|
||||
self.addr2 = Addr.fromstring("192.168.1.1:*")
|
||||
self.addr3 = Addr.fromstring("192.168.1.1:80")
|
||||
|
||||
def test_fromstring(self):
|
||||
self.assertEqual(self.addr1.get_addr(), "192.168.1.1")
|
||||
@@ -36,9 +35,10 @@ class AddrTest(unittest.TestCase):
|
||||
self.assertNotEqual(self.addr1, 3333)
|
||||
|
||||
def test_set_inclusion(self):
|
||||
from letsencrypt.client.apache.obj import Addr
|
||||
set_a = set([self.addr1, self.addr2])
|
||||
addr1b = obj.Addr.fromstring("192.168.1.1")
|
||||
addr2b = obj.Addr.fromstring("192.168.1.1:*")
|
||||
addr1b = Addr.fromstring("192.168.1.1")
|
||||
addr2b = Addr.fromstring("192.168.1.1:*")
|
||||
set_b = set([addr1b, addr2b])
|
||||
|
||||
self.assertEqual(set_a, set_b)
|
||||
@@ -47,14 +47,18 @@ class AddrTest(unittest.TestCase):
|
||||
class VirtualHostTest(unittest.TestCase):
|
||||
"""Test the VirtualHost class."""
|
||||
def setUp(self):
|
||||
self.vhost1 = obj.VirtualHost(
|
||||
from letsencrypt.client.apache.obj import VirtualHost
|
||||
from letsencrypt.client.apache.obj import Addr
|
||||
self.vhost1 = VirtualHost(
|
||||
"filep", "vh_path",
|
||||
set([obj.Addr.fromstring("localhost")]), False, False)
|
||||
set([Addr.fromstring("localhost")]), False, False)
|
||||
|
||||
def test_eq(self):
|
||||
vhost1b = obj.VirtualHost(
|
||||
from letsencrypt.client.apache.obj import Addr
|
||||
from letsencrypt.client.apache.obj import VirtualHost
|
||||
vhost1b = VirtualHost(
|
||||
"filep", "vh_path",
|
||||
set([obj.Addr.fromstring("localhost")]), False, False)
|
||||
set([Addr.fromstring("localhost")]), False, False)
|
||||
|
||||
self.assertEqual(vhost1b, self.vhost1)
|
||||
self.assertEqual(str(vhost1b), str(self.vhost1))
|
||||
+3
-2
@@ -1,3 +1,4 @@
|
||||
"""Tests the ApacheParser class."""
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
@@ -10,11 +11,11 @@ import zope.component
|
||||
from letsencrypt.client import display
|
||||
from letsencrypt.client import errors
|
||||
from letsencrypt.client.apache import parser
|
||||
from letsencrypt.client.tests import config_util
|
||||
from letsencrypt.client.tests.apache import config_util
|
||||
|
||||
|
||||
class ApacheParserTest(unittest.TestCase):
|
||||
|
||||
"""Apache Parser Test."""
|
||||
def setUp(self):
|
||||
zope.component.provideUtility(display.FileDisplay(sys.stdout))
|
||||
|
||||
@@ -0,0 +1,425 @@
|
||||
"""Test auth_handler.py."""
|
||||
import unittest
|
||||
import mock
|
||||
|
||||
from letsencrypt.client.tests import acme_util
|
||||
|
||||
|
||||
TRANSLATE = {"dvsni": "DvsniChall",
|
||||
"simpleHttps": "SimpleHttpsChall",
|
||||
"dns": "DnsChall",
|
||||
"recoveryToken": "RecTokenChall",
|
||||
"recoveryContact": "RecContactChall",
|
||||
"proofOfPossession": "PopChall"}
|
||||
|
||||
|
||||
# pylint: disable=protected-access
|
||||
class SatisfyChallengesTest(unittest.TestCase):
|
||||
"""verify_identities test."""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.auth_handler import AuthHandler
|
||||
|
||||
self.mock_dv_auth = mock.MagicMock(name='ApacheConfigurator')
|
||||
self.mock_client_auth = mock.MagicMock(name='ClientAuthenticator')
|
||||
|
||||
self.mock_dv_auth.get_chall_pref.return_value = ["dvsni"]
|
||||
self.mock_client_auth.get_chall_pref.return_value = ["recoveryToken"]
|
||||
|
||||
self.mock_client_auth.perform.side_effect = gen_auth_resp
|
||||
self.mock_dv_auth.perform.side_effect = gen_auth_resp
|
||||
|
||||
self.handler = AuthHandler(
|
||||
self.mock_dv_auth, self.mock_client_auth, None)
|
||||
|
||||
def test_name1_dvsni1(self):
|
||||
dom = "0"
|
||||
challenge = [acme_util.CHALLENGES["dvsni"]]
|
||||
msg = acme_util.get_chall_msg(dom, "nonce0", challenge)
|
||||
self.handler.add_chall_msg(dom, msg, "dummy_key")
|
||||
|
||||
self.handler._satisfy_challenges()
|
||||
|
||||
self.assertEqual(len(self.handler.responses), 1)
|
||||
self.assertEqual(len(self.handler.responses[dom]), 1)
|
||||
|
||||
self.assertEqual("DvsniChall0", self.handler.responses[dom][0])
|
||||
self.assertEqual(len(self.handler.dv_c), 1)
|
||||
self.assertEqual(len(self.handler.client_c), 1)
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.client_c[dom]), 0)
|
||||
|
||||
def test_name5_dvsni5(self):
|
||||
challenge = [acme_util.CHALLENGES["dvsni"]]
|
||||
for i in range(5):
|
||||
self.handler.add_chall_msg(
|
||||
str(i),
|
||||
acme_util.get_chall_msg(str(i), "nonce%d" % i, challenge),
|
||||
"dummy_key")
|
||||
|
||||
self.handler._satisfy_challenges()
|
||||
|
||||
self.assertEqual(len(self.handler.responses), 5)
|
||||
self.assertEqual(len(self.handler.dv_c), 5)
|
||||
self.assertEqual(len(self.handler.client_c), 5)
|
||||
# Each message contains 1 auth, 0 client
|
||||
|
||||
for i in range(5):
|
||||
dom = str(i)
|
||||
self.assertEqual(len(self.handler.responses[dom]), 1)
|
||||
self.assertEqual(self.handler.responses[dom][0], "DvsniChall%d" % i)
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.client_c[dom]), 0)
|
||||
self.assertEqual(
|
||||
type(self.handler.dv_c[dom][0].chall).__name__, "DvsniChall")
|
||||
|
||||
@mock.patch("letsencrypt.client.auth_handler.gen_challenge_path")
|
||||
def test_name1_auth(self, mock_chall_path):
|
||||
dom = "0"
|
||||
|
||||
challenges = acme_util.get_dv_challenges()
|
||||
combos = acme_util.gen_combos(challenges)
|
||||
self.handler.add_chall_msg(
|
||||
dom,
|
||||
acme_util.get_chall_msg("0", "nonce0", challenges, combos),
|
||||
"dummy_key")
|
||||
|
||||
path = gen_path(["simpleHttps"], challenges)
|
||||
mock_chall_path.return_value = path
|
||||
self.handler._satisfy_challenges()
|
||||
|
||||
self.assertEqual(len(self.handler.responses), 1)
|
||||
self.assertEqual(len(self.handler.responses[dom]), len(challenges))
|
||||
self.assertEqual(len(self.handler.dv_c), 1)
|
||||
self.assertEqual(len(self.handler.client_c), 1)
|
||||
|
||||
self.assertEqual(
|
||||
self.handler.responses[dom],
|
||||
self._get_exp_response(dom, path, challenges))
|
||||
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.client_c[dom]), 0)
|
||||
self.assertEqual(
|
||||
type(self.handler.dv_c[dom][0].chall).__name__, "SimpleHttpsChall")
|
||||
|
||||
@mock.patch("letsencrypt.client.auth_handler.gen_challenge_path")
|
||||
def test_name1_all(self, mock_chall_path):
|
||||
dom = "0"
|
||||
|
||||
challenges = acme_util.get_challenges()
|
||||
combos = acme_util.gen_combos(challenges)
|
||||
self.handler.add_chall_msg(
|
||||
dom,
|
||||
acme_util.get_chall_msg(dom, "nonce0", challenges, combos),
|
||||
"dummy_key")
|
||||
|
||||
path = gen_path(["simpleHttps", "recoveryToken"], challenges)
|
||||
mock_chall_path.return_value = path
|
||||
|
||||
self.handler._satisfy_challenges()
|
||||
|
||||
self.assertEqual(len(self.handler.responses), 1)
|
||||
self.assertEqual(len(self.handler.responses[dom]), len(challenges))
|
||||
self.assertEqual(len(self.handler.dv_c), 1)
|
||||
self.assertEqual(len(self.handler.client_c), 1)
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.client_c[dom]), 1)
|
||||
|
||||
self.assertEqual(
|
||||
self.handler.responses[dom],
|
||||
self._get_exp_response(dom, path, challenges))
|
||||
self.assertEqual(
|
||||
type(self.handler.dv_c[dom][0].chall).__name__, "SimpleHttpsChall")
|
||||
self.assertEqual(
|
||||
type(self.handler.client_c[dom][0].chall).__name__, "RecTokenChall")
|
||||
|
||||
@mock.patch("letsencrypt.client.auth_handler.gen_challenge_path")
|
||||
def test_name5_all(self, mock_chall_path):
|
||||
challenges = acme_util.get_challenges()
|
||||
combos = acme_util.gen_combos(challenges)
|
||||
for i in range(5):
|
||||
self.handler.add_chall_msg(
|
||||
str(i),
|
||||
acme_util.get_chall_msg(
|
||||
str(i), "nonce%d" % i, challenges, combos),
|
||||
"dummy_key")
|
||||
|
||||
path = gen_path(["dvsni", "recoveryContact"], challenges)
|
||||
mock_chall_path.return_value = path
|
||||
|
||||
self.handler._satisfy_challenges()
|
||||
|
||||
self.assertEqual(len(self.handler.responses), 5)
|
||||
for i in range(5):
|
||||
self.assertEqual(
|
||||
len(self.handler.responses[str(i)]), len(challenges))
|
||||
self.assertEqual(len(self.handler.dv_c), 5)
|
||||
self.assertEqual(len(self.handler.client_c), 5)
|
||||
|
||||
for i in range(5):
|
||||
dom = str(i)
|
||||
self.assertEqual(
|
||||
self.handler.responses[dom],
|
||||
self._get_exp_response(dom, path, challenges))
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(len(self.handler.client_c[dom]), 1)
|
||||
|
||||
self.assertEqual(
|
||||
type(self.handler.dv_c[dom][0].chall).__name__, "DvsniChall")
|
||||
self.assertEqual(
|
||||
type(self.handler.client_c[dom][0].chall).__name__,
|
||||
"RecContactChall")
|
||||
|
||||
@mock.patch("letsencrypt.client.auth_handler.gen_challenge_path")
|
||||
def test_name5_mix(self, mock_chall_path):
|
||||
paths = []
|
||||
chosen_chall = [["dns"],
|
||||
["dvsni"],
|
||||
["simpleHttps", "proofOfPossession"],
|
||||
["simpleHttps"],
|
||||
["dns", "recoveryToken"]]
|
||||
challenge_list = [acme_util.get_dv_challenges(),
|
||||
[acme_util.CHALLENGES["dvsni"]],
|
||||
acme_util.get_challenges(),
|
||||
acme_util.get_dv_challenges(),
|
||||
acme_util.get_challenges()]
|
||||
|
||||
# Combos doesn't matter since I am overriding the gen_path function
|
||||
for i in range(5):
|
||||
dom = str(i)
|
||||
paths.append(gen_path(chosen_chall[i], challenge_list[i]))
|
||||
self.handler.add_chall_msg(
|
||||
dom,
|
||||
acme_util.get_chall_msg(
|
||||
dom, "nonce%d" % i, challenge_list[i]),
|
||||
"dummy_key")
|
||||
|
||||
mock_chall_path.side_effect = paths
|
||||
|
||||
self.handler._satisfy_challenges()
|
||||
|
||||
self.assertEqual(len(self.handler.responses), 5)
|
||||
self.assertEqual(len(self.handler.dv_c), 5)
|
||||
self.assertEqual(len(self.handler.client_c), 5)
|
||||
|
||||
for i in range(5):
|
||||
dom = str(i)
|
||||
resp = self._get_exp_response(i, paths[i], challenge_list[i])
|
||||
self.assertEqual(self.handler.responses[dom], resp)
|
||||
self.assertEqual(len(self.handler.dv_c[dom]), 1)
|
||||
self.assertEqual(
|
||||
len(self.handler.client_c[dom]), len(chosen_chall[i]) - 1)
|
||||
|
||||
self.assertEqual(
|
||||
type(self.handler.dv_c["0"][0].chall).__name__, "DnsChall")
|
||||
self.assertEqual(
|
||||
type(self.handler.dv_c["1"][0].chall).__name__, "DvsniChall")
|
||||
self.assertEqual(
|
||||
type(self.handler.dv_c["2"][0].chall).__name__, "SimpleHttpsChall")
|
||||
self.assertEqual(
|
||||
type(self.handler.dv_c["3"][0].chall).__name__, "SimpleHttpsChall")
|
||||
self.assertEqual(
|
||||
type(self.handler.dv_c["4"][0].chall).__name__, "DnsChall")
|
||||
|
||||
self.assertEqual(
|
||||
type(self.handler.client_c["2"][0].chall).__name__, "PopChall")
|
||||
self.assertEqual(
|
||||
type(self.handler.client_c["4"][0].chall).__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
|
||||
|
||||
|
||||
# pylint: disable=protected-access
|
||||
class GetAuthorizationsTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.client.auth_handler import AuthHandler
|
||||
|
||||
self.mock_dv_auth = mock.MagicMock(name='ApacheConfigurator')
|
||||
self.mock_client_auth = mock.MagicMock(name='ClientAuthenticator')
|
||||
|
||||
self.mock_sat_chall = mock.MagicMock(name="_satisfy_challenges")
|
||||
self.mock_acme_auth = mock.MagicMock(name="acme_authorization")
|
||||
|
||||
self.iteration = 0
|
||||
|
||||
self.handler = AuthHandler(
|
||||
self.mock_dv_auth, self.mock_client_auth, None)
|
||||
|
||||
self.handler._satisfy_challenges = self.mock_sat_chall
|
||||
self.handler.acme_authorization = self.mock_acme_auth
|
||||
|
||||
def test_solved3_at_once(self):
|
||||
# Set 3 DVSNI challenges
|
||||
challenge = [acme_util.CHALLENGES["dvsni"]]
|
||||
for i in range(3):
|
||||
self.handler.add_chall_msg(
|
||||
str(i),
|
||||
acme_util.get_chall_msg(str(i), "nonce%d" % i, challenge),
|
||||
"dummy_key")
|
||||
|
||||
self.mock_sat_chall.side_effect = self._sat_solved_at_once
|
||||
self.handler.get_authorizations()
|
||||
|
||||
self.assertEqual(self.mock_sat_chall.call_count, 1)
|
||||
self.assertEqual(self.mock_acme_auth.call_count, 3)
|
||||
|
||||
exp_call_list = [mock.call("0"), mock.call("1"), mock.call("2")]
|
||||
self.assertEqual(
|
||||
self.mock_acme_auth.call_args_list, exp_call_list)
|
||||
self._test_finished()
|
||||
|
||||
def _sat_solved_at_once(self):
|
||||
for i in range(3):
|
||||
dom = str(i)
|
||||
self.handler.responses[dom] = ["DvsniChall%d" % i]
|
||||
self.handler.paths[dom] = [0]
|
||||
# Assignment was > 80 char...
|
||||
dv_c, c_c = self.handler._challenge_factory(dom, [0])
|
||||
|
||||
self.handler.dv_c[dom], self.handler.client_c[dom] = dv_c, c_c
|
||||
|
||||
def test_progress_failure(self):
|
||||
from letsencrypt.client.errors import LetsEncryptAuthHandlerError
|
||||
challenges = acme_util.get_challenges()
|
||||
self.handler.add_chall_msg(
|
||||
"0",
|
||||
acme_util.get_chall_msg("0", "nonce0", challenges),
|
||||
"dummy_key")
|
||||
|
||||
# Don't do anything to satisfy challenges
|
||||
self.mock_sat_chall.side_effect = self._sat_failure
|
||||
|
||||
self.assertRaises(
|
||||
LetsEncryptAuthHandlerError, self.handler.get_authorizations)
|
||||
|
||||
# Check to make sure program didn't loop
|
||||
self.assertEqual(self.mock_sat_chall.call_count, 1)
|
||||
|
||||
def _sat_failure(self):
|
||||
dom = "0"
|
||||
self.handler.paths[dom] = gen_path(
|
||||
["dns", "recoveryToken"], self.handler.msgs[dom]["challenges"])
|
||||
dv_c, c_c = self.handler._challenge_factory(
|
||||
dom, self.handler.paths[dom])
|
||||
self.handler.dv_c[dom], self.handler.client_c[dom] = dv_c, c_c
|
||||
|
||||
def test_incremental_progress(self):
|
||||
challs = []
|
||||
challs.append(acme_util.get_challenges())
|
||||
challs.append(acme_util.get_dv_challenges())
|
||||
for i in range(2):
|
||||
dom = str(i)
|
||||
self.handler.add_chall_msg(
|
||||
dom,
|
||||
acme_util.get_chall_msg(dom, "nonce%d" % i, challs[i]),
|
||||
"dummy_key")
|
||||
|
||||
self.mock_sat_chall.side_effect = self._sat_incremental
|
||||
|
||||
self.handler.get_authorizations()
|
||||
|
||||
self._test_finished()
|
||||
self.assertEqual(self.mock_acme_auth.call_args_list,
|
||||
[mock.call("1"), mock.call("0")])
|
||||
|
||||
def _sat_incremental(self):
|
||||
from letsencrypt.client.errors import LetsEncryptAuthHandlerError
|
||||
|
||||
# Exact responses don't matter, just path/response match
|
||||
if self.iteration == 0:
|
||||
# Only solve one of "0" required challs
|
||||
self.handler.responses["0"][1] = "onecomplete"
|
||||
self.handler.responses["0"][3] = None
|
||||
self.handler.responses["1"] = ["null", "null", "goodresp"]
|
||||
self.handler.paths["0"] = [1, 3]
|
||||
self.handler.paths["1"] = [2]
|
||||
# This is probably overkill... but set it anyway
|
||||
dv_c, c_c = self.handler._challenge_factory("0", [1, 3])
|
||||
self.handler.dv_c["0"], self.handler.client_c["0"] = dv_c, c_c
|
||||
dv_c, c_c = self.handler._challenge_factory("1", [2])
|
||||
self.handler.dv_c["1"], self.handler.client_c["1"] = dv_c, c_c
|
||||
|
||||
self.iteration += 1
|
||||
|
||||
elif self.iteration == 1:
|
||||
# Quick check to make sure it was actually completed.
|
||||
self.assertEqual(
|
||||
self.mock_acme_auth.call_args_list, [mock.call("1")])
|
||||
self.handler.responses["0"][1] = "now_finish"
|
||||
self.handler.responses["0"][3] = "finally!"
|
||||
|
||||
else:
|
||||
raise LetsEncryptAuthHandlerError(
|
||||
"Failed incremental test: too many invocations")
|
||||
|
||||
def _test_finished(self):
|
||||
self.assertFalse(self.handler.msgs)
|
||||
self.assertFalse(self.handler.dv_c)
|
||||
self.assertFalse(self.handler.responses)
|
||||
self.assertFalse(self.handler.paths)
|
||||
self.assertFalse(self.handler.domains)
|
||||
|
||||
# pylint: disable=protected-access
|
||||
class PathSatisfiedTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.client.auth_handler import AuthHandler
|
||||
self.handler = AuthHandler(None, None, None)
|
||||
|
||||
def test_satisfied_true(self):
|
||||
dom = ["0", "1", "2", "3", "4"]
|
||||
self.handler.paths[dom[0]] = [1, 2]
|
||||
self.handler.responses[dom[0]] = ["null", "sat", "sat2", "null"]
|
||||
|
||||
self.handler.paths[dom[1]] = [0]
|
||||
self.handler.responses[dom[1]] = ["sat", None, None, "null"]
|
||||
|
||||
self.handler.paths[dom[2]] = [0]
|
||||
self.handler.responses[dom[2]] = ["sat"]
|
||||
|
||||
self.handler.paths[dom[3]] = []
|
||||
self.handler.responses[dom[3]] = []
|
||||
|
||||
self.handler.paths[dom[4]] = []
|
||||
self.handler.responses[dom[4]] = ["respond... sure"]
|
||||
|
||||
for i in range(5):
|
||||
self.assertTrue(self.handler._path_satisfied(dom[i]))
|
||||
|
||||
def test_not_satisfied(self):
|
||||
dom = ["0", "1", "2", "3", "4"]
|
||||
self.handler.paths[dom[0]] = [1, 2]
|
||||
self.handler.responses[dom[0]] = ["sat1", "null", "sat2", "null"]
|
||||
|
||||
self.handler.paths[dom[1]] = [0]
|
||||
self.handler.responses[dom[1]] = [None, "null", "null", "null"]
|
||||
|
||||
self.handler.paths[dom[2]] = [0]
|
||||
self.handler.responses[dom[2]] = [None]
|
||||
|
||||
self.handler.paths[dom[3]] = [0]
|
||||
self.handler.responses[dom[3]] = ["null"]
|
||||
|
||||
for i in range(4):
|
||||
self.assertFalse(self.handler._path_satisfied(dom[i]))
|
||||
|
||||
|
||||
def gen_auth_resp(chall_list):
|
||||
return ["%s%s" % (type(chall).__name__, chall.domain)
|
||||
for chall in chall_list]
|
||||
|
||||
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,80 @@
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
|
||||
class PerformTest(unittest.TestCase):
|
||||
"""Test client perform function."""
|
||||
def setUp(self):
|
||||
from letsencrypt.client.client_authenticator import ClientAuthenticator
|
||||
|
||||
self.auth = ClientAuthenticator("demo_server.org")
|
||||
self.auth.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.auth.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.auth.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 LetsEncryptClientAuthError
|
||||
|
||||
unexpected = DvsniChall("0", "rb64", "123", "invalid_key")
|
||||
|
||||
self.assertRaises(
|
||||
LetsEncryptClientAuthError, self.auth.perform, [unexpected])
|
||||
|
||||
|
||||
class CleanupTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.client.client_authenticator import ClientAuthenticator
|
||||
|
||||
self.auth = ClientAuthenticator("demo_server.org")
|
||||
self.mock_cleanup = mock.MagicMock(name="rec_token_cleanup")
|
||||
self.auth.rec_token.cleanup = self.mock_cleanup
|
||||
|
||||
def test_rec_token2(self):
|
||||
from letsencrypt.client.challenge_util import RecTokenChall
|
||||
token1 = RecTokenChall("0")
|
||||
token2 = RecTokenChall("1")
|
||||
|
||||
self.auth.cleanup([token1, token2])
|
||||
|
||||
self.assertEqual(self.mock_cleanup.call_args_list,
|
||||
[mock.call(token1), mock.call(token2)])
|
||||
|
||||
def test_unexpected(self):
|
||||
from letsencrypt.client.challenge_util import DvsniChall
|
||||
from letsencrypt.client.challenge_util import RecTokenChall
|
||||
from letsencrypt.client.errors import LetsEncryptClientAuthError
|
||||
|
||||
token = RecTokenChall("0")
|
||||
unexpected = DvsniChall("0", "rb64", "123", "dummy_key")
|
||||
|
||||
self.assertRaises(
|
||||
LetsEncryptClientAuthError, self.auth.cleanup, [token, unexpected])
|
||||
|
||||
|
||||
def gen_client_resp(chall):
|
||||
return "%s%s" % (type(chall).__name__, chall.domain)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -83,6 +83,9 @@ class UniqueFileTest(unittest.TestCase):
|
||||
self.root_path = tempfile.mkdtemp()
|
||||
self.default_name = os.path.join(self.root_path, 'foo.txt')
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.root_path, ignore_errors=True)
|
||||
|
||||
def _call(self, mode=0o600):
|
||||
from letsencrypt.client.le_util import unique_file
|
||||
return unique_file(self.default_name, mode)
|
||||
|
||||
@@ -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.assertIs(response, None)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -122,7 +122,7 @@ def main():
|
||||
acme = client.Client(server, domains, privkey, auth, installer)
|
||||
|
||||
# Validate the key and csr
|
||||
client.validate_key_csr(privkey, csr, domains)
|
||||
client.validate_key_csr(privkey, csr)
|
||||
|
||||
cert_file, chain_file = acme.obtain_certificate(csr)
|
||||
vhost = acme.deploy_certificate(privkey, cert_file, chain_file)
|
||||
@@ -150,8 +150,7 @@ def choose_names(installer):
|
||||
code, names = zope.component.getUtility(
|
||||
interfaces.IDisplay).filter_names(get_all_names(installer))
|
||||
if code == display.OK and names:
|
||||
# TODO: Allow multiple names once it is setup
|
||||
return [names[0]]
|
||||
return names
|
||||
else:
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user