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

Commit 998aa02

Browse files
committed
Merge pull request #215 from Lukasa/mylh-development
Client certs work.
2 parents aa459b9 + ff15e11 commit 998aa02

File tree

8 files changed

+233
-53
lines changed

8 files changed

+233
-53
lines changed

hyper/contrib.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from hyper.common.connection import HTTPConnection
1818
from hyper.compat import urlparse
19+
from hyper.tls import init_context
1920

2021

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

31-
def get_connection(self, host, port, scheme):
32+
def get_connection(self, host, port, scheme, cert=None):
3233
"""
33-
Gets an appropriate HTTP/2 connection object based on host/port/scheme
34-
tuples.
34+
Gets an appropriate HTTP/2 connection object based on
35+
host/port/scheme/cert tuples.
3536
"""
3637
secure = (scheme == 'https')
3738

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

42+
ssl_context = None
43+
if cert is not None:
44+
ssl_context = init_context(cert=cert)
45+
4146
try:
42-
conn = self.connections[(host, port, scheme)]
47+
conn = self.connections[(host, port, scheme, cert)]
4348
except KeyError:
44-
conn = HTTPConnection(host, port, secure=secure)
45-
self.connections[(host, port, scheme)] = conn
49+
conn = HTTPConnection(
50+
host,
51+
port,
52+
secure=secure,
53+
ssl_context=ssl_context)
54+
self.connections[(host, port, scheme, cert)] = conn
4655

4756
return conn
4857

49-
def send(self, request, stream=False, **kwargs):
58+
def send(self, request, stream=False, cert=None, **kwargs):
5059
"""
5160
Sends a HTTP message to the server.
5261
"""
5362
parsed = urlparse(request.url)
54-
55-
conn = self.get_connection(parsed.hostname, parsed.port, parsed.scheme)
63+
conn = self.get_connection(
64+
parsed.hostname,
65+
parsed.port,
66+
parsed.scheme,
67+
cert=cert)
5668

5769
# Build the selector.
5870
selector = parsed.path

hyper/tls.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def wrap_socket(sock, server_hostname, ssl_context=None, force_proto=None):
6565
return (ssl_sock, proto)
6666

6767

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

112+
if cert is not None:
113+
try:
114+
basestring
115+
except NameError:
116+
basestring = (str, bytes)
117+
if not isinstance(cert, basestring):
118+
context.load_cert_chain(cert[0], cert[1], cert_password)
119+
else:
120+
context.load_cert_chain(cert, password=cert_password)
121+
95122
return context

test/certs/client.crt

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
-----BEGIN CERTIFICATE-----
2-
MIICnzCCAggCCQCGXOeu96ab6zANBgkqhkiG9w0BAQsFADCBkzELMAkGA1UEBhMC
3-
Q0ExGTAXBgNVBAgMEEJyaXRpc2ggQ29sdW1iaWExEjAQBgNVBAcMCVZhbmNvdXZl
4-
cjEdMBsGA1UECgwUVGVzdCBDbGllbnQgQ2VydCBJbmMxGzAZBgNVBAsMElVuaXQg
5-
VGVzdCBEaXZpc2lvbjEZMBcGA1UEAwwQY2xpZW50LnRlc3QuY2VydDAeFw0xNTEy
6-
MjIyMjA4NDJaFw0xNjAxMjEyMjA4NDJaMIGTMQswCQYDVQQGEwJDQTEZMBcGA1UE
7-
CAwQQnJpdGlzaCBDb2x1bWJpYTESMBAGA1UEBwwJVmFuY291dmVyMR0wGwYDVQQK
8-
DBRUZXN0IENsaWVudCBDZXJ0IEluYzEbMBkGA1UECwwSVW5pdCBUZXN0IERpdmlz
9-
aW9uMRkwFwYDVQQDDBBjbGllbnQudGVzdC5jZXJ0MIGfMA0GCSqGSIb3DQEBAQUA
10-
A4GNADCBiQKBgQCzHp5Md/ALVxfxTQy2ZmKYKwzx8z5hQQT1lPzuLLR5tpWALhMz
11-
1t11nnMABzc0mUrtWQYQIt+skNnrna/snKhnFor9GABsPArtZPP+cdxvjr510okz
12-
X87bJjwD35wDmEEjJbpGVQl4MMmC+TVBDxXdVSJpS/Cx2DtsevBbrydsBwIDAQAB
13-
MA0GCSqGSIb3DQEBCwUAA4GBAIId1RjLsA5q3XRgMQs0zIez7bXjpnaNIfRVSZQm
14-
+xXZcDkGdIYG8zSjHM/oRotvMpA3vC03IplTO0HWbSNIywTtxuGoz2meyWu3hLUb
15-
wGT++dYEBzLkEZIi1bAYnSd14eLrrtkAbOf47pki0QSqGcIiwzwMV5dakziaUAcm
16-
jNUJ
2+
MIICpDCCAg2gAwIBAgIJAIZc5673ppvrMA0GCSqGSIb3DQEBCwUAMIGTMQswCQYD
3+
VQQGEwJDQTEZMBcGA1UECAwQQnJpdGlzaCBDb2x1bWJpYTESMBAGA1UEBwwJVmFu
4+
Y291dmVyMR0wGwYDVQQKDBRUZXN0IENsaWVudCBDZXJ0IEluYzEbMBkGA1UECwwS
5+
VW5pdCBUZXN0IERpdmlzaW9uMRkwFwYDVQQDDBBjbGllbnQudGVzdC5jZXJ0MB4X
6+
DTE1MTIyMjIyMDg0MloXDTQ2MDMyMjE0MTUxM1owgZMxCzAJBgNVBAYTAkNBMRkw
7+
FwYDVQQIDBBCcml0aXNoIENvbHVtYmlhMRIwEAYDVQQHDAlWYW5jb3V2ZXIxHTAb
8+
BgNVBAoMFFRlc3QgQ2xpZW50IENlcnQgSW5jMRswGQYDVQQLDBJVbml0IFRlc3Qg
9+
RGl2aXNpb24xGTAXBgNVBAMMEGNsaWVudC50ZXN0LmNlcnQwgZ8wDQYJKoZIhvcN
10+
AQEBBQADgY0AMIGJAoGBALMenkx38AtXF/FNDLZmYpgrDPHzPmFBBPWU/O4stHm2
11+
lYAuEzPW3XWecwAHNzSZSu1ZBhAi36yQ2eudr+ycqGcWiv0YAGw8Cu1k8/5x3G+O
12+
vnXSiTNfztsmPAPfnAOYQSMlukZVCXgwyYL5NUEPFd1VImlL8LHYO2x68FuvJ2wH
13+
AgMBAAEwDQYJKoZIhvcNAQELBQADgYEAml8JTaI9VEZIkJh8HLMG9MpxRAV2brWf
14+
bVE8arBlYgwpjSq/Oi/NlJfdAZNbYu0/Hp0XXgL0dsIaeVBU9LsiG5YUlsWi2aLn
15+
VjtvpzPm8AFuNl6+3oMdeO49DAExTJBUNfhcYDISF5LMtQjIjftXSlzIEe77ESiF
16+
US+HOF+gvns=
1717
-----END CERTIFICATE-----

test/certs/nopassword.pem

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIICXAIBAAKBgQCzHp5Md/ALVxfxTQy2ZmKYKwzx8z5hQQT1lPzuLLR5tpWALhMz
3+
1t11nnMABzc0mUrtWQYQIt+skNnrna/snKhnFor9GABsPArtZPP+cdxvjr510okz
4+
X87bJjwD35wDmEEjJbpGVQl4MMmC+TVBDxXdVSJpS/Cx2DtsevBbrydsBwIDAQAB
5+
AoGBAIrpsgbH9Yg18UGYacWDve8ZZB64cKZoEHqNI2NrlEPYkDUqhoXrmzC+w50/
6+
+1Z1lCBirEsTYLiqVfdfAbLVA9wygxowGHFBfgerhsFOT6QdTWXYphesdKHOk0Tm
7+
zC2KBudXAwuZFk3gGwqltg3oJ410nuFzHr2LlkJmLgcCUCCRAkEA4hSMfw0IGB24
8+
NwvGuJmTQiIoWfEG6FE0jORi5EK5/rvaAQn2G1v2CWkNpeGl5ChJuUFELgEYoXyA
9+
jUWT6Q0jZQJBAMrTEq97cC24wkCFXIRhUifLR7BIESJl0LsQMIpHBbugqKlV80P+
10+
XT38xC1/Ko3VTRzzW5CNXhxIkS7uhOx/WPsCQCNfb7QZR+DuFXqXXxpW/RFl4sZf
11+
zeLfgcdhnI92WT+gCLYM5vLU6qQCSd5gdeJC43YW3iIMjdwGbN6tujzGj3kCQA+m
12+
g92Yl8pfZFXYNJBedhlt/SUkGnZeBuI4WJaC7nVQ61LNuAy6FvXOiFC+bEkveVjP
13+
inQ4BFgIXXOSoh0honsCQFXlJHaKavfmmk46nHUdO+zv0Oc4+gPcwqpqy4w2y1g/
14+
L9HVcdq2dHtwdd9a5n7S1UI73rrspIEhOJA/A5poB0w=
15+
-----END RSA PRIVATE KEY-----
16+
-----BEGIN CERTIFICATE-----
17+
MIICpDCCAg2gAwIBAgIJAIZc5673ppvrMA0GCSqGSIb3DQEBCwUAMIGTMQswCQYD
18+
VQQGEwJDQTEZMBcGA1UECAwQQnJpdGlzaCBDb2x1bWJpYTESMBAGA1UEBwwJVmFu
19+
Y291dmVyMR0wGwYDVQQKDBRUZXN0IENsaWVudCBDZXJ0IEluYzEbMBkGA1UECwwS
20+
VW5pdCBUZXN0IERpdmlzaW9uMRkwFwYDVQQDDBBjbGllbnQudGVzdC5jZXJ0MB4X
21+
DTE1MTIyMjIyMDg0MloXDTQ2MDMyMjE0MTUxM1owgZMxCzAJBgNVBAYTAkNBMRkw
22+
FwYDVQQIDBBCcml0aXNoIENvbHVtYmlhMRIwEAYDVQQHDAlWYW5jb3V2ZXIxHTAb
23+
BgNVBAoMFFRlc3QgQ2xpZW50IENlcnQgSW5jMRswGQYDVQQLDBJVbml0IFRlc3Qg
24+
RGl2aXNpb24xGTAXBgNVBAMMEGNsaWVudC50ZXN0LmNlcnQwgZ8wDQYJKoZIhvcN
25+
AQEBBQADgY0AMIGJAoGBALMenkx38AtXF/FNDLZmYpgrDPHzPmFBBPWU/O4stHm2
26+
lYAuEzPW3XWecwAHNzSZSu1ZBhAi36yQ2eudr+ycqGcWiv0YAGw8Cu1k8/5x3G+O
27+
vnXSiTNfztsmPAPfnAOYQSMlukZVCXgwyYL5NUEPFd1VImlL8LHYO2x68FuvJ2wH
28+
AgMBAAEwDQYJKoZIhvcNAQELBQADgYEAml8JTaI9VEZIkJh8HLMG9MpxRAV2brWf
29+
bVE8arBlYgwpjSq/Oi/NlJfdAZNbYu0/Hp0XXgL0dsIaeVBU9LsiG5YUlsWi2aLn
30+
VjtvpzPm8AFuNl6+3oMdeO49DAExTJBUNfhcYDISF5LMtQjIjftXSlzIEe77ESiF
31+
US+HOF+gvns=
32+
-----END CERTIFICATE-----
33+

test/test_SSLContext.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,22 @@
22
"""
33
Tests the hyper SSLContext.
44
"""
5+
import os
6+
57
import hyper
68
from hyper.common.connection import HTTPConnection
79
from hyper.compat import ssl
10+
811
import pytest
912

13+
14+
TEST_DIR = os.path.abspath(os.path.dirname(__file__))
15+
TEST_CERTS_DIR = os.path.join(TEST_DIR, 'certs')
16+
CLIENT_CERT_FILE = os.path.join(TEST_CERTS_DIR, 'client.crt')
17+
CLIENT_KEY_FILE = os.path.join(TEST_CERTS_DIR, 'client.key')
18+
CLIENT_PEM_FILE = os.path.join(TEST_CERTS_DIR, 'nopassword.pem')
19+
20+
1021
class TestSSLContext(object):
1122
"""
1223
Tests default and custom SSLContext
@@ -47,3 +58,10 @@ def test_HTTPConnection_with_custom_context(self):
4758
assert conn.ssl_context.check_hostname == True
4859
assert conn.ssl_context.verify_mode == ssl.CERT_REQUIRED
4960
assert conn.ssl_context.options & ssl.OP_NO_COMPRESSION != 0
61+
62+
63+
def test_client_certificates(self):
64+
context = hyper.tls.init_context(
65+
cert=(CLIENT_CERT_FILE, CLIENT_KEY_FILE),
66+
cert_password=b'abc123')
67+
context = hyper.tls.init_context(cert=CLIENT_PEM_FILE)

test/test_hyper.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
import hyper
2929

3030

31+
TEST_DIR = os.path.abspath(os.path.dirname(__file__))
32+
TEST_CERTS_DIR = os.path.join(TEST_DIR, 'certs')
33+
CLIENT_PEM_FILE = os.path.join(TEST_CERTS_DIR, 'nopassword.pem')
34+
35+
3136
def decode_frame(frame_data):
3237
f, length = Frame.parse_frame_header(frame_data[:9])
3338
f.parse_body(memoryview(frame_data[9:9 + length]))
@@ -784,6 +789,21 @@ def test_adapter_reuses_connections(self):
784789

785790
assert conn1 is conn2
786791

792+
def test_adapter_accept_client_certificate(self):
793+
a = HTTP20Adapter()
794+
conn1 = a.get_connection(
795+
'http2bin.org',
796+
80,
797+
'http',
798+
cert=CLIENT_PEM_FILE)
799+
conn2 = a.get_connection(
800+
'http2bin.org',
801+
80,
802+
'http',
803+
cert=CLIENT_PEM_FILE)
804+
assert conn1 is conn2
805+
806+
787807

788808
class TestUtilities(object):
789809
def test_combining_repeated_headers(self):

test/test_integration.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import requests
1010
import threading
1111
import time
12-
import socket
1312
import hyper
1413
import hyper.http11.connection
1514
import pytest
@@ -26,7 +25,6 @@
2625
REQUEST_CODES, REQUEST_CODES_LENGTH
2726
)
2827
from hyper.http20.exceptions import ConnectionError, StreamResetError
29-
from hyper.tls import wrap_socket
3028
from server import SocketLevelTest
3129

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

7876

79-
class TestBasicSocketManipulation(SocketLevelTest):
80-
# These aren't HTTP/2 tests, but it doesn't hurt to leave it.
81-
h2 = True
82-
83-
def test_connection_string(self):
84-
self.set_up()
85-
evt = threading.Event()
86-
87-
def socket_handler(listener):
88-
sock = listener.accept()[0]
89-
90-
evt.wait(5)
91-
sock.close()
92-
93-
self._start_server(socket_handler)
94-
s = socket.create_connection((self.host, self.port))
95-
s, proto = wrap_socket(s, "localhost", force_proto=b"test")
96-
s.close()
97-
evt.set()
98-
99-
assert proto == b"test"
100-
101-
self.tear_down()
102-
103-
10477
class TestHyperIntegration(SocketLevelTest):
10578
# These are HTTP/2 tests.
10679
h2 = True

test/test_ssl_socket.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
test/test_ssl_socket
4+
~~~~~~~~~~~~~~~~~~~~
5+
6+
This file defines tests for hyper that validate our TLS handling.
7+
"""
8+
import os
9+
import socket
10+
import ssl
11+
import threading
12+
13+
import pytest
14+
15+
from hyper.tls import wrap_socket, init_context
16+
17+
from server import SocketLevelTest
18+
19+
20+
TEST_DIR = os.path.abspath(os.path.dirname(__file__))
21+
TEST_CERTS_DIR = os.path.join(TEST_DIR, "certs")
22+
CLIENT_CERT_FILE = os.path.join(TEST_CERTS_DIR, 'client.crt')
23+
CLIENT_KEY_FILE = os.path.join(TEST_CERTS_DIR, 'client.key')
24+
CLIENT_PEM_FILE = os.path.join(TEST_CERTS_DIR, 'nopassword.pem')
25+
SERVER_CERT_FILE = os.path.join(TEST_CERTS_DIR, 'server.crt')
26+
SERVER_KEY_FILE = os.path.join(TEST_CERTS_DIR, 'server.key')
27+
28+
29+
class TestBasicSocketManipulation(SocketLevelTest):
30+
# These aren't HTTP/2 tests, but it doesn't hurt to leave it.
31+
h2 = True
32+
33+
def test_connection_string(self):
34+
self.set_up()
35+
evt = threading.Event()
36+
37+
def socket_handler(listener):
38+
sock = listener.accept()[0]
39+
40+
evt.wait(5)
41+
sock.close()
42+
43+
self._start_server(socket_handler)
44+
s = socket.create_connection((self.host, self.port))
45+
s, proto = wrap_socket(s, "localhost", force_proto=b"test")
46+
s.close()
47+
evt.set()
48+
49+
assert proto == b"test"
50+
51+
self.tear_down()
52+
53+
@pytest.mark.parametrize(
54+
'context_kwargs',
55+
[
56+
{'cert': CLIENT_PEM_FILE},
57+
{
58+
'cert': (CLIENT_CERT_FILE, CLIENT_KEY_FILE),
59+
'cert_password': b'abc123'
60+
},
61+
]
62+
)
63+
def test_client_certificate(self, context_kwargs):
64+
# Don't have the server thread do TLS: we'll do it ourselves.
65+
self.set_up(secure=False)
66+
evt = threading.Event()
67+
data = []
68+
69+
def socket_handler(listener):
70+
sock = listener.accept()[0]
71+
sock = ssl.wrap_socket(
72+
sock,
73+
ssl_version=ssl.PROTOCOL_SSLv23,
74+
certfile=SERVER_CERT_FILE,
75+
keyfile=SERVER_KEY_FILE,
76+
cert_reqs=ssl.CERT_REQUIRED,
77+
ca_certs=CLIENT_PEM_FILE,
78+
server_side=True
79+
)
80+
data.append(sock.recv(65535))
81+
evt.wait(5)
82+
sock.close()
83+
84+
self._start_server(socket_handler)
85+
86+
# Set up the client context. Don't validate the server cert though.
87+
context = init_context(**context_kwargs)
88+
context.check_hostname = False
89+
context.verify_mode = ssl.CERT_NONE
90+
91+
s = socket.create_connection((self.host, self.port))
92+
s, proto = wrap_socket(s, "localhost", ssl_context=context)
93+
s.sendall(b'hi')
94+
s.close()
95+
evt.set()
96+
97+
self.tear_down()

0 commit comments

Comments
 (0)