1
0
mirror of https://github.com/certbot/certbot.git synced 2026-01-23 07:20:55 +03:00

Merge branch 'master' into apache_failed_vhost

This commit is contained in:
James Kasten
2015-06-25 17:03:22 -07:00
28 changed files with 695 additions and 234 deletions

5
.gitignore vendored
View File

@@ -15,4 +15,7 @@ dist/
# editor temporary files
*~
*.swp
\#*#
\#*#
# auth --cert-path --chain-path
/*.pem

View File

@@ -3,19 +3,24 @@ language: python
# http://docs.travis-ci.com/user/ci-environment/#CI-environment-OS
before_install: travis_retry sudo ./bootstrap/ubuntu.sh
install: "travis_retry pip install tox coveralls"
script: "travis_retry tox"
after_success: '[ "$TOXENV" == "cover" ] && coveralls'
# using separate envs with different TOXENVs creates 4x1 Travis build
# matrix, which allows us to clearly distinguish which component under
# test has failed
env:
- TOXENV=py26
- TOXENV=py27
- TOXENV=lint
- TOXENV=cover
global:
- GOPATH=/tmp/go
matrix:
- TOXENV=py26
- TOXENV=py27
- TOXENV=lint
- TOXENV=cover
install: "travis_retry pip install tox coveralls"
before_script: '[ "${TOXENV:0:2}" != "py" ] || ./tests/boulder-start.sh'
# TODO: eliminate substring slice bashism
script: 'travis_retry tox && ([ "${TOXENV:0:2}" != "py" ] || (source .tox/$TOXENV/bin/activate && ./tests/boulder-integration.sh))'
after_success: '[ "$TOXENV" == "cover" ] && coveralls'
notifications:
email: false

View File

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

View File

@@ -27,3 +27,10 @@
.. automodule:: letsencrypt_apache.parser
:members:
:mod:`letsencrypt_apache.augeas_configurator`
=============================================
.. automodule:: letsencrypt_apache.augeas_configurator
:members:

3
examples/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# generate-csr.sh:
/key.pem
/csr.der

28
examples/generate-csr.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/sh
# This script generates a simple SAN CSR to be used with Let's Encrypt
# CA. Mostly intedened for "auth --csr" testing, but, since its easily
# auditable, feel free to adjust it and use on you production web
# server.
if [ "$#" -lt 1 ]
then
echo "Usage: $0 domain [domain...]" >&2
exit 1
fi
domains="DNS:$1"
shift
for x in "$@"
do
domains="$domains,DNS:$x"
done
SAN="$domains" openssl req -config "${OPENSSL_CNF:-openssl.cnf}" \
-new -nodes -subj '/' -reqexts san \
-out "${CSR_PATH:-csr.der}" \
-keyout key.pem \
-newkey rsa:2048 \
-outform DER
# 512 or 1024 too low for Boulder, 2048 is smallest for tests
echo "You can now run: letsencrypt auth --csr ${CSR_PATH:-csr.der}"

5
examples/openssl.cnf Normal file
View File

@@ -0,0 +1,5 @@
[ req ]
distinguished_name = req_distinguished_name
[ req_distinguished_name ]
[ san ]
subjectAltName=${ENV::SAN}

View File

@@ -3,12 +3,15 @@ import itertools
import logging
import time
import zope.component
from acme import challenges
from acme import messages
from letsencrypt import achallenges
from letsencrypt import constants
from letsencrypt import errors
from letsencrypt import interfaces
class AuthHandler(object):
@@ -193,6 +196,7 @@ class AuthHandler(object):
updated for _, updated in failed_achalls)
if all_failed_achalls:
_report_failed_challs(all_failed_achalls)
raise errors.FailedChallenges(all_failed_achalls)
dom_to_check -= comp_domains
@@ -480,3 +484,80 @@ def is_preferred(offered_challb, satisfied,
different=True):
return False
return True
_ERROR_HELP_COMMON = (
"To fix these errors, please make sure that your domain name was entered "
"correctly and the DNS A/AAAA record(s) for that domain contains the "
"right IP address.")
_ERROR_HELP = {
"connection" :
_ERROR_HELP_COMMON + " Additionally, please check that your computer "
"has publicly routable IP address and no firewalls are preventing the "
"server from communicating with the client.",
"dnssec" :
_ERROR_HELP_COMMON + " Additionally, if you have DNSSEC enabled for "
"your domain, please ensure the signature is valid.",
"malformed" :
"To fix these errors, please make sure that you did not provide any "
"invalid information to the client and try running Let's Encrypt "
"again.",
"serverInternal" :
"Unfortunately, an error on the ACME server prevented you from completing "
"authorization. Please try again later.",
"tls" :
_ERROR_HELP_COMMON + " Additionally, please check that you have an up "
"to date TLS configuration that allows the server to communicate with "
"the Let's Encrypt client.",
"unauthorized" : _ERROR_HELP_COMMON,
"unknownHost" : _ERROR_HELP_COMMON,}
def _report_failed_challs(failed_achalls):
"""Notifies the user about failed challenges.
:param set failed_achalls: A set of failed
:class:`letsencrypt.achallenges.AnnotatedChallenge`.
"""
problems = dict()
for achall in failed_achalls:
if achall.error:
problems.setdefault(achall.error.typ, []).append(achall)
reporter = zope.component.getUtility(interfaces.IReporter)
for achalls in problems.itervalues():
reporter.add_message(
_generate_failed_chall_msg(achalls), reporter.MEDIUM_PRIORITY, True)
def _generate_failed_chall_msg(failed_achalls):
"""Creates a user friendly error message about failed challenges.
:param list failed_achalls: A list of failed
:class:`letsencrypt.achallenges.AnnotatedChallenge` with the same error
type.
:returns: A formatted error message for the client.
:rtype: str
"""
typ = failed_achalls[0].error.typ
msg = [
"The following '{0}' errors were reported by the server:".format(typ)]
problems = dict()
for achall in failed_achalls:
problems.setdefault(achall.error.description, set()).add(achall.domain)
for problem in problems:
msg.append("\n\nDomains: ")
msg.append(", ".join(sorted(problems[problem])))
msg.append("\nError: {0}".format(problem))
if typ in _ERROR_HELP:
msg.append("\n\n")
msg.append(_ERROR_HELP[typ])
return "".join(msg)

View File

@@ -89,16 +89,20 @@ def _account_init(args, config):
return None
def _common_run(args, config, acc, authenticator, installer):
def _find_domains(args, installer):
if args.domains is None:
doms = display_ops.choose_names(installer)
domains = display_ops.choose_names(installer)
else:
doms = args.domains
domains = args.domains
if not doms:
if not domains:
sys.exit("Please specify --domains, or --installer that will "
"help in domain names autodiscovery")
return domains
def _init_acme(config, acc, authenticator, installer):
acme = client.Client(config, acc, authenticator, installer)
# Validate the key and csr
@@ -111,7 +115,7 @@ def _common_run(args, config, acc, authenticator, installer):
except errors.LetsEncryptClientError:
sys.exit("Unable to register an account with ACME server")
return acme, doms
return acme
def run(args, config, plugins):
@@ -139,19 +143,26 @@ def run(args, config, plugins):
if installer is None or authenticator is None:
return "Configurator could not be determined"
acme, doms = _common_run(args, config, acc, authenticator, installer)
# TODO: Handle errors from _common_run?
lineage = acme.obtain_and_enroll_certificate(doms, authenticator,
installer, plugins)
domains = _find_domains(args, installer)
# TODO: Handle errors from _init_acme?
acme = _init_acme(config, acc, authenticator, installer)
lineage = acme.obtain_and_enroll_certificate(
domains, authenticator, installer, plugins)
if not lineage:
return "Certificate could not be obtained"
acme.deploy_certificate(doms, lineage.privkey, lineage.cert, lineage.chain)
acme.enhance_config(doms, args.redirect)
acme.deploy_certificate(domains, lineage.privkey, lineage.cert, lineage.chain)
acme.enhance_config(domains, args.redirect)
def auth(args, config, plugins):
"""Authenticate & obtain cert, but do not install it."""
# XXX: Update for renewer / RenewableCert
if args.domains is not None and args.csr is not None:
# TODO: --csr could have a priority, when --domains is
# supplied, check if CSR matches given domains?
return "--domains and --csr are mutually exclusive"
acc = _account_init(args, config)
if acc is None:
return None
@@ -166,13 +177,18 @@ def auth(args, config, plugins):
else:
installer = None
# TODO: Handle errors from _common_run?
acme, doms = _common_run(
args, config, acc, authenticator=authenticator, installer=installer)
if not acme.obtain_and_enroll_certificate(
doms, authenticator, installer, plugins):
return "Certificate could not be obtained"
# TODO: Handle errors from _init_acme?
acme = _init_acme(config, acc, authenticator, installer)
if args.csr is not None:
certr, chain = acme.obtain_certificate_from_csr(le_util.CSR(
file=args.csr[0], data=args.csr[1], form="der"))
acme.save_certificate(certr, chain, args.cert_path, args.chain_path)
else:
domains = _find_domains(args, installer)
if not acme.obtain_and_enroll_certificate(
domains, authenticator, installer, plugins):
return "Certificate could not be obtained"
def install(args, config, plugins):
"""Install a previously obtained cert in a server."""
@@ -184,11 +200,11 @@ def install(args, config, plugins):
installer = display_ops.pick_installer(config, args.installer, plugins)
if installer is None:
return "Installer could not be determined"
acme, doms = _common_run(
args, config, acc, authenticator=None, installer=installer)
domains = _find_domains(args, installer)
acme = _init_acme(config, acc, authenticator=None, installer=installer)
assert args.cert_path is not None
acme.deploy_certificate(doms, acc.key.file, args.cert_path, args.chain_path)
acme.enhance_config(doms, args.redirect)
acme.deploy_certificate(domains, acc.key.file, args.cert_path, args.chain_path)
acme.enhance_config(domains, args.redirect)
def revoke(args, unused_config, unused_plugins):
@@ -243,10 +259,11 @@ def plugins_cmd(args, config, plugins): # TODO: Use IDiplay rathern than print
print str(available)
def read_file(filename):
"""Returns the given file's contents with universal new line support.
def read_file(filename, mode="rb"):
"""Returns the given file's contents.
:param str filename: Filename
:param str mode: open mode (see `open`)
:returns: A tuple of filename and its contents
:rtype: tuple
@@ -255,7 +272,7 @@ def read_file(filename):
"""
try:
return filename, open(filename, "rU").read()
return filename, open(filename, mode).read()
except IOError as exc:
raise argparse.ArgumentTypeError(exc.strerror)
@@ -466,12 +483,22 @@ def create_parser(plugins, args):
return subparser
add_subparser("run", run)
add_subparser("auth", auth)
parser_auth = add_subparser("auth", auth)
add_subparser("install", install)
parser_revoke = add_subparser("revoke", revoke)
parser_rollback = add_subparser("rollback", rollback)
add_subparser("config_changes", config_changes)
parser_auth.add_argument(
"--csr", type=read_file, help="Path to a Certificate Signing "
"Request (CSR) in DER format.")
parser_auth.add_argument(
"--cert-path", default=flag_default("cert_path"),
help="When using --csr this is where certificate is saved.")
parser_auth.add_argument(
"--chain-path", default=flag_default("chain_path"),
help="When using --csr this is where certificate chain is saved.")
parser_plugins = add_subparser("plugins", plugins_cmd)
parser_plugins.add_argument("--init", action="store_true")
parser_plugins.add_argument("--prepare", action="store_true")
@@ -539,10 +566,6 @@ def _paths_parser(helpful):
help=config_help("cert_dir"))
add("paths", "--le-vhost-ext", default="-le-ssl.conf",
help=config_help("le_vhost_ext"))
add("paths", "--cert-path", default=flag_default("cert_path"),
help=config_help("cert_path"))
add("paths", "--chain-path", default=flag_default("chain_path"),
help=config_help("chain_path"))
add("paths", "--renewer-config-file", default=flag_default("renewer_config_file"),
help=config_help("renewer_config_file"))
add("paths", "-s", "--server", default=flag_default("server"),

View File

@@ -4,6 +4,7 @@ import os
import pkg_resources
import M2Crypto
import OpenSSL.crypto
import zope.component
from acme import jose
@@ -11,6 +12,8 @@ from acme.jose import jwk
from letsencrypt import account
from letsencrypt import auth_handler
from letsencrypt import configuration
from letsencrypt import constants
from letsencrypt import continuity_auth
from letsencrypt import crypto_util
from letsencrypt import errors
@@ -123,22 +126,20 @@ class Client(object):
"{0}.".format(self.account.email))
reporter.add_message(recovery_msg, reporter.HIGH_PRIORITY, True)
def obtain_certificate(self, domains, csr=None):
"""Obtains a certificate from the ACME server.
def _obtain_certificate(self, domains, csr):
"""Obtain certificate.
:meth:`.register` must be called before :meth:`.obtain_certificate`
Internal function with precondition that `domains` are
consistent with identifiers present in the `csr`.
.. todo:: This function does not currently handle CSR correctly.
:param list domains: Domain names.
:param .le_util.CSR csr: DER-encoded Certificate Signing
Request. The key used to generate this CSR can be different
than `authkey`.
:param set domains: domains to get a certificate
:param csr: CSR must contain requested domains, the key used to generate
this CSR can be different than self.authkey
:type csr: :class:`CSR`
:returns: Certificate, private key, and certificate chain (all
PEM-encoded).
:rtype: `tuple` of `str`
:returns: `.CertificateResource` and certificate chain (as
returned by `.fetch_chain`).
:rtype: tuple
"""
if self.auth_handler is None:
@@ -149,37 +150,54 @@ class Client(object):
if self.account.regr is None:
raise errors.Error("Please register with the ACME server first.")
# Perform Challenges/Get Authorizations
logging.debug("CSR: %s, domains: %s", csr, domains)
authzr = self.auth_handler.get_authorizations(domains)
# Create CSR from names
cert_key = crypto_util.init_save_key(
self.config.rsa_key_size, self.config.key_dir)
csr = crypto_util.init_save_csr(
cert_key, domains, self.config.cert_dir)
# Retrieve certificate
certr = self.network.request_issuance(
jose.ComparableX509(
M2Crypto.X509.load_request_der_string(csr.data)),
authzr)
return certr, self.network.fetch_chain(certr)
cert_pem = certr.body.as_pem()
chain_pem = None
if certr.cert_chain_uri is not None:
chain_pem = self.network.fetch_chain(certr)
def obtain_certificate_from_csr(self, csr):
"""Obtain certficiate from CSR.
if chain_pem is None:
# XXX: just to stop RenewableCert from complaining; this is
# probably not a good solution
chain_pem = ""
else:
chain_pem = chain_pem.as_pem()
:param .le_util.CSR csr: DER-encoded Certificate Signing
Request.
return cert_pem, cert_key.pem, chain_pem
:returns: `.CertificateResource` and certificate chain (as
returned by `.fetch_chain`).
:rtype: tuple
"""
return self._obtain_certificate(
# TODO: add CN to domains?
crypto_util.get_sans_from_csr(
csr.data, OpenSSL.crypto.FILETYPE_ASN1), csr)
def obtain_certificate(self, domains):
"""Obtains a certificate from the ACME server.
`.register` must be called before `.obtain_certificate`
:param set domains: domains to get a certificate
:returns: `.CertificateResource`, certificate chain (as
returned by `.fetch_chain`), and newly generated private key
(`.le_util.Key`) and DER-encoded Certificate Signing Request
(`.le_util.CSR`).
:rtype: tuple
"""
# Create CSR from names
key = crypto_util.init_save_key(
self.config.rsa_key_size, self.config.key_dir)
csr = crypto_util.init_save_csr(key, domains, self.config.cert_dir)
return self._obtain_certificate(domains, csr) + (key, csr)
def obtain_and_enroll_certificate(
self, domains, authenticator, installer, plugins, csr=None):
self, domains, authenticator, installer, plugins):
"""Obtain and enroll certificate.
Get a new certificate for the specified domains using the specified
@@ -195,14 +213,14 @@ class Client(object):
:param plugins: A PluginsFactory object.
:param str csr: A preexisting CSR to use with this request.
:returns: A new :class:`letsencrypt.storage.RenewableCert` instance
referred to the enrolled cert lineage, or False if the cert could
not be obtained.
"""
cert, privkey, chain = self.obtain_certificate(domains, csr)
certr, chain, key, _ = self.obtain_certificate(domains)
# TODO: remove this dirty hack
self.config.namespace.authenticator = plugins.find_init(
authenticator).name
if installer is not None:
@@ -215,19 +233,29 @@ class Client(object):
# ideally should be a ConfigObj, but in this case a dict will be
# accepted in practice.)
params = vars(self.config.namespace)
config = {"renewer_config_file":
params["renewer_config_file"]} if "renewer_config_file" in params else None
renewable_cert = storage.RenewableCert.new_lineage(domains[0], cert, privkey,
chain, params, config)
self._report_renewal_status(renewable_cert)
return renewable_cert
config = {}
cli_config = configuration.RenewerConfiguration(self.config.namespace)
if (cli_config.config_dir != constants.CLI_DEFAULTS["config_dir"] or
cli_config.work_dir != constants.CLI_DEFAULTS["work_dir"]):
logging.warning(
"Non-standard path(s), might not work with crontab installed "
"by your operating system package manager")
# XXX: just to stop RenewableCert from complaining; this is
# probably not a good solution
chain_pem = "" if chain is None else chain.as_pem()
lineage = storage.RenewableCert.new_lineage(
domains[0], certr.body.as_pem(), key.pem, chain_pem, params,
config, cli_config)
self._report_renewal_status(lineage)
return lineage
def _report_renewal_status(self, cert):
# pylint: disable=no-self-use
"""Informs the user about automatic renewal and deployment.
:param cert: Newly issued certificate
:type cert: :class:`letsencrypt.storage.RenewableCert`
:param .RenewableCert cert: Newly issued certificate
"""
if ("autorenew" not in cert.configuration
@@ -246,17 +274,18 @@ class Client(object):
msg += ("been enabled for your certificate. These settings can be "
"configured in the directories under {0}.").format(
cert.configuration["renewal_configs_dir"])
cert.cli_config.renewal_configs_dir)
reporter = zope.component.getUtility(interfaces.IReporter)
reporter.add_message(msg, reporter.LOW_PRIORITY, True)
def save_certificate(self, certr, cert_path, chain_path):
def save_certificate(self, certr, chain_cert, cert_path, chain_path):
# pylint: disable=no-self-use
"""Saves the certificate received from the ACME server.
:param certr: ACME "certificate" resource.
:type certr: :class:`acme.messages.Certificate`
:param chain_cert:
:param str cert_path: Candidate path to a certificate.
:param str chain_path: Candidate path to a certificate chain.
@@ -266,6 +295,10 @@ class Client(object):
:raises IOError: If unable to find room to write the cert files
"""
for path in cert_path, chain_path:
le_util.make_or_verify_dir(
os.path.dirname(path), 0o755, os.geteuid())
# try finally close
cert_chain_abspath = None
cert_file, act_cert_path = le_util.unique_file(cert_path, 0o644)
@@ -278,22 +311,20 @@ class Client(object):
logging.info("Server issued certificate; certificate written to %s",
act_cert_path)
if certr.cert_chain_uri is not None:
if chain_cert is not None:
chain_file, act_chain_path = le_util.unique_file(
chain_path, 0o644)
# TODO: Except
chain_cert = self.network.fetch_chain(certr)
if chain_cert is not None:
chain_file, act_chain_path = le_util.unique_file(
chain_path, 0o644)
chain_pem = chain_cert.as_pem()
try:
chain_file.write(chain_pem)
finally:
chain_file.close()
chain_pem = chain_cert.as_pem()
try:
chain_file.write(chain_pem)
finally:
chain_file.close()
logging.info("Cert chain written to %s", act_chain_path)
logging.info("Cert chain written to %s", act_chain_path)
# This expects a valid chain file
cert_chain_abspath = os.path.abspath(act_chain_path)
# This expects a valid chain file
cert_chain_abspath = os.path.abspath(act_chain_path)
return os.path.abspath(act_cert_path), cert_chain_abspath
@@ -382,8 +413,7 @@ def validate_key_csr(privkey, csr=None):
:param privkey: Key associated with CSR
:type privkey: :class:`letsencrypt.le_util.Key`
:param csr: CSR
:type csr: :class:`letsencrypt.le_util.CSR`
:param .le_util.CSR csr: CSR
:raises .errors.Error: when validation fails

View File

@@ -59,15 +59,15 @@ class NamespaceConfig(object):
def backup_dir(self): # pylint: disable=missing-docstring
return os.path.join(self.namespace.work_dir, constants.BACKUP_DIR)
@property
def cert_dir(self): # pylint: disable=missing-docstring
return os.path.join(self.namespace.config_dir, constants.CERT_DIR)
@property
def cert_key_backup(self): # pylint: disable=missing-docstring
return os.path.join(self.namespace.work_dir,
constants.CERT_KEY_BACKUP_DIR, self.server_path)
@property
def cert_dir(self): # pylint: disable=missing-docstring
return os.path.join(self.namespace.config_dir, constants.CERT_DIR)
@property
def in_progress_dir(self): # pylint: disable=missing-docstring
return os.path.join(self.namespace.work_dir, constants.IN_PROGRESS_DIR)
@@ -81,12 +81,35 @@ class NamespaceConfig(object):
def rec_token_dir(self): # pylint: disable=missing-docstring
return os.path.join(self.namespace.work_dir, constants.REC_TOKEN_DIR)
@property
def renewer_config_file(self): # pylint: disable=missing-docstring
return os.path.join(
self.namespace.config_dir, constants.RENEWER_CONFIG_FILENAME)
@property
def temp_checkpoint_dir(self): # pylint: disable=missing-docstring
return os.path.join(
self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR)
class RenewerConfiguration(object):
"""Configuration wrapper for renewer."""
def __init__(self, namespace):
self.namespace = namespace
def __getattr__(self, name):
return getattr(self.namespace, name)
@property
def archive_dir(self): # pylint: disable=missing-docstring
return os.path.join(self.namespace.config_dir, constants.ARCHIVE_DIR)
@property
def live_dir(self): # pylint: disable=missing-docstring
return os.path.join(self.namespace.config_dir, constants.LIVE_DIR)
@property
def renewal_configs_dir(self): # pylint: disable=missing-docstring
return os.path.join(
self.namespace.config_dir, constants.RENEWAL_CONFIGS_DIR)
@property
def renewer_config_file(self): # pylint: disable=missing-docstring
return os.path.join(
self.namespace.config_dir, constants.RENEWER_CONFIG_FILENAME)

View File

@@ -17,23 +17,19 @@ CLI_DEFAULTS = dict(
work_dir="/var/lib/letsencrypt",
no_verify_ssl=False,
dvsni_port=challenges.DVSNI.PORT,
cert_path="./cert.pem",
chain_path="./chain.pem",
# TODO: blocked by #485, values ignored
backup_dir="not used",
key_dir="not used",
certs_dir="not used",
cert_path="not used",
chain_path="not used",
renewer_config_file="not used",
)
"""Defaults for CLI flags and `.IConfig` attributes."""
RENEWER_DEFAULTS = dict(
renewer_config_file="/etc/letsencrypt/renewer.conf",
renewal_configs_dir="/etc/letsencrypt/configs",
archive_dir="/etc/letsencrypt/archive",
live_dir="/etc/letsencrypt/live",
renewer_enabled="yes",
renew_before_expiry="30 days",
deploy_before_expiry="20 days",
@@ -58,6 +54,8 @@ List of expected options parameters:
"""
ARCHIVE_DIR = "archive"
"""Archive directory, relative to `IConfig.config_dir`."""
CONFIG_DIRS_MODE = 0o755
"""Directory mode for ``.IConfig.config_dir`` et al."""
@@ -71,13 +69,13 @@ ACCOUNT_KEYS_DIR = "keys"
BACKUP_DIR = "backups"
"""Directory (relative to `IConfig.work_dir`) where backups are kept."""
CERT_DIR = "certs"
"""See `.IConfig.cert_dir`."""
CERT_KEY_BACKUP_DIR = "keys-certs"
"""Directory where all certificates and keys are stored (relative to
`IConfig.work_dir`). Used for easy revocation."""
CERT_DIR = "certs"
"""See `.IConfig.cert_dir`."""
IN_PROGRESS_DIR = "IN_PROGRESS"
"""Directory used before a permanent checkpoint is finalized (relative to
`IConfig.work_dir`)."""
@@ -85,6 +83,9 @@ IN_PROGRESS_DIR = "IN_PROGRESS"
KEY_DIR = "keys"
"""Directory (relative to `IConfig.config_dir`) where keys are saved."""
LIVE_DIR = "live"
"""Live directory, relative to `IConfig.config_dir`."""
TEMP_CHECKPOINT_DIR = "temp_checkpoint"
"""Temporary checkpoint directory (relative to `IConfig.work_dir`)."""
@@ -92,6 +93,8 @@ REC_TOKEN_DIR = "recovery_tokens"
"""Directory where all recovery tokens are saved (relative to
`IConfig.work_dir`)."""
RENEWAL_CONFIGS_DIR = "configs"
"""Renewal configs directory, relative to `IConfig.config_dir`."""
RENEWER_CONFIG_FILENAME = "renewer.conf"
"""Renewer config file name (relative to `IConfig.config_dir`)."""

View File

@@ -90,6 +90,9 @@ def make_csr(key_str, domains):
:param str key_str: RSA key.
:param list domains: Domains included in the certificate.
.. todo:: Detect duplicates in `domains`? Using a set doesn't
preserve order...
:returns: new CSR in PEM and DER form containing all domains
:rtype: tuple
@@ -101,13 +104,7 @@ def make_csr(key_str, domains):
csr = M2Crypto.X509.Request()
csr.set_pubkey(pubkey)
name = csr.get_subject()
name.C = "US"
name.ST = "Michigan"
name.L = "Ann Arbor"
name.O = "EFF"
name.OU = "University of Michigan"
name.CN = domains[0]
# TODO: what to put into csr.get_subject()?
extstack = M2Crypto.X509.X509_Extension_Stack()
ext = M2Crypto.X509.new_extension(

View File

@@ -205,8 +205,22 @@ def success_installation(domains):
"""
util(interfaces.IDisplay).notification(
"Congratulations! You have successfully enabled "
"%s!" % _gen_https_names(domains), pause=False)
"Congratulations! You have successfully enabled {0}!{1}{1}"
"You should test your configuration at:{1}{2}".format(
_gen_https_names(domains),
os.linesep,
os.linesep.join(_gen_ssl_lab_urls(domains))),
height=(10 + len(domains)),
pause=False)
def _gen_ssl_lab_urls(domains):
"""Returns a list of urls.
:param list domains: Each domain is a 'str'
"""
return ["https://www.ssllabs.com/ssltest/analyze.html?d=%s" % dom for dom in domains]
def _gen_https_names(domains):

View File

@@ -190,8 +190,6 @@ class IConfig(zope.interface.Interface):
# TODO: the following are not used, but blocked by #485
le_vhost_ext = zope.interface.Attribute("not used")
cert_path = zope.interface.Attribute("not used")
chain_path = zope.interface.Attribute("not used")
class IInstaller(IPlugin):

View File

@@ -7,15 +7,21 @@ within lineages of successor certificates, according to configuration.
.. todo:: Call new installer API to restart servers after deployment
"""
import argparse
import os
import sys
import configobj
import zope.component
from letsencrypt import configuration
from letsencrypt import cli
from letsencrypt import client
from letsencrypt import crypto_util
from letsencrypt import notify
from letsencrypt import storage
from letsencrypt.display import util as display_util
from letsencrypt.plugins import disco as plugins_disco
@@ -60,6 +66,7 @@ def renew(cert, old_version):
# XXX: this loses type data (for example, the fact that key_size
# was an int, not a str)
config.rsa_key_size = int(config.rsa_key_size)
config.dvsni_port = int(config.dvsni_port)
try:
authenticator = plugins[renewalparams["authenticator"]]
except KeyError:
@@ -76,14 +83,15 @@ def renew(cert, old_version):
our_client = client.Client(config, account, authenticator, None)
with open(cert.version("cert", old_version)) as f:
sans = crypto_util.get_sans_from_cert(f.read())
new_cert, new_key, new_chain = our_client.obtain_certificate(sans)
if new_cert and new_key and new_chain:
new_certr, new_chain, new_key, _ = our_client.obtain_certificate(sans)
if new_chain is not None:
# XXX: Assumes that there was no key change. We need logic
# for figuring out whether there was or not. Probably
# best is to have obtain_certificate return None for
# new_key if the old key is to be used (since save_successor
# already understands this distinction!)
return cert.save_successor(old_version, new_cert, new_key, new_chain)
return cert.save_successor(old_version, new_certr.body.as_pem(),
new_key.pem, new_chain.as_pem())
# TODO: Notify results
else:
# TODO: Notify negative results
@@ -92,7 +100,23 @@ def renew(cert, old_version):
# (where fewer than all names were renewed)
def main(config=None):
def _paths_parser(parser):
add = parser.add_argument_group("paths").add_argument
add("--config-dir", default=cli.flag_default("config_dir"),
help=cli.config_help("config_dir"))
add("--work-dir", default=cli.flag_default("work_dir"),
help=cli.config_help("work_dir"))
return parser
def _create_parser():
parser = argparse.ArgumentParser()
#parser.add_argument("--cron", action="store_true", help="Run as cronjob.")
# pylint: disable=protected-access
return _paths_parser(parser)
def main(config=None, args=sys.argv[1:]):
"""Main function for autorenewer script."""
# TODO: Distinguish automated invocation from manual invocation,
# perhaps by looking at sys.argv[0] and inhibiting automated
@@ -100,6 +124,11 @@ def main(config=None):
# turned it off. (The boolean parameter should probably be
# called renewer_enabled.)
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
cli_config = configuration.RenewerConfiguration(
_create_parser().parse_args(args))
config = storage.config_with_defaults(config)
# Now attempt to read the renewer config file and augment or replace
# the renewer defaults with any options contained in that file. If
@@ -108,14 +137,17 @@ def main(config=None):
# elaborate renewer command line, we will presumably also be able to
# specify a config file on the command line, which, if provided, should
# take precedence over this one.
config.merge(configobj.ConfigObj(config.get("renewer_config_file", "")))
config.merge(configobj.ConfigObj(cli_config.renewer_config_file))
for i in os.listdir(config["renewal_configs_dir"]):
for i in os.listdir(cli_config.renewal_configs_dir):
print "Processing", i
if not i.endswith(".conf"):
continue
rc_config = configobj.ConfigObj(
os.path.join(config["renewal_configs_dir"], i))
rc_config = configobj.ConfigObj(cli_config.renewer_config_file)
rc_config.merge(configobj.ConfigObj(
os.path.join(cli_config.renewal_configs_dir, i)))
# TODO: this is a dirty hack!
rc_config.filename = os.path.join(cli_config.renewal_configs_dir, i)
try:
# TODO: Before trying to initialize the RenewableCert object,
# we could check here whether the combination of the config
@@ -125,7 +157,7 @@ def main(config=None):
# RenewableCert object for this cert at all, which could
# dramatically improve performance for large deployments
# where autorenewal is widely turned off.
cert = storage.RenewableCert(rc_config)
cert = storage.RenewableCert(rc_config, cli_config=cli_config)
except ValueError:
# This indicates an invalid renewal configuration file, such
# as one missing a required parameter (in the future, perhaps

View File

@@ -66,7 +66,8 @@ class Reporter(object):
If there is an unhandled exception, only messages for which
``on_crash`` is ``True`` are printed.
"""
"""
bold_on = False
if not self.messages.empty():
no_exception = sys.exc_info()[0] is None
@@ -74,14 +75,21 @@ class Reporter(object):
if bold_on:
print self._BOLD
print 'IMPORTANT NOTES:'
wrapper = textwrap.TextWrapper(initial_indent=' - ',
subsequent_indent=(' ' * 3))
first_wrapper = textwrap.TextWrapper(
initial_indent=' - ', subsequent_indent=(' ' * 3))
next_wrapper = textwrap.TextWrapper(
initial_indent=first_wrapper.subsequent_indent,
subsequent_indent=first_wrapper.subsequent_indent)
while not self.messages.empty():
msg = self.messages.get()
if no_exception or msg.on_crash:
if bold_on and msg.priority > self.HIGH_PRIORITY:
sys.stdout.write(self._RESET)
bold_on = False
print wrapper.fill(msg.text)
lines = msg.text.splitlines()
print first_wrapper.fill(lines[0])
if len(lines) > 1:
print "\n".join(
next_wrapper.fill(line) for line in lines[1:])
if bold_on:
sys.stdout.write(self._RESET)

View File

@@ -78,14 +78,17 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
renewal configuration file and/or systemwide defaults.
"""
def __init__(self, configfile, config_opts=None):
def __init__(self, configfile, config_opts=None, cli_config=None):
"""Instantiate a RenewableCert object from an existing lineage.
:param configobj.ConfigObj configfile: an already-parsed
ConfigObj object made from reading the renewal config file
that defines this lineage. :param configobj.ConfigObj
config_opts: systemwide defaults for renewal properties not
otherwise specified in the individual renewal config file.
ConfigObj object made from reading the renewal config file
that defines this lineage.
:param configobj.ConfigObj config_opts: systemwide defaults for
renewal properties not otherwise specified in the individual
renewal config file.
:param .RenewerConfiguration cli_config:
:raises ValueError: if the configuration file's name didn't end
in ".conf", or the file is missing or broken.
@@ -93,6 +96,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
ConfigObj object.
"""
self.cli_config = cli_config
if isinstance(configfile, configobj.ConfigObj):
if not os.path.basename(configfile.filename).endswith(".conf"):
raise ValueError("renewal config file name must end in .conf")
@@ -149,7 +153,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
# Each element's link must point within the cert lineage's
# directory within the official archive directory
desired_directory = os.path.join(
self.configuration["archive_dir"], self.lineagename)
self.cli_config.archive_dir, self.lineagename)
if not os.path.samefile(os.path.dirname(target),
desired_directory):
return False
@@ -499,7 +503,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
@classmethod
def new_lineage(cls, lineagename, cert, privkey, chain,
renewalparams=None, config=None):
renewalparams=None, config=None, cli_config=None):
# pylint: disable=too-many-locals,too-many-arguments
"""Create a new certificate lineage.
@@ -536,17 +540,15 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
# the renewer defaults with any options contained in that file. If
# renewer_config_file is undefined or if the file is nonexistent or
# empty, this .merge() will have no effect.
config.merge(configobj.ConfigObj(config.get("renewer_config_file", "")))
config.merge(configobj.ConfigObj(cli_config.renewer_config_file))
# Examine the configuration and find the new lineage's name
configs_dir = config["renewal_configs_dir"]
archive_dir = config["archive_dir"]
live_dir = config["live_dir"]
for i in (configs_dir, archive_dir, live_dir):
for i in (cli_config.renewal_configs_dir, cli_config.archive_dir,
cli_config.live_dir):
if not os.path.exists(i):
os.makedirs(i, 0700)
config_file, config_filename = le_util.unique_lineage_name(configs_dir,
lineagename)
config_file, config_filename = le_util.unique_lineage_name(
cli_config.renewal_configs_dir, lineagename)
if not config_filename.endswith(".conf"):
raise ValueError("renewal config file name must end in .conf")
@@ -554,8 +556,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
# lineagename will now potentially be modified based on which
# renewal configuration file could actually be created
lineagename = os.path.basename(config_filename)[:-len(".conf")]
archive = os.path.join(archive_dir, lineagename)
live_dir = os.path.join(live_dir, lineagename)
archive = os.path.join(cli_config.archive_dir, lineagename)
live_dir = os.path.join(cli_config.live_dir, lineagename)
if os.path.exists(archive):
raise ValueError("archive directory exists for " + lineagename)
if os.path.exists(live_dir):
@@ -593,7 +595,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
# TODO: add human-readable comments explaining other available
# parameters
new_config.write()
return cls(new_config, config)
return cls(new_config, config, cli_config)
def save_successor(self, prior_version, new_cert, new_privkey, new_chain):
@@ -624,7 +626,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
# Figure out what the new version is and hence where to save things
target_version = self.next_free_version()
archive = self.configuration["archive_dir"]
archive = self.cli_config.archive_dir
prefix = os.path.join(archive, self.lineagename)
target = dict(
[(kind,

View File

@@ -216,7 +216,8 @@ class PollChallengesTest(unittest.TestCase):
self.assertEqual(authzr.body.status, messages.STATUS_PENDING)
@mock.patch("letsencrypt.auth_handler.time")
def test_poll_challenges_failure(self, unused_mock_time):
@mock.patch("letsencrypt.auth_handler.zope.component.getUtility")
def test_poll_challenges_failure(self, unused_mock_time, unused_mock_zope):
self.mock_net.poll.side_effect = self._mock_poll_solve_one_invalid
self.assertRaises(
errors.AuthorizationError, self.handler._poll_challenges,
@@ -420,6 +421,54 @@ class IsPreferredTest(unittest.TestCase):
self._call(acme_util.DVSNI_P, frozenset([acme_util.DVSNI_P])))
class ReportFailedChallsTest(unittest.TestCase):
"""Tests for letsencrypt.auth_handler._report_failed_challs."""
# pylint: disable=protected-access
def setUp(self):
from letsencrypt import achallenges
kwargs = {
"chall" : acme_util.SIMPLE_HTTP,
"uri": "uri",
"status": messages.STATUS_INVALID,
"error": messages.Error(typ="tls", detail="detail"),
}
self.simple_http = achallenges.SimpleHTTP(
challb=messages.ChallengeBody(**kwargs),# pylint: disable=star-args
domain="example.com",
key=acme_util.KEY)
kwargs["chall"] = acme_util.DVSNI
self.dvsni_same = achallenges.DVSNI(
challb=messages.ChallengeBody(**kwargs),# pylint: disable=star-args
domain="example.com",
key=acme_util.KEY)
kwargs["error"] = messages.Error(typ="dnssec", detail="detail")
self.dvsni_diff = achallenges.DVSNI(
challb=messages.ChallengeBody(**kwargs),# pylint: disable=star-args
domain="foo.bar",
key=acme_util.KEY)
@mock.patch("letsencrypt.auth_handler.zope.component.getUtility")
def test_same_error_and_domain(self, mock_zope):
from letsencrypt import auth_handler
auth_handler._report_failed_challs([self.simple_http, self.dvsni_same])
call_list = mock_zope().add_message.call_args_list
self.assertTrue(len(call_list) == 1)
self.assertTrue("Domains: example.com\n" in call_list[0][0][0])
@mock.patch("letsencrypt.auth_handler.zope.component.getUtility")
def test_different_errors_and_domains(self, mock_zope):
from letsencrypt import auth_handler
auth_handler._report_failed_challs([self.simple_http, self.dvsni_diff])
self.assertTrue(mock_zope().add_message.call_count == 2)
def gen_auth_resp(chall_list):
"""Generate a dummy authorization response."""
return ["%s%s" % (chall.__class__.__name__, chall.domain)

View File

@@ -6,8 +6,11 @@ import shutil
import tempfile
import configobj
import M2Crypto.X509
import mock
from acme import jose
from letsencrypt import account
from letsencrypt import configuration
from letsencrypt import le_util
@@ -15,31 +18,74 @@ from letsencrypt import le_util
KEY = pkg_resources.resource_string(
__name__, os.path.join("testdata", "rsa512_key.pem"))
CSR_SAN = pkg_resources.resource_string(
__name__, os.path.join("testdata", "csr-san.der"))
class ClientTest(unittest.TestCase):
"""Tests for letsencrypt.client.Client."""
def setUp(self):
self.config = mock.MagicMock(no_verify_ssl=False)
self.config = mock.MagicMock(
no_verify_ssl=False, config_dir="/etc/letsencrypt")
# pylint: disable=star-args
self.account = mock.MagicMock(**{"key.pem": KEY})
from letsencrypt.client import Client
with mock.patch("letsencrypt.client.network") as network:
with mock.patch("letsencrypt.client.network.Network") as network:
self.client = Client(
config=self.config, account_=self.account, dv_auth=None,
installer=None)
config=self.config, account_=self.account,
dv_auth=None, installer=None)
self.network = network
def test_init_network_verify_ssl(self):
self.network.Network.assert_called_once_with(
self.network.assert_called_once_with(
mock.ANY, mock.ANY, verify_ssl=True)
def _mock_obtain_certificate(self):
self.client.auth_handler = mock.MagicMock()
self.network().request_issuance.return_value = mock.sentinel.certr
self.network().fetch_chain.return_value = mock.sentinel.chain
def _check_obtain_certificate(self):
self.client.auth_handler.get_authorizations.assert_called_once_with(
["example.com", "www.example.com"])
self.network.request_issuance.assert_callend_once_with(
jose.ComparableX509(
M2Crypto.X509.load_request_der_string(CSR_SAN)),
self.client.auth_handler.get_authorizations())
self.network().fetch_chain.assert_called_once_with(mock.sentinel.certr)
def test_obtain_certificate_from_csr(self):
self._mock_obtain_certificate()
self.assertEqual(
(mock.sentinel.certr, mock.sentinel.chain),
self.client.obtain_certificate_from_csr(le_util.CSR(
form="der", file=None, data=CSR_SAN)))
self._check_obtain_certificate()
@mock.patch("letsencrypt.client.crypto_util")
def test_obtain_certificate(self, mock_crypto_util):
self._mock_obtain_certificate()
csr = le_util.CSR(form="der", file=None, data=CSR_SAN)
mock_crypto_util.init_save_csr.return_value = csr
mock_crypto_util.init_save_key.return_value = mock.sentinel.key
domains = ["example.com", "www.example.com"]
self.assertEqual(
self.client.obtain_certificate(domains),
(mock.sentinel.certr, mock.sentinel.chain, mock.sentinel.key, csr))
mock_crypto_util.init_save_key.assert_called_once_with(
self.config.rsa_key_size, self.config.key_dir)
mock_crypto_util.init_save_csr.assert_called_once_with(
mock.sentinel.key, domains, self.config.cert_dir)
self._check_obtain_certificate()
@mock.patch("letsencrypt.client.zope.component.getUtility")
def test_report_new_account(self, mock_zope):
# pylint: disable=protected-access
self.config.config_dir = "/usr/bin/coffee"
self.account.recovery_token = "ECCENTRIC INVISIBILITY RHINOCEROS"
self.account.email = "rhino@jungle.io"
@@ -54,32 +100,33 @@ class ClientTest(unittest.TestCase):
# pylint: disable=protected-access
cert = mock.MagicMock()
cert.configuration = configobj.ConfigObj()
cert.configuration["renewal_configs_dir"] = "/etc/letsencrypt/configs"
cert.cli_config = configuration.RenewerConfiguration(self.config)
cert.configuration["autorenew"] = "True"
cert.configuration["autodeploy"] = "True"
self.client._report_renewal_status(cert)
msg = mock_zope().add_message.call_args[0][0]
self.assertTrue("renewal and deployment has been" in msg)
self.assertTrue(cert.configuration["renewal_configs_dir"] in msg)
self.assertTrue(cert.cli_config.renewal_configs_dir in msg)
cert.configuration["autorenew"] = "False"
self.client._report_renewal_status(cert)
msg = mock_zope().add_message.call_args[0][0]
self.assertTrue("deployment but not automatic renewal" in msg)
self.assertTrue(cert.configuration["renewal_configs_dir"] in msg)
self.assertTrue(cert.cli_config.renewal_configs_dir in msg)
cert.configuration["autodeploy"] = "False"
self.client._report_renewal_status(cert)
msg = mock_zope().add_message.call_args[0][0]
self.assertTrue("renewal and deployment has not" in msg)
self.assertTrue(cert.configuration["renewal_configs_dir"] in msg)
self.assertTrue(cert.cli_config.renewal_configs_dir in msg)
cert.configuration["autorenew"] = "True"
self.client._report_renewal_status(cert)
msg = mock_zope().add_message.call_args[0][0]
self.assertTrue("renewal but not automatic deployment" in msg)
self.assertTrue(cert.configuration["renewal_configs_dir"] in msg)
self.assertTrue(cert.cli_config.renewal_configs_dir in msg)
class DetermineAccountTest(unittest.TestCase):
"""Tests for letsencrypt.client.determine_authenticator."""

View File

@@ -9,10 +9,10 @@ class NamespaceConfigTest(unittest.TestCase):
"""Tests for letsencrypt.configuration.NamespaceConfig."""
def setUp(self):
from letsencrypt.configuration import NamespaceConfig
self.namespace = mock.MagicMock(
config_dir='/tmp/config', work_dir='/tmp/foo', foo='bar',
server='https://acme-server.org:443/new')
from letsencrypt.configuration import NamespaceConfig
self.config = NamespaceConfig(self.namespace)
def test_proxy_getattr(self):
@@ -38,7 +38,6 @@ class NamespaceConfigTest(unittest.TestCase):
constants.IN_PROGRESS_DIR = '../p'
constants.KEY_DIR = 'keys'
constants.REC_TOKEN_DIR = '/r'
constants.RENEWER_CONFIG_FILENAME = 'r.conf'
constants.TEMP_CHECKPOINT_DIR = 't'
self.assertEqual(
@@ -53,9 +52,30 @@ class NamespaceConfigTest(unittest.TestCase):
self.assertEqual(self.config.in_progress_dir, '/tmp/foo/../p')
self.assertEqual(self.config.key_dir, '/tmp/config/keys')
self.assertEqual(self.config.rec_token_dir, '/r')
self.assertEqual(self.config.renewer_config_file, '/tmp/config/r.conf')
self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t')
class RenewerConfigurationTest(unittest.TestCase):
"""Test for letsencrypt.configuration.RenewerConfiguration."""
def setUp(self):
self.namespace = mock.MagicMock(config_dir='/tmp/config')
from letsencrypt.configuration import RenewerConfiguration
self.config = RenewerConfiguration(self.namespace)
@mock.patch('letsencrypt.configuration.constants')
def test_dynamic_dirs(self, constants):
constants.ARCHIVE_DIR = "a"
constants.LIVE_DIR = 'l'
constants.RENEWAL_CONFIGS_DIR = "renewal_configs"
constants.RENEWER_CONFIG_FILENAME = 'r.conf'
self.assertEqual(self.config.archive_dir, '/tmp/config/a')
self.assertEqual(self.config.live_dir, '/tmp/config/l')
self.assertEqual(
self.config.renewal_configs_dir, '/tmp/config/renewal_configs')
self.assertEqual(self.config.renewer_config_file, '/tmp/config/r.conf')
if __name__ == '__main__':
unittest.main() # pragma: no cover

View File

@@ -182,6 +182,24 @@ class ChooseAccountTest(unittest.TestCase):
self.assertTrue(self._call([self.acc1, self.acc2]) is None)
class GenSSLLabURLs(unittest.TestCase):
"""Loose test of _gen_ssl_lab_urls. URL can change easily in the future."""
def setUp(self):
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
@classmethod
def _call(cls, domains):
from letsencrypt.display.ops import _gen_ssl_lab_urls
return _gen_ssl_lab_urls(domains)
def test_zero(self):
self.assertEqual(self._call([]), [])
def test_two(self):
urls = self._call(["eff.org", "umich.edu"])
self.assertTrue("eff.org" in urls[0])
self.assertTrue("umich.edu" in urls[1])
class GenHttpsNamesTest(unittest.TestCase):
"""Test _gen_https_names."""
def setUp(self):

View File

@@ -10,6 +10,7 @@ import configobj
import mock
import pytz
from letsencrypt import configuration
from letsencrypt.storage import ALL_FOUR
@@ -31,22 +32,24 @@ class RenewableCertTests(unittest.TestCase):
def setUp(self):
from letsencrypt import storage
self.tempdir = tempfile.mkdtemp()
self.cli_config = configuration.RenewerConfiguration(
namespace=mock.MagicMock(config_dir=self.tempdir))
# TODO: maybe provide RenewerConfiguration.make_dirs?
os.makedirs(os.path.join(self.tempdir, "live", "example.org"))
os.makedirs(os.path.join(self.tempdir, "archive", "example.org"))
os.makedirs(os.path.join(self.tempdir, "configs"))
defaults = configobj.ConfigObj()
defaults["live_dir"] = os.path.join(self.tempdir, "live")
defaults["archive_dir"] = os.path.join(self.tempdir, "archive")
defaults["renewal_configs_dir"] = os.path.join(self.tempdir,
"configs")
config = configobj.ConfigObj()
for kind in ALL_FOUR:
config[kind] = os.path.join(self.tempdir, "live", "example.org",
kind + ".pem")
config.filename = os.path.join(self.tempdir, "configs",
"example.org.conf")
self.defaults = defaults # for main() test
self.test_rc = storage.RenewableCert(config, defaults)
self.defaults = configobj.ConfigObj()
self.test_rc = storage.RenewableCert(
config, self.defaults, self.cli_config)
def tearDown(self):
shutil.rmtree(self.tempdir)
@@ -457,60 +460,57 @@ class RenewableCertTests(unittest.TestCase):
def test_new_lineage(self):
"""Test for new_lineage() class method."""
from letsencrypt import storage
config_dir = self.defaults["renewal_configs_dir"]
archive_dir = self.defaults["archive_dir"]
live_dir = self.defaults["live_dir"]
result = storage.RenewableCert.new_lineage("the-lineage.com", "cert",
"privkey", "chain", None,
self.defaults)
result = storage.RenewableCert.new_lineage(
"the-lineage.com", "cert", "privkey", "chain", None,
self.defaults, self.cli_config)
# This consistency check tests most relevant properties about the
# newly created cert lineage.
self.assertTrue(result.consistent())
self.assertTrue(os.path.exists(os.path.join(config_dir,
"the-lineage.com.conf")))
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.renewal_configs_dir, "the-lineage.com.conf")))
with open(result.fullchain) as f:
self.assertEqual(f.read(), "cert" + "chain")
# Let's do it again and make sure it makes a different lineage
result = storage.RenewableCert.new_lineage("the-lineage.com", "cert2",
"privkey2", "chain2", None,
self.defaults)
self.assertTrue(os.path.exists(
os.path.join(config_dir, "the-lineage.com-0001.conf")))
result = storage.RenewableCert.new_lineage(
"the-lineage.com", "cert2", "privkey2", "chain2", None,
self.defaults, self.cli_config)
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.renewal_configs_dir, "the-lineage.com-0001.conf")))
# Now trigger the detection of already existing files
os.mkdir(os.path.join(live_dir, "the-lineage.com-0002"))
os.mkdir(os.path.join(
self.cli_config.live_dir, "the-lineage.com-0002"))
self.assertRaises(ValueError, storage.RenewableCert.new_lineage,
"the-lineage.com", "cert3", "privkey3", "chain3",
None, self.defaults)
os.mkdir(os.path.join(archive_dir, "other-example.com"))
None, self.defaults, self.cli_config)
os.mkdir(os.path.join(self.cli_config.archive_dir, "other-example.com"))
self.assertRaises(ValueError, storage.RenewableCert.new_lineage,
"other-example.com", "cert4", "privkey4", "chain4",
None, self.defaults)
None, self.defaults, self.cli_config)
# Make sure it can accept renewal parameters
params = {"stuff": "properties of stuff", "great": "awesome"}
result = storage.RenewableCert.new_lineage("the-lineage.com", "cert2",
"privkey2", "chain2",
params, self.defaults)
result = storage.RenewableCert.new_lineage(
"the-lineage.com", "cert2", "privkey2", "chain2",
params, self.defaults, self.cli_config)
# TODO: Conceivably we could test that the renewal parameters actually
# got saved
def test_new_lineage_nonexistent_dirs(self):
"""Test that directories can be created if they don't exist."""
from letsencrypt import storage
config_dir = self.defaults["renewal_configs_dir"]
archive_dir = self.defaults["archive_dir"]
live_dir = self.defaults["live_dir"]
shutil.rmtree(config_dir)
shutil.rmtree(archive_dir)
shutil.rmtree(live_dir)
storage.RenewableCert.new_lineage("the-lineage.com", "cert2",
"privkey2", "chain2",
None, self.defaults)
shutil.rmtree(self.cli_config.renewal_configs_dir)
shutil.rmtree(self.cli_config.archive_dir)
shutil.rmtree(self.cli_config.live_dir)
storage.RenewableCert.new_lineage(
"the-lineage.com", "cert2", "privkey2", "chain2",
None, self.defaults, self.cli_config)
self.assertTrue(os.path.exists(
os.path.join(config_dir, "the-lineage.com.conf")))
self.assertTrue(os.path.exists(
os.path.join(live_dir, "the-lineage.com", "privkey.pem")))
self.assertTrue(os.path.exists(
os.path.join(archive_dir, "the-lineage.com", "privkey1.pem")))
os.path.join(
self.cli_config.renewal_configs_dir, "the-lineage.com.conf")))
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.live_dir, "the-lineage.com", "privkey.pem")))
self.assertTrue(os.path.exists(os.path.join(
self.cli_config.archive_dir, "the-lineage.com", "privkey1.pem")))
@mock.patch("letsencrypt.storage.le_util.unique_lineage_name")
def test_invalid_config_filename(self, mock_uln):
@@ -518,7 +518,7 @@ class RenewableCertTests(unittest.TestCase):
mock_uln.return_value = "this_does_not_end_with_dot_conf", "yikes"
self.assertRaises(ValueError, storage.RenewableCert.new_lineage,
"example.com", "cert", "privkey", "chain",
None, self.defaults)
None, self.defaults, self.cli_config)
def test_bad_kind(self):
self.assertRaises(ValueError, self.test_rc.current_target, "elephant")
@@ -574,19 +574,25 @@ class RenewableCertTests(unittest.TestCase):
self.test_rc.configfile["renewalparams"]["rsa_key_size"] = "2048"
self.test_rc.configfile["renewalparams"]["server"] = "acme.example.com"
self.test_rc.configfile["renewalparams"]["authenticator"] = "fake"
self.test_rc.configfile["renewalparams"]["dvsni_port"] = "4430"
mock_auth = mock.MagicMock()
mock_pd.PluginsRegistry.find_all.return_value = {"apache": mock_auth}
# Fails because "fake" != "apache"
self.assertFalse(renewer.renew(self.test_rc, 1))
self.test_rc.configfile["renewalparams"]["authenticator"] = "apache"
mock_client = mock.MagicMock()
mock_client.obtain_certificate.return_value = ("cert", "key", "chain")
# pylint: disable=star-args
mock_client.obtain_certificate.return_value = (
mock.Mock(**{'body.as_pem.return_value': 'cert'}),
mock.Mock(**{'as_pem.return_value': 'chain'}),
mock.Mock(pem="key"), mock.sentinel.csr)
mock_c.return_value = mock_client
self.assertEqual(2, renewer.renew(self.test_rc, 1))
# TODO: We could also make several assertions about calls that should
# have been made to the mock functions here.
self.assertEqual(mock_da.call_count, 1)
mock_client.obtain_certificate.return_value = (None, None, None)
mock_client.obtain_certificate.return_value = (
mock.sentinel.certr, None, mock.sentinel.key, mock.sentinel.csr)
# This should fail because the renewal itself appears to fail
self.assertFalse(renewer.renew(self.test_rc, 1))
@@ -602,22 +608,23 @@ class RenewableCertTests(unittest.TestCase):
mock_rc_instance.should_autorenew.return_value = True
mock_rc_instance.latest_common_version.return_value = 10
mock_rc.return_value = mock_rc_instance
with open(os.path.join(self.defaults["renewal_configs_dir"],
with open(os.path.join(self.cli_config.renewal_configs_dir,
"README"), "w") as f:
f.write("This is a README file to make sure that the renewer is")
f.write("able to correctly ignore files that don't end in .conf.")
with open(os.path.join(self.defaults["renewal_configs_dir"],
with open(os.path.join(self.cli_config.renewal_configs_dir,
"example.org.conf"), "w") as f:
# This isn't actually parsed in this test; we have a separate
# test_initialization that tests the initialization, assuming
# that configobj can correctly parse the config file.
f.write("cert = cert.pem\nprivkey = privkey.pem\n")
f.write("chain = chain.pem\nfullchain = fullchain.pem\n")
with open(os.path.join(self.defaults["renewal_configs_dir"],
with open(os.path.join(self.cli_config.renewal_configs_dir,
"example.com.conf"), "w") as f:
f.write("cert = cert.pem\nprivkey = privkey.pem\n")
f.write("chain = chain.pem\nfullchain = fullchain.pem\n")
renewer.main(self.defaults)
renewer.main(self.defaults, args=[
'--config-dir', self.cli_config.config_dir])
self.assertEqual(mock_rc.call_count, 2)
self.assertEqual(mock_rc_instance.update_all_links_to.call_count, 2)
self.assertEqual(mock_notify.notify.call_count, 4)
@@ -630,7 +637,8 @@ class RenewableCertTests(unittest.TestCase):
mock_happy_instance.should_autorenew.return_value = False
mock_happy_instance.latest_common_version.return_value = 10
mock_rc.return_value = mock_happy_instance
renewer.main(self.defaults)
renewer.main(self.defaults, args=[
'--config-dir', self.cli_config.config_dir])
self.assertEqual(mock_rc.call_count, 4)
self.assertEqual(mock_happy_instance.update_all_links_to.call_count, 0)
self.assertEqual(mock_notify.notify.call_count, 4)
@@ -638,10 +646,11 @@ class RenewableCertTests(unittest.TestCase):
def test_bad_config_file(self):
from letsencrypt import renewer
with open(os.path.join(self.defaults["renewal_configs_dir"],
with open(os.path.join(self.cli_config.renewal_configs_dir,
"bad.conf"), "w") as f:
f.write("incomplete = configfile\n")
renewer.main(self.defaults)
renewer.main(self.defaults, args=[
'--config-dir', self.cli_config.config_dir])
# The ValueError is caught inside and nothing happens.

View File

@@ -12,7 +12,6 @@ import zope.interface
from acme import challenges
from letsencrypt import achallenges
from letsencrypt import augeas_configurator
from letsencrypt import constants as core_constants
from letsencrypt import errors
from letsencrypt import interfaces
@@ -20,6 +19,7 @@ from letsencrypt import le_util
from letsencrypt.plugins import common
from letsencrypt_apache import augeas_configurator
from letsencrypt_apache import constants
from letsencrypt_apache import display_ops
from letsencrypt_apache import dvsni

View File

@@ -59,7 +59,6 @@ letsencrypt_install_requires = [
# https://pyopenssl.readthedocs.org/en/latest/api/crypto.html#OpenSSL.crypto.X509Req.get_extensions
'PyOpenSSL>=0.15',
'pyrfc3339',
'python-augeas',
'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280
'pytz',
'requests',

47
tests/boulder-integration.sh Executable file
View File

@@ -0,0 +1,47 @@
#!/bin/sh -xe
# Simple integration test, make sure to activate virtualenv beforehand
# (source venv/bin/activate) and that you are running Boulder test
# instance (see ./boulder-start.sh).
root="$(mktemp -d)"
echo "\nRoot integration tests directory: $root"
store_flags="--config-dir $root/conf --work-dir $root/work"
common() {
# first three flags required, rest is handy defaults
letsencrypt \
--server http://localhost:4000/acme/new-reg \
--no-verify-ssl \
--dvsni-port 5001 \
$store_flags \
--text \
--agree-eula \
--email "" \
--authenticator standalone \
-vvvvvvv "$@"
}
common --domains le.wtf auth
export CSR_PATH="${root}/csr.der" OPENSSL_CNF=examples/openssl.cnf
./examples/generate-csr.sh le.wtf
common auth --csr "$CSR_PATH" \
--cert-path "${root}/csr/cert.pem" \
--chain-path "${root}/csr/chain.pem"
openssl x509 -in "${root}/csr/0000_cert.pem" -text
openssl x509 -in "${root}/csr/0000_chain.pem" -text
# the following assumes that Boulder issues certificates for less than
# 10 years, otherwise renewal will not take place
cat <<EOF > "$root/conf/renewer.conf"
renew_before_expiry = 10 years
deploy_before_expiry = 10 years
EOF
letsencrypt-renewer $store_flags
dir="$root/conf/archive/le.wtf"
for x in cert chain fullchain privkey;
do
latest="$(ls -1t $dir/ | grep -e "^${x}" | head -n1)"
live="$(readlink -f "$root/conf/live/le.wtf/${x}.pem")"
#[ "${dir}/${latest}" = "$live" ] # renewer fails this test
done

15
tests/boulder-start.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/sh -xe
# Download and run Boulder instance for integration testing
export GOPATH="${GOPATH:-/tmp/go}"
# $ go get github.com/letsencrypt/boulder
# package github.com/letsencrypt/boulder
# imports github.com/letsencrypt/boulder
# imports github.com/letsencrypt/boulder: no buildable Go source files in /tmp/go/src/github.com/letsencrypt/boulder
go get -d github.com/letsencrypt/boulder/cmd/boulder
cd $GOPATH/src/github.com/letsencrypt/boulder
make -j4 # Travis has 2 cores per build instance.
./start.sh &
# Hopefully start.sh bootstraps before integration test is started...