|
13 | 13 | # limitations under the License. |
14 | 14 |
|
15 | 15 | import os |
| 16 | +import ssl |
16 | 17 | import unittest |
| 18 | +from datetime import datetime, timedelta, timezone |
17 | 19 | from types import SimpleNamespace |
18 | 20 |
|
19 | 21 | from unittest.mock import Mock, patch |
20 | 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 |
| 31 | + |
21 | 32 | from tests.unit.io.utils import TimerTestMixin |
22 | 33 | from tests import notpypy, EVENT_LOOP_MANAGER |
23 | 34 |
|
|
36 | 47 | os.path.dirname(__file__), '..', '..', 'integration', 'long', 'ssl', 'rootCa.crt')) |
37 | 48 |
|
38 | 49 |
|
| 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 | + |
39 | 71 | @unittest.skipIf(EventletConnection is None, "eventlet is not available") |
40 | 72 | @unittest.skipIf(not getattr(eventletreactor, '_PYOPENSSL', False), "pyOpenSSL is not available") |
41 | 73 | class EventletSSLContextTest(unittest.TestCase): |
@@ -88,6 +120,48 @@ def test_check_hostname_option_enables_hostname_validation(self): |
88 | 120 |
|
89 | 121 | assert conn._check_hostname |
90 | 122 |
|
| 123 | + def test_validate_hostname_rejects_mismatch(self): |
| 124 | + conn = EventletConnection.__new__(EventletConnection) |
| 125 | + conn.uses_legacy_ssl_options = False |
| 126 | + conn.endpoint = DefaultEndPoint('1.2.3.4') |
| 127 | + conn.ssl_options = {} |
| 128 | + conn._socket = Mock() |
| 129 | + conn._socket.get_peer_certificate.return_value = _make_certificate('wrong.host') |
| 130 | + |
| 131 | + with self.assertRaises(ssl.CertificateError): |
| 132 | + conn._validate_hostname() |
| 133 | + |
| 134 | + def test_validate_hostname_uses_server_hostname(self): |
| 135 | + conn = EventletConnection.__new__(EventletConnection) |
| 136 | + conn.uses_legacy_ssl_options = False |
| 137 | + conn.endpoint = DefaultEndPoint('proxy.host') |
| 138 | + conn.ssl_options = {'server_hostname': 'sni.host'} |
| 139 | + conn._socket = Mock() |
| 140 | + conn._socket.get_peer_certificate.return_value = _make_certificate('proxy.host', ['sni.host']) |
| 141 | + |
| 142 | + conn._validate_hostname() |
| 143 | + |
| 144 | + def test_validate_hostname_prefers_san_over_common_name(self): |
| 145 | + conn = EventletConnection.__new__(EventletConnection) |
| 146 | + conn.uses_legacy_ssl_options = False |
| 147 | + conn.endpoint = DefaultEndPoint('sni.host') |
| 148 | + conn.ssl_options = {} |
| 149 | + conn._socket = Mock() |
| 150 | + conn._socket.get_peer_certificate.return_value = _make_certificate('sni.host', ['other.host']) |
| 151 | + |
| 152 | + with self.assertRaises(ssl.CertificateError): |
| 153 | + conn._validate_hostname() |
| 154 | + |
| 155 | + def test_validate_hostname_matches_wildcard_san(self): |
| 156 | + conn = EventletConnection.__new__(EventletConnection) |
| 157 | + conn.uses_legacy_ssl_options = False |
| 158 | + conn.endpoint = DefaultEndPoint('node.example.com') |
| 159 | + conn.ssl_options = {} |
| 160 | + conn._socket = Mock() |
| 161 | + conn._socket.get_peer_certificate.return_value = _make_certificate('other.host', ['*.example.com']) |
| 162 | + |
| 163 | + conn._validate_hostname() |
| 164 | + |
91 | 165 | def test_wrap_socket_from_context_returns_wrapped_socket(self): |
92 | 166 | conn = EventletConnection.__new__(EventletConnection) |
93 | 167 | conn.ssl_context = object() |
|
0 commit comments