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

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

5
.gitignore vendored
View File

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

3
examples/.gitignore vendored Normal file
View File

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

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

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

5
examples/openssl.cnf Normal file
View File

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

View File

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

View File

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

View File

@@ -59,15 +59,15 @@ class NamespaceConfig(object):
def backup_dir(self): # pylint: disable=missing-docstring
return os.path.join(self.namespace.work_dir, constants.BACKUP_DIR)
@property
def cert_dir(self): # pylint: disable=missing-docstring
return os.path.join(self.namespace.config_dir, constants.CERT_DIR)
@property
def cert_key_backup(self): # pylint: disable=missing-docstring
return os.path.join(self.namespace.work_dir,
constants.CERT_KEY_BACKUP_DIR, self.server_path)
@property
def cert_dir(self): # pylint: disable=missing-docstring
return os.path.join(self.namespace.config_dir, constants.CERT_DIR)
@property
def in_progress_dir(self): # pylint: disable=missing-docstring
return os.path.join(self.namespace.work_dir, constants.IN_PROGRESS_DIR)

View File

@@ -17,13 +17,13 @@ CLI_DEFAULTS = dict(
work_dir="/var/lib/letsencrypt",
no_verify_ssl=False,
dvsni_port=challenges.DVSNI.PORT,
cert_path="./cert.pem",
chain_path="./chain.pem",
# TODO: blocked by #485, values ignored
backup_dir="not used",
key_dir="not used",
certs_dir="not used",
cert_path="not used",
chain_path="not used",
renewer_config_file="not used",
)
"""Defaults for CLI flags and `.IConfig` attributes."""
@@ -69,13 +69,13 @@ ACCOUNT_KEYS_DIR = "keys"
BACKUP_DIR = "backups"
"""Directory (relative to `IConfig.work_dir`) where backups are kept."""
CERT_DIR = "certs"
"""See `.IConfig.cert_dir`."""
CERT_KEY_BACKUP_DIR = "keys-certs"
"""Directory where all certificates and keys are stored (relative to
`IConfig.work_dir`). Used for easy revocation."""
CERT_DIR = "certs"
"""See `.IConfig.cert_dir`."""
IN_PROGRESS_DIR = "IN_PROGRESS"
"""Directory used before a permanent checkpoint is finalized (relative to
`IConfig.work_dir`)."""

View File

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

View File

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

View File

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

View File

@@ -83,14 +83,15 @@ def renew(cert, old_version):
our_client = client.Client(config, account, authenticator, None)
with open(cert.version("cert", old_version)) as f:
sans = crypto_util.get_sans_from_cert(f.read())
new_cert, new_key, new_chain = our_client.obtain_certificate(sans)
if new_cert and new_key and new_chain:
new_certr, new_chain, new_key, _ = our_client.obtain_certificate(sans)
if new_chain is not None:
# XXX: Assumes that there was no key change. We need logic
# for figuring out whether there was or not. Probably
# best is to have obtain_certificate return None for
# new_key if the old key is to be used (since save_successor
# already understands this distinction!)
return cert.save_successor(old_version, new_cert, new_key, new_chain)
return cert.save_successor(old_version, new_certr.body.as_pem(),
new_key.pem, new_chain.as_pem())
# TODO: Notify results
else:
# TODO: Notify negative results

View File

@@ -6,8 +6,11 @@ import shutil
import tempfile
import configobj
import M2Crypto.X509
import mock
from acme import jose
from letsencrypt import account
from letsencrypt import configuration
from letsencrypt import le_util
@@ -15,6 +18,8 @@ from letsencrypt import le_util
KEY = pkg_resources.resource_string(
__name__, os.path.join("testdata", "rsa512_key.pem"))
CSR_SAN = pkg_resources.resource_string(
__name__, os.path.join("testdata", "csr-san.der"))
class ClientTest(unittest.TestCase):
@@ -27,16 +32,57 @@ class ClientTest(unittest.TestCase):
self.account = mock.MagicMock(**{"key.pem": KEY})
from letsencrypt.client import Client
with mock.patch("letsencrypt.client.network") as network:
with mock.patch("letsencrypt.client.network.Network") as network:
self.client = Client(
config=self.config, account_=self.account, dv_auth=None,
installer=None)
config=self.config, account_=self.account,
dv_auth=None, installer=None)
self.network = network
def test_init_network_verify_ssl(self):
self.network.Network.assert_called_once_with(
self.network.assert_called_once_with(
mock.ANY, mock.ANY, verify_ssl=True)
def _mock_obtain_certificate(self):
self.client.auth_handler = mock.MagicMock()
self.network().request_issuance.return_value = mock.sentinel.certr
self.network().fetch_chain.return_value = mock.sentinel.chain
def _check_obtain_certificate(self):
self.client.auth_handler.get_authorizations.assert_called_once_with(
["example.com", "www.example.com"])
self.network.request_issuance.assert_callend_once_with(
jose.ComparableX509(
M2Crypto.X509.load_request_der_string(CSR_SAN)),
self.client.auth_handler.get_authorizations())
self.network().fetch_chain.assert_called_once_with(mock.sentinel.certr)
def test_obtain_certificate_from_csr(self):
self._mock_obtain_certificate()
self.assertEqual(
(mock.sentinel.certr, mock.sentinel.chain),
self.client.obtain_certificate_from_csr(le_util.CSR(
form="der", file=None, data=CSR_SAN)))
self._check_obtain_certificate()
@mock.patch("letsencrypt.client.crypto_util")
def test_obtain_certificate(self, mock_crypto_util):
self._mock_obtain_certificate()
csr = le_util.CSR(form="der", file=None, data=CSR_SAN)
mock_crypto_util.init_save_csr.return_value = csr
mock_crypto_util.init_save_key.return_value = mock.sentinel.key
domains = ["example.com", "www.example.com"]
self.assertEqual(
self.client.obtain_certificate(domains),
(mock.sentinel.certr, mock.sentinel.chain, mock.sentinel.key, csr))
mock_crypto_util.init_save_key.assert_called_once_with(
self.config.rsa_key_size, self.config.key_dir)
mock_crypto_util.init_save_csr.assert_called_once_with(
mock.sentinel.key, domains, self.config.cert_dir)
self._check_obtain_certificate()
@mock.patch("letsencrypt.client.zope.component.getUtility")
def test_report_new_account(self, mock_zope):
# pylint: disable=protected-access

View File

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

View File

@@ -581,13 +581,18 @@ class RenewableCertTests(unittest.TestCase):
self.assertFalse(renewer.renew(self.test_rc, 1))
self.test_rc.configfile["renewalparams"]["authenticator"] = "apache"
mock_client = mock.MagicMock()
mock_client.obtain_certificate.return_value = ("cert", "key", "chain")
# pylint: disable=star-args
mock_client.obtain_certificate.return_value = (
mock.Mock(**{'body.as_pem.return_value': 'cert'}),
mock.Mock(**{'as_pem.return_value': 'chain'}),
mock.Mock(pem="key"), mock.sentinel.csr)
mock_c.return_value = mock_client
self.assertEqual(2, renewer.renew(self.test_rc, 1))
# TODO: We could also make several assertions about calls that should
# have been made to the mock functions here.
self.assertEqual(mock_da.call_count, 1)
mock_client.obtain_certificate.return_value = (None, None, None)
mock_client.obtain_certificate.return_value = (
mock.sentinel.certr, None, mock.sentinel.key, mock.sentinel.csr)
# This should fail because the renewal itself appears to fail
self.assertFalse(renewer.renew(self.test_rc, 1))

View File

@@ -7,19 +7,29 @@ root="$(mktemp -d)"
echo "\nRoot integration tests directory: $root"
store_flags="--config-dir $root/conf --work-dir $root/work"
# first three flags required, rest is handy defaults
letsencrypt \
--server http://localhost:4000/acme/new-reg \
--no-verify-ssl \
--dvsni-port 5001 \
$store_flags \
--text \
--agree-eula \
--email "" \
--domains le.wtf \
--authenticator standalone \
-vvvvvvv \
auth
common() {
# first three flags required, rest is handy defaults
letsencrypt \
--server http://localhost:4000/acme/new-reg \
--no-verify-ssl \
--dvsni-port 5001 \
$store_flags \
--text \
--agree-eula \
--email "" \
--authenticator standalone \
-vvvvvvv "$@"
}
common --domains le.wtf auth
export CSR_PATH="${root}/csr.der" OPENSSL_CNF=examples/openssl.cnf
./examples/generate-csr.sh le.wtf
common auth --csr "$CSR_PATH" \
--cert-path "${root}/csr/cert.pem" \
--chain-path "${root}/csr/chain.pem"
openssl x509 -in "${root}/csr/0000_cert.pem" -text
openssl x509 -in "${root}/csr/0000_chain.pem" -text
# the following assumes that Boulder issues certificates for less than
# 10 years, otherwise renewal will not take place