Merge branch 'master' into apache_modules

This commit is contained in:
James Kasten
2015-07-24 17:07:16 -07:00
28 changed files with 714 additions and 161 deletions
+9 -1
View File
@@ -1,5 +1,8 @@
language: python language: python
services:
- rabbitmq
# http://docs.travis-ci.com/user/ci-environment/#CI-environment-OS # http://docs.travis-ci.com/user/ci-environment/#CI-environment-OS
before_install: before_install:
- travis_retry sudo ./bootstrap/ubuntu.sh - travis_retry sudo ./bootstrap/ubuntu.sh
@@ -19,8 +22,13 @@ env:
- TOXENV=lint - TOXENV=lint
- TOXENV=cover - TOXENV=cover
# make sure simplehttp simple verification works (custom /etc/hosts)
addons:
hosts:
- le.wtf
install: "travis_retry pip install tox coveralls" install: "travis_retry pip install tox coveralls"
before_script: '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/boulder-start.sh' before_script: '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/boulder-start.sh amqp'
script: 'travis_retry tox && ([ "xxx$BOULDER_INTEGRATION" = "xxx" ] || (source .tox/$TOXENV/bin/activate && ./tests/boulder-integration.sh))' script: 'travis_retry tox && ([ "xxx$BOULDER_INTEGRATION" = "xxx" ] || (source .tox/$TOXENV/bin/activate && ./tests/boulder-integration.sh))'
after_success: '[ "$TOXENV" == "cover" ] && coveralls' after_success: '[ "$TOXENV" == "cover" ] && coveralls'
+89 -1
View File
@@ -4,9 +4,15 @@ import functools
import hashlib import hashlib
import logging import logging
import os import os
import socket
from cryptography.hazmat.backends import default_backend
from cryptography import x509
import OpenSSL
import requests import requests
from acme import errors
from acme import crypto_util
from acme import fields from acme import fields
from acme import jose from acme import jose
from acme import other from acme import other
@@ -191,6 +197,18 @@ class DVSNI(DVChallenge):
""" """
return binascii.hexlify(self.nonce) + self.DOMAIN_SUFFIX return binascii.hexlify(self.nonce) + self.DOMAIN_SUFFIX
def probe_cert(self, domain, **kwargs):
"""Probe DVSNI challenge certificate."""
host = socket.gethostbyname(domain)
logging.debug('%s resolved to %s', domain, host)
kwargs.setdefault("host", host)
kwargs.setdefault("port", self.PORT)
kwargs["name"] = self.nonce_domain
# TODO: try different methods?
# pylint: disable=protected-access
return crypto_util._probe_sni(**kwargs)
@ChallengeResponse.register @ChallengeResponse.register
class DVSNIResponse(ChallengeResponse): class DVSNIResponse(ChallengeResponse):
@@ -229,9 +247,79 @@ class DVSNIResponse(ChallengeResponse):
return z.hexdigest().encode() return z.hexdigest().encode()
def z_domain(self, chall): def z_domain(self, chall):
"""Domain name for certificate subjectAltName.""" """Domain name for certificate subjectAltName.
:rtype bytes:
"""
return self.z(chall) + self.DOMAIN_SUFFIX return self.z(chall) + self.DOMAIN_SUFFIX
def gen_cert(self, chall, domain, key):
"""Generate DVSNI certificate.
:param .DVSNI chall: Corresponding challenge.
:param unicode domain:
:param OpenSSL.crypto.PKey
"""
return crypto_util.gen_ss_cert(key, [
domain, chall.nonce_domain.decode(), self.z_domain(chall).decode()])
def simple_verify(self, chall, domain, public_key, **kwargs):
"""Simple verify.
Probes DVSNI certificate and checks it using `verify_cert`;
hence all arguments documented in `verify_cert`.
"""
try:
cert = chall.probe_cert(domain=domain, **kwargs)
except errors.Error as error:
logger.debug(error, exc_info=True)
return False
return self.verify_cert(chall, domain, public_key, cert)
def verify_cert(self, chall, domain, public_key, cert):
"""Verify DVSNI certificate.
:param .challenges.DVSNI chall: Corresponding challenge.
:param str domain: Domain name being validated.
:param public_key: Public key for the key pair
being authorized. If ``None`` key verification is not
performed!
:type public_key:
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
wrapped in `.ComparableKey
:param OpenSSL.crypto.X509 cert:
:returns: ``True`` iff client's control of the domain has been
verified, ``False`` otherwise.
:rtype: bool
"""
# TODO: check "It is a valid self-signed certificate" and
# return False if not
# pylint: disable=protected-access
sans = crypto_util._pyopenssl_cert_or_req_san(cert)
logging.debug('Certificate %s. SANs: %s', cert.digest('sha1'), sans)
cert = x509.load_der_x509_certificate(
OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, cert),
default_backend())
if public_key is None:
logging.warn('No key verification is performed')
elif public_key != jose.ComparableKey(cert.public_key()):
return False
return domain in sans and self.z_domain(chall).decode() in sans
@Challenge.register @Challenge.register
class RecoveryContact(ContinuityChallenge): class RecoveryContact(ContinuityChallenge):
"""ACME "recoveryContact" challenge. """ACME "recoveryContact" challenge.
+85 -13
View File
@@ -7,6 +7,7 @@ import requests
from six.moves.urllib import parse as urllib_parse # pylint: disable=import-error from six.moves.urllib import parse as urllib_parse # pylint: disable=import-error
from acme import errors
from acme import jose from acme import jose
from acme import other from acme import other
from acme import test_util from acme import test_util
@@ -177,31 +178,63 @@ class DVSNITest(unittest.TestCase):
self.assertRaises( self.assertRaises(
jose.DeserializationError, DVSNI.from_json, self.jmsg) jose.DeserializationError, DVSNI.from_json, self.jmsg)
@mock.patch('acme.challenges.socket.gethostbyname')
@mock.patch('acme.challenges.crypto_util._probe_sni')
def test_probe_cert(self, mock_probe_sni, mock_gethostbyname):
mock_gethostbyname.return_value = '127.0.0.1'
self.msg.probe_cert('foo.com')
mock_gethostbyname.assert_called_once_with('foo.com')
mock_probe_sni.assert_called_once_with(
host='127.0.0.1', port=self.msg.PORT,
name=b'a82d5ff8ef740d12881f6d3c2277ab2e.acme.invalid')
self.msg.probe_cert('foo.com', host='8.8.8.8')
mock_probe_sni.assert_called_with(
host='8.8.8.8', port=mock.ANY, name=mock.ANY)
self.msg.probe_cert('foo.com', port=1234)
mock_probe_sni.assert_called_with(
host=mock.ANY, port=1234, name=mock.ANY)
self.msg.probe_cert('foo.com', bar='baz')
mock_probe_sni.assert_called_with(
host=mock.ANY, port=mock.ANY, name=mock.ANY, bar='baz')
self.msg.probe_cert('foo.com', name=b'xxx')
mock_probe_sni.assert_called_with(
host=mock.ANY, port=mock.ANY,
name=b'a82d5ff8ef740d12881f6d3c2277ab2e.acme.invalid')
class DVSNIResponseTest(unittest.TestCase): class DVSNIResponseTest(unittest.TestCase):
def setUp(self): def setUp(self):
from acme.challenges import DVSNIResponse from acme.challenges import DVSNIResponse
self.msg = DVSNIResponse( # pylint: disable=invalid-name
s=b'\xf5\xd6\xe3\xb2]\xe0L\x0bN\x9cKJ\x14I\xa1K\xa3#\xf9\xa8' s = '9dbjsl3gTAtOnEtKFEmhS6Mj-ajNjDcOmRkp3Lfzm3c'
b'\xcd\x8c7\x0e\x99\x19)\xdc\xb7\xf3\x9bw') self.msg = DVSNIResponse(s=jose.decode_b64jose(s))
self.jmsg = { self.jmsg = {
'resource': 'challenge', 'resource': 'challenge',
'type': 'dvsni', 'type': 'dvsni',
's': '9dbjsl3gTAtOnEtKFEmhS6Mj-ajNjDcOmRkp3Lfzm3c', 's': s,
} }
def test_z_and_domain(self):
from acme.challenges import DVSNI from acme.challenges import DVSNI
challenge = DVSNI( self.chall = DVSNI(
r=b"O*\xb4-\xad\xec\x95>\xed\xa9\r0\x94\xe8\x97\x9c&6" r=jose.decode_b64jose('Tyq0La3slT7tqQ0wlOiXnCY2vyez7Zo5blgPJ1xt5xI'),
b"\xbf'\xb3\xed\x9a9nX\x0f'\\m\xe7\x12", nonce=jose.decode_b64jose('a82d5ff8ef740d12881f6d3c2277ab2e'))
nonce=int('439736375371401115242521957580409149254868992063' self.z = (b'38e612b0397cc2624a07d351d7ef50e4'
'44333654741504362774620418661')) b'6134c0213d9ed52f7d7c611acaeed41b')
self.domain = 'foo.com'
self.key = test_util.load_pyopenssl_private_key('rsa512_key.pem')
self.public_key = test_util.load_rsa_private_key(
'rsa512_key.pem').public_key()
def test_z_and_domain(self):
# pylint: disable=invalid-name # pylint: disable=invalid-name
z = b'38e612b0397cc2624a07d351d7ef50e46134c0213d9ed52f7d7c611acaeed41b' self.assertEqual(self.z, self.msg.z(self.chall))
self.assertEqual(z, self.msg.z(challenge)) self.assertEqual(
self.assertEqual(z + b'.acme.invalid', self.msg.z_domain(challenge)) self.z + b'.acme.invalid', self.msg.z_domain(self.chall))
def test_to_partial_json(self): def test_to_partial_json(self):
self.assertEqual(self.jmsg, self.msg.to_partial_json()) self.assertEqual(self.jmsg, self.msg.to_partial_json())
@@ -214,6 +247,45 @@ class DVSNIResponseTest(unittest.TestCase):
from acme.challenges import DVSNIResponse from acme.challenges import DVSNIResponse
hash(DVSNIResponse.from_json(self.jmsg)) hash(DVSNIResponse.from_json(self.jmsg))
@mock.patch('acme.challenges.DVSNIResponse.verify_cert')
def test_simple_verify(self, mock_verify_cert):
chall = mock.Mock()
chall.probe_cert.return_value = mock.sentinel.cert
mock_verify_cert.return_value = 'x'
self.assertEqual('x', self.msg.simple_verify(
chall, mock.sentinel.domain, mock.sentinel.key))
chall.probe_cert.assert_called_once_with(domain=mock.sentinel.domain)
self.msg.verify_cert.assert_called_once_with(
chall, mock.sentinel.domain, mock.sentinel.key,
mock.sentinel.cert)
def test_simple_verify_false_on_probe_error(self):
chall = mock.Mock()
chall.probe_cert.side_effect = errors.Error
self.assertFalse(self.msg.simple_verify(
chall=chall, domain=None, public_key=None))
def test_gen_verify_cert_postive_no_key(self):
cert = self.msg.gen_cert(self.chall, self.domain, self.key)
self.assertTrue(self.msg.verify_cert(
self.chall, self.domain, public_key=None, cert=cert))
def test_gen_verify_cert_postive_with_key(self):
cert = self.msg.gen_cert(self.chall, self.domain, self.key)
self.assertTrue(self.msg.verify_cert(
self.chall, self.domain, public_key=self.public_key, cert=cert))
def test_gen_verify_cert_negative_with_wrong_key(self):
cert = self.msg.gen_cert(self.chall, self.domain, self.key)
key = test_util.load_rsa_private_key('rsa256_key.pem').public_key()
self.assertFalse(self.msg.verify_cert(
self.chall, self.domain, public_key=key, cert=cert))
def test_gen_verify_cert_negative(self):
cert = self.msg.gen_cert(self.chall, self.domain + 'x', self.key)
self.assertFalse(self.msg.verify_cert(
self.chall, self.domain, public_key=None, cert=cert))
class RecoveryContactTest(unittest.TestCase): class RecoveryContactTest(unittest.TestCase):
+195
View File
@@ -0,0 +1,195 @@
"""Crypto utilities."""
import contextlib
import logging
import socket
import sys
from six.moves import range # pylint: disable=import-error,redefined-builtin
import OpenSSL
from acme import errors
logger = logging.getLogger(__name__)
# DVSNI certificate serving and probing is not affected by SSL
# vulnerabilities: prober needs to check certificate for expected
# contents anyway. Working SNI is the only thing that's necessary for
# the challenge and thus scoping down SSL/TLS method (version) would
# cause interoperability issues: TLSv1_METHOD is only compatible with
# TLSv1_METHOD, while SSLv23_METHOD is compatible with all other
# methods, including TLSv2_METHOD (read more at
# https://www.openssl.org/docs/ssl/SSLv23_method.html). _serve_sni
# should be changed to use "set_options" to disable SSLv2 and SSLv3,
# in case it's used for things other than probing/serving!
_DEFAULT_DVSNI_SSL_METHOD = OpenSSL.SSL.SSLv23_METHOD
def _serve_sni(certs, sock, reuseaddr=True, method=_DEFAULT_DVSNI_SSL_METHOD,
accept=None):
"""Start SNI-enabled server, that drops connection after handshake.
:param certs: Mapping from SNI name to ``(key, cert)`` `tuple`.
:param sock: Already bound socket.
:param bool reuseaddr: Should `socket.SO_REUSEADDR` be set?
:param method: See `OpenSSL.SSL.Context` for allowed values.
:param accept: Callable that doesn't take any arguments and
returns ``True`` if more connections should be served.
"""
def _pick_certificate(connection):
try:
key, cert = certs[connection.get_servername()]
except KeyError:
return
new_context = OpenSSL.SSL.Context(method)
new_context.use_privatekey(key)
new_context.use_certificate(cert)
connection.set_context(new_context)
if reuseaddr:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.listen(1) # TODO: add func arg?
while accept is None or accept():
server, addr = sock.accept()
logger.debug('Received connection from %s', addr)
with contextlib.closing(server):
context = OpenSSL.SSL.Context(method)
context.set_tlsext_servername_callback(_pick_certificate)
server_ssl = OpenSSL.SSL.Connection(context, server)
server_ssl.set_accept_state()
try:
server_ssl.do_handshake()
server_ssl.shutdown()
except OpenSSL.SSL.Error as error:
raise errors.Error(error)
def _probe_sni(name, host, port=443, timeout=300,
method=_DEFAULT_DVSNI_SSL_METHOD, source_address=('0', 0)):
"""Probe SNI server for SSL certificate.
:param bytes name: Byte string to send as the server name in the
client hello message.
:param bytes host: Host to connect to.
:param int port: Port to connect to.
:param int timeout: Timeout in seconds.
:param method: See `OpenSSL.SSL.Context` for allowed values.
:param tuple source_address: Enables multi-path probing (selection
of source interface). See `socket.creation_connection` for more
info. Available only in Python 2.7+.
:raises acme.errors.Error: In case of any problems.
:returns: SSL certificate presented by the server.
:rtype: OpenSSL.crypto.X509
"""
context = OpenSSL.SSL.Context(method)
context.set_timeout(timeout)
socket_kwargs = {} if sys.version_info < (2, 7) else {
'source_address': source_address}
try:
# pylint: disable=star-args
sock = socket.create_connection((host, port), **socket_kwargs)
except socket.error as error:
raise errors.Error(error)
with contextlib.closing(sock) as client:
client_ssl = OpenSSL.SSL.Connection(context, client)
client_ssl.set_connect_state()
client_ssl.set_tlsext_host_name(name) # pyOpenSSL>=0.13
try:
client_ssl.do_handshake()
client_ssl.shutdown()
except OpenSSL.SSL.Error as error:
raise errors.Error(error)
return client_ssl.get_peer_certificate()
def _pyopenssl_cert_or_req_san(cert_or_req):
"""Get Subject Alternative Names from certificate or CSR using pyOpenSSL.
.. todo:: Implement directly in PyOpenSSL!
.. note:: Although this is `acme` internal API, it is used by
`letsencrypt`.
:param cert_or_req: Certificate or CSR.
:type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`.
:returns: A list of Subject Alternative Names.
:rtype: `list` of `unicode`
"""
# constants based on implementation of
# OpenSSL.crypto.X509Error._subjectAltNameString
parts_separator = ", "
part_separator = ":"
extension_short_name = b"subjectAltName"
if hasattr(cert_or_req, 'get_extensions'): # X509Req
extensions = cert_or_req.get_extensions()
else: # X509
extensions = [cert_or_req.get_extension(i)
for i in range(cert_or_req.get_extension_count())]
# pylint: disable=protected-access,no-member
label = OpenSSL.crypto.X509Extension._prefixes[OpenSSL.crypto._lib.GEN_DNS]
assert parts_separator not in label
prefix = label + part_separator
san_extensions = [
ext._subjectAltNameString().split(parts_separator)
for ext in extensions if ext.get_short_name() == extension_short_name]
# WARNING: this function assumes that no SAN can include
# parts_separator, hence the split!
return [part.split(part_separator)[1] for parts in san_extensions
for part in parts if part.startswith(prefix)]
def gen_ss_cert(key, domains, not_before=None, validity=(7 * 24 * 60 * 60)):
"""Generate new self-signed certificate.
:type domains: `list` of `unicode`
:param OpenSSL.crypto.PKey key:
Uses key and contains all domains.
"""
assert domains, "Must provide one or more hostnames for the cert."
cert = OpenSSL.crypto.X509()
cert.set_serial_number(1337)
cert.set_version(2)
extensions = [
OpenSSL.crypto.X509Extension(
b"basicConstraints", True, b"CA:TRUE, pathlen:0"),
]
cert.get_subject().CN = domains[0]
# TODO: what to put into cert.get_subject()?
cert.set_issuer(cert.get_subject())
if len(domains) > 1:
extensions.append(OpenSSL.crypto.X509Extension(
b"subjectAltName",
critical=False,
value=b", ".join(b"DNS:" + d.encode() for d in domains)
))
cert.add_extensions(extensions)
cert.gmtime_adj_notBefore(0 if not_before is None else not_before)
cert.gmtime_adj_notAfter(validity)
cert.set_pubkey(key)
cert.sign(key, "sha256")
return cert
+104
View File
@@ -0,0 +1,104 @@
"""Tests for acme.crypto_util."""
import socket
import threading
import time
import unittest
import mock
import OpenSSL
from acme import errors
from acme import jose
from acme import test_util
class ServeProbeSNITest(unittest.TestCase):
"""Tests for acme.crypto_util._serve_sni/_probe_sni."""
def setUp(self):
self.cert = test_util.load_cert('cert.pem')
key = OpenSSL.crypto.load_privatekey(
OpenSSL.crypto.FILETYPE_PEM,
test_util.load_vector('rsa512_key.pem'))
# pylint: disable=protected-access
certs = {b'foo': (key, self.cert._wrapped)}
sock = socket.socket()
sock.bind(('', 0)) # pick random port
self.port = sock.getsockname()[1]
self.server = threading.Thread(target=self._run_server, args=(certs, sock))
self.server.start()
time.sleep(1) # TODO: avoid race conditions in other way
@classmethod
def _run_server(cls, certs, sock):
from acme.crypto_util import _serve_sni
# TODO: improve testing of server errors and their conditions
try:
return _serve_sni(
certs, sock, accept=mock.Mock(side_effect=[True, False]))
except errors.Error:
pass
def tearDown(self):
self.server.join()
def _probe(self, name):
from acme.crypto_util import _probe_sni
return jose.ComparableX509(_probe_sni(
name, host='127.0.0.1', port=self.port))
def test_probe_ok(self):
self.assertEqual(self.cert, self._probe(b'foo'))
def test_probe_not_recognized_name(self):
self.assertRaises(errors.Error, self._probe, b'bar')
def test_probe_connection_error(self):
self._probe(b'foo')
time.sleep(1) # TODO: avoid race conditions in other way
self.assertRaises(errors.Error, self._probe, b'bar')
class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
"""Test for acme.crypto_util._pyopenssl_cert_or_req_san."""
@classmethod
def _call(cls, loader, name):
# pylint: disable=protected-access
from acme.crypto_util import _pyopenssl_cert_or_req_san
return _pyopenssl_cert_or_req_san(loader(name))
def _call_cert(self, name):
return self._call(test_util.load_cert, name)
def _call_csr(self, name):
return self._call(test_util.load_csr, name)
def test_cert_no_sans(self):
self.assertEqual(self._call_cert('cert.pem'), [])
def test_cert_two_sans(self):
self.assertEqual(self._call_cert('cert-san.pem'),
['example.com', 'www.example.com'])
def test_csr_no_sans(self):
self.assertEqual(self._call_csr('csr-nosans.pem'), [])
def test_csr_one_san(self):
self.assertEqual(self._call_csr('csr.pem'), ['example.com'])
def test_csr_two_sans(self):
self.assertEqual(self._call_csr('csr-san.pem'),
['example.com', 'www.example.com'])
def test_csr_six_sans(self):
self.assertEqual(self._call_csr('csr-6sans.pem'),
["example.com", "example.org", "example.net",
"example.info", "subdomain.example.com",
"other.subdomain.example.com"])
if __name__ == "__main__":
unittest.main() # pragma: no cover
+1
View File
@@ -76,6 +76,7 @@ from acme.jose.jws import (
from acme.jose.util import ( from acme.jose.util import (
ComparableX509, ComparableX509,
ComparableKey,
ComparableRSAKey, ComparableRSAKey,
ImmutableMap, ImmutableMap,
) )
+26 -16
View File
@@ -66,14 +66,14 @@ class ComparableX509(object): # pylint: disable=too-few-public-methods
return '<{0}({1!r})>'.format(self.__class__.__name__, self._wrapped) return '<{0}({1!r})>'.format(self.__class__.__name__, self._wrapped)
class ComparableRSAKey(object): # pylint: disable=too-few-public-methods class ComparableKey(object): # pylint: disable=too-few-public-methods
"""Wrapper for `cryptography` RSA keys. """Comparable wrapper for `cryptography` keys.
Wraps around: See https://github.com/pyca/cryptography/issues/2122.
- `cryptography.hazmat.primitives.assymetric.RSAPrivateKey`
- `cryptography.hazmat.primitives.assymetric.RSAPublicKey`
""" """
__hash__ = NotImplemented
def __init__(self, wrapped): def __init__(self, wrapped):
self._wrapped = wrapped self._wrapped = wrapped
@@ -85,19 +85,36 @@ class ComparableRSAKey(object): # pylint: disable=too-few-public-methods
if (not isinstance(other, self.__class__) or if (not isinstance(other, self.__class__) or
self._wrapped.__class__ is not other._wrapped.__class__): self._wrapped.__class__ is not other._wrapped.__class__):
return NotImplemented return NotImplemented
# RSA*KeyWithSerialization requires cryptography>=0.8 elif hasattr(self._wrapped, 'private_numbers'):
if isinstance(self._wrapped, rsa.RSAPrivateKeyWithSerialization):
return self.private_numbers() == other.private_numbers() return self.private_numbers() == other.private_numbers()
elif isinstance(self._wrapped, rsa.RSAPublicKeyWithSerialization): elif hasattr(self._wrapped, 'public_numbers'):
return self.public_numbers() == other.public_numbers() return self.public_numbers() == other.public_numbers()
else: else:
return False # we shouldn't reach here... return NotImplemented
def __ne__(self, other): def __ne__(self, other):
return not self == other return not self == other
def __repr__(self):
return '<{0}({1!r})>'.format(self.__class__.__name__, self._wrapped)
def public_key(self):
"""Get wrapped public key."""
return self.__class__(self._wrapped.public_key())
class ComparableRSAKey(ComparableKey): # pylint: disable=too-few-public-methods
"""Wrapper for `cryptography` RSA keys.
Wraps around:
- `cryptography.hazmat.primitives.assymetric.RSAPrivateKey`
- `cryptography.hazmat.primitives.assymetric.RSAPublicKey`
"""
def __hash__(self): def __hash__(self):
# public_numbers() hasn't got stable hash! # public_numbers() hasn't got stable hash!
# https://github.com/pyca/cryptography/issues/2143
if isinstance(self._wrapped, rsa.RSAPrivateKeyWithSerialization): if isinstance(self._wrapped, rsa.RSAPrivateKeyWithSerialization):
priv = self.private_numbers() priv = self.private_numbers()
pub = priv.public_numbers pub = priv.public_numbers
@@ -107,13 +124,6 @@ class ComparableRSAKey(object): # pylint: disable=too-few-public-methods
pub = self.public_numbers() pub = self.public_numbers()
return hash((self.__class__, pub.n, pub.e)) return hash((self.__class__, pub.n, pub.e))
def __repr__(self):
return '<{0}({1!r})>'.format(self.__class__.__name__, self._wrapped)
def public_key(self):
"""Get wrapped public key."""
return self.__class__(self._wrapped.public_key())
class ImmutableMap(collections.Mapping, collections.Hashable): class ImmutableMap(collections.Mapping, collections.Hashable):
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
+6
View File
@@ -55,3 +55,9 @@ def load_rsa_private_key(*names):
serialization.load_der_private_key) serialization.load_der_private_key)
return jose.ComparableRSAKey(loader( return jose.ComparableRSAKey(loader(
load_vector(*names), password=None, backend=default_backend())) load_vector(*names), password=None, backend=default_backend()))
def load_pyopenssl_private_key(*names):
"""Load pyOpenSSL private key."""
loader = _guess_loader(
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
return OpenSSL.crypto.load_privatekey(loader, load_vector(*names))
+12
View File
@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBuzCCAWUCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE1pY2hpZ2FuMRIw
EAYDVQQHEwlBbm4gQXJib3IxDDAKBgNVBAoTA0VGRjEfMB0GA1UECxMWVW5pdmVy
c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wXDANBgkqhkiG
9w0BAQEFAANLADBIAkEA9LYRcVE3Nr+qleecEcX8JwVDnjeG1X7ucsCasuuZM0e0
9cmYuUzxIkMjO/9x4AVcvXXRXPEV+LzWWkfkTlzRMwIDAQABoIGGMIGDBgkqhkiG
9w0BCQ4xdjB0MHIGA1UdEQRrMGmCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZ4IL
ZXhhbXBsZS5uZXSCDGV4YW1wbGUuaW5mb4IVc3ViZG9tYWluLmV4YW1wbGUuY29t
ghtvdGhlci5zdWJkb21haW4uZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADQQBd
k4BE5qvEvkYoZM/2++Xd9RrQ6wsdj0QiJQCozfsI4lQx6ZJnbtNc7HpDrX4W6XIv
IvzVBz/nD11drfz/RNuX
-----END CERTIFICATE REQUEST-----
+8
View File
@@ -0,0 +1,8 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBFTCBwAIBADBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh
MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtleGFt
cGxlLm9yZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD0thFxUTc2v6qV55wRxfwn
BUOeN4bVfu5ywJqy65kzR7T1yZi5TPEiQyM7/3HgBVy9ddFc8RX4vNZaR+ROXNEz
AgMBAAGgADANBgkqhkiG9w0BAQsFAANBAMikGL8Ch7hQCStXH7chhDp6+pt2+VSo
wgsrPQ2Bw4veDMlSemUrH+4e0TwbbntHfvXTDHWs9P3BiIDJLxFrjuA=
-----END CERTIFICATE REQUEST-----
+2 -1
View File
@@ -13,7 +13,8 @@ install_requires = [
'pyrfc3339', 'pyrfc3339',
'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304) 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304)
'pyasn1', # urllib3 InsecurePlatformWarning (#304) 'pyasn1', # urllib3 InsecurePlatformWarning (#304)
'PyOpenSSL', # Connection.set_tlsext_host_name (>=0.13), X509Req.get_extensions (>=0.15)
'PyOpenSSL>=0.15',
'pytz', 'pytz',
'requests', 'requests',
'six', 'six',
+1 -1
View File
@@ -83,7 +83,7 @@ Mac OSX
.. code-block:: shell .. code-block:: shell
sudo ./bootstrap/mac.sh ./bootstrap/mac.sh
Fedora Fedora
@@ -7,9 +7,11 @@ import socket
import subprocess import subprocess
import sys import sys
import OpenSSL
import zope.interface import zope.interface
from acme import challenges from acme import challenges
from acme import crypto_util as acme_crypto_util
from letsencrypt import achallenges from letsencrypt import achallenges
from letsencrypt import constants as core_constants from letsencrypt import constants as core_constants
@@ -271,14 +273,17 @@ class NginxConfigurator(common.Plugin):
def _get_snakeoil_paths(self): def _get_snakeoil_paths(self):
# TODO: generate only once # TODO: generate only once
tmp_dir = os.path.join(self.config.work_dir, "snakeoil") tmp_dir = os.path.join(self.config.work_dir, "snakeoil")
key = crypto_util.init_save_key( le_key = crypto_util.init_save_key(
key_size=1024, key_dir=tmp_dir, keyname="key.pem") key_size=1024, key_dir=tmp_dir, keyname="key.pem")
cert_pem = crypto_util.make_ss_cert( key = OpenSSL.crypto.load_privatekey(
key.pem, domains=[socket.gethostname()]) OpenSSL.crypto.FILETYPE_PEM, le_key.pem)
cert = os.path.join(tmp_dir, "cert.pem") cert = acme_crypto_util.gen_ss_cert(key, domains=[socket.gethostname()])
with open(cert, 'w') as cert_file: cert_path = os.path.join(tmp_dir, "cert.pem")
cert_pem = OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM, cert)
with open(cert_path, 'w') as cert_file:
cert_file.write(cert_pem) cert_file.write(cert_pem)
return cert, key.file return cert_path, le_key.file
def _make_server_ssl(self, vhost): def _make_server_ssl(self, vhost):
"""Make a server SSL. """Make a server SSL.
+8 -3
View File
@@ -17,6 +17,8 @@ Note, that all annotated challenges act as a proxy objects::
achall.token == challb.token achall.token == challb.token
""" """
import OpenSSL
from acme import challenges from acme import challenges
from acme.jose import util as jose_util from acme.jose import util as jose_util
@@ -48,7 +50,7 @@ class DVSNI(AnnotatedChallenge):
acme_type = challenges.DVSNI acme_type = challenges.DVSNI
def gen_cert_and_response(self, s=None): # pylint: disable=invalid-name def gen_cert_and_response(self, s=None): # pylint: disable=invalid-name
"""Generate a DVSNI cert and save it to filepath. """Generate a DVSNI cert and response.
:returns: ``(cert_pem, response)`` tuple, where ``cert_pem`` is the PEM :returns: ``(cert_pem, response)`` tuple, where ``cert_pem`` is the PEM
encoded certificate and ``response`` is an instance encoded certificate and ``response`` is an instance
@@ -56,9 +58,12 @@ class DVSNI(AnnotatedChallenge):
:rtype: tuple :rtype: tuple
""" """
key = crypto_util.private_jwk_to_pyopenssl(self.key)
response = challenges.DVSNIResponse(s=s) response = challenges.DVSNIResponse(s=s)
cert_pem = crypto_util.make_ss_cert(self.key, [ cert = response.gen_cert(self.challb.chall, self.domain, key)
self.domain, self.nonce_domain, response.z_domain(self.challb)]) cert_pem = OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM, cert)
return cert_pem, response return cert_pem, response
+7 -2
View File
@@ -153,13 +153,18 @@ class AuthHandler(object):
""" """
active_achalls = [] active_achalls = []
for achall, resp in itertools.izip(achalls, resps): for achall, resp in itertools.izip(achalls, resps):
# XXX: make sure that all achalls, including those
# corresponding to None or False returned from
# Authenticator are removed from the queue and thus avoid
# infinite loop
active_achalls.append(achall)
# Don't send challenges for None and False authenticator responses # Don't send challenges for None and False authenticator responses
if resp: if resp is not None and resp:
self.acme.answer_challenge(achall.challb, resp) self.acme.answer_challenge(achall.challb, resp)
# TODO: answer_challenge returns challr, with URI, # TODO: answer_challenge returns challr, with URI,
# that can be used in _find_updated_challr # that can be used in _find_updated_challr
# comparisons... # comparisons...
active_achalls.append(achall)
if achall.domain in chall_update: if achall.domain in chall_update:
chall_update[achall.domain].append(achall) chall_update[achall.domain].append(achall)
else: else:
+1 -1
View File
@@ -57,7 +57,7 @@ USAGE = SHORT_USAGE + """Major SUBCOMMANDS are:
install Install a previously obtained cert in a server install Install a previously obtained cert in a server
revoke Revoke a previously obtained certificate revoke Revoke a previously obtained certificate
rollback Rollback server configuration changes made during install rollback Rollback server configuration changes made during install
config-changes Show changes made to server config during installation config_changes Show changes made to server config during installation
Choice of server for authentication/installation: Choice of server for authentication/installation:
+7 -81
View File
@@ -11,7 +11,7 @@ import os
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
import OpenSSL import OpenSSL
from acme import jose from acme import crypto_util as acme_crypto_util
from letsencrypt import errors from letsencrypt import errors
from letsencrypt import le_util from letsencrypt import le_util
@@ -215,88 +215,13 @@ def pyopenssl_load_certificate(data):
return _pyopenssl_load(data, OpenSSL.crypto.load_certificate) return _pyopenssl_load(data, OpenSSL.crypto.load_certificate)
def make_ss_cert(key, domains, not_before=None, def private_jwk_to_pyopenssl(jwk):
validity=(7 * 24 * 60 * 60)): """Convert private JWK to pyOpenSSL key."""
"""Returns new self-signed cert in PEM form. key_pem = jwk.key.private_bytes(
Uses key and contains all domains.
"""
if isinstance(key, jose.JWK):
key = key.key.private_bytes(
encoding=serialization.Encoding.PEM, encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL, format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()) encryption_algorithm=serialization.NoEncryption())
return OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, key_pem)
assert domains, "Must provide one or more hostnames for the cert."
pkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)
cert = OpenSSL.crypto.X509()
cert.set_serial_number(1337)
cert.set_version(2)
extensions = [
OpenSSL.crypto.X509Extension(
"basicConstraints", True, 'CA:TRUE, pathlen:0'),
]
cert.get_subject().CN = domains[0]
# TODO: what to put into cert.get_subject()?
cert.set_issuer(cert.get_subject())
if len(domains) > 1:
extensions.append(OpenSSL.crypto.X509Extension(
"subjectAltName",
critical=False,
value=", ".join("DNS:%s" % d for d in domains)
))
cert.add_extensions(extensions)
cert.gmtime_adj_notBefore(0 if not_before is None else not_before)
cert.gmtime_adj_notAfter(validity)
cert.set_pubkey(pkey)
cert.sign(pkey, "sha256")
return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
def _pyopenssl_cert_or_req_san(cert_or_req):
"""Get Subject Alternative Names from certificate or CSR using pyOpenSSL.
.. todo:: Implement directly in PyOpenSSL!
:param cert_or_req: Certificate or CSR.
:type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`.
:returns: A list of Subject Alternative Names.
:rtype: list
"""
# constants based on implementation of
# OpenSSL.crypto.X509Error._subjectAltNameString
parts_separator = ", "
part_separator = ":"
extension_short_name = "subjectAltName"
if hasattr(cert_or_req, 'get_extensions'): # X509Req
extensions = cert_or_req.get_extensions()
else: # X509
extensions = [cert_or_req.get_extension(i)
for i in xrange(cert_or_req.get_extension_count())]
# pylint: disable=protected-access,no-member
label = OpenSSL.crypto.X509Extension._prefixes[OpenSSL.crypto._lib.GEN_DNS]
assert parts_separator not in label
prefix = label + part_separator
san_extensions = [
ext._subjectAltNameString().split(parts_separator)
for ext in extensions if ext.get_short_name() == extension_short_name]
# WARNING: this function assumes that no SAN can include
# parts_separator, hence the split!
return [part.split(part_separator)[1] for parts in san_extensions
for part in parts if part.startswith(prefix)]
def _get_sans_from_cert_or_req( def _get_sans_from_cert_or_req(
@@ -306,7 +231,8 @@ def _get_sans_from_cert_or_req(
except OpenSSL.crypto.Error as error: except OpenSSL.crypto.Error as error:
logger.exception(error) logger.exception(error)
raise raise
return _pyopenssl_cert_or_req_san(cert_or_req) # pylint: disable=protected-access
return acme_crypto_util._pyopenssl_cert_or_req_san(cert_or_req)
def get_sans_from_cert(cert, typ=OpenSSL.crypto.FILETYPE_PEM): def get_sans_from_cert(cert, typ=OpenSSL.crypto.FILETYPE_PEM):
+1 -1
View File
@@ -182,7 +182,7 @@ class Dvsni(object):
# Write out challenge key # Write out challenge key
key_pem = achall.key.key.private_bytes( key_pem = achall.key.key.private_bytes(
encoding=serialization.Encoding.PEM, encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL, format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()) encryption_algorithm=serialization.NoEncryption())
with le_util.safe_open(key_path, 'wb', chmod=0o400) as key_file: with le_util.safe_open(key_path, 'wb', chmod=0o400) as key_file:
key_file.write(key_pem) key_file.write(key_pem)
+67 -11
View File
@@ -1,6 +1,12 @@
"""Manual plugin.""" """Manual plugin."""
import os import os
import logging
import shutil
import signal
import subprocess
import sys import sys
import tempfile
import time
import zope.component import zope.component
import zope.interface import zope.interface
@@ -8,10 +14,14 @@ import zope.interface
from acme import challenges from acme import challenges
from acme import jose from acme import jose
from letsencrypt import errors
from letsencrypt import interfaces from letsencrypt import interfaces
from letsencrypt.plugins import common from letsencrypt.plugins import common
logger = logging.getLogger(__name__)
class ManualAuthenticator(common.Plugin): class ManualAuthenticator(common.Plugin):
"""Manual Authenticator. """Manual Authenticator.
@@ -43,8 +53,8 @@ command on the target server (as root):
# anything recursively under the cwd # anything recursively under the cwd
HTTP_TEMPLATE = """\ HTTP_TEMPLATE = """\
mkdir -p /tmp/letsencrypt/public_html/{response.URI_ROOT_PATH} mkdir -p {root}/public_html/{response.URI_ROOT_PATH}
cd /tmp/letsencrypt/public_html cd {root}/public_html
echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path} echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path}
# run only once per server: # run only once per server:
python -c "import BaseHTTPServer, SimpleHTTPServer; \\ python -c "import BaseHTTPServer, SimpleHTTPServer; \\
@@ -55,8 +65,8 @@ s.serve_forever()" """
# https://www.piware.de/2011/01/creating-an-https-server-in-python/ # https://www.piware.de/2011/01/creating-an-https-server-in-python/
HTTPS_TEMPLATE = """\ HTTPS_TEMPLATE = """\
mkdir -p /tmp/letsencrypt/public_html/{response.URI_ROOT_PATH} mkdir -p {root}/public_html/{response.URI_ROOT_PATH}
cd /tmp/letsencrypt/public_html cd {root}/public_html
echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path} echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path}
# run only once per server: # run only once per server:
openssl req -new -newkey rsa:4096 -subj "/" -days 1 -nodes -x509 -keyout ../key.pem -out ../cert.pem openssl req -new -newkey rsa:4096 -subj "/" -days 1 -nodes -x509 -keyout ../key.pem -out ../cert.pem
@@ -77,6 +87,15 @@ s.serve_forever()" """
super(ManualAuthenticator, self).__init__(*args, **kwargs) super(ManualAuthenticator, self).__init__(*args, **kwargs)
self.template = (self.HTTP_TEMPLATE if self.config.no_simple_http_tls self.template = (self.HTTP_TEMPLATE if self.config.no_simple_http_tls
else self.HTTPS_TEMPLATE) else self.HTTPS_TEMPLATE)
self._root = (tempfile.mkdtemp() if self.conf("test-mode")
else "/tmp/letsencrypt")
self._httpd = None
@classmethod
def add_parser_arguments(cls, add):
add("test-mode", action="store_true",
help="Test mode. Executes the manual command in subprocess. "
"Requires openssl to be installed unless --no-simple-http-tls.")
def prepare(self): # pylint: disable=missing-docstring,no-self-use def prepare(self): # pylint: disable=missing-docstring,no-self-use
pass # pragma: no cover pass # pragma: no cover
@@ -110,17 +129,44 @@ binary for temporary key/certificate generation.""".replace("\n", "")
tls=(not self.config.no_simple_http_tls)) tls=(not self.config.no_simple_http_tls))
assert response.good_path # is encoded os.urandom(18) good? assert response.good_path # is encoded os.urandom(18) good?
command = self.template.format(
root=self._root, achall=achall, response=response,
ct=response.CONTENT_TYPE, port=(
response.port if self.config.simple_http_port is None
else self.config.simple_http_port))
if self.conf("test-mode"):
logger.debug("Test mode. Executing the manual command: %s", command)
try:
self._httpd = subprocess.Popen(
command,
# don't care about setting stdout and stderr,
# we're in test mode anyway
shell=True,
# "preexec_fn" is UNIX specific, but so is "command"
preexec_fn=os.setsid)
except OSError as error: # ValueError should not happen!
logger.debug(
"Couldn't execute manual command: %s", error, exc_info=True)
return False
logger.debug("Manual command running as PID %s.", self._httpd.pid)
# give it some time to bootstrap, before we try to verify
# (cert generation in case of simpleHttpS might take time)
time.sleep(4) # XXX
if self._httpd.poll() is not None:
raise errors.Error("Couldn't execute manual command")
else:
self._notify_and_wait(self.MESSAGE_TEMPLATE.format( self._notify_and_wait(self.MESSAGE_TEMPLATE.format(
achall=achall, response=response, uri=response.uri(achall.domain), achall=achall, response=response,
ct=response.CONTENT_TYPE, command=self.template.format( uri=response.uri(achall.domain), ct=response.CONTENT_TYPE,
achall=achall, response=response, ct=response.CONTENT_TYPE, command=command))
port=(response.port if self.config.simple_http_port is None
else self.config.simple_http_port))))
if response.simple_verify( if response.simple_verify(
achall.challb, achall.domain, self.config.simple_http_port): achall.challb, achall.domain, self.config.simple_http_port):
return response return response
else: else:
if self.conf("test-mode") and self._httpd.poll() is not None:
# simply verify cause command failure...
return False
return None return None
def _notify_and_wait(self, message): # pylint: disable=no-self-use def _notify_and_wait(self, message): # pylint: disable=no-self-use
@@ -130,5 +176,15 @@ binary for temporary key/certificate generation.""".replace("\n", "")
sys.stdout.write(message) sys.stdout.write(message)
raw_input("Press ENTER to continue") raw_input("Press ENTER to continue")
def cleanup(self, achalls): # pylint: disable=missing-docstring,no-self-use def cleanup(self, achalls):
pass # pragma: no cover # pylint: disable=missing-docstring,no-self-use,unused-argument
if self.conf("test-mode"):
assert self._httpd is not None, (
"cleanup() must be called after perform()")
if self._httpd.poll() is None:
logger.debug("Terminating manual command process")
os.killpg(self._httpd.pid, signal.SIGTERM)
else:
logger.debug("Manual command process already terminated "
"with %s code", self._httpd.returncode)
shutil.rmtree(self._root)
+50 -1
View File
@@ -1,4 +1,5 @@
"""Tests for letsencrypt.plugins.manual.""" """Tests for letsencrypt.plugins.manual."""
import signal
import unittest import unittest
import mock import mock
@@ -6,6 +7,8 @@ import mock
from acme import challenges from acme import challenges
from letsencrypt import achallenges from letsencrypt import achallenges
from letsencrypt import errors
from letsencrypt.tests import acme_util from letsencrypt.tests import acme_util
@@ -15,11 +18,18 @@ class ManualAuthenticatorTest(unittest.TestCase):
def setUp(self): def setUp(self):
from letsencrypt.plugins.manual import ManualAuthenticator from letsencrypt.plugins.manual import ManualAuthenticator
self.config = mock.MagicMock( self.config = mock.MagicMock(
no_simple_http_tls=True, simple_http_port=4430) no_simple_http_tls=True, simple_http_port=4430,
manual_test_mode=False)
self.auth = ManualAuthenticator(config=self.config, name="manual") self.auth = ManualAuthenticator(config=self.config, name="manual")
self.achalls = [achallenges.SimpleHTTP( self.achalls = [achallenges.SimpleHTTP(
challb=acme_util.SIMPLE_HTTP, domain="foo.com", key=None)] challb=acme_util.SIMPLE_HTTP, domain="foo.com", key=None)]
config_test_mode = mock.MagicMock(
no_simple_http_tls=True, simple_http_port=4430,
manual_test_mode=True)
self.auth_test_mode = ManualAuthenticator(
config=config_test_mode, name="manual")
def test_more_info(self): def test_more_info(self):
self.assertTrue(isinstance(self.auth.more_info(), str)) self.assertTrue(isinstance(self.auth.more_info(), str))
@@ -51,6 +61,45 @@ class ManualAuthenticatorTest(unittest.TestCase):
mock_verify.return_value = False mock_verify.return_value = False
self.assertEqual([None], self.auth.perform(self.achalls)) self.assertEqual([None], self.auth.perform(self.achalls))
@mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True)
def test_perform_test_command_oserror(self, mock_popen):
mock_popen.side_effect = OSError
self.assertEqual([False], self.auth_test_mode.perform(self.achalls))
@mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True)
@mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True)
def test_perform_test_command_run_failure(
self, mock_popen, unused_mock_sleep):
mock_popen.poll.return_value = 10
mock_popen.return_value.pid = 1234
self.assertRaises(
errors.Error, self.auth_test_mode.perform, self.achalls)
@mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True)
@mock.patch("acme.challenges.SimpleHTTPResponse.simple_verify",
autospec=True)
@mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True)
def test_perform_test_mode(self, mock_popen, mock_verify, mock_sleep):
mock_popen.return_value.poll.side_effect = [None, 10]
mock_popen.return_value.pid = 1234
mock_verify.return_value = False
self.assertEqual([False], self.auth_test_mode.perform(self.achalls))
self.assertEqual(1, mock_sleep.call_count)
def test_cleanup_test_mode_already_terminated(self):
# pylint: disable=protected-access
self.auth_test_mode._httpd = httpd = mock.Mock()
httpd.poll.return_value = 0
self.auth_test_mode.cleanup(self.achalls)
@mock.patch("letsencrypt.plugins.manual.os.killpg", autospec=True)
def test_cleanup_test_mode_kills_still_running(self, mock_killpg):
# pylint: disable=protected-access
self.auth_test_mode._httpd = httpd = mock.Mock(pid=1234)
httpd.poll.return_value = None
self.auth_test_mode.cleanup(self.achalls)
mock_killpg.assert_called_once_with(1234, signal.SIGTERM)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() # pragma: no cover unittest.main() # pragma: no cover
@@ -196,6 +196,10 @@ class StandaloneAuthenticator(common.Plugin):
""" """
signal.signal(signal.SIGINT, self.subproc_signal_handler) signal.signal(signal.SIGINT, self.subproc_signal_handler)
self.sock = socket.socket() self.sock = socket.socket()
# SO_REUSEADDR flag tells the kernel to reuse a local socket
# in TIME_WAIT state, without waiting for its natural timeout
# to expire.
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try: try:
self.sock.bind(("0.0.0.0", port)) self.sock.bind(("0.0.0.0", port))
except socket.error, error: except socket.error, error:
+2 -1
View File
@@ -16,6 +16,7 @@ import tempfile
import OpenSSL import OpenSSL
from acme import client as acme_client from acme import client as acme_client
from acme import crypto_util as acme_crypto_util
from acme.jose import util as jose_util from acme.jose import util as jose_util
from letsencrypt import crypto_util from letsencrypt import crypto_util
@@ -520,7 +521,7 @@ class Cert(object):
def get_san(self): def get_san(self):
"""Get subject alternative name if available.""" """Get subject alternative name if available."""
# pylint: disable=protected-access # pylint: disable=protected-access
return ", ".join(crypto_util._pyopenssl_cert_or_req_san(self._cert)) return ", ".join(acme_crypto_util._pyopenssl_cert_or_req_san(self._cert))
def __str__(self): def __str__(self):
text = [ text = [
+2 -3
View File
@@ -4,10 +4,9 @@ import unittest
import OpenSSL import OpenSSL
from acme import challenges from acme import challenges
from acme import crypto_util as acme_crypto_util
from acme import jose from acme import jose
from letsencrypt import crypto_util
from letsencrypt.tests import acme_util from letsencrypt.tests import acme_util
from letsencrypt.tests import test_util from letsencrypt.tests import test_util
@@ -35,7 +34,7 @@ class DVSNITest(unittest.TestCase):
OpenSSL.crypto.FILETYPE_PEM, cert_pem) OpenSSL.crypto.FILETYPE_PEM, cert_pem)
self.assertEqual(cert.get_subject().CN, "example.com") self.assertEqual(cert.get_subject().CN, "example.com")
# pylint: disable=protected-access # pylint: disable=protected-access
self.assertEqual(crypto_util._pyopenssl_cert_or_req_san(cert), [ self.assertEqual(acme_crypto_util._pyopenssl_cert_or_req_san(cert), [
"example.com", self.chall.nonce_domain, "example.com", self.chall.nonce_domain,
self.response.z_domain(self.chall)]) self.response.z_domain(self.chall)])
-9
View File
@@ -160,15 +160,6 @@ class ValidPrivkeyTest(unittest.TestCase):
self.assertFalse(self._call('foo bar')) self.assertFalse(self._call('foo bar'))
class MakeSSCertTest(unittest.TestCase):
# pylint: disable=too-few-public-methods
"""Tests for letsencrypt.crypto_util.make_ss_cert."""
def test_it(self): # pylint: disable=no-self-use
from letsencrypt.crypto_util import make_ss_cert
make_ss_cert(RSA512_KEY, ['example.com', 'www.example.com'])
class GetSANsFromCertTest(unittest.TestCase): class GetSANsFromCertTest(unittest.TestCase):
"""Tests for letsencrypt.crypto_util.get_sans_from_cert.""" """Tests for letsencrypt.crypto_util.get_sans_from_cert."""
+1 -2
View File
@@ -37,8 +37,7 @@ install_requires = [
'mock<1.1.0', # py26 'mock<1.1.0', # py26
'parsedatetime', 'parsedatetime',
'psutil>=2.1.0', # net_connections introduced in 2.1.0 'psutil>=2.1.0', # net_connections introduced in 2.1.0
# https://pyopenssl.readthedocs.org/en/latest/api/crypto.html#OpenSSL.crypto.X509Req.get_extensions 'PyOpenSSL',
'PyOpenSSL>=0.15',
'pyrfc3339', 'pyrfc3339',
'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280 'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280
'pytz', 'pytz',
+2
View File
@@ -23,6 +23,8 @@ common() {
common --domains le1.wtf auth common --domains le1.wtf auth
common --domains le2.wtf run common --domains le2.wtf run
common -a manual -d le.wtf auth
common -a manual -d le.wtf --no-simple-http-tls auth
export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \ export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \
OPENSSL_CNF=examples/openssl.cnf OPENSSL_CNF=examples/openssl.cnf
+6 -1
View File
@@ -11,5 +11,10 @@ export GOPATH="${GOPATH:-/tmp/go}"
go get -d github.com/letsencrypt/boulder/cmd/boulder go get -d github.com/letsencrypt/boulder/cmd/boulder
cd $GOPATH/src/github.com/letsencrypt/boulder cd $GOPATH/src/github.com/letsencrypt/boulder
make -j4 # Travis has 2 cores per build instance. make -j4 # Travis has 2 cores per build instance.
if [ "$1" = "amqp" ];
then
./start.py &
else
./start.sh & ./start.sh &
# Hopefully start.sh bootstraps before integration test is started... fi
# Hopefully start.py/start.sh bootstraps before integration test is started...
+2 -1
View File
@@ -10,11 +10,12 @@ store_flags="$store_flags --logs-dir $root/logs"
export root store_flags export root store_flags
letsencrypt_test () { letsencrypt_test () {
# first three flags required, rest is handy defaults
letsencrypt \ letsencrypt \
--server "${SERVER:-http://localhost:4000/acme/new-reg}" \ --server "${SERVER:-http://localhost:4000/acme/new-reg}" \
--no-verify-ssl \ --no-verify-ssl \
--dvsni-port 5001 \ --dvsni-port 5001 \
--simple-http-port 5001 \
--manual-test-mode \
$store_flags \ $store_flags \
--text \ --text \
--agree-eula \ --agree-eula \