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:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -15,4 +15,7 @@ dist/
|
||||
# editor temporary files
|
||||
*~
|
||||
*.swp
|
||||
\#*#
|
||||
\#*#
|
||||
|
||||
# auth --cert-path --chain-path
|
||||
/*.pem
|
||||
23
.travis.yml
23
.travis.yml
@@ -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
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
:mod:`letsencrypt.augeas_configurator`
|
||||
--------------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.augeas_configurator
|
||||
:members:
|
||||
@@ -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
3
examples/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# generate-csr.sh:
|
||||
/key.pem
|
||||
/csr.der
|
||||
28
examples/generate-csr.sh
Executable file
28
examples/generate-csr.sh
Executable 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
5
examples/openssl.cnf
Normal file
@@ -0,0 +1,5 @@
|
||||
[ req ]
|
||||
distinguished_name = req_distinguished_name
|
||||
[ req_distinguished_name ]
|
||||
[ san ]
|
||||
subjectAltName=${ENV::SAN}
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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`)."""
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
1
setup.py
1
setup.py
@@ -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
47
tests/boulder-integration.sh
Executable 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
15
tests/boulder-start.sh
Executable 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...
|
||||
Reference in New Issue
Block a user