Merge pull request #504 from kuba/csr

Initial support for user-supplied CSRs (fixes: #370)
This commit is contained in:
James Kasten
2015-06-25 14:09:53 -04:00
12 changed files with 255 additions and 120 deletions
+3
View File
@@ -16,3 +16,6 @@ dist/
*~ *~
*.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"),
+66 -44
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,12 +311,10 @@ 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:
# TODO: Except
chain_cert = self.network.fetch_chain(certr)
if chain_cert is not None: if chain_cert is not None:
chain_file, act_chain_path = le_util.unique_file( chain_file, act_chain_path = le_util.unique_file(
chain_path, 0o644) chain_path, 0o644)
# TODO: Except
chain_pem = chain_cert.as_pem() chain_pem = chain_cert.as_pem()
try: try:
chain_file.write(chain_pem) chain_file.write(chain_pem)
@@ -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(
-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):
+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
+13 -3
View File
@@ -6,6 +6,7 @@
root="$(mktemp -d)" root="$(mktemp -d)"
echo "\nRoot integration tests directory: $root" echo "\nRoot integration tests directory: $root"
common() {
# first three flags required, rest is handy defaults # first three flags required, rest is handy defaults
letsencrypt \ letsencrypt \
--server http://localhost:4000/acme/new-reg \ --server http://localhost:4000/acme/new-reg \
@@ -16,7 +17,16 @@ letsencrypt \
--text \ --text \
--agree-eula \ --agree-eula \
--email "" \ --email "" \
--domains le.wtf \
--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