diff --git a/letsencrypt_auto/Dockerfile b/letsencrypt_auto/Dockerfile
new file mode 100644
index 000000000..4bdb1426f
--- /dev/null
+++ b/letsencrypt_auto/Dockerfile
@@ -0,0 +1,33 @@
+# For running tests, build a docker image with a passwordless sudo and a trust
+# store we can manipulate.
+
+FROM ubuntu:trusty
+
+# Add an unprivileged user:
+RUN useradd --create-home --home-dir /home/lea --shell /bin/bash --groups sudo --uid 1000 lea
+
+# Let that user sudo:
+RUN adduser lea sudo
+RUN sed -i.bkp -e \
+ 's/%sudo\s\+ALL=(ALL\(:ALL\)\?)\s\+ALL/%sudo ALL=NOPASSWD:ALL/g' \
+ /etc/sudoers
+
+# Install pip and nose:
+RUN apt-get update && \
+ apt-get -q -y install python-pip && \
+ apt-get clean
+RUN pip install nose
+
+RUN mkdir -p /home/lea/letsencrypt/letsencrypt
+
+# Install fake testing CA:
+COPY ./tests/certs/ca/my-root-ca.crt.pem /usr/local/share/ca-certificates/
+RUN update-ca-certificates
+
+# Copy code:
+COPY . /home/lea/letsencrypt/letsencrypt_auto
+
+USER lea
+WORKDIR /home/lea
+
+CMD ["nosetests", "-s", "letsencrypt/letsencrypt_auto/tests"]
diff --git a/letsencrypt_auto/__init__.py b/letsencrypt_auto/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/letsencrypt_auto/build.py b/letsencrypt_auto/build.py
index d89c81470..9a5fc46a7 100755
--- a/letsencrypt_auto/build.py
+++ b/letsencrypt_auto/build.py
@@ -6,11 +6,14 @@ contents of the file at ./pieces/some/file except for certain tokens which have
other, special definitions.
"""
-from os.path import dirname, join
+from os.path import abspath, dirname, join
import re
from sys import argv
+DIR = dirname(abspath(__file__))
+
+
def le_version(build_script_dir):
"""Return the version number stamped in letsencrypt/__init__.py."""
return re.search('''^__version__ = ['"](.+)['"].*''',
@@ -25,25 +28,36 @@ def file_contents(path):
return file.read()
-def main():
- dir = dirname(argv[0])
+def build(version=None, requirements=None):
+ """Return the built contents of the letsencrypt-auto script.
+ :arg version: The version to attach to the script. Default: the version of
+ the letsencrypt package
+ :arg requirements: The contents of the requirements file to embed. Default:
+ contents of letsencrypt-auto-requirements.txt
+
+ """
special_replacements = {
- 'LE_AUTO_VERSION': le_version(dir)
+ 'LE_AUTO_VERSION': version or le_version(DIR)
}
+ if requirements:
+ special_replacements['letsencrypt-auto-requirements.txt'] = requirements
def replacer(match):
token = match.group(1)
if token in special_replacements:
return special_replacements[token]
else:
- return file_contents(join(dir, 'pieces', token))
+ return file_contents(join(DIR, 'pieces', token))
- result = re.sub(r'{{\s*([A-Za-z0-9_./-]+)\s*}}',
- replacer,
- file_contents(join(dir, 'letsencrypt-auto.template')))
- with open(join(dir, 'letsencrypt-auto'), 'w') as out:
- out.write(result)
+ return re.sub(r'{{\s*([A-Za-z0-9_./-]+)\s*}}',
+ replacer,
+ file_contents(join(DIR, 'letsencrypt-auto.template')))
+
+
+def main():
+ with open(join(DIR, 'letsencrypt-auto'), 'w') as out:
+ out.write(build())
if __name__ == '__main__':
diff --git a/letsencrypt_auto/letsencrypt-auto.template b/letsencrypt_auto/letsencrypt-auto.template
index 07adef413..ee18e8366 100755
--- a/letsencrypt_auto/letsencrypt-auto.template
+++ b/letsencrypt_auto/letsencrypt-auto.template
@@ -155,13 +155,7 @@ else
SUDO=
fi
-if [ ! -f "$VENV_BIN/letsencrypt" ]; then
- # If it looks like we've never bootstrapped before, bootstrap:
- Bootstrap
-fi
-if [ "$1" = "--os-packages-only" ]; then
- echo "OS packages installed."
-elif [ "$1" = "--no-self-upgrade" ]; then
+if [ "$1" = "--no-self-upgrade" ]; then
# Phase 2: Create venv, install LE, and run.
shift 1 # the --no-self-upgrade arg
@@ -184,7 +178,7 @@ elif [ "$1" = "--no-self-upgrade" ]; then
echo "Installing Python packages..."
TEMP_DIR=$(TempDir)
- # There is no $ interpolation due to quotes on heredoc delimiters.
+ # There is no $ interpolation due to quotes on starting heredoc delimiter.
# -------------------------------------------------------------------------
cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt"
{{ letsencrypt-auto-requirements.txt }}
@@ -216,7 +210,16 @@ else
# upgrading. Phase 1 checks the version of the latest release of
# letsencrypt-auto (which is always the same as that of the letsencrypt
# package). Phase 2 checks the version of the locally installed letsencrypt.
-
+
+ if [ ! -f "$VENV_BIN/letsencrypt" ]; then
+ # If it looks like we've never bootstrapped before, bootstrap:
+ Bootstrap
+ fi
+ if [ "$1" = "--os-packages-only" ]; then
+ echo "OS packages installed."
+ exit 0
+ fi
+
echo "Checking for new version..."
TEMP_DIR=$(TempDir)
# ---------------------------------------------------------------------------
diff --git a/letsencrypt_auto/pieces/fetch.py b/letsencrypt_auto/pieces/fetch.py
index 9625c224a..d094e6347 100644
--- a/letsencrypt_auto/pieces/fetch.py
+++ b/letsencrypt_auto/pieces/fetch.py
@@ -93,7 +93,7 @@ def verified_new_le_auto(get, tag, temp_dir):
"""
le_auto_dir = environ.get(
- 'LE_AUTO_DOWNLOAD_TEMPLATE',
+ 'LE_AUTO_DIR_TEMPLATE',
'https://raw.githubusercontent.com/letsencrypt/letsencrypt/%s/'
'letsencrypt-auto/') % tag
write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto')
@@ -129,4 +129,5 @@ def main():
return 0
-exit(main())
+if __name__ == '__main__':
+ exit(main())
diff --git a/letsencrypt_auto/pieces/letsencrypt-auto-requirements.txt b/letsencrypt_auto/pieces/letsencrypt-auto-requirements.txt
index 820b44396..70b005d2d 100644
--- a/letsencrypt_auto/pieces/letsencrypt-auto-requirements.txt
+++ b/letsencrypt_auto/pieces/letsencrypt-auto-requirements.txt
@@ -20,8 +20,8 @@
# sha256: 1F3TmncLSvtZHIJVX2qLvBrH6wGe2ptiHu4aCnIgEiA
cffi==1.3.1
-# sha256: 0ayQAF3qd2CBys5QjLnHMi4EONHA82AN8auXEZEBJME
-https://github.com/kuba/ConfigArgParse/archive/a58b35d75a10e8b8fbee7f3c69163b63bb506325.zip#egg=ConfigArgParse
+# sha256: O1CoPdWBSd_O6Yy2VlJl0QtT6cCivKfu73-19VJIkKc
+ConfigArgParse == 0.10.0
# sha256: ovVlB3DhyH-zNa8Zqbfrc_wFzPIhROto230AzSvLCQI
configobj==5.0.6
diff --git a/letsencrypt_auto/tests/__init__.py b/letsencrypt_auto/tests/__init__.py
new file mode 100644
index 000000000..13180b5f5
--- /dev/null
+++ b/letsencrypt_auto/tests/__init__.py
@@ -0,0 +1,7 @@
+"""Tests for letsencrypt-auto
+
+For now, run these by saying... ::
+
+ ./build.py && docker build -t lea . && docker run --rm -t -i lea
+
+"""
diff --git a/letsencrypt_auto/tests/auto_test.py b/letsencrypt_auto/tests/auto_test.py
new file mode 100644
index 000000000..c2b9fc378
--- /dev/null
+++ b/letsencrypt_auto/tests/auto_test.py
@@ -0,0 +1,247 @@
+"""Tests for letsencrypt-auto"""
+
+from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+from contextlib import contextmanager
+from functools import partial
+from json import dumps
+from os import environ
+from os.path import abspath, dirname, join
+import re
+from shutil import rmtree
+import socket
+import ssl
+from subprocess import CalledProcessError, check_output, Popen, PIPE
+from tempfile import mkdtemp
+from threading import Thread
+from unittest import TestCase
+
+from nose.tools import eq_, nottest, ok_
+
+from ..build import build as build_le_auto
+
+
+class RequestHandler(BaseHTTPRequestHandler):
+ """An HTTPS request handler which is quiet and serves a specific folder."""
+
+ def __init__(self, resources, *args, **kwargs):
+ """
+ :arg resources: A dict of resource paths pointing to content bytes
+
+ """
+ self.resources = resources
+ BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
+
+ def log_message(self, format, *args):
+ """Don't log each request to the terminal."""
+
+ def do_GET(self):
+ """Serve a GET request."""
+ content = self.send_head()
+ if content is not None:
+ self.wfile.write(content)
+
+ def send_head(self):
+ """Common code for GET and HEAD commands
+
+ This sends the response code and MIME headers and returns either a
+ bytestring of content or, if none is found, None.
+
+ """
+ path = self.path[1:] # Strip leading slash.
+ content = self.resources.get(path)
+ if content is None:
+ self.send_error(404, 'Path "%s" not found in self.resources' % path)
+ else:
+ self.send_response(200)
+ self.send_header('Content-type', 'text/plain')
+ self.send_header('Content-Length', str(len(content)))
+ self.end_headers()
+ return content
+
+
+def server_and_port(resources):
+ """Return an unstarted HTTPS server and the port it will use."""
+ # Find a port, and bind to it. I can't get the OS to close the socket
+ # promptly after we shut down the server, so we typically need to try
+ # a couple ports after the first test case. Setting
+ # TCPServer.allow_reuse_address = True seems to have nothing to do
+ # with this behavior.
+ worked = False
+ for port in xrange(4443, 4543):
+ try:
+ server = HTTPServer(('localhost', port),
+ partial(RequestHandler, resources))
+ except socket.error:
+ pass
+ else:
+ worked = True
+ server.socket = ssl.wrap_socket(
+ server.socket,
+ certfile=join(tests_dir(), 'certs', 'localhost', 'server.pem'),
+ server_side=True)
+ break
+ if not worked:
+ raise RuntimeError("Couldn't find an unused socket for the testing HTTPS server.")
+ return server, port
+
+
+@contextmanager
+def serving(resources):
+ """Spin up a local HTTPS server, and yield its base URL.
+
+ Use a self-signed cert generated as outlined by
+ https://coolaj86.com/articles/create-your-own-certificate-authority-for-
+ testing/.
+
+ """
+ server, port = server_and_port(resources)
+ thread = Thread(target=server.serve_forever)
+ try:
+ thread.start()
+ yield 'https://localhost:{port}/'.format(port=port)
+ finally:
+ server.shutdown()
+ thread.join()
+
+
+@nottest
+def tests_dir():
+ """Return a path to the "tests" directory."""
+ return dirname(abspath(__file__))
+
+
+@contextmanager
+def ephemeral_dir():
+ dir = mkdtemp(prefix='le-test-')
+ try:
+ yield dir
+ finally:
+ rmtree(dir)
+
+
+def out_and_err(command, input=None, shell=False, env=None):
+ """Run a shell command, and return stderr and stdout as string.
+
+ If the command returns nonzero, raise CalledProcessError.
+
+ :arg command: A list of commandline args
+ :arg input: Data to pipe to stdin. Omit for none.
+
+ Remaining args have the same meaning as for Popen.
+
+ """
+ process = Popen(command,
+ stdout=PIPE,
+ stdin=PIPE,
+ stderr=PIPE,
+ shell=shell,
+ env=env)
+ out, err = process.communicate(input=input)
+ status = process.poll() # same as in check_output(), though wait() sounds better
+ if status:
+ raise CalledProcessError(status, command, output=out)
+ return out, err
+
+
+def signed(content, private_key_name='signing.key'):
+ """Return the signed SHA-256 hash of ``content``, using the given key file."""
+ command = ['openssl', 'dgst', '-sha256', '-sign',
+ join(tests_dir(), private_key_name)]
+ out, err = out_and_err(command, input=content)
+ return out
+
+
+def run_le_auto(venv_dir, base_url):
+ """Run the prebuilt version of letsencrypt-auto, returning stdout and
+ stderr strings.
+
+ If the command returns other than 0, raise CalledProcessError.
+
+ """
+ env = environ.copy()
+ d = dict(XDG_DATA_HOME=venv_dir,
+ LE_AUTO_JSON_URL=base_url + 'letsencrypt/json',
+ LE_AUTO_DIR_TEMPLATE=base_url + '%s/',
+ # The public key corresponding to signing_keys/test.key:
+ LE_AUTO_PUBLIC_KEY="""-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsMoSzLYQ7E1sdSOkwelg
+tzKIh2qi3bpXuYtcfFC0XrvWig071NwIj+dZiT0OLZ2hPispEH0B7ISuuWg1ll7G
+hFW0VdbxL6JdGzS2ShNWkX9hE9z+j8VqwDPOBn3ZHm03qwpYkBDwQib3KqOdYbTT
+uUtJmmGcuk3a9Aq/sCT6DdfmTSdP5asdQYwIcaQreDrOosaS84DTWI3IU+UYJVgl
+LsIVPBuy9IcgHidUQ96hJnoPsDCWsHwX62495QKEarauyKQrJzFes0EY95orDM47
+Z5o/NDiQB11m91yNB0MmPYY9QSbnOA9j7IaaC97AwRLuwXY+/R2ablTcxurWou68
+iQIDAQAB
+-----END PUBLIC KEY-----""")
+ env.update(d)
+ return out_and_err(
+ join(dirname(tests_dir()), 'letsencrypt-auto') + ' --version',
+ shell=True,
+ env=env)
+
+
+class AutoTests(TestCase):
+ def test_all(self):
+ """Exercise most branches of letsencrypt-auto.
+
+ The branches:
+
+ * An le-auto upgrade is needed.
+ * An le-auto upgrade is not needed.
+ * There was an out-of-date LE script installed.
+ * There was a current LE script installed.
+ * There was no LE script installed. (not that important)
+ * Peep verification passes.
+ * Peep has a hash mismatch.
+ * The OpenSSL sig mismatches.
+
+ I violate my usual rule of having small, decoupled tests, because...
+
+ 1. We shouldn't need to run a Cartesian product of the branches: the
+ phases run in separate shell processes, containing state leakage
+ pretty effectively. The only shared state is FS state, and it's
+ limited to a temp dir, assuming (if we dare) all functions properly.
+ 2. One combination of branches happens to set us up nicely for testing
+ the next, saving code.
+
+ At the moment, we let bootstrapping run. We probably wanted those
+ packages installed anyway for local development.
+
+ For tests which get this far, we run merely ``letsencrypt --version``.
+ The functioning of the rest of the letsencrypt script is covered by
+ other test suites.
+
+ """
+ NEW_LE_AUTO = build_le_auto(version='99.9.9')
+ NEW_LE_AUTO_SIG = signed(NEW_LE_AUTO)
+
+ with ephemeral_dir() as venv_dir:
+ # This serves a PyPI page with a higher version, a GitHub-alike
+ # with a corresponding le-auto script, and a matching signature.
+ resources = {'': """
+
Directory listing for /
+
+ Directory listing for /
+
+
+
+
+ """, # TODO: Cut this down.
+ 'letsencrypt/json': dumps({'releases': {'99.9.9': None}}),
+ 'v99.9.9/letsencrypt-auto': NEW_LE_AUTO,
+ 'v99.9.9/letsencrypt-auto.sig': NEW_LE_AUTO_SIG}
+ with serving(resources) as base_url:
+ # Test when a phase-1 upgrade is needed, there's no LE binary
+ # installed, and peep verifies:
+ out, err = run_le_auto(venv_dir, base_url)
+ ok_(re.match(r'letsencrypt \d+\.\d+\.\d+',
+ err.strip().splitlines()[-1]))
+
+
+ # This conveniently sets us up to test the next 2 cases:
+ # Test when no phase-1 upgrade is needed and no LE upgrade is needed (probably a common case).
+
+ # Test (when no phase-1 upgrade is needed), there's an out-of-date LE script installed, (and peep works).
+ # Test when peep has a hash mismatch.
+ # Test when the OpenSSL sig mismatches.
diff --git a/letsencrypt_auto/tests/certs/ca/my-root-ca.crt.pem b/letsencrypt_auto/tests/certs/ca/my-root-ca.crt.pem
new file mode 100644
index 000000000..4e4d29bd2
--- /dev/null
+++ b/letsencrypt_auto/tests/certs/ca/my-root-ca.crt.pem
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIID5jCCAs6gAwIBAgIJAI1Qkfyw88REMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMRswGQYDVQQKExJNeSBCb2d1cyBS
+b290IENlcnQxFDASBgNVBAMTC2V4YW1wbGUuY29tMB4XDTE1MTIwNDIwNTIxNVoX
+DTQwMTIwMzIwNTIxNVowVTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3Rh
+dGUxGzAZBgNVBAoTEk15IEJvZ3VzIFJvb3QgQ2VydDEUMBIGA1UEAxMLZXhhbXBs
+ZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQVQpQ2EH4gTJB
+NJP6+ocT3xJwT8mSXYUnvzjj6iv+JxZiXRGzAPziNzrrSRKY0yDHF+UiJwuOerLa
+n8laZkLb1Ogqzs2u64rKeb0xWv90Qp+eXG0J/1xb4dw+GExqe5QFo1JUJzO/eK7m
+1S04SeFkN1qV9mD5yJUy7DGiTUzDHgCxM2tXMLusXYqkxsQQ9+2EJ7BEOK4YJGEx
+Sign5FuSxb64PiNow6OA97CaLl7tV4INP4w195ueDRIaS4poeOep4s8U7IAdMjIZ
+EryJgKNCij50xK92vPBBJSj0NOitltBlwoEqkOZpQCOZamFd6nvt78LQ6W8Am+l6
+y6oCON5JAgMBAAGjgbgwgbUwHQYDVR0OBBYEFAlrdStDhaayLLj89Whe3Gc+HE8y
+MIGFBgNVHSMEfjB8gBQJa3UrQ4Wmsiy4/PVoXtxnPhxPMqFZpFcwVTELMAkGA1UE
+BhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxGzAZBgNVBAoTEk15IEJvZ3VzIFJv
+b3QgQ2VydDEUMBIGA1UEAxMLZXhhbXBsZS5jb22CCQCNUJH8sPPERDAMBgNVHRME
+BTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQC7KAQfDTiNM3QO8Ic3x21CAPJUavkH
+zshifN+Ei0+nmseHDTCTgsGfGDOToLUpUEZ4PuiHnz08UwRfd9wotc3SgY9ZaXMe
+vRs8KUAF9EoyTvESzPyv2b6cS9NNMpj5y7KyXSyP17VoGbNavtiGQ4dwgEH6VgNl
+0RtBvcSBv/tqxIIx1tWzL74tVEm0Kbd9BAZsYpQNKL8e6WXP35/j0PvCCvtofGrA
+E8LTqMz4kCwnX+QaJIMJhBophRCsjXdAkvFbFxX0DGPztQtzIwBPcdMjsft7AFeE
+0XchhDDXxw8YsbpvPfCvrD8XiiVuBycbnB1zt0LLVwB/QsCzUW9ImpLC
+-----END CERTIFICATE-----
diff --git a/letsencrypt_auto/tests/certs/ca/my-root-ca.key.pem b/letsencrypt_auto/tests/certs/ca/my-root-ca.key.pem
new file mode 100644
index 000000000..9caa7ddaa
--- /dev/null
+++ b/letsencrypt_auto/tests/certs/ca/my-root-ca.key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEA0FUKUNhB+IEyQTST+vqHE98ScE/Jkl2FJ7844+or/icWYl0R
+swD84jc660kSmNMgxxflIicLjnqy2p/JWmZC29ToKs7NruuKynm9MVr/dEKfnlxt
+Cf9cW+HcPhhManuUBaNSVCczv3iu5tUtOEnhZDdalfZg+ciVMuwxok1Mwx4AsTNr
+VzC7rF2KpMbEEPfthCewRDiuGCRhMUooJ+RbksW+uD4jaMOjgPewmi5e7VeCDT+M
+Nfebng0SGkuKaHjnqeLPFOyAHTIyGRK8iYCjQoo+dMSvdrzwQSUo9DTorZbQZcKB
+KpDmaUAjmWphXep77e/C0OlvAJvpesuqAjjeSQIDAQABAoIBAH+qbVzneV3wxjwh
+HUHi/p3VyHXc3xh7iNq3mwRH/1eK2nPCttLsGwwBbnC64dOXJfH7maWZKcLRPAMv
+gfOM0RHn4bJB8tdrbizv91lke0DihvBDkWpb+1wvB4lh2Io0Wpwt3ojFUTfXm87G
++iQRWjbQmQlm5zyKh6uiBDSCjDTQdb9omZEBMAwlGPTZwt8TRUEtWd8QgW8FCHoB
+iLER2WBwXdvn3PBtocI3VE6IYDSeZ81Xv+d7925RtVintT8Suk4toYwX+jfSz+wZ
+sgHd5V6PSv9a7GUlWoUihD99D9wqDZE8IvMDZ5ofSAUd1KfICDtmsEyugY7u2yYZ
+tYt49AECgYEA73f7ITMHg8JsUipqb6eG10gCRtRhkqrrO1g/TNeTBh3CTrQGb56e
+y6kmUivn5gK46t3T2N4Ht4IR8fpLcJcbPYPQNulSjmWm5y6WduafXW/VCW1NA9Lc
+FyGPkMxFCIVJTLFxfLFepBVvtUzLLDKGGtQxru/GNbBzjdtmVfDPIoECgYEA3rbM
+cTfvj+jWrV1YsRbphyjy+k3OJEIVx6KA4s5d7Tp12UfYQp/B3HPhXXm5wqeo1Nos
+UAEWZIMi1VoE8iu6jjeJ6uERtbKKQVed25Us/ff0jUPbxlXgiBOtRcllq9d9Srjm
+ybHUgfjLsZ2/xpIcOl+oI5pDM9JvD8Sq4ZCFR8kCgYBK/H0tFjeiML2OtS2DLShy
+PWBJIbQ0I0Vp3eZkf5TQc30m/ASP61G6YItZa9pAElYpZbEy1cQA2MAZz9DTvt2O
+07ndmA57/KTY+6OuM+Vvctd5DjrxmZPFwoKcSvrLAkHDvETXUQtbwkKquRNeEawg
+tpWgPAELSufEYhGXk8KpAQKBgBDCqPgMQZcO6rj5QWdyVfi5+C8mE9Fet8ziSdjH
+twHXWG8VnQzGgQxaHCewtW4Ut/vsv1D2A/1kcQalU6H18IArZdGrRm3qFcV9FoAj
+5dLnChxncu6mH9Odx3htA52/BcrNx3B+VYPCeXHQcVI8RKuP71NelJgdygXhwwpe
+mekhAoGBAOUovnqylciYa9HRqo+xZk59eyX+ehhnlV8SeJ2K0PwaQkzQ0KYtCmE7
+kdSdhcv8h/IQKGaFfc/LyFMM/a26PfAeY5bj41UjkT0K5hQrYuL/52xaT401YLcb
+Xo+bZz9K0hrdP7TdZFuTY/WxojXgjsVAuAN1NwnJumqxhzPh+hfl
+-----END RSA PRIVATE KEY-----
diff --git a/letsencrypt_auto/tests/certs/ca/my-root-ca.srl b/letsencrypt_auto/tests/certs/ca/my-root-ca.srl
new file mode 100644
index 000000000..ad6d262b4
--- /dev/null
+++ b/letsencrypt_auto/tests/certs/ca/my-root-ca.srl
@@ -0,0 +1 @@
+D613482D0EF95DD0
diff --git a/letsencrypt_auto/tests/certs/localhost/cert.pem b/letsencrypt_auto/tests/certs/localhost/cert.pem
new file mode 100644
index 000000000..ac83535ce
--- /dev/null
+++ b/letsencrypt_auto/tests/certs/localhost/cert.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDKjCCAhICCQDWE0gtDvld0DANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJB
+VTETMBEGA1UECBMKU29tZS1TdGF0ZTEbMBkGA1UEChMSTXkgQm9ndXMgUm9vdCBD
+ZXJ0MRQwEgYDVQQDEwtleGFtcGxlLmNvbTAeFw0xNTEyMDQyMDU0MzFaFw00MDEy
+MDMyMDU0MzFaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEw
+HwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2Fs
+aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK2WIIi86Mis4UQH
+a5PrFbX2PBtQHbI3t3ekN1CewRsgQ/2X3lCeWhKmr3CJYXVgA7q/23PORQAiuV6y
+DG2dQIrjeahWCXaCptTi49ljfVRTW2IxrHke/iA8TkDuZbWGzVLb8TB83ipBOD41
+SjuomoN4A/ktnIfbNqRqgjjHs2wwJHDfxPiCQlwyOayjHmdlh8cqfVE8rWEm5/3T
+Iu0X1J53SammR1SbUmsLJNofxFYMK1ogHb0CaFEG9QuuUDPJl5K74Rr6InMQZKPn
+ne4W3cGoALxPHAca7yicpSMSmdsmd6pqylc2Fdua7o/wf0SwShxS4A1DqA/HWLEM
+V6MSEF8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAz5sMAFG6W/ZEULZITkBTCU6P
+NttpGiKufnqyBW5HyNylaczfnHnClvQjr8f/84xvKVcfC3xP0lz+92aIQqo+5L/n
+v7gLhBFR4Vr2XwMt2qz2FpkaxmVwnhVAHaaC05WIKQ6W2gDwWT0u1K8YdTh+7mvN
+AT9FW4vDgtNZWq4W/PePh9QCiOOQhGOuBYj/7zqLtz4XPifhi66ILIRDHiu0kond
+3YMFcECIAf4MPT9vT0iNcWX+c8CfAixPt8nMD6bzOo3oTcfuZh/2enfgLbMqOlOi
+uk72FM5VVPXTWAckJvL/vVjqsvDuJQKqbr0oUc3bdWbS36xtWZUycp4IQLguAQ==
+-----END CERTIFICATE-----
diff --git a/letsencrypt_auto/tests/certs/localhost/localhost.csr.pem b/letsencrypt_auto/tests/certs/localhost/localhost.csr.pem
new file mode 100644
index 000000000..8a6189f88
--- /dev/null
+++ b/letsencrypt_auto/tests/certs/localhost/localhost.csr.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICnjCCAYYCAQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUx
+ITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAxMJbG9j
+YWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArZYgiLzoyKzh
+RAdrk+sVtfY8G1Adsje3d6Q3UJ7BGyBD/ZfeUJ5aEqavcIlhdWADur/bc85FACK5
+XrIMbZ1AiuN5qFYJdoKm1OLj2WN9VFNbYjGseR7+IDxOQO5ltYbNUtvxMHzeKkE4
+PjVKO6iag3gD+S2ch9s2pGqCOMezbDAkcN/E+IJCXDI5rKMeZ2WHxyp9UTytYSbn
+/dMi7RfUnndJqaZHVJtSawsk2h/EVgwrWiAdvQJoUQb1C65QM8mXkrvhGvoicxBk
+o+ed7hbdwagAvE8cBxrvKJylIxKZ2yZ3qmrKVzYV25ruj/B/RLBKHFLgDUOoD8dY
+sQxXoxIQXwIDAQABoAAwDQYJKoZIhvcNAQEFBQADggEBAFbg3WrAokoPx7iAYG6z
+PqeDd4/XanXjeL4Ryxv6LoGhu69mmBAd3N5ILPyQJjnkWpIjEmJDzEcPMzhQjRh5
+GlWTyvKWO4zClYU840KZk7crVkpzNZ+HP0YeM/Agz6sab00ffRcq5m1wEF9MCvDE
+8FUXk1HBHRAb/6t9QV/7axsPOkGT8SjQ1v2SCaiB0HQL3sYChYLi5zu4dfmQNPGq
+ar9Xm5a0YqOQIFfmy8RSwxk0Q/ipNFTGN1uvlIRkgbT9zPnodxjWZsSI9BF+q5Af
+uiE/oAk7MxfJ0LyLfhOWB+T98bKIOVtFT3wMLS1IIgMogwqCEXFf30Q9p2iTEzqT
+6UE=
+-----END CERTIFICATE REQUEST-----
diff --git a/letsencrypt_auto/tests/certs/localhost/privkey.pem b/letsencrypt_auto/tests/certs/localhost/privkey.pem
new file mode 100644
index 000000000..18feba403
--- /dev/null
+++ b/letsencrypt_auto/tests/certs/localhost/privkey.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEArZYgiLzoyKzhRAdrk+sVtfY8G1Adsje3d6Q3UJ7BGyBD/Zfe
+UJ5aEqavcIlhdWADur/bc85FACK5XrIMbZ1AiuN5qFYJdoKm1OLj2WN9VFNbYjGs
+eR7+IDxOQO5ltYbNUtvxMHzeKkE4PjVKO6iag3gD+S2ch9s2pGqCOMezbDAkcN/E
++IJCXDI5rKMeZ2WHxyp9UTytYSbn/dMi7RfUnndJqaZHVJtSawsk2h/EVgwrWiAd
+vQJoUQb1C65QM8mXkrvhGvoicxBko+ed7hbdwagAvE8cBxrvKJylIxKZ2yZ3qmrK
+VzYV25ruj/B/RLBKHFLgDUOoD8dYsQxXoxIQXwIDAQABAoIBAG8bVJ+xKt6nqVg9
+16HKKw9ZGIfy888K0qgFuFImCzwtntdGycmYUdb2Uf0aMgNK/ZgfDXxGXuwDTdtK
+46GVsaY0i74vs8bjQZ2pzGVsxN+gqzFi0h6Es+w2LXBqJzfVnL6YgPykMB+jtzg6
+K9Wbyaq0uvZXN4XNzl/WvJtTV4i7Cff1MOd5EhKFdqxrZvB/SRBCr/SMMafRtB9P
+EvMneNKzhmlrutHAxuyxEKZR32Kkx7ydAdTjGgn+rE+NL5BweXfeWhLU4Bv14bn9
+Mkneu3w5o1ryJfE2YnVajUP//jeopUT0nTQ3MpEusBQCLBlvFXjjM9uCaFX+5+MP
+0H4xVcECgYEA1Q+wR3GHbk37vIGSlbENyUsri5WlMt8IVAHsDsTOpxAjYB0yyo+x
+h9RS+RJZQECJlA6H72peUl3GM7RgdWIcKOT3nZ12XqYKG57rr/N5zlUuxbdS8KBk
+JhyZeJdYjq/Jrno1ZP+OSmc7VvBLcM7irY7LHlvK0o8W1W0TNJ8jrZkCgYEA0JHX
+lJd+fiezcUS7g4moHtzJp0JKquQiXLX+c2urmpyhb3ZrTuQ8OUjSy6DlwHlgDx8K
+Hg2sdx/ZCuDaGjR4IY/Qs5RFt9WUqlK9gi9V3nYVrzBOQkdFOf/Ad3j4pQ8/aeCX
+nP6snHXz1WqPpbCXG6l6GzFGbQU473GfuKsDuLcCgYAWQaNKc0OQdDj9whNL68ji
+5CVSWXl+TOoTzHeaO1jS/s6TNbmei1AiPj3EovQL0DIO802j5tqfhAg2UntZB7yl
+UPXE0zQQQwv/QqSgJrDsqt1N7g6N8FNF3+rwO+8WSKqqvT1ipYd5ojsCo+tdh18K
+fkYdj70qLaRW+yPsdUtG0QKBgEYc8NqbvsML94+ZKmwCh4iwcf2PFGi0PjTqXTpR
+tKNKCh7dMR+ZLAGZ0HrxgKqeYsNSjOUjdZmqFB1LDyaGAuhNXzwvGOy+mLZVEC3G
+Wdhp28pDs9sl+EiSCBJhkTxzjr656F23YzFJmYlhxB5P6cw7wbeIbgNSIRylFqtO
+mfarAoGBAICsAEWypOctxtmtOcjxgJ7jMbOA7rrsGlXpiy1/WlwIwRGF5LMvIIFX
+qFAfiPcZn05ZgdAGzaFYowdjmQB10FW0jZbDf+nIHfOF5YmfmfWjsaweEGALJmqB
+okGu/lGNGf3XoYzy0/hC3WAqk3znSZtQLUq8jEWF7dLNUizUeUow
+-----END RSA PRIVATE KEY-----
diff --git a/letsencrypt_auto/tests/certs/localhost/server.pem b/letsencrypt_auto/tests/certs/localhost/server.pem
new file mode 100644
index 000000000..c5765dd89
--- /dev/null
+++ b/letsencrypt_auto/tests/certs/localhost/server.pem
@@ -0,0 +1,46 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEArZYgiLzoyKzhRAdrk+sVtfY8G1Adsje3d6Q3UJ7BGyBD/Zfe
+UJ5aEqavcIlhdWADur/bc85FACK5XrIMbZ1AiuN5qFYJdoKm1OLj2WN9VFNbYjGs
+eR7+IDxOQO5ltYbNUtvxMHzeKkE4PjVKO6iag3gD+S2ch9s2pGqCOMezbDAkcN/E
++IJCXDI5rKMeZ2WHxyp9UTytYSbn/dMi7RfUnndJqaZHVJtSawsk2h/EVgwrWiAd
+vQJoUQb1C65QM8mXkrvhGvoicxBko+ed7hbdwagAvE8cBxrvKJylIxKZ2yZ3qmrK
+VzYV25ruj/B/RLBKHFLgDUOoD8dYsQxXoxIQXwIDAQABAoIBAG8bVJ+xKt6nqVg9
+16HKKw9ZGIfy888K0qgFuFImCzwtntdGycmYUdb2Uf0aMgNK/ZgfDXxGXuwDTdtK
+46GVsaY0i74vs8bjQZ2pzGVsxN+gqzFi0h6Es+w2LXBqJzfVnL6YgPykMB+jtzg6
+K9Wbyaq0uvZXN4XNzl/WvJtTV4i7Cff1MOd5EhKFdqxrZvB/SRBCr/SMMafRtB9P
+EvMneNKzhmlrutHAxuyxEKZR32Kkx7ydAdTjGgn+rE+NL5BweXfeWhLU4Bv14bn9
+Mkneu3w5o1ryJfE2YnVajUP//jeopUT0nTQ3MpEusBQCLBlvFXjjM9uCaFX+5+MP
+0H4xVcECgYEA1Q+wR3GHbk37vIGSlbENyUsri5WlMt8IVAHsDsTOpxAjYB0yyo+x
+h9RS+RJZQECJlA6H72peUl3GM7RgdWIcKOT3nZ12XqYKG57rr/N5zlUuxbdS8KBk
+JhyZeJdYjq/Jrno1ZP+OSmc7VvBLcM7irY7LHlvK0o8W1W0TNJ8jrZkCgYEA0JHX
+lJd+fiezcUS7g4moHtzJp0JKquQiXLX+c2urmpyhb3ZrTuQ8OUjSy6DlwHlgDx8K
+Hg2sdx/ZCuDaGjR4IY/Qs5RFt9WUqlK9gi9V3nYVrzBOQkdFOf/Ad3j4pQ8/aeCX
+nP6snHXz1WqPpbCXG6l6GzFGbQU473GfuKsDuLcCgYAWQaNKc0OQdDj9whNL68ji
+5CVSWXl+TOoTzHeaO1jS/s6TNbmei1AiPj3EovQL0DIO802j5tqfhAg2UntZB7yl
+UPXE0zQQQwv/QqSgJrDsqt1N7g6N8FNF3+rwO+8WSKqqvT1ipYd5ojsCo+tdh18K
+fkYdj70qLaRW+yPsdUtG0QKBgEYc8NqbvsML94+ZKmwCh4iwcf2PFGi0PjTqXTpR
+tKNKCh7dMR+ZLAGZ0HrxgKqeYsNSjOUjdZmqFB1LDyaGAuhNXzwvGOy+mLZVEC3G
+Wdhp28pDs9sl+EiSCBJhkTxzjr656F23YzFJmYlhxB5P6cw7wbeIbgNSIRylFqtO
+mfarAoGBAICsAEWypOctxtmtOcjxgJ7jMbOA7rrsGlXpiy1/WlwIwRGF5LMvIIFX
+qFAfiPcZn05ZgdAGzaFYowdjmQB10FW0jZbDf+nIHfOF5YmfmfWjsaweEGALJmqB
+okGu/lGNGf3XoYzy0/hC3WAqk3znSZtQLUq8jEWF7dLNUizUeUow
+-----END RSA PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIIDKjCCAhICCQDWE0gtDvld0DANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJB
+VTETMBEGA1UECBMKU29tZS1TdGF0ZTEbMBkGA1UEChMSTXkgQm9ndXMgUm9vdCBD
+ZXJ0MRQwEgYDVQQDEwtleGFtcGxlLmNvbTAeFw0xNTEyMDQyMDU0MzFaFw00MDEy
+MDMyMDU0MzFaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEw
+HwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2Fs
+aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK2WIIi86Mis4UQH
+a5PrFbX2PBtQHbI3t3ekN1CewRsgQ/2X3lCeWhKmr3CJYXVgA7q/23PORQAiuV6y
+DG2dQIrjeahWCXaCptTi49ljfVRTW2IxrHke/iA8TkDuZbWGzVLb8TB83ipBOD41
+SjuomoN4A/ktnIfbNqRqgjjHs2wwJHDfxPiCQlwyOayjHmdlh8cqfVE8rWEm5/3T
+Iu0X1J53SammR1SbUmsLJNofxFYMK1ogHb0CaFEG9QuuUDPJl5K74Rr6InMQZKPn
+ne4W3cGoALxPHAca7yicpSMSmdsmd6pqylc2Fdua7o/wf0SwShxS4A1DqA/HWLEM
+V6MSEF8CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAz5sMAFG6W/ZEULZITkBTCU6P
+NttpGiKufnqyBW5HyNylaczfnHnClvQjr8f/84xvKVcfC3xP0lz+92aIQqo+5L/n
+v7gLhBFR4Vr2XwMt2qz2FpkaxmVwnhVAHaaC05WIKQ6W2gDwWT0u1K8YdTh+7mvN
+AT9FW4vDgtNZWq4W/PePh9QCiOOQhGOuBYj/7zqLtz4XPifhi66ILIRDHiu0kond
+3YMFcECIAf4MPT9vT0iNcWX+c8CfAixPt8nMD6bzOo3oTcfuZh/2enfgLbMqOlOi
+uk72FM5VVPXTWAckJvL/vVjqsvDuJQKqbr0oUc3bdWbS36xtWZUycp4IQLguAQ==
+-----END CERTIFICATE-----
diff --git a/letsencrypt_auto/tests/signing.key b/letsencrypt_auto/tests/signing.key
new file mode 100644
index 000000000..b9964d00c
--- /dev/null
+++ b/letsencrypt_auto/tests/signing.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAsMoSzLYQ7E1sdSOkwelgtzKIh2qi3bpXuYtcfFC0XrvWig07
+1NwIj+dZiT0OLZ2hPispEH0B7ISuuWg1ll7GhFW0VdbxL6JdGzS2ShNWkX9hE9z+
+j8VqwDPOBn3ZHm03qwpYkBDwQib3KqOdYbTTuUtJmmGcuk3a9Aq/sCT6DdfmTSdP
+5asdQYwIcaQreDrOosaS84DTWI3IU+UYJVglLsIVPBuy9IcgHidUQ96hJnoPsDCW
+sHwX62495QKEarauyKQrJzFes0EY95orDM47Z5o/NDiQB11m91yNB0MmPYY9QSbn
+OA9j7IaaC97AwRLuwXY+/R2ablTcxurWou68iQIDAQABAoIBAQCJE3W2Mqk2f+XL
+geKa1BjAkzcXQJCduYGRhUQlw/HGzoBPtGki56Tf53MeHTAkIGfIq3CAr1zRhiNv
+8SQzvrLQIx/buvhxhcQJdzqsfwgNcqXT3/OliF34P3LMx8GUfPy/6xq2Qdv4fvwA
+nLJH8wyDTKP6RxtdvUY7GSZ+Ln2QQv/3Nco7tax4GHNGom8iSgeH/YKTDnvitdqh
+a0fr930QzU39TfOftLmasdmKUOIg8G2wr4Sy6Kn060+OUoQr1fZF5mnLvvQeILCK
+uav91JkIeMLggzk+t88IJUFWdOoxv5hWTnNzHyt+/GYfovyRz2fKQMwzdh1F8iM5
++867rEb9AoGBANn1ncemJBedDshStdCBUH0+2ExPrawveaXOZKnx8/VGFXNi0hAf
+KzkntMWd5g5kB077FtKO9CYTBvK4pZBWIFLcJEqAz88JeXME6dfUbRucDr72ko+l
+rcLHXj7F0IDVzj/9CphMGAhC9J/4YW9SPcSbMw6dQ6xOk73f1Vowve0DAoGBAM+k
+/F+hVqCS3f22Bg9KuDtx+zCydaZxC842DgIkV1SO2iFhNHjnpQ5EIR0WrSYeV2n+
+rD7kVs5OH1HvnGScHaQKtAVqZClSwF14jzE+Aj8XDwxiHLSOhJgKlzfVX7h1ymMh
+7fsslDl6xNGQ+40gubhkCLT5qABFKy1mrZ8b+3yDAoGAGLGUI6d2FVrM7vM3+Bx+
+gwIYvWSVl5l1XcypaPupmRNMoNsEU6FEY2BVQcJm6yB4F4GpD0f0709ejSdQUq7/
+UIPydKJtaNZ49QgMelBt4B/pJ8eFyVKLAjNWQSRmQAJ5MJS5m5Gbc2wqjOk2GMen
+idvPiAtXPHFWmb9/S42UJwMCgYEAjymAe2qgcGtyNNfIC8kHhqzKdEPGi/ALJKzu
+MZnewEURrcv4QpfrnA9rCUQ2Mz7eJA1bsqz6EJmaTIK4wEFGynA6uDUnQ7pzOL7D
+cz7+i4MZc/89LVvJnY5Hvk4WBfboiDq/etq8g3jatGaSmTYD9la6DhTHORB3eYD+
+meHQHYMCgYEA18y9hnx2k4vNeBei4YXF4pAvKdwKLQD+CcP9ljb3VT+kXktjRA1C
+aWj3HhMwvcxtttfkQzEnwwGRAkTEtNewJ8KFxhmc9nYElZTNZ+SuHD5Dkv8xqoj8
+NvG8rU1eiEyPwE2wQxpM5JLqbo7IWtR0dmptjKoF1gRxn6Wh4TwEiHA=
+-----END RSA PRIVATE KEY-----