Skip to content

Commit 9bcedb1

Browse files
committed
ssl: move pyopenssl hostname validation to follow-up
1 parent f8a83b8 commit 9bcedb1

5 files changed

Lines changed: 10 additions & 251 deletions

File tree

cassandra/connection.py

Lines changed: 0 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from functools import wraps, partial, total_ordering
1919
from heapq import heappush, heappop
2020
import io
21-
import ipaddress
2221
import logging
2322
import socket
2423
import struct
@@ -57,103 +56,6 @@
5756
log = logging.getLogger(__name__)
5857

5958

60-
def _pyopenssl_cert_to_ssl_cert(peer_cert):
61-
from cryptography import x509
62-
from cryptography.x509.oid import NameOID
63-
64-
cert = peer_cert.to_cryptography()
65-
ssl_cert = {}
66-
67-
subject = tuple((('commonName', attr.value),)
68-
for attr in cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME))
69-
if subject:
70-
ssl_cert['subject'] = subject
71-
72-
subject_alt_names = []
73-
try:
74-
san = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value
75-
except x509.ExtensionNotFound:
76-
pass
77-
else:
78-
subject_alt_names.extend(('DNS', name) for name in san.get_values_for_type(x509.DNSName))
79-
subject_alt_names.extend(('IP Address', str(address))
80-
for address in san.get_values_for_type(x509.IPAddress))
81-
82-
if subject_alt_names:
83-
ssl_cert['subjectAltName'] = tuple(subject_alt_names)
84-
85-
return ssl_cert
86-
87-
88-
def _validate_pyopenssl_hostname(peer_cert, hostname):
89-
cert = _pyopenssl_cert_to_ssl_cert(peer_cert)
90-
try:
91-
host_ip = ipaddress.ip_address(hostname)
92-
except ValueError:
93-
host_ip = None
94-
95-
names = []
96-
for key, value in cert.get('subjectAltName', ()):
97-
if key == 'DNS':
98-
if host_ip is None and _dnsname_match(value, hostname):
99-
return
100-
names.append(value)
101-
elif key == 'IP Address':
102-
if host_ip is not None and _ipaddress_match(value, host_ip):
103-
return
104-
names.append(value)
105-
106-
if not names:
107-
for subject in cert.get('subject', ()):
108-
for key, value in subject:
109-
if key == 'commonName':
110-
if _dnsname_match(value, hostname):
111-
return
112-
names.append(value)
113-
114-
if len(names) > 1:
115-
raise ssl.CertificateError("hostname {!r} doesn't match either of {}".format(
116-
hostname, ', '.join(map(repr, names))))
117-
if len(names) == 1:
118-
raise ssl.CertificateError("hostname {!r} doesn't match {!r}".format(hostname, names[0]))
119-
raise ssl.CertificateError("no appropriate commonName or subjectAltName fields were found")
120-
121-
122-
def _dnsname_match(pattern, hostname):
123-
if not pattern:
124-
return False
125-
126-
wildcards = pattern.count('*')
127-
if not wildcards:
128-
return pattern.lower() == hostname.lower()
129-
if wildcards > 1:
130-
raise ssl.CertificateError(
131-
"too many wildcards in certificate DNS name: {!r}.".format(pattern))
132-
133-
leftmost, sep, remainder = pattern.partition('.')
134-
if '*' in remainder:
135-
raise ssl.CertificateError(
136-
"wildcard can only be present in the leftmost label: {!r}.".format(pattern))
137-
if not sep:
138-
raise ssl.CertificateError(
139-
"sole wildcard without additional labels is not supported: {!r}.".format(pattern))
140-
if leftmost != '*':
141-
raise ssl.CertificateError(
142-
"partial wildcards in leftmost label are not supported: {!r}.".format(pattern))
143-
144-
hostname_leftmost, sep, hostname_remainder = hostname.partition('.')
145-
if not hostname_leftmost or not sep:
146-
return False
147-
return remainder.lower() == hostname_remainder.lower()
148-
149-
150-
def _ipaddress_match(pattern, host_ip):
151-
try:
152-
return ipaddress.ip_address(pattern) == host_ip
153-
except ValueError:
154-
return False
155-
156-
15759
segment_codec_no_compression = SegmentCodec()
15860
segment_codec_lz4 = None
15961

cassandra/io/eventletreactor.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@
2525

2626
from cassandra.connection import (Connection, ConnectionShutdown, Timer, TimerManager,
2727
_build_pyopenssl_context_from_options as _build_pyopenssl_context,
28-
_default_pyopenssl_ssl_method,
29-
_validate_pyopenssl_hostname)
28+
_default_pyopenssl_ssl_method)
3029
try:
3130
from eventlet.green.OpenSSL import SSL
3231
_PYOPENSSL = True
@@ -133,8 +132,10 @@ def _validate_hostname(self):
133132
if self.uses_legacy_ssl_options:
134133
super(EventletConnection, self)._validate_hostname()
135134
else:
136-
expected_name = (self.ssl_options or {}).get('server_hostname') or self.endpoint.address
137-
_validate_pyopenssl_hostname(self._socket.get_peer_certificate(), expected_name)
135+
cert_name = self._socket.get_peer_certificate().get_subject().commonName
136+
if cert_name != self.endpoint.address:
137+
raise Exception("Hostname verification failed! Certificate name '{}' "
138+
"doesn't endpoint '{}'".format(cert_name, self.endpoint.address))
138139

139140
def _build_ssl_context_from_options(self):
140141
_check_pyopenssl()

cassandra/io/twistedreactor.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@
3030

3131
from cassandra.connection import (Connection, ConnectionShutdown, Timer, TimerManager, ConnectionException,
3232
_build_pyopenssl_context_from_options as _build_pyopenssl_context,
33-
_default_pyopenssl_ssl_method,
34-
_validate_pyopenssl_hostname)
33+
_default_pyopenssl_ssl_method)
3534

3635
try:
3736
from OpenSSL import SSL
@@ -173,17 +172,11 @@ def __init__(self, endpoint, ssl_context, ssl_options, check_hostname, timeout):
173172
def verify_callback(self, connection, x509, errnum, errdepth, ok):
174173
return ok
175174

176-
def _hostname_to_verify(self):
177-
return self.ssl_options.get("server_hostname") or self.endpoint.address
178-
179175
def info_callback(self, connection, where, ret):
180176
if where & SSL.SSL_CB_HANDSHAKE_DONE:
181-
if self.check_hostname:
182-
try:
183-
_validate_pyopenssl_hostname(connection.get_peer_certificate(), self._hostname_to_verify())
184-
except Exception as exc:
185-
transport = connection.get_app_data()
186-
transport.failVerification(Failure(ConnectionException(str(exc), self.endpoint)))
177+
if self.check_hostname and self.endpoint.address != connection.get_peer_certificate().get_subject().commonName:
178+
transport = connection.get_app_data()
179+
transport.failVerification(Failure(ConnectionException("Hostname verification failed", self.endpoint)))
187180

188181
def clientConnectionForTLS(self, tlsProtocol):
189182
connection = SSL.Connection(self.context, None)

tests/unit/io/test_eventletreactor.py

Lines changed: 1 addition & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,9 @@
1515
import os
1616
import ssl
1717
import unittest
18-
from datetime import datetime, timedelta, timezone
1918
from types import SimpleNamespace
2019

21-
from unittest.mock import Mock, patch
22-
23-
try:
24-
from cryptography import x509
25-
from cryptography.hazmat.primitives import hashes
26-
from cryptography.hazmat.primitives.asymmetric import rsa
27-
from cryptography.x509.oid import NameOID
28-
from OpenSSL import crypto
29-
except ImportError:
30-
crypto = None
20+
from unittest.mock import patch
3121

3222
from tests.unit.io.utils import TimerTestMixin
3323
from tests import notpypy, EVENT_LOOP_MANAGER
@@ -47,27 +37,6 @@
4737
os.path.dirname(__file__), '..', '..', 'integration', 'long', 'ssl', 'rootCa.crt'))
4838

4939

50-
def _make_certificate(common_name, san_dns_names=None):
51-
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
52-
subject = issuer = x509.Name([
53-
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
54-
])
55-
now = datetime.now(timezone.utc)
56-
builder = (x509.CertificateBuilder()
57-
.subject_name(subject)
58-
.issuer_name(issuer)
59-
.public_key(key.public_key())
60-
.serial_number(x509.random_serial_number())
61-
.not_valid_before(now - timedelta(days=1))
62-
.not_valid_after(now + timedelta(days=1)))
63-
if san_dns_names:
64-
builder = builder.add_extension(
65-
x509.SubjectAlternativeName([x509.DNSName(name) for name in san_dns_names]),
66-
critical=False)
67-
68-
return crypto.X509.from_cryptography(builder.sign(key, hashes.SHA256()))
69-
70-
7140
@unittest.skipIf(EventletConnection is None, "eventlet is not available")
7241
@unittest.skipIf(not getattr(eventletreactor, '_PYOPENSSL', False), "pyOpenSSL is not available")
7342
class EventletSSLContextTest(unittest.TestCase):
@@ -138,48 +107,6 @@ def test_check_hostname_option_enables_hostname_validation(self):
138107

139108
assert conn._check_hostname
140109

141-
def test_validate_hostname_rejects_mismatch(self):
142-
conn = EventletConnection.__new__(EventletConnection)
143-
conn.uses_legacy_ssl_options = False
144-
conn.endpoint = DefaultEndPoint('1.2.3.4')
145-
conn.ssl_options = {}
146-
conn._socket = Mock()
147-
conn._socket.get_peer_certificate.return_value = _make_certificate('wrong.host')
148-
149-
with self.assertRaises(ssl.CertificateError):
150-
conn._validate_hostname()
151-
152-
def test_validate_hostname_uses_server_hostname(self):
153-
conn = EventletConnection.__new__(EventletConnection)
154-
conn.uses_legacy_ssl_options = False
155-
conn.endpoint = DefaultEndPoint('proxy.host')
156-
conn.ssl_options = {'server_hostname': 'sni.host'}
157-
conn._socket = Mock()
158-
conn._socket.get_peer_certificate.return_value = _make_certificate('proxy.host', ['sni.host'])
159-
160-
conn._validate_hostname()
161-
162-
def test_validate_hostname_prefers_san_over_common_name(self):
163-
conn = EventletConnection.__new__(EventletConnection)
164-
conn.uses_legacy_ssl_options = False
165-
conn.endpoint = DefaultEndPoint('sni.host')
166-
conn.ssl_options = {}
167-
conn._socket = Mock()
168-
conn._socket.get_peer_certificate.return_value = _make_certificate('sni.host', ['other.host'])
169-
170-
with self.assertRaises(ssl.CertificateError):
171-
conn._validate_hostname()
172-
173-
def test_validate_hostname_matches_wildcard_san(self):
174-
conn = EventletConnection.__new__(EventletConnection)
175-
conn.uses_legacy_ssl_options = False
176-
conn.endpoint = DefaultEndPoint('node.example.com')
177-
conn.ssl_options = {}
178-
conn._socket = Mock()
179-
conn._socket.get_peer_certificate.return_value = _make_certificate('other.host', ['*.example.com'])
180-
181-
conn._validate_hostname()
182-
183110
def test_wrap_socket_from_context_returns_wrapped_socket(self):
184111
conn = EventletConnection.__new__(EventletConnection)
185112
conn.ssl_context = object()

tests/unit/io/test_twistedreactor.py

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,9 @@
1515
import os
1616
import ssl
1717
import unittest
18-
from datetime import datetime, timedelta, timezone
1918
from types import SimpleNamespace
2019
from unittest.mock import Mock, patch
2120

22-
try:
23-
from cryptography import x509
24-
from cryptography.hazmat.primitives import hashes
25-
from cryptography.hazmat.primitives.asymmetric import rsa
26-
from cryptography.x509.oid import NameOID
27-
from OpenSSL import crypto
28-
except ImportError:
29-
crypto = None
30-
3121
from cassandra.connection import Connection, DefaultEndPoint
3222

3323
try:
@@ -46,27 +36,6 @@
4636
os.path.dirname(__file__), '..', '..', 'integration', 'long', 'ssl', 'rootCa.crt'))
4737

4838

49-
def _make_certificate(common_name, san_dns_names=None):
50-
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
51-
subject = issuer = x509.Name([
52-
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
53-
])
54-
now = datetime.now(timezone.utc)
55-
builder = (x509.CertificateBuilder()
56-
.subject_name(subject)
57-
.issuer_name(issuer)
58-
.public_key(key.public_key())
59-
.serial_number(x509.random_serial_number())
60-
.not_valid_before(now - timedelta(days=1))
61-
.not_valid_after(now + timedelta(days=1)))
62-
if san_dns_names:
63-
builder = builder.add_extension(
64-
x509.SubjectAlternativeName([x509.DNSName(name) for name in san_dns_names]),
65-
critical=False)
66-
67-
return crypto.X509.from_cryptography(builder.sign(key, hashes.SHA256()))
68-
69-
7039
@unittest.skipIf(TwistedConnection is None, "Twisted libraries are not available")
7140
@unittest.skipIf(not getattr(twistedreactor, '_HAS_SSL', False), "pyOpenSSL is not available")
7241
class TwistedSSLContextTest(unittest.TestCase):
@@ -127,39 +96,6 @@ def test_check_hostname_option_enables_hostname_validation(self):
12796

12897
assert conn._check_hostname
12998

130-
def test_hostname_verification_uses_server_hostname(self):
131-
context = Mock()
132-
creator = twistedreactor._SSLCreator(DefaultEndPoint('proxy.host'), context,
133-
{'server_hostname': 'sni.host'}, True, None)
134-
connection = Mock()
135-
connection.get_peer_certificate.return_value = _make_certificate('proxy.host', ['sni.host'])
136-
137-
creator.info_callback(connection, twistedreactor.SSL.SSL_CB_HANDSHAKE_DONE, None)
138-
139-
connection.get_app_data.assert_not_called()
140-
141-
def test_hostname_verification_prefers_san_over_common_name(self):
142-
context = Mock()
143-
creator = twistedreactor._SSLCreator(DefaultEndPoint('sni.host'), context, {}, True, None)
144-
connection = Mock()
145-
transport = Mock()
146-
connection.get_app_data.return_value = transport
147-
connection.get_peer_certificate.return_value = _make_certificate('sni.host', ['other.host'])
148-
149-
creator.info_callback(connection, twistedreactor.SSL.SSL_CB_HANDSHAKE_DONE, None)
150-
151-
transport.failVerification.assert_called_once()
152-
153-
def test_hostname_verification_matches_wildcard_san(self):
154-
context = Mock()
155-
creator = twistedreactor._SSLCreator(DefaultEndPoint('node.example.com'), context, {}, True, None)
156-
connection = Mock()
157-
connection.get_peer_certificate.return_value = _make_certificate('other.host', ['*.example.com'])
158-
159-
creator.info_callback(connection, twistedreactor.SSL.SSL_CB_HANDSHAKE_DONE, None)
160-
161-
connection.get_app_data.assert_not_called()
162-
16399

164100
class TestTwistedTimer(TimerTestMixin, unittest.TestCase):
165101
"""

0 commit comments

Comments
 (0)