Skip to content

Commit

Permalink
Merge branch 'master' into dialogerror
Browse files Browse the repository at this point in the history
  • Loading branch information
TheNavigat committed May 25, 2016
2 parents 12a3562 + 5f1ead8 commit d1e9ffb
Show file tree
Hide file tree
Showing 29 changed files with 419 additions and 273 deletions.
25 changes: 7 additions & 18 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -93,7 +85,6 @@ addons:
- boulder
- boulder-mysql
- boulder-rabbitmq
mariadb: "10.0"
apt:
sources:
- augeas
Expand All @@ -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)'
Expand Down
3 changes: 2 additions & 1 deletion acme/acme/crypto_util.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Crypto utilities."""
import binascii
import contextlib
import logging
import re
Expand Down Expand Up @@ -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 = [
Expand Down
20 changes: 20 additions & 0 deletions acme/acme/crypto_util_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
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
Expand Down Expand Up @@ -126,5 +128,23 @@ def test_csr_idn_sans(self):
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
67 changes: 30 additions & 37 deletions certbot/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@
import logging.handlers
import os
import sys
import traceback

import configargparse
import OpenSSL
import six

import certbot
Expand Down Expand Up @@ -336,58 +334,53 @@ def parse_args(self):

# 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":
raise errors.Error("Currently, a CSR file may only be specified "
"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
Expand Down
57 changes: 38 additions & 19 deletions certbot/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
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
Expand Down Expand Up @@ -317,23 +318,30 @@ def save_certificate(self, certr, chain_cert,

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):
Expand Down Expand Up @@ -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.
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
def _save_chain(chain_pem, chain_path):
: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)
57 changes: 53 additions & 4 deletions certbot/crypto_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""
import logging
import os
import traceback

import OpenSSL
import pyrfc3339
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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.
Expand Down
Loading

0 comments on commit d1e9ffb

Please sign in to comment.