Skip to content
This repository has been archived by the owner on Jan 13, 2021. It is now read-only.

Commit

Permalink
Merge pull request #215 from Lukasa/mylh-development
Browse files Browse the repository at this point in the history
Client certs work.
  • Loading branch information
Lukasa committed Mar 29, 2016
2 parents aa459b9 + ff15e11 commit 998aa02
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 53 deletions.
30 changes: 21 additions & 9 deletions hyper/contrib.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from hyper.common.connection import HTTPConnection
from hyper.compat import urlparse
from hyper.tls import init_context


class HTTP20Adapter(HTTPAdapter):
Expand All @@ -28,31 +29,42 @@ def __init__(self, *args, **kwargs):
#: A mapping between HTTP netlocs and ``HTTP20Connection`` objects.
self.connections = {}

def get_connection(self, host, port, scheme):
def get_connection(self, host, port, scheme, cert=None):
"""
Gets an appropriate HTTP/2 connection object based on host/port/scheme
tuples.
Gets an appropriate HTTP/2 connection object based on
host/port/scheme/cert tuples.
"""
secure = (scheme == 'https')

if port is None: # pragma: no cover
port = 80 if not secure else 443

ssl_context = None
if cert is not None:
ssl_context = init_context(cert=cert)

try:
conn = self.connections[(host, port, scheme)]
conn = self.connections[(host, port, scheme, cert)]
except KeyError:
conn = HTTPConnection(host, port, secure=secure)
self.connections[(host, port, scheme)] = conn
conn = HTTPConnection(
host,
port,
secure=secure,
ssl_context=ssl_context)
self.connections[(host, port, scheme, cert)] = conn

return conn

def send(self, request, stream=False, **kwargs):
def send(self, request, stream=False, cert=None, **kwargs):
"""
Sends a HTTP message to the server.
"""
parsed = urlparse(request.url)

conn = self.get_connection(parsed.hostname, parsed.port, parsed.scheme)
conn = self.get_connection(
parsed.hostname,
parsed.port,
parsed.scheme,
cert=cert)

# Build the selector.
selector = parsed.path
Expand Down
31 changes: 29 additions & 2 deletions hyper/tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def wrap_socket(sock, server_hostname, ssl_context=None, force_proto=None):
return (ssl_sock, proto)


def init_context(cert_path=None):
def init_context(cert_path=None, cert=None, cert_password=None):
"""
Create a new ``SSLContext`` that is correctly set up for an HTTP/2 connection.
This SSL context object can be customized and passed as a parameter to the
Expand All @@ -74,7 +74,24 @@ def init_context(cert_path=None):
certificate. The path to the certificate can be absolute or relative
to your working directory.
:param cert_path: (optional) The path to the certificate file.
:param cert_path: (optional) The path to the certificate file of
“certification authority” (CA) certificates
:param cert: (optional) if string, path to ssl client cert file (.pem).
If tuple, ('cert', 'key') pair.
The certfile string must be the path to a single file in PEM format
containing the certificate as well as any number of CA certificates
needed to establish the certificate’s authenticity. The keyfile string,
if present, must point to a file containing the private key in.
Otherwise the private key will be taken from certfile as well.
:param cert_password: (optional) The password argument may be a function to
call to get the password for decrypting the private key. It will only
be called if the private key is encrypted and a password is necessary.
It will be called with no arguments, and it should return a string,
bytes, or bytearray. If the return value is a string it will be
encoded as UTF-8 before using it to decrypt the key. Alternatively a
string, bytes, or bytearray value may be supplied directly as the
password argument. It will be ignored if the private key is not
encrypted and no password is needed.
:returns: An ``SSLContext`` correctly set up for HTTP/2.
"""
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
Expand All @@ -92,4 +109,14 @@ def init_context(cert_path=None):
# required by the spec
context.options |= ssl.OP_NO_COMPRESSION

if cert is not None:
try:
basestring
except NameError:
basestring = (str, bytes)
if not isinstance(cert, basestring):
context.load_cert_chain(cert[0], cert[1], cert_password)
else:
context.load_cert_chain(cert, password=cert_password)

return context
30 changes: 15 additions & 15 deletions test/certs/client.crt
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICnzCCAggCCQCGXOeu96ab6zANBgkqhkiG9w0BAQsFADCBkzELMAkGA1UEBhMC
Q0ExGTAXBgNVBAgMEEJyaXRpc2ggQ29sdW1iaWExEjAQBgNVBAcMCVZhbmNvdXZl
cjEdMBsGA1UECgwUVGVzdCBDbGllbnQgQ2VydCBJbmMxGzAZBgNVBAsMElVuaXQg
VGVzdCBEaXZpc2lvbjEZMBcGA1UEAwwQY2xpZW50LnRlc3QuY2VydDAeFw0xNTEy
MjIyMjA4NDJaFw0xNjAxMjEyMjA4NDJaMIGTMQswCQYDVQQGEwJDQTEZMBcGA1UE
CAwQQnJpdGlzaCBDb2x1bWJpYTESMBAGA1UEBwwJVmFuY291dmVyMR0wGwYDVQQK
DBRUZXN0IENsaWVudCBDZXJ0IEluYzEbMBkGA1UECwwSVW5pdCBUZXN0IERpdmlz
aW9uMRkwFwYDVQQDDBBjbGllbnQudGVzdC5jZXJ0MIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQCzHp5Md/ALVxfxTQy2ZmKYKwzx8z5hQQT1lPzuLLR5tpWALhMz
1t11nnMABzc0mUrtWQYQIt+skNnrna/snKhnFor9GABsPArtZPP+cdxvjr510okz
X87bJjwD35wDmEEjJbpGVQl4MMmC+TVBDxXdVSJpS/Cx2DtsevBbrydsBwIDAQAB
MA0GCSqGSIb3DQEBCwUAA4GBAIId1RjLsA5q3XRgMQs0zIez7bXjpnaNIfRVSZQm
+xXZcDkGdIYG8zSjHM/oRotvMpA3vC03IplTO0HWbSNIywTtxuGoz2meyWu3hLUb
wGT++dYEBzLkEZIi1bAYnSd14eLrrtkAbOf47pki0QSqGcIiwzwMV5dakziaUAcm
jNUJ
MIICpDCCAg2gAwIBAgIJAIZc5673ppvrMA0GCSqGSIb3DQEBCwUAMIGTMQswCQYD
VQQGEwJDQTEZMBcGA1UECAwQQnJpdGlzaCBDb2x1bWJpYTESMBAGA1UEBwwJVmFu
Y291dmVyMR0wGwYDVQQKDBRUZXN0IENsaWVudCBDZXJ0IEluYzEbMBkGA1UECwwS
VW5pdCBUZXN0IERpdmlzaW9uMRkwFwYDVQQDDBBjbGllbnQudGVzdC5jZXJ0MB4X
DTE1MTIyMjIyMDg0MloXDTQ2MDMyMjE0MTUxM1owgZMxCzAJBgNVBAYTAkNBMRkw
FwYDVQQIDBBCcml0aXNoIENvbHVtYmlhMRIwEAYDVQQHDAlWYW5jb3V2ZXIxHTAb
BgNVBAoMFFRlc3QgQ2xpZW50IENlcnQgSW5jMRswGQYDVQQLDBJVbml0IFRlc3Qg
RGl2aXNpb24xGTAXBgNVBAMMEGNsaWVudC50ZXN0LmNlcnQwgZ8wDQYJKoZIhvcN
AQEBBQADgY0AMIGJAoGBALMenkx38AtXF/FNDLZmYpgrDPHzPmFBBPWU/O4stHm2
lYAuEzPW3XWecwAHNzSZSu1ZBhAi36yQ2eudr+ycqGcWiv0YAGw8Cu1k8/5x3G+O
vnXSiTNfztsmPAPfnAOYQSMlukZVCXgwyYL5NUEPFd1VImlL8LHYO2x68FuvJ2wH
AgMBAAEwDQYJKoZIhvcNAQELBQADgYEAml8JTaI9VEZIkJh8HLMG9MpxRAV2brWf
bVE8arBlYgwpjSq/Oi/NlJfdAZNbYu0/Hp0XXgL0dsIaeVBU9LsiG5YUlsWi2aLn
VjtvpzPm8AFuNl6+3oMdeO49DAExTJBUNfhcYDISF5LMtQjIjftXSlzIEe77ESiF
US+HOF+gvns=
-----END CERTIFICATE-----
33 changes: 33 additions & 0 deletions test/certs/nopassword.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCzHp5Md/ALVxfxTQy2ZmKYKwzx8z5hQQT1lPzuLLR5tpWALhMz
1t11nnMABzc0mUrtWQYQIt+skNnrna/snKhnFor9GABsPArtZPP+cdxvjr510okz
X87bJjwD35wDmEEjJbpGVQl4MMmC+TVBDxXdVSJpS/Cx2DtsevBbrydsBwIDAQAB
AoGBAIrpsgbH9Yg18UGYacWDve8ZZB64cKZoEHqNI2NrlEPYkDUqhoXrmzC+w50/
+1Z1lCBirEsTYLiqVfdfAbLVA9wygxowGHFBfgerhsFOT6QdTWXYphesdKHOk0Tm
zC2KBudXAwuZFk3gGwqltg3oJ410nuFzHr2LlkJmLgcCUCCRAkEA4hSMfw0IGB24
NwvGuJmTQiIoWfEG6FE0jORi5EK5/rvaAQn2G1v2CWkNpeGl5ChJuUFELgEYoXyA
jUWT6Q0jZQJBAMrTEq97cC24wkCFXIRhUifLR7BIESJl0LsQMIpHBbugqKlV80P+
XT38xC1/Ko3VTRzzW5CNXhxIkS7uhOx/WPsCQCNfb7QZR+DuFXqXXxpW/RFl4sZf
zeLfgcdhnI92WT+gCLYM5vLU6qQCSd5gdeJC43YW3iIMjdwGbN6tujzGj3kCQA+m
g92Yl8pfZFXYNJBedhlt/SUkGnZeBuI4WJaC7nVQ61LNuAy6FvXOiFC+bEkveVjP
inQ4BFgIXXOSoh0honsCQFXlJHaKavfmmk46nHUdO+zv0Oc4+gPcwqpqy4w2y1g/
L9HVcdq2dHtwdd9a5n7S1UI73rrspIEhOJA/A5poB0w=
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIICpDCCAg2gAwIBAgIJAIZc5673ppvrMA0GCSqGSIb3DQEBCwUAMIGTMQswCQYD
VQQGEwJDQTEZMBcGA1UECAwQQnJpdGlzaCBDb2x1bWJpYTESMBAGA1UEBwwJVmFu
Y291dmVyMR0wGwYDVQQKDBRUZXN0IENsaWVudCBDZXJ0IEluYzEbMBkGA1UECwwS
VW5pdCBUZXN0IERpdmlzaW9uMRkwFwYDVQQDDBBjbGllbnQudGVzdC5jZXJ0MB4X
DTE1MTIyMjIyMDg0MloXDTQ2MDMyMjE0MTUxM1owgZMxCzAJBgNVBAYTAkNBMRkw
FwYDVQQIDBBCcml0aXNoIENvbHVtYmlhMRIwEAYDVQQHDAlWYW5jb3V2ZXIxHTAb
BgNVBAoMFFRlc3QgQ2xpZW50IENlcnQgSW5jMRswGQYDVQQLDBJVbml0IFRlc3Qg
RGl2aXNpb24xGTAXBgNVBAMMEGNsaWVudC50ZXN0LmNlcnQwgZ8wDQYJKoZIhvcN
AQEBBQADgY0AMIGJAoGBALMenkx38AtXF/FNDLZmYpgrDPHzPmFBBPWU/O4stHm2
lYAuEzPW3XWecwAHNzSZSu1ZBhAi36yQ2eudr+ycqGcWiv0YAGw8Cu1k8/5x3G+O
vnXSiTNfztsmPAPfnAOYQSMlukZVCXgwyYL5NUEPFd1VImlL8LHYO2x68FuvJ2wH
AgMBAAEwDQYJKoZIhvcNAQELBQADgYEAml8JTaI9VEZIkJh8HLMG9MpxRAV2brWf
bVE8arBlYgwpjSq/Oi/NlJfdAZNbYu0/Hp0XXgL0dsIaeVBU9LsiG5YUlsWi2aLn
VjtvpzPm8AFuNl6+3oMdeO49DAExTJBUNfhcYDISF5LMtQjIjftXSlzIEe77ESiF
US+HOF+gvns=
-----END CERTIFICATE-----

18 changes: 18 additions & 0 deletions test/test_SSLContext.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,22 @@
"""
Tests the hyper SSLContext.
"""
import os

import hyper
from hyper.common.connection import HTTPConnection
from hyper.compat import ssl

import pytest


TEST_DIR = os.path.abspath(os.path.dirname(__file__))
TEST_CERTS_DIR = os.path.join(TEST_DIR, 'certs')
CLIENT_CERT_FILE = os.path.join(TEST_CERTS_DIR, 'client.crt')
CLIENT_KEY_FILE = os.path.join(TEST_CERTS_DIR, 'client.key')
CLIENT_PEM_FILE = os.path.join(TEST_CERTS_DIR, 'nopassword.pem')


class TestSSLContext(object):
"""
Tests default and custom SSLContext
Expand Down Expand Up @@ -47,3 +58,10 @@ def test_HTTPConnection_with_custom_context(self):
assert conn.ssl_context.check_hostname == True
assert conn.ssl_context.verify_mode == ssl.CERT_REQUIRED
assert conn.ssl_context.options & ssl.OP_NO_COMPRESSION != 0


def test_client_certificates(self):
context = hyper.tls.init_context(
cert=(CLIENT_CERT_FILE, CLIENT_KEY_FILE),
cert_password=b'abc123')
context = hyper.tls.init_context(cert=CLIENT_PEM_FILE)
20 changes: 20 additions & 0 deletions test/test_hyper.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
import hyper


TEST_DIR = os.path.abspath(os.path.dirname(__file__))
TEST_CERTS_DIR = os.path.join(TEST_DIR, 'certs')
CLIENT_PEM_FILE = os.path.join(TEST_CERTS_DIR, 'nopassword.pem')


def decode_frame(frame_data):
f, length = Frame.parse_frame_header(frame_data[:9])
f.parse_body(memoryview(frame_data[9:9 + length]))
Expand Down Expand Up @@ -784,6 +789,21 @@ def test_adapter_reuses_connections(self):

assert conn1 is conn2

def test_adapter_accept_client_certificate(self):
a = HTTP20Adapter()
conn1 = a.get_connection(
'http2bin.org',
80,
'http',
cert=CLIENT_PEM_FILE)
conn2 = a.get_connection(
'http2bin.org',
80,
'http',
cert=CLIENT_PEM_FILE)
assert conn1 is conn2



class TestUtilities(object):
def test_combining_repeated_headers(self):
Expand Down
27 changes: 0 additions & 27 deletions test/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import requests
import threading
import time
import socket
import hyper
import hyper.http11.connection
import pytest
Expand All @@ -26,7 +25,6 @@
REQUEST_CODES, REQUEST_CODES_LENGTH
)
from hyper.http20.exceptions import ConnectionError, StreamResetError
from hyper.tls import wrap_socket
from server import SocketLevelTest

# Turn off certificate verification for the tests.
Expand Down Expand Up @@ -76,31 +74,6 @@ def receive_preamble(sock):
return


class TestBasicSocketManipulation(SocketLevelTest):
# These aren't HTTP/2 tests, but it doesn't hurt to leave it.
h2 = True

def test_connection_string(self):
self.set_up()
evt = threading.Event()

def socket_handler(listener):
sock = listener.accept()[0]

evt.wait(5)
sock.close()

self._start_server(socket_handler)
s = socket.create_connection((self.host, self.port))
s, proto = wrap_socket(s, "localhost", force_proto=b"test")
s.close()
evt.set()

assert proto == b"test"

self.tear_down()


class TestHyperIntegration(SocketLevelTest):
# These are HTTP/2 tests.
h2 = True
Expand Down
97 changes: 97 additions & 0 deletions test/test_ssl_socket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
"""
test/test_ssl_socket
~~~~~~~~~~~~~~~~~~~~
This file defines tests for hyper that validate our TLS handling.
"""
import os
import socket
import ssl
import threading

import pytest

from hyper.tls import wrap_socket, init_context

from server import SocketLevelTest


TEST_DIR = os.path.abspath(os.path.dirname(__file__))
TEST_CERTS_DIR = os.path.join(TEST_DIR, "certs")
CLIENT_CERT_FILE = os.path.join(TEST_CERTS_DIR, 'client.crt')
CLIENT_KEY_FILE = os.path.join(TEST_CERTS_DIR, 'client.key')
CLIENT_PEM_FILE = os.path.join(TEST_CERTS_DIR, 'nopassword.pem')
SERVER_CERT_FILE = os.path.join(TEST_CERTS_DIR, 'server.crt')
SERVER_KEY_FILE = os.path.join(TEST_CERTS_DIR, 'server.key')


class TestBasicSocketManipulation(SocketLevelTest):
# These aren't HTTP/2 tests, but it doesn't hurt to leave it.
h2 = True

def test_connection_string(self):
self.set_up()
evt = threading.Event()

def socket_handler(listener):
sock = listener.accept()[0]

evt.wait(5)
sock.close()

self._start_server(socket_handler)
s = socket.create_connection((self.host, self.port))
s, proto = wrap_socket(s, "localhost", force_proto=b"test")
s.close()
evt.set()

assert proto == b"test"

self.tear_down()

@pytest.mark.parametrize(
'context_kwargs',
[
{'cert': CLIENT_PEM_FILE},
{
'cert': (CLIENT_CERT_FILE, CLIENT_KEY_FILE),
'cert_password': b'abc123'
},
]
)
def test_client_certificate(self, context_kwargs):
# Don't have the server thread do TLS: we'll do it ourselves.
self.set_up(secure=False)
evt = threading.Event()
data = []

def socket_handler(listener):
sock = listener.accept()[0]
sock = ssl.wrap_socket(
sock,
ssl_version=ssl.PROTOCOL_SSLv23,
certfile=SERVER_CERT_FILE,
keyfile=SERVER_KEY_FILE,
cert_reqs=ssl.CERT_REQUIRED,
ca_certs=CLIENT_PEM_FILE,
server_side=True
)
data.append(sock.recv(65535))
evt.wait(5)
sock.close()

self._start_server(socket_handler)

# Set up the client context. Don't validate the server cert though.
context = init_context(**context_kwargs)
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE

s = socket.create_connection((self.host, self.port))
s, proto = wrap_socket(s, "localhost", ssl_context=context)
s.sendall(b'hi')
s.close()
evt.set()

self.tear_down()

0 comments on commit 998aa02

Please sign in to comment.