Skip to content

Commit

Permalink
Merge remote-tracking branch 'github/letsencrypt/master' into py2.6-3
Browse files Browse the repository at this point in the history
  • Loading branch information
kuba committed Oct 17, 2015
2 parents 4c2d5db + 7fe8bbe commit 09fa115
Show file tree
Hide file tree
Showing 73 changed files with 2,035 additions and 1,478 deletions.
9 changes: 9 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ env:
- TOXENV=lint
- TOXENV=cover


# Only build pushes to the master branch, PRs, and branches beginning with
# `test-`. This reduces the number of simultaneous Travis runs, which speeds
# turnaround time on review since there is a cap of 5 simultaneous runs.
branches:
only:
- master
- /^test-.*$/

sudo: false # containers
addons:
# make sure simplehttp simple verification works (custom /etc/hosts)
Expand Down
16 changes: 12 additions & 4 deletions acme/acme/challenges.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ class SimpleHTTP(DVChallenge):
TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec
"""Minimum size of the :attr:`token` in bytes."""

URI_ROOT_PATH = ".well-known/acme-challenge"
"""URI root path for the server provisioned resource."""

# TODO: acme-spec doesn't specify token as base64-encoded value
token = jose.Field(
"token", encoder=jose.encode_b64jose, decoder=functools.partial(
Expand All @@ -106,6 +109,11 @@ def good_token(self): # XXX: @token.decoder
# URI_ROOT_PATH!
return b'..' not in self.token and b'/' not in self.token

@property
def path(self):
"""Path (starting with '/') for provisioned resource."""
return '/' + self.URI_ROOT_PATH + '/' + self.encode('token')


@ChallengeResponse.register
class SimpleHTTPResponse(ChallengeResponse):
Expand All @@ -117,12 +125,12 @@ class SimpleHTTPResponse(ChallengeResponse):
typ = "simpleHttp"
tls = jose.Field("tls", default=True, omitempty=True)

URI_ROOT_PATH = ".well-known/acme-challenge"
"""URI root path for the server provisioned resource."""

URI_ROOT_PATH = SimpleHTTP.URI_ROOT_PATH
_URI_TEMPLATE = "{scheme}://{domain}/" + URI_ROOT_PATH + "/{token}"

CONTENT_TYPE = "application/jose+json"
PORT = 80
TLS_PORT = 443

@property
def scheme(self):
Expand All @@ -132,7 +140,7 @@ def scheme(self):
@property
def port(self):
"""Port that the ACME client should be listening for validation."""
return 443 if self.tls else 80
return self.TLS_PORT if self.tls else self.PORT

def uri(self, domain, chall):
"""Create an URI to the provisioned resource.
Expand Down
95 changes: 64 additions & 31 deletions acme/acme/crypto_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,47 +26,80 @@
_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.
class SSLSocket(object): # pylint: disable=too-few-public-methods
"""SSL wrapper for sockets.
: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.
:ivar socket sock: Original wrapped socket.
:ivar dict certs: Mapping from domain names (`bytes`) to
`OpenSSL.crypto.X509`.
:ivar method: See `OpenSSL.SSL.Context` for allowed values.
"""
def _pick_certificate(connection):
def __init__(self, sock, certs, method=_DEFAULT_DVSNI_SSL_METHOD):
self.sock = sock
self.certs = certs
self.method = method

def __getattr__(self, name):
return getattr(self.sock, name)

def _pick_certificate_cb(self, connection):
"""SNI certificate callback.
This method will set a new OpenSSL context object for this
connection when an incoming connection provides an SNI name
(in order to serve the appropriate certificate, if any).
:param connection: The TLS connection object on which the SNI
extension was received.
:type connection: :class:`OpenSSL.Connection`
"""
server_name = connection.get_servername()
try:
key, cert = certs[connection.get_servername()]
key, cert = self.certs[server_name]
except KeyError:
logger.debug("Server name (%s) not recognized, dropping SSL",
server_name)
return
new_context = OpenSSL.SSL.Context(method)
new_context = OpenSSL.SSL.Context(self.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)
class FakeConnection(object):
"""Fake OpenSSL.SSL.Connection."""

# pylint: disable=missing-docstring

def __init__(self, connection):
self._wrapped = connection

def __getattr__(self, name):
return getattr(self._wrapped, name)

def shutdown(self, *unused_args):
# OpenSSL.SSL.Connection.shutdown doesn't accept any args
return self._wrapped.shutdown()

def accept(self): # pylint: disable=missing-docstring
sock, addr = self.sock.accept()

context = OpenSSL.SSL.Context(self.method)
context.set_tlsext_servername_callback(self._pick_certificate_cb)

ssl_sock = self.FakeConnection(OpenSSL.SSL.Connection(context, sock))
ssl_sock.set_accept_state()

logger.debug("Performing handshake with %s", addr)
try:
ssl_sock.do_handshake()
except OpenSSL.SSL.Error as error:
# _pick_certificate_cb might have returned without
# creating SSL context (wrong server name)
raise socket.error(error)

return ssl_sock, addr


def probe_sni(name, host, port=443, timeout=300,
Expand Down
44 changes: 21 additions & 23 deletions acme/acme/crypto_util_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,43 @@
import time
import unittest

import mock
import OpenSSL
from six.moves import socketserver # pylint: disable=import-error

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."""
class SSLSocketAndProbeSNITest(unittest.TestCase):
"""Tests for acme.crypto_util.SSLSocket/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'))
key = test_util.load_pyopenssl_private_key('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]
from acme.crypto_util import SSLSocket

self.server = threading.Thread(target=self._run_server, args=(certs, sock))
self.server.start()
time.sleep(1) # TODO: avoid race conditions in other way
class _TestServer(socketserver.TCPServer):

@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
# pylint: disable=too-few-public-methods
# six.moves.* | pylint: disable=attribute-defined-outside-init,no-init

def server_bind(self): # pylint: disable=missing-docstring
self.socket = SSLSocket(socket.socket(), certs=certs)
socketserver.TCPServer.server_bind(self)

self.server = _TestServer(('', 0), socketserver.BaseRequestHandler)
self.port = self.server.socket.getsockname()[1]
self.server_thread = threading.Thread(
# pylint: disable=no-member
target=self.server.handle_request)
self.server_thread.start()
time.sleep(1) # TODO: avoid race conditions in other way

def tearDown(self):
self.server.join()
self.server_thread.join()

def _probe(self, name):
from acme.crypto_util import probe_sni
Expand Down
1 change: 1 addition & 0 deletions acme/acme/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class Error(jose.JSONObjectWithFields, Exception):
'connection': 'The server could not connect to the client for DV',
'dnssec': 'The server could not validate a DNSSEC signed domain',
'malformed': 'The request message was malformed',
'rateLimited': 'There were too many requests of a given type',
'serverInternal': 'The server experienced an internal error',
'tls': 'The server experienced a TLS error during DV',
'unauthorized': 'The client lacks sufficient authorization',
Expand Down
Loading

0 comments on commit 09fa115

Please sign in to comment.