mirror of
https://github.com/certbot/certbot.git
synced 2026-05-28 04:34:11 -04:00
updated pylintrc file to 1.3.1 version.
This commit is contained in:
commit
a0969b1f29
73 changed files with 5057 additions and 3459 deletions
58
.pylintrc
58
.pylintrc
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
29
docs/api/client/apache.rst
Normal file
29
docs/api/client/apache.rst
Normal 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:
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
:mod:`letsencrypt.client.apache_configurator`
|
||||
---------------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.apache_configurator
|
||||
:members:
|
||||
5
docs/api/client/auth_handler.rst
Normal file
5
docs/api/client/auth_handler.rst
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
:mod:`letsencrypt.client.auth_handler`
|
||||
--------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.auth_handler
|
||||
:members:
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
:mod:`letsencrypt.client.challenge`
|
||||
-----------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.challenge
|
||||
:members:
|
||||
5
docs/api/client/challenge_util.rst
Normal file
5
docs/api/client/challenge_util.rst
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
:mod:`letsencrypt.client.challenge_util`
|
||||
----------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.challenge_util
|
||||
:members:
|
||||
5
docs/api/client/client_authenticator.rst
Normal file
5
docs/api/client/client_authenticator.rst
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
:mod:`letsencrypt.client.client_authenticator`
|
||||
--------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.client_authenticator
|
||||
:members:
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
:mod:`letsencrypt.client.configurator`
|
||||
--------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.configurator
|
||||
:members:
|
||||
5
docs/api/client/interfaces.rst
Normal file
5
docs/api/client/interfaces.rst
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
:mod:`letsencrypt.client.interfaces`
|
||||
------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.interfaces
|
||||
:members:
|
||||
5
docs/api/client/network.rst
Normal file
5
docs/api/client/network.rst
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
:mod:`letsencrypt.client.network`
|
||||
---------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.network
|
||||
:members:
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
:mod:`letsencrypt.client.nginx_configurator`
|
||||
--------------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.nginx_configurator
|
||||
:members:
|
||||
5
docs/api/client/recovery_token.rst
Normal file
5
docs/api/client/recovery_token.rst
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
:mod:`letsencrypt.client.recovery_token`
|
||||
--------------------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.recovery_token
|
||||
:members:
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
:mod:`letsencrypt.client.recovery_token_challenge`
|
||||
--------------------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.recovery_token_challenge
|
||||
:members:
|
||||
5
docs/api/client/revoker.rst
Normal file
5
docs/api/client/revoker.rst
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
:mod:`letsencrypt.client.revoker`
|
||||
---------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.revoker
|
||||
:members:
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
:mod:`letsencrypt.client.validator`
|
||||
-----------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.client.validator
|
||||
:members:
|
||||
|
|
@ -30,9 +30,10 @@ IN_PROGRESS_DIR = os.path.join(BACKUP_DIR, "IN_PROGRESS/")
|
|||
"""Directory used before a permanent checkpoint is finalized"""
|
||||
|
||||
CERT_KEY_BACKUP = os.path.join(WORK_DIR, "keys-certs/")
|
||||
"""Directory where all certificates/keys are stored.
|
||||
"""Directory where all certificates/keys are stored. Used for easy revocation"""
|
||||
|
||||
Used for easy revocation"""
|
||||
REV_TOKENS_DIR = os.path.join(WORK_DIR, "revocation_tokens/")
|
||||
"""Directory where all revocation tokens are saved."""
|
||||
|
||||
KEY_DIR = os.path.join(SERVER_ROOT, "ssl/")
|
||||
"""Where all keys should be stored"""
|
||||
|
|
@ -47,9 +48,6 @@ OPTIONS_SSL_CONF = os.path.join(CONFIG_DIR, "options-ssl.conf")
|
|||
LE_VHOST_EXT = "-le-ssl.conf"
|
||||
"""Let's Encrypt SSL vhost configuration extension"""
|
||||
|
||||
APACHE_CHALLENGE_CONF = os.path.join(CONFIG_DIR, "le_dvsni_cert_challenge.conf")
|
||||
"""Temporary file for challenge virtual hosts"""
|
||||
|
||||
CERT_PATH = CERT_DIR + "cert-letsencrypt.pem"
|
||||
"""Let's Encrypt cert file."""
|
||||
|
||||
|
|
@ -59,15 +57,15 @@ CHAIN_PATH = CERT_DIR + "chain-letsencrypt.pem"
|
|||
INVALID_EXT = ".acme.invalid"
|
||||
"""Invalid Extension"""
|
||||
|
||||
# Challenge Information
|
||||
CHALLENGE_PREFERENCES = ["dvsni", "recoveryToken"]
|
||||
"""Challenge Preferences Dict for currently supported challenges"""
|
||||
|
||||
EXCLUSIVE_CHALLENGES = [frozenset(["dvsni", "simpleHttps"])]
|
||||
"""Mutually Exclusive Challenges - only solve 1"""
|
||||
|
||||
CONFIG_CHALLENGES = frozenset(["dvsni", "simpleHttps"])
|
||||
"""These are challenges that must be solved by a Configurator object"""
|
||||
DV_CHALLENGES = frozenset(["dvsni", "simpleHttps", "dns"])
|
||||
"""These are challenges that must be solved by an Authenticator object"""
|
||||
|
||||
CLIENT_CHALLENGES = frozenset(
|
||||
["recoveryToken", "recoveryContact", "proofOfPossession"])
|
||||
"""These are challenges that are handled by client.py"""
|
||||
|
||||
# Challenge Constants
|
||||
S_SIZE = 32
|
||||
|
|
|
|||
1
letsencrypt/client/apache/__init__.py
Normal file
1
letsencrypt/client/apache/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Let's Encrypt client.apache."""
|
||||
1109
letsencrypt/client/apache/configurator.py
Normal file
1109
letsencrypt/client/apache/configurator.py
Normal file
File diff suppressed because it is too large
Load diff
179
letsencrypt/client/apache/dvsni.py
Normal file
179
letsencrypt/client/apache/dvsni.py
Normal 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"
|
||||
91
letsencrypt/client/apache/obj.py
Normal file
91
letsencrypt/client/apache/obj.py
Normal 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
|
||||
401
letsencrypt/client/apache/parser.py
Normal file
401
letsencrypt/client/apache/parser.py
Normal 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
|
|
@ -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)
|
||||
|
|
|
|||
441
letsencrypt/client/auth_handler.py
Normal file
441
letsencrypt/client/auth_handler.py
Normal 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
|
||||
|
|
@ -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
|
||||
75
letsencrypt/client/challenge_util.py
Normal file
75
letsencrypt/client/challenge_util.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
49
letsencrypt/client/client_authenticator.py
Normal file
49
letsencrypt/client/client_authenticator.py
Normal 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")
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
174
letsencrypt/client/interfaces.py
Normal file
174
letsencrypt/client/interfaces.py
Normal 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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
122
letsencrypt/client/network.py
Normal file
122
letsencrypt/client/network.py
Normal 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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
79
letsencrypt/client/recovery_token.py
Normal file
79
letsencrypt/client/recovery_token.py
Normal 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))
|
||||
|
|
@ -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,
|
||||
}
|
||||
140
letsencrypt/client/revoker.py
Normal file
140
letsencrypt/client/revoker.py
Normal 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
2
letsencrypt/client/setup.sh
Normal file → Executable file
|
|
@ -1,2 +1,2 @@
|
|||
#!/usr/bin/sh
|
||||
#!/bin/sh
|
||||
cp options-ssl.conf /etc/letsencrypt/options-ssl.conf
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
112
letsencrypt/client/tests/acme_util.py
Normal file
112
letsencrypt/client/tests/acme_util.py
Normal 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
|
||||
1
letsencrypt/client/tests/apache/__init__.py
Normal file
1
letsencrypt/client/tests/apache/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Let's Encrypt Apache Tests"""
|
||||
93
letsencrypt/client/tests/apache/config_util.py
Normal file
93
letsencrypt/client/tests/apache/config_util.py
Normal 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
|
||||
200
letsencrypt/client/tests/apache/configurator_test.py
Normal file
200
letsencrypt/client/tests/apache/configurator_test.py
Normal 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()
|
||||
151
letsencrypt/client/tests/apache/dvsni_test.py
Normal file
151
letsencrypt/client/tests/apache/dvsni_test.py
Normal 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)]))
|
||||
66
letsencrypt/client/tests/apache/obj_test.py
Normal file
66
letsencrypt/client/tests/apache/obj_test.py
Normal 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)
|
||||
114
letsencrypt/client/tests/apache/parser_test.py
Normal file
114
letsencrypt/client/tests/apache/parser_test.py
Normal 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"])
|
||||
|
|
@ -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()
|
||||
427
letsencrypt/client/tests/auth_handler_test.py
Normal file
427
letsencrypt/client/tests/auth_handler_test.py
Normal 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()
|
||||
65
letsencrypt/client/tests/challenge_util_test.py
Normal file
65
letsencrypt/client/tests/challenge_util_test.py
Normal 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)
|
||||
80
letsencrypt/client/tests/client_authenticator_test.py
Normal file
80
letsencrypt/client/tests/client_authenticator_test.py
Normal 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()
|
||||
178
letsencrypt/client/tests/crypto_util_test.py
Normal file
178
letsencrypt/client/tests/crypto_util_test.py
Normal 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()
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
64
letsencrypt/client/tests/recovery_token_test.py
Normal file
64
letsencrypt/client/tests/recovery_token_test.py
Normal 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()
|
||||
14
letsencrypt/client/tests/testdata/cert-san.pem
vendored
Normal file
14
letsencrypt/client/tests/testdata/cert-san.pem
vendored
Normal 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-----
|
||||
1
letsencrypt/client/tests/testdata/cert.b64jose
vendored
Normal file
1
letsencrypt/client/tests/testdata/cert.b64jose
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR7R_drnBSQ_zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c-pVE6K-EdE_twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksllvr6zJepBH5fMndfk3XJp10jT6VE-14KNtjh02a56GoraAvJAT5_H67E8GvJ_ocNnB_o
|
||||
13
letsencrypt/client/tests/testdata/cert.pem
vendored
Normal file
13
letsencrypt/client/tests/testdata/cert.pem
vendored
Normal 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-----
|
||||
BIN
letsencrypt/client/tests/testdata/csr-san.der
vendored
Normal file
BIN
letsencrypt/client/tests/testdata/csr-san.der
vendored
Normal file
Binary file not shown.
10
letsencrypt/client/tests/testdata/csr-san.pem
vendored
Normal file
10
letsencrypt/client/tests/testdata/csr-san.pem
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw
|
||||
EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy
|
||||
c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG
|
||||
9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f
|
||||
p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN
|
||||
AQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t
|
||||
MA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy
|
||||
tmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A==
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
BIN
letsencrypt/client/tests/testdata/csr.der
vendored
Normal file
BIN
letsencrypt/client/tests/testdata/csr.der
vendored
Normal file
Binary file not shown.
10
letsencrypt/client/tests/testdata/csr.pem
vendored
Normal file
10
letsencrypt/client/tests/testdata/csr.pem
vendored
Normal 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-----
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
sites-available/letsencrypt.conf, letencrypt.demo
|
||||
sites-available/letsencrypt.conf, letsencrypt.demo
|
||||
sites-available/encryption-example.conf, encryption-example.demo
|
||||
|
|
|
|||
9
letsencrypt/client/tests/testdata/rsa256_key.pem
vendored
Normal file
9
letsencrypt/client/tests/testdata/rsa256_key.pem
vendored
Normal 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-----
|
||||
9
letsencrypt/client/tests/testdata/rsa512_key.pem
vendored
Normal file
9
letsencrypt/client/tests/testdata/rsa512_key.pem
vendored
Normal 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-----
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
4
setup.py
4
setup.py
|
|
@ -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,
|
||||
|
|
|
|||
2
tox.ini
2
tox.ini
|
|
@ -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 =
|
||||
|
|
|
|||
Loading…
Reference in a new issue