tigrcorn-security 0.3.16.dev5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ """Security helpers."""
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ _ALLOWED_ALPN = {'http/1.1', 'h2', 'h3'}
4
+
5
+
6
+ def normalize_alpn(selected: str | None) -> str | None:
7
+ if selected is None:
8
+ return None
9
+ candidate = selected.strip().lower()
10
+ if not candidate:
11
+ return None
12
+ if candidate in _ALLOWED_ALPN:
13
+ return candidate
14
+ return candidate
15
+
16
+
17
+ def normalize_alpn_list(values: list[str] | tuple[str, ...] | None, *, for_udp: bool = False) -> list[str]:
18
+ items = [normalize_alpn(v) for v in (values or []) if v]
19
+ normalized = [v for v in items if v]
20
+ if not normalized:
21
+ normalized = ['h3'] if for_udp else ['h2', 'http/1.1']
22
+ seen: list[str] = []
23
+ for item in normalized:
24
+ if item not in seen:
25
+ seen.append(item)
26
+ return seen
27
+
28
+
29
+ __all__ = ['normalize_alpn', 'normalize_alpn_list']
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(slots=True)
7
+ class PeerCertificate:
8
+ subject: tuple | None = None
9
+ issuer: tuple | None = None
10
+ serial_number: str | None = None
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import timedelta
5
+
6
+ from tigrcorn_config.model import ListenerConfig
7
+ from tigrcorn_security.x509.path import (
8
+ CertificatePurpose,
9
+ CertificateValidationPolicy,
10
+ RevocationCache,
11
+ RevocationFetchPolicy,
12
+ RevocationFreshnessPolicy,
13
+ RevocationMaterial,
14
+ RevocationMode,
15
+ load_crls_from_file,
16
+ )
17
+
18
+
19
+ @dataclass(slots=True)
20
+ class TLSPolicy:
21
+ require_client_cert: bool = False
22
+
23
+
24
+ def revocation_mode_from_listener(listener: ListenerConfig) -> RevocationMode:
25
+ ocsp_mode = getattr(listener, 'ocsp_mode', 'off') or 'off'
26
+ crl_mode = getattr(listener, 'crl_mode', 'off') or 'off'
27
+ if 'require' in {ocsp_mode, crl_mode}:
28
+ return RevocationMode.REQUIRE
29
+ if (
30
+ 'soft-fail' in {ocsp_mode, crl_mode}
31
+ or (getattr(listener, 'ocsp_soft_fail', False) and ocsp_mode != 'off')
32
+ ):
33
+ return RevocationMode.SOFT_FAIL
34
+ return RevocationMode.OFF
35
+
36
+
37
+ def build_validation_policy_for_listener(listener: ListenerConfig) -> CertificateValidationPolicy:
38
+ ocsp_max_age = getattr(listener, 'ocsp_max_age', None)
39
+ freshness = RevocationFreshnessPolicy(
40
+ ocsp_max_age_without_next_update=timedelta(seconds=ocsp_max_age) if ocsp_max_age is not None else RevocationFreshnessPolicy().ocsp_max_age_without_next_update,
41
+ )
42
+ revocation_fetch_enabled = getattr(listener, 'revocation_fetch', True)
43
+ ocsp_enabled = getattr(listener, 'ocsp_mode', 'off') != 'off'
44
+ crl_enabled = getattr(listener, 'crl_mode', 'off') != 'off'
45
+ fetch_policy = RevocationFetchPolicy(
46
+ enable_ocsp_aia=revocation_fetch_enabled and ocsp_enabled,
47
+ enable_crl_distribution_points=revocation_fetch_enabled and crl_enabled,
48
+ freshness=freshness,
49
+ cache=RevocationCache(max_entries=max(1, int(getattr(listener, 'ocsp_cache_size', 128) or 128))),
50
+ )
51
+ if not revocation_fetch_enabled or (not ocsp_enabled and not crl_enabled):
52
+ fetch_policy = None
53
+ local_crls = ()
54
+ if getattr(listener, 'ssl_crl', None):
55
+ local_crls = load_crls_from_file(str(listener.ssl_crl))
56
+ return CertificateValidationPolicy(
57
+ purpose=CertificatePurpose.CLIENT_AUTH,
58
+ revocation_mode=revocation_mode_from_listener(listener),
59
+ revocation_material=RevocationMaterial(crls=local_crls),
60
+ revocation_fetch_policy=fetch_policy,
61
+ )
62
+
63
+
64
+ __all__ = [
65
+ 'TLSPolicy',
66
+ 'build_validation_policy_for_listener',
67
+ 'revocation_mode_from_listener',
68
+ ]
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,583 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import threading
6
+ from dataclasses import dataclass
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Any, Iterable
10
+
11
+ class _MissingDependencyProxy:
12
+ def __init__(self, package: str) -> None:
13
+ self._package = package
14
+
15
+ def __getattr__(self, name: str):
16
+ raise ModuleNotFoundError(
17
+ f"{self._package} is required for this TLS/X.509 operation; install tigrcorn[tls-x509]"
18
+ )
19
+
20
+
21
+ try:
22
+ from cryptography import x509
23
+ from cryptography.hazmat.primitives import serialization
24
+ except ModuleNotFoundError: # pragma: no cover - exercised in dependency-light environments
25
+ x509 = _MissingDependencyProxy("cryptography") # type: ignore[assignment]
26
+ serialization = _MissingDependencyProxy("cryptography") # type: ignore[assignment]
27
+
28
+ from tigrcorn_config.model import ListenerConfig
29
+ from tigrcorn_core.errors import ProtocolError
30
+ from tigrcorn_security.tls13.handshake import QuicTlsHandshakeDriver, TlsAlertError
31
+ from tigrcorn_security.tls13.key_schedule import Tls13KeySchedule
32
+ from tigrcorn_security.tls13.messages import decode_handshake_message
33
+ from tigrcorn_security.policies import build_validation_policy_for_listener
34
+ from tigrcorn_security.x509.path import (
35
+ CertificatePurpose,
36
+ CertificateValidationPolicy,
37
+ RevocationCache,
38
+ RevocationCacheEntry,
39
+ RevocationFetchPolicy,
40
+ RevocationFreshnessPolicy,
41
+ RevocationMaterial,
42
+ RevocationMode,
43
+ load_pem_certificates,
44
+ verify_certificate_chain as _verify_certificate_chain,
45
+ verify_certificate_hostname,
46
+ verify_certificate_validity,
47
+ )
48
+ from tigrcorn_transports.quic.crypto import aes_gcm_decrypt, aes_gcm_encrypt
49
+
50
+ _TLS_CONTENT_CHANGE_CIPHER_SPEC = 20
51
+ _TLS_CONTENT_ALERT = 21
52
+ _TLS_CONTENT_HANDSHAKE = 22
53
+ _TLS_CONTENT_APPLICATION_DATA = 23
54
+ _TLS_LEGACY_RECORD_VERSION = 0x0303
55
+ _TLS_MAX_PLAINTEXT = 16384
56
+ _TLS_ALERT_LEVEL_FATAL = 2
57
+ _TLS_ALERT_CLOSE_NOTIFY = 0
58
+
59
+ _CIPHER_NAMES = {
60
+ 0x1301: ('TLS_AES_128_GCM_SHA256', 128),
61
+ 0x1302: ('TLS_AES_256_GCM_SHA384', 256),
62
+ }
63
+
64
+
65
+ @dataclass(frozen=True, slots=True)
66
+ class ServerTLSContext:
67
+ certificate_pem: bytes
68
+ private_key_pem: bytes
69
+ private_key_password: bytes | None
70
+ trusted_certificates: tuple[bytes, ...]
71
+ alpn_protocols: tuple[str, ...]
72
+ require_client_certificate: bool
73
+ validation_policy: CertificateValidationPolicy
74
+ cipher_suites: tuple[int, ...] = (0x1302, 0x1301)
75
+ server_name: str = 'localhost'
76
+
77
+
78
+ @dataclass(slots=True)
79
+ class _RecordProtectionState:
80
+ key: bytes
81
+ iv: bytes
82
+ sequence_number: int = 0
83
+
84
+ def next_nonce(self) -> bytes:
85
+ sequence = self.sequence_number.to_bytes(8, 'big')
86
+ padded = b'\x00' * (len(self.iv) - len(sequence)) + sequence
87
+ nonce = bytes(left ^ right for left, right in zip(self.iv, padded))
88
+ self.sequence_number += 1
89
+ return nonce
90
+
91
+
92
+ class PackageOwnedSSLObject:
93
+ def __init__(
94
+ self,
95
+ *,
96
+ selected_alpn_protocol: str | None,
97
+ cipher_suite: int,
98
+ peer_certificate: x509.Certificate | None,
99
+ ) -> None:
100
+ self._selected_alpn_protocol = selected_alpn_protocol
101
+ self._cipher_suite = cipher_suite
102
+ self._peer_certificate = peer_certificate
103
+ self._peer_certificate_der = (
104
+ peer_certificate.public_bytes(serialization.Encoding.DER)
105
+ if peer_certificate is not None
106
+ else None
107
+ )
108
+
109
+ def selected_alpn_protocol(self) -> str | None:
110
+ return self._selected_alpn_protocol
111
+
112
+ def version(self) -> str:
113
+ return 'TLSv1.3'
114
+
115
+ def cipher(self) -> tuple[str, str, int]:
116
+ name, bits = _CIPHER_NAMES.get(self._cipher_suite, ('TLS_UNKNOWN', 0))
117
+ return name, 'TLSv1.3', bits
118
+
119
+ def getpeercert(self, binary_form: bool = False) -> dict[str, Any] | bytes | None:
120
+ if self._peer_certificate is None:
121
+ return None
122
+ if binary_form:
123
+ return self._peer_certificate_der
124
+ return describe_peer_certificate(self._peer_certificate)
125
+
126
+
127
+ class PackageOwnedTLSConnection:
128
+ def __init__(
129
+ self,
130
+ raw_reader: asyncio.StreamReader,
131
+ raw_writer: asyncio.StreamWriter,
132
+ context: ServerTLSContext,
133
+ ) -> None:
134
+ self._raw_reader = raw_reader
135
+ self._raw_writer = raw_writer
136
+ self._context = context
137
+ self._driver = QuicTlsHandshakeDriver(
138
+ is_client=False,
139
+ alpn=context.alpn_protocols,
140
+ server_name=context.server_name,
141
+ certificate_pem=context.certificate_pem,
142
+ private_key_pem=context.private_key_pem,
143
+ private_key_password=context.private_key_password,
144
+ trusted_certificates=context.trusted_certificates,
145
+ require_client_certificate=context.require_client_certificate,
146
+ transport_mode='stream',
147
+ validation_policy=context.validation_policy,
148
+ cipher_suites=context.cipher_suites,
149
+ )
150
+ self._read_lock = asyncio.Lock()
151
+ self._write_lock = threading.Lock()
152
+ self._closed = False
153
+ self._eof = False
154
+ self._plaintext_buffer = bytearray()
155
+ self._handshake_inbound: _RecordProtectionState | None = None
156
+ self._handshake_outbound: _RecordProtectionState | None = None
157
+ self._application_inbound: _RecordProtectionState | None = None
158
+ self._application_outbound: _RecordProtectionState | None = None
159
+ self._ssl_object: PackageOwnedSSLObject | None = None
160
+
161
+ @property
162
+ def ssl_object(self) -> PackageOwnedSSLObject | None:
163
+ return self._ssl_object
164
+
165
+ async def handshake(self) -> None:
166
+ try:
167
+ server_flight = b''
168
+ while not server_flight:
169
+ content_type, payload = await self._read_raw_record()
170
+ if content_type == _TLS_CONTENT_CHANGE_CIPHER_SPEC:
171
+ continue
172
+ if content_type == _TLS_CONTENT_HANDSHAKE:
173
+ server_flight = self._driver.receive(payload)
174
+ continue
175
+ if content_type == _TLS_CONTENT_ALERT:
176
+ self._eof = True
177
+ raise ProtocolError('peer closed the TLS handshake before completion')
178
+ raise ProtocolError('unexpected TLS record before ServerHello')
179
+
180
+ await self._send_server_flight(server_flight)
181
+
182
+ while not self._driver.complete:
183
+ content_type, payload = await self._read_raw_record()
184
+ if content_type == _TLS_CONTENT_CHANGE_CIPHER_SPEC:
185
+ continue
186
+ if content_type == _TLS_CONTENT_HANDSHAKE:
187
+ self._driver.receive(payload)
188
+ continue
189
+ if content_type == _TLS_CONTENT_ALERT:
190
+ self._eof = True
191
+ raise ProtocolError('peer closed the TLS handshake before completion')
192
+ if content_type != _TLS_CONTENT_APPLICATION_DATA:
193
+ raise ProtocolError('unexpected TLS record during encrypted handshake')
194
+ if self._handshake_inbound is None:
195
+ raise ProtocolError('TLS handshake keys are unavailable')
196
+ plaintext, inner_type = _decrypt_record(payload, self._handshake_inbound)
197
+ if inner_type == _TLS_CONTENT_CHANGE_CIPHER_SPEC:
198
+ continue
199
+ if inner_type == _TLS_CONTENT_HANDSHAKE:
200
+ self._driver.receive(plaintext)
201
+ continue
202
+ if inner_type == _TLS_CONTENT_ALERT:
203
+ self._eof = True
204
+ raise ProtocolError('peer sent a fatal TLS alert during the handshake')
205
+ raise ProtocolError('unexpected TLS inner content type during handshake')
206
+
207
+ traffic = self._driver.traffic_secrets
208
+ if traffic is None:
209
+ raise ProtocolError('TLS handshake completed without negotiated traffic secrets')
210
+ parameters = self._driver.cipher_parameters
211
+ self._application_inbound = _build_record_state(
212
+ traffic.client_application_secret,
213
+ key_length=parameters.key_length,
214
+ iv_length=parameters.iv_length,
215
+ hash_name=parameters.hash_name,
216
+ )
217
+ self._application_outbound = _build_record_state(
218
+ traffic.server_application_secret,
219
+ key_length=parameters.key_length,
220
+ iv_length=parameters.iv_length,
221
+ hash_name=parameters.hash_name,
222
+ )
223
+ peer_certificate = None
224
+ if self._driver.peer_certificate_pem is not None:
225
+ peer_certificate = load_pem_certificates((self._driver.peer_certificate_pem,))[0]
226
+ self._ssl_object = PackageOwnedSSLObject(
227
+ selected_alpn_protocol=self._driver.selected_alpn,
228
+ cipher_suite=getattr(self._driver, '_selected_cipher_suite'),
229
+ peer_certificate=peer_certificate,
230
+ )
231
+ except TlsAlertError as exc:
232
+ with contextlib.suppress(Exception):
233
+ await self._send_plain_alert(int(exc.description))
234
+ raise ProtocolError(str(exc)) from exc
235
+
236
+ async def read(self, n: int = -1) -> bytes:
237
+ if n == 0:
238
+ return b''
239
+ async with self._read_lock:
240
+ if n < 0:
241
+ while not self._eof:
242
+ await self._fill_plaintext_buffer()
243
+ data = bytes(self._plaintext_buffer)
244
+ self._plaintext_buffer.clear()
245
+ return data
246
+ while not self._plaintext_buffer and not self._eof:
247
+ await self._fill_plaintext_buffer()
248
+ if not self._plaintext_buffer and self._eof:
249
+ return b''
250
+ take = min(n, len(self._plaintext_buffer))
251
+ data = bytes(self._plaintext_buffer[:take])
252
+ del self._plaintext_buffer[:take]
253
+ return data
254
+
255
+ async def readexactly(self, n: int) -> bytes:
256
+ if n < 0:
257
+ raise ValueError('readexactly size must be non-negative')
258
+ async with self._read_lock:
259
+ while len(self._plaintext_buffer) < n and not self._eof:
260
+ await self._fill_plaintext_buffer()
261
+ if len(self._plaintext_buffer) < n:
262
+ partial = bytes(self._plaintext_buffer)
263
+ self._plaintext_buffer.clear()
264
+ raise asyncio.IncompleteReadError(partial=partial, expected=n)
265
+ data = bytes(self._plaintext_buffer[:n])
266
+ del self._plaintext_buffer[:n]
267
+ return data
268
+
269
+ async def readuntil(self, separator: bytes = b'\n') -> bytes:
270
+ return await self.readuntil_limited(separator, limit=None)
271
+
272
+ async def readuntil_limited(self, separator: bytes = b'\n', *, limit: int | None) -> bytes:
273
+ if not separator:
274
+ raise ValueError('separator must not be empty')
275
+ async with self._read_lock:
276
+ while True:
277
+ index = self._plaintext_buffer.find(separator)
278
+ if index >= 0:
279
+ end = index + len(separator)
280
+ data = bytes(self._plaintext_buffer[:end])
281
+ del self._plaintext_buffer[:end]
282
+ return data
283
+ if limit is not None and len(self._plaintext_buffer) > limit:
284
+ raise asyncio.LimitOverrunError('separator is not found, and chunk exceed the limit', consumed=len(self._plaintext_buffer))
285
+ if self._eof:
286
+ partial = bytes(self._plaintext_buffer)
287
+ self._plaintext_buffer.clear()
288
+ raise asyncio.IncompleteReadError(partial=partial, expected=len(partial) + len(separator))
289
+ await self._fill_plaintext_buffer()
290
+ if limit is not None and len(self._plaintext_buffer) > limit:
291
+ raise asyncio.LimitOverrunError('separator is not found, and chunk exceed the limit', consumed=len(self._plaintext_buffer))
292
+
293
+ def write(self, data: bytes) -> None:
294
+ if self._closed or not data:
295
+ return
296
+ if self._application_outbound is None:
297
+ raise RuntimeError('TLS application keys are not available')
298
+ with self._write_lock:
299
+ offset = 0
300
+ while offset < len(data):
301
+ chunk = data[offset:offset + _TLS_MAX_PLAINTEXT]
302
+ offset += len(chunk)
303
+ record = _encrypt_record(chunk, _TLS_CONTENT_APPLICATION_DATA, self._application_outbound)
304
+ self._raw_writer.write(record)
305
+
306
+ async def drain(self) -> None:
307
+ await self._raw_writer.drain()
308
+
309
+ def close(self) -> None:
310
+ if self._closed:
311
+ return
312
+ self._closed = True
313
+ if self._application_outbound is not None and not self._raw_writer.is_closing():
314
+ with contextlib.suppress(Exception):
315
+ self._raw_writer.write(
316
+ _encrypt_record(
317
+ bytes([1, _TLS_ALERT_CLOSE_NOTIFY]),
318
+ _TLS_CONTENT_ALERT,
319
+ self._application_outbound,
320
+ )
321
+ )
322
+ self._raw_writer.close()
323
+
324
+ async def wait_closed(self) -> None:
325
+ await self._raw_writer.wait_closed()
326
+
327
+ def is_closing(self) -> bool:
328
+ return self._closed or self._raw_writer.is_closing()
329
+
330
+ def can_write_eof(self) -> bool:
331
+ return False
332
+
333
+ def write_eof(self) -> None:
334
+ self.close()
335
+
336
+ def get_extra_info(self, name: str, default: Any = None) -> Any:
337
+ if name == 'ssl_object':
338
+ return self._ssl_object
339
+ if name == 'sslcontext':
340
+ return self._context
341
+ if name == 'peercert' and self._ssl_object is not None:
342
+ return self._ssl_object.getpeercert(binary_form=False)
343
+ if name == 'cipher' and self._ssl_object is not None:
344
+ return self._ssl_object.cipher()
345
+ if name == 'tls.negotiated_alpn':
346
+ return None if self._ssl_object is None else self._ssl_object.selected_alpn_protocol()
347
+ return self._raw_writer.get_extra_info(name, default)
348
+
349
+ async def _fill_plaintext_buffer(self) -> None:
350
+ content_type, payload = await self._read_raw_record()
351
+ if content_type == _TLS_CONTENT_CHANGE_CIPHER_SPEC:
352
+ return
353
+ if content_type == _TLS_CONTENT_ALERT:
354
+ self._eof = True
355
+ return
356
+ if content_type != _TLS_CONTENT_APPLICATION_DATA:
357
+ raise ProtocolError('unexpected TLS record after the handshake completed')
358
+ if self._application_inbound is None:
359
+ raise ProtocolError('TLS application keys are not available')
360
+ plaintext, inner_type = _decrypt_record(payload, self._application_inbound)
361
+ if inner_type == _TLS_CONTENT_APPLICATION_DATA:
362
+ if plaintext:
363
+ self._plaintext_buffer.extend(plaintext)
364
+ return
365
+ if inner_type == _TLS_CONTENT_ALERT:
366
+ self._eof = True
367
+ return
368
+ if inner_type == _TLS_CONTENT_CHANGE_CIPHER_SPEC:
369
+ return
370
+ raise ProtocolError('unexpected TLS inner content type after the handshake completed')
371
+
372
+ async def _send_server_flight(self, flight: bytes) -> None:
373
+ _message, offset = decode_handshake_message(flight, 0)
374
+ server_hello = flight[:offset]
375
+ encrypted_handshake = flight[offset:]
376
+ self._raw_writer.write(_encode_plain_record(_TLS_CONTENT_HANDSHAKE, server_hello))
377
+ self._raw_writer.write(_encode_plain_record(_TLS_CONTENT_CHANGE_CIPHER_SPEC, b'\x01'))
378
+ traffic = self._driver.traffic_secrets
379
+ if traffic is None:
380
+ raise ProtocolError('TLS handshake traffic secrets were not negotiated')
381
+ parameters = self._driver.cipher_parameters
382
+ self._handshake_inbound = _build_record_state(
383
+ traffic.client_handshake_secret,
384
+ key_length=parameters.key_length,
385
+ iv_length=parameters.iv_length,
386
+ hash_name=parameters.hash_name,
387
+ )
388
+ self._handshake_outbound = _build_record_state(
389
+ traffic.server_handshake_secret,
390
+ key_length=parameters.key_length,
391
+ iv_length=parameters.iv_length,
392
+ hash_name=parameters.hash_name,
393
+ )
394
+ if encrypted_handshake:
395
+ self._raw_writer.write(_encrypt_record(encrypted_handshake, _TLS_CONTENT_HANDSHAKE, self._handshake_outbound))
396
+ await self._raw_writer.drain()
397
+
398
+ async def _read_raw_record(self) -> tuple[int, bytes]:
399
+ try:
400
+ header = await self._raw_reader.readexactly(5)
401
+ except asyncio.IncompleteReadError:
402
+ self._eof = True
403
+ return _TLS_CONTENT_ALERT, b''
404
+ content_type = header[0]
405
+ length = int.from_bytes(header[3:5], 'big')
406
+ try:
407
+ payload = await self._raw_reader.readexactly(length)
408
+ except asyncio.IncompleteReadError as exc:
409
+ raise ProtocolError('truncated TLS record') from exc
410
+ return content_type, payload
411
+
412
+ async def _send_plain_alert(self, description: int) -> None:
413
+ self._raw_writer.write(
414
+ _encode_plain_record(_TLS_CONTENT_ALERT, bytes([_TLS_ALERT_LEVEL_FATAL, description]))
415
+ )
416
+ await self._raw_writer.drain()
417
+
418
+
419
+ def _build_record_state(secret: bytes, *, key_length: int, iv_length: int, hash_name: str) -> _RecordProtectionState:
420
+ schedule = Tls13KeySchedule(hash_name=hash_name)
421
+ return _RecordProtectionState(
422
+ key=schedule.expand_label(secret, 'key', b'', key_length),
423
+ iv=schedule.expand_label(secret, 'iv', b'', iv_length),
424
+ )
425
+
426
+
427
+ def _encode_plain_record(content_type: int, payload: bytes) -> bytes:
428
+ return bytes([content_type]) + _TLS_LEGACY_RECORD_VERSION.to_bytes(2, 'big') + len(payload).to_bytes(2, 'big') + payload
429
+
430
+
431
+ def _encrypt_record(payload: bytes, inner_content_type: int, state: _RecordProtectionState) -> bytes:
432
+ inner = payload + bytes([inner_content_type])
433
+ nonce = state.next_nonce()
434
+ body_length = len(inner) + 16
435
+ header = (
436
+ bytes([_TLS_CONTENT_APPLICATION_DATA])
437
+ + _TLS_LEGACY_RECORD_VERSION.to_bytes(2, 'big')
438
+ + body_length.to_bytes(2, 'big')
439
+ )
440
+ ciphertext, tag = aes_gcm_encrypt(state.key, nonce, inner, aad=header)
441
+ return header + ciphertext + tag
442
+
443
+
444
+ def _decrypt_record(payload: bytes, state: _RecordProtectionState) -> tuple[bytes, int]:
445
+ if len(payload) < 16:
446
+ raise ProtocolError('truncated TLS application-data record')
447
+ header = (
448
+ bytes([_TLS_CONTENT_APPLICATION_DATA])
449
+ + _TLS_LEGACY_RECORD_VERSION.to_bytes(2, 'big')
450
+ + len(payload).to_bytes(2, 'big')
451
+ )
452
+ ciphertext = payload[:-16]
453
+ tag = payload[-16:]
454
+ nonce = state.next_nonce()
455
+ plaintext = aes_gcm_decrypt(state.key, nonce, ciphertext, tag, aad=header)
456
+ index = len(plaintext) - 1
457
+ while index >= 0 and plaintext[index] == 0:
458
+ index -= 1
459
+ if index < 0:
460
+ raise ProtocolError('TLS inner plaintext is missing a content type')
461
+ return plaintext[:index], plaintext[index]
462
+
463
+
464
+ def describe_peer_certificate(certificate: x509.Certificate) -> dict[str, Any]:
465
+ return {
466
+ 'subject': certificate.subject.rfc4514_string(),
467
+ 'issuer': certificate.issuer.rfc4514_string(),
468
+ 'serial_number': hex(certificate.serial_number),
469
+ 'not_valid_before': _iso_utc(
470
+ certificate.not_valid_before_utc if hasattr(certificate, 'not_valid_before_utc') else certificate.not_valid_before
471
+ ),
472
+ 'not_valid_after': _iso_utc(
473
+ certificate.not_valid_after_utc if hasattr(certificate, 'not_valid_after_utc') else certificate.not_valid_after
474
+ ),
475
+ }
476
+
477
+
478
+ def tls_extension_payload(writer: Any) -> dict[str, Any] | None:
479
+ ssl_object = getattr(writer, 'get_extra_info', lambda *args, **kwargs: None)('ssl_object')
480
+ if ssl_object is None:
481
+ return None
482
+ payload: dict[str, Any] = {}
483
+ selected_alpn = getattr(ssl_object, 'selected_alpn_protocol', lambda: None)()
484
+ if selected_alpn is not None:
485
+ payload['selected_alpn_protocol'] = selected_alpn
486
+ getpeercert = getattr(ssl_object, 'getpeercert', None)
487
+ if callable(getpeercert):
488
+ peer_cert = getpeercert(binary_form=False)
489
+ if peer_cert is not None:
490
+ payload['peer_cert'] = peer_cert
491
+ return payload or None
492
+
493
+
494
+ def build_server_ssl_context(listener: ListenerConfig) -> ServerTLSContext | None:
495
+ if not listener.ssl_enabled:
496
+ return None
497
+ assert listener.ssl_certfile is not None
498
+ assert listener.ssl_keyfile is not None
499
+ certificate_pem = Path(listener.ssl_certfile).read_bytes()
500
+ private_key_pem = Path(listener.ssl_keyfile).read_bytes()
501
+ private_key_password = getattr(listener, 'ssl_keyfile_password', None)
502
+ if private_key_password is not None and not isinstance(private_key_password, bytes):
503
+ private_key_password = str(private_key_password).encode('utf-8')
504
+ trusted = (Path(listener.ssl_ca_certs).read_bytes(),) if listener.ssl_ca_certs else ()
505
+ validation_policy = build_validation_policy_for_listener(listener)
506
+ server_name = _listener_server_name(listener)
507
+ return ServerTLSContext(
508
+ certificate_pem=certificate_pem,
509
+ private_key_pem=private_key_pem,
510
+ private_key_password=private_key_password,
511
+ trusted_certificates=trusted,
512
+ alpn_protocols=tuple(listener.alpn_protocols),
513
+ require_client_certificate=listener.ssl_require_client_cert,
514
+ validation_policy=validation_policy,
515
+ cipher_suites=tuple(int(item) for item in (getattr(listener, 'resolved_cipher_suites', ()) or (0x1302, 0x1301))),
516
+ server_name=server_name,
517
+ )
518
+
519
+
520
+ async def wrap_server_tls_connection(
521
+ raw_reader: asyncio.StreamReader,
522
+ raw_writer: asyncio.StreamWriter,
523
+ context: ServerTLSContext,
524
+ ) -> PackageOwnedTLSConnection:
525
+ connection = PackageOwnedTLSConnection(raw_reader, raw_writer, context)
526
+ await connection.handshake()
527
+ return connection
528
+
529
+
530
+ build_server_tls_context = build_server_ssl_context
531
+
532
+
533
+ def verify_certificate_chain(
534
+ chain_pems: Iterable[bytes],
535
+ trust_roots_pems: Iterable[bytes],
536
+ *,
537
+ server_name: str = '',
538
+ moment: datetime | None = None,
539
+ policy: CertificateValidationPolicy | None = None,
540
+ ) -> x509.Certificate:
541
+ return _verify_certificate_chain(
542
+ chain_pems,
543
+ trust_roots_pems,
544
+ server_name=server_name,
545
+ moment=moment,
546
+ policy=policy,
547
+ )
548
+
549
+
550
+ def _listener_server_name(listener: ListenerConfig) -> str:
551
+ host = listener.host or 'localhost'
552
+ if host in {'0.0.0.0', '::', ''}:
553
+ return 'localhost'
554
+ return host
555
+
556
+
557
+ def _iso_utc(moment: datetime) -> str:
558
+ if moment.tzinfo is None:
559
+ moment = moment.replace(tzinfo=timezone.utc)
560
+ return moment.astimezone(timezone.utc).isoformat().replace('+00:00', 'Z')
561
+
562
+
563
+ __all__ = [
564
+ 'PackageOwnedSSLObject',
565
+ 'PackageOwnedTLSConnection',
566
+ 'ServerTLSContext',
567
+ 'build_server_ssl_context',
568
+ 'build_server_tls_context',
569
+ 'wrap_server_tls_connection',
570
+ 'tls_extension_payload',
571
+ 'CertificatePurpose',
572
+ 'CertificateValidationPolicy',
573
+ 'RevocationCache',
574
+ 'RevocationCacheEntry',
575
+ 'RevocationFetchPolicy',
576
+ 'RevocationFreshnessPolicy',
577
+ 'RevocationMaterial',
578
+ 'RevocationMode',
579
+ 'load_pem_certificates',
580
+ 'verify_certificate_validity',
581
+ 'verify_certificate_hostname',
582
+ 'verify_certificate_chain',
583
+ ]