Merge remote-tracking branch 'github/letsencrypt/master' into renewer

Conflicts:
	tests/boulder-integration.sh
This commit is contained in:
Jakub Warmuz
2015-06-25 19:07:21 +00:00
16 changed files with 299 additions and 126 deletions
+4 -1
View File
@@ -15,4 +15,7 @@ dist/
# editor temporary files # editor temporary files
*~ *~
*.swp *.swp
\#*# \#*#
# auth --cert-path --chain-path
/*.pem
+3
View File
@@ -0,0 +1,3 @@
# generate-csr.sh:
/key.pem
/csr.der
+28
View File
@@ -0,0 +1,28 @@
#!/bin/sh
# This script generates a simple SAN CSR to be used with Let's Encrypt
# CA. Mostly intedened for "auth --csr" testing, but, since its easily
# auditable, feel free to adjust it and use on you production web
# server.
if [ "$#" -lt 1 ]
then
echo "Usage: $0 domain [domain...]" >&2
exit 1
fi
domains="DNS:$1"
shift
for x in "$@"
do
domains="$domains,DNS:$x"
done
SAN="$domains" openssl req -config "${OPENSSL_CNF:-openssl.cnf}" \
-new -nodes -subj '/' -reqexts san \
-out "${CSR_PATH:-csr.der}" \
-keyout key.pem \
-newkey rsa:2048 \
-outform DER
# 512 or 1024 too low for Boulder, 2048 is smallest for tests
echo "You can now run: letsencrypt auth --csr ${CSR_PATH:-csr.der}"
+5
View File
@@ -0,0 +1,5 @@
[ req ]
distinguished_name = req_distinguished_name
[ req_distinguished_name ]
[ san ]
subjectAltName=${ENV::SAN}
+52 -29
View File
@@ -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
View File
@@ -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
+4 -4
View File
@@ -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)
+5 -5
View File
@@ -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`)."""
+4 -7
View File
@@ -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(
+16 -2
View File
@@ -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):
-2
View File
@@ -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):
+4 -3
View File
@@ -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
+50 -4
View File
@@ -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
+18
View File
@@ -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):
+7 -2
View File
@@ -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))
+23 -13
View File
@@ -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