Merge branch 'master' into dialogerror

This commit is contained in:
Amjad Mashaal 2016-05-26 01:00:04 +02:00
commit d1e9ffbe9a
29 changed files with 418 additions and 272 deletions

View file

@ -4,26 +4,18 @@ cache:
directories:
- $HOME/.cache/pip
services:
- rabbitmq
- mariadb
# apacheconftest
#- apache2
# This makes sure we get a host with docker-compose present.
dist: trusty
# http://docs.travis-ci.com/user/ci-environment/#CI-environment-OS
# gimme has to be kept in sync with Boulder's Go version setting in .travis.yml
before_install:
- 'dpkg -s libaugeas0'
- '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || eval "$(gimme 1.5.1)"'
# using separate envs with different TOXENVs creates 4x1 Travis build
# matrix, which allows us to clearly distinguish which component under
# test has failed
env:
global:
- GOPATH=/tmp/go
- PATH=$GOPATH/bin:$PATH
- GO15VENDOREXPERIMENT=1 # Fixes problems with vendor directories
- BOULDERPATH=$PWD/boulder/
matrix:
include:
@ -93,7 +85,6 @@ addons:
- boulder
- boulder-mysql
- boulder-rabbitmq
mariadb: "10.0"
apt:
sources:
- augeas
@ -109,13 +100,11 @@ addons:
# For certbot-nginx integration testing
- nginx-light
- openssl
# For Boulder integration testing
- rsyslog
# for apacheconftest
#- apache2
#- libapache2-mod-wsgi
#- libapache2-mod-macro
#- sudo
- apache2
- libapache2-mod-wsgi
- libapache2-mod-macro
- sudo
install: "travis_retry pip install tox coveralls"
script: 'travis_retry tox && ([ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/travis-integration.sh)'

View file

@ -1,4 +1,5 @@
"""Crypto utilities."""
import binascii
import contextlib
import logging
import re
@ -203,7 +204,7 @@ def gen_ss_cert(key, domains, not_before=None,
"""
assert domains, "Must provide one or more hostnames for the cert."
cert = OpenSSL.crypto.X509()
cert.set_serial_number(1337)
cert.set_serial_number(int(binascii.hexlify(OpenSSL.rand.bytes(16)), 16))
cert.set_version(2)
extensions = [

View file

@ -8,6 +8,8 @@ import unittest
import six
from six.moves import socketserver # pylint: disable=import-error
import OpenSSL
from acme import errors
from acme import jose
from acme import test_util
@ -126,5 +128,23 @@ class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
self._get_idn_names())
class RandomSnTest(unittest.TestCase):
"""Test for random certificate serial numbers."""
def setUp(self):
self.cert_count = 5
self.serial_num = []
self.key = OpenSSL.crypto.PKey()
self.key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
def test_sn_collisions(self):
from acme.crypto_util import gen_ss_cert
for _ in range(self.cert_count):
cert = gen_ss_cert(self.key, ['dummy'], force_san=True)
self.serial_num.append(cert.get_serial_number())
self.assertTrue(len(set(self.serial_num)) > 1)
if __name__ == '__main__':
unittest.main() # pragma: no cover

View file

@ -6,10 +6,8 @@ import logging
import logging.handlers
import os
import sys
import traceback
import configargparse
import OpenSSL
import six
import certbot
@ -336,36 +334,41 @@ class HelpfulArgumentParser(object):
# Do any post-parsing homework here
if self.verb == "renew":
parsed_args.noninteractive_mode = True
if parsed_args.staging or parsed_args.dry_run:
if parsed_args.server not in (flag_default("server"), constants.STAGING_URI):
conflicts = ["--staging"] if parsed_args.staging else []
conflicts += ["--dry-run"] if parsed_args.dry_run else []
raise errors.Error("--server value conflicts with {0}".format(
" and ".join(conflicts)))
parsed_args.server = constants.STAGING_URI
if parsed_args.dry_run:
if self.verb not in ["certonly", "renew"]:
raise errors.Error("--dry-run currently only works with the "
"'certonly' or 'renew' subcommands (%r)" % self.verb)
parsed_args.break_my_certs = parsed_args.staging = True
if glob.glob(os.path.join(parsed_args.config_dir, constants.ACCOUNTS_DIR, "*")):
# The user has a prod account, but might not have a staging
# one; we don't want to start trying to perform interactive registration
parsed_args.tos = True
parsed_args.register_unsafely_without_email = True
self.set_test_server(parsed_args)
if parsed_args.csr:
if parsed_args.allow_subset_of_names:
raise errors.Error("--allow-subset-of-names "
"cannot be used with --csr")
self.handle_csr(parsed_args)
hooks.validate_hooks(parsed_args)
return parsed_args
def set_test_server(self, parsed_args):
"""We have --staging/--dry-run; perform sanity check and set config.server"""
if parsed_args.server not in (flag_default("server"), constants.STAGING_URI):
conflicts = ["--staging"] if parsed_args.staging else []
conflicts += ["--dry-run"] if parsed_args.dry_run else []
raise errors.Error("--server value conflicts with {0}".format(
" and ".join(conflicts)))
parsed_args.server = constants.STAGING_URI
if parsed_args.dry_run:
if self.verb not in ["certonly", "renew"]:
raise errors.Error("--dry-run currently only works with the "
"'certonly' or 'renew' subcommands (%r)" % self.verb)
parsed_args.break_my_certs = parsed_args.staging = True
if glob.glob(os.path.join(parsed_args.config_dir, constants.ACCOUNTS_DIR, "*")):
# The user has a prod account, but might not have a staging
# one; we don't want to start trying to perform interactive registration
parsed_args.tos = True
parsed_args.register_unsafely_without_email = True
def handle_csr(self, parsed_args):
"""Process a --csr flag."""
if parsed_args.verb != "certonly":
@ -373,21 +376,11 @@ class HelpfulArgumentParser(object):
"when obtaining a new or replacement "
"via the certonly command. Please try the "
"certonly command instead.")
if parsed_args.allow_subset_of_names:
raise errors.Error("--allow-subset-of-names cannot be used with --csr")
try:
csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="der")
typ = OpenSSL.crypto.FILETYPE_ASN1
domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1)
except OpenSSL.crypto.Error:
try:
e1 = traceback.format_exc()
typ = OpenSSL.crypto.FILETYPE_PEM
csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="pem")
domains = crypto_util.get_sans_from_csr(csr.data, typ)
except OpenSSL.crypto.Error:
logger.debug("DER CSR parse error %s", e1)
logger.debug("PEM CSR parse error %s", traceback.format_exc())
raise errors.Error("Failed to parse CSR file: {0}".format(parsed_args.csr[0]))
csrfile, contents = parsed_args.csr[0:2]
typ, csr, domains = crypto_util.import_csr_file(csrfile, contents)
# This is not necessary for webroot to work, however,
# obtain_certificate_from_csr requires parsed_args.domains to be set

View file

@ -24,6 +24,7 @@ from certbot import interfaces
from certbot import le_util
from certbot import reverter
from certbot import storage
from certbot import cli
from certbot.display import ops as display_ops
from certbot.display import enhancements
@ -317,23 +318,30 @@ class Client(object):
cert_pem = OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped)
cert_file, act_cert_path = le_util.unique_file(cert_path, 0o644)
cert_file, abs_cert_path = _open_pem_file('cert_path', cert_path)
try:
cert_file.write(cert_pem)
finally:
cert_file.close()
logger.info("Server issued certificate; certificate written to %s",
act_cert_path)
abs_cert_path)
cert_chain_abspath = None
fullchain_abspath = None
if chain_cert:
if not chain_cert:
return abs_cert_path, None, None
else:
chain_pem = crypto_util.dump_pyopenssl_chain(chain_cert)
cert_chain_abspath = _save_chain(chain_pem, chain_path)
fullchain_abspath = _save_chain(cert_pem + chain_pem,
fullchain_path)
return os.path.abspath(act_cert_path), cert_chain_abspath, fullchain_abspath
chain_file, abs_chain_path =\
_open_pem_file('chain_path', chain_path)
fullchain_file, abs_fullchain_path =\
_open_pem_file('fullchain_path', fullchain_path)
_save_chain(chain_pem, chain_file)
_save_chain(cert_pem + chain_pem, fullchain_file)
return abs_cert_path, abs_chain_path, abs_fullchain_path
def deploy_certificate(self, domains, privkey_path,
cert_path, chain_path, fullchain_path):
@ -565,24 +573,35 @@ def view_config_changes(config, num=None):
rev.recovery_routine()
rev.view_config_changes(num)
def _open_pem_file(cli_arg_path, pem_path):
"""Open a pem file.
def _save_chain(chain_pem, chain_path):
If cli_arg_path was set by the client, open that.
Otherwise, uniquify the file path.
:param str cli_arg_path: the cli arg name, e.g. cert_path
:param str pem_path: the pem file path to open
:returns: a tuple of file object and its absolute file path
"""
if cli.set_by_cli(cli_arg_path):
return le_util.safe_open(pem_path, chmod=0o644),\
os.path.abspath(pem_path)
else:
uniq = le_util.unique_file(pem_path, 0o644)
return uniq[0], os.path.abspath(uniq[1])
def _save_chain(chain_pem, chain_file):
"""Saves chain_pem at a unique path based on chain_path.
:param str chain_pem: certificate chain in PEM format
:param str chain_path: candidate path for the cert chain
:returns: absolute path to saved cert chain
:rtype: str
:param str chain_file: chain file object
"""
chain_file, act_chain_path = le_util.unique_file(chain_path, 0o644)
try:
chain_file.write(chain_pem)
finally:
chain_file.close()
logger.info("Cert chain written to %s", act_chain_path)
# This expects a valid chain file
return os.path.abspath(act_chain_path)
logger.info("Cert chain written to %s", chain_file.name)

View file

@ -6,6 +6,7 @@
"""
import logging
import os
import traceback
import OpenSSL
import pyrfc3339
@ -179,6 +180,30 @@ def csr_matches_pubkey(csr, privkey):
return False
def import_csr_file(csrfile, data):
"""Import a CSR file, which can be either PEM or DER.
:param str csrfile: CSR filename
:param str data: contents of the CSR file
:returns: (`OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1`,
le_util.CSR object representing the CSR,
list of domains requested in the CSR)
:rtype: tuple
"""
for form, typ in (("der", OpenSSL.crypto.FILETYPE_ASN1,),
("pem", OpenSSL.crypto.FILETYPE_PEM,),):
try:
domains = get_names_from_csr(data, typ)
except OpenSSL.crypto.Error:
logger.debug("CSR parse error (form=%s, typ=%s):", form, typ)
logger.debug(traceback.format_exc())
continue
return typ, le_util.CSR(file=csrfile, data=data, form=form), domains
raise errors.Error("Failed to parse CSR file: {0}".format(csrfile))
def make_key(bits):
"""Generate PEM encoded RSA key.
@ -228,15 +253,20 @@ def pyopenssl_load_certificate(data):
str(error) for error in openssl_errors)))
def _get_sans_from_cert_or_req(cert_or_req_str, load_func,
typ=OpenSSL.crypto.FILETYPE_PEM):
def _load_cert_or_req(cert_or_req_str, load_func,
typ=OpenSSL.crypto.FILETYPE_PEM):
try:
cert_or_req = load_func(typ, cert_or_req_str)
return load_func(typ, cert_or_req_str)
except OpenSSL.crypto.Error as error:
logger.exception(error)
raise
def _get_sans_from_cert_or_req(cert_or_req_str, load_func,
typ=OpenSSL.crypto.FILETYPE_PEM):
# pylint: disable=protected-access
return acme_crypto_util._pyopenssl_cert_or_req_san(cert_or_req)
return acme_crypto_util._pyopenssl_cert_or_req_san(_load_cert_or_req(
cert_or_req_str, load_func, typ))
def get_sans_from_cert(cert, typ=OpenSSL.crypto.FILETYPE_PEM):
@ -267,6 +297,25 @@ def get_sans_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM):
csr, OpenSSL.crypto.load_certificate_request, typ)
def get_names_from_csr(csr, typ=OpenSSL.crypto.FILETYPE_PEM):
"""Get a list of domains from a CSR, including the CN if it is set.
:param str csr: CSR (encoded).
:param typ: `OpenSSL.crypto.FILETYPE_PEM` or `OpenSSL.crypto.FILETYPE_ASN1`
:returns: A list of domain names.
:rtype: list
"""
loaded_csr = _load_cert_or_req(
csr, OpenSSL.crypto.load_certificate_request, typ)
# Use a set to avoid duplication with CN and Subject Alt Names
domains = set(d for d in (loaded_csr.get_subject().CN,) if d is not None)
# pylint: disable=protected-access
domains.update(acme_crypto_util._pyopenssl_cert_or_req_san(loaded_csr))
return list(domains)
def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM):
"""Dump certificate chain into a bundle.

View file

@ -1,6 +1,9 @@
"""Utilities for all Certbot."""
import argparse
import collections
# distutils.version under virtualenv confuses pylint
# For more info, see: https://github.com/PyCQA/pylint/issues/73
import distutils.version # pylint: disable=import-error,no-name-in-module
import errno
import logging
import os
@ -151,7 +154,8 @@ def _unique_file(path, filename_pat, count, mode):
while True:
current_path = os.path.join(path, filename_pat(count))
try:
return safe_open(current_path, chmod=mode), current_path
return safe_open(current_path, chmod=mode),\
os.path.abspath(current_path)
except OSError as err:
# "File exists," is okay, try a different name.
if err.errno != errno.EEXIST:
@ -342,3 +346,17 @@ def enforce_domain_sanity(domain):
if not fqdn.match(domain):
raise errors.ConfigurationError("Requested domain {0} is not a FQDN".format(domain))
return domain
def get_strict_version(normalized):
"""Converts a normalized version to a strict version.
:param str normalized: normalized version string
:returns: An equivalent strict version
:rtype: distutils.version.StrictVersion
"""
# strict version ending with "a" and a number designates a pre-release
# pylint: disable=no-member
return distutils.version.StrictVersion(normalized.replace(".dev", "a"))

View file

@ -685,9 +685,6 @@ def main(cli_args=sys.argv[1:]):
displayer = display_util.NoninteractiveDisplay(sys.stdout)
elif config.text_mode:
displayer = display_util.FileDisplay(sys.stdout)
elif config.verb == "renew":
config.noninteractive_mode = True
displayer = display_util.NoninteractiveDisplay(sys.stdout)
else:
displayer = display_util.NcursesDisplay()
zope.component.provideUtility(displayer)

View file

@ -181,12 +181,8 @@ to serve all files under specified web root ({0})."""
os.chown(self.full_roots[name], stat_path.st_uid,
stat_path.st_gid)
except OSError as exception:
if exception.errno == errno.EACCES:
logger.debug("Insufficient permissions to change owner and uid - ignoring")
else:
raise errors.PluginError(
"Couldn't create root for {0} http-01 "
"challenge responses: {1}", name, exception)
logger.info("Unable to change owner and uid of webroot directory")
logger.debug("Error was: %s", exception)
except OSError as exception:
if exception.errno != errno.EEXIST:
@ -235,17 +231,9 @@ to serve all files under specified web root ({0})."""
logger.debug("All challenges cleaned up, removing %s",
root_path)
except OSError as exc:
if exc.errno == errno.ENOTEMPTY:
logger.debug("Challenges cleaned up but %s not empty",
root_path)
elif exc.errno == errno.EACCES:
logger.debug("Challenges cleaned up but no permissions for %s",
root_path)
elif exc.errno == errno.ENOENT:
logger.debug("Challenges cleaned up, %s does not exists",
root_path)
else:
raise
logger.info(
"Unable to clean up challenge directory %s", root_path)
logger.debug("Error was: %s", exc)
class _WebrootMapAction(argparse.Action):

View file

@ -138,15 +138,10 @@ class AuthenticatorTest(unittest.TestCase):
os.chmod(self.path, 0o700)
@mock.patch("certbot.plugins.webroot.os.chown")
def test_failed_chown_eacces(self, mock_chown):
def test_failed_chown(self, mock_chown):
mock_chown.side_effect = OSError(errno.EACCES, "msg")
self.auth.perform([self.achall]) # exception caught and logged
@mock.patch("certbot.plugins.webroot.os.chown")
def test_failed_chown_not_eacces(self, mock_chown):
mock_chown.side_effect = OSError()
self.assertRaises(errors.PluginError, self.auth.perform, [])
def test_perform_permissions(self):
self.auth.prepare()
@ -200,7 +195,7 @@ class AuthenticatorTest(unittest.TestCase):
os.rmdir(leftover_path)
@mock.patch('os.rmdir')
def test_cleanup_permission_denied(self, mock_rmdir):
def test_cleanup_failure(self, mock_rmdir):
self.auth.prepare()
self.auth.perform([self.achall])
@ -212,32 +207,6 @@ class AuthenticatorTest(unittest.TestCase):
self.assertFalse(os.path.exists(self.validation_path))
self.assertTrue(os.path.exists(self.root_challenge_path))
@mock.patch('os.rmdir')
def test_cleanup_oserror(self, mock_rmdir):
self.auth.prepare()
self.auth.perform([self.achall])
os_error = OSError()
os_error.errno = errno.EPERM
mock_rmdir.side_effect = os_error
self.assertRaises(OSError, self.auth.cleanup, [self.achall])
self.assertFalse(os.path.exists(self.validation_path))
self.assertTrue(os.path.exists(self.root_challenge_path))
@mock.patch('os.rmdir')
def test_cleanup_file_not_exists(self, mock_rmdir):
self.auth.prepare()
self.auth.perform([self.achall])
os_error = OSError()
os_error.errno = errno.ENOENT
mock_rmdir.side_effect = os_error
self.auth.cleanup([self.achall])
self.assertFalse(os.path.exists(self.validation_path))
self.assertTrue(os.path.exists(self.root_challenge_path))
class WebrootActionTest(unittest.TestCase):
"""Tests for webroot argparse actions."""

View file

@ -7,6 +7,7 @@ import shutil
import time
import traceback
import zope.component
from certbot import constants
@ -489,7 +490,7 @@ class Reverter(object):
if not os.path.exists(changes_since_path):
logger.info("Rollback checkpoint is empty (no changes made?)")
with open(self.config.changes_since_path) as f:
with open(changes_since_path, 'w') as f:
f.write("No changes\n")
# Add title to self.config.in_progress_dir CHANGES_SINCE

View file

@ -8,6 +8,7 @@ import configobj
import parsedatetime
import pytz
import certbot
from certbot import constants
from certbot import crypto_util
from certbot import errors
@ -17,6 +18,7 @@ from certbot import le_util
logger = logging.getLogger(__name__)
ALL_FOUR = ("cert", "privkey", "chain", "fullchain")
CURRENT_VERSION = le_util.get_strict_version(certbot.__version__)
def config_with_defaults(config=None):
@ -63,6 +65,7 @@ def write_renewal_config(o_filename, n_filename, target, relevant_data):
"""
config = configobj.ConfigObj(o_filename)
config["version"] = certbot.__version__
for kind in ALL_FOUR:
config[kind] = target[kind]
@ -259,6 +262,14 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
"renewal config file {0} is missing a required "
"file reference".format(self.configfile))
conf_version = self.configuration.get("version")
if (conf_version is not None and
le_util.get_strict_version(conf_version) > CURRENT_VERSION):
logger.warning(
"Attempting to parse the version %s renewal configuration "
"file found at %s with version %s of Certbot. This might not "
"work.", conf_version, config_filename, certbot.__version__)
self.cert = self.configuration["cert"]
self.privkey = self.configuration["privkey"]
self.chain = self.configuration["chain"]

View file

@ -349,8 +349,9 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
['-d', '204.11.231.35'])
def test_csr_with_besteffort(self):
args = ["--csr", CSR, "--allow-subset-of-names"]
self.assertRaises(errors.Error, self._call, args)
self.assertRaises(
errors.Error, self._call,
'certonly --csr {0} --allow-subset-of-names'.format(CSR).split())
def test_run_with_csr(self):
# This is an error because you can only use --csr with certonly
@ -361,6 +362,17 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
return
assert False, "Expected supplying --csr to fail with default verb"
def test_csr_with_no_domains(self):
self.assertRaises(
errors.Error, self._call,
'certonly --csr {0}'.format(
test_util.vector_path('csr-nonames.pem')).split())
def test_csr_with_inconsistent_domains(self):
self.assertRaises(
errors.Error, self._call,
'certonly -d example.org --csr {0}'.format(CSR).split())
def _get_argument_parser(self):
plugins = disco.PluginsRegistry.find_all()
return functools.partial(cli.prepare_and_parse_args, plugins)

View file

@ -134,57 +134,39 @@ class ClientTest(unittest.TestCase):
self.acme.fetch_chain.assert_called_once_with(mock.sentinel.certr)
# FIXME move parts of this to test_cli.py...
@mock.patch("certbot.client.logger")
def test_obtain_certificate_from_csr(self, mock_logger):
self._mock_obtain_certificate()
from certbot import cli
test_csr = le_util.CSR(form="der", file=None, data=CSR_SAN)
mock_parsed_args = mock.MagicMock()
# The CLI should believe that this is a certonly request, because
# a CSR would not be allowed with other kinds of requests!
mock_parsed_args.verb = "certonly"
with mock.patch("certbot.client.le_util.CSR") as mock_CSR:
mock_CSR.return_value = test_csr
mock_parsed_args.domains = self.eg_domains[:]
mock_parser = mock.MagicMock(cli.HelpfulArgumentParser)
cli.HelpfulArgumentParser.handle_csr(mock_parser, mock_parsed_args)
auth_handler = self.client.auth_handler
# Now provoke an inconsistent domains error...
mock_parsed_args.domains.append("hippopotamus.io")
self.assertRaises(errors.ConfigurationError,
cli.HelpfulArgumentParser.handle_csr, mock_parser, mock_parsed_args)
authzr = self.client.auth_handler.get_authorizations(self.eg_domains, False)
self.assertEqual(
(mock.sentinel.certr, mock.sentinel.chain),
self.client.obtain_certificate_from_csr(
self.eg_domains,
test_csr,
authzr=authzr))
# and that the cert was obtained correctly
self._check_obtain_certificate()
# Test for authzr=None
self.assertEqual(
(mock.sentinel.certr, mock.sentinel.chain),
self.client.obtain_certificate_from_csr(
self.eg_domains,
test_csr,
authzr=None))
self.client.auth_handler.get_authorizations.assert_called_with(
self.eg_domains)
# Test for no auth_handler
self.client.auth_handler = None
self.assertRaises(
errors.Error,
self.client.obtain_certificate_from_csr,
authzr = auth_handler.get_authorizations(self.eg_domains, False)
self.assertEqual(
(mock.sentinel.certr, mock.sentinel.chain),
self.client.obtain_certificate_from_csr(
self.eg_domains,
test_csr)
mock_logger.warning.assert_called_once_with(mock.ANY)
test_csr,
authzr=authzr))
# and that the cert was obtained correctly
self._check_obtain_certificate()
# Test for authzr=None
self.assertEqual(
(mock.sentinel.certr, mock.sentinel.chain),
self.client.obtain_certificate_from_csr(
self.eg_domains,
test_csr,
authzr=None))
auth_handler.get_authorizations.assert_called_with(self.eg_domains)
# Test for no auth_handler
self.client.auth_handler = None
self.assertRaises(
errors.Error,
self.client.obtain_certificate_from_csr,
self.eg_domains,
test_csr)
mock_logger.warning.assert_called_once_with(mock.ANY)
@mock.patch("certbot.client.crypto_util")
def test_obtain_certificate(self, mock_crypto_util):
@ -221,7 +203,9 @@ class ClientTest(unittest.TestCase):
mock.sentinel.key, domains, self.config.csr_dir)
self._check_obtain_certificate()
def test_save_certificate(self):
@mock.patch("certbot.cli.helpful_parser")
def test_save_certificate(self, mock_parser):
# pylint: disable=too-many-locals
certs = ["matching_cert.pem", "cert.pem", "cert-san.pem"]
tmp_path = tempfile.mkdtemp()
os.chmod(tmp_path, 0o755) # TODO: really??
@ -232,6 +216,10 @@ class ClientTest(unittest.TestCase):
candidate_cert_path = os.path.join(tmp_path, "certs", "cert.pem")
candidate_chain_path = os.path.join(tmp_path, "chains", "chain.pem")
candidate_fullchain_path = os.path.join(tmp_path, "chains", "fullchain.pem")
mock_parser.verb = "certonly"
mock_parser.args = ["--cert-path", candidate_cert_path,
"--chain-path", candidate_chain_path,
"--fullchain-path", candidate_fullchain_path]
cert_path, chain_path, fullchain_path = self.client.save_certificate(
certr, chain_cert, candidate_cert_path, candidate_chain_path,

View file

@ -10,6 +10,7 @@ import zope.component
from certbot import errors
from certbot import interfaces
from certbot import le_util
from certbot.tests import test_util
@ -159,6 +160,44 @@ class CSRMatchesPubkeyTest(unittest.TestCase):
test_util.load_vector('csr.pem'), RSA256_KEY))
class ImportCSRFileTest(unittest.TestCase):
"""Tests for certbot.certbot_util.import_csr_file."""
@classmethod
def _call(cls, *args, **kwargs):
from certbot.crypto_util import import_csr_file
return import_csr_file(*args, **kwargs)
def test_der_csr(self):
csrfile = test_util.vector_path('csr.der')
data = test_util.load_vector('csr.der')
self.assertEqual(
(OpenSSL.crypto.FILETYPE_ASN1,
le_util.CSR(file=csrfile,
data=data,
form="der"),
["example.com"],),
self._call(csrfile, data))
def test_pem_csr(self):
csrfile = test_util.vector_path('csr.pem')
data = test_util.load_vector('csr.pem')
self.assertEqual(
(OpenSSL.crypto.FILETYPE_PEM,
le_util.CSR(file=csrfile,
data=data,
form="pem"),
["example.com"],),
self._call(csrfile, data))
def test_bad_csr(self):
self.assertRaises(errors.Error, self._call,
test_util.vector_path('cert.pem'),
test_util.load_vector('cert.pem'))
class MakeKeyTest(unittest.TestCase): # pylint: disable=too-few-public-methods
"""Tests for certbot.crypto_util.make_key."""
@ -234,6 +273,36 @@ class GetSANsFromCSRTest(unittest.TestCase):
[], self._call(test_util.load_vector('csr-nosans.pem')))
class GetNamesFromCSRTest(unittest.TestCase):
"""Tests for certbot.crypto_util.get_names_from_csr."""
@classmethod
def _call(cls, *args, **kwargs):
from certbot.crypto_util import get_names_from_csr
return get_names_from_csr(*args, **kwargs)
def test_extract_one_san(self):
self.assertEqual(['example.com'], self._call(
test_util.load_vector('csr.pem')))
def test_extract_two_sans(self):
self.assertEqual(set(('example.com', 'www.example.com',)), set(
self._call(test_util.load_vector('csr-san.pem'))))
def test_extract_six_sans(self):
self.assertEqual(
set(self._call(test_util.load_vector('csr-6sans.pem'))),
set(("example.com", "example.org", "example.net",
"example.info", "subdomain.example.com",
"other.subdomain.example.com",)))
def test_parse_non_csr(self):
self.assertRaises(OpenSSL.crypto.Error, self._call, "hello there")
def test_parse_no_sans(self):
self.assertEqual(["example.org"],
self._call(test_util.load_vector('csr-nosans.pem')))
class CertLoaderTest(unittest.TestCase):
"""Tests for certbot.crypto_util.pyopenssl_load_certificate"""

View file

@ -10,6 +10,7 @@ import unittest
import mock
import six
import certbot
from certbot import errors
@ -339,5 +340,32 @@ class EnforceDomainSanityTest(unittest.TestCase):
u"eichh\u00f6rnchen.example.com")
class GetStrictVersionTest(unittest.TestCase):
"""Tests for certbot.le_util.get_strict_version."""
@classmethod
def _call(cls, *args, **kwargs):
from certbot.le_util import get_strict_version
return get_strict_version(*args, **kwargs)
def test_two_dev_versions(self):
self.assertTrue(
self._call("0.0.0.dev20151006") < self._call("0.0.0.dev20151008"))
def test_one_dev_one_release_version(self):
self.assertTrue(self._call("1.0.0.dev0") < self._call("1.0.0"))
self.assertTrue(self._call("1.0.0") < self._call("1.0.1.dev0"))
def test_two_release_versions(self):
self.assertTrue(self._call("0.0.0") < self._call("0.0.1"))
self.assertTrue(self._call("0.0.0") < self._call("0.1.0"))
self.assertTrue(self._call("0.0.0") < self._call("1.0.0"))
def test_current_version(self):
current_version = self._call(certbot.__version__)
self.assertTrue(self._call("0.6.0") < current_version)
self.assertTrue(current_version < self._call("99.99.99"))
if __name__ == "__main__":
unittest.main() # pragma: no cover

View file

@ -34,6 +34,20 @@ class ReverterCheckpointLocalTest(unittest.TestCase):
logging.disable(logging.NOTSET)
@mock.patch("certbot.reverter.Reverter._read_and_append")
def test_no_change(self, mock_read):
mock_read.side_effect = OSError("cannot even")
try:
self.reverter.add_to_checkpoint(self.sets[0], "save1")
except OSError:
pass
self.reverter.finalize_checkpoint("blah")
path = os.listdir(self.reverter.config.backup_dir)[0]
no_change = os.path.join(self.reverter.config.backup_dir, path, "CHANGES_SINCE")
with open(no_change, "r") as f:
x = f.read()
self.assertTrue("No changes" in x)
def test_basic_add_to_temp_checkpoint(self):
# These shouldn't conflict even though they are both named config.txt
self.reverter.add_to_temp_checkpoint(self.sets[0], "save1")

View file

@ -10,6 +10,7 @@ import configobj
import mock
import pytz
import certbot
from certbot import configuration
from certbot import errors
from certbot.storage import ALL_FOUR
@ -137,6 +138,28 @@ class RenewableCertTests(BaseRenewableCertTest):
self.assertRaises(errors.CertStorageError, storage.RenewableCert,
config.filename, self.cli_config)
def test_no_renewal_version(self):
from certbot import storage
self._write_out_ex_kinds()
self.assertTrue("version" not in self.config)
with mock.patch("certbot.storage.logger") as mock_logger:
storage.RenewableCert(self.config.filename, self.cli_config)
self.assertFalse(mock_logger.warning.called)
def test_renewal_newer_version(self):
from certbot import storage
self._write_out_ex_kinds()
self.config["version"] = "99.99.99"
self.config.write()
with mock.patch("certbot.storage.logger") as mock_logger:
storage.RenewableCert(self.config.filename, self.cli_config)
self.assertTrue(mock_logger.warning.called)
self.assertTrue("version" in mock_logger.warning.call_args[0][0])
def test_consistent(self):
# pylint: disable=too-many-statements,protected-access
oldcert = self.test_rc.cert
@ -760,11 +783,14 @@ class RenewableCertTests(BaseRenewableCertTest):
with open(temp2, "r") as f:
content = f.read()
# useful value was updated
assert "useful = new_value" in content
self.assertTrue("useful = new_value" in content)
# associated comment was preserved
assert "A useful value" in content
self.assertTrue("A useful value" in content)
# useless value was deleted
assert "useless" not in content
self.assertTrue("useless" not in content)
# check version was stored
self.assertTrue("version = {0}".format(certbot.__version__) in content)
if __name__ == "__main__":
unittest.main() # pragma: no cover

View file

@ -0,0 +1,8 @@
-----BEGIN CERTIFICATE REQUEST-----
MIH/MIGqAgEAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwXDANBgkqhkiG9w0BAQEF
AANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+
6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoAAwDQYJKoZIhvcNAQELBQAD
QQBt9XLSZ9DGfWcGGaBUTCiSY7lWBegpNlCeo8pK3ydWmKpjcza+j7lF5paph2LH
lKWVQ8+xwYMscGWK0NApHGco
-----END CERTIFICATE REQUEST-----

View file

@ -460,7 +460,7 @@ repo, if you have not already done so. Then run:
.. code-block:: shell
sudo apt-get install certbot python-certbot-apache -t jessie-backports
sudo apt-get install letsencrypt python-letsencrypt-apache -t jessie-backports
**Fedora**

View file

@ -425,7 +425,8 @@ BootstrapMac() {
$pkgcmd augeas
$pkgcmd dialog
if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" ]; then
if [ "$(which python)" = "/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python" \
-o "$(which python)" = "/usr/bin/python" ]; then
# We want to avoid using the system Python because it requires root to use pip.
# python.org, MacPorts or HomeBrew Python installations should all be OK.
echo "Installing python..."
@ -531,6 +532,7 @@ if [ "$1" = "--le-auto-phase2" ]; then
echo "Installing Python packages..."
TEMP_DIR=$(TempDir)
trap 'rm -rf "$TEMP_DIR"' EXIT
# There is no $ interpolation due to quotes on starting heredoc delimiter.
# -------------------------------------------------------------------------
cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt"
@ -888,7 +890,6 @@ UNLIKELY_EOF
PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1`
PIP_STATUS=$?
set -e
rm -rf "$TEMP_DIR"
if [ "$PIP_STATUS" != 0 ]; then
# Report error. (Otherwise, be quiet.)
echo "Had a problem while installing Python packages:"
@ -934,6 +935,7 @@ else
if [ "$NO_SELF_UPGRADE" != 1 ]; then
TEMP_DIR=$(TempDir)
trap 'rm -rf "$TEMP_DIR"' EXIT
# ---------------------------------------------------------------------------
cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py"
"""Do downloading and JSON parsing without additional dependencies. ::
@ -1006,7 +1008,7 @@ def latest_stable_version(get):
"""Return the latest stable release of letsencrypt."""
metadata = loads(get(
environ.get('LE_AUTO_JSON_URL',
'https://pypi.python.org/pypi/letsencrypt/json')))
'https://pypi.python.org/pypi/certbot/json')))
# metadata['info']['version'] actually returns the latest of any kind of
# release release, contrary to https://wiki.python.org/moin/PyPIJSON.
# The regex is a sufficient regex for picking out prereleases for most
@ -1088,8 +1090,6 @@ UNLIKELY_EOF
# filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the
# cp is unlikely to fail (esp. under sudo) if the rm doesn't.
$SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0"
# TODO: Clean up temp dir safely, even if it has quotes in its path.
rm -rf "$TEMP_DIR"
fi # A newer version is available.
fi # Self-upgrading is allowed.

View file

@ -229,6 +229,7 @@ if [ "$1" = "--le-auto-phase2" ]; then
echo "Installing Python packages..."
TEMP_DIR=$(TempDir)
trap 'rm -rf "$TEMP_DIR"' EXIT
# There is no $ interpolation due to quotes on starting heredoc delimiter.
# -------------------------------------------------------------------------
cat << "UNLIKELY_EOF" > "$TEMP_DIR/letsencrypt-auto-requirements.txt"
@ -245,7 +246,6 @@ UNLIKELY_EOF
PIP_OUT=`"$VENV_BIN/pip" install --no-cache-dir --require-hashes -r "$TEMP_DIR/letsencrypt-auto-requirements.txt" 2>&1`
PIP_STATUS=$?
set -e
rm -rf "$TEMP_DIR"
if [ "$PIP_STATUS" != 0 ]; then
# Report error. (Otherwise, be quiet.)
echo "Had a problem while installing Python packages:"
@ -291,6 +291,7 @@ else
if [ "$NO_SELF_UPGRADE" != 1 ]; then
TEMP_DIR=$(TempDir)
trap 'rm -rf "$TEMP_DIR"' EXIT
# ---------------------------------------------------------------------------
cat << "UNLIKELY_EOF" > "$TEMP_DIR/fetch.py"
{{ fetch.py }}
@ -319,8 +320,6 @@ UNLIKELY_EOF
# filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the
# cp is unlikely to fail (esp. under sudo) if the rm doesn't.
$SUDO mv -f "$TEMP_DIR/letsencrypt-auto.permission-clone" "$0"
# TODO: Clean up temp dir safely, even if it has quotes in its path.
rm -rf "$TEMP_DIR"
fi # A newer version is available.
fi # Self-upgrading is allowed.

View file

@ -68,7 +68,7 @@ def latest_stable_version(get):
"""Return the latest stable release of letsencrypt."""
metadata = loads(get(
environ.get('LE_AUTO_JSON_URL',
'https://pypi.python.org/pypi/letsencrypt/json')))
'https://pypi.python.org/pypi/certbot/json')))
# metadata['info']['version'] actually returns the latest of any kind of
# release release, contrary to https://wiki.python.org/moin/PyPIJSON.
# The regex is a sufficient regex for picking out prereleases for most

View file

@ -183,7 +183,7 @@ def run_le_auto(venv_dir, base_url, **kwargs):
d = dict(XDG_DATA_HOME=venv_dir,
# URL to PyPI-style JSON that tell us the latest released version
# of LE:
LE_AUTO_JSON_URL=base_url + 'letsencrypt/json',
LE_AUTO_JSON_URL=base_url + 'certbot/json',
# URL to dir containing letsencrypt-auto and letsencrypt-auto.sig:
LE_AUTO_DIR_TEMPLATE=base_url + '%s/',
# The public key corresponding to signing.key:
@ -258,7 +258,7 @@ class AutoTests(TestCase):
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 = {'letsencrypt/json': dumps({'releases': {'99.9.9': None}}),
resources = {'certbot/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:
@ -301,8 +301,8 @@ class AutoTests(TestCase):
with ephemeral_dir() as venv_dir:
# Serve an unrelated hash signed with the good key (easier than
# making a bad key, and a mismatch is a mismatch):
resources = {'': '<a href="letsencrypt/">letsencrypt/</a>',
'letsencrypt/json': dumps({'releases': {'99.9.9': None}}),
resources = {'': '<a href="certbot/">certbot/</a>',
'certbot/json': dumps({'releases': {'99.9.9': None}}),
'v99.9.9/letsencrypt-auto': build_le_auto(version='99.9.9'),
'v99.9.9/letsencrypt-auto.sig': signed('something else')}
with serving(resources) as base_url:
@ -320,8 +320,8 @@ class AutoTests(TestCase):
def test_pip_failure(self):
"""Make sure pip stops us if there is a hash mismatch."""
with ephemeral_dir() as venv_dir:
resources = {'': '<a href="letsencrypt/">letsencrypt/</a>',
'letsencrypt/json': dumps({'releases': {'99.9.9': None}})}
resources = {'': '<a href="certbot/">certbot/</a>',
'certbot/json': dumps({'releases': {'99.9.9': None}})}
with serving(resources) as base_url:
# Build a le-auto script embedding a bad requirements file:
install_le_auto(

View file

@ -71,6 +71,7 @@ dev_extras = [
'nose',
'nosexcover',
'pep8',
'pkginfo<=1.2.1',
'pylint==1.4.2', # upstream #248
'tox',
'twine',

View file

@ -1,40 +1,10 @@
#!/bin/bash
# Download and run Boulder instance for integration testing
# ugh, go version output is like:
# go version go1.4.2 linux/amd64
GOVER=`go version | cut -d" " -f3 | cut -do -f2`
# version comparison
function verlte {
#OS X doesn't support version sorting; emulate with sed
if [ `uname` == 'Darwin' ]; then
[ "$1" = "`echo -e \"$1\n$2\" | sed 's/\b\([0-9]\)\b/0\1/g' \
| sort | sed 's/\b0\([0-9]\)/\1/g' | head -n1`" ]
else
[ "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ]
fi
}
if ! verlte 1.5 "$GOVER" ; then
echo "We require go version 1.5 or later; you have... $GOVER"
exit 1
fi
set -xe
# `/...` avoids `no buildable Go source files` errors, for more info
# see `go help packages`
go get -d github.com/letsencrypt/boulder/...
cd $GOPATH/src/github.com/letsencrypt/boulder
# goose is needed for ./test/create_db.sh
wget https://github.com/jsha/boulder-tools/raw/master/goose.gz && \
mkdir $GOPATH/bin && \
zcat goose.gz > $GOPATH/bin/goose && \
chmod +x $GOPATH/bin/goose
./test/create_db.sh
go run cmd/rabbitmq-setup/main.go -server amqp://localhost
# listenbuddy is needed for ./start.py
go get github.com/jsha/listenbuddy
cd -
# Check out special branch until latest docker changes land in Boulder master.
git clone -b docker-integration https://github.com/letsencrypt/boulder $BOULDERPATH
cd $BOULDERPATH
sed -i 's/FAKE_DNS: .*/FAKE_DNS: 172.17.42.1/' docker-compose.yml
docker-compose up -d

View file

@ -43,8 +43,8 @@ export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \
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
openssl x509 -in "${root}/csr/cert.pem" -text
openssl x509 -in "${root}/csr/chain.pem" -text
common --domains le3.wtf install \
--cert-path "${root}/csr/cert.pem" \

View file

@ -2,27 +2,8 @@
# >>>> only tested on Ubuntu 14.04LTS <<<<
# non-interactive install of mariadb and other dependencies
export DEBIAN_FRONTEND=noninteractive
sudo debconf-set-selections <<< 'mariadb-server mysql-server/root_password password PASS'
sudo debconf-set-selections <<< 'mariadb-server mysql-server/root_password_again password PASS'
apt-get -y --no-upgrade install git make libltdl3-dev mariadb-server rabbitmq-server
sudo mysql -uroot -pPASS -e "SET PASSWORD = PASSWORD(\'\');"
# install go
wget https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz
tar xzvf go1.5.1.linux-amd64.tar.gz
mkdir gocode
echo "export GOROOT=/home/ubuntu/go \n\
export GOPATH=/home/ubuntu/gocode\n\
export PATH=/home/ubuntu/go/bin:/home/ubuntu/gocode/bin:$PATH" >> .bashrc
# install boulder and its go dependencies
go get -d github.com/letsencrypt/boulder/...
cd $GOPATH/src/github.com/letsencrypt/boulder
wget https://github.com/jsha/boulder-tools/raw/master/goose.gz
mkdir $GOPATH/bin
zcat goose.gz > $GOPATH/bin/goose
chmod +x $GOPATH/bin/goose
./test/create_db.sh
go get github.com/jsha/listenbuddy
# Check out special branch until latest docker changes land in Boulder master.
git clone -b docker-integration https://github.com/letsencrypt/boulder $BOULDERPATH
cd $BOULDERPATH
sed -i 's/FAKE_DNS: .*/FAKE_DNS: 172.17.42.1/' docker-compose.yml
docker-compose up -d

View file

@ -6,14 +6,9 @@ set -o errexit
source .tox/$TOXENV/bin/activate
export CERTBOT_PATH=`pwd`
until curl http://boulder:4000/directory 2>/dev/null; do
echo waiting for boulder
sleep 1
done
cd $GOPATH/src/github.com/letsencrypt/boulder/
# boulder's integration-test.py has code that knows to start and wait for the
# boulder processes to start reliably and then will run the certbot
# boulder-interation.sh on its own. The --certbot flag says to run only the
# certbot tests (instead of any other client tests it might run). We're
# going to want to define a more robust interaction point between the boulder
# and certbot tests, but that will be better built off of this.
python test/integration-test.py --certbot
./tests/boulder-integration.sh