updated pylintrc file to 1.3.1 version.

This commit is contained in:
James Kasten 2015-01-16 03:14:24 -08:00
commit a0969b1f29
73 changed files with 5057 additions and 3459 deletions

View file

@ -21,25 +21,15 @@ persistent=yes
# usually to register additional checkers.
load-plugins=
# Use multiple processes to speed up Pylint.
jobs=1
# DEPRECATED
include-ids=no
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
# DEPRECATED
symbols=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time. See also the "--disable" option for examples.
@ -94,7 +84,7 @@ comment=no
required-attributes=
# List of builtins function names that should not be used, separated by a comma
bad-functions=map,filter,input
bad-functions=map,filter,apply,input,file
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_
@ -191,23 +181,6 @@ notes=FIXME,XXX,TODO
logging-modules=logging
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
@ -221,10 +194,6 @@ dummy-variables-rgx=_$|dummy
# you should avoid to define new builtins when possible.
additional-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
[SIMILARITIES]
@ -257,7 +226,7 @@ single-line-if-stmt=no
no-space-check=trailing-comma,dict-separator
# Maximum number of lines in a module
max-module-lines=2000
max-module-lines=1250
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
@ -266,9 +235,6 @@ indent-string=' '
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
[TYPECHECK]
@ -279,7 +245,9 @@ ignore-mixin-members=yes
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis
ignored-modules=
# Get rid of the spurious no-member errors in pkg_resources
ignored-modules=pkg_resources
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set).
@ -287,7 +255,7 @@ ignored-classes=SQLObject
# When zope mode is activated, add a predefined set of Zope acquired attributes
# to generated-members.
zope=no
zope=yes
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed. Python regular
@ -328,10 +296,6 @@ valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make
[DESIGN]
@ -364,7 +328,7 @@ max-attributes=7
min-public-methods=2
# Maximum number of public methods for a class (see R0904).
max-public-methods=30
max-public-methods=20
[EXCEPTIONS]

View file

@ -46,7 +46,7 @@ sudo apt-get install python python-setuptools python-virtualenv \
### Installation
```
virtualenv --no-site-packages venv
virtualenv --no-site-packages -p python2 venv
./venv/bin/python setup.py install
sudo ./venv/bin/letsencrypt
```

View file

@ -0,0 +1,29 @@
:mod:`letsencrypt.client.apache`
--------------------------------
.. automodule:: letsencrypt.client.apache
:members:
:mod:`letsencrypt.client.apache.configurator`
=============================================
.. automodule:: letsencrypt.client.apache.configurator
:members:
:mod:`letsencrypt.client.apache.dvsni`
=============================================
.. automodule:: letsencrypt.client.apache.dvsni
:members:
:mod:`letsencrypt.client.apache.obj`
====================================
.. automodule:: letsencrypt.client.apache.obj
:members:
:mod:`letsencrypt.client.apache.parser`
=======================================
.. automodule:: letsencrypt.client.apache.parser
:members:

View file

@ -1,5 +0,0 @@
:mod:`letsencrypt.client.apache_configurator`
---------------------------------------------
.. automodule:: letsencrypt.client.apache_configurator
:members:

View file

@ -0,0 +1,5 @@
:mod:`letsencrypt.client.auth_handler`
--------------------------------
.. automodule:: letsencrypt.client.auth_handler
:members:

View file

@ -1,5 +0,0 @@
:mod:`letsencrypt.client.challenge`
-----------------------------------
.. automodule:: letsencrypt.client.challenge
:members:

View file

@ -0,0 +1,5 @@
:mod:`letsencrypt.client.challenge_util`
----------------------------------------
.. automodule:: letsencrypt.client.challenge_util
:members:

View file

@ -0,0 +1,5 @@
:mod:`letsencrypt.client.client_authenticator`
--------------------------------
.. automodule:: letsencrypt.client.client_authenticator
:members:

View file

@ -1,5 +0,0 @@
:mod:`letsencrypt.client.configurator`
--------------------------------------
.. automodule:: letsencrypt.client.configurator
:members:

View file

@ -0,0 +1,5 @@
:mod:`letsencrypt.client.interfaces`
------------------------------------
.. automodule:: letsencrypt.client.interfaces
:members:

View file

@ -0,0 +1,5 @@
:mod:`letsencrypt.client.network`
---------------------------------
.. automodule:: letsencrypt.client.network
:members:

View file

@ -1,5 +0,0 @@
:mod:`letsencrypt.client.nginx_configurator`
--------------------------------------------
.. automodule:: letsencrypt.client.nginx_configurator
:members:

View file

@ -0,0 +1,5 @@
:mod:`letsencrypt.client.recovery_token`
--------------------------------------------------
.. automodule:: letsencrypt.client.recovery_token
:members:

View file

@ -1,5 +0,0 @@
:mod:`letsencrypt.client.recovery_token_challenge`
--------------------------------------------------
.. automodule:: letsencrypt.client.recovery_token_challenge
:members:

View file

@ -0,0 +1,5 @@
:mod:`letsencrypt.client.revoker`
---------------------------------
.. automodule:: letsencrypt.client.revoker
:members:

View file

@ -1,5 +0,0 @@
:mod:`letsencrypt.client.validator`
-----------------------------------
.. automodule:: letsencrypt.client.validator
:members:

View file

@ -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

View file

@ -0,0 +1 @@
"""Let's Encrypt client.apache."""

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,179 @@
"""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
"""
# 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"

View file

@ -0,0 +1,91 @@
"""Module contains classes used by the Apache Configurator."""
class Addr(object):
"""Represents an Apache VirtualHost address.
:param str addr: addr part of vhost address
:param str port: port number or \*, or ""
"""
def __init__(self, tup):
self.tup = tup
@classmethod
def fromstring(cls, str_addr):
"""Initialize Addr from string."""
tup = str_addr.partition(':')
return cls((tup[0], tup[2]))
def __str__(self):
if self.tup[1]:
return "%s:%s" % self.tup
return self.tup[0]
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.tup == other.tup
return False
def __hash__(self):
return hash(self.tup)
def get_addr(self):
"""Return addr part of Addr object."""
return self.tup[0]
def get_port(self):
"""Return port."""
return self.tup[1]
def get_addr_obj(self, port):
"""Return new address object with same addr and new port."""
return self.__class__((self.tup[0], port))
# pylint: disable=too-few-public-methods
class VirtualHost(object):
"""Represents an Apache Virtualhost.
:ivar str filep: file path of VH
:ivar str path: Augeas path to virtual host
:ivar set addrs: Virtual Host addresses (:class:`set` of :class:`Addr`)
:ivar set names: Server names/aliases of vhost
(:class:`list` of :class:`str`)
:ivar bool ssl: SSLEngine on in vhost
:ivar bool enabled: Virtual host is enabled
"""
def __init__(self, filep, path, addrs, ssl, enabled, names=None):
"""Initialize a VH."""
self.filep = filep
self.path = path
self.addrs = addrs
self.names = set() if names is None else set(names)
self.ssl = ssl
self.enabled = enabled
def add_name(self, name):
"""Add name to vhost."""
self.names.add(name)
def __str__(self):
addr_str = ", ".join(str(addr) for addr in self.addrs)
return ("file: %s\n"
"vh_path: %s\n"
"addrs: %s\n"
"names: %s\n"
"ssl: %s\n"
"enabled: %s" % (self.filep, self.path, addr_str,
self.names, self.ssl, self.enabled))
def __eq__(self, other):
if isinstance(other, self.__class__):
return (self.filep == other.filep and self.path == other.path and
self.addrs == other.addrs and
self.names == other.names and
self.ssl == other.ssl and self.enabled == other.enabled)
return False

View file

@ -0,0 +1,401 @@
"""ApacheParser is a member object of the ApacheConfigurator class."""
import os
import re
from letsencrypt.client import errors
class ApacheParser(object):
"""Class handles the fine details of parsing the Apache Configuration."""
def __init__(self, aug, root, ssl_options):
# Find configuration root and make sure augeas can parse it.
self.aug = aug
self.root = root
self.loc = self._set_locations(ssl_options)
self._parse_file(self.loc["root"])
# Must also attempt to parse sites-available or equivalent
# Sites-available is not included naturally in configuration
self._parse_file(os.path.join(self.root, "sites-available/*"))
# This problem has been fixed in Augeas 1.0
self.standardize_excl()
def add_dir_to_ifmodssl(self, aug_conf_path, directive, val):
"""Adds directive and value to IfMod ssl block.
Adds given directive and value along configuration path within
an IfMod mod_ssl.c block. If the IfMod block does not exist in
the file, it is created.
:param str aug_conf_path: Desired Augeas config path to add directive
:param str directive: Directive you would like to add
:param str val: Value of directive ie. Listen 443, 443 is the value
"""
# TODO: Add error checking code... does the path given even exist?
# Does it throw exceptions?
if_mod_path = self._get_ifmod(aug_conf_path, "mod_ssl.c")
# IfModule can have only one valid argument, so append after
self.aug.insert(if_mod_path + "arg", "directive", False)
nvh_path = if_mod_path + "directive[1]"
self.aug.set(nvh_path, directive)
self.aug.set(nvh_path + "/arg", val)
def _get_ifmod(self, aug_conf_path, mod):
"""Returns the path to <IfMod mod> and creates one if it doesn't exist.
:param str aug_conf_path: Augeas configuration path
:param str mod: module ie. mod_ssl.c
"""
if_mods = self.aug.match(("%s/IfModule/*[self::arg='%s']" %
(aug_conf_path, mod)))
if len(if_mods) == 0:
self.aug.set("%s/IfModule[last() + 1]" % aug_conf_path, "")
self.aug.set("%s/IfModule[last()]/arg" % aug_conf_path, mod)
if_mods = self.aug.match(("%s/IfModule/*[self::arg='%s']" %
(aug_conf_path, mod)))
# Strip off "arg" at end of first ifmod path
return if_mods[0][:len(if_mods[0]) - 3]
def add_dir(self, aug_conf_path, directive, arg):
"""Appends directive to the end fo the file given by aug_conf_path.
.. note:: Not added to AugeasConfigurator because it may depend
on the lens
:param str aug_conf_path: Augeas configuration path to add directive
:param str directive: Directive to add
:param str arg: Value of the directive. ie. Listen 443, 443 is arg
"""
self.aug.set(aug_conf_path + "/directive[last() + 1]", directive)
if type(arg) is not list:
self.aug.set(aug_conf_path + "/directive[last()]/arg", arg)
else:
for i in range(len(arg)):
self.aug.set("%s/directive[last()]/arg[%d]" %
(aug_conf_path, (i+1)),
arg[i])
def find_dir(self, directive, arg=None, start=None):
"""Finds directive in the configuration.
Recursively searches through config files to find directives
Directives should be in the form of a case insensitive regex currently
.. todo:: Add order to directives returned. Last directive comes last..
.. todo:: arg should probably be a list
Note: Augeas is inherently case sensitive while Apache is case
insensitive. Augeas 1.0 allows case insensitive regexes like
regexp(/Listen/, 'i'), however the version currently supported
by Ubuntu 0.10 does not. Thus I have included my own case insensitive
transformation by calling case_i() on everything to maintain
compatibility.
:param str directive: Directive to look for
:param arg: Specific value directive must have, None if all should
be considered
:type arg: str or None
:param str start: Beginning Augeas path to begin looking
"""
# Cannot place member variable in the definition of the function so...
if not start:
start = get_aug_path(self.loc["root"])
# Debug code
# print "find_dir:", directive, "arg:", arg, " | Looking in:", start
# No regexp code
# if arg is None:
# matches = self.aug.match(start +
# "//*[self::directive='"+directive+"']/arg")
# else:
# matches = self.aug.match(start +
# "//*[self::directive='" + directive+"']/* [self::arg='" + arg + "']")
# includes = self.aug.match(start +
# "//* [self::directive='Include']/* [label()='arg']")
if arg is None:
matches = self.aug.match(("%s//*[self::directive=~regexp('%s')]/arg"
% (start, directive)))
else:
matches = self.aug.match(("%s//*[self::directive=~regexp('%s')]/*"
"[self::arg=~regexp('%s')]" %
(start, directive, arg)))
incl_regex = "(%s)|(%s)" % (case_i('Include'),
case_i('IncludeOptional'))
includes = self.aug.match(("%s//* [self::directive=~regexp('%s')]/* "
"[label()='arg']" % (start, incl_regex)))
# for inc in includes:
# print inc, self.aug.get(inc)
for include in includes:
# start[6:] to strip off /files
matches.extend(self.find_dir(
directive, arg, self._get_include_path(
strip_dir(start[6:]), self.aug.get(include))))
return matches
def _get_include_path(self, cur_dir, arg):
"""Converts an Apache Include directive into Augeas path.
Converts an Apache Include directive argument into an Augeas
searchable path
.. todo:: convert to use os.path.join()
:param str cur_dir: current working directory
:param str arg: Argument of Include directive
:returns: Augeas path string
:rtype: str
"""
# Sanity check argument - maybe
# Question: what can the attacker do with control over this string
# Effect parse file... maybe exploit unknown errors in Augeas
# If the attacker can Include anything though... and this function
# only operates on Apache real config data... then the attacker has
# already won.
# Perhaps it is better to simply check the permissions on all
# included files?
# check_config to validate apache config doesn't work because it
# would create a race condition between the check and this input
# TODO: Maybe... although I am convinced we have lost if
# Apache files can't be trusted. The augeas include path
# should be made to be exact.
# Check to make sure only expected characters are used <- maybe remove
# validChars = re.compile("[a-zA-Z0-9.*?_-/]*")
# matchObj = validChars.match(arg)
# if matchObj.group() != arg:
# logging.error("Error: Invalid regexp characters in %s", arg)
# return []
# Standardize the include argument based on server root
if not arg.startswith("/"):
arg = cur_dir + arg
# conf/ is a special variable for ServerRoot in Apache
elif arg.startswith("conf/"):
arg = self.root + arg[5:]
# TODO: Test if Apache allows ../ or ~/ for Includes
# Attempts to add a transform to the file if one does not already exist
self._parse_file(arg)
# Argument represents an fnmatch regular expression, convert it
# Split up the path and convert each into an Augeas accepted regex
# then reassemble
if "*" in arg or "?" in arg:
split_arg = arg.split("/")
for idx, split in enumerate(split_arg):
# * and ? are the two special fnmatch characters
if "*" in split or "?" in split:
# Turn it into a augeas regex
# TODO: Can this instead be an augeas glob instead of regex
split_arg[idx] = ("* [label()=~regexp('%s')]" %
self.fnmatch_to_re(split))
# Reassemble the argument
arg = "/".join(split_arg)
# If the include is a directory, just return the directory as a file
if arg.endswith("/"):
return get_aug_path(arg[:len(arg)-1])
return get_aug_path(arg)
def fnmatch_to_re(self, clean_fn_match): # pylint: disable=no-self-use
"""Method converts Apache's basic fnmatch to regular expression.
:param str clean_fn_match: Apache style filename match, similar to globs
:returns: regex suitable for augeas
:rtype: str
"""
# Checkout fnmatch.py in venv/local/lib/python2.7/fnmatch.py
regex = ""
for letter in clean_fn_match:
if letter == '.':
regex = regex + r"\."
elif letter == '*':
regex = regex + ".*"
# According to apache.org ? shouldn't appear
# but in case it is valid...
elif letter == '?':
regex = regex + "."
else:
regex = regex + letter
return regex
def _parse_file(self, file_path):
"""Parse file with Augeas
Checks to see if file_path is parsed by Augeas
If file_path isn't parsed, the file is added and Augeas is reloaded
:param str file_path: Apache config file path
"""
# Test if augeas included file for Httpd.lens
# Note: This works for augeas globs, ie. *.conf
inc_test = self.aug.match(
"/augeas/load/Httpd/incl [. ='%s']" % file_path)
if not inc_test:
# Load up files
# self.httpd_incl.append(file_path)
# self.aug.add_transform("Httpd.lns",
# self.httpd_incl, None, self.httpd_excl)
self._add_httpd_transform(file_path)
self.aug.load()
def standardize_excl(self):
"""Standardize the excl arguments for the Httpd lens in Augeas.
Note: Hack!
Standardize the excl arguments for the Httpd lens in Augeas
Servers sometimes give incorrect defaults
Note: This problem should be fixed in Augeas 1.0. Unfortunately,
Augeas 0.10 appears to be the most popular version currently.
"""
# attempt to protect against augeas error in 0.10.0 - ubuntu
# *.augsave -> /*.augsave upon augeas.load()
# Try to avoid bad httpd files
# There has to be a better way... but after a day and a half of testing
# I had no luck
# This is a hack... work around... submit to augeas if still not fixed
excl = ["*.augnew", "*.augsave", "*.dpkg-dist", "*.dpkg-bak",
"*.dpkg-new", "*.dpkg-old", "*.rpmsave", "*.rpmnew",
"*~",
self.root + "*.augsave",
self.root + "*~",
self.root + "*/*augsave",
self.root + "*/*~",
self.root + "*/*/*.augsave",
self.root + "*/*/*~"]
for i in range(len(excl)):
self.aug.set("/augeas/load/Httpd/excl[%d]" % (i+1), excl[i])
self.aug.load()
def _add_httpd_transform(self, incl):
"""Add a transform to Augeas.
This function will correctly add a transform to augeas
The existing augeas.add_transform in python is broken.
:param str incl: TODO
"""
last_include = self.aug.match("/augeas/load/Httpd/incl [last()]")
self.aug.insert(last_include[0], "incl", False)
self.aug.set("/augeas/load/Httpd/incl[last()]", incl)
def _set_locations(self, ssl_options):
"""Set default location for directives.
Locations are given as file_paths
.. todo:: Make sure that files are included
"""
root = self._find_config_root()
default = self._set_user_config_file(root)
temp = os.path.join(self.root, "ports.conf")
if os.path.isfile(temp):
listen = temp
name = temp
else:
listen = default
name = default
return {"root": root, "default": default, "listen": listen,
"name": name, "ssl_options": ssl_options}
def _find_config_root(self):
"""Find the Apache Configuration Root file."""
location = ["apache2.conf", "httpd.conf"]
for name in location:
if os.path.isfile(os.path.join(self.root, name)):
return os.path.join(self.root, name)
raise errors.LetsEncryptConfiguratorError(
"Could not find configuration root")
def _set_user_config_file(self, root):
"""Set the appropriate user configuration file
.. todo:: This will have to be updated for other distros versions
:param str root: pathname which contains the user config
"""
# Basic check to see if httpd.conf exists and
# in hierarchy via direct include
# httpd.conf was very common as a user file in Apache 2.2
if (os.path.isfile(os.path.join(self.root, 'httpd.conf')) and
self.find_dir(
case_i("Include"), case_i("httpd.conf"), root)):
return os.path.join(self.root, 'httpd.conf')
else:
return os.path.join(self.root, 'apache2.conf')
def case_i(string):
"""Returns case insensitive regex.
Returns a sloppy, but necessary version of a case insensitive regex.
Any string should be able to be submitted and the string is
escaped and then made case insensitive.
May be replaced by a more proper /i once augeas 1.0 is widely
supported.
:param str string: string to make case i regex
"""
return "".join(["["+c.upper()+c.lower()+"]"
if c.isalpha() else c for c in re.escape(string)])
def get_aug_path(file_path):
"""Return augeas path for full filepath.
:param str file_path: Full filepath
"""
return "/files%s" % file_path
def strip_dir(path):
"""Returns directory of file path.
.. todo:: Replace this with Python standard function
:param str path: path is a file path. not an augeas section or
directive path
:returns: directory
:rtype: str
"""
index = path.rfind("/")
if index > 0:
return path[:index+1]
# No directory
return ""

File diff suppressed because it is too large Load diff

View file

@ -8,15 +8,12 @@ import time
import augeas
from letsencrypt.client import CONFIG
from letsencrypt.client import configurator
from letsencrypt.client import le_util
class AugeasConfigurator(configurator.Configurator):
class AugeasConfigurator(object):
"""Base Augeas Configurator class.
.. todo:: Fix generic exception handling.
:ivar aug: Augeas object
:type aug: :class:`augeas.Augeas`
@ -32,7 +29,6 @@ class AugeasConfigurator(configurator.Configurator):
(used mostly for testing)
"""
super(AugeasConfigurator, self).__init__()
if not direc:
direc = {"backup": CONFIG.BACKUP_DIR,
@ -247,7 +243,6 @@ class AugeasConfigurator(configurator.Configurator):
le_util.make_or_verify_dir(cp_dir, 0o755, os.geteuid())
existing_filepaths = []
op_fd = None
filepaths_path = os.path.join(cp_dir, "FILEPATHS")
# Open up FILEPATHS differently depending on if it already exists
@ -291,8 +286,7 @@ class AugeasConfigurator(configurator.Configurator):
for idx, path in enumerate(filepaths):
shutil.copy2(os.path.join(
cp_dir,
os.path.basename(path) + '_' + str(idx)),
path)
os.path.basename(path) + '_' + str(idx)), path)
except (IOError, OSError):
# This file is required in all checkpoints.
logging.error("Unable to recover files from %s", cp_dir)
@ -329,7 +323,7 @@ class AugeasConfigurator(configurator.Configurator):
return True, ""
# pylint: disable=no-self-use
# pylint: disable=no-self-use, anomalous-backslash-in-string
def register_file_creation(self, temporary, *files):
"""Register the creation of all files during letsencrypt execution.
@ -348,7 +342,7 @@ class AugeasConfigurator(configurator.Configurator):
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:
@ -405,7 +399,7 @@ class AugeasConfigurator(configurator.Configurator):
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)

View file

@ -0,0 +1,441 @@
"""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:
auth = 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")
logging.info("Received Authorization for %s", domain)
return auth
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 for %s", domain)
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 path:
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

View file

@ -1,140 +0,0 @@
"""ACME challenge."""
import logging
import sys
from letsencrypt.client import CONFIG
class Challenge(object):
"""Let's Encrypt challenge."""
def __init__(self, configurator):
self.config = configurator
def perform(self, quiet=True):
"""Perform the challange.
:param bool quiet: TODO
"""
raise NotImplementedError()
def generate_response(self):
"""Generate response."""
raise NotImplementedError()
def cleanup(self):
"""Cleanup."""
raise NotImplementedError()
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 challange 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 challanges: 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

View file

@ -0,0 +1,75 @@
"""Challenge specific utility functions."""
import collections
import hashlib
from Crypto import Random
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):
"""Generate a DVSNI cert and save it to filepath.
:param str filepath: destination to save certificate. This will overwrite
any file that is currently at the location.
:param str name: domain to validate
:param str r_b64: jose base64 encoded dvsni r value
:param str nonce: hex value of nonce
:param key: Key to perform challenge
:type key: :class:`letsencrypt.client.client.Client.Key`
:returns: dvsni s value jose base64 encoded
:rtype: str
"""
# Generate S
dvsni_s = Random.get_random_bytes(CONFIG.S_SIZE)
dvsni_r = le_util.jose_b64decode(r_b64)
# Generate extension
ext = _dvsni_gen_ext(dvsni_r, dvsni_s)
cert_pem = crypto_util.make_ss_cert(
key.pem, [nonce + CONFIG.INVALID_EXT, name, ext])
with open(filepath, 'w') as chall_cert_file:
chall_cert_file.write(cert_pem)
return le_util.jose_b64encode(dvsni_s)
def _dvsni_gen_ext(dvsni_r, dvsni_s):
"""Generates z extension to be placed in certificate extension.
:param bytearray dvsni_r: DVSNI r value
:param bytearray dvsni_s: DVSNI s value
:returns: z + CONFIG.INVALID_EXT
:rtype: str
"""
z_base = hashlib.new('sha256')
z_base.update(dvsni_r)
z_base.update(dvsni_s)
return z_base.hexdigest() + CONFIG.INVALID_EXT

View file

@ -1,27 +1,25 @@
"""ACME protocol client class and helper functions."""
import collections
import csv
import json
import logging
import os
import shutil
import socket
import string
import sys
import time
import jsonschema
import M2Crypto
import requests
import zope.component
from letsencrypt.client import acme
from letsencrypt.client import apache_configurator
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 display
from letsencrypt.client import errors
from letsencrypt.client import interfaces
from letsencrypt.client import le_util
from letsencrypt.client import network
# it's weird to point to chocolate servers via raw IPv6 addresses, and
@ -33,153 +31,92 @@ ALLOW_RAW_IPV6_SERVER = False
class Client(object):
"""ACME protocol client.
:ivar config: Configurator.
:type config: :class:`letsencrypt.client.configurator.Configurator`
:ivar str server: Certificate authority server
:ivar str server_url: Full URL of the CSR server
:ivar csr: Certificate Signing Request
:type csr: :class:`CSR`
:ivar network: Network object for sending and receiving messages
:type network: :class:`letsencrypt.client.network.Network`
:ivar list names: Domain names (:class:`list` of :class:`str`).
:ivar privkey: Private key
:type privkey: :class:`Key`
:ivar authkey: Authorization Key
:type authkey: :class:`letsencrypt.client.client.Client.Key`
:ivar bool use_curses: Use curses UI
: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 type")
CSR = collections.namedtuple("CSR", "file data form")
def __init__(self, server, csr=CSR(None, None, None),
privkey=Key(None, None), use_curses=True):
"""Initialize a client."""
self.server = server
self.server_url = "https://%s/acme/" % self.server
self.names = []
self.use_curses = use_curses
def __init__(self, server, names, authkey, dv_auth, installer):
"""Initialize a client.
self.csr = csr
self.privkey = privkey
self._validate_csr_key_cli() # TODO: catch exceptions
# TODO: Can probably figure out which configurator to use
# without special packaging based on system info Command
# line arg or client function to discover
self.config = apache_configurator.ApacheConfigurator(
CONFIG.SERVER_ROOT)
def authenticate(self, domains=None, eula=False, redirect=None):
"""
:param list domains: List of domains
:param bool eula: EULA accepted
:param redirect: If traffic should be forwarded from HTTP to HTTPS.
:type redirect: bool or None
:raises errors.LetsEncryptClientError: CSR does not contain one of the
specified names.
: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`
"""
domains = [] if domains is None else domains
self.network = network.Network(server)
self.names = names
self.authkey = authkey
# Check configuration
if not self.config.config_test():
sys.exit(1)
sanity_check_names([server] + names)
# Display preview warning
if not eula:
with open('EULA') as eula_file:
if not display.generic_yesno(eula_file.read(),
"Agree", "Cancel"):
sys.exit(0)
self.installer = installer
# Display screen to select domains to validate
if domains:
sanity_check_names([self.server] + domains)
self.names = domains
else:
# This function adds all names
# found within the config to self.names
# Then filters them based on user selection
code, self.names = display.filter_names(self.get_all_names())
if code == display.OK and self.names:
# TODO: Allow multiple names once it is setup
self.names = [self.names[0]]
else:
sys.exit(0)
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):
"""Obtains a certificate from the ACME server.
:param csr: A valid CSR in DER format for the certificate the client
intends to receive.
: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)
:rtype: `tuple` of `str`
"""
# 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)
# Make sure we have key and csr to perform challenges
self.init_key_csr()
# TODO: Handle this exception/problem
if not crypto_util.csr_matches_names(self.csr.data, self.names):
raise errors.LetsEncryptClientError(
"CSR subject does not contain one of the specified names")
# 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(self.csr.data)
certificate_dict = self.acme_certificate(csr.data)
# Find set of virtual hosts to deploy certificates to
vhost = self.get_virtual_hosts(self.names)
# Install Certificate
cert_file = self.install_certificate(certificate_dict, vhost)
# Perform optimal config changes
self.optimize_config(vhost, redirect)
self.config.save("Completed Let's Encrypt Authentication")
# Save Certificate
cert_file, chain_file = self.save_certificate(
certificate_dict, cert_path, chain_path)
self.store_cert_key(cert_file, False)
def acme_challenge(self):
"""Handle ACME "challenge" phase.
return cert_file, chain_file
.. todo:: Handle more than one domain name in self.names
def acme_challenge(self, domain):
"""Handle ACME "challenge" phase.
:returns: ACME "challenge" message.
:rtype: dict
"""
return self.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
:param responses: TODO
:returns: ACME "authorization" message.
:rtype: dict
"""
auth_dict = self.send(acme.authorization_request(
challenge_msg["sessionID"], self.names[0],
challenge_msg["nonce"], responses, self.privkey.pem))
try:
return self.is_expected_msg(auth_dict, "authorization")
except:
logging.fatal(
"Failed Authorization procedure - cleaning up challenges")
sys.exit(1)
finally:
self.cleanup_challenges(chal_objs)
return self.network.send_and_receive_expected(
acme.challenge_request(domain), "challenge")
def acme_certificate(self, csr_der):
"""Handle ACME "certificate" phase.
@ -191,207 +128,25 @@ class Client(object):
"""
logging.info("Preparing and sending CSR...")
return self.send_and_receive_expected(
acme.certificate_request(csr_der, self.privkey.pem), "certificate")
return self.network.send_and_receive_expected(
acme.certificate_request(csr_der, self.authkey.pem), "certificate")
def acme_revocation(self, cert):
"""Handle ACME "revocation" phase.
# pylint: disable=no-self-use
def save_certificate(self, certificate_dict, cert_path, chain_path):
"""Saves the certificate received from the ACME server.
:param dict cert: TODO
:param dict certificate_dict: certificate message from server
:param str cert_path: Path to attempt to save the cert file
:param str chain_path: Path to attempt to save the chain file
:returns: ACME "revocation" message.
:rtype: dict
:returns: cert_file, chain_file (absolute paths to the actual files)
:rtype: `tuple` of `str`
"""
cert_der = M2Crypto.X509.load_cert(cert["backup_cert_file"]).as_der()
with open(cert["backup_key_file"], 'rU') as backup_key_file:
key = backup_key_file.read()
revocation = self.send_and_receive_expected(
acme.revocation_request(cert_der, key), "revocation")
display.generic_notification(
"You have successfully revoked the certificate for "
"%s" % cert["cn"], width=70, height=9)
remove_cert_key(cert)
self.list_certs_keys()
return revocation
def send(self, msg):
"""Send ACME message to server.
:param dict msg: ACME message (JSON serializable).
:returns: Server response message.
:rtype: dict
:raises TypeError: if `msg` is not JSON serializable
:raises jsonschema.ValidationError: if not valid ACME message
:raises errors.LetsEncryptClientError: in case of connection error
or if response from server is not a valid ACME message.
"""
json_encoded = json.dumps(msg)
acme.acme_object_validate(json_encoded)
try:
response = requests.post(
self.server_url,
data=json_encoded,
headers={"Content-Type": "application/json"},
)
except requests.exceptions.RequestException as error:
raise errors.LetsEncryptClientError(
'Sending ACME message to server has failed: %s' % error)
try:
acme.acme_object_validate(response.content)
except ValueError:
raise errors.LetsEncryptClientError(
'Server did not send JSON serializable message')
except jsonschema.ValidationError as error:
raise errors.LetsEncryptClientError(
'Response from server is not a valid ACME message')
return response.json()
def send_and_receive_expected(self, msg, expected):
"""Send ACME message to server and return expected message.
:param dict msg: ACME message (JSON serializable).
:param str expected: Name of the expected response ACME message type.
:returns: ACME response message of expected type.
:rtype: dict
:raises errors.LetsEncryptClientError: An exception is thrown
"""
response = self.send(msg)
try:
return self.is_expected_msg(response, expected)
except: # TODO: too generic exception
raise errors.LetsEncryptClientError(
'Expected message (%s) not received' % expected)
def is_expected_msg(self, response, expected, delay=3, rounds=20):
"""Is reponse expected ACME message?
:param dict response: ACME response message from server.
:param str expected: Name of the expected response ACME message type.
:param int delay: Number of seconds to delay before next round
in case of ACME "defer" response message.
:param int rounds: Number of resend attempts in case of ACME "defer"
reponse message.
:returns: ACME response message from server.
:rtype: dict
:raises LetsEncryptClientError: if server sent ACME "error" message
"""
for _ in xrange(rounds):
if response["type"] == expected:
return response
elif response["type"] == "error":
logging.error(
"%s: %s - More Info: %s", response["error"],
response.get("message", ""), response.get("moreInfo", ""))
raise errors.LetsEncryptClientError(response["error"])
elif response["type"] == "defer":
logging.info("Waiting for %d seconds...", delay)
time.sleep(delay)
response = self.send(acme.status_request(response["token"]))
else:
logging.fatal("Received unexpected message")
logging.fatal("Expected: %s" % expected)
logging.fatal("Received: " + response)
sys.exit(33)
logging.error(
"Server has deferred past the max of %d seconds", rounds * delay)
def list_certs_keys(self):
"""List trusted Let's Encrypt certificates."""
list_file = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST")
certs = []
if not os.path.isfile(list_file):
logging.info(
"You don't have any certificates saved from letsencrypt")
return
c_sha1_vh = {}
for (cert, _, path) in self.config.get_all_certs_keys():
try:
c_sha1_vh[M2Crypto.X509.load_cert(
cert).get_fingerprint(md='sha1')] = path
except:
continue
with open(list_file, 'rb') as csvfile:
csvreader = csv.reader(csvfile)
for row in csvreader:
cert = crypto_util.get_cert_info(row[1])
b_k = os.path.join(CONFIG.CERT_KEY_BACKUP,
os.path.basename(row[2]) + "_" + row[0])
b_c = os.path.join(CONFIG.CERT_KEY_BACKUP,
os.path.basename(row[1]) + "_" + row[0])
cert.update({
"orig_key_file": row[2],
"orig_cert_file": row[1],
"idx": int(row[0]),
"backup_key_file": b_k,
"backup_cert_file": b_c,
"installed": c_sha1_vh.get(cert["fingerprint"], ""),
})
certs.append(cert)
if certs:
self.choose_certs(certs)
else:
display.generic_notification(
"There are not any trusted Let's Encrypt "
"certificates for this server.")
def choose_certs(self, certs):
"""Display choose certificates menu.
:param list certs: List of cert dicts.
"""
code, tag = display.display_certs(certs)
if code == display.OK:
cert = certs[tag]
if display.confirm_revocation(cert):
self.acme_revocation(cert)
else:
self.choose_certs(certs)
elif code == display.HELP:
cert = certs[tag]
display.more_info_cert(cert)
self.choose_certs(certs)
else:
exit(0)
def install_certificate(self, certificate_dict, vhost):
"""Install certificate
:returns: Path to a certificate file.
:rtype: str
:raises IOError: If unable to find room to write the cert files
"""
cert_chain_abspath = None
cert_fd, cert_file = le_util.unique_file(CONFIG.CERT_PATH, 0o644)
cert_fd, cert_file = le_util.unique_file(cert_path, 0o644)
cert_fd.write(
crypto_util.b64_cert_to_pem(certificate_dict["certificate"]))
cert_fd.close()
@ -399,7 +154,7 @@ class Client(object):
"Server issued certificate; certificate written to %s", cert_file)
if certificate_dict.get("chain", None):
chain_fd, chain_fn = le_util.unique_file(CONFIG.CHAIN_PATH, 0o644)
chain_fd, chain_fn = le_util.unique_file(chain_path, 0o644)
for cert in certificate_dict.get("chain", []):
chain_fd.write(crypto_util.b64_cert_to_pem(cert))
chain_fd.close()
@ -409,40 +164,58 @@ class Client(object):
# This expects a valid chain file
cert_chain_abspath = os.path.abspath(chain_fn)
return os.path.abspath(cert_file), cert_chain_abspath
def deploy_certificate(self, privkey, cert_file, chain_file):
"""Install certificate
:returns: Path to a certificate file.
:rtype: str
"""
# Find set of virtual hosts to deploy certificates to
vhost = self.get_virtual_hosts(self.names)
chain = None if chain_file is None else os.path.abspath(chain_file)
for host in vhost:
self.config.deploy_cert(host,
os.path.abspath(cert_file),
os.path.abspath(self.privkey.file),
cert_chain_abspath)
self.installer.deploy_cert(host,
os.path.abspath(cert_file),
os.path.abspath(privkey.file),
chain)
# Enable any vhost that was issued to, but not enabled
if not host.enabled:
logging.info("Enabling Site %s", host.filep)
self.config.enable_site(host)
self.installer.enable_site(host)
self.installer.save("Deployed Let's Encrypt Certificate")
# sites may have been enabled / final cleanup
self.config.restart(quiet=self.use_curses)
self.installer.restart()
display.success_installation(self.names)
zope.component.getUtility(
interfaces.IDisplay).success_installation(self.names)
return cert_file
return vhost
def optimize_config(self, vhost, redirect=None):
"""Optimize the configuration.
.. todo:: Handle multiple vhosts
:param vhost: vhost to optimize
:type vhost: :class:`apache_configurator.VH`
:type vhost: :class:`letsencrypt.client.apache.obj.VirtualHost`
:param redirect: If traffic should be forwarded from HTTP to HTTPS.
:type redirect: bool or None
"""
# TODO: this should most definitely be moved to __init__
if redirect is None:
redirect = display.redirect_by_default()
redirect = zope.component.getUtility(
interfaces.IDisplay).redirect_by_default()
if redirect:
self.redirect_to_ssl(vhost)
self.config.restart(quiet=self.use_curses)
self.installer.restart()
# if self.ocsp_stapling is None:
# q = ("Would you like to protect the privacy of your users "
@ -454,60 +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.config.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):
response = "null"
if c_obj["type"] in CONFIG.CONFIG_CHALLENGES:
response = self.config.perform(c_obj)
else:
# Handle RecoveryToken type challenges
pass
for index in indices[i]:
responses[index] = response
logging.info(
"Configured Apache for challenges; waiting for verification...")
return responses, challenge_objs
def store_cert_key(self, cert_file, encrypt=False):
"""Store certificate key.
@ -536,17 +255,17 @@ class Client(object):
for row in csvreader:
idx = int(row[0]) + 1
csvwriter = csv.writer(csvfile)
csvwriter.writerow([str(idx), cert_file, self.privkey.file])
csvwriter.writerow([str(idx), cert_file, self.authkey.file])
else:
with open(list_file, 'wb') as csvfile:
csvwriter = csv.writer(csvfile)
csvwriter.writerow(["0", cert_file, self.privkey.file])
csvwriter.writerow(["0", cert_file, self.authkey.file])
shutil.copy2(self.privkey.file,
shutil.copy2(self.authkey.file,
os.path.join(
CONFIG.CERT_KEY_BACKUP,
os.path.basename(self.privkey.file) + "_" + str(idx)))
os.path.basename(self.authkey.file) + "_" + str(idx)))
shutil.copy2(cert_file,
os.path.join(
CONFIG.CERT_KEY_BACKUP,
@ -558,16 +277,16 @@ class Client(object):
"""Redirect all traffic from HTTP to HTTPS
:param vhost: list of ssl_vhosts
:type vhost: :class:`apache_configurator.VH`
:type vhost: :class:`letsencrypt.client.interfaces.IInstaller`
"""
for ssl_vh in vhost:
success, redirect_vhost = self.config.enable_redirect(ssl_vh)
success, redirect_vhost = self.installer.enable_redirect(ssl_vh)
logging.info(
"\nRedirect vhost: %s - %s ", redirect_vhost.filep, success)
# If successful, make sure redirect site is enabled
if success:
self.config.enable_site(redirect_vhost)
self.installer.enable_site(redirect_vhost)
def get_virtual_hosts(self, domains):
"""Retrieve the appropriate virtual host for the domain
@ -575,187 +294,104 @@ class Client(object):
:param list domains: Domains to find ssl vhosts for
:returns: associated vhosts
:rtype: :class:`apache_configurator.VH`
:rtype: :class:`letsencrypt.client.apache.obj.VirtualHost`
"""
vhost = set()
for name in domains:
host = self.config.choose_virtual_host(name)
host = self.installer.choose_virtual_host(name)
if host is not None:
vhost.add(host)
return vhost
def challenge_factory(self, name, challenges, path):
"""
:param name: TODO
def validate_key_csr(privkey, csr):
"""Validate CSR and key files.
: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.
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 list path: List of indices from `challenges`.
:param privkey: Key associated with CSR
:type privkey: :class:`letsencrypt.client.client.Client.Key`
:returns: A pair of TODO
:rtype: tuple
:param csr: CSR
:type csr: :class:`letsencrypt.client.client.Client.CSR`
"""
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.privkey,
})
challenge_obj_indices.append(sni_satisfies)
logging.debug(sni_todo)
return challenge_objs, challenge_obj_indices
def init_key_csr(self):
"""Initializes privkey and csr.
Inits key and CSR using provided files or generating new files
if necessary. Both will be saved in PEM format on the
filesystem. The CSR is placed into DER format to allow
the namedtuple to easily work with the protocol.
"""
if not self.privkey.file:
key_pem = crypto_util.make_key(CONFIG.RSA_KEY_SIZE)
# Save file
le_util.make_or_verify_dir(CONFIG.KEY_DIR, 0o700)
key_f, key_filename = le_util.unique_file(
os.path.join(CONFIG.KEY_DIR, "key-letsencrypt.pem"), 0o600)
key_f.write(key_pem)
key_f.close()
logging.info("Generating key: %s", key_filename)
self.privkey = Client.Key(key_filename, key_pem)
if not self.csr.file:
csr_pem, csr_der = crypto_util.make_csr(
self.privkey.pem, self.names)
# Save CSR
le_util.make_or_verify_dir(CONFIG.CERT_DIR, 0o755)
csr_f, csr_filename = le_util.unique_file(
os.path.join(CONFIG.CERT_DIR, "csr-letsencrypt.pem"), 0o644)
csr_f.write(csr_pem)
csr_f.close()
logging.info("Creating CSR: %s", csr_filename)
self.csr = Client.CSR(csr_filename, csr_der, "der")
elif self.csr.type != "der":
# The user is going to pass in a pem format file
# That is why we must conver it to der since the
# protocol uses der exclusively.
csr_obj = M2Crypto.X509.load_request_string(self.csr.data)
self.csr = Client.CSR(self.csr.file, csr_obj.as_der(), "der")
def _validate_csr_key_cli(self):
"""Validate CSR and key files.
Verifies that the client key and csr arguments are valid and
correspond to one another.
:raises LetsEncryptClientError: if validation fails
"""
# TODO: Handle all of these problems appropriately
# The client can eventually do things like prompt the user
# and allow the user to take more appropriate actions
# If CSR is provided, it must be readable and valid.
if self.csr.data and not crypto_util.valid_csr(self.csr.data):
raise errors.LetsEncryptClientError(
"The provided CSR is not a valid CSR")
# If key is provided, it must be readable and valid.
if (self.privkey.pem and
not crypto_util.valid_privkey(self.privkey.pem)):
raise errors.LetsEncryptClientError(
"The provided key is not a valid key")
# If CSR and key are provided, the key must be the same key used
# in the CSR.
if self.csr.data and self.privkey.pem:
if not crypto_util.csr_matches_pubkey(
self.csr.data, self.privkey.pem):
raise errors.LetsEncryptClientError(
"The key and CSR do not match")
def get_all_names(self):
"""Return all valid names in the configuration."""
names = list(self.config.get_all_names())
sanity_check_names(names)
if not names:
logging.fatal("No domain names were found in your apache config")
logging.fatal("Either specify which names you would like "
"letsencrypt to validate or add server names "
"to your virtual hosts")
sys.exit(1)
return names
def remove_cert_key(cert):
"""Remove certificate key.
:param dict cert:
:raises LetsEncryptClientError: if validation fails
"""
list_file = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST")
list_file2 = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST.tmp")
# TODO: Handle all of these problems appropriately
# The client can eventually do things like prompt the user
# and allow the user to take more appropriate actions
with open(list_file, 'rb') as orgfile:
csvreader = csv.reader(orgfile)
if csr.form == "der":
csr_obj = M2Crypto.X509.load_request_der_string(csr.data)
csr = Client.CSR(csr.file, csr_obj.as_pem(), "der")
with open(list_file2, 'wb') as newfile:
csvwriter = csv.writer(newfile)
# If CSR is provided, it must be readable and valid.
if csr.data and not crypto_util.valid_csr(csr.data):
raise errors.LetsEncryptClientError(
"The provided CSR is not a valid CSR")
for row in csvreader:
if not (row[0] == str(cert["idx"]) and
row[1] == cert["orig_cert_file"] and
row[2] == cert["orig_key_file"]):
csvwriter.writerow(row)
# If key is provided, it must be readable and valid.
if privkey.pem and not crypto_util.valid_privkey(privkey.pem):
raise errors.LetsEncryptClientError(
"The provided key is not a valid key")
shutil.copy2(list_file2, list_file)
os.remove(list_file2)
os.remove(cert["backup_cert_file"])
os.remove(cert["backup_key_file"])
# If CSR and key are provided, the key must be the same key used
# in the CSR.
if csr.data and privkey.pem:
if not crypto_util.csr_matches_pubkey(
csr.data, privkey.pem):
raise errors.LetsEncryptClientError(
"The key and CSR do not match")
def init_key():
"""Initializes privkey.
Inits key and CSR using provided files or generating new files
if necessary. Both will be saved in PEM format on the
filesystem. The CSR is placed into DER format to allow
the namedtuple to easily work with the protocol.
"""
key_pem = crypto_util.make_key(CONFIG.RSA_KEY_SIZE)
# Save file
le_util.make_or_verify_dir(CONFIG.KEY_DIR, 0o700)
key_f, key_filename = le_util.unique_file(
os.path.join(CONFIG.KEY_DIR, "key-letsencrypt.pem"), 0o600)
key_f.write(key_pem)
key_f.close()
logging.info("Generating key: %s", key_filename)
return Client.Key(key_filename, key_pem)
def init_csr(privkey, names):
"""Initialize a CSR with the given private key."""
csr_pem, csr_der = crypto_util.make_csr(privkey.pem, names)
# Save CSR
le_util.make_or_verify_dir(CONFIG.CERT_DIR, 0o755)
csr_f, csr_filename = le_util.unique_file(
os.path.join(CONFIG.CERT_DIR, "csr-letsencrypt.pem"), 0o644)
csr_f.write(csr_pem)
csr_f.close()
logging.info("Creating CSR: %s", csr_filename)
return Client.CSR(csr_filename, csr_der, "der")
def csr_pem_to_der(csr):
"""Convert pem CSR to der."""
csr_obj = M2Crypto.X509.load_request_string(csr.data)
return Client.CSR(csr.file, csr_obj.as_der(), "der")
def sanity_check_names(names):
@ -795,5 +431,5 @@ def is_hostname_sane(hostname):
# is this a valid IPv6 address?
socket.getaddrinfo(hostname, 443, socket.AF_INET6)
return True
except:
except socket.error:
return False

View file

@ -0,0 +1,49 @@
"""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")

View file

@ -1,98 +0,0 @@
"""Configurator."""
class Configurator(object):
"""Generic Let's Encrypt configurator.
Class represents all possible webservers and configuration editors
This includes the generic webserver which wont have configuration
files at all, but instead create a new process to handle the DVSNI
and other challenges.
"""
def deploy_cert(self, vhost, cert, key, cert_chain=None):
"""Deploy certificate.
:param vhost
:param str cert: CSR
:param str key: Private key
"""
raise NotImplementedError()
def choose_virtual_host(self, name):
"""Chooses a virtual host based on a given domain name."""
raise NotImplementedError()
def get_all_names(self):
"""Returns all names found in the configuration."""
raise NotImplementedError()
def enable_redirect(self, ssl_vhost):
"""Redirect all traffic to the given ssl_vhost (port 80 => 443)."""
raise NotImplementedError()
def enable_hsts(self, ssl_vhost):
"""Enable HSTS on the given ssl_vhost."""
raise NotImplementedError()
def enable_ocsp_stapling(self, ssl_vhost):
"""Enable OCSP stapling on given ssl_vhost."""
raise NotImplementedError()
def get_all_certs_keys(self):
"""Retrieve all certs and keys set in configuration.
:returns: List of tuples with form [(cert, key, path)].
:rtype: list
"""
raise NotImplementedError()
def enable_site(self, vhost):
"""Enable the site at the given vhost."""
raise NotImplementedError()
def save(self, title=None, temporary=False):
"""Saves all changes to the configuration files.
Both title and temporary are needed because a save may be
intended to be permanent, but the save is not ready to be a full
checkpoint
:param str title: The title of the save. If a title is given, the
configuration will be saved as a new checkpoint and put in a
timestamped directory. `title` has no effect if temporary is true.
:param bool temporary: Indicates whether the changes made will
be quickly reversed in the future (challenges)
"""
raise NotImplementedError()
def revert_challenge_config(self):
"""Reload the users original configuration files."""
raise NotImplementedError()
def rollback_checkpoints(self, rollback=1):
"""Revert `rollback` number of configuration checkpoints."""
raise NotImplementedError()
def display_checkpoints(self):
"""Display the saved configuration checkpoints."""
raise NotImplementedError()
def config_test(self):
"""Make sure the configuration is valid."""
raise NotImplementedError()
def restart(self, quiet=False):
"""Restart or refresh the server content."""
raise NotImplementedError()
def perform(self, chall_dict):
"""Perform the given challenge"""
raise NotImplementedError()
def cleanup(self):
"""Cleanup configuration changes from challenge."""
raise NotImplementedError()

View file

@ -1,6 +1,5 @@
"""Let's Encrypt client crypto utility functions"""
import binascii
import hashlib
import logging
import time
@ -15,13 +14,6 @@ from letsencrypt.client import CONFIG
from letsencrypt.client import le_util
# TODO: All of these functions need unit tests
def b64_cert_to_pem(b64_der_cert):
return M2Crypto.X509.load_cert_der_string(
le_util.jose_b64decode(b64_der_cert)).as_pem()
def create_sig(msg, key_str, nonce=None, nonce_len=CONFIG.NONCE_SIZE):
"""Create signature with nonce prepended to the message.
@ -36,10 +28,10 @@ def create_sig(msg, key_str, nonce=None, nonce_len=CONFIG.NONCE_SIZE):
:param str msg: Message to be signed
:param nonce: Nonce to be used. If None, nonce of `nonce_len` size
will be randomly genereted.
will be randomly generated.
:type nonce: str or None
:param int nonce_len: Size of the automaticaly generated nonce.
:param int nonce_len: Size of the automatically generated nonce.
:returns: Signature.
:rtype: dict
@ -55,8 +47,8 @@ def create_sig(msg, key_str, nonce=None, nonce_len=CONFIG.NONCE_SIZE):
logging.debug('%s signed as %s', msg_with_nonce, signature)
n_bytes = binascii.unhexlify(leading_zeros(hex(key.n)[2:].rstrip("L")))
e_bytes = binascii.unhexlify(leading_zeros(hex(key.e)[2:].rstrip("L")))
n_bytes = binascii.unhexlify(_leading_zeros(hex(key.n)[2:].rstrip("L")))
e_bytes = binascii.unhexlify(_leading_zeros(hex(key.e)[2:].rstrip("L")))
return {
"nonce": le_util.jose_b64encode(nonce),
@ -70,33 +62,21 @@ def create_sig(msg, key_str, nonce=None, nonce_len=CONFIG.NONCE_SIZE):
}
def leading_zeros(arg):
def _leading_zeros(arg):
if len(arg) % 2:
return "0" + arg
return arg
def sha256(arg):
return hashlib.sha256(arg).hexdigest()
# based on M2Crypto unit test written by Toby Allsopp
def make_key(bits=CONFIG.RSA_KEY_SIZE):
"""
Returns new RSA key in PEM form with specified bits
"""
# Python Crypto module doesn't produce any stdout
key = Crypto.PublicKey.RSA.generate(bits)
# rsa = M2Crypto.RSA.gen_key(bits, 65537)
# key_pem = rsa.as_pem(cipher=None)
# rsa = None # should not be freed here
return key.exportKey(format='PEM')
def make_csr(key_str, domains):
"""
Returns new CSR in PEM and DER form using key_file containing all domains
"""Generate a CSR.
:param str key_str: RSA key.
:param list domains: Domains included in the certificate.
:returns: new CSR in PEM and DER form containing all domains
:rtype: tuple
"""
assert domains, "Must provide one or more hostnames for the CSR."
rsa_key = M2Crypto.RSA.load_key_string(key_str)
@ -115,7 +95,7 @@ def make_csr(key_str, domains):
extstack = M2Crypto.X509.X509_Extension_Stack()
ext = M2Crypto.X509.new_extension(
'subjectAltName', ", ".join(["DNS:%s" % d for d in domains]))
'subjectAltName', ", ".join("DNS:%s" % d for d in domains))
extstack.push(ext)
csr.add_extensions(extstack)
@ -126,10 +106,82 @@ def make_csr(key_str, domains):
return csr.as_pem(), csr.as_der()
def make_ss_cert(key_str, domains):
# WARNING: the csr and private key file are possible attack vectors for TOCTOU
# We should either...
# A. Do more checks to verify that the CSR is trusted/valid
# B. Audit the parsing code for vulnerabilities
def valid_csr(csr):
"""Validate CSR.
Check if `csr` is a valid CSR for the given domains.
:param str csr: CSR in PEM.
:returns: Validity of CSR.
:rtype: bool
"""
try:
csr_obj = M2Crypto.X509.load_request_string(csr)
return bool(csr_obj.verify(csr_obj.get_pubkey()))
except M2Crypto.X509.X509Error:
return False
def csr_matches_pubkey(csr, privkey):
"""Does private key correspond to the subject public key in the CSR?
:param str csr: CSR in PEM.
:param str privkey: Private key file contents
:returns: Correspondence of private key to CSR subject public key.
:rtype: bool
"""
csr_obj = M2Crypto.X509.load_request_string(csr)
privkey_obj = M2Crypto.RSA.load_key_string(privkey)
return csr_obj.get_pubkey().get_rsa().pub() == privkey_obj.pub()
# based on M2Crypto unit test written by Toby Allsopp
def make_key(bits=CONFIG.RSA_KEY_SIZE):
"""Generate PEM encoded RSA key.
:param int bits: Number of bits, at least 1024.
:returns: new RSA key in PEM form with specified number of bits
:rtype: str
"""
# rsa = M2Crypto.RSA.gen_key(bits, 65537)
# key_pem = rsa.as_pem(cipher=None)
# rsa = None # should not be freed here
# Python Crypto module doesn't produce any stdout
return Crypto.PublicKey.RSA.generate(bits).exportKey(format='PEM')
def valid_privkey(privkey):
"""Is valid RSA private key?
:param str privkey: Private key file contents
:returns: Validity of private key.
:rtype: bool
"""
try:
return bool(M2Crypto.RSA.load_key_string(privkey).check_key())
except M2Crypto.RSA.RSAError:
return False
def make_ss_cert(key_str, domains, not_before=None,
validity=(7 * 24 * 60 * 60)):
"""Returns new self-signed cert in PEM form.
Uses key_str and contains all domains.
"""
assert domains, "Must provide one or more hostnames for the CSR."
@ -142,11 +194,11 @@ def make_ss_cert(key_str, domains):
cert.set_serial_number(1337)
cert.set_version(2)
current_ts = long(time.time())
current_ts = long(time.time() if not_before is None else not_before)
current = M2Crypto.ASN1.ASN1_UTCTIME()
current.set_time(current_ts)
expire = M2Crypto.ASN1.ASN1_UTCTIME()
expire.set_time((7 * 24 * 60 * 60) + current_ts)
expire.set_time(current_ts + validity)
cert.set_not_before(current)
cert.set_not_after(expire)
@ -158,11 +210,13 @@ def make_ss_cert(key_str, domains):
subject.CN = domains[0]
cert.set_issuer(cert.get_subject())
cert.add_ext(M2Crypto.X509.new_extension('basicConstraints', 'CA:FALSE'))
# cert.add_ext(M2Crypto.X509.new_extension(
# 'extendedKeyUsage', 'TLS Web Server Authentication'))
cert.add_ext(M2Crypto.X509.new_extension(
'subjectAltName', ", ".join(["DNS:%s" % d for d in domains])))
if len(domains) > 1:
cert.add_ext(M2Crypto.X509.new_extension(
'basicConstraints', 'CA:FALSE'))
# cert.add_ext(M2Crypto.X509.new_extension(
# 'extendedKeyUsage', 'TLS Web Server Authentication'))
cert.add_ext(M2Crypto.X509.new_extension(
'subjectAltName', ", ".join(["DNS:%s" % d for d in domains])))
cert.sign(pubkey, 'sha256')
assert cert.verify(pubkey)
@ -200,75 +254,6 @@ def get_cert_info(filename):
}
# WARNING: the csr and private key file are possible attack vectors for TOCTOU
# We should either...
# A. Do more checks to verify that the CSR is trusted/valid
# B. Audit the parsing code for vulnerabilities
def valid_csr(csr):
"""Validate CSR.
Check if `csr` is a valid CSR for the given domains.
:param str csr: CSR file contents
:returns: Validity of CSR.
:rtype: bool
"""
try:
csr_obj = M2Crypto.X509.load_request_string(csr)
return bool(csr_obj.verify(csr_obj.get_pubkey()))
except M2Crypto.X509.X509Error:
return False
def csr_matches_names(csr, domains):
"""Check if CSR contains the subject of one of the domains.
M2Crypto currently does not expose the OpenSSL interface to
also check the SAN extension. This is insufficient for full testing
:param str csr: CSR file contents
:param list domains: Domains the CSR should contain.
:returns: If the CSR subject contains one of the domains
:rtype: bool
"""
try:
csr_obj = M2Crypto.X509.load_request_der_string(csr)
return csr_obj.get_subject().CN in domains
except M2Crypto.X509.X509Error:
return False
def valid_privkey(privkey):
"""Is valid RSA private key?
:param str privkey: Private key file contents
:returns: Validity of private key.
:rtype: bool
"""
try:
return bool(M2Crypto.RSA.load_key_string(privkey).check_key())
except M2Crypto.RSA.RSAError:
return False
def csr_matches_pubkey(csr, privkey):
"""Does private key correspond to the subject public key in the CSR?
:param str csr: CSR file contents
:param str privkey: Private key file contents
:returns: Correspondence of private key to CSR subject public key.
:rtype: bool
"""
csr_obj = M2Crypto.X509.load_request_string(csr)
privkey_obj = M2Crypto.RSA.load_key_string(privkey)
return csr_obj.get_pubkey().get_rsa().pub() == privkey_obj.pub()
def b64_cert_to_pem(b64_der_cert):
return M2Crypto.X509.load_cert_der_string(
le_util.jose_b64decode(b64_der_cert)).as_pem()

View file

@ -1,73 +1,37 @@
import textwrap
import dialog
import zope.interface
from letsencrypt.client import interfaces
WIDTH = 72
HEIGHT = 20
class SingletonD(object):
_instance = None
class NcursesDisplay(object):
zope.interface.implements(interfaces.IDisplay)
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(SingletonD, cls).__new__(
cls, *args, **kwargs)
return cls._instance
class Display(SingletonD):
"""Generic display."""
def generic_notification(self, message, width=WIDTH, height=HEIGHT):
raise NotImplementedError()
def generic_menu(self, message, choices, input_text="",
width=WIDTH, height=HEIGHT):
raise NotImplementedError()
def generic_input(self, message):
raise NotImplementedError()
def generic_yesno(self, message, yes_label="Yes", no_label="No"):
raise NotImplementedError()
def filter_names(self, names):
raise NotImplementedError()
def success_installation(self, domains):
raise NotImplementedError()
def display_certs(self, certs):
raise NotImplementedError()
def confirm_revocation(self, cert):
raise NotImplementedError()
def more_info_cert(self, cert):
raise NotImplementedError()
class NcursesDisplay(Display):
def __init__(self):
def __init__(self, width=WIDTH, height=HEIGHT):
super(NcursesDisplay, self).__init__()
self.dialog = dialog.Dialog()
self.width = width
self.height = height
def generic_notification(self, message, w=WIDTH, h=HEIGHT):
self.dialog.msgbox(message, width=w, height=h)
def generic_notification(self, message):
self.dialog.msgbox(message, width=self.width)
def generic_menu(self, message, choices, input_text="", width=WIDTH,
height=HEIGHT):
def generic_menu(self, message, choices, input_text=""):
# Can accept either tuples or just the actual choices
if choices and isinstance(choices[0], tuple):
code, selection = self.dialog.menu(
message, choices=choices, width=WIDTH, height=HEIGHT)
message, choices=choices, width=self.width, height=self.height)
return code, str(selection)
else:
choices = list(enumerate(choices, 1))
code, tag = self.dialog.menu(
message, choices=choices, width=WIDTH, height=HEIGHT)
message, choices=choices, width=self.width, height=self.height)
return code(int(tag) - 1)
@ -76,7 +40,7 @@ class NcursesDisplay(Display):
def generic_yesno(self, message, yes="Yes", no="No"):
return self.dialog.DIALOG_OK == self.dialog.yesno(
message, HEIGHT, WIDTH, yes_label=yes, no_label=no)
message, self.height, self.width, yes_label=yes, no_label=no)
def filter_names(self, names):
choices = [(n, "", 0) for n in names]
@ -88,12 +52,12 @@ class NcursesDisplay(Display):
def success_installation(self, domains):
self.dialog.msgbox(
"\nCongratulations! You have successfully enabled "
+ gen_https_names(domains) + "!", width=WIDTH)
+ gen_https_names(domains) + "!", width=self.width)
def display_certs(self, certs):
list_choices = [
(str(i+1), "%s | %s | %s" %
(str(c["cn"].ljust(WIDTH - 39)),
(str(c["cn"].ljust(self.width - 39)),
c["not_before"].strftime("%m-%d-%y"),
"Installed" if c["installed"] else ""))
for i, c in enumerate(certs)]
@ -102,7 +66,7 @@ class NcursesDisplay(Display):
"Which certificates would you like to revoke?",
choices=list_choices, help_button=True,
help_label="More Info", ok_label="Revoke",
width=WIDTH, height=HEIGHT)
width=self.width, height=self.height)
if not tag:
tag = -1
return code, (int(tag) - 1)
@ -113,35 +77,51 @@ class NcursesDisplay(Display):
text += cert_info_frame(cert)
text += "This action cannot be reversed!"
return self.dialog.DIALOG_OK == self.dialog.yesno(
text, width=WIDTH, height=HEIGHT)
text, width=self.width, height=self.height)
def more_info_cert(self, cert):
text = "Certificate Information:\n"
text += cert_info_frame(cert)
print text
self.dialog.msgbox(text, width=WIDTH, height=HEIGHT)
self.dialog.msgbox(text, width=self.width, height=self.height)
def redirect_by_default(self):
choices = [
("Easy", "Allow both HTTP and HTTPS access to these sites"),
("Secure", "Make all requests redirect to secure HTTPS access")]
result = self.generic_menu(
"Please choose whether HTTPS access is required or optional.",
choices, "Please enter the appropriate number")
if result[0] != OK:
return False
# different answer for each type of display
return str(result[1]) == "Secure" or result[1] == 1
class FileDisplay(Display):
class FileDisplay(object):
zope.interface.implements(interfaces.IDisplay)
def __init__(self, outfile):
super(FileDisplay, self).__init__()
self.outfile = outfile
def generic_notification(self, message, width=WIDTH, height=HEIGHT):
side_frame = '-' * (79)
def generic_notification(self, message):
side_frame = '-' * 79
wm = textwrap.fill(message, 80)
text = "\n%s\n%s\n%s\n" % (side_frame, wm, side_frame)
self.outfile.write(text)
raw_input("Press Enter to Continue")
def generic_menu(self, message, choices, input_text="",
width=WIDTH, height=HEIGHT):
def generic_menu(self, message, choices, input_text=""):
# Can take either tuples or single items in choices list
if choices and isinstance(choices[0], tuple):
choices = ["%s - %s" % (c[0], c[1]) for c in choices]
self.outfile.write("\n%s\n" % message)
side_frame = '-' * (79)
side_frame = '-' * 79
self.outfile.write("%s\n" % side_frame)
for i, choice in enumerate(choices, 1):
@ -202,7 +182,7 @@ class FileDisplay(Display):
else:
try:
selection = int(ans)
# TODO add check to make sure it is liess than max
# TODO add check to make sure it is less than max
if selection < 0:
self.outfile.write(e_msg)
continue
@ -214,7 +194,7 @@ class FileDisplay(Display):
return code, selection
def success_installation(self, domains):
s_f = '*' * (79)
s_f = '*' * 79
wm = textwrap.fill(("Congratulations! You have successfully " +
"enabled %s!") % gen_https_names(domains))
msg = "%s\n%s\n%s\n"
@ -232,41 +212,11 @@ class FileDisplay(Display):
self.outfile.write("\nCertificate Information:\n")
self.outfile.write(cert_info_frame(cert))
display = None
OK = "ok"
CANCEL = "cancel"
HELP = "help"
def set_display(display_inst):
global display
display = display_inst
def generic_notification(message, width=WIDTH, height=HEIGHT):
display.generic_notification(message, width, height)
def generic_menu(message, choices, input_text="", width=WIDTH, height=HEIGHT):
return display.generic_menu(message, choices, input_text, width, height)
def generic_input(message):
return display.generic_message(message)
def generic_yesno(message, yes_label="Yes", no_label="No"):
return display.generic_yesno(message, yes_label, no_label)
def filter_names(names):
return display.filter_names(names)
def display_certs(certs):
return display.display_certs(certs)
def cert_info_frame(cert):
text = "-" * (WIDTH - 4) + "\n"
text += cert_info_string(cert)
@ -303,33 +253,3 @@ def gen_https_names(domains):
result = result + "https://" + domains[len(domains)-1]
return result
def success_installation(domains):
return display.success_installation(domains)
def redirect_by_default():
choices = [
("Easy", "Allow both HTTP and HTTPS access to these sites"),
("Secure", "Make all requests redirect to secure HTTPS access")]
result = display.generic_menu("Please choose whether HTTPS access " +
"is required or optional.",
choices,
"Please enter the appropriate number",
width=WIDTH)
if result[0] != OK:
return False
# different answer for each type of display
return str(result[1]) == "Secure" or result[1] == 1
def confirm_revocation(cert):
return display.confirm_revocation(cert)
def more_info_cert(cert):
return display.more_info_cert(cert)

View file

@ -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):

View file

@ -1,12 +1,13 @@
import textwrap
import dialog
import zope.interface
from letsencrypt.client import challenge
from letsencrypt.client import interfaces
class InteractiveChallenge(challenge.Challenge):
"""Interactive challange.
class InteractiveChallenge(object):
"""Interactive challenge.
Interactive challenge displays the string sent by the CA formatted
to fit on the screen of the client. The Challenge also adds proper
@ -14,9 +15,12 @@ class InteractiveChallenge(challenge.Challenge):
process.
"""
zope.interface.implements(interfaces.IChallenge)
BOX_SIZE = 70
def __init__(self, string):
super(InteractiveChallenge, self).__init__()
self.string = string
def perform(self, quiet=True):

View file

@ -0,0 +1,174 @@
"""Let's Encrypt client interfaces."""
import zope.interface
# pylint: disable=no-self-argument,no-method-argument
class IAuthenticator(zope.interface.Interface):
"""Generic Let's Encrypt Authenticator.
Class represents all possible tools processes that have the
ability to perform challenges and attain a certificate.
"""
def get_chall_pref(domain):
"""Return list of challenge preferences.
:param str domain: Domain for which challenge preferences are sought.
:returns: list of strings with the most preferred challenges first.
: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():
"""Perform the challenge."""
def generate_response():
"""Generate response."""
def cleanup():
"""Cleanup."""
class IInstaller(zope.interface.Interface):
"""Generic Let's Encrypt Installer Interface.
Represents any server that an X509 certificate can be placed.
With a focus on HTTPS optimizations.
.. todo:: All optimizations should be of the form .enable("hsts")
This will make it general towards any optimization... we should also
define a function to glean what optimizations are available.
Perhaps with text that describes the optimizations...
"""
def get_all_names():
"""Returns all names that may be authenticated."""
def deploy_cert(vhost, cert, key, cert_chain=None):
"""Deploy certificate.
:param vhost
:param str cert: CSR
:param str key: Private key
"""
def choose_virtual_host(name):
"""Chooses a virtual host based on a given domain name."""
def enable_redirect(ssl_vhost):
"""Redirect all traffic to the given ssl_vhost (port 80 => 443)."""
def enable_hsts(ssl_vhost):
"""Enable HSTS on the given ssl_vhost."""
def enable_ocsp_stapling(ssl_vhost):
"""Enable OCSP stapling on given ssl_vhost."""
def get_all_certs_keys():
"""Retrieve all certs and keys set in configuration.
:returns: List of tuples with form [(cert, key, path)].
:rtype: list
"""
def enable_site(vhost):
"""Enable the site at the given vhost."""
def save(title=None, temporary=False):
"""Saves all changes to the configuration files.
Both title and temporary are needed because a save may be
intended to be permanent, but the save is not ready to be a full
checkpoint
:param str title: The title of the save. If a title is given, the
configuration will be saved as a new checkpoint and put in a
timestamped directory. `title` has no effect if temporary is true.
:param bool temporary: Indicates whether the changes made will
be quickly reversed in the future (challenges)
"""
def rollback_checkpoints(rollback=1):
"""Revert `rollback` number of configuration checkpoints."""
def display_checkpoints():
"""Display the saved configuration checkpoints."""
def config_test():
"""Make sure the configuration is valid."""
def restart():
"""Restart or refresh the server content."""
class IDisplay(zope.interface.Interface):
"""Generic display."""
def generic_notification(message):
pass
def generic_menu(message, choices, input_text=""):
pass
def generic_input(message):
pass
def generic_yesno(message, yes_label="Yes", no_label="No"):
pass
def filter_names(names):
pass
def success_installation(domains):
pass
def display_certs(certs):
pass
def confirm_revocation(cert):
pass
def more_info_cert(cert):
pass
def redirect_by_default():
pass
class IValidator(object):
"""Configuration validator."""
def redirect(name):
pass
def ocsp_stapling(name):
pass
def https(names):
pass
def hsts(name):
pass

View file

@ -10,8 +10,8 @@ from letsencrypt.client import errors
def make_or_verify_dir(directory, mode=0o755, uid=0):
"""Make sure directory exists with proper permissions.
:param str directory: Path to a directry.
:param int mode: Diretory mode.
:param str directory: Path to a directory.
:param int mode: Directory mode.
:param int uid: Directory owner.
:raises LetsEncryptClientError: if a directory already exists,
@ -28,7 +28,7 @@ def make_or_verify_dir(directory, mode=0o755, uid=0):
if exception.errno == errno.EEXIST:
if not check_permissions(directory, mode, uid):
raise errors.LetsEncryptClientError(
'%s exists and does not contain the proper '
'%s exists, but does not have the proper '
'permissions or owner' % directory)
else:
raise
@ -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]
@ -107,7 +107,7 @@ def jose_b64decode(data):
:returns: Decoded data.
:raises TypeError: if input is of incorrect type
:raises ValueError: if unput is unicode with non-ASCII characters
:raises ValueError: if input is unicode with non-ASCII characters
"""
if isinstance(data, unicode):

View file

@ -0,0 +1,122 @@
"""Network Module."""
import json
import logging
import sys
import time
import jsonschema
import requests
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.
:ivar str server: Certificate authority server
:ivar str server_url: Full URL of the CSR server
"""
def __init__(self, server):
self.server = server
self.server_url = "https://%s/acme/" % self.server
def send(self, msg):
"""Send ACME message to server.
:param dict msg: ACME message (JSON serializable).
:returns: Server response message.
:rtype: dict
:raises TypeError: if `msg` is not JSON serializable
:raises jsonschema.ValidationError: if not valid ACME message
:raises errors.LetsEncryptClientError: in case of connection error
or if response from server is not a valid ACME message.
"""
json_encoded = json.dumps(msg)
acme.acme_object_validate(json_encoded)
try:
response = requests.post(
self.server_url,
data=json_encoded,
headers={"Content-Type": "application/json"},
)
except requests.exceptions.RequestException as error:
raise errors.LetsEncryptClientError(
'Sending ACME message to server has failed: %s' % error)
try:
acme.acme_object_validate(response.content)
except ValueError:
raise errors.LetsEncryptClientError(
'Server did not send JSON serializable message')
except jsonschema.ValidationError as error:
raise errors.LetsEncryptClientError(
'Response from server is not a valid ACME message')
return response.json()
def send_and_receive_expected(self, msg, expected):
"""Send ACME message to server and return expected message.
:param dict msg: ACME message (JSON serializable).
:param str expected: Name of the expected response ACME message type.
:returns: ACME response message of expected type.
:rtype: dict
:raises errors.LetsEncryptClientError: An exception is thrown
"""
response = self.send(msg)
try:
return self.is_expected_msg(response, expected)
except: # TODO: too generic exception
raise errors.LetsEncryptClientError(
'Expected message (%s) not received' % expected)
def is_expected_msg(self, response, expected, delay=3, rounds=20):
"""Is response expected ACME message?
:param dict response: ACME response message from server.
:param str expected: Name of the expected response ACME message type.
:param int delay: Number of seconds to delay before next round
in case of ACME "defer" response message.
:param int rounds: Number of resend attempts in case of ACME "defer"
response message.
:returns: ACME response message from server.
:rtype: dict
:raises LetsEncryptClientError: if server sent ACME "error" message
"""
for _ in xrange(rounds):
if response["type"] == expected:
return response
elif response["type"] == "error":
logging.error(
"%s: %s - More Info: %s", response["error"],
response.get("message", ""), response.get("moreInfo", ""))
raise errors.LetsEncryptClientError(response["error"])
elif response["type"] == "defer":
logging.info("Waiting for %d seconds...", delay)
time.sleep(delay)
response = self.send(acme.status_request(response["token"]))
else:
logging.fatal("Received unexpected message")
logging.fatal("Expected: %s", expected)
logging.fatal("Received: %s", response)
sys.exit(33)
logging.error(
"Server has deferred past the max of %d seconds", rounds * delay)

View file

@ -1,203 +0,0 @@
from letsencrypt.client import CONFIG
from letsencrypt.client import augeas_configurator
# This might be helpful... but feel free to use whatever you want
# class VH(object):
# def __init__(self, filename_path, vh_path, vh_addrs, is_ssl, is_enabled):
# self.file = filename_path
# self.path = vh_path
# self.addrs = vh_addrs
# self.names = []
# self.ssl = is_ssl
# self.enabled = is_enabled
# def set_names(self, listOfNames):
# self.names = listOfNames
# def add_name(self, name):
# self.names.append(name)
class NginxConfigurator(augeas_configurator.AugeasConfigurator):
def __init__(self, server_root=CONFIG.SERVER_ROOT):
super(NginxConfigurator, self).__init__()
self.server_root = server_root
# See if any temporary changes need to be recovered
# This needs to occur before VH objects are setup...
# because this will change the underlying configuration and potential
# vhosts
self.recovery_routine()
# Check for errors in parsing files with Augeas
# TODO - insert nginx lens info here???
#self.check_parsing_errors("httpd.aug")
def deploy_cert(self, vhost, cert, key, cert_chain=None):
"""Deploy cert in nginx"""
def choose_virtual_host(self, name):
"""Chooses a virtual host based on the given domain name"""
def get_all_names(self):
"""Returns all names found in the nginx configuration"""
return set()
# Might be helpful... I know nothing about nginx lens
# def get_include_path(self, cur_dir, arg):
# """
# Converts an Apache Include directive argument into an Augeas
# searchable path
# Returns path string
# """
# # Sanity check argument - maybe
# # Question: what can the attacker do with control over this string
# # Effect parse file... maybe exploit unknown errors in Augeas
# # If the attacker can Include anything though... and this function
# # only operates on Apache real config data... then the attacker has
# # already won.
# # Perhaps it is better to simply check the permissions on all
# # included files?
# # check_config to validate apache config doesn't work because it
# # would create a race condition between the check and this input
# # TODO: Fix this
# # Check to make sure only expected characters are used, maybe remove
# # validChars = re.compile("[a-zA-Z0-9.*?_-/]*")
# # matchObj = validChars.match(arg)
# # if matchObj.group() != arg:
# # logging.error("Error: Invalid regexp characters in %s", arg)
# # return []
# # Standardize the include argument based on server root
# if not arg.startswith("/"):
# arg = cur_dir + arg
# # conf/ is a special variable for ServerRoot in Apache
# elif arg.startswith("conf/"):
# arg = self.server_root + arg[5:]
# # TODO: Test if Apache allows ../ or ~/ for Includes
# # Attempts to add a transform to the file if one does not already
# # exist
# self.parse_file(arg)
# # Argument represents an fnmatch regular expression, convert it
# # Split up the path and convert each into an Augeas accepted regex
# # then reassemble
# if "*" in arg or "?" in arg:
# postfix = ""
# splitArg = arg.split("/")
# for idx, split in enumerate(splitArg):
# # * and ? are the two special fnmatch characters
# if "*" in split or "?" in split:
# # Turn it into a augeas regex
# # TODO: Can this be an augeas glob instead of regex
# splitArg[idx] = ("* [label()=~regexp('%s')]" %
# self.fnmatch_to_re(split)
# # Reassemble the argument
# arg = "/".join(splitArg)
# # If the include is a directory, just return the directory as a file
# if arg.endswith("/"):
# return "/files" + arg[:len(arg)-1]
# return "/files"+arg
def enable_redirect(self, ssl_vhost):
"""
Adds Redirect directive to the port 80 equivalent of ssl_vhost
First the function attempts to find the vhost with equivalent
ip addresses that serves on non-ssl ports
The function then adds the directive
"""
return
def enable_ocsp_stapling(self, ssl_vhost):
return False
def enable_hsts(self, ssl_vhost):
return False
def get_all_certs_keys(self):
"""
Retrieve all certs and keys set in VirtualHosts on the Apache server
returns: list of tuples with form [(cert, key, path)]
"""
return None
# Probably helpful reference
# def get_file_path(self, vhost_path):
# """
# Takes in Augeas path and returns the file name
# """
# # Strip off /files
# avail_fp = vhost_path[6:]
# # This can be optimized...
# while True:
# # Cast both to lowercase to be case insensitive
# find_if = avail_fp.lower().find("/ifmodule")
# if find_if != -1:
# avail_fp = avail_fp[:find_if]
# continue
# find_vh = avail_fp.lower().find("/virtualhost")
# if find_vh != -1:
# avail_fp = avail_fp[:find_vh]
# continue
# break
# return avail_fp
def enable_site(self, vhost):
"""Enables an available site, Apache restart required"""
return False
# Might be a usefule reference
# def parse_file(self, file_path):
# """
# Checks to see if file_path is parsed by Augeas
# If file_path isn't parsed, the file is added and Augeas is reloaded
# """
# # Test if augeas included file for Httpd.lens
# # Note: This works for augeas globs, ie. *.conf
# incTest = self.aug.match(
# "/augeas/load/Httpd/incl [. ='" + file_path + "']")
# if not incTest:
# # Load up files
# #self.httpd_incl.append(file_path)
# #self.aug.add_transform(
# # "Httpd.lns", self.httpd_incl, None, self.httpd_excl)
# self.__add_httpd_transform(file_path)
# self.aug.load()
# Helpful reference?
# def verify_setup(self):
# """
# Make sure that files/directories are setup with appropriate
# permissions. Aim for defensive coding... make sure all input files
# have permissions of root
# """
# le_util.make_or_verify_dir(CONFIG.CONFIG_DIR, 0o755)
# le_util.make_or_verify_dir(CONFIG.WORK_DIR, 0o755)
# le_util.make_or_verify_dir(CONFIG.BACKUP_DIR, 0o755)
def restart(self, quiet=False):
"""Restarts nginx server"""
# May be of use?
# def __add_httpd_transform(self, incl):
# """
# This function will correctly add a transform to augeas
# The existing augeas.add_transform in python is broken
# """
# lastInclude = self.aug.match("/augeas/load/Httpd/incl [last()]")
# self.aug.insert(lastInclude[0], "incl", False)
# self.aug.set("/augeas/load/Httpd/incl[last()]", incl)
def config_test(self):
"""Check Configuration"""
return False
def main():
return
if __name__ == "__main__":
main()

View file

@ -8,19 +8,22 @@ import time
import dialog
import requests
import zope.interface
from letsencrypt.client import challenge
from letsencrypt.client import interfaces
class RecoveryContact(challenge.Challenge):
"""Recovery Contact Identitifier Validation Challange.
class RecoveryContact(object):
"""Recovery Contact Identifier Validation Challenge.
Based on draft-barnes-acme, section 6.3.
"""
zope.interface.implements(interfaces.IChallenge)
def __init__(self, activation_url="", success_url="", contact="",
poll_delay=3):
super(RecoveryContact, self).__init__()
self.token = ""
self.activation_url = activation_url
self.success_url = success_url

View file

@ -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))

View file

@ -1,34 +0,0 @@
"""Recovery Token Identifier Validation Challenge.
.. note:: This challenge has not been implemented into the project yet
"""
import display
from letsencrypt.client import challenge
class RecoveryToken(challenge.Challenge):
"""Recovery Token Identifier Validation Challenge.
Based on draft-barnes-acme, section 6.4.
"""
def __init__(self, configurator):
super(RecoveryToken, self).__init__(configurator)
self.token = ""
def perform(self, quiet=True):
cancel, self.token = display.generic_input(
"Please Input Recovery Token: ")
return cancel != 1
def cleanup(self):
pass
def generate_response(self):
return {
"type": "recoveryToken",
"token": self.token,
}

View file

@ -0,0 +1,140 @@
"""Revoker module to enable LE revocations."""
import csv
import logging
import os
import shutil
import M2Crypto
import zope.component
from letsencrypt.client import acme
from letsencrypt.client import CONFIG
from letsencrypt.client import crypto_util
from letsencrypt.client import display
from letsencrypt.client import interfaces
from letsencrypt.client import network
class Revoker(object):
"""A revocation class for LE."""
def __init__(self, server, installer):
self.network = network.Network(server)
self.installer = installer
def acme_revocation(self, cert):
"""Handle ACME "revocation" phase.
:param dict cert: TODO
:returns: ACME "revocation" message.
:rtype: dict
"""
cert_der = M2Crypto.X509.load_cert(cert["backup_cert_file"]).as_der()
with open(cert["backup_key_file"], 'rU') as backup_key_file:
key = backup_key_file.read()
revocation = self.network.send_and_receive_expected(
acme.revocation_request(cert_der, key), "revocation")
zope.component.getUtility(interfaces.IDisplay).generic_notification(
"You have successfully revoked the certificate for "
"%s" % cert["cn"])
self.remove_cert_key(cert)
self.list_certs_keys()
return revocation
def list_certs_keys(self):
"""List trusted Let's Encrypt certificates."""
list_file = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST")
certs = []
if not os.path.isfile(list_file):
logging.info(
"You don't have any certificates saved from letsencrypt")
return
c_sha1_vh = {}
for (cert, _, path) in self.installer.get_all_certs_keys():
try:
c_sha1_vh[M2Crypto.X509.load_cert(
cert).get_fingerprint(md='sha1')] = path
except:
continue
with open(list_file, 'rb') as csvfile:
csvreader = csv.reader(csvfile)
for row in csvreader:
cert = crypto_util.get_cert_info(row[1])
b_k = os.path.join(CONFIG.CERT_KEY_BACKUP,
os.path.basename(row[2]) + "_" + row[0])
b_c = os.path.join(CONFIG.CERT_KEY_BACKUP,
os.path.basename(row[1]) + "_" + row[0])
cert.update({
"orig_key_file": row[2],
"orig_cert_file": row[1],
"idx": int(row[0]),
"backup_key_file": b_k,
"backup_cert_file": b_c,
"installed": c_sha1_vh.get(cert["fingerprint"], ""),
})
certs.append(cert)
if certs:
self.choose_certs(certs)
else:
zope.component.getUtility(interfaces.IDisplay).generic_notification(
"There are not any trusted Let's Encrypt "
"certificates for this server.")
def choose_certs(self, certs):
"""Display choose certificates menu.
:param list certs: List of cert dicts.
"""
displayer = zope.component.getUtility(interfaces.IDisplay)
code, tag = displayer.display_certs(certs)
if code == display.OK:
cert = certs[tag]
if displayer.confirm_revocation(cert):
self.acme_revocation(cert)
else:
self.choose_certs(certs)
elif code == display.HELP:
cert = certs[tag]
displayer.more_info_cert(cert)
self.choose_certs(certs)
else:
exit(0)
# pylint: disable=no-self-use
def remove_cert_key(self, cert):
"""Remove certificate and key.
:param dict cert: Cert dict used throughout revocation
"""
list_file = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST")
list_file2 = os.path.join(CONFIG.CERT_KEY_BACKUP, "LIST.tmp")
with open(list_file, 'rb') as orgfile:
csvreader = csv.reader(orgfile)
with open(list_file2, 'wb') as newfile:
csvwriter = csv.writer(newfile)
for row in csvreader:
if not (row[0] == str(cert["idx"]) and
row[1] == cert["orig_cert_file"] and
row[2] == cert["orig_key_file"]):
csvwriter.writerow(row)
shutil.copy2(list_file2, list_file)
os.remove(list_file2)
os.remove(cert["backup_cert_file"])
os.remove(cert["backup_key_file"])

2
letsencrypt/client/setup.sh Normal file → Executable file
View file

@ -1,2 +1,2 @@
#!/usr/bin/sh
#!/bin/sh
cp options-ssl.conf /etc/letsencrypt/options-ssl.conf

View file

@ -1,4 +1,5 @@
"""Tests for letsencrypt.client.acme."""
import pkg_resources
import unittest
import jsonschema
@ -58,15 +59,8 @@ class MessageFactoriesTest(unittest.TestCase):
"""Tests for ACME message factories from letsencrypt.client.acme."""
def setUp(self):
self.privkey = """-----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBAKx1c7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79
vukFhN9HFoHZiUvOjm0c+pVE6K+EdE/twuUCAwEAAQJAMbrEnJCrQe8YqAbw1/Bn
elAzIamndfE3U8bTavf9sgFpS4HL83rhd6PDbvx81ucaJAT/5x048fM/nFl4fzAc
mQIhAOF/a9o3EIsDKEmUl+Z1OaOiUxDF3kqWSmALEsmvDhwXAiEAw8ljV5RO/rUp
Zu2YMDFq3MKpyyMgBIJ8CxmGRc6gCmMCIGRQzkcmhfqBrhOFwkmozrqIBRIKJIjj
8TRm2LXWZZ2DAiAqVO7PztdNpynugUy4jtbGKKjBrTSNBRGA7OHlUgm0dQIhALQq
6oGU29Vxlvt3k0vmiRKU4AVfLyNXIGtcWcNG46h/
-----END RSA PRIVATE KEY-----"""
self.privkey = pkg_resources.resource_string(
__name__, 'testdata/rsa256_key.pem')
self.nonce = '\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9'
self.b64nonce = '7Nbyb1lI6xPVI3Hg3aKSqQ'

View file

@ -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

View file

@ -0,0 +1 @@
"""Let's Encrypt Apache Tests"""

View file

@ -0,0 +1,93 @@
import os
import pkg_resources
import shutil
import tempfile
import mock
from letsencrypt.client import CONFIG
from letsencrypt.client.apache import configurator
from letsencrypt.client.apache import obj
def dir_setup(test_dir="debian_apache_2_4/two_vhost_80"):
"""Setup the directories necessary for the configurator."""
temp_dir = tempfile.mkdtemp("temp")
config_dir = tempfile.mkdtemp("config")
work_dir = tempfile.mkdtemp("work")
test_configs = pkg_resources.resource_filename(
"letsencrypt.client.tests", "testdata/%s" % test_dir)
shutil.copytree(
test_configs, os.path.join(temp_dir, test_dir), symlinks=True)
return temp_dir, config_dir, work_dir
def setup_apache_ssl_options(config_dir):
"""Move the ssl_options into position and return the path."""
option_path = os.path.join(config_dir, "options-ssl.conf")
temp_options = pkg_resources.resource_filename(
"letsencrypt.client.apache", os.path.basename(CONFIG.OPTIONS_SSL_CONF))
shutil.copyfile(
temp_options, option_path)
return option_path
def get_apache_configurator(
config_path, config_dir, work_dir, ssl_options, version=(2, 4, 7)):
"""Create an Apache Configurator with the specified options."""
backups = os.path.join(work_dir, "backups")
with mock.patch("letsencrypt.client.apache.configurator."
"subprocess.Popen") as mock_popen:
# This just states that the ssl module is already loaded
mock_popen().communicate.return_value = ("ssl_module", "")
config = configurator.ApacheConfigurator(
config_path,
{
"backup": backups,
"temp": os.path.join(work_dir, "temp_checkpoint"),
"progress": os.path.join(backups, "IN_PROGRESS"),
"config": config_dir,
"work": work_dir,
},
ssl_options,
version)
return config
def get_vh_truth(temp_dir, config_name):
"""Return the ground truth for the specified directory."""
if config_name == "debian_apache_2_4/two_vhost_80":
prefix = os.path.join(
temp_dir, config_name, "apache2/sites-available")
aug_pre = "/files" + prefix
vh_truth = [
obj.VirtualHost(
os.path.join(prefix, "encryption-example.conf"),
os.path.join(aug_pre, "encryption-example.conf/VirtualHost"),
set([obj.Addr.fromstring("*:80")]),
False, True, set(["encryption-example.demo"])),
obj.VirtualHost(
os.path.join(prefix, "default-ssl.conf"),
os.path.join(aug_pre, "default-ssl.conf/IfModule/VirtualHost"),
set([obj.Addr.fromstring("_default_:443")]), True, False),
obj.VirtualHost(
os.path.join(prefix, "000-default.conf"),
os.path.join(aug_pre, "000-default.conf/VirtualHost"),
set([obj.Addr.fromstring("*:80")]), False, True,
set(["ip-172-30-0-17"])),
obj.VirtualHost(
os.path.join(prefix, "letsencrypt.conf"),
os.path.join(aug_pre, "letsencrypt.conf/VirtualHost"),
set([obj.Addr.fromstring("*:80")]), False, True,
set(["letsencrypt.demo"])),
]
return vh_truth
return None

View file

@ -0,0 +1,200 @@
"""Test for letsencrypt.client.apache.configurator."""
import os
import pkg_resources
import re
import shutil
import unittest
import mock
import zope.component
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.apache import config_util
class TwoVhost80Test(unittest.TestCase):
"""Test two standard well configured HTTP vhosts."""
def setUp(self):
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/")
self.config = config_util.get_apache_configurator(
self.config_path, self.config_dir, self.work_dir, self.ssl_options)
self.vh_truth = config_util.get_vh_truth(
self.temp_dir, "debian_apache_2_4/two_vhost_80")
def tearDown(self):
shutil.rmtree(self.temp_dir)
shutil.rmtree(self.config_dir)
shutil.rmtree(self.work_dir)
def test_get_all_names(self):
names = self.config.get_all_names()
self.assertEqual(names, set(
['letsencrypt.demo', 'encryption-example.demo', 'ip-172-30-0-17']))
def test_get_virtual_hosts(self):
vhs = self.config.get_virtual_hosts()
self.assertEqual(len(vhs), 4)
found = 0
for vhost in vhs:
for truth in self.vh_truth:
if vhost == truth:
found += 1
break
self.assertEqual(found, 4)
def test_is_site_enabled(self):
self.assertTrue(self.config.is_site_enabled(self.vh_truth[0].filep))
self.assertFalse(self.config.is_site_enabled(self.vh_truth[1].filep))
self.assertTrue(self.config.is_site_enabled(self.vh_truth[2].filep))
self.assertTrue(self.config.is_site_enabled(self.vh_truth[3].filep))
def test_deploy_cert(self):
self.config.deploy_cert(
self.vh_truth[1],
"example/cert.pem", "example/key.pem", "example/cert_chain.pem")
loc_cert = self.config.parser.find_dir(
parser.case_i("sslcertificatefile"),
re.escape("example/cert.pem"), self.vh_truth[1].path)
loc_key = self.config.parser.find_dir(
parser.case_i("sslcertificateKeyfile"),
re.escape("example/key.pem"), self.vh_truth[1].path)
loc_chain = self.config.parser.find_dir(
parser.case_i("SSLCertificateChainFile"),
re.escape("example/cert_chain.pem"), self.vh_truth[1].path)
# Verify one directive was found in the correct file
self.assertEqual(len(loc_cert), 1)
self.assertEqual(configurator.get_file_path(loc_cert[0]),
self.vh_truth[1].filep)
self.assertEqual(len(loc_key), 1)
self.assertEqual(configurator.get_file_path(loc_key[0]),
self.vh_truth[1].filep)
self.assertEqual(len(loc_chain), 1)
self.assertEqual(configurator.get_file_path(loc_chain[0]),
self.vh_truth[1].filep)
def test_is_name_vhost(self):
addr = obj.Addr.fromstring("*:80")
self.assertTrue(self.config.is_name_vhost(addr))
self.config.version = (2, 2)
self.assertFalse(self.config.is_name_vhost(addr))
def test_add_name_vhost(self):
self.config.add_name_vhost("*:443")
self.assertTrue(self.config.parser.find_dir(
"NameVirtualHost", re.escape("*:443")))
def test_make_vhost_ssl(self):
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
self.assertEqual(
ssl_vhost.filep,
os.path.join(self.config_path, "sites-available",
"encryption-example-le-ssl.conf"))
self.assertEqual(ssl_vhost.path,
"/files" + ssl_vhost.filep + "/IfModule/VirtualHost")
self.assertEqual(len(ssl_vhost.addrs), 1)
self.assertEqual(set([obj.Addr.fromstring("*:443")]), ssl_vhost.addrs)
self.assertEqual(ssl_vhost.names, set(["encryption-example.demo"]))
self.assertTrue(ssl_vhost.ssl)
self.assertFalse(ssl_vhost.enabled)
self.assertTrue(self.config.parser.find_dir(
"SSLCertificateFile", None, ssl_vhost.path))
self.assertTrue(self.config.parser.find_dir(
"SSLCertificateKeyFile", None, ssl_vhost.path))
self.assertTrue(self.config.parser.find_dir(
"Include", self.ssl_options, ssl_vhost.path))
self.assertEqual(self.config.is_name_vhost(self.vh_truth[0]),
self.config.is_name_vhost(ssl_vhost))
self.assertEqual(len(self.config.vhosts), 5)
@mock.patch("letsencrypt.client.apache.configurator."
"subprocess.Popen")
def test_get_version(self, mock_popen):
mock_popen().communicate.return_value = (
"Server Version: Apache/2.4.2 (Debian)", "")
self.assertEqual(self.config.get_version(), (2, 4, 2))
mock_popen().communicate.return_value = (
"Server Version: Apache/2 (Linux)", "")
self.assertEqual(self.config.get_version(), (2,))
mock_popen().communicate.return_value = (
"Server Version: Apache (Debian)", "")
self.assertRaises(
errors.LetsEncryptConfiguratorError, self.config.get_version)
mock_popen().communicate.return_value = (
"Server Version: Apache/2.3\n Apache/2.4.7", "")
self.assertRaises(
errors.LetsEncryptConfiguratorError, self.config.get_version)
mock_popen.side_effect = OSError("Can't find program")
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()

View file

@ -0,0 +1,151 @@
"""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
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.assertTrue(resp is 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)]))

View file

@ -0,0 +1,66 @@
"""Test the helper objects in apache.obj.py."""
import unittest
class AddrTest(unittest.TestCase):
"""Test the Addr class."""
def setUp(self):
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")
self.assertEqual(self.addr1.get_port(), "")
self.assertEqual(self.addr2.get_addr(), "192.168.1.1")
self.assertEqual(self.addr2.get_port(), "*")
self.assertEqual(self.addr3.get_addr(), "192.168.1.1")
self.assertEqual(self.addr3.get_port(), "80")
def test_str(self):
self.assertEqual(str(self.addr1), "192.168.1.1")
self.assertEqual(str(self.addr2), "192.168.1.1:*")
self.assertEqual(str(self.addr3), "192.168.1.1:80")
def test_get_addr_obj(self):
self.assertEqual(str(self.addr1.get_addr_obj("443")), "192.168.1.1:443")
self.assertEqual(str(self.addr2.get_addr_obj("")), "192.168.1.1")
self.assertEqual(str(self.addr1.get_addr_obj("*")), "192.168.1.1:*")
def test_eq(self):
self.assertEqual(self.addr1, self.addr2.get_addr_obj(""))
self.assertNotEqual(self.addr1, self.addr2)
# This is specifically designed to hit line 28 but coverage denies me
# the satisfaction :(
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 = Addr.fromstring("192.168.1.1")
addr2b = Addr.fromstring("192.168.1.1:*")
set_b = set([addr1b, addr2b])
self.assertEqual(set_a, set_b)
class VirtualHostTest(unittest.TestCase):
"""Test the VirtualHost class."""
def setUp(self):
from letsencrypt.client.apache.obj import VirtualHost
from letsencrypt.client.apache.obj import Addr
self.vhost1 = VirtualHost(
"filep", "vh_path",
set([Addr.fromstring("localhost")]), False, False)
def test_eq(self):
from letsencrypt.client.apache.obj import Addr
from letsencrypt.client.apache.obj import VirtualHost
vhost1b = VirtualHost(
"filep", "vh_path",
set([Addr.fromstring("localhost")]), False, False)
self.assertEqual(vhost1b, self.vhost1)
self.assertEqual(str(vhost1b), str(self.vhost1))
self.assertNotEqual(vhost1b, 1234)

View file

@ -0,0 +1,114 @@
"""Tests the ApacheParser class."""
import os
import shutil
import sys
import unittest
import augeas
import mock
import zope.component
from letsencrypt.client import display
from letsencrypt.client import errors
from letsencrypt.client.apache import parser
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))
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/")
self.parser = parser.ApacheParser(
augeas.Augeas(flags=augeas.Augeas.NONE),
self.config_path, self.ssl_options)
def tearDown(self):
shutil.rmtree(self.temp_dir)
shutil.rmtree(self.config_dir)
shutil.rmtree(self.work_dir)
def test_parse_file(self):
"""Test parse_file.
letsencrypt.conf is chosen as the test file as it will not be
included during the normal course of execution.
"""
file_path = os.path.join(
self.config_path, "sites-available", "letsencrypt.conf")
# pylint: disable=protected-access
self.parser._parse_file(file_path)
# search for the httpd incl
matches = self.parser.aug.match(
"/augeas/load/Httpd/incl [. ='%s']" % file_path)
self.assertTrue(matches)
def test_find_dir(self):
test = self.parser.find_dir(parser.case_i("Listen"), "443")
# This will only look in enabled hosts
test2 = self.parser.find_dir(
parser.case_i("documentroot"))
self.assertEqual(len(test), 2)
self.assertEqual(len(test2), 3)
def test_add_dir(self):
aug_default = "/files" + self.parser.loc["default"]
self.parser.add_dir(aug_default, "AddDirective", "test")
self.assertTrue(
self.parser.find_dir("AddDirective", "test", aug_default))
self.parser.add_dir(aug_default, "AddList", ["1", "2", "3", "4"])
matches = self.parser.find_dir("AddList", None, aug_default)
for i, match in enumerate(matches):
self.assertEqual(self.parser.aug.get(match), str(i + 1))
def test_add_dir_to_ifmodssl(self):
"""test add_dir_to_ifmodssl.
Path must be valid before attempting to add to augeas
"""
self.parser.add_dir_to_ifmodssl(
parser.get_aug_path(self.parser.loc["default"]),
"FakeDirective", "123")
matches = self.parser.find_dir("FakeDirective", "123")
self.assertEqual(len(matches), 1)
self.assertTrue("IfModule" in matches[0])
def test_get_aug_path(self):
self.assertEqual(
"/files/etc/apache", parser.get_aug_path("/etc/apache"))
def test_set_locations(self):
with mock.patch("letsencrypt.client.apache.parser."
"os.path") as mock_path:
mock_path.isfile.return_value = False
# pylint: disable=protected-access
self.assertRaises(errors.LetsEncryptConfiguratorError,
self.parser._set_locations, self.ssl_options)
mock_path.isfile.side_effect = [True, False, False]
# pylint: disable=protected-access
results = self.parser._set_locations(self.ssl_options)
self.assertEqual(results["default"], results["listen"])
self.assertEqual(results["default"], results["name"])

View file

@ -1,252 +0,0 @@
"""Test for letsencrypt.client.apache_configurator."""
import os
import pkg_resources
import re
import shutil
import sys
import tempfile
import unittest
import mock
from letsencrypt.client import apache_configurator
from letsencrypt.client import CONFIG
from letsencrypt.client import display
from letsencrypt.client import errors
UBUNTU_CONFIGS = pkg_resources.resource_filename(
__name__, "testdata/debian_apache_2_4")
class TwoVhost80Test(unittest.TestCase):
"""Test two standard well configured HTTP vhosts."""
def setUp(self):
display.set_display(display.NcursesDisplay())
self.temp_dir = os.path.join(
tempfile.mkdtemp("temp"), "debian_apache_2_4")
self.config_dir = tempfile.mkdtemp("config")
self.work_dir = tempfile.mkdtemp("work")
shutil.copytree(UBUNTU_CONFIGS, self.temp_dir, symlinks=True)
temp_options = pkg_resources.resource_filename(
"letsencrypt.client", os.path.basename(CONFIG.OPTIONS_SSL_CONF))
shutil.copyfile(
temp_options, os.path.join(self.config_dir, "options-ssl.conf"))
# Final slash is currently important
self.config_path = os.path.join(self.temp_dir, "two_vhost_80/apache2/")
self.ssl_options = os.path.join(self.config_dir, "options-ssl.conf")
backups = os.path.join(self.work_dir, "backups")
with mock.patch("letsencrypt.client.apache_configurator."
"subprocess.Popen") as mock_popen:
# This just states that the ssl module is already loaded
mock_popen().communicate.return_value = ("ssl_module", "")
self.config = apache_configurator.ApacheConfigurator(
self.config_path,
{
"backup": backups,
"temp": os.path.join(self.work_dir, "temp_checkpoint"),
"progress": os.path.join(backups, "IN_PROGRESS"),
"config": self.config_dir,
"work": self.work_dir,
},
self.ssl_options,
(2, 4, 7))
prefix = os.path.join(
self.temp_dir, "two_vhost_80/apache2/sites-available")
aug_pre = "/files" + prefix
self.vh_truth = [
apache_configurator.VH(
os.path.join(prefix, "encryption-example.conf"),
os.path.join(aug_pre, "encryption-example.conf/VirtualHost"),
["*:80"], False, True, ["encryption-example.demo"]),
apache_configurator.VH(
os.path.join(prefix, "default-ssl.conf"),
os.path.join(aug_pre, "default-ssl.conf/IfModule/VirtualHost"),
["_default_:443"], True, False),
apache_configurator.VH(
os.path.join(prefix, "000-default.conf"),
os.path.join(aug_pre, "000-default.conf/VirtualHost"),
["*:80"], False, True, ["ip-172-30-0-17"]),
apache_configurator.VH(
os.path.join(prefix, "letsencrypt.conf"),
os.path.join(aug_pre, "letsencrypt.conf/VirtualHost"),
["*:80"], False, True, ["letsencrypt.demo"]),
]
def tearDown(self):
shutil.rmtree(self.temp_dir)
shutil.rmtree(self.config_dir)
shutil.rmtree(self.work_dir)
def test_parse_file(self):
"""Test parse_file.
letsencrypt.conf is chosen as the test file as it will not be
included during the normal course of execution.
"""
file_path = os.path.join(
self.config_path, "sites-available", "letsencrypt.conf")
self.config._parse_file(file_path) # pylint: disable=protected-access
# search for the httpd incl
matches = self.config.aug.match(
"/augeas/load/Httpd/incl [. ='%s']" % file_path)
self.assertTrue(matches)
def test_get_all_names(self):
names = self.config.get_all_names()
self.assertEqual(set(names), set(
['letsencrypt.demo', 'encryption-example.demo', 'ip-172-30-0-17']))
def test_find_directive(self):
test = self.config.find_directive(
apache_configurator.case_i("Listen"), "443")
# This will only look in enabled hosts
test2 = self.config.find_directive(
apache_configurator.case_i("documentroot"))
self.assertEqual(len(test), 2)
self.assertEqual(len(test2), 3)
def test_get_virtual_hosts(self):
vhs = self.config.get_virtual_hosts()
self.assertEqual(len(vhs), 4)
found = 0
for vhost in vhs:
for truth in self.vh_truth:
if vhost == truth:
found += 1
break
self.assertEqual(found, 4)
def test_is_site_enabled(self):
self.assertTrue(self.config.is_site_enabled(self.vh_truth[0].filep))
self.assertFalse(self.config.is_site_enabled(self.vh_truth[1].filep))
self.assertTrue(self.config.is_site_enabled(self.vh_truth[2].filep))
self.assertTrue(self.config.is_site_enabled(self.vh_truth[3].filep))
def test_add_dir(self):
aug_default = "/files" + self.config.location["default"]
self.config.add_dir(
aug_default, "AddDirective", "test")
self.assertTrue(
self.config.find_directive("AddDirective", "test", aug_default))
def test_deploy_cert(self):
self.config.deploy_cert(
self.vh_truth[1],
"example/cert.pem", "example/key.pem", "example/cert_chain.pem")
loc_cert = self.config.find_directive(
apache_configurator.case_i("sslcertificatefile"),
re.escape("example/cert.pem"), self.vh_truth[1].path)
loc_key = self.config.find_directive(
apache_configurator.case_i("sslcertificateKeyfile"),
re.escape("example/key.pem"), self.vh_truth[1].path)
loc_chain = self.config.find_directive(
apache_configurator.case_i("SSLCertificateChainFile"),
re.escape("example/cert_chain.pem"), self.vh_truth[1].path)
# Verify one directive was found in the correct file
self.assertEqual(len(loc_cert), 1)
self.assertEqual(apache_configurator.get_file_path(loc_cert[0]),
self.vh_truth[1].filep)
self.assertEqual(len(loc_key), 1)
self.assertEqual(apache_configurator.get_file_path(loc_key[0]),
self.vh_truth[1].filep)
self.assertEqual(len(loc_chain), 1)
self.assertEqual(apache_configurator.get_file_path(loc_chain[0]),
self.vh_truth[1].filep)
def test_is_name_vhost(self):
self.assertTrue(self.config.is_name_vhost("*:80"))
self.config.version = (2, 2)
self.assertFalse(self.config.is_name_vhost("*:80"))
def test_add_name_vhost(self):
self.config.add_name_vhost("*:443")
# self.config.save(temporary=True)
self.assertTrue(self.config.find_directive(
"NameVirtualHost", re.escape("*:443")))
def test_add_dir_to_ifmodssl(self):
"""test _add_dir_to_ifmodssl.
Path must be valid before attempting to add to augeas
"""
self.config._add_dir_to_ifmodssl( # pylint: disable=protected-access
"/files" + self.config.location["default"], "FakeDirective", "123")
matches = self.config.find_directive("FakeDirective", "123")
self.assertEqual(len(matches), 1)
self.assertTrue("IfModule" in matches[0])
def test_make_vhost_ssl(self):
ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])
self.assertEqual(
ssl_vhost.filep,
os.path.join(self.config_path, "sites-available",
"encryption-example-le-ssl.conf"))
self.assertEqual(ssl_vhost.path,
"/files" + ssl_vhost.filep + "/IfModule/VirtualHost")
self.assertEqual(ssl_vhost.addrs, ["*:443"])
self.assertEqual(ssl_vhost.names, ["encryption-example.demo"])
self.assertTrue(ssl_vhost.ssl)
self.assertFalse(ssl_vhost.enabled)
self.assertTrue(self.config.find_directive(
"SSLCertificateFile", None, ssl_vhost.path))
self.assertTrue(self.config.find_directive(
"SSLCertificateKeyFile", None, ssl_vhost.path))
self.assertTrue(self.config.find_directive(
"Include", self.ssl_options, ssl_vhost.path))
self.assertEqual(self.config.is_name_vhost(self.vh_truth[0]),
self.config.is_name_vhost(ssl_vhost))
self.assertEqual(len(self.config.vhosts), 5)
@mock.patch("letsencrypt.client.apache_configurator."
"subprocess.Popen")
def test_get_version(self, mock_popen):
mock_popen().communicate.return_value = (
"Server Version: Apache/2.4.2 (Debian)", "")
self.assertEqual(self.config.get_version(), (2, 4, 2))
mock_popen().communicate.return_value = (
"Server Version: Apache/2 (Linux)", "")
self.assertEqual(self.config.get_version(), (2,))
mock_popen().communicate.return_value = (
"Server Version: Apache (Debian)", "")
self.assertRaises(
errors.LetsEncryptConfiguratorError, self.config.get_version)
mock_popen().communicate.return_value = (
"Server Version: Apache/2.3\n Apache/2.4.7", "")
self.assertRaises(
errors.LetsEncryptConfiguratorError, self.config.get_version)
mock_popen.side_effect = OSError("Can't find program")
self.assertRaises(
errors.LetsEncryptConfiguratorError, self.config.get_version)
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,427 @@
"""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()

View file

@ -0,0 +1,65 @@
"""Tests for challenge_util."""
import os
import pkg_resources
import re
import unittest
import M2Crypto
import mock
from letsencrypt.client import challenge_util
from letsencrypt.client import client
from letsencrypt.client import CONFIG
from letsencrypt.client import le_util
# pylint: disable=too-few-public-methods
class DvsniGenCertTest(unittest.TestCase):
"""Tests for letsencrypt.client.challenge_util.dvsni_gen_cert."""
def test_standard(self):
"""Basic test for straightline code."""
# This is a helper function that can be used for handling
# open context managers more elegantly. It avoids dealing with
# __enter__ and __exit__ calls.
# http://www.voidspace.org.uk/python/mock/helpers.html#mock.mock_open
m_open = mock.mock_open()
with mock.patch("letsencrypt.client.challenge_util.open",
m_open, create=True):
domain = "example.com"
dvsni_r = "r_value"
r_b64 = le_util.jose_b64encode(dvsni_r)
pem = pkg_resources.resource_string(
__name__, os.path.join("testdata", "rsa256_key.pem"))
key = client.Client.Key("path", pem)
nonce = "12345ABCDE"
s_b64 = self._call("tmp.crt", domain, r_b64, nonce, key)
self.assertTrue(m_open.called)
self.assertEqual(m_open.call_args[0], ("tmp.crt", 'w'))
self.assertEqual(m_open().write.call_count, 1)
# pylint: disable=protected-access
ext = challenge_util._dvsni_gen_ext(
dvsni_r, le_util.jose_b64decode(s_b64))
self._standard_check_cert(
m_open().write.call_args[0][0], domain, nonce, ext)
def _standard_check_cert(self, pem, domain, nonce, ext):
"""Check the certificate fields."""
dns_regex = r"DNS:([^, $]*)"
cert = M2Crypto.X509.load_cert_string(pem)
self.assertEqual(
cert.get_subject().CN, nonce + CONFIG.INVALID_EXT)
sans = cert.get_ext("subjectAltName").get_value()
exp_sans = set([nonce + CONFIG.INVALID_EXT, domain, ext])
act_sans = set(re.findall(dns_regex, sans))
self.assertEqual(exp_sans, act_sans)
# pylint: disable= no-self-use
def _call(self, filepath, name, r_b64, nonce, key):
from letsencrypt.client.challenge_util import dvsni_gen_cert
return dvsni_gen_cert(filepath, name, r_b64, nonce, key)

View file

@ -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()

View file

@ -0,0 +1,178 @@
"""Tests for letsencrypt.client.crypto_util."""
import datetime
import os
import pkg_resources
import unittest
import M2Crypto
RSA256_KEY = pkg_resources.resource_string(__name__, 'testdata/rsa256_key.pem')
RSA512_KEY = pkg_resources.resource_string(__name__, 'testdata/rsa512_key.pem')
class CreateSigTest(unittest.TestCase):
"""Tests for letsencrypt.client.crypto_util.create_sig."""
def setUp(self):
self.nonce = '\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9'
self.b64nonce = '7Nbyb1lI6xPVI3Hg3aKSqQ'
self.signature = {
'nonce': self.b64nonce,
'alg': 'RS256',
'jwk': {
'kty': 'RSA',
'e': 'AQAB',
'n': 'rHVztFHtH92ucFJD_N_HW9AsdRsUuHUBBBDlHwNlRd3fp5'
'80rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3C5Q',
},
'sig': 'SUPYKucUnhlTt8_sMxLiigOYdf_wlOLXPI-o7aRLTsOquVjDd6r'
'AX9AFJHk-bCMQPJbSzXKjG6H1IWbvxjS2Ew',
}
def _call(self, *args, **kwargs):
from letsencrypt.client.crypto_util import create_sig
return create_sig(*args, **kwargs)
def test_it(self):
self.assertEqual(
self._call('message', RSA256_KEY, self.nonce), self.signature)
def test_random_nonce(self):
signature = self._call('message', RSA256_KEY)
signature.pop('sig')
signature.pop('nonce')
del self.signature['sig']
del self.signature['nonce']
self.assertEqual(signature, self.signature)
class ValidCSRTest(unittest.TestCase):
"""Tests for letsencrypt.client.crypto_util.valid_csr."""
def _call(self, csr):
from letsencrypt.client.crypto_util import valid_csr
return valid_csr(csr)
def _call_testdata(self, name):
return self._call(pkg_resources.resource_string(
__name__, os.path.join('testdata', name)))
def test_valid_pem_true(self):
self.assertTrue(self._call_testdata('csr.pem'))
def test_valid_pem_san_true(self):
self.assertTrue(self._call_testdata('csr-san.pem'))
def test_valid_der_false(self):
self.assertFalse(self._call_testdata('csr.der'))
def test_valid_der_san_false(self):
self.assertFalse(self._call_testdata('csr-san.der'))
def test_empty_false(self):
self.assertFalse(self._call(''))
def test_random_false(self):
self.assertFalse(self._call('foo bar'))
class CSRMatchesPubkeyTest(unittest.TestCase):
"""Tests for letsencrypt.client.crypto_util.csr_matches_pubkey."""
def _call_testdata(self, name, privkey):
from letsencrypt.client.crypto_util import csr_matches_pubkey
return csr_matches_pubkey(pkg_resources.resource_string(
__name__, os.path.join('testdata', name)), privkey)
def test_valid_true(self):
self.assertTrue(self._call_testdata('csr.pem', RSA256_KEY))
def test_invalid_false(self):
self.assertFalse(self._call_testdata('csr.pem', RSA512_KEY))
class MakeKeyTest(unittest.TestCase):
"""Tests for letsencrypt.client.crypto_util.make_key."""
def test_it(self):
from letsencrypt.client.crypto_util import make_key
M2Crypto.RSA.load_key_string(make_key(1024))
class ValidPrivkeyTest(unittest.TestCase):
"""Tests for letsencrypt.client.crypto_util.valid_privkey."""
def _call(self, privkey):
from letsencrypt.client.crypto_util import valid_privkey
return valid_privkey(privkey)
def test_valid_true(self):
self.assertTrue(self._call(RSA256_KEY))
def test_empty_false(self):
self.assertFalse(self._call(''))
def test_random_false(self):
self.assertFalse(self._call('foo bar'))
class MakeSSCertTest(unittest.TestCase):
"""Tests for letsencrypt.client.crypto_util.make_ss_cert."""
def test_it(self):
from letsencrypt.client.crypto_util import make_ss_cert
make_ss_cert(RSA256_KEY, ['example.com', 'www.example.com'])
class GetCertInfoTest(unittest.TestCase):
"""Tests for letsencrypt.client.crypto_util.get_cert_info."""
def setUp(self):
self.cert_info = {
'not_before': datetime.datetime(
2014, 12, 11, 22, 34, 45, tzinfo=M2Crypto.ASN1.UTC),
'not_after': datetime.datetime(
2014, 12, 18, 22, 34, 45, tzinfo=M2Crypto.ASN1.UTC),
'subject': 'C=US, ST=Michigan, L=Ann Arbor, O=University '
'of Michigan and the EFF, CN=example.com',
'cn': 'example.com',
'issuer': 'C=US, ST=Michigan, L=Ann Arbor, O=University '
'of Michigan and the EFF, CN=example.com',
'serial': 1337L,
'pub_key': 'RSA 512',
}
def _call(self, name):
from letsencrypt.client.crypto_util import get_cert_info
self.assertEqual(get_cert_info(pkg_resources.resource_filename(
__name__, os.path.join('testdata', name))), self.cert_info)
def test_single_domain(self):
self.cert_info.update({
'san': '',
'fingerprint': '9F8CE01450D288467C3326AC0457E351939C72E',
})
self._call('cert.pem')
def test_san(self):
self.cert_info.update({
'san': 'DNS:example.com, DNS:www.example.com',
'fingerprint': '62F7110431B8E8F55905DBE5592518F9634AC50A',
})
self._call('cert-san.pem')
class B64CertToPEMTest(unittest.TestCase):
"""Tests for letsencrypt.client.crypto_util.b64_cert_to_pem."""
def test_it(self):
from letsencrypt.client.crypto_util import b64_cert_to_pem
self.assertEqual(
b64_cert_to_pem(pkg_resources.resource_string(
__name__, 'testdata/cert.b64jose')),
pkg_resources.resource_string(__name__, 'testdata/cert.pem'))
if __name__ == '__main__':
unittest.main()

View file

@ -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)

View file

@ -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.assertTrue(response is None)
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICFjCCAcCgAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx
ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM
IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4
YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG
A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix
KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS
BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR
7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c
+pVE6K+EdE/twuUCAwEAAaM2MDQwCQYDVR0TBAIwADAnBgNVHREEIDAeggtleGFt
cGxlLmNvbYIPd3d3LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA0EASuvNKFTF
nTJsvnSXn52f4BMZJJ2id/kW7+r+FJRm+L20gKQ1aqq8d3e/lzRUrv5SMf1TAOe7
RDjyGMKy5ZgM2w==
-----END CERTIFICATE-----

View file

@ -0,0 +1 @@
MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR7R_drnBSQ_zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c-pVE6K-EdE_twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksllvr6zJepBH5fMndfk3XJp10jT6VE-14KNtjh02a56GoraAvJAT5_H67E8GvJ_ocNnB_o

View file

@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx
ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM
IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4
YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG
A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix
KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS
BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR
7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c
+pVE6K+EdE/twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksll
vr6zJepBH5fMndfk3XJp10jT6VE+14KNtjh02a56GoraAvJAT5/H67E8GvJ/ocNn
B/o=
-----END CERTIFICATE-----

Binary file not shown.

View file

@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw
EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy
c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG
9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f
p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN
AQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t
MA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy
tmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A==
-----END CERTIFICATE REQUEST-----

Binary file not shown.

View file

@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw
EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy
c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG
9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f
p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoCkwJwYJKoZIhvcN
AQkOMRowGDAWBgNVHREEDzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAANB
AHJH/O6BtC9aGzEVCMGOZ7z9iIRHWSzr9x/bOzn7hLwsbXPAgO1QxEwL+X+4g20G
n9XBE1N9W6HCIEut2d8wACg=
-----END CERTIFICATE REQUEST-----

View file

@ -1,2 +1,2 @@
sites-available/letsencrypt.conf, letencrypt.demo
sites-available/letsencrypt.conf, letsencrypt.demo
sites-available/encryption-example.conf, encryption-example.demo

View file

@ -0,0 +1,9 @@
-----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBAKx1c7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79
vukFhN9HFoHZiUvOjm0c+pVE6K+EdE/twuUCAwEAAQJAMbrEnJCrQe8YqAbw1/Bn
elAzIamndfE3U8bTavf9sgFpS4HL83rhd6PDbvx81ucaJAT/5x048fM/nFl4fzAc
mQIhAOF/a9o3EIsDKEmUl+Z1OaOiUxDF3kqWSmALEsmvDhwXAiEAw8ljV5RO/rUp
Zu2YMDFq3MKpyyMgBIJ8CxmGRc6gCmMCIGRQzkcmhfqBrhOFwkmozrqIBRIKJIjj
8TRm2LXWZZ2DAiAqVO7PztdNpynugUy4jtbGKKjBrTSNBRGA7OHlUgm0dQIhALQq
6oGU29Vxlvt3k0vmiRKU4AVfLyNXIGtcWcNG46h/
-----END RSA PRIVATE KEY-----

View file

@ -0,0 +1,9 @@
-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAPS2EXFRNza/qpXnnBHF/CcFQ543htV+7nLAmrLrmTNHtPXJmLlM
8SJDIzv/ceAFXL110VzxFfi81lpH5E5c0TMCAwEAAQJBALmppYQ/JVARjWBcsEm/
1/bXBJ127YLv4gQIY5baL4r6IdEE33OXMTTmD9wf+ajuq1eaH0htHkwhOvREu0sz
bskCIQD/Cg+xhEVLcwK3pFp3afPIhj1IPFiL3Uy/nqyMZ6O/RQIhAPWiDBofp7Cp
J4dGZs+hkRySq/IOeeRJlNK1Pq64nToXAiBZ7+te1100YSd5KT051SRB94zO13EG
SZESFduVW8rz3QIgK+tLiqg6TYYRQUi/PUTAM4GuKNuZw828RGiPyqHLywUCIQCd
pkZrNphL/y0D7HSbPIfZzD90M2V8tUjlK0BTqk1bHA==
-----END RSA PRIVATE KEY-----

View file

@ -1,14 +0,0 @@
class Validator(object):
"""Configuration validator."""
def redirect(self, name):
raise NotImplementedError()
def ocsp_stapling(self, name):
raise NotImplementedError()
def https(self, names):
raise NotImplementedError()
def hsts(self, name):
raise NotImplementedError()

View file

@ -5,11 +5,16 @@ import logging
import os
import sys
from letsencrypt.client import apache_configurator
import zope.component
from letsencrypt.client import CONFIG
from letsencrypt.client import client
from letsencrypt.client import display
from letsencrypt.client import interfaces
from letsencrypt.client import errors
from letsencrypt.client import log
from letsencrypt.client import revoker
from letsencrypt.client.apache import configurator
def main():
@ -62,9 +67,40 @@ def main():
# Set up logging
logger = logging.getLogger()
logger.setLevel(logging.INFO) # TODO: --log
logger.setLevel(logging.INFO)
if args.use_curses:
logger.addHandler(log.DialogHandler())
displayer = display.NcursesDisplay()
else:
displayer = display.FileDisplay(sys.stdout)
zope.component.provideUtility(displayer)
installer = determine_installer()
server = CONFIG.ACME_SERVER if args.server is None else args.server
if args.revoke:
revoc = revoker.Revoker(server, installer)
revoc.list_certs_keys()
sys.exit()
if args.rollback > 0:
rollback(installer, args.rollback)
sys.exit()
if args.view_checkpoints:
view_checkpoints(installer)
sys.exit()
# Use the same object if possible
if interfaces.IAuthenticator.providedBy(installer):
auth = installer
else:
auth = determine_authenticator()
if not args.eula:
display_eula()
domains = choose_names(installer) if args.domains is None else args.domains
# Enforce '--privkey' is set along with '--csr'.
if args.csr and not args.privkey:
@ -72,36 +108,88 @@ def main():
"with the certificate signing request file (--csr)"
.format(os.linesep))
if args.use_curses:
display.set_display(display.NcursesDisplay())
else:
display.set_display(display.FileDisplay(sys.stdout))
if args.rollback > 0:
rollback(apache_configurator.ApacheConfigurator(), args.rollback)
sys.exit()
if args.view_checkpoints:
view_checkpoints(apache_configurator.ApacheConfigurator())
sys.exit()
server = args.server is None and CONFIG.ACME_SERVER or args.server
# Prepare for init of Client
if args.privkey is None:
privkey = client.Client.Key(None, None)
privkey = client.init_key()
else:
privkey = client.Client.Key(args.privkey[0], args.privkey[1])
if args.csr is None:
csr = client.Client.CSR(None, None, None)
csr = client.init_csr(privkey, domains)
else:
csr = client.Client.CSR(args.csr[0], args.csr[1], "pem")
csr = client.csr_pem_to_der(
client.Client.CSR(args.csr[0], args.csr[1], "pem"))
acme = client.Client(server, csr, privkey, args.use_curses)
if args.revoke:
acme.list_certs_keys()
acme = client.Client(server, domains, privkey, auth, installer)
# Validate the key and csr
client.validate_key_csr(privkey, csr)
cert_file, chain_file = acme.obtain_certificate(csr)
vhost = acme.deploy_certificate(privkey, cert_file, chain_file)
acme.optimize_config(vhost, args.redirect)
def display_eula():
"""Displays the end user agreement."""
with open('EULA') as eula_file:
if not zope.component.getUtility(interfaces.IDisplay).generic_yesno(
eula_file.read(), "Agree", "Cancel"):
sys.exit(0)
def choose_names(installer):
"""Display screen to select domains to validate.
:param installer: An installer object
:type installer: :class:`letsencrypt.client.interfaces.IInstaller`
"""
# This function adds all names
# found within the config to self.names
# Then filters them based on user selection
code, names = zope.component.getUtility(
interfaces.IDisplay).filter_names(get_all_names(installer))
if code == display.OK and names:
return names
else:
acme.authenticate(args.domains, args.eula, args.redirect)
sys.exit(0)
def get_all_names(installer):
"""Return all valid names in the configuration.
:param installer: An installer object
:type installer: :class:`letsencrypt.client.interfaces.IInstaller`
"""
names = list(installer.get_all_names())
client.sanity_check_names(names)
if not names:
logging.fatal("No domain names were found in your installation")
logging.fatal("Either specify which names you would like "
"letsencrypt to validate or add server names "
"to your virtual hosts")
sys.exit(1)
return names
# This should be controlled by commandline parameters
def determine_authenticator():
"""Returns a valid authenticator."""
try:
return configurator.ApacheConfigurator()
except errors.LetsEncryptConfiguratorError:
logging.info("Unable to find a way to authenticate.")
def determine_installer():
"""Returns a valid installer if one exists."""
try:
return configurator.ApacheConfigurator()
except errors.LetsEncryptConfiguratorError:
logging.info("Unable to find a way to install the certificate.")
def read_file(filename):
@ -116,7 +204,7 @@ def read_file(filename):
"""
try:
return filename, file(filename, 'rU').read()
return filename, open(filename, 'rU').read()
except IOError as exc:
raise argparse.ArgumentTypeError(exc.strerror)
@ -143,6 +231,5 @@ def view_checkpoints(config):
"""
config.display_checkpoints()
if __name__ == "__main__":
main()

View file

@ -11,6 +11,8 @@ install_requires = [
'python-augeas',
'python2-pythondialog',
'requests',
'zope.component',
'zope.interface',
]
docs_extras = [
@ -35,6 +37,8 @@ setup(
packages=[
'letsencrypt',
'letsencrypt.client',
'letsencrypt.client.apache',
'letsencrypt.client.tests',
'letsencrypt.scripts',
],
install_requires=install_requires,

View file

@ -14,7 +14,7 @@ commands =
[testenv:cover]
commands =
python setup.py dev
python setup.py nosetests --with-coverage --cover-min-percentage=45
python setup.py nosetests --with-coverage --cover-min-percentage=59
[testenv:lint]
commands =