IConfig, constants

This commit is contained in:
Jakub Warmuz
2015-01-31 11:31:29 +00:00
parent c59dc61cf0
commit 687541505b
23 changed files with 322 additions and 240 deletions
+5
View File
@@ -0,0 +1,5 @@
:mod:`letsencrypt.client.constants`
-----------------------------------
.. automodule:: letsencrypt.client.constants
:members:
+31 -53
View File
@@ -1,6 +1,13 @@
"""Config for Let's Encrypt.""" """Config for Let's Encrypt."""
import os.path import os.path
import zope.component
from letsencrypt.client import interfaces
zope.component.moduleProvides(interfaces.IConfig)
ACME_SERVER = "letsencrypt-demo.org:443" ACME_SERVER = "letsencrypt-demo.org:443"
"""CA hostname (and optionally :port). """CA hostname (and optionally :port).
@@ -10,9 +17,6 @@ If you create your own server... change this line
Note: the server certificate must be trusted in order to avoid Note: the server certificate must be trusted in order to avoid
further modifications to the client.""" further modifications to the client."""
# Directories
SERVER_ROOT = "/etc/apache2/"
"""Apache server root directory"""
CONFIG_DIR = "/etc/letsencrypt/" CONFIG_DIR = "/etc/letsencrypt/"
"""Configuration file directory for letsencrypt""" """Configuration file directory for letsencrypt"""
@@ -35,69 +39,43 @@ CERT_KEY_BACKUP = os.path.join(WORK_DIR, "keys-certs/")
REV_TOKENS_DIR = os.path.join(WORK_DIR, "revocation_tokens/") REV_TOKENS_DIR = os.path.join(WORK_DIR, "revocation_tokens/")
"""Directory where all revocation tokens are saved.""" """Directory where all revocation tokens are saved."""
KEY_DIR = os.path.join(SERVER_ROOT, "keys/") KEY_DIR = os.path.join(CONFIG_DIR, "keys/")
"""Where all keys should be stored""" """Keys storage."""
CERT_DIR = os.path.join(SERVER_ROOT, "certs/") CERT_DIR = os.path.join(CONFIG_DIR, "certs/")
"""Certificate storage""" """Certificate storage."""
# Files and extensions
OPTIONS_SSL_CONF = os.path.join(CONFIG_DIR, "options-ssl.conf")
"""Contains standard Apache SSL directives"""
LE_VHOST_EXT = "-le-ssl.conf" LE_VHOST_EXT = "-le-ssl.conf"
"""Let's Encrypt SSL vhost configuration extension""" """Let's Encrypt SSL vhost configuration extension."""
CERT_PATH = CERT_DIR + "cert-letsencrypt.pem" CERT_PATH = os.path.join(CERT_DIR, "cert-letsencrypt.pem")
"""Let's Encrypt cert file.""" """Let's Encrypt cert file."""
CHAIN_PATH = CERT_DIR + "chain-letsencrypt.pem" CHAIN_PATH = os.path.join(CERT_DIR, "chain-letsencrypt.pem")
"""Let's Encrypt chain file.""" """Let's Encrypt chain file."""
INVALID_EXT = ".acme.invalid"
"""Invalid Extension"""
# Challenge Sets
EXCLUSIVE_CHALLENGES = [frozenset(["dvsni", "simpleHttps"])]
"""Mutually Exclusive Challenges - only solve 1"""
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
"""Byte size of S"""
NONCE_SIZE = 16
"""byte size of Nonce"""
# Key Sizes
RSA_KEY_SIZE = 2048 RSA_KEY_SIZE = 2048
"""Key size""" """Key size"""
# Enhancements
ENHANCEMENTS = ["redirect", "http-header", "ocsp-stapling", "spdy"]
"""List of possible IInstaller enhancements.
List of expected options parameters:
redirect, None
http-header, TODO
ocsp-stapling, TODO
spdy, TODO
"""
# Apache Enhancement Arguments
REWRITE_HTTPS_ARGS = [
"^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"]
"""Rewrite rule arguments used for redirections to https vhost"""
# Apache Interaction
APACHE_CTL = "/usr/sbin/apache2ctl" APACHE_CTL = "/usr/sbin/apache2ctl"
"""Command used for configtest and version number.""" """Path to the ``apache2ctl`` binary, used for ``configtest`` and
retrieving Apache2 version number."""
APACHE2 = "/etc/init.d/apache2" APACHE_ENMOD = "apache"
"""Command used for reload and restart.""" """Path to the Apache ``a2enmod`` binary."""
APACHE_INIT_SCRIPT = "/etc/init.d/apache2"
"""Path to the Apache init script (used for server reload/restart)."""
APACHE_REWRITE_HTTPS_ARGS = [
"^.*$", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,R=permanent]"]
"""Apache rewrite rule arguments used for redirections to https vhost"""
APACHE_SERVER_ROOT = "/etc/apache2/"
"""Apache server root directory"""
APACHE_MOD_SSL_CONF = os.path.join(CONFIG_DIR, "options-ssl.conf")
"""Contains standard Apache SSL directives"""
+62 -55
View File
@@ -1,7 +1,6 @@
"""Apache Configuration based off of Augeas Configurator.""" """Apache Configuration based off of Augeas Configurator."""
import logging import logging
import os import os
import pkg_resources
import re import re
import shutil import shutil
import socket import socket
@@ -12,7 +11,7 @@ import zope.interface
from letsencrypt.client import augeas_configurator from letsencrypt.client import augeas_configurator
from letsencrypt.client import challenge_util from letsencrypt.client import challenge_util
from letsencrypt.client import CONFIG from letsencrypt.client import constants
from letsencrypt.client import errors from letsencrypt.client import errors
from letsencrypt.client import interfaces from letsencrypt.client import interfaces
from letsencrypt.client import le_util from letsencrypt.client import le_util
@@ -63,6 +62,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
The API of this class will change in the coming weeks as the exact The API of this class will change in the coming weeks as the exact
needs of clients are clarified with the new and developing protocol. needs of clients are clarified with the new and developing protocol.
:ivar config: Configuration.
:type config: :class:`~letsencrypt.client.interfaces.IConfig`
:ivar str server_root: Path to Apache root directory :ivar str server_root: Path to Apache root directory
:ivar dict location: Path to various files associated :ivar dict location: Path to various files associated
with the configuration with the configuration
@@ -75,27 +77,27 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
""" """
zope.interface.implements(interfaces.IAuthenticator, interfaces.IInstaller) zope.interface.implements(interfaces.IAuthenticator, interfaces.IInstaller)
def __init__(self, server_root=CONFIG.SERVER_ROOT, direc=None, def __init__(self, config, direc=None, version=None):
ssl_options=CONFIG.OPTIONS_SSL_CONF, version=None):
"""Initialize an Apache Configurator. """Initialize an Apache Configurator.
:param str server_root: the apache server root directory
:param dict direc: locations of various config directories :param dict direc: locations of various config directories
(used mostly for unittesting) (used mostly for unittesting)
:param str ssl_options: path of options-ssl.conf
(used mostly for unittesting)
:param tup version: version of Apache as a tuple (2, 4, 7) :param tup version: version of Apache as a tuple (2, 4, 7)
(used mostly for unittesting) (used mostly for unittesting)
""" """
if direc is None: self.config = config
direc = {"backup": CONFIG.BACKUP_DIR, server_root = self.config.APACHE_SERVER_ROOT
"temp": CONFIG.TEMP_CHECKPOINT_DIR, ssl_options = self.config.APACHE_MOD_SSL_CONF
"progress": CONFIG.IN_PROGRESS_DIR,
"config": CONFIG.CONFIG_DIR,
"work": CONFIG.WORK_DIR}
super(ApacheConfigurator, self).__init__(direc) if direc is None:
direc = {"backup": self.config.BACKUP_DIR,
"temp": self.config.TEMP_CHECKPOINT_DIR,
"progress": self.config.IN_PROGRESS_DIR,
"config": self.config.CONFIG_DIR,
"work": self.config.WORK_DIR}
super(ApacheConfigurator, self).__init__(config, direc)
self.direc = direc self.direc = direc
# Verify that all directories and files exist with proper permissions # Verify that all directories and files exist with proper permissions
@@ -382,9 +384,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
is appropriately listening on port 443. is appropriately listening on port 443.
""" """
if not mod_loaded("ssl_module"): if not mod_loaded("ssl_module", self.config.APACHE_CTL):
logging.info("Loading mod_ssl into Apache Server") logging.info("Loading mod_ssl into Apache Server")
enable_mod("ssl") enable_mod("ssl", self.config.APACHE_INIT_SCRIPT,
self.config.APACHE_ENMOD)
# Check for Listen 443 # Check for Listen 443
# Note: This could be made to also look for ip:443 combo # Note: This could be made to also look for ip:443 combo
@@ -427,7 +430,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""Makes an ssl_vhost version of a nonssl_vhost. """Makes an ssl_vhost version of a nonssl_vhost.
Duplicates vhost and adds default ssl options Duplicates vhost and adds default ssl options
New vhost will reside as (nonssl_vhost.path) + CONFIG.LE_VHOST_EXT New vhost will reside as (nonssl_vhost.path) + ``IConfig.LE_VHOST_EXT``
.. note:: This function saves the configuration .. note:: This function saves the configuration
:param nonssl_vhost: Valid VH that doesn't have SSLEngine on :param nonssl_vhost: Valid VH that doesn't have SSLEngine on
@@ -440,27 +444,24 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
avail_fp = nonssl_vhost.filep avail_fp = nonssl_vhost.filep
# Get filepath of new ssl_vhost # Get filepath of new ssl_vhost
if avail_fp.endswith(".conf"): if avail_fp.endswith(".conf"):
ssl_fp = avail_fp[:-(len(".conf"))] + CONFIG.LE_VHOST_EXT ssl_fp = avail_fp[:-(len(".conf"))] + self.config.LE_VHOST_EXT
else: else:
ssl_fp = avail_fp + CONFIG.LE_VHOST_EXT ssl_fp = avail_fp + self.config.LE_VHOST_EXT
# First register the creation so that it is properly removed if # First register the creation so that it is properly removed if
# configuration is rolled back # configuration is rolled back
self.reverter.register_file_creation(False, ssl_fp) self.reverter.register_file_creation(False, ssl_fp)
try: try:
orig_file = open(avail_fp, 'r') with open(avail_fp, 'r') as orig_file:
new_file = open(ssl_fp, 'w') with open(ssl_fp, 'w') as new_file:
new_file.write("<IfModule mod_ssl.c>\n") new_file.write("<IfModule mod_ssl.c>\n")
for line in orig_file: for line in orig_file:
new_file.write(line) new_file.write(line)
new_file.write("</IfModule>\n") new_file.write("</IfModule>\n")
except IOError: except IOError:
logging.fatal("Error writing/reading to file in make_vhost_ssl") logging.fatal("Error writing/reading to file in make_vhost_ssl")
sys.exit(49) sys.exit(49)
finally:
orig_file.close()
new_file.close()
self.aug.load() self.aug.load()
@@ -528,9 +529,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:param str domain: domain to enhance :param str domain: domain to enhance
:param str enhancement: enhancement type defined in :param str enhancement: enhancement type defined in
:class:`letsencrypt.client.CONFIG.ENHANCEMENTS` :const:`~letsencrypt.client.constants.ENHANCEMENTS`
:param options: options for the enhancement :param options: options for the enhancement
:type options: See :class:`letsencrypt.client.CONFIG.ENHANCEMENTS` See :const:`~letsencrypt.client.constants.ENHANCEMENTS`
documentation for appropriate parameter. documentation for appropriate parameter.
""" """
@@ -565,8 +566,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
:rtype: (bool, :class:`letsencrypt.client.apache.obj.VirtualHost`) :rtype: (bool, :class:`letsencrypt.client.apache.obj.VirtualHost`)
""" """
if not mod_loaded("rewrite_module"): if not mod_loaded("rewrite_module", self.config.APACHE_CTL):
enable_mod("rewrite") enable_mod("rewrite", self.config.APACHE_INIT_SCRIPT,
self.config.APACHE_ENMOD)
general_v = self._general_vhost(ssl_vhost) general_v = self._general_vhost(ssl_vhost)
if general_v is None: if general_v is None:
@@ -590,8 +592,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"in {}".format(general_v.filep)) "in {}".format(general_v.filep))
# Add directives to server # Add directives to server
self.parser.add_dir(general_v.path, "RewriteEngine", "On") self.parser.add_dir(general_v.path, "RewriteEngine", "On")
self.parser.add_dir( self.parser.add_dir(general_v.path, "RewriteRule",
general_v.path, "RewriteRule", CONFIG.REWRITE_HTTPS_ARGS) self.config.APACHE_REWRITE_HTTPS_ARGS)
self.save_notes += ('Redirecting host in %s to ssl vhost in %s\n' % self.save_notes += ('Redirecting host in %s to ssl vhost in %s\n' %
(general_v.filep, ssl_vhost.filep)) (general_v.filep, ssl_vhost.filep))
self.save() self.save()
@@ -630,9 +632,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if not rewrite_path: if not rewrite_path:
# "No existing redirection for virtualhost" # "No existing redirection for virtualhost"
return False, -1 return False, -1
if len(rewrite_path) == len(CONFIG.REWRITE_HTTPS_ARGS): if len(rewrite_path) == len(self.config.APACHE_REWRITE_HTTPS_ARGS):
for idx, match in enumerate(rewrite_path): for idx, match in enumerate(rewrite_path):
if self.aug.get(match) != CONFIG.REWRITE_HTTPS_ARGS[idx]: if (self.aug.get(match) !=
self.config.APACHE_REWRITE_HTTPS_ARGS[idx]):
# Not a letsencrypt https rewrite # Not a letsencrypt https rewrite
return True, 2 return True, 2
# Existing letsencrypt https rewrite rule is in place # Existing letsencrypt https rewrite rule is in place
@@ -681,7 +684,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"LogLevel warn\n" "LogLevel warn\n"
"</VirtualHost>\n" "</VirtualHost>\n"
% (servername, serveralias, % (servername, serveralias,
" ".join(CONFIG.REWRITE_HTTPS_ARGS))) " ".join(self.config.APACHE_REWRITE_HTTPS_ARGS)))
# Write out the file # Write out the file
# This is the default name # This is the default name
@@ -876,14 +879,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
return True return True
return False return False
def restart(self): # pylint: disable=no-self-use def restart(self):
"""Restarts apache server. """Restarts apache server.
:returns: Success :returns: Success
:rtype: bool :rtype: bool
""" """
return apache_restart() return apache_restart(self.config.APACHE_INIT_SCRIPT)
def config_test(self): # pylint: disable=no-self-use def config_test(self): # pylint: disable=no-self-use
"""Check the configuration of Apache for errors. """Check the configuration of Apache for errors.
@@ -894,7 +897,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
""" """
try: try:
proc = subprocess.Popen( proc = subprocess.Popen(
['sudo', '/usr/sbin/apache2ctl', 'configtest'], ['sudo', self.config.APACHE_CTL, 'configtest'], # TODO: sudo?
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE) stderr=subprocess.PIPE)
stdout, stderr = proc.communicate() stdout, stderr = proc.communicate()
@@ -925,13 +928,13 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
""" """
try: try:
proc = subprocess.Popen( proc = subprocess.Popen(
[CONFIG.APACHE_CTL, '-v'], [self.config.APACHE_CTL, '-v'],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE) stderr=subprocess.PIPE)
text = proc.communicate()[0] text = proc.communicate()[0]
except (OSError, ValueError): except (OSError, ValueError):
raise errors.LetsEncryptConfiguratorError( raise errors.LetsEncryptConfiguratorError(
"Unable to run %s -v" % CONFIG.APACHE_CTL) "Unable to run %s -v" % self.config.APACHE_CTL)
regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE) regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE)
matches = regex.findall(text) matches = regex.findall(text)
@@ -1012,47 +1015,51 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.restart() self.restart()
def enable_mod(mod_name): def enable_mod(mod_name, apache_init_script, apache_enmod):
"""Enables module in Apache. """Enables module in Apache.
Both enables and restarts Apache so module is active. Both enables and restarts Apache so module is active.
:param str mod_name: Name of the module to enable :param str mod_name: Name of the module to enable.
:param str apache_init_script: Path to the Apache init script.
:param str apache_enmod: Path to the Apache a2enmod script.
""" """
try: try:
# Use check_output so the command will finish before reloading # Use check_output so the command will finish before reloading
# TODO: a2enmod is debian specific... # TODO: a2enmod is debian specific...
subprocess.check_call(["sudo", "a2enmod", mod_name], subprocess.check_call(["sudo", apache_enmod, mod_name], # TODO: sudo?
stdout=open("/dev/null", 'w'), stdout=open("/dev/null", 'w'),
stderr=open("/dev/null", 'w')) stderr=open("/dev/null", 'w'))
apache_restart() apache_restart(apache_init_script)
except (OSError, subprocess.CalledProcessError) as err: except (OSError, subprocess.CalledProcessError) as err:
logging.error("Error enabling mod_%s", mod_name) logging.error("Error enabling mod_%s", mod_name)
logging.error("Exception: %s", err) logging.error("Exception: %s", err)
sys.exit(1) sys.exit(1)
def mod_loaded(module): def mod_loaded(module, apache_ctl):
"""Checks to see if mod_ssl is loaded """Checks to see if mod_ssl is loaded
Uses CONFIG.APACHE_CTL to get loaded module list. This also effectively Uses ``apache_ctl`` to get loaded module list. This also effectively
serves as a config_test. serves as a config_test.
:param str apache_ctl: Path to apache2ctl binary.
:returns: If ssl_module is included and active in Apache :returns: If ssl_module is included and active in Apache
:rtype: bool :rtype: bool
""" """
try: try:
proc = subprocess.Popen( proc = subprocess.Popen(
[CONFIG.APACHE_CTL, '-M'], [apache_ctl, '-M'],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE) stderr=subprocess.PIPE)
stdout, stderr = proc.communicate() stdout, stderr = proc.communicate()
except (OSError, ValueError): except (OSError, ValueError):
logging.error( logging.error(
"Error accessing %s for loaded modules!", CONFIG.APACHE_CTL) "Error accessing %s for loaded modules!", apache_ctl)
raise errors.LetsEncryptConfiguratorError( raise errors.LetsEncryptConfiguratorError(
"Error accessing loaded modules") "Error accessing loaded modules")
# Small errors that do not impede # Small errors that do not impede
@@ -1067,9 +1074,11 @@ def mod_loaded(module):
return False return False
def apache_restart(): def apache_restart(apache_init_script):
"""Restarts the Apache Server. """Restarts the Apache Server.
:param str apache_init_script: Path to the Apache init script.
.. todo:: Try to use reload instead. (This caused timing problems before) .. todo:: Try to use reload instead. (This caused timing problems before)
.. todo:: On failure, this should be a recovery_routine call with another .. todo:: On failure, this should be a recovery_routine call with another
@@ -1081,7 +1090,7 @@ def apache_restart():
""" """
try: try:
proc = subprocess.Popen([CONFIG.APACHE2, 'restart'], proc = subprocess.Popen([apache_init_script, 'restart'],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE) stderr=subprocess.PIPE)
stdout, stderr = proc.communicate() stdout, stderr = proc.communicate()
@@ -1138,6 +1147,4 @@ def temp_install(options_ssl):
# Check to make sure options-ssl.conf is installed # Check to make sure options-ssl.conf is installed
if not os.path.isfile(options_ssl): if not os.path.isfile(options_ssl):
dist_conf = pkg_resources.resource_filename( shutil.copyfile(constants.APACHE_MOD_SSL_CONF, options_ssl)
__name__, os.path.basename(options_ssl))
shutil.copyfile(dist_conf, options_ssl)
+4 -3
View File
@@ -3,7 +3,7 @@ import logging
import os import os
from letsencrypt.client import challenge_util from letsencrypt.client import challenge_util
from letsencrypt.client import CONFIG from letsencrypt.client import constants
from letsencrypt.client.apache import parser from letsencrypt.client.apache import parser
@@ -12,7 +12,8 @@ class ApacheDvsni(object):
"""Class performs DVSNI challenges within the Apache configurator. """Class performs DVSNI challenges within the Apache configurator.
:ivar config: ApacheConfigurator object :ivar config: ApacheConfigurator object
:type config: :class:`letsencrypt.client.apache.configurator` :type config:
:class:`letsencrypt.client.apache.configurator.ApacheConfigurator`
:ivar dvsni_chall: Data required for challenges. :ivar dvsni_chall: Data required for challenges.
where DvsniChall tuples have the following fields where DvsniChall tuples have the following fields
@@ -165,7 +166,7 @@ class ApacheDvsni(object):
""" """
ips = " ".join(str(i) for i in ip_addrs) ips = " ".join(str(i) for i in ip_addrs)
return ("<VirtualHost " + ips + ">\n" return ("<VirtualHost " + ips + ">\n"
"ServerName " + nonce + CONFIG.INVALID_EXT + "\n" "ServerName " + nonce + constants.DVSNI_DOMAIN_SUFFIX + "\n"
"UseCanonicalName on\n" "UseCanonicalName on\n"
"SSLStrictSNIVHostCheck on\n" "SSLStrictSNIVHostCheck on\n"
"\n" "\n"
+8 -7
View File
@@ -3,7 +3,6 @@ import logging
import augeas import augeas
from letsencrypt.client import CONFIG
from letsencrypt.client import reverter from letsencrypt.client import reverter
@@ -20,18 +19,20 @@ class AugeasConfigurator(object):
""" """
def __init__(self, direc=None): def __init__(self, config, direc=None):
"""Initialize Augeas Configurator. """Initialize Augeas Configurator.
:param config: Configuration.
:type config: :class:`~letsencrypt.client.interfaces.IConfig`
:param dict direc: location of save directories :param dict direc: location of save directories
(used mostly for testing) (used mostly for testing)
""" """
if not direc: if not direc:
direc = {"backup": CONFIG.BACKUP_DIR, direc = {"backup": config.BACKUP_DIR,
"temp": CONFIG.TEMP_CHECKPOINT_DIR, "temp": config.TEMP_CHECKPOINT_DIR,
"progress": CONFIG.IN_PROGRESS_DIR} "progress": config.IN_PROGRESS_DIR}
# Set Augeas flags to not save backup (we do it ourselves) # Set Augeas flags to not save backup (we do it ourselves)
# Set Augeas to not load anything by default # Set Augeas to not load anything by default
@@ -43,7 +44,7 @@ class AugeasConfigurator(object):
# This needs to occur before VirtualHost objects are setup... # This needs to occur before VirtualHost objects are setup...
# because this will change the underlying configuration and potential # because this will change the underlying configuration and potential
# vhosts # vhosts
self.reverter = reverter.Reverter(direc) self.reverter = reverter.Reverter(config, direc)
self.reverter.recovery_routine() self.reverter.recovery_routine()
def check_parsing_errors(self, lens): def check_parsing_errors(self, lens):
+8 -6
View File
@@ -3,18 +3,20 @@ import logging
import sys import sys
from letsencrypt.client import acme from letsencrypt.client import acme
from letsencrypt.client import CONFIG
from letsencrypt.client import challenge_util from letsencrypt.client import challenge_util
from letsencrypt.client import constants
from letsencrypt.client import errors from letsencrypt.client import errors
class AuthHandler(object): # pylint: disable=too-many-instance-attributes class AuthHandler(object): # pylint: disable=too-many-instance-attributes
"""ACME Authorization Handler for a client. """ACME Authorization Handler for a client.
:ivar dv_auth: Authenticator capable of solving CONFIG.DV_CHALLENGES :ivar dv_auth: Authenticator capable of solving
:const:`~letsencrypt.client.constants.DV_CHALLENGES`
:type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator` :type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
:ivar client_auth: Authenticator capable of solving CONFIG.CLIENT_CHALLENGES :ivar client_auth: Authenticator capable of solving
:const:`~letsencrypt.client_auth.constants.CLIENT_CHALLENGES`
:type client_auth: :class:`letsencrypt.client.interfaces.IAuthenticator` :type client_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
:ivar network: Network object for sending and receiving authorization :ivar network: Network object for sending and receiving authorization
@@ -238,12 +240,12 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
chall = challenges[index] chall = challenges[index]
# Authenticator Challenges # Authenticator Challenges
if chall["type"] in CONFIG.DV_CHALLENGES: if chall["type"] in constants.DV_CHALLENGES:
dv_chall.append(challenge_util.IndexedChall( dv_chall.append(challenge_util.IndexedChall(
self._construct_dv_chall(chall, domain), index)) self._construct_dv_chall(chall, domain), index))
# Client Challenges # Client Challenges
elif chall["type"] in CONFIG.CLIENT_CHALLENGES: elif chall["type"] in constants.CLIENT_CHALLENGES:
client_chall.append(challenge_util.IndexedChall( client_chall.append(challenge_util.IndexedChall(
self._construct_client_chall(chall, domain), index)) self._construct_client_chall(chall, domain), index))
@@ -430,7 +432,7 @@ def _find_dumb_path(challenges, preferences):
def is_preferred(offered_challenge_type, path): def is_preferred(offered_challenge_type, path):
"""Return whether or not the challenge is preferred in path.""" """Return whether or not the challenge is preferred in path."""
for _, challenge_type in path: for _, challenge_type in path:
for mutually_exclusive in CONFIG.EXCLUSIVE_CHALLENGES: for mutually_exclusive in constants.EXCLUSIVE_CHALLENGES:
# Second part is in case we eventually allow multiple names # Second part is in case we eventually allow multiple names
# to be challenges at the same time # to be challenges at the same time
if (challenge_type in mutually_exclusive and if (challenge_type in mutually_exclusive and
+5 -5
View File
@@ -4,7 +4,7 @@ import hashlib
from Crypto import Random from Crypto import Random
from letsencrypt.client import CONFIG from letsencrypt.client import constants
from letsencrypt.client import crypto_util from letsencrypt.client import crypto_util
from letsencrypt.client import le_util from letsencrypt.client import le_util
@@ -44,14 +44,14 @@ def dvsni_gen_cert(name, r_b64, nonce, key):
""" """
# Generate S # Generate S
dvsni_s = Random.get_random_bytes(CONFIG.S_SIZE) dvsni_s = Random.get_random_bytes(constants.S_SIZE)
dvsni_r = le_util.jose_b64decode(r_b64) dvsni_r = le_util.jose_b64decode(r_b64)
# Generate extension # Generate extension
ext = _dvsni_gen_ext(dvsni_r, dvsni_s) ext = _dvsni_gen_ext(dvsni_r, dvsni_s)
cert_pem = crypto_util.make_ss_cert( cert_pem = crypto_util.make_ss_cert(
key.pem, [nonce + CONFIG.INVALID_EXT, name, ext]) key.pem, [nonce + constants.DVSNI_DOMAIN_SUFFIX, name, ext])
return cert_pem, le_util.jose_b64encode(dvsni_s) return cert_pem, le_util.jose_b64encode(dvsni_s)
@@ -62,7 +62,7 @@ def _dvsni_gen_ext(dvsni_r, dvsni_s):
:param bytearray dvsni_r: DVSNI r value :param bytearray dvsni_r: DVSNI r value
:param bytearray dvsni_s: DVSNI s value :param bytearray dvsni_s: DVSNI s value
:returns: z + CONFIG.INVALID_EXT :returns: z + :const:`~letsencrypt.client.constants.DVSNI_DOMAIN_SUFFIX`
:rtype: str :rtype: str
""" """
@@ -70,4 +70,4 @@ def _dvsni_gen_ext(dvsni_r, dvsni_s):
z_base.update(dvsni_r) z_base.update(dvsni_r)
z_base.update(dvsni_s) z_base.update(dvsni_s)
return z_base.hexdigest() + CONFIG.INVALID_EXT return z_base.hexdigest() + constants.DVSNI_DOMAIN_SUFFIX
+74 -43
View File
@@ -12,7 +12,6 @@ import zope.component
from letsencrypt.client import acme from letsencrypt.client import acme
from letsencrypt.client import auth_handler from letsencrypt.client import auth_handler
from letsencrypt.client import client_authenticator from letsencrypt.client import client_authenticator
from letsencrypt.client import CONFIG
from letsencrypt.client import crypto_util from letsencrypt.client import crypto_util
from letsencrypt.client import errors from letsencrypt.client import errors
from letsencrypt.client import interfaces from letsencrypt.client import interfaces
@@ -40,6 +39,9 @@ class Client(object):
:ivar installer: Object supporting the IInstaller interface. :ivar installer: Object supporting the IInstaller interface.
:type installer: :class:`letsencrypt.client.interfaces.IInstaller` :type installer: :class:`letsencrypt.client.interfaces.IInstaller`
:ivar config: Configuration.
:type config: :class:`~letsencrypt.client.interfaces.IConfig`
""" """
zope.interface.implements(interfaces.IAuthenticator) zope.interface.implements(interfaces.IAuthenticator)
@@ -47,12 +49,12 @@ class Client(object):
# Note: form is the type of data, "pem" or "der" # Note: form is the type of data, "pem" or "der"
CSR = collections.namedtuple("CSR", "file data form") CSR = collections.namedtuple("CSR", "file data form")
def __init__(self, server, authkey, dv_auth, installer): def __init__(self, server, authkey, dv_auth, installer, config):
"""Initialize a client. """Initialize a client.
:param str server: CA server to contact :param str server: CA server to contact
:param dv_auth: IAuthenticator Interface that can solve the :param dv_auth: IAuthenticator that can solve the
CONFIG.DV_CHALLENGES :const:`letsencrypt.client.constants.DV_CHALLENGES`
:type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator` :type dv_auth: :class:`letsencrypt.client.interfaces.IAuthenticator`
""" """
@@ -61,30 +63,32 @@ class Client(object):
self.installer = installer self.installer = installer
self.config = config
if dv_auth is not None: if dv_auth is not None:
client_auth = client_authenticator.ClientAuthenticator(server) client_auth = client_authenticator.ClientAuthenticator(
server, config)
self.auth_handler = auth_handler.AuthHandler( self.auth_handler = auth_handler.AuthHandler(
dv_auth, client_auth, self.network) dv_auth, client_auth, self.network)
else: else:
self.auth_handler = None self.auth_handler = None
def obtain_certificate(self, domains, csr=None, def obtain_certificate(self, domains, csr=None):
cert_path=CONFIG.CERT_PATH,
chain_path=CONFIG.CHAIN_PATH):
"""Obtains a certificate from the ACME server. """Obtains a certificate from the ACME server.
:param str domains: list of domains to get a certificate :param str domains: list of domains to get a certificate
:param csr: CSR must contain requested domains, the key used to generate :param csr: CSR must contain requested domains, the key used to generate
this CSR can be different than self.authkey this CSR can be different than self.authkey
:type csr: :class:`CSR` :type csr: :class:`CSR`
:param str cert_path: Full desired path to end certificate.
:param str chain_path: Full desired path to end chain file.
:returns: cert_file, chain_file (paths to respective files) :returns: cert_file, chain_file (paths to respective files)
:rtype: `tuple` of `str` :rtype: `tuple` of `str`
""" """
cert_path = self.config.CERT_PATH
chain_path = self.config.CHAIN_PATH
if self.auth_handler is None: if self.auth_handler is None:
logging.warning("Unable to obtain a certificate, because client " logging.warning("Unable to obtain a certificate, because client "
"does not have a valid auth handler.") "does not have a valid auth handler.")
@@ -99,7 +103,7 @@ class Client(object):
# Create CSR from names # Create CSR from names
if csr is None: if csr is None:
csr = init_csr(self.authkey, domains) csr = init_csr(self.authkey, domains, self.config.CERT_DIR)
# Retrieve certificate # Retrieve certificate
certificate_dict = self.acme_certificate(csr.data) certificate_dict = self.acme_certificate(csr.data)
@@ -241,8 +245,8 @@ class Client(object):
:rtype: bool :rtype: bool
""" """
list_file = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST") list_file = os.path.join(self.config.CERT_KEY_BACKUP, "LIST")
le_util.make_or_verify_dir(CONFIG.CERT_KEY_BACKUP, 0o700) le_util.make_or_verify_dir(self.config.CERT_KEY_BACKUP, 0o700)
idx = 0 idx = 0
if encrypt: if encrypt:
@@ -267,11 +271,11 @@ class Client(object):
shutil.copy2(self.authkey.file, shutil.copy2(self.authkey.file,
os.path.join( os.path.join(
CONFIG.CERT_KEY_BACKUP, self.config.CERT_KEY_BACKUP,
os.path.basename(self.authkey.file) + "_" + str(idx))) os.path.basename(self.authkey.file) + "_" + str(idx)))
shutil.copy2(cert_file, shutil.copy2(cert_file,
os.path.join( os.path.join(
CONFIG.CERT_KEY_BACKUP, self.config.CERT_KEY_BACKUP,
os.path.basename(cert_file) + "_" + str(idx))) os.path.basename(cert_file) + "_" + str(idx)))
return True return True
@@ -339,7 +343,7 @@ def validate_key_csr(privkey, csr=None):
"The key and CSR do not match") "The key and CSR do not match")
def init_key(key_size): def init_key(key_size, key_dir):
"""Initializes privkey. """Initializes privkey.
Inits key and CSR using provided files or generating new files Inits key and CSR using provided files or generating new files
@@ -347,19 +351,19 @@ def init_key(key_size):
filesystem. The CSR is placed into DER format to allow filesystem. The CSR is placed into DER format to allow
the namedtuple to easily work with the protocol. the namedtuple to easily work with the protocol.
:param str key_dir: Key save directory.
""" """
try: try:
key_pem = crypto_util.make_key(key_size) key_pem = crypto_util.make_key(key_size)
except ValueError as err: except ValueError as err:
logging.fatal(str(err)) logging.fatal(str(err))
logging.info("Note: The default RSA key size is %d bits.",
CONFIG.RSA_KEY_SIZE)
sys.exit(1) sys.exit(1)
# Save file # Save file
le_util.make_or_verify_dir(CONFIG.KEY_DIR, 0o700) le_util.make_or_verify_dir(key_dir, 0o700)
key_f, key_filename = le_util.unique_file( key_f, key_filename = le_util.unique_file(
os.path.join(CONFIG.KEY_DIR, "key-letsencrypt.pem"), 0o600) os.path.join(key_dir, "key-letsencrypt.pem"), 0o600)
key_f.write(key_pem) key_f.write(key_pem)
key_f.close() key_f.close()
@@ -368,15 +372,18 @@ def init_key(key_size):
return Client.Key(key_filename, key_pem) return Client.Key(key_filename, key_pem)
def init_csr(privkey, names): def init_csr(privkey, names, cert_dir):
"""Initialize a CSR with the given private key.""" """Initialize a CSR with the given private key.
:param str cert_dir: Certificate save directory.
"""
csr_pem, csr_der = crypto_util.make_csr(privkey.pem, names) csr_pem, csr_der = crypto_util.make_csr(privkey.pem, names)
# Save CSR # Save CSR
le_util.make_or_verify_dir(CONFIG.CERT_DIR, 0o755) le_util.make_or_verify_dir(cert_dir, 0o755)
csr_f, csr_filename = le_util.unique_file( csr_f, csr_filename = le_util.unique_file(
os.path.join(CONFIG.CERT_DIR, "csr-letsencrypt.pem"), 0o644) os.path.join(cert_dir, "csr-letsencrypt.pem"), 0o644)
csr_f.write(csr_pem) csr_f.write(csr_pem)
csr_f.close() csr_f.close()
@@ -393,23 +400,33 @@ def csr_pem_to_der(csr):
# This should be controlled by commandline parameters # This should be controlled by commandline parameters
def determine_authenticator(): def determine_authenticator(config):
"""Returns a valid IAuthenticator.""" """Returns a valid IAuthenticator.
:param config: Configuration.
:type config: :class:`letsencrypt.client.interfaces.IConfig`
"""
try: try:
return configurator.ApacheConfigurator() return configurator.ApacheConfigurator(config)
except errors.LetsEncryptNoInstallationError: except errors.LetsEncryptNoInstallationError:
logging.info("Unable to determine a way to authenticate the server") logging.info("Unable to determine a way to authenticate the server")
def determine_installer(): def determine_installer(config):
"""Returns a valid installer if one exists.""" """Returns a valid installer if one exists.
:param config: Configuration.
:type config: :class:`letsencrypt.client.interfaces.IConfig`
"""
try: try:
return configurator.ApacheConfigurator() return configurator.ApacheConfigurator(config)
except errors.LetsEncryptNoInstallationError: except errors.LetsEncryptNoInstallationError:
logging.info("Unable to find a way to install the certificate.") logging.info("Unable to find a way to install the certificate.")
def rollback(checkpoints): def rollback(checkpoints, config):
"""Revert configuration the specified number of checkpoints. """Revert configuration the specified number of checkpoints.
.. note:: If another installer uses something other than the reverter class .. note:: If another installer uses something other than the reverter class
@@ -426,12 +443,15 @@ def rollback(checkpoints):
:param int checkpoints: Number of checkpoints to revert. :param int checkpoints: Number of checkpoints to revert.
:param config: Configuration.
:type config: :class:`letsencrypt.client.interfaces.IConfig`
""" """
# Misconfigurations are only a slight problems... allow the user to rollback # Misconfigurations are only a slight problems... allow the user to rollback
try: try:
installer = determine_installer() installer = determine_installer(config)
except errors.LetsEncryptMisconfigurationError: except errors.LetsEncryptMisconfigurationError:
_misconfigured_rollback(checkpoints) _misconfigured_rollback(checkpoints, config)
return return
# No Errors occurred during init... proceed normally # No Errors occurred during init... proceed normally
@@ -442,8 +462,13 @@ def rollback(checkpoints):
installer.restart() installer.restart()
def _misconfigured_rollback(checkpoints): def _misconfigured_rollback(checkpoints, config):
"""Handles the case where the Installer is misconfigured.""" """Handles the case where the Installer is misconfigured.
:param config: Configuration.
:type config: :class:`letsencrypt.client.interfaces.IConfig`
"""
yes = zope.component.getUtility(interfaces.IDisplay).generic_yesno( yes = zope.component.getUtility(interfaces.IDisplay).generic_yesno(
"Oh, no! The web server is currently misconfigured.{0}{0}" "Oh, no! The web server is currently misconfigured.{0}{0}"
"Would you still like to rollback the " "Would you still like to rollback the "
@@ -457,13 +482,13 @@ def _misconfigured_rollback(checkpoints):
# recovery routine has probably already been run by installer # recovery routine has probably already been run by installer
# in the__init__ attempt, run it again for safety... it shouldn't hurt # in the__init__ attempt, run it again for safety... it shouldn't hurt
# Also... not sure how future installers will handle recovery. # Also... not sure how future installers will handle recovery.
rev = reverter.Reverter() rev = reverter.Reverter(config)
rev.recovery_routine() rev.recovery_routine()
rev.rollback_checkpoints(checkpoints) rev.rollback_checkpoints(checkpoints)
# We should try to restart the server # We should try to restart the server
try: try:
installer = determine_installer() installer = determine_installer(config)
installer.restart() installer.restart()
logging.info("Hooray! Rollback solved the misconfiguration!") logging.info("Hooray! Rollback solved the misconfiguration!")
logging.info("Your web server is back up and running.") logging.info("Your web server is back up and running.")
@@ -472,16 +497,19 @@ def _misconfigured_rollback(checkpoints):
"Rollback was unable to solve the misconfiguration issues") "Rollback was unable to solve the misconfiguration issues")
def revoke(server): def revoke(server, config):
"""Revoke certificates. """Revoke certificates.
:param str server: ACME server the client wishes to revoke certificates from :param str server: ACME server the client wishes to revoke certificates from
:param config: Configuration.
:type config: :class:`letsencrypt.client.interfaces.IConfig`
""" """
# Misconfigurations don't really matter. Determine installer better choose # Misconfigurations don't really matter. Determine installer better choose
# correctly though. # correctly though.
try: try:
installer = determine_installer() installer = determine_installer(config)
except errors.LetsEncryptMisconfigurationError: except errors.LetsEncryptMisconfigurationError:
zope.component.getUtility(interfaces.IDisplay).generic_notification( zope.component.getUtility(interfaces.IDisplay).generic_notification(
"The web server is currently misconfigured. Some " "The web server is currently misconfigured. Some "
@@ -497,16 +525,19 @@ def revoke(server):
"revocation without a valid installer. This feature should come " "revocation without a valid installer. This feature should come "
"soon.") "soon.")
return return
revoc = revoker.Revoker(server, installer) revoc = revoker.Revoker(server, installer, config)
revoc.list_certs_keys() revoc.list_certs_keys()
def view_config_changes(): def view_config_changes(config):
"""View checkpoints and associated configuration changes. """View checkpoints and associated configuration changes.
.. note:: This assumes that the installation is using a Reverter object. .. note:: This assumes that the installation is using a Reverter object.
:param config: Configuration.
:type config: :class:`letsencrypt.client.interfaces.IConfig`
""" """
rev = reverter.Reverter() rev = reverter.Reverter(config)
rev.recovery_routine() rev.recovery_routine()
rev.view_config_changes() rev.view_config_changes()
+8 -3
View File
@@ -8,7 +8,8 @@ from letsencrypt.client import recovery_token
class ClientAuthenticator(object): class ClientAuthenticator(object):
"""IAuthenticator for CONFIG.CLIENT_CHALLENGES. """IAuthenticator for
:const:`~letsencrypt.client.constants.CLIENT_CHALLENGES`.
:ivar rec_token: Performs "recoveryToken" challenges :ivar rec_token: Performs "recoveryToken" challenges
:type rec_token: :class:`letsencrypt.client.recovery_token.RecoveryToken` :type rec_token: :class:`letsencrypt.client.recovery_token.RecoveryToken`
@@ -17,13 +18,17 @@ class ClientAuthenticator(object):
zope.interface.implements(interfaces.IAuthenticator) zope.interface.implements(interfaces.IAuthenticator)
# This will have an installer soon for get_key/cert purposes # This will have an installer soon for get_key/cert purposes
def __init__(self, server): def __init__(self, server, config):
"""Initialize Client Authenticator. """Initialize Client Authenticator.
:param str server: ACME CA Server :param str server: ACME CA Server
:param config: Configuration.
:type config: :class:`letsencrypt.client.interfaces.IConfig`
""" """
self.rec_token = recovery_token.RecoveryToken(server) self.rec_token = recovery_token.RecoveryToken(
server, config.REV_TOKEN_DIRS)
def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use
"""Return list of challenge preferences.""" """Return list of challenge preferences."""
+41
View File
@@ -0,0 +1,41 @@
"""Let's Encrypt constants."""
import pkg_resources
S_SIZE = 32
"""Size (in bytes) of secret base64-encoded octet string "s" used in
challanges."""
NONCE_SIZE = 16
"""Size of nonce used in JWS objects (in bytes)."""
EXCLUSIVE_CHALLENGES = [frozenset(["dvsni", "simpleHttps"])]
"""Mutually exclusive challenges."""
DV_CHALLENGES = frozenset(["dvsni", "simpleHttps", "dns"])
"""Challenges that must be solved by a
:class:`letsencrypt.client.interfaces.IAuthenticator` object."""
CLIENT_CHALLENGES = frozenset(
["recoveryToken", "recoveryContact", "proofOfPossession"])
"""Challenges that are handled by the Let's Encrypt client."""
ENHANCEMENTS = ["redirect", "http-header", "ocsp-stapling", "spdy"]
"""List of possible :class:`letsencrypt.client.interfaces.IInstaller`
enhancements.
List of expected options parameters:
- redirect: None
- http-header: TODO
- ocsp-stapling: TODO
- spdy: TODO
"""
APACHE_MOD_SSL_CONF = pkg_resources.resource_filename(
'letsencrypt.client.apache', 'options-ssl.conf')
"""Path to the Apache mod_ssl config file found in the Let's Encrypt
distribution."""
DVSNI_DOMAIN_SUFFIX = ".acme.invalid"
"""Suffix appended to domains in DVSNI validation."""
+6 -11
View File
@@ -10,11 +10,11 @@ import Crypto.Signature.PKCS1_v1_5
import M2Crypto import M2Crypto
from letsencrypt.client import CONFIG from letsencrypt.client import constants
from letsencrypt.client import le_util from letsencrypt.client import le_util
def create_sig(msg, key_str, nonce=None, nonce_len=CONFIG.NONCE_SIZE): def create_sig(msg, key_str, nonce=None):
"""Create signature with nonce prepended to the message. """Create signature with nonce prepended to the message.
.. todo:: Change this over to M2Crypto... PKey .. todo:: Change this over to M2Crypto... PKey
@@ -24,22 +24,17 @@ def create_sig(msg, key_str, nonce=None, nonce_len=CONFIG.NONCE_SIZE):
:param str key_str: Key in string form. Accepted formats :param str key_str: Key in string form. Accepted formats
are the same as for `Crypto.PublicKey.RSA.importKey`. are the same as for `Crypto.PublicKey.RSA.importKey`.
:param str msg: Message to be signed :param str msg: Message to be signed
:param str nonce: Nonce to be used (required size
:param nonce: Nonce to be used. If None, nonce of `nonce_len` size
will be randomly generated.
:type nonce: str or None
:param int nonce_len: Size of the automatically generated nonce.
:returns: Signature. :returns: Signature.
:rtype: dict :rtype: dict
""" """
msg = str(msg)
key = Crypto.PublicKey.RSA.importKey(key_str) key = Crypto.PublicKey.RSA.importKey(key_str)
nonce = Random.get_random_bytes(nonce_len) if nonce is None else nonce if nonce is None:
nonce = Random.get_random_bytes(constants.NONCE_SIZE)
assert len(nonce) == constants.NONCE_SIZE
msg_with_nonce = nonce + msg msg_with_nonce = nonce + msg
hashed = Crypto.Hash.SHA256.new(msg_with_nonce) hashed = Crypto.Hash.SHA256.new(msg_with_nonce)
+5
View File
@@ -2,6 +2,7 @@
import zope.interface import zope.interface
# pylint: disable=no-self-argument,no-method-argument,no-init,inherit-non-class # pylint: disable=no-self-argument,no-method-argument,no-init,inherit-non-class
# pylint: disable=too-few-public-methods
class IAuthenticator(zope.interface.Interface): class IAuthenticator(zope.interface.Interface):
@@ -58,6 +59,10 @@ class IChallenge(zope.interface.Interface):
"""Cleanup.""" """Cleanup."""
class IConfig(zope.interface.Interface):
"""Marker interface for Let's Encrypt config."""
class IInstaller(zope.interface.Interface): class IInstaller(zope.interface.Interface):
"""Generic Let's Encrypt Installer Interface. """Generic Let's Encrypt Installer Interface.
+1 -3
View File
@@ -3,9 +3,7 @@ import errno
import os import os
import zope.component import zope.component
# import zope.interface
from letsencrypt.client import CONFIG
from letsencrypt.client import le_util from letsencrypt.client import le_util
from letsencrypt.client import interfaces from letsencrypt.client import interfaces
@@ -16,7 +14,7 @@ class RecoveryToken(object):
Based on draft-barnes-acme, section 6.4. Based on draft-barnes-acme, section 6.4.
""" """
def __init__(self, server, direc=CONFIG.REV_TOKENS_DIR): def __init__(self, server, direc):
self.token_dir = os.path.join(direc, server) self.token_dir = os.path.join(direc, server)
def perform(self, chall): def perform(self, chall):
+13 -7
View File
@@ -6,7 +6,6 @@ import time
import zope.component import zope.component
from letsencrypt.client import CONFIG
from letsencrypt.client import display from letsencrypt.client import display
from letsencrypt.client import errors from letsencrypt.client import errors
from letsencrypt.client import interfaces from letsencrypt.client import interfaces
@@ -14,12 +13,19 @@ from letsencrypt.client import le_util
class Reverter(object): class Reverter(object):
"""Reverter Class - save and revert configuration checkpoints""" """Reverter Class - save and revert configuration checkpoints."""
def __init__(self, direc=None):
if not direc: def __init__(self, config, direc=None):
direc = {'backup': CONFIG.BACKUP_DIR, """Initialize Reverter.
'temp': CONFIG.TEMP_CHECKPOINT_DIR,
'progress': CONFIG.IN_PROGRESS_DIR} :param config: Configuration.
:type config: :class:`letsencrypt.client.interfaces.IConfig`
"""
if direc is None:
direc = {'backup': config.BACKUP_DIR,
'temp': config.TEMP_CHECKPOINT_DIR,
'progress': config.IN_PROGRESS_DIR}
self.direc = direc self.direc = direc
def revert_temporary_config(self): def revert_temporary_config(self):
+14 -8
View File
@@ -8,7 +8,6 @@ import M2Crypto
import zope.component import zope.component
from letsencrypt.client import acme from letsencrypt.client import acme
from letsencrypt.client import CONFIG
from letsencrypt.client import crypto_util from letsencrypt.client import crypto_util
from letsencrypt.client import display from letsencrypt.client import display
from letsencrypt.client import interfaces from letsencrypt.client import interfaces
@@ -16,10 +15,17 @@ from letsencrypt.client import network
class Revoker(object): class Revoker(object):
"""A revocation class for LE.""" """A revocation class for LE.
def __init__(self, server, installer):
:param config: Configuration.
:type config: :class:`letsencrypt.client.interfaces.IConfig`
"""
def __init__(self, server, installer, config):
self.network = network.Network(server) self.network = network.Network(server)
self.installer = installer self.installer = installer
self.config = config
def acme_revocation(self, cert): def acme_revocation(self, cert):
"""Handle ACME "revocation" phase. """Handle ACME "revocation" phase.
@@ -48,7 +54,7 @@ class Revoker(object):
def list_certs_keys(self): def list_certs_keys(self):
"""List trusted Let's Encrypt certificates.""" """List trusted Let's Encrypt certificates."""
list_file = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST") list_file = os.path.join(self.config.CERT_KEY_BACKUP, "LIST")
certs = [] certs = []
if not os.path.isfile(list_file): if not os.path.isfile(list_file):
@@ -69,9 +75,9 @@ class Revoker(object):
for row in csvreader: for row in csvreader:
cert = crypto_util.get_cert_info(row[1]) cert = crypto_util.get_cert_info(row[1])
b_k = os.path.join(CONFIG.CERT_KEY_BACKUP, b_k = os.path.join(self.config.CERT_KEY_BACKUP,
os.path.basename(row[2]) + "_" + row[0]) os.path.basename(row[2]) + "_" + row[0])
b_c = os.path.join(CONFIG.CERT_KEY_BACKUP, b_c = os.path.join(self.config.CERT_KEY_BACKUP,
os.path.basename(row[1]) + "_" + row[0]) os.path.basename(row[1]) + "_" + row[0])
cert.update({ cert.update({
@@ -118,8 +124,8 @@ class Revoker(object):
:param dict cert: Cert dict used throughout revocation :param dict cert: Cert dict used throughout revocation
""" """
list_file = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST") list_file = os.path.join(self.config.CERT_KEY_BACKUP, "LIST")
list_file2 = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST.tmp") list_file2 = os.path.join(self.config.CERT_KEY_BACKUP, "LIST.tmp")
with open(list_file, 'rb') as orgfile: with open(list_file, 'rb') as orgfile:
csvreader = csv.reader(orgfile) csvreader = csv.reader(orgfile)
+4 -4
View File
@@ -1,5 +1,5 @@
"""Class helps construct valid ACME messages for testing.""" """Class helps construct valid ACME messages for testing."""
from letsencrypt.client import CONFIG from letsencrypt.client import constants
CHALLENGES = { CHALLENGES = {
@@ -62,13 +62,13 @@ CHALLENGES = {
def get_dv_challenges(): def get_dv_challenges():
"""Returns all auth challenges.""" """Returns all auth challenges."""
return [chall for typ, chall in CHALLENGES.iteritems() return [chall for typ, chall in CHALLENGES.iteritems()
if typ in CONFIG.DV_CHALLENGES] if typ in constants.DV_CHALLENGES]
def get_client_challenges(): def get_client_challenges():
"""Returns all client challenges.""" """Returns all client challenges."""
return [chall for typ, chall in CHALLENGES.iteritems() return [chall for typ, chall in CHALLENGES.iteritems()
if typ in CONFIG.CLIENT_CHALLENGES] if typ in constants.CLIENT_CHALLENGES]
def get_challenges(): def get_challenges():
@@ -83,7 +83,7 @@ def gen_combos(challs):
combos = [] combos = []
for i, chall in enumerate(challs): for i, chall in enumerate(challs):
if chall["type"] in CONFIG.DV_CHALLENGES: if chall["type"] in constants.DV_CHALLENGES:
dv_chall.append(i) dv_chall.append(i)
else: else:
renewal_chall.append(i) renewal_chall.append(i)
@@ -6,8 +6,8 @@ import shutil
import mock import mock
from letsencrypt.client import challenge_util from letsencrypt.client import challenge_util
from letsencrypt.client import constants
from letsencrypt.client import client from letsencrypt.client import client
from letsencrypt.client import CONFIG
from letsencrypt.client.tests.apache import util from letsencrypt.client.tests.apache import util
@@ -157,12 +157,14 @@ class DvsniPerformTest(util.ApacheTest):
if vhost.addrs == set(v_addr1): if vhost.addrs == set(v_addr1):
self.assertEqual( self.assertEqual(
vhost.names, vhost.names,
set([str(self.challs[0].nonce + CONFIG.INVALID_EXT)])) set([str(self.challs[0].nonce +
constants.DVSNI_DOMAIN_SUFFIX)]))
else: else:
self.assertEqual(vhost.addrs, set(v_addr2)) self.assertEqual(vhost.addrs, set(v_addr2))
self.assertEqual( self.assertEqual(
vhost.names, vhost.names,
set([str(self.challs[1].nonce + CONFIG.INVALID_EXT)])) set([str(self.challs[1].nonce +
constants.DVSNI_DOMAIN_SUFFIX)]))
if __name__ == '__main__': if __name__ == '__main__':
+5 -8
View File
@@ -7,7 +7,7 @@ import unittest
import mock import mock
from letsencrypt.client import CONFIG from letsencrypt.client import constants
from letsencrypt.client.apache import configurator from letsencrypt.client.apache import configurator
from letsencrypt.client.apache import obj from letsencrypt.client.apache import obj
@@ -50,11 +50,7 @@ def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"):
def setup_apache_ssl_options(config_dir): def setup_apache_ssl_options(config_dir):
"""Move the ssl_options into position and return the path.""" """Move the ssl_options into position and return the path."""
option_path = os.path.join(config_dir, "options-ssl.conf") option_path = os.path.join(config_dir, "options-ssl.conf")
temp_options = pkg_resources.resource_filename( shutil.copyfile(constants.APACHE_MOD_SSL_CONF, option_path)
"letsencrypt.client.apache", os.path.basename(CONFIG.OPTIONS_SSL_CONF))
shutil.copyfile(
temp_options, option_path)
return option_path return option_path
@@ -69,7 +65,9 @@ def get_apache_configurator(
# This just states that the ssl module is already loaded # This just states that the ssl module is already loaded
mock_popen().communicate.return_value = ("ssl_module", "") mock_popen().communicate.return_value = ("ssl_module", "")
config = configurator.ApacheConfigurator( config = configurator.ApacheConfigurator(
config_path, mock.MagicMock(APACHE_SERVER_ROOT=config_path,
APACHE_MOD_SSL_CONF=ssl_options,
LE_VHOST_EXT="-le-ssl.conf"),
{ {
"backup": backups, "backup": backups,
"temp": os.path.join(work_dir, "temp_checkpoint"), "temp": os.path.join(work_dir, "temp_checkpoint"),
@@ -77,7 +75,6 @@ def get_apache_configurator(
"config": config_dir, "config": config_dir,
"work": work_dir, "work": work_dir,
}, },
ssl_options,
version) version)
return config return config
@@ -7,8 +7,8 @@ import unittest
import M2Crypto import M2Crypto
from letsencrypt.client import challenge_util from letsencrypt.client import challenge_util
from letsencrypt.client import constants
from letsencrypt.client import client from letsencrypt.client import client
from letsencrypt.client import CONFIG
from letsencrypt.client import le_util from letsencrypt.client import le_util
@@ -37,11 +37,11 @@ class DvsniGenCertTest(unittest.TestCase):
dns_regex = r"DNS:([^, $]*)" dns_regex = r"DNS:([^, $]*)"
cert = M2Crypto.X509.load_cert_string(pem) cert = M2Crypto.X509.load_cert_string(pem)
self.assertEqual( self.assertEqual(
cert.get_subject().CN, nonce + CONFIG.INVALID_EXT) cert.get_subject().CN, nonce + constants.DVSNI_DOMAIN_SUFFIX)
sans = cert.get_ext("subjectAltName").get_value() sans = cert.get_ext("subjectAltName").get_value()
exp_sans = set([nonce + CONFIG.INVALID_EXT, domain, ext]) exp_sans = set([nonce + constants.DVSNI_DOMAIN_SUFFIX, domain, ext])
act_sans = set(re.findall(dns_regex, sans)) act_sans = set(re.findall(dns_regex, sans))
self.assertEqual(exp_sans, act_sans) self.assertEqual(exp_sans, act_sans)
@@ -6,10 +6,11 @@ import mock
class PerformTest(unittest.TestCase): class PerformTest(unittest.TestCase):
"""Test client perform function.""" """Test client perform function."""
def setUp(self): def setUp(self):
from letsencrypt.client.client_authenticator import ClientAuthenticator from letsencrypt.client.client_authenticator import ClientAuthenticator
self.auth = ClientAuthenticator("demo_server.org") self.auth = ClientAuthenticator("demo_server.org", mock.MagicMock())
self.auth.rec_token.perform = mock.MagicMock( self.auth.rec_token.perform = mock.MagicMock(
name="rec_token_perform", side_effect=gen_client_resp) name="rec_token_perform", side_effect=gen_client_resp)
@@ -45,10 +46,11 @@ class PerformTest(unittest.TestCase):
class CleanupTest(unittest.TestCase): class CleanupTest(unittest.TestCase):
"""Test the Authenticator cleanup function.""" """Test the Authenticator cleanup function."""
def setUp(self): def setUp(self):
from letsencrypt.client.client_authenticator import ClientAuthenticator from letsencrypt.client.client_authenticator import ClientAuthenticator
self.auth = ClientAuthenticator("demo_server.org") self.auth = ClientAuthenticator("demo_server.org", mock.MagicMock())
self.mock_cleanup = mock.MagicMock(name="rec_token_cleanup") self.mock_cleanup = mock.MagicMock(name="rec_token_cleanup")
self.auth.rec_token.cleanup = self.mock_cleanup self.auth.rec_token.cleanup = self.mock_cleanup
+1 -1
View File
@@ -14,7 +14,7 @@ class RollbackTest(unittest.TestCase):
@classmethod @classmethod
def _call(cls, checkpoints): def _call(cls, checkpoints):
from letsencrypt.client.client import rollback from letsencrypt.client.client import rollback
rollback(checkpoints) rollback(checkpoints, mock.MagicMock())
@mock.patch("letsencrypt.client.client.determine_installer") @mock.patch("letsencrypt.client.client.determine_installer")
def test_no_problems(self, mock_det): def test_no_problems(self, mock_det):
+8 -8
View File
@@ -21,7 +21,7 @@ class ReverterCheckpointLocalTest(unittest.TestCase):
self.work_dir, self.direc = setup_work_direc() self.work_dir, self.direc = setup_work_direc()
self.reverter = Reverter(self.direc) self.reverter = Reverter(mock.MagicMock(), self.direc)
tup = setup_test_files() tup = setup_test_files()
self.config1, self.config2, self.dir1, self.dir2, self.sets = tup self.config1, self.config2, self.dir1, self.dir2, self.sets = tup
@@ -241,7 +241,7 @@ class TestFullCheckpointsReverter(unittest.TestCase):
logging.disable(logging.CRITICAL) logging.disable(logging.CRITICAL)
self.work_dir, self.direc = setup_work_direc() self.work_dir, self.direc = setup_work_direc()
self.reverter = Reverter(self.direc) self.reverter = Reverter(mock.MagicMock(), self.direc)
tup = setup_test_files() tup = setup_test_files()
self.config1, self.config2, self.dir1, self.dir2, self.sets = tup self.config1, self.config2, self.dir1, self.dir2, self.sets = tup
@@ -387,14 +387,14 @@ class TestFullCheckpointsReverter(unittest.TestCase):
class QuickInitReverterTest(unittest.TestCase): class QuickInitReverterTest(unittest.TestCase):
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
"""Quick test of init.""" """Quick test of init."""
def test_init(self): def test_init(self):
from letsencrypt.client.reverter import Reverter from letsencrypt.client.reverter import Reverter
rev = Reverter() config = mock.MagicMock()
rev = Reverter(config)
# Verify direc is set self.assertEqual(rev.direc['backup'], config.BACKUP_DIR)
self.assertTrue(rev.direc['backup']) self.assertEqual(rev.direc['temp'], config.TEMP_CHECKPOINT_DIR)
self.assertTrue(rev.direc['temp']) self.assertEqual(rev.direc['progress'], config.IN_PROGRESS_DIR)
self.assertTrue(rev.direc['progress'])
def setup_work_direc(): def setup_work_direc():
+7 -7
View File
@@ -82,15 +82,15 @@ def main(): # pylint: disable=too-many-statements,too-many-branches
zope.component.provideUtility(displayer) zope.component.provideUtility(displayer)
if args.view_config_changes: if args.view_config_changes:
client.view_config_changes() client.view_config_changes(CONFIG)
sys.exit() sys.exit()
if args.revoke: if args.revoke:
client.revoke(args.server) client.revoke(args.server, CONFIG)
sys.exit() sys.exit()
if args.rollback > 0: if args.rollback > 0:
client.rollback(args.rollback) client.rollback(args.rollback, CONFIG)
sys.exit() sys.exit()
if not args.eula: if not args.eula:
@@ -99,7 +99,7 @@ def main(): # pylint: disable=too-many-statements,too-many-branches
# Make sure we actually get an installer that is functioning properly # Make sure we actually get an installer that is functioning properly
# before we begin to try to use it. # before we begin to try to use it.
try: try:
installer = client.determine_installer() installer = client.determine_installer(CONFIG)
except errors.LetsEncryptMisconfigurationError as err: except errors.LetsEncryptMisconfigurationError as err:
logging.fatal("Please fix your configuration before proceeding. " logging.fatal("Please fix your configuration before proceeding. "
"The Installer exited with the following message: " "The Installer exited with the following message: "
@@ -110,17 +110,17 @@ def main(): # pylint: disable=too-many-statements,too-many-branches
if interfaces.IAuthenticator.providedBy(installer): # pylint: disable=no-member if interfaces.IAuthenticator.providedBy(installer): # pylint: disable=no-member
auth = installer auth = installer
else: else:
auth = client.determine_authenticator() auth = client.determine_authenticator(CONFIG)
domains = choose_names(installer) if args.domains is None else args.domains domains = choose_names(installer) if args.domains is None else args.domains
# Prepare for init of Client # Prepare for init of Client
if args.privkey is None: if args.privkey is None:
privkey = client.init_key(args.key_size) privkey = client.init_key(args.key_size, CONFIG.KEY_DIR)
else: else:
privkey = client.Client.Key(args.privkey[0], args.privkey[1]) privkey = client.Client.Key(args.privkey[0], args.privkey[1])
acme = client.Client(args.server, privkey, auth, installer) acme = client.Client(args.server, privkey, auth, installer, CONFIG)
# Validate the key and csr # Validate the key and csr
client.validate_key_csr(privkey) client.validate_key_csr(privkey)