mirror of
https://github.com/certbot/certbot.git
synced 2026-07-30 18:04:31 +02:00
Merge remote-tracking branch 'github/letsencrypt/master' into renewer
Conflicts: tests/boulder-integration.sh
This commit is contained in:
+4
-1
@@ -15,4 +15,7 @@ dist/
|
|||||||
# editor temporary files
|
# editor temporary files
|
||||||
*~
|
*~
|
||||||
*.swp
|
*.swp
|
||||||
\#*#
|
\#*#
|
||||||
|
|
||||||
|
# auth --cert-path --chain-path
|
||||||
|
/*.pem
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# generate-csr.sh:
|
||||||
|
/key.pem
|
||||||
|
/csr.der
|
||||||
Executable
+28
@@ -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}"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[ req ]
|
||||||
|
distinguished_name = req_distinguished_name
|
||||||
|
[ req_distinguished_name ]
|
||||||
|
[ san ]
|
||||||
|
subjectAltName=${ENV::SAN}
|
||||||
+52
-29
@@ -89,16 +89,20 @@ def _account_init(args, config):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _common_run(args, config, acc, authenticator, installer):
|
def _find_domains(args, installer):
|
||||||
if args.domains is None:
|
if args.domains is None:
|
||||||
doms = display_ops.choose_names(installer)
|
domains = display_ops.choose_names(installer)
|
||||||
else:
|
else:
|
||||||
doms = args.domains
|
domains = args.domains
|
||||||
|
|
||||||
if not doms:
|
if not domains:
|
||||||
sys.exit("Please specify --domains, or --installer that will "
|
sys.exit("Please specify --domains, or --installer that will "
|
||||||
"help in domain names autodiscovery")
|
"help in domain names autodiscovery")
|
||||||
|
|
||||||
|
return domains
|
||||||
|
|
||||||
|
|
||||||
|
def _init_acme(config, acc, authenticator, installer):
|
||||||
acme = client.Client(config, acc, authenticator, installer)
|
acme = client.Client(config, acc, authenticator, installer)
|
||||||
|
|
||||||
# Validate the key and csr
|
# Validate the key and csr
|
||||||
@@ -111,7 +115,7 @@ def _common_run(args, config, acc, authenticator, installer):
|
|||||||
except errors.LetsEncryptClientError:
|
except errors.LetsEncryptClientError:
|
||||||
sys.exit("Unable to register an account with ACME server")
|
sys.exit("Unable to register an account with ACME server")
|
||||||
|
|
||||||
return acme, doms
|
return acme
|
||||||
|
|
||||||
|
|
||||||
def run(args, config, plugins):
|
def run(args, config, plugins):
|
||||||
@@ -139,19 +143,26 @@ def run(args, config, plugins):
|
|||||||
if installer is None or authenticator is None:
|
if installer is None or authenticator is None:
|
||||||
return "Configurator could not be determined"
|
return "Configurator could not be determined"
|
||||||
|
|
||||||
acme, doms = _common_run(args, config, acc, authenticator, installer)
|
domains = _find_domains(args, installer)
|
||||||
# TODO: Handle errors from _common_run?
|
# TODO: Handle errors from _init_acme?
|
||||||
lineage = acme.obtain_and_enroll_certificate(doms, authenticator,
|
acme = _init_acme(config, acc, authenticator, installer)
|
||||||
installer, plugins)
|
lineage = acme.obtain_and_enroll_certificate(
|
||||||
|
domains, authenticator, installer, plugins)
|
||||||
if not lineage:
|
if not lineage:
|
||||||
return "Certificate could not be obtained"
|
return "Certificate could not be obtained"
|
||||||
acme.deploy_certificate(doms, lineage.privkey, lineage.cert, lineage.chain)
|
acme.deploy_certificate(domains, lineage.privkey, lineage.cert, lineage.chain)
|
||||||
acme.enhance_config(doms, args.redirect)
|
acme.enhance_config(domains, args.redirect)
|
||||||
|
|
||||||
|
|
||||||
def auth(args, config, plugins):
|
def auth(args, config, plugins):
|
||||||
"""Authenticate & obtain cert, but do not install it."""
|
"""Authenticate & obtain cert, but do not install it."""
|
||||||
# XXX: Update for renewer / RenewableCert
|
# 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)
|
acc = _account_init(args, config)
|
||||||
if acc is None:
|
if acc is None:
|
||||||
return None
|
return None
|
||||||
@@ -166,13 +177,18 @@ def auth(args, config, plugins):
|
|||||||
else:
|
else:
|
||||||
installer = None
|
installer = None
|
||||||
|
|
||||||
# TODO: Handle errors from _common_run?
|
# TODO: Handle errors from _init_acme?
|
||||||
acme, doms = _common_run(
|
acme = _init_acme(config, acc, authenticator, installer)
|
||||||
args, config, acc, authenticator=authenticator, installer=installer)
|
|
||||||
if not acme.obtain_and_enroll_certificate(
|
|
||||||
doms, authenticator, installer, plugins):
|
|
||||||
return "Certificate could not be obtained"
|
|
||||||
|
|
||||||
|
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):
|
def install(args, config, plugins):
|
||||||
"""Install a previously obtained cert in a server."""
|
"""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)
|
installer = display_ops.pick_installer(config, args.installer, plugins)
|
||||||
if installer is None:
|
if installer is None:
|
||||||
return "Installer could not be determined"
|
return "Installer could not be determined"
|
||||||
acme, doms = _common_run(
|
domains = _find_domains(args, installer)
|
||||||
args, config, acc, authenticator=None, installer=installer)
|
acme = _init_acme(config, acc, authenticator=None, installer=installer)
|
||||||
assert args.cert_path is not None
|
assert args.cert_path is not None
|
||||||
acme.deploy_certificate(doms, acc.key.file, args.cert_path, args.chain_path)
|
acme.deploy_certificate(domains, acc.key.file, args.cert_path, args.chain_path)
|
||||||
acme.enhance_config(doms, args.redirect)
|
acme.enhance_config(domains, args.redirect)
|
||||||
|
|
||||||
|
|
||||||
def revoke(args, unused_config, unused_plugins):
|
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)
|
print str(available)
|
||||||
|
|
||||||
|
|
||||||
def read_file(filename):
|
def read_file(filename, mode="rb"):
|
||||||
"""Returns the given file's contents with universal new line support.
|
"""Returns the given file's contents.
|
||||||
|
|
||||||
:param str filename: Filename
|
:param str filename: Filename
|
||||||
|
:param str mode: open mode (see `open`)
|
||||||
|
|
||||||
:returns: A tuple of filename and its contents
|
:returns: A tuple of filename and its contents
|
||||||
:rtype: tuple
|
:rtype: tuple
|
||||||
@@ -255,7 +272,7 @@ def read_file(filename):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return filename, open(filename, "rU").read()
|
return filename, open(filename, mode).read()
|
||||||
except IOError as exc:
|
except IOError as exc:
|
||||||
raise argparse.ArgumentTypeError(exc.strerror)
|
raise argparse.ArgumentTypeError(exc.strerror)
|
||||||
|
|
||||||
@@ -466,12 +483,22 @@ def create_parser(plugins, args):
|
|||||||
return subparser
|
return subparser
|
||||||
|
|
||||||
add_subparser("run", run)
|
add_subparser("run", run)
|
||||||
add_subparser("auth", auth)
|
parser_auth = add_subparser("auth", auth)
|
||||||
add_subparser("install", install)
|
add_subparser("install", install)
|
||||||
parser_revoke = add_subparser("revoke", revoke)
|
parser_revoke = add_subparser("revoke", revoke)
|
||||||
parser_rollback = add_subparser("rollback", rollback)
|
parser_rollback = add_subparser("rollback", rollback)
|
||||||
add_subparser("config_changes", config_changes)
|
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_subparser("plugins", plugins_cmd)
|
||||||
parser_plugins.add_argument("--init", action="store_true")
|
parser_plugins.add_argument("--init", action="store_true")
|
||||||
parser_plugins.add_argument("--prepare", action="store_true")
|
parser_plugins.add_argument("--prepare", action="store_true")
|
||||||
@@ -539,10 +566,6 @@ def _paths_parser(helpful):
|
|||||||
help=config_help("cert_dir"))
|
help=config_help("cert_dir"))
|
||||||
add("paths", "--le-vhost-ext", default="-le-ssl.conf",
|
add("paths", "--le-vhost-ext", default="-le-ssl.conf",
|
||||||
help=config_help("le_vhost_ext"))
|
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"),
|
add("paths", "--renewer-config-file", default=flag_default("renewer_config_file"),
|
||||||
help=config_help("renewer_config_file"))
|
help=config_help("renewer_config_file"))
|
||||||
add("paths", "-s", "--server", default=flag_default("server"),
|
add("paths", "-s", "--server", default=flag_default("server"),
|
||||||
|
|||||||
+76
-54
@@ -4,6 +4,7 @@ import os
|
|||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
import M2Crypto
|
import M2Crypto
|
||||||
|
import OpenSSL.crypto
|
||||||
import zope.component
|
import zope.component
|
||||||
|
|
||||||
from acme import jose
|
from acme import jose
|
||||||
@@ -125,22 +126,20 @@ class Client(object):
|
|||||||
"{0}.".format(self.account.email))
|
"{0}.".format(self.account.email))
|
||||||
reporter.add_message(recovery_msg, reporter.HIGH_PRIORITY, True)
|
reporter.add_message(recovery_msg, reporter.HIGH_PRIORITY, True)
|
||||||
|
|
||||||
def obtain_certificate(self, domains, csr=None):
|
def _obtain_certificate(self, domains, csr):
|
||||||
"""Obtains a certificate from the ACME server.
|
"""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
|
:returns: `.CertificateResource` and certificate chain (as
|
||||||
|
returned by `.fetch_chain`).
|
||||||
:param csr: CSR must contain requested domains, the key used to generate
|
:rtype: tuple
|
||||||
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`
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if self.auth_handler is None:
|
if self.auth_handler is None:
|
||||||
@@ -151,37 +150,54 @@ class Client(object):
|
|||||||
if self.account.regr is None:
|
if self.account.regr is None:
|
||||||
raise errors.Error("Please register with the ACME server first.")
|
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)
|
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(
|
certr = self.network.request_issuance(
|
||||||
jose.ComparableX509(
|
jose.ComparableX509(
|
||||||
M2Crypto.X509.load_request_der_string(csr.data)),
|
M2Crypto.X509.load_request_der_string(csr.data)),
|
||||||
authzr)
|
authzr)
|
||||||
|
return certr, self.network.fetch_chain(certr)
|
||||||
|
|
||||||
cert_pem = certr.body.as_pem()
|
def obtain_certificate_from_csr(self, csr):
|
||||||
chain_pem = None
|
"""Obtain certficiate from CSR.
|
||||||
if certr.cert_chain_uri is not None:
|
|
||||||
chain_pem = self.network.fetch_chain(certr)
|
|
||||||
|
|
||||||
if chain_pem is None:
|
:param .le_util.CSR csr: DER-encoded Certificate Signing
|
||||||
# XXX: just to stop RenewableCert from complaining; this is
|
Request.
|
||||||
# probably not a good solution
|
|
||||||
chain_pem = ""
|
|
||||||
else:
|
|
||||||
chain_pem = chain_pem.as_pem()
|
|
||||||
|
|
||||||
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(
|
def obtain_and_enroll_certificate(
|
||||||
self, domains, authenticator, installer, plugins, csr=None):
|
self, domains, authenticator, installer, plugins):
|
||||||
"""Obtain and enroll certificate.
|
"""Obtain and enroll certificate.
|
||||||
|
|
||||||
Get a new certificate for the specified domains using the specified
|
Get a new certificate for the specified domains using the specified
|
||||||
@@ -197,14 +213,14 @@ class Client(object):
|
|||||||
|
|
||||||
:param plugins: A PluginsFactory 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
|
:returns: A new :class:`letsencrypt.storage.RenewableCert` instance
|
||||||
referred to the enrolled cert lineage, or False if the cert could
|
referred to the enrolled cert lineage, or False if the cert could
|
||||||
not be obtained.
|
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(
|
self.config.namespace.authenticator = plugins.find_init(
|
||||||
authenticator).name
|
authenticator).name
|
||||||
if installer is not None:
|
if installer is not None:
|
||||||
@@ -226,8 +242,12 @@ class Client(object):
|
|||||||
"Non-standard path(s), might not work with crontab installed "
|
"Non-standard path(s), might not work with crontab installed "
|
||||||
"by your operating system package manager")
|
"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(
|
lineage = storage.RenewableCert.new_lineage(
|
||||||
domains[0], cert, privkey, chain, params, config, cli_config)
|
domains[0], certr.body.as_pem(), key.pem, chain_pem, params,
|
||||||
|
config, cli_config)
|
||||||
self._report_renewal_status(lineage)
|
self._report_renewal_status(lineage)
|
||||||
return lineage
|
return lineage
|
||||||
|
|
||||||
@@ -258,13 +278,14 @@ class Client(object):
|
|||||||
reporter = zope.component.getUtility(interfaces.IReporter)
|
reporter = zope.component.getUtility(interfaces.IReporter)
|
||||||
reporter.add_message(msg, reporter.LOW_PRIORITY, True)
|
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
|
# pylint: disable=no-self-use
|
||||||
"""Saves the certificate received from the ACME server.
|
"""Saves the certificate received from the ACME server.
|
||||||
|
|
||||||
:param certr: ACME "certificate" resource.
|
:param certr: ACME "certificate" resource.
|
||||||
:type certr: :class:`acme.messages.Certificate`
|
:type certr: :class:`acme.messages.Certificate`
|
||||||
|
|
||||||
|
:param chain_cert:
|
||||||
:param str cert_path: Candidate path to a certificate.
|
:param str cert_path: Candidate path to a certificate.
|
||||||
:param str chain_path: Candidate path to a certificate chain.
|
:param str chain_path: Candidate path to a certificate chain.
|
||||||
|
|
||||||
@@ -274,6 +295,10 @@ class Client(object):
|
|||||||
:raises IOError: If unable to find room to write the cert files
|
: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
|
# try finally close
|
||||||
cert_chain_abspath = None
|
cert_chain_abspath = None
|
||||||
cert_file, act_cert_path = le_util.unique_file(cert_path, 0o644)
|
cert_file, act_cert_path = le_util.unique_file(cert_path, 0o644)
|
||||||
@@ -286,22 +311,20 @@ class Client(object):
|
|||||||
logging.info("Server issued certificate; certificate written to %s",
|
logging.info("Server issued certificate; certificate written to %s",
|
||||||
act_cert_path)
|
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
|
# TODO: Except
|
||||||
chain_cert = self.network.fetch_chain(certr)
|
chain_pem = chain_cert.as_pem()
|
||||||
if chain_cert is not None:
|
try:
|
||||||
chain_file, act_chain_path = le_util.unique_file(
|
chain_file.write(chain_pem)
|
||||||
chain_path, 0o644)
|
finally:
|
||||||
chain_pem = chain_cert.as_pem()
|
chain_file.close()
|
||||||
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
|
# This expects a valid chain file
|
||||||
cert_chain_abspath = os.path.abspath(act_chain_path)
|
cert_chain_abspath = os.path.abspath(act_chain_path)
|
||||||
|
|
||||||
return os.path.abspath(act_cert_path), cert_chain_abspath
|
return os.path.abspath(act_cert_path), cert_chain_abspath
|
||||||
|
|
||||||
@@ -390,8 +413,7 @@ def validate_key_csr(privkey, csr=None):
|
|||||||
:param privkey: Key associated with CSR
|
:param privkey: Key associated with CSR
|
||||||
:type privkey: :class:`letsencrypt.le_util.Key`
|
:type privkey: :class:`letsencrypt.le_util.Key`
|
||||||
|
|
||||||
:param csr: CSR
|
:param .le_util.CSR csr: CSR
|
||||||
:type csr: :class:`letsencrypt.le_util.CSR`
|
|
||||||
|
|
||||||
:raises .errors.Error: when validation fails
|
:raises .errors.Error: when validation fails
|
||||||
|
|
||||||
|
|||||||
@@ -59,15 +59,15 @@ class NamespaceConfig(object):
|
|||||||
def backup_dir(self): # pylint: disable=missing-docstring
|
def backup_dir(self): # pylint: disable=missing-docstring
|
||||||
return os.path.join(self.namespace.work_dir, constants.BACKUP_DIR)
|
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
|
@property
|
||||||
def cert_key_backup(self): # pylint: disable=missing-docstring
|
def cert_key_backup(self): # pylint: disable=missing-docstring
|
||||||
return os.path.join(self.namespace.work_dir,
|
return os.path.join(self.namespace.work_dir,
|
||||||
constants.CERT_KEY_BACKUP_DIR, self.server_path)
|
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
|
@property
|
||||||
def in_progress_dir(self): # pylint: disable=missing-docstring
|
def in_progress_dir(self): # pylint: disable=missing-docstring
|
||||||
return os.path.join(self.namespace.work_dir, constants.IN_PROGRESS_DIR)
|
return os.path.join(self.namespace.work_dir, constants.IN_PROGRESS_DIR)
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ CLI_DEFAULTS = dict(
|
|||||||
work_dir="/var/lib/letsencrypt",
|
work_dir="/var/lib/letsencrypt",
|
||||||
no_verify_ssl=False,
|
no_verify_ssl=False,
|
||||||
dvsni_port=challenges.DVSNI.PORT,
|
dvsni_port=challenges.DVSNI.PORT,
|
||||||
|
cert_path="./cert.pem",
|
||||||
|
chain_path="./chain.pem",
|
||||||
|
|
||||||
# TODO: blocked by #485, values ignored
|
# TODO: blocked by #485, values ignored
|
||||||
backup_dir="not used",
|
backup_dir="not used",
|
||||||
key_dir="not used",
|
key_dir="not used",
|
||||||
certs_dir="not used",
|
certs_dir="not used",
|
||||||
cert_path="not used",
|
|
||||||
chain_path="not used",
|
|
||||||
renewer_config_file="not used",
|
renewer_config_file="not used",
|
||||||
)
|
)
|
||||||
"""Defaults for CLI flags and `.IConfig` attributes."""
|
"""Defaults for CLI flags and `.IConfig` attributes."""
|
||||||
@@ -69,13 +69,13 @@ ACCOUNT_KEYS_DIR = "keys"
|
|||||||
BACKUP_DIR = "backups"
|
BACKUP_DIR = "backups"
|
||||||
"""Directory (relative to `IConfig.work_dir`) where backups are kept."""
|
"""Directory (relative to `IConfig.work_dir`) where backups are kept."""
|
||||||
|
|
||||||
|
CERT_DIR = "certs"
|
||||||
|
"""See `.IConfig.cert_dir`."""
|
||||||
|
|
||||||
CERT_KEY_BACKUP_DIR = "keys-certs"
|
CERT_KEY_BACKUP_DIR = "keys-certs"
|
||||||
"""Directory where all certificates and keys are stored (relative to
|
"""Directory where all certificates and keys are stored (relative to
|
||||||
`IConfig.work_dir`). Used for easy revocation."""
|
`IConfig.work_dir`). Used for easy revocation."""
|
||||||
|
|
||||||
CERT_DIR = "certs"
|
|
||||||
"""See `.IConfig.cert_dir`."""
|
|
||||||
|
|
||||||
IN_PROGRESS_DIR = "IN_PROGRESS"
|
IN_PROGRESS_DIR = "IN_PROGRESS"
|
||||||
"""Directory used before a permanent checkpoint is finalized (relative to
|
"""Directory used before a permanent checkpoint is finalized (relative to
|
||||||
`IConfig.work_dir`)."""
|
`IConfig.work_dir`)."""
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ def make_csr(key_str, domains):
|
|||||||
:param str key_str: RSA key.
|
:param str key_str: RSA key.
|
||||||
:param list domains: Domains included in the certificate.
|
: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
|
:returns: new CSR in PEM and DER form containing all domains
|
||||||
:rtype: tuple
|
:rtype: tuple
|
||||||
|
|
||||||
@@ -101,13 +104,7 @@ def make_csr(key_str, domains):
|
|||||||
|
|
||||||
csr = M2Crypto.X509.Request()
|
csr = M2Crypto.X509.Request()
|
||||||
csr.set_pubkey(pubkey)
|
csr.set_pubkey(pubkey)
|
||||||
name = csr.get_subject()
|
# TODO: what to put into 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]
|
|
||||||
|
|
||||||
extstack = M2Crypto.X509.X509_Extension_Stack()
|
extstack = M2Crypto.X509.X509_Extension_Stack()
|
||||||
ext = M2Crypto.X509.new_extension(
|
ext = M2Crypto.X509.new_extension(
|
||||||
|
|||||||
@@ -205,8 +205,22 @@ def success_installation(domains):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
util(interfaces.IDisplay).notification(
|
util(interfaces.IDisplay).notification(
|
||||||
"Congratulations! You have successfully enabled "
|
"Congratulations! You have successfully enabled {0}!{1}{1}"
|
||||||
"%s!" % _gen_https_names(domains), pause=False)
|
"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):
|
def _gen_https_names(domains):
|
||||||
|
|||||||
@@ -190,8 +190,6 @@ class IConfig(zope.interface.Interface):
|
|||||||
|
|
||||||
# TODO: the following are not used, but blocked by #485
|
# TODO: the following are not used, but blocked by #485
|
||||||
le_vhost_ext = zope.interface.Attribute("not used")
|
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):
|
class IInstaller(IPlugin):
|
||||||
|
|||||||
@@ -83,14 +83,15 @@ def renew(cert, old_version):
|
|||||||
our_client = client.Client(config, account, authenticator, None)
|
our_client = client.Client(config, account, authenticator, None)
|
||||||
with open(cert.version("cert", old_version)) as f:
|
with open(cert.version("cert", old_version)) as f:
|
||||||
sans = crypto_util.get_sans_from_cert(f.read())
|
sans = crypto_util.get_sans_from_cert(f.read())
|
||||||
new_cert, new_key, new_chain = our_client.obtain_certificate(sans)
|
new_certr, new_chain, new_key, _ = our_client.obtain_certificate(sans)
|
||||||
if new_cert and new_key and new_chain:
|
if new_chain is not None:
|
||||||
# XXX: Assumes that there was no key change. We need logic
|
# XXX: Assumes that there was no key change. We need logic
|
||||||
# for figuring out whether there was or not. Probably
|
# for figuring out whether there was or not. Probably
|
||||||
# best is to have obtain_certificate return None for
|
# best is to have obtain_certificate return None for
|
||||||
# new_key if the old key is to be used (since save_successor
|
# new_key if the old key is to be used (since save_successor
|
||||||
# already understands this distinction!)
|
# 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
|
# TODO: Notify results
|
||||||
else:
|
else:
|
||||||
# TODO: Notify negative results
|
# TODO: Notify negative results
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ import shutil
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import configobj
|
import configobj
|
||||||
|
import M2Crypto.X509
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
|
from acme import jose
|
||||||
|
|
||||||
from letsencrypt import account
|
from letsencrypt import account
|
||||||
from letsencrypt import configuration
|
from letsencrypt import configuration
|
||||||
from letsencrypt import le_util
|
from letsencrypt import le_util
|
||||||
@@ -15,6 +18,8 @@ from letsencrypt import le_util
|
|||||||
|
|
||||||
KEY = pkg_resources.resource_string(
|
KEY = pkg_resources.resource_string(
|
||||||
__name__, os.path.join("testdata", "rsa512_key.pem"))
|
__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):
|
class ClientTest(unittest.TestCase):
|
||||||
@@ -27,16 +32,57 @@ class ClientTest(unittest.TestCase):
|
|||||||
self.account = mock.MagicMock(**{"key.pem": KEY})
|
self.account = mock.MagicMock(**{"key.pem": KEY})
|
||||||
|
|
||||||
from letsencrypt.client import Client
|
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(
|
self.client = Client(
|
||||||
config=self.config, account_=self.account, dv_auth=None,
|
config=self.config, account_=self.account,
|
||||||
installer=None)
|
dv_auth=None, installer=None)
|
||||||
self.network = network
|
self.network = network
|
||||||
|
|
||||||
def test_init_network_verify_ssl(self):
|
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)
|
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")
|
@mock.patch("letsencrypt.client.zope.component.getUtility")
|
||||||
def test_report_new_account(self, mock_zope):
|
def test_report_new_account(self, mock_zope):
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
|
|||||||
@@ -182,6 +182,24 @@ class ChooseAccountTest(unittest.TestCase):
|
|||||||
self.assertTrue(self._call([self.acc1, self.acc2]) is None)
|
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):
|
class GenHttpsNamesTest(unittest.TestCase):
|
||||||
"""Test _gen_https_names."""
|
"""Test _gen_https_names."""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -581,13 +581,18 @@ class RenewableCertTests(unittest.TestCase):
|
|||||||
self.assertFalse(renewer.renew(self.test_rc, 1))
|
self.assertFalse(renewer.renew(self.test_rc, 1))
|
||||||
self.test_rc.configfile["renewalparams"]["authenticator"] = "apache"
|
self.test_rc.configfile["renewalparams"]["authenticator"] = "apache"
|
||||||
mock_client = mock.MagicMock()
|
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
|
mock_c.return_value = mock_client
|
||||||
self.assertEqual(2, renewer.renew(self.test_rc, 1))
|
self.assertEqual(2, renewer.renew(self.test_rc, 1))
|
||||||
# TODO: We could also make several assertions about calls that should
|
# TODO: We could also make several assertions about calls that should
|
||||||
# have been made to the mock functions here.
|
# have been made to the mock functions here.
|
||||||
self.assertEqual(mock_da.call_count, 1)
|
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
|
# This should fail because the renewal itself appears to fail
|
||||||
self.assertFalse(renewer.renew(self.test_rc, 1))
|
self.assertFalse(renewer.renew(self.test_rc, 1))
|
||||||
|
|
||||||
|
|||||||
@@ -7,19 +7,29 @@ root="$(mktemp -d)"
|
|||||||
echo "\nRoot integration tests directory: $root"
|
echo "\nRoot integration tests directory: $root"
|
||||||
store_flags="--config-dir $root/conf --work-dir $root/work"
|
store_flags="--config-dir $root/conf --work-dir $root/work"
|
||||||
|
|
||||||
# first three flags required, rest is handy defaults
|
common() {
|
||||||
letsencrypt \
|
# first three flags required, rest is handy defaults
|
||||||
--server http://localhost:4000/acme/new-reg \
|
letsencrypt \
|
||||||
--no-verify-ssl \
|
--server http://localhost:4000/acme/new-reg \
|
||||||
--dvsni-port 5001 \
|
--no-verify-ssl \
|
||||||
$store_flags \
|
--dvsni-port 5001 \
|
||||||
--text \
|
$store_flags \
|
||||||
--agree-eula \
|
--text \
|
||||||
--email "" \
|
--agree-eula \
|
||||||
--domains le.wtf \
|
--email "" \
|
||||||
--authenticator standalone \
|
--authenticator standalone \
|
||||||
-vvvvvvv \
|
-vvvvvvv "$@"
|
||||||
auth
|
}
|
||||||
|
|
||||||
|
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
|
# the following assumes that Boulder issues certificates for less than
|
||||||
# 10 years, otherwise renewal will not take place
|
# 10 years, otherwise renewal will not take place
|
||||||
|
|||||||
Reference in New Issue
Block a user