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,1411 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import hashlib
5
+ import hmac
6
+ import json
7
+ import os
8
+ import time
9
+ from dataclasses import dataclass
10
+ from datetime import datetime, timedelta, timezone
11
+ from typing import Iterable, Sequence
12
+
13
+ class _MissingDependencyProxy:
14
+ def __init__(self, package: str) -> None:
15
+ self._package = package
16
+
17
+ def __getattr__(self, name: str):
18
+ raise ModuleNotFoundError(
19
+ f"{self._package} is required for this TLS 1.3 certificate operation; install tigrcorn[tls-x509]"
20
+ )
21
+
22
+
23
+ try:
24
+ from cryptography import x509
25
+ from cryptography.hazmat.primitives import hashes, serialization
26
+ from cryptography.hazmat.primitives.asymmetric import ec, ed25519, rsa, x25519, padding as asym_padding
27
+ from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
28
+ except ModuleNotFoundError: # pragma: no cover - exercised in dependency-light environments
29
+ x509 = _MissingDependencyProxy("cryptography") # type: ignore[assignment]
30
+ hashes = _MissingDependencyProxy("cryptography") # type: ignore[assignment]
31
+ serialization = _MissingDependencyProxy("cryptography") # type: ignore[assignment]
32
+ ec = _MissingDependencyProxy("cryptography") # type: ignore[assignment]
33
+ ed25519 = _MissingDependencyProxy("cryptography") # type: ignore[assignment]
34
+ rsa = _MissingDependencyProxy("cryptography") # type: ignore[assignment]
35
+ x25519 = _MissingDependencyProxy("cryptography") # type: ignore[assignment]
36
+ asym_padding = _MissingDependencyProxy("cryptography") # type: ignore[assignment]
37
+ ExtendedKeyUsageOID = _MissingDependencyProxy("cryptography") # type: ignore[assignment]
38
+ NameOID = _MissingDependencyProxy("cryptography") # type: ignore[assignment]
39
+
40
+ from tigrcorn_core.errors import ProtocolError
41
+ from tigrcorn_security.x509.path import (
42
+ CertificatePurpose,
43
+ CertificateValidationPolicy,
44
+ load_pem_certificates,
45
+ verify_certificate_chain,
46
+ )
47
+ from tigrcorn_security.tls13.extensions import (
48
+ CIPHER_TLS_AES_128_GCM_SHA256,
49
+ CIPHER_TLS_AES_256_GCM_SHA384,
50
+ GROUP_SECP256R1,
51
+ GROUP_X25519,
52
+ PSK_MODE_DHE_KE,
53
+ QUIC_EARLY_DATA_SENTINEL,
54
+ SIG_ECDSA_SECP256R1_SHA256,
55
+ SIG_ED25519,
56
+ SIG_RSA_PSS_PSS_SHA256,
57
+ SIG_RSA_PSS_RSAE_SHA256,
58
+ SUPPORTED_CERTIFICATE_SIGNATURE_SCHEMES,
59
+ SUPPORTED_CIPHER_SUITES,
60
+ SUPPORTED_GROUPS,
61
+ SUPPORTED_SIGNATURE_SCHEMES,
62
+ CipherSuiteParameters,
63
+ ExtensionType,
64
+ OfferedPsks,
65
+ PskIdentity,
66
+ TlsExtension,
67
+ TransportParameters,
68
+ cipher_suite_parameters,
69
+ extension_dict,
70
+ encode_pre_shared_key_client_without_binders,
71
+ )
72
+ from tigrcorn_security.tls13.key_schedule import Tls13KeySchedule
73
+ from tigrcorn_security.tls13.messages import (
74
+ HELLO_RETRY_REQUEST_RANDOM,
75
+ Certificate,
76
+ CertificateEntry,
77
+ CertificateRequest,
78
+ CertificateVerify,
79
+ ClientHello,
80
+ EncryptedExtensions,
81
+ Finished,
82
+ HandshakeMessage,
83
+ KeyUpdate,
84
+ NeedMoreData,
85
+ NewSessionTicket,
86
+ ServerHello,
87
+ decode_handshake_message,
88
+ )
89
+ from tigrcorn_security.tls13.transcript import HandshakeTranscript
90
+ from tigrcorn_transports.quic.tls_adapter import split_handshake_flights
91
+
92
+ _SERVER_CERT_VERIFY_CONTEXT = b'TLS 1.3, server CertificateVerify'
93
+ _CLIENT_CERT_VERIFY_CONTEXT = b'TLS 1.3, client CertificateVerify'
94
+ _QUIC_TLS_ALERT_BASE = 0x0100
95
+ _QUIC_TRANSPORT_ERROR_PROTOCOL_VIOLATION = 0x0A
96
+ _MAX_TICKET_LIFETIME_SECONDS = 7 * 24 * 60 * 60
97
+ _MAX_AGE_SKEW_MS = 10_000
98
+
99
+
100
+ class AlertDescription:
101
+ UNEXPECTED_MESSAGE = 10
102
+ HANDSHAKE_FAILURE = 40
103
+ BAD_CERTIFICATE = 42
104
+ UNSUPPORTED_CERTIFICATE = 43
105
+ CERTIFICATE_EXPIRED = 45
106
+ CERTIFICATE_UNKNOWN = 46
107
+ ILLEGAL_PARAMETER = 47
108
+ UNKNOWN_CA = 48
109
+ DECODE_ERROR = 50
110
+ DECRYPT_ERROR = 51
111
+ PROTOCOL_VERSION = 70
112
+ INTERNAL_ERROR = 80
113
+ MISSING_EXTENSION = 109
114
+ CERTIFICATE_REQUIRED = 116
115
+
116
+
117
+ class TlsAlertError(ProtocolError):
118
+ def __init__(self, description: int, message: str) -> None:
119
+ super().__init__(message)
120
+ self.description = description
121
+ self.quic_error_code = _QUIC_TLS_ALERT_BASE + description
122
+
123
+
124
+ class QuicTransportError(ProtocolError):
125
+ def __init__(self, error_code: int, message: str) -> None:
126
+ super().__init__(message)
127
+ self.quic_error_code = error_code
128
+
129
+
130
+ @dataclass(slots=True)
131
+ class HandshakeFlight:
132
+ packet_space: str
133
+ data: bytes
134
+
135
+
136
+ @dataclass(slots=True)
137
+ class QuicTrafficSecrets:
138
+ client_handshake_secret: bytes
139
+ server_handshake_secret: bytes
140
+ client_application_secret: bytes
141
+ server_application_secret: bytes
142
+ client_early_secret: bytes | None = None
143
+ exporter_master_secret: bytes | None = None
144
+ resumption_master_secret: bytes | None = None
145
+
146
+
147
+ @dataclass(slots=True)
148
+ class QuicSessionTicket:
149
+ ticket: bytes
150
+ resumption_secret: bytes
151
+ server_name: str
152
+ alpn: str
153
+ transport_parameters: TransportParameters
154
+ ticket_age_add: int
155
+ ticket_nonce: bytes
156
+ ticket_lifetime: int
157
+ issued_at: int
158
+ cipher_suite: int = CIPHER_TLS_AES_128_GCM_SHA256
159
+ max_early_data_size: int = 0
160
+
161
+ def serialize(self) -> bytes:
162
+ payload = {
163
+ 'ticket': _b64(self.ticket),
164
+ 'resumption_secret': _b64(self.resumption_secret),
165
+ 'server_name': self.server_name,
166
+ 'alpn': self.alpn,
167
+ 'transport_parameters': _b64(self.transport_parameters.to_bytes()),
168
+ 'ticket_age_add': self.ticket_age_add,
169
+ 'ticket_nonce': _b64(self.ticket_nonce),
170
+ 'ticket_lifetime': self.ticket_lifetime,
171
+ 'issued_at': self.issued_at,
172
+ 'cipher_suite': self.cipher_suite,
173
+ 'max_early_data_size': self.max_early_data_size,
174
+ }
175
+ return json.dumps(payload, sort_keys=True, separators=(',', ':')).encode('utf-8')
176
+
177
+ @classmethod
178
+ def deserialize(cls, data: bytes) -> 'QuicSessionTicket':
179
+ payload = json.loads(data.decode('utf-8'))
180
+ return cls(
181
+ ticket=_unb64(payload['ticket']),
182
+ resumption_secret=_unb64(payload['resumption_secret']),
183
+ server_name=str(payload['server_name']),
184
+ alpn=str(payload['alpn']),
185
+ transport_parameters=TransportParameters.from_bytes(_unb64(payload['transport_parameters'])),
186
+ ticket_age_add=int(payload['ticket_age_add']),
187
+ ticket_nonce=_unb64(payload['ticket_nonce']),
188
+ ticket_lifetime=int(payload['ticket_lifetime']),
189
+ issued_at=int(_normalize_ticket_payload(payload)['issued_at']),
190
+ cipher_suite=int(payload.get('cipher_suite', CIPHER_TLS_AES_128_GCM_SHA256)),
191
+ max_early_data_size=int(payload.get('max_early_data_size', 0)),
192
+ )
193
+
194
+
195
+ _REPLAY_CACHE: dict[bytes, int] = {}
196
+
197
+
198
+
199
+ def _purge_replay_cache(now_ms: int) -> None:
200
+ expired = [key for key, expiry in _REPLAY_CACHE.items() if expiry <= now_ms]
201
+ for key in expired:
202
+ _REPLAY_CACHE.pop(key, None)
203
+
204
+
205
+
206
+ def _claim_ticket_for_0rtt(ticket_identity: bytes, *, now_ms: int, ticket_lifetime: int) -> bool:
207
+ _purge_replay_cache(now_ms)
208
+ token = hashlib.sha256(ticket_identity).digest()
209
+ expiry = now_ms + (ticket_lifetime * 1000)
210
+ if token in _REPLAY_CACHE:
211
+ return False
212
+ _REPLAY_CACHE[token] = expiry
213
+ return True
214
+
215
+
216
+
217
+ def _b64(data: bytes) -> str:
218
+ return base64.b64encode(data).decode('ascii')
219
+
220
+
221
+
222
+ def _unb64(data: str) -> bytes:
223
+ return base64.b64decode(data.encode('ascii'))
224
+
225
+
226
+
227
+ def _raise_tls(description: int, message: str) -> None:
228
+ raise TlsAlertError(description, message)
229
+
230
+
231
+
232
+ def _raise_quic_transport(error_code: int, message: str) -> None:
233
+ raise QuicTransportError(error_code, message)
234
+
235
+
236
+
237
+ def _select_alpn(client_alpns: Sequence[str], server_alpns: Sequence[str]) -> str:
238
+ for alpn in client_alpns:
239
+ if alpn in server_alpns:
240
+ return alpn
241
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'ALPN negotiation failed')
242
+
243
+
244
+
245
+ def _certificate_verify_input(context: bytes, transcript_hash: bytes) -> bytes:
246
+ return (b' ' * 64) + context + b'\x00' + transcript_hash
247
+
248
+
249
+
250
+ def _current_time_ms() -> int:
251
+ return int(time.time() * 1000)
252
+
253
+
254
+
255
+ def _signature_algorithms_for_public_key(public_key: object) -> tuple[int, ...]:
256
+ if isinstance(public_key, ed25519.Ed25519PublicKey):
257
+ return (SIG_ED25519,)
258
+ if isinstance(public_key, rsa.RSAPublicKey):
259
+ return (SIG_RSA_PSS_RSAE_SHA256, SIG_RSA_PSS_PSS_SHA256)
260
+ if isinstance(public_key, ec.EllipticCurvePublicKey):
261
+ return (SIG_ECDSA_SECP256R1_SHA256,)
262
+ return ()
263
+
264
+
265
+
266
+ def _select_certificate_verify_scheme(offered: Sequence[int], public_key: object) -> int:
267
+ compatible = _signature_algorithms_for_public_key(public_key)
268
+ for scheme in offered:
269
+ if scheme in compatible:
270
+ return scheme
271
+ _raise_tls(AlertDescription.HANDSHAKE_FAILURE, 'no compatible certificate signature algorithm')
272
+
273
+
274
+
275
+ def _sign_with_scheme(private_key: object, scheme: int, payload: bytes) -> bytes:
276
+ if scheme == SIG_ED25519:
277
+ if not isinstance(private_key, ed25519.Ed25519PrivateKey):
278
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'certificate key is not compatible with ed25519')
279
+ return private_key.sign(payload)
280
+ if scheme in {SIG_RSA_PSS_RSAE_SHA256, SIG_RSA_PSS_PSS_SHA256}:
281
+ if not isinstance(private_key, rsa.RSAPrivateKey):
282
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'certificate key is not compatible with RSA-PSS')
283
+ return private_key.sign(
284
+ payload,
285
+ asym_padding.PSS(mgf=asym_padding.MGF1(hashes.SHA256()), salt_length=hashes.SHA256().digest_size),
286
+ hashes.SHA256(),
287
+ )
288
+ if scheme == SIG_ECDSA_SECP256R1_SHA256:
289
+ if not isinstance(private_key, ec.EllipticCurvePrivateKey):
290
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'certificate key is not compatible with ECDSA')
291
+ return private_key.sign(payload, ec.ECDSA(hashes.SHA256()))
292
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'unsupported certificate verify signature algorithm')
293
+
294
+
295
+
296
+ def _verify_with_scheme(public_key: object, scheme: int, signature: bytes, payload: bytes) -> None:
297
+ try:
298
+ if scheme == SIG_ED25519:
299
+ if not isinstance(public_key, ed25519.Ed25519PublicKey):
300
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'peer certificate key is not compatible with ed25519')
301
+ public_key.verify(signature, payload)
302
+ return
303
+ if scheme in {SIG_RSA_PSS_RSAE_SHA256, SIG_RSA_PSS_PSS_SHA256}:
304
+ if not isinstance(public_key, rsa.RSAPublicKey):
305
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'peer certificate key is not compatible with RSA-PSS')
306
+ public_key.verify(
307
+ signature,
308
+ payload,
309
+ asym_padding.PSS(mgf=asym_padding.MGF1(hashes.SHA256()), salt_length=hashes.SHA256().digest_size),
310
+ hashes.SHA256(),
311
+ )
312
+ return
313
+ if scheme == SIG_ECDSA_SECP256R1_SHA256:
314
+ if not isinstance(public_key, ec.EllipticCurvePublicKey):
315
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'peer certificate key is not compatible with ECDSA')
316
+ public_key.verify(signature, payload, ec.ECDSA(hashes.SHA256()))
317
+ return
318
+ except TlsAlertError:
319
+ raise
320
+ except Exception as exc: # pragma: no cover - crypto backend specifics vary.
321
+ _raise_tls(AlertDescription.DECRYPT_ERROR, 'peer CertificateVerify signature is invalid')
322
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'unsupported peer certificate verify signature algorithm')
323
+
324
+
325
+
326
+ def _generate_key_share(group: int) -> tuple[object, bytes]:
327
+ if group == GROUP_X25519:
328
+ private_key = x25519.X25519PrivateKey.generate()
329
+ public_key = private_key.public_key().public_bytes(
330
+ serialization.Encoding.Raw,
331
+ serialization.PublicFormat.Raw,
332
+ )
333
+ return private_key, public_key
334
+ if group == GROUP_SECP256R1:
335
+ private_key = ec.generate_private_key(ec.SECP256R1())
336
+ public_key = private_key.public_key().public_bytes(
337
+ serialization.Encoding.X962,
338
+ serialization.PublicFormat.UncompressedPoint,
339
+ )
340
+ return private_key, public_key
341
+ raise ValueError(f'unsupported TLS key share group: {group}')
342
+
343
+
344
+
345
+ def _derive_shared_secret(private_key: object, group: int, peer_key_exchange: bytes) -> bytes:
346
+ try:
347
+ if group == GROUP_X25519:
348
+ if not isinstance(private_key, x25519.X25519PrivateKey):
349
+ _raise_tls(AlertDescription.INTERNAL_ERROR, 'x25519 key share state is unavailable')
350
+ peer_public = x25519.X25519PublicKey.from_public_bytes(peer_key_exchange)
351
+ return private_key.exchange(peer_public)
352
+ if group == GROUP_SECP256R1:
353
+ if not isinstance(private_key, ec.EllipticCurvePrivateKey):
354
+ _raise_tls(AlertDescription.INTERNAL_ERROR, 'secp256r1 key share state is unavailable')
355
+ peer_public = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256R1(), peer_key_exchange)
356
+ return private_key.exchange(ec.ECDH(), peer_public)
357
+ except TlsAlertError:
358
+ raise
359
+ except Exception: # pragma: no cover - crypto backend specifics vary.
360
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'peer key share could not be processed')
361
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'unsupported TLS key share group')
362
+
363
+
364
+
365
+ def _preferred_supported_group(*, supported_groups: Sequence[int], key_shares: dict[int, bytes]) -> int | None:
366
+ for group in SUPPORTED_GROUPS:
367
+ if group in key_shares:
368
+ return group
369
+ for group in SUPPORTED_GROUPS:
370
+ if group in supported_groups:
371
+ return group
372
+ return None
373
+
374
+
375
+ def _select_cipher_suite(offered: Sequence[int], supported: Sequence[int]) -> int | None:
376
+ for cipher_suite in supported:
377
+ if cipher_suite in offered:
378
+ return cipher_suite
379
+ return None
380
+
381
+
382
+
383
+ def _ticket_protection_key(private_key_pem: bytes | None, certificate_pem: bytes | None) -> bytes:
384
+ material = private_key_pem or certificate_pem or b'tigrcorn-quic-tls13-ticket-key'
385
+ return hashlib.sha256(b'tigrcorn-ticket-v1' + material).digest()
386
+
387
+
388
+
389
+ def _seal_ticket(ticket_key: bytes, payload: dict[str, object]) -> bytes:
390
+ serialized = json.dumps(payload, sort_keys=True, separators=(',', ':')).encode('utf-8')
391
+ mac = hmac.new(ticket_key, serialized, hashlib.sha256).digest()
392
+ return b'TGT1' + mac + serialized
393
+
394
+
395
+
396
+ def _normalize_ticket_payload(payload: dict[str, object]) -> dict[str, object]:
397
+ if 'version' in payload:
398
+ return payload
399
+ if 'v' in payload:
400
+ return {
401
+ 'version': int(payload.get('v', 1)),
402
+ 'issued_at': int(payload['i']),
403
+ 'ticket_lifetime': int(payload['l']),
404
+ 'ticket_age_add': int(payload['a']),
405
+ 'ticket_nonce': str(payload['n']),
406
+ 'server_name': str(payload['s']),
407
+ 'alpn': str(payload['h']),
408
+ 'transport_parameters': str(payload['p']),
409
+ 'cipher_suite': int(payload.get('c', CIPHER_TLS_AES_128_GCM_SHA256)),
410
+ 'resumption_secret': str(payload['r']),
411
+ 'max_early_data_size': int(payload.get('e', 0)),
412
+ }
413
+ return payload
414
+
415
+
416
+
417
+ def _open_ticket(ticket_key: bytes, ticket: bytes) -> dict[str, object]:
418
+ if not ticket.startswith(b'TGT1') or len(ticket) < 4 + 32:
419
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'invalid session ticket format')
420
+ mac = ticket[4:36]
421
+ serialized = ticket[36:]
422
+ expected = hmac.new(ticket_key, serialized, hashlib.sha256).digest()
423
+ if not hmac.compare_digest(mac, expected):
424
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'session ticket integrity verification failed')
425
+ return _normalize_ticket_payload(json.loads(serialized.decode('utf-8')))
426
+
427
+
428
+
429
+ def _session_ticket_from_payload(payload: dict[str, object], *, opaque_ticket: bytes) -> QuicSessionTicket:
430
+ payload = _normalize_ticket_payload(payload)
431
+ return QuicSessionTicket(
432
+ ticket=opaque_ticket,
433
+ resumption_secret=_unb64(str(payload['resumption_secret'])),
434
+ server_name=str(payload['server_name']),
435
+ alpn=str(payload['alpn']),
436
+ transport_parameters=TransportParameters.from_bytes(_unb64(str(payload['transport_parameters']))),
437
+ ticket_age_add=int(payload['ticket_age_add']),
438
+ ticket_nonce=_unb64(str(payload['ticket_nonce'])),
439
+ ticket_lifetime=int(payload['ticket_lifetime']),
440
+ issued_at=int(payload['issued_at']),
441
+ cipher_suite=int(payload.get('cipher_suite', CIPHER_TLS_AES_128_GCM_SHA256)),
442
+ max_early_data_size=int(payload.get('max_early_data_size', 0)),
443
+ )
444
+
445
+
446
+
447
+
448
+ def _client_hello_without_binders(full_client_hello: bytes, binders: Sequence[bytes]) -> bytes:
449
+ binders_length = 2 + sum(1 + len(binder) for binder in binders)
450
+ if binders_length <= 2 or binders_length > len(full_client_hello):
451
+ raise ProtocolError('invalid ClientHello pre_shared_key binder vector')
452
+ return full_client_hello[:-binders_length]
453
+
454
+
455
+
456
+ def generate_self_signed_certificate(common_name: str = 'tigrcorn-quic', *, purpose: str = 'server') -> tuple[bytes, bytes]:
457
+ private_key = ed25519.Ed25519PrivateKey.generate()
458
+ subject = issuer = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name)])
459
+ now = datetime.now(timezone.utc)
460
+ if purpose not in {'server', 'client', 'both'}:
461
+ raise ValueError("purpose must be 'server', 'client', or 'both'")
462
+ eku_oids: list[x509.ObjectIdentifier] = []
463
+ if purpose in {'server', 'both'}:
464
+ eku_oids.append(ExtendedKeyUsageOID.SERVER_AUTH)
465
+ if purpose in {'client', 'both'}:
466
+ eku_oids.append(ExtendedKeyUsageOID.CLIENT_AUTH)
467
+ builder = (
468
+ x509.CertificateBuilder()
469
+ .subject_name(subject)
470
+ .issuer_name(issuer)
471
+ .public_key(private_key.public_key())
472
+ .serial_number(x509.random_serial_number())
473
+ .not_valid_before(now - timedelta(minutes=1))
474
+ .not_valid_after(now + timedelta(days=7))
475
+ .add_extension(x509.SubjectAlternativeName([x509.DNSName(common_name)]), critical=False)
476
+ .add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
477
+ .add_extension(x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()), critical=False)
478
+ .add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(private_key.public_key()), critical=False)
479
+ .add_extension(
480
+ x509.KeyUsage(
481
+ digital_signature=True,
482
+ key_encipherment=False,
483
+ content_commitment=False,
484
+ data_encipherment=False,
485
+ key_agreement=False,
486
+ key_cert_sign=False,
487
+ crl_sign=False,
488
+ encipher_only=False,
489
+ decipher_only=False,
490
+ ),
491
+ critical=True,
492
+ )
493
+ .add_extension(x509.ExtendedKeyUsage(eku_oids), critical=False)
494
+ )
495
+ certificate = builder.sign(private_key, algorithm=None)
496
+ return (
497
+ certificate.public_bytes(serialization.Encoding.PEM),
498
+ private_key.private_bytes(
499
+ serialization.Encoding.PEM,
500
+ serialization.PrivateFormat.PKCS8,
501
+ serialization.NoEncryption(),
502
+ ),
503
+ )
504
+
505
+
506
+ class QuicTlsHandshakeDriver:
507
+ def __init__(
508
+ self,
509
+ *,
510
+ is_client: bool,
511
+ alpn: str | Sequence[str] = 'h3',
512
+ server_name: str = 'localhost',
513
+ transport_parameters: TransportParameters | None = None,
514
+ certificate_pem: bytes | None = None,
515
+ private_key_pem: bytes | None = None,
516
+ private_key_password: bytes | None = None,
517
+ trusted_certificates: Iterable[bytes] | None = None,
518
+ require_client_certificate: bool = False,
519
+ session_ticket: QuicSessionTicket | bytes | None = None,
520
+ enable_early_data: bool = False,
521
+ transport_mode: str = 'quic',
522
+ validation_policy: CertificateValidationPolicy | None = None,
523
+ cipher_suites: Sequence[int] | None = None,
524
+ ) -> None:
525
+ self.is_client = is_client
526
+ if isinstance(alpn, str):
527
+ self.alpns = (alpn,)
528
+ else:
529
+ offered = tuple(alpn)
530
+ if not offered:
531
+ raise ValueError('at least one ALPN identifier is required')
532
+ self.alpns = offered
533
+ self.alpn = self.alpns[0]
534
+ if transport_mode not in {'quic', 'stream'}:
535
+ raise ValueError(f'unsupported TLS transport_mode: {transport_mode!r}')
536
+ self.transport_mode = transport_mode
537
+ self.server_name = server_name
538
+ self.transport_parameters = transport_parameters or (TransportParameters() if transport_mode == 'quic' else None)
539
+ self.validation_policy = validation_policy
540
+ configured_cipher_suites = tuple(int(item) for item in (cipher_suites or SUPPORTED_CIPHER_SUITES))
541
+ if not configured_cipher_suites:
542
+ raise ValueError('at least one TLS 1.3 cipher suite must be configured')
543
+ unsupported_cipher_suites = [item for item in configured_cipher_suites if item not in SUPPORTED_CIPHER_SUITES]
544
+ if unsupported_cipher_suites:
545
+ raise ValueError(f'unsupported TLS 1.3 cipher suites: {unsupported_cipher_suites!r}')
546
+ self.supported_cipher_suites = configured_cipher_suites
547
+ if not is_client and (certificate_pem is None or private_key_pem is None):
548
+ certificate_pem, private_key_pem = generate_self_signed_certificate(server_name)
549
+ if isinstance(session_ticket, bytes):
550
+ self.session_ticket = QuicSessionTicket.deserialize(session_ticket)
551
+ else:
552
+ self.session_ticket = session_ticket
553
+ self.certificate_pem = certificate_pem
554
+ self.private_key_pem = private_key_pem
555
+ self.trusted_certificates = tuple(trusted_certificates or ())
556
+ self.require_client_certificate = bool(require_client_certificate)
557
+ if not self.is_client and self.require_client_certificate and not self.trusted_certificates:
558
+ raise ValueError('trusted_certificates are required when client certificates are mandatory')
559
+ if self.transport_mode == 'stream':
560
+ self.enable_early_data = False
561
+ self._private_key = serialization.load_pem_private_key(private_key_pem, password=private_key_password) if private_key_pem is not None else None
562
+ if certificate_pem is not None:
563
+ self._certificate_chain = tuple(load_pem_certificates((certificate_pem,)))
564
+ self._certificate_chain_pem = tuple(
565
+ certificate.public_bytes(serialization.Encoding.PEM) for certificate in self._certificate_chain
566
+ )
567
+ else:
568
+ self._certificate_chain = ()
569
+ self._certificate_chain_pem = ()
570
+ self._certificate_chain_der = tuple(certificate.public_bytes(serialization.Encoding.DER) for certificate in self._certificate_chain)
571
+ self._ticket_key = _ticket_protection_key(private_key_pem, certificate_pem)
572
+ self.enable_early_data = enable_early_data and self.transport_mode == 'quic'
573
+ self.early_data_requested = bool(self.session_ticket and self.enable_early_data and is_client)
574
+ self.early_data_accepted = False
575
+ self.issued_session_ticket: QuicSessionTicket | None = None
576
+ self.received_session_ticket: QuicSessionTicket | None = None
577
+ self.selected_alpn: str | None = None
578
+ self.peer_transport_parameters: TransportParameters | None = None
579
+ self.peer_certificate_pem: bytes | None = None
580
+ self.peer_certificate_chain_pem: tuple[bytes, ...] = ()
581
+ self.complete = False
582
+ self.state = 'client_idle' if is_client else 'server_idle'
583
+
584
+ initial_cipher_suite = (
585
+ self.session_ticket.cipher_suite
586
+ if (
587
+ self.session_ticket is not None
588
+ and self.session_ticket.cipher_suite in self.supported_cipher_suites
589
+ )
590
+ else self.supported_cipher_suites[0]
591
+ )
592
+ self._selected_cipher_suite = int(initial_cipher_suite)
593
+ self._cipher_parameters = cipher_suite_parameters(self._selected_cipher_suite)
594
+ self._key_schedule = Tls13KeySchedule(hash_name=self._cipher_parameters.hash_name)
595
+ self._transcript = HandshakeTranscript(hash_name=self._cipher_parameters.hash_name)
596
+ self._receive_buffer = bytearray()
597
+ self._local_key_share_group = GROUP_X25519
598
+ self._local_key_share_private, self._local_key_share_public = _generate_key_share(self._local_key_share_group)
599
+ self._last_client_hello: ClientHello | None = None
600
+ self._last_client_hello_bytes: bytes | None = None
601
+ self._hello_retry_request_bytes: bytes | None = None
602
+ self._received_hrr = False
603
+ self._hrr_requested_group: int | None = None
604
+ self._cookie: bytes | None = None
605
+ self._client_certificate_requested = False
606
+ self._client_certificate_request_context = b''
607
+ self._certificate_request_signature_algorithms: tuple[int, ...] = ()
608
+ self._peer_signature_algorithms: tuple[int, ...] = SUPPORTED_SIGNATURE_SCHEMES
609
+ self._peer_certificate_signature_algorithms: tuple[int, ...] = SUPPORTED_CERTIFICATE_SIGNATURE_SCHEMES
610
+ self._using_psk = False
611
+ self._selected_psk_index: int | None = None
612
+ self._selected_psk_ticket: QuicSessionTicket | None = None
613
+ self._peer_certificate_present = False
614
+ self._peer_certificate_verify_received = False
615
+ self._shared_secret: bytes | None = None
616
+ self._early_secret: bytes | None = None
617
+ self._client_early_secret: bytes | None = None
618
+ self._master_secret: bytes | None = None
619
+ self._traffic_secrets: QuicTrafficSecrets | None = None
620
+ self._client_handshake_secret: bytes | None = None
621
+ self._server_handshake_secret: bytes | None = None
622
+ self._resumption_master_secret: bytes | None = None
623
+ self._exporter_master_secret: bytes | None = None
624
+
625
+ @property
626
+ def traffic_secrets(self) -> QuicTrafficSecrets | None:
627
+ return self._traffic_secrets
628
+
629
+ @property
630
+ def cipher_parameters(self) -> CipherSuiteParameters:
631
+ return self._cipher_parameters
632
+
633
+ def packet_protection_parameters(self, *, stage: str) -> CipherSuiteParameters:
634
+ if stage == '0rtt':
635
+ if self._selected_psk_ticket is not None:
636
+ return cipher_suite_parameters(self._selected_psk_ticket.cipher_suite)
637
+ if self.session_ticket is not None:
638
+ return cipher_suite_parameters(self.session_ticket.cipher_suite)
639
+ return self._cipher_parameters
640
+
641
+ def _configure_cipher_suite(self, cipher_suite: int) -> None:
642
+ parameters = cipher_suite_parameters(cipher_suite)
643
+ self._selected_cipher_suite = int(cipher_suite)
644
+ self._cipher_parameters = parameters
645
+ self._key_schedule = Tls13KeySchedule(hash_name=parameters.hash_name)
646
+ self._transcript.hash_name = parameters.hash_name
647
+
648
+ def outbound_flights(self, data: bytes) -> list[HandshakeFlight]:
649
+ return [HandshakeFlight(packet_space=flight.packet_space, data=flight.data) for flight in split_handshake_flights(data)]
650
+
651
+ def _current_transcript_hash(self) -> bytes:
652
+ return self._transcript.digest()
653
+
654
+ def _set_traffic_secrets(
655
+ self,
656
+ *,
657
+ client_handshake_secret: bytes,
658
+ server_handshake_secret: bytes,
659
+ client_application_secret: bytes,
660
+ server_application_secret: bytes,
661
+ client_early_secret: bytes | None,
662
+ ) -> None:
663
+ self._client_handshake_secret = client_handshake_secret
664
+ self._server_handshake_secret = server_handshake_secret
665
+ self._traffic_secrets = QuicTrafficSecrets(
666
+ client_handshake_secret=client_handshake_secret,
667
+ server_handshake_secret=server_handshake_secret,
668
+ client_application_secret=client_application_secret,
669
+ server_application_secret=server_application_secret,
670
+ client_early_secret=client_early_secret,
671
+ exporter_master_secret=self._exporter_master_secret,
672
+ resumption_master_secret=self._resumption_master_secret,
673
+ )
674
+
675
+ def _server_base_key(self) -> bytes:
676
+ if self._server_handshake_secret is None:
677
+ _raise_tls(AlertDescription.INTERNAL_ERROR, 'server handshake secret is not available')
678
+ return self._server_handshake_secret
679
+
680
+ def _client_base_key(self) -> bytes:
681
+ if self._client_handshake_secret is None:
682
+ _raise_tls(AlertDescription.INTERNAL_ERROR, 'client handshake secret is not available')
683
+ return self._client_handshake_secret
684
+
685
+ def _certificate_entry_chain(self) -> tuple[CertificateEntry, ...]:
686
+ return tuple(CertificateEntry(cert_data=certificate_der) for certificate_der in self._certificate_chain_der)
687
+
688
+ def _build_client_hello(self) -> tuple[ClientHello, bytes]:
689
+ base_extensions: list[TlsExtension] = [
690
+ TlsExtension(ExtensionType.SERVER_NAME, self.server_name),
691
+ TlsExtension(ExtensionType.SUPPORTED_VERSIONS, (0x0304,)),
692
+ TlsExtension(ExtensionType.SUPPORTED_GROUPS, SUPPORTED_GROUPS),
693
+ TlsExtension(ExtensionType.SIGNATURE_ALGORITHMS, SUPPORTED_SIGNATURE_SCHEMES),
694
+ TlsExtension(ExtensionType.SIGNATURE_ALGORITHMS_CERT, SUPPORTED_CERTIFICATE_SIGNATURE_SCHEMES),
695
+ TlsExtension(ExtensionType.ALPN, self.alpns),
696
+ TlsExtension(ExtensionType.KEY_SHARE, ((self._local_key_share_group, self._local_key_share_public),)),
697
+ ]
698
+ if self.transport_mode == 'quic':
699
+ base_extensions.append(TlsExtension(ExtensionType.QUIC_TRANSPORT_PARAMETERS, self.transport_parameters))
700
+ if self._cookie is not None:
701
+ base_extensions.append(TlsExtension(ExtensionType.COOKIE, self._cookie))
702
+
703
+ offered_psks: OfferedPsks | None = None
704
+ if self.session_ticket is not None:
705
+ age_ms = max(_current_time_ms() - self.session_ticket.issued_at, 0)
706
+ identity = PskIdentity(
707
+ identity=self.session_ticket.ticket,
708
+ obfuscated_ticket_age=(age_ms + self.session_ticket.ticket_age_add) % (2**32),
709
+ )
710
+ offered_psks = OfferedPsks(identities=(identity,), binders=(b'\x00' * self._key_schedule.hash_length,))
711
+ base_extensions.append(TlsExtension(ExtensionType.PSK_KEY_EXCHANGE_MODES, (PSK_MODE_DHE_KE,)))
712
+ if self.early_data_requested and not self._received_hrr:
713
+ base_extensions.append(TlsExtension(ExtensionType.EARLY_DATA, True))
714
+
715
+ hello = ClientHello(
716
+ random=os.urandom(32),
717
+ legacy_session_id=b'' if self.transport_mode == 'quic' else os.urandom(32),
718
+ cipher_suites=self.supported_cipher_suites,
719
+ extensions=tuple(base_extensions),
720
+ )
721
+
722
+ if offered_psks is None:
723
+ encoded = hello.encode()
724
+ return hello, encoded
725
+
726
+ psk_extension = TlsExtension(ExtensionType.PRE_SHARED_KEY, offered_psks)
727
+ hello_with_placeholder = hello.with_extensions(tuple(base_extensions) + (psk_extension,))
728
+ placeholder_bytes = hello_with_placeholder.encode()
729
+ truncated_bytes = _client_hello_without_binders(placeholder_bytes, offered_psks.binders)
730
+ early_secret = self._key_schedule.make_early_secret(self.session_ticket.resumption_secret)
731
+ binder_key = self._key_schedule.make_binder_key(early_secret)
732
+ transcript_hash = self._transcript.digest_with(truncated_bytes)
733
+ binder = hmac.new(
734
+ self._key_schedule.finished_key(binder_key),
735
+ transcript_hash,
736
+ getattr(hashlib, self._key_schedule.hash_name),
737
+ ).digest()
738
+ final_psk = TlsExtension(
739
+ ExtensionType.PRE_SHARED_KEY,
740
+ OfferedPsks(identities=offered_psks.identities, binders=(binder,)),
741
+ )
742
+ final_hello = hello.with_extensions(tuple(base_extensions) + (final_psk,))
743
+ encoded = final_hello.encode()
744
+ self._early_secret = early_secret
745
+ self._client_early_secret = self._key_schedule.client_early_traffic_secret(early_secret, encoded)
746
+ return final_hello, encoded
747
+
748
+ def initiate(self) -> bytes:
749
+ if not self.is_client:
750
+ raise ProtocolError('only a client can initiate the handshake')
751
+ if self.state not in {'client_idle', 'client_wait_server'}:
752
+ raise ProtocolError('unexpected client handshake state')
753
+ hello, encoded = self._build_client_hello()
754
+ self._last_client_hello = hello
755
+ self._last_client_hello_bytes = encoded
756
+ self._transcript.append(encoded)
757
+ self.state = 'client_wait_server'
758
+ return encoded
759
+
760
+ def _derive_handshake_secrets(self) -> tuple[bytes, bytes]:
761
+ if self._shared_secret is None:
762
+ _raise_tls(AlertDescription.INTERNAL_ERROR, 'shared secret is not available')
763
+ if self._early_secret is None:
764
+ self._early_secret = self._key_schedule.make_early_secret(None)
765
+ handshake_secret = self._key_schedule.handshake_secret(self._early_secret, self._shared_secret)
766
+ return self._key_schedule.handshake_traffic_secrets(handshake_secret, self._transcript)
767
+
768
+ def _derive_application_secrets(self) -> tuple[bytes, bytes]:
769
+ if self._shared_secret is None:
770
+ _raise_tls(AlertDescription.INTERNAL_ERROR, 'shared secret is not available')
771
+ if self._early_secret is None:
772
+ self._early_secret = self._key_schedule.make_early_secret(None)
773
+ handshake_secret = self._key_schedule.handshake_secret(self._early_secret, self._shared_secret)
774
+ self._master_secret = self._key_schedule.master_secret(handshake_secret)
775
+ return self._key_schedule.application_traffic_secrets(self._master_secret, self._transcript)
776
+
777
+ def _finalize_post_handshake_secrets(self) -> None:
778
+ if self._master_secret is None:
779
+ return
780
+ self._exporter_master_secret = self._key_schedule.exporter_master_secret(self._master_secret, self._transcript)
781
+ self._resumption_master_secret = self._key_schedule.resumption_master_secret(self._master_secret, self._transcript)
782
+ if self._traffic_secrets is not None:
783
+ self._traffic_secrets.exporter_master_secret = self._exporter_master_secret
784
+ self._traffic_secrets.resumption_master_secret = self._resumption_master_secret
785
+
786
+ def _load_selected_peer_certificate(self) -> x509.Certificate:
787
+ if not self.peer_certificate_chain_pem:
788
+ _raise_tls(AlertDescription.BAD_CERTIFICATE, 'peer certificate chain is missing')
789
+ try:
790
+ if self.validation_policy is None:
791
+ policy = CertificateValidationPolicy(
792
+ purpose=CertificatePurpose.SERVER_AUTH if self.is_client else CertificatePurpose.CLIENT_AUTH,
793
+ )
794
+ else:
795
+ policy = self.validation_policy
796
+ leaf = verify_certificate_chain(
797
+ self.peer_certificate_chain_pem,
798
+ self.trusted_certificates,
799
+ server_name=self.server_name if self.is_client else '',
800
+ policy=policy,
801
+ )
802
+ except ProtocolError as exc:
803
+ _raise_tls(AlertDescription.BAD_CERTIFICATE, str(exc))
804
+ self.peer_certificate_pem = leaf.public_bytes(serialization.Encoding.PEM)
805
+ return leaf
806
+
807
+ def _handle_client_hello(self, message: ClientHello, *, raw_message: bytes | None = None) -> bytes:
808
+ extension_types = [int(extension.extension_type) for extension in message.extensions]
809
+ if ExtensionType.PRE_SHARED_KEY in extension_types and extension_types[-1] != ExtensionType.PRE_SHARED_KEY:
810
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'pre_shared_key must be the final ClientHello extension')
811
+ if ExtensionType.EARLY_DATA in extension_types and ExtensionType.PRE_SHARED_KEY not in extension_types:
812
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'early_data requires a matching pre_shared_key offer')
813
+ if self.transport_mode == 'quic' and message.legacy_session_id:
814
+ _raise_quic_transport(
815
+ _QUIC_TRANSPORT_ERROR_PROTOCOL_VIOLATION,
816
+ 'QUIC clients must not use TLS middlebox compatibility mode',
817
+ )
818
+ offered = extension_dict(message.extensions)
819
+ versions = tuple(int(version) for version in offered.get(ExtensionType.SUPPORTED_VERSIONS, ()))
820
+ if 0x0304 not in versions:
821
+ _raise_tls(AlertDescription.PROTOCOL_VERSION, 'client did not offer TLS 1.3')
822
+ selected_cipher_suite = _select_cipher_suite(message.cipher_suites, self.supported_cipher_suites)
823
+ if selected_cipher_suite is None:
824
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'client did not offer a mutually supported TLS 1.3 cipher suite')
825
+ self._configure_cipher_suite(selected_cipher_suite)
826
+ offered_alpns = tuple(str(item) for item in offered.get(ExtensionType.ALPN, ()))
827
+ self.selected_alpn = _select_alpn(offered_alpns, self.alpns)
828
+ peer_transport_parameters = offered.get(ExtensionType.QUIC_TRANSPORT_PARAMETERS)
829
+ if self.transport_mode == 'quic':
830
+ self.peer_transport_parameters = peer_transport_parameters
831
+ if not isinstance(self.peer_transport_parameters, TransportParameters):
832
+ _raise_tls(AlertDescription.MISSING_EXTENSION, 'client did not provide QUIC transport parameters')
833
+ else:
834
+ self.peer_transport_parameters = peer_transport_parameters if isinstance(peer_transport_parameters, TransportParameters) else None
835
+ peer_signature_algorithms = offered.get(ExtensionType.SIGNATURE_ALGORITHMS)
836
+ if not isinstance(peer_signature_algorithms, tuple) or not peer_signature_algorithms:
837
+ _raise_tls(AlertDescription.MISSING_EXTENSION, 'client did not provide signature_algorithms')
838
+ self._peer_signature_algorithms = tuple(int(item) for item in peer_signature_algorithms)
839
+ peer_certificate_algorithms = offered.get(ExtensionType.SIGNATURE_ALGORITHMS_CERT, peer_signature_algorithms)
840
+ if not isinstance(peer_certificate_algorithms, tuple) or not peer_certificate_algorithms:
841
+ _raise_tls(AlertDescription.MISSING_EXTENSION, 'client did not provide certificate signature algorithms')
842
+ self._peer_certificate_signature_algorithms = tuple(int(item) for item in peer_certificate_algorithms)
843
+ supported_groups = tuple(int(group) for group in offered.get(ExtensionType.SUPPORTED_GROUPS, ()))
844
+ key_shares = offered.get(ExtensionType.KEY_SHARE)
845
+ if not isinstance(key_shares, dict):
846
+ key_shares = {}
847
+
848
+ selected_group: int | None
849
+ if self.state == 'server_wait_client_hello_retry':
850
+ if self._hrr_requested_group is None:
851
+ _raise_tls(AlertDescription.INTERNAL_ERROR, 'HelloRetryRequest state is unavailable')
852
+ if self._hrr_requested_group not in key_shares:
853
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'client did not supply the requested key share after HelloRetryRequest')
854
+ selected_group = self._hrr_requested_group
855
+ else:
856
+ selected_group = None
857
+ for group in SUPPORTED_GROUPS:
858
+ if group in key_shares:
859
+ selected_group = group
860
+ break
861
+ if selected_group is None:
862
+ requested_group = _preferred_supported_group(supported_groups=supported_groups, key_shares=key_shares)
863
+ if requested_group is None:
864
+ _raise_tls(AlertDescription.HANDSHAKE_FAILURE, 'client does not support a mutually compatible key exchange group')
865
+ hrr = ServerHello(
866
+ random=HELLO_RETRY_REQUEST_RANDOM,
867
+ legacy_session_id_echo=message.legacy_session_id,
868
+ cipher_suite=selected_cipher_suite,
869
+ extensions=(
870
+ TlsExtension(ExtensionType.SUPPORTED_VERSIONS, 0x0304),
871
+ TlsExtension(ExtensionType.KEY_SHARE, requested_group),
872
+ ),
873
+ )
874
+ encoded_hrr = hrr.encode(message_context='hello_retry_request')
875
+ self._configure_cipher_suite(selected_cipher_suite)
876
+ if self._last_client_hello_bytes is not None:
877
+ self._transcript.reset_with_message_hash(self._last_client_hello_bytes)
878
+ else:
879
+ self._transcript.reset_with_message_hash(message.encode())
880
+ self._transcript.append(encoded_hrr)
881
+ self._hello_retry_request_bytes = encoded_hrr
882
+ self._received_hrr = True
883
+ self._hrr_requested_group = requested_group
884
+ self.early_data_accepted = False
885
+ self.state = 'server_wait_client_hello_retry'
886
+ return encoded_hrr
887
+
888
+ offered_psks = offered.get(ExtensionType.PRE_SHARED_KEY)
889
+ psk_modes = tuple(int(item) for item in offered.get(ExtensionType.PSK_KEY_EXCHANGE_MODES, ()))
890
+ client_requested_early_data = bool(offered.get(ExtensionType.EARLY_DATA, False))
891
+ self._using_psk = False
892
+ self._selected_psk_index = None
893
+ self._selected_psk_ticket = None
894
+ if isinstance(offered_psks, OfferedPsks) and PSK_MODE_DHE_KE in psk_modes:
895
+ if raw_message is not None:
896
+ truncated_bytes = _client_hello_without_binders(raw_message, offered_psks.binders)
897
+ else:
898
+ truncated_extensions: list[TlsExtension] = []
899
+ for extension in message.extensions:
900
+ if int(extension.extension_type) == ExtensionType.PRE_SHARED_KEY:
901
+ truncated_extensions.append(
902
+ TlsExtension(
903
+ ExtensionType.PRE_SHARED_KEY,
904
+ extension.value,
905
+ raw_data=encode_pre_shared_key_client_without_binders(offered_psks.identities),
906
+ )
907
+ )
908
+ else:
909
+ truncated_extensions.append(extension)
910
+ truncated_message = message.with_extensions(tuple(truncated_extensions))
911
+ truncated_bytes = truncated_message.encode()
912
+ transcript_hash = self._transcript.digest_with(truncated_bytes)
913
+ now_ms = _current_time_ms()
914
+ for index, (identity, binder) in enumerate(zip(offered_psks.identities, offered_psks.binders)):
915
+ try:
916
+ payload = _open_ticket(self._ticket_key, identity.identity)
917
+ except TlsAlertError:
918
+ continue
919
+ ticket = _session_ticket_from_payload(payload, opaque_ticket=identity.identity)
920
+ if ticket.server_name != self.server_name:
921
+ continue
922
+ if ticket.alpn not in offered_alpns:
923
+ continue
924
+ if ticket.cipher_suite != selected_cipher_suite:
925
+ continue
926
+ age_ms = (identity.obfuscated_ticket_age - ticket.ticket_age_add) % (2**32)
927
+ actual_age_ms = max(now_ms - ticket.issued_at, 0)
928
+ if actual_age_ms > (ticket.ticket_lifetime * 1000):
929
+ continue
930
+ if abs(int(actual_age_ms) - int(age_ms)) > _MAX_AGE_SKEW_MS:
931
+ continue
932
+ early_secret = self._key_schedule.make_early_secret(ticket.resumption_secret)
933
+ binder_key = self._key_schedule.make_binder_key(early_secret)
934
+ expected_binder = hmac.new(
935
+ self._key_schedule.finished_key(binder_key),
936
+ transcript_hash,
937
+ getattr(hashlib, self._key_schedule.hash_name),
938
+ ).digest()
939
+ if not hmac.compare_digest(expected_binder, binder):
940
+ continue
941
+ self._using_psk = True
942
+ self._selected_psk_index = index
943
+ self._selected_psk_ticket = ticket
944
+ self._early_secret = early_secret
945
+ self._client_early_secret = self._key_schedule.client_early_traffic_secret(early_secret, message.encode())
946
+ if (
947
+ self.transport_mode == 'quic'
948
+ and client_requested_early_data
949
+ and index == 0
950
+ and self.enable_early_data
951
+ and ticket.max_early_data_size == QUIC_EARLY_DATA_SENTINEL
952
+ and ticket.transport_parameters.is_0rtt_compatible_with(self.transport_parameters)
953
+ and _claim_ticket_for_0rtt(ticket.ticket, now_ms=now_ms, ticket_lifetime=ticket.ticket_lifetime)
954
+ ):
955
+ self.early_data_accepted = True
956
+ else:
957
+ self.early_data_accepted = False
958
+ break
959
+ if not self._using_psk:
960
+ self._early_secret = self._key_schedule.make_early_secret(None)
961
+ self._client_early_secret = None
962
+ self.early_data_accepted = False
963
+
964
+ self._last_client_hello = message
965
+ self._last_client_hello_bytes = message.encode()
966
+ self._transcript.append(self._last_client_hello_bytes)
967
+
968
+ assert selected_group is not None
969
+ if self._local_key_share_group != selected_group:
970
+ self._local_key_share_group = selected_group
971
+ self._local_key_share_private, self._local_key_share_public = _generate_key_share(selected_group)
972
+ self._shared_secret = _derive_shared_secret(self._local_key_share_private, selected_group, key_shares[selected_group])
973
+
974
+ server_hello_extensions: list[TlsExtension] = [
975
+ TlsExtension(ExtensionType.SUPPORTED_VERSIONS, 0x0304),
976
+ TlsExtension(ExtensionType.KEY_SHARE, (selected_group, self._local_key_share_public)),
977
+ ]
978
+ if self._using_psk and self._selected_psk_index is not None:
979
+ server_hello_extensions.append(TlsExtension(ExtensionType.PRE_SHARED_KEY, self._selected_psk_index))
980
+ server_hello = ServerHello(
981
+ random=os.urandom(32),
982
+ legacy_session_id_echo=message.legacy_session_id,
983
+ cipher_suite=selected_cipher_suite,
984
+ extensions=tuple(server_hello_extensions),
985
+ )
986
+ encoded_server_hello = server_hello.encode()
987
+ self._transcript.append(encoded_server_hello)
988
+ client_hs, server_hs = self._derive_handshake_secrets()
989
+
990
+ ee_extensions = [
991
+ TlsExtension(ExtensionType.ALPN, self.selected_alpn),
992
+ ]
993
+ if self.transport_mode == 'quic':
994
+ ee_extensions.append(TlsExtension(ExtensionType.QUIC_TRANSPORT_PARAMETERS, self.transport_parameters))
995
+ if self.early_data_accepted:
996
+ ee_extensions.append(TlsExtension(ExtensionType.EARLY_DATA, True))
997
+ encrypted_extensions = EncryptedExtensions(extensions=tuple(ee_extensions))
998
+ encoded_ee = encrypted_extensions.encode()
999
+ self._transcript.append(encoded_ee)
1000
+
1001
+ flight = bytearray(encoded_server_hello)
1002
+ flight.extend(encoded_ee)
1003
+ if self.require_client_certificate:
1004
+ certificate_request = CertificateRequest(
1005
+ request_context=b'',
1006
+ extensions=(
1007
+ TlsExtension(ExtensionType.SIGNATURE_ALGORITHMS, SUPPORTED_SIGNATURE_SCHEMES),
1008
+ TlsExtension(ExtensionType.SIGNATURE_ALGORITHMS_CERT, SUPPORTED_CERTIFICATE_SIGNATURE_SCHEMES),
1009
+ ),
1010
+ )
1011
+ encoded_certificate_request = certificate_request.encode()
1012
+ self._transcript.append(encoded_certificate_request)
1013
+ flight.extend(encoded_certificate_request)
1014
+ self._client_certificate_requested = True
1015
+ self._client_certificate_request_context = b''
1016
+ if not self._using_psk:
1017
+ certificate = Certificate(certificate_list=self._certificate_entry_chain())
1018
+ encoded_certificate = certificate.encode()
1019
+ self._transcript.append(encoded_certificate)
1020
+ flight.extend(encoded_certificate)
1021
+ public_key = self._certificate_chain[0].public_key()
1022
+ selected_scheme = _select_certificate_verify_scheme(self._peer_signature_algorithms, public_key)
1023
+ signature_payload = _certificate_verify_input(_SERVER_CERT_VERIFY_CONTEXT, self._current_transcript_hash())
1024
+ signature = _sign_with_scheme(self._private_key, selected_scheme, signature_payload)
1025
+ certificate_verify = CertificateVerify(algorithm=selected_scheme, signature=signature)
1026
+ encoded_cv = certificate_verify.encode()
1027
+ self._transcript.append(encoded_cv)
1028
+ flight.extend(encoded_cv)
1029
+
1030
+ finished = Finished(verify_data=self._key_schedule.finished_verify_data(server_hs, self._transcript))
1031
+ encoded_finished = finished.encode()
1032
+ self._transcript.append(encoded_finished)
1033
+ flight.extend(encoded_finished)
1034
+ client_ap, server_ap = self._derive_application_secrets()
1035
+ client_early = getattr(self, '_client_early_secret', None)
1036
+ self._set_traffic_secrets(
1037
+ client_handshake_secret=client_hs,
1038
+ server_handshake_secret=server_hs,
1039
+ client_application_secret=client_ap,
1040
+ server_application_secret=server_ap,
1041
+ client_early_secret=client_early,
1042
+ )
1043
+ self.state = 'server_wait_client_finished'
1044
+ return bytes(flight)
1045
+
1046
+ def _handle_client_finished(self, message: Finished) -> bytes:
1047
+ if self.require_client_certificate:
1048
+ if not self.peer_certificate_chain_pem:
1049
+ _raise_tls(AlertDescription.CERTIFICATE_REQUIRED, 'client certificate is required')
1050
+ if not self._peer_certificate_verify_received:
1051
+ _raise_tls(AlertDescription.HANDSHAKE_FAILURE, 'client CertificateVerify is missing')
1052
+ if self._client_handshake_secret is None:
1053
+ _raise_tls(AlertDescription.INTERNAL_ERROR, 'client handshake secret is unavailable')
1054
+ if not self._key_schedule.verify_finished(message.verify_data, base_key=self._client_handshake_secret, transcript=self._transcript):
1055
+ _raise_tls(AlertDescription.DECRYPT_ERROR, 'client Finished verify_data is invalid')
1056
+ self._transcript.append(message.encode())
1057
+ self._finalize_post_handshake_secrets()
1058
+ self.complete = True
1059
+ self.state = 'complete'
1060
+ return b''
1061
+
1062
+ def _handle_server_hello(self, message: ServerHello) -> bytes:
1063
+ if self._last_client_hello is None or self._last_client_hello_bytes is None:
1064
+ _raise_tls(AlertDescription.INTERNAL_ERROR, 'client hello state is unavailable')
1065
+ offered = extension_dict(message.extensions)
1066
+ if message.is_hello_retry_request:
1067
+ if self._received_hrr:
1068
+ _raise_tls(AlertDescription.UNEXPECTED_MESSAGE, 'received a second HelloRetryRequest')
1069
+ selected_version = int(offered.get(ExtensionType.SUPPORTED_VERSIONS, 0))
1070
+ if selected_version != 0x0304:
1071
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'HelloRetryRequest selected an invalid TLS version')
1072
+ if message.cipher_suite not in self._last_client_hello.cipher_suites:
1073
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'HelloRetryRequest selected an unexpected cipher suite')
1074
+ requested_group = offered.get(ExtensionType.KEY_SHARE)
1075
+ if not isinstance(requested_group, int) or requested_group not in SUPPORTED_GROUPS:
1076
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'HelloRetryRequest requested an unsupported key share group')
1077
+ if message.legacy_session_id_echo != self._last_client_hello.legacy_session_id:
1078
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'HelloRetryRequest echoed the wrong session id')
1079
+ self._cookie = offered.get(ExtensionType.COOKIE) if isinstance(offered.get(ExtensionType.COOKIE), bytes) else None
1080
+ self._received_hrr = True
1081
+ self.early_data_requested = False
1082
+ self._configure_cipher_suite(message.cipher_suite)
1083
+ self._transcript.reset_with_message_hash(self._last_client_hello_bytes)
1084
+ encoded_hrr = message.encode(message_context='hello_retry_request')
1085
+ self._hello_retry_request_bytes = encoded_hrr
1086
+ self._transcript.append(encoded_hrr)
1087
+ self._local_key_share_group = requested_group
1088
+ self._local_key_share_private, self._local_key_share_public = _generate_key_share(self._local_key_share_group)
1089
+ hello, encoded = self._build_client_hello()
1090
+ self._last_client_hello = hello
1091
+ self._last_client_hello_bytes = encoded
1092
+ self._transcript.append(encoded)
1093
+ return encoded
1094
+
1095
+ if int(offered.get(ExtensionType.SUPPORTED_VERSIONS, 0)) != 0x0304:
1096
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'server selected an invalid TLS version')
1097
+ if message.legacy_session_id_echo != self._last_client_hello.legacy_session_id:
1098
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'ServerHello echoed the wrong session id')
1099
+ if message.cipher_suite not in self._last_client_hello.cipher_suites:
1100
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'server selected an unexpected cipher suite')
1101
+ self._configure_cipher_suite(message.cipher_suite)
1102
+ selected_psk = offered.get(ExtensionType.PRE_SHARED_KEY)
1103
+ self._using_psk = selected_psk is not None
1104
+ if self._using_psk:
1105
+ if self.session_ticket is None or int(selected_psk) != 0:
1106
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'server selected an unexpected PSK identity')
1107
+ if self.session_ticket.cipher_suite != message.cipher_suite:
1108
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'server resumed with an unexpected PSK cipher suite')
1109
+ self._early_secret = self._key_schedule.make_early_secret(self.session_ticket.resumption_secret)
1110
+ else:
1111
+ self._early_secret = self._key_schedule.make_early_secret(None)
1112
+ key_share = offered.get(ExtensionType.KEY_SHARE)
1113
+ if not isinstance(key_share, tuple) or len(key_share) != 2:
1114
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'server did not supply a valid key share')
1115
+ selected_group = int(key_share[0])
1116
+ if selected_group != self._local_key_share_group:
1117
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'server selected an unexpected key share group')
1118
+ self._shared_secret = _derive_shared_secret(self._local_key_share_private, selected_group, bytes(key_share[1]))
1119
+ encoded = message.encode()
1120
+ self._transcript.append(encoded)
1121
+ client_hs, server_hs = self._derive_handshake_secrets()
1122
+ self._client_handshake_secret = client_hs
1123
+ self._server_handshake_secret = server_hs
1124
+ return b''
1125
+
1126
+ def _handle_encrypted_extensions(self, message: EncryptedExtensions) -> None:
1127
+ offered = extension_dict(message.extensions)
1128
+ if offered.get(ExtensionType.EARLY_DATA, False) and (not self.early_data_requested or self._received_hrr):
1129
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'server accepted early data without a valid client offer')
1130
+ peer_transport_parameters = offered.get(ExtensionType.QUIC_TRANSPORT_PARAMETERS)
1131
+ if self.transport_mode == 'quic':
1132
+ self.peer_transport_parameters = peer_transport_parameters
1133
+ if not isinstance(self.peer_transport_parameters, TransportParameters):
1134
+ _raise_tls(AlertDescription.MISSING_EXTENSION, 'server did not provide QUIC transport parameters')
1135
+ else:
1136
+ self.peer_transport_parameters = peer_transport_parameters if isinstance(peer_transport_parameters, TransportParameters) else None
1137
+ selected_alpn = offered.get(ExtensionType.ALPN)
1138
+ if not isinstance(selected_alpn, str) or selected_alpn not in self.alpns:
1139
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'server selected an unexpected ALPN')
1140
+ self.selected_alpn = selected_alpn
1141
+ self.early_data_accepted = bool(offered.get(ExtensionType.EARLY_DATA, False))
1142
+ encoded = message.encode()
1143
+ self._transcript.append(encoded)
1144
+
1145
+ def _handle_certificate_request(self, message: CertificateRequest) -> None:
1146
+ if self._client_certificate_requested:
1147
+ _raise_tls(AlertDescription.UNEXPECTED_MESSAGE, 'received duplicate CertificateRequest')
1148
+ if message.request_context:
1149
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'unexpected non-empty CertificateRequest context during handshake')
1150
+ offered = extension_dict(message.extensions)
1151
+ signature_algorithms = offered.get(ExtensionType.SIGNATURE_ALGORITHMS)
1152
+ if not isinstance(signature_algorithms, tuple) or not signature_algorithms:
1153
+ _raise_tls(AlertDescription.MISSING_EXTENSION, 'server CertificateRequest did not provide signature_algorithms')
1154
+ self._client_certificate_requested = True
1155
+ self._client_certificate_request_context = bytes(message.request_context)
1156
+ self._certificate_request_signature_algorithms = tuple(int(item) for item in signature_algorithms)
1157
+ encoded = message.encode()
1158
+ self._transcript.append(encoded)
1159
+
1160
+ def _handle_server_certificate(self, message: Certificate) -> x509.Certificate:
1161
+ if not message.certificate_list:
1162
+ _raise_tls(AlertDescription.BAD_CERTIFICATE, 'server certificate chain is empty')
1163
+ chain = tuple(entry.cert_data for entry in message.certificate_list)
1164
+ self.peer_certificate_chain_pem = chain
1165
+ encoded = message.encode()
1166
+ self._transcript.append(encoded)
1167
+ return self._load_selected_peer_certificate()
1168
+
1169
+ def _handle_server_certificate_verify(self, message: CertificateVerify) -> None:
1170
+ leaf = self._load_selected_peer_certificate()
1171
+ payload = _certificate_verify_input(_SERVER_CERT_VERIFY_CONTEXT, self._current_transcript_hash())
1172
+ _verify_with_scheme(leaf.public_key(), message.algorithm, message.signature, payload)
1173
+ self._transcript.append(message.encode())
1174
+
1175
+ def _handle_client_certificate(self, message: Certificate) -> None:
1176
+ if not self._client_certificate_requested:
1177
+ _raise_tls(AlertDescription.UNEXPECTED_MESSAGE, 'received an unexpected client Certificate message')
1178
+ if message.request_context != self._client_certificate_request_context:
1179
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'client Certificate request context mismatch')
1180
+ self.peer_certificate_chain_pem = tuple(entry.cert_data for entry in message.certificate_list)
1181
+ self._peer_certificate_present = bool(self.peer_certificate_chain_pem)
1182
+ self._transcript.append(message.encode())
1183
+ if self._peer_certificate_present:
1184
+ self._load_selected_peer_certificate()
1185
+
1186
+ def _handle_client_certificate_verify(self, message: CertificateVerify) -> None:
1187
+ if not self._peer_certificate_present:
1188
+ _raise_tls(AlertDescription.UNEXPECTED_MESSAGE, 'received CertificateVerify without a client certificate')
1189
+ leaf = self._load_selected_peer_certificate()
1190
+ payload = _certificate_verify_input(_CLIENT_CERT_VERIFY_CONTEXT, self._current_transcript_hash())
1191
+ _verify_with_scheme(leaf.public_key(), message.algorithm, message.signature, payload)
1192
+ self._transcript.append(message.encode())
1193
+ self._peer_certificate_verify_received = True
1194
+
1195
+ def _handle_server_finished(self, message: Finished) -> bytes:
1196
+ if self._server_handshake_secret is None:
1197
+ _raise_tls(AlertDescription.INTERNAL_ERROR, 'server handshake secret is unavailable')
1198
+ if not self._key_schedule.verify_finished(message.verify_data, base_key=self._server_handshake_secret, transcript=self._transcript):
1199
+ _raise_tls(AlertDescription.DECRYPT_ERROR, 'server Finished verify_data is invalid')
1200
+ encoded = message.encode()
1201
+ self._transcript.append(encoded)
1202
+ client_ap, server_ap = self._derive_application_secrets()
1203
+ self._set_traffic_secrets(
1204
+ client_handshake_secret=self._client_handshake_secret,
1205
+ server_handshake_secret=self._server_handshake_secret,
1206
+ client_application_secret=client_ap,
1207
+ server_application_secret=server_ap,
1208
+ client_early_secret=getattr(self, '_client_early_secret', None),
1209
+ )
1210
+ outbound = bytearray()
1211
+ if self._client_certificate_requested:
1212
+ certificate = Certificate(
1213
+ request_context=self._client_certificate_request_context,
1214
+ certificate_list=self._certificate_entry_chain() if self._private_key is not None else (),
1215
+ )
1216
+ encoded_certificate = certificate.encode()
1217
+ self._transcript.append(encoded_certificate)
1218
+ outbound.extend(encoded_certificate)
1219
+ if certificate.certificate_list:
1220
+ public_key = self._certificate_chain[0].public_key()
1221
+ selected_scheme = _select_certificate_verify_scheme(
1222
+ self._certificate_request_signature_algorithms or SUPPORTED_SIGNATURE_SCHEMES,
1223
+ public_key,
1224
+ )
1225
+ signature_payload = _certificate_verify_input(_CLIENT_CERT_VERIFY_CONTEXT, self._current_transcript_hash())
1226
+ signature = _sign_with_scheme(self._private_key, selected_scheme, signature_payload)
1227
+ certificate_verify = CertificateVerify(algorithm=selected_scheme, signature=signature)
1228
+ encoded_certificate_verify = certificate_verify.encode()
1229
+ self._transcript.append(encoded_certificate_verify)
1230
+ outbound.extend(encoded_certificate_verify)
1231
+ finished = Finished(verify_data=self._key_schedule.finished_verify_data(self._client_handshake_secret, self._transcript))
1232
+ encoded_finished = finished.encode()
1233
+ self._transcript.append(encoded_finished)
1234
+ outbound.extend(encoded_finished)
1235
+ self._finalize_post_handshake_secrets()
1236
+ self.complete = True
1237
+ self.state = 'complete'
1238
+ return bytes(outbound)
1239
+
1240
+ def _handle_new_session_ticket(self, message: NewSessionTicket) -> None:
1241
+ if self.transport_mode != 'quic':
1242
+ _raise_tls(AlertDescription.UNEXPECTED_MESSAGE, 'received unexpected NewSessionTicket on stream TLS')
1243
+ if self._resumption_master_secret is None or self.selected_alpn is None or self.peer_transport_parameters is None:
1244
+ _raise_tls(AlertDescription.UNEXPECTED_MESSAGE, 'received NewSessionTicket before the handshake completed')
1245
+ offered = extension_dict(message.extensions)
1246
+ max_early_data_size = int(offered.get(ExtensionType.EARLY_DATA, 0) or 0)
1247
+ if max_early_data_size not in {0, QUIC_EARLY_DATA_SENTINEL}:
1248
+ _raise_tls(AlertDescription.ILLEGAL_PARAMETER, 'invalid QUIC early_data sentinel in NewSessionTicket')
1249
+ resumption_secret = self._key_schedule.resumption_psk(self._resumption_master_secret, message.ticket_nonce)
1250
+ self.received_session_ticket = QuicSessionTicket(
1251
+ ticket=message.ticket,
1252
+ resumption_secret=resumption_secret,
1253
+ server_name=self.server_name,
1254
+ alpn=self.selected_alpn,
1255
+ transport_parameters=self.peer_transport_parameters,
1256
+ ticket_age_add=message.ticket_age_add,
1257
+ ticket_nonce=message.ticket_nonce,
1258
+ ticket_lifetime=message.ticket_lifetime,
1259
+ issued_at=_current_time_ms(),
1260
+ cipher_suite=self._selected_cipher_suite,
1261
+ max_early_data_size=max_early_data_size,
1262
+ )
1263
+
1264
+ def receive(self, data: bytes) -> bytes:
1265
+ self._receive_buffer.extend(data)
1266
+ outbound = bytearray()
1267
+ pending_leaf: x509.Certificate | None = None
1268
+ while self._receive_buffer:
1269
+ raw_view = bytes(self._receive_buffer)
1270
+ try:
1271
+ message, consumed = decode_handshake_message(raw_view, 0)
1272
+ except NeedMoreData:
1273
+ break
1274
+ raw_message = raw_view[:consumed]
1275
+ del self._receive_buffer[:consumed]
1276
+ if isinstance(message, KeyUpdate):
1277
+ _raise_tls(AlertDescription.UNEXPECTED_MESSAGE, 'TLS KeyUpdate is not used with QUIC')
1278
+ if self.is_client:
1279
+ if isinstance(message, ServerHello):
1280
+ outbound.extend(self._handle_server_hello(message))
1281
+ continue
1282
+ if isinstance(message, EncryptedExtensions):
1283
+ self._handle_encrypted_extensions(message)
1284
+ continue
1285
+ if isinstance(message, CertificateRequest):
1286
+ self._handle_certificate_request(message)
1287
+ continue
1288
+ if isinstance(message, Certificate):
1289
+ pending_leaf = self._handle_server_certificate(message)
1290
+ continue
1291
+ if isinstance(message, CertificateVerify):
1292
+ if pending_leaf is None:
1293
+ pending_leaf = self._load_selected_peer_certificate()
1294
+ self._handle_server_certificate_verify(message)
1295
+ continue
1296
+ if isinstance(message, Finished):
1297
+ outbound.extend(self._handle_server_finished(message))
1298
+ continue
1299
+ if isinstance(message, NewSessionTicket):
1300
+ self._handle_new_session_ticket(message)
1301
+ continue
1302
+ _raise_tls(AlertDescription.UNEXPECTED_MESSAGE, 'unexpected handshake message received by client')
1303
+ else:
1304
+ if isinstance(message, ClientHello):
1305
+ outbound.extend(self._handle_client_hello(message, raw_message=raw_message))
1306
+ continue
1307
+ if isinstance(message, Certificate):
1308
+ self._handle_client_certificate(message)
1309
+ continue
1310
+ if isinstance(message, CertificateVerify):
1311
+ self._handle_client_certificate_verify(message)
1312
+ continue
1313
+ if isinstance(message, Finished):
1314
+ outbound.extend(self._handle_client_finished(message))
1315
+ continue
1316
+ _raise_tls(AlertDescription.UNEXPECTED_MESSAGE, 'unexpected handshake message received by server')
1317
+ return bytes(outbound)
1318
+
1319
+ def issue_session_ticket(self, *, max_early_data_size: int = 0) -> bytes:
1320
+ if self.transport_mode != 'quic':
1321
+ raise ProtocolError('session tickets are not exposed on the stream TLS path')
1322
+ if not self.complete or self._resumption_master_secret is None or self.selected_alpn is None:
1323
+ raise ProtocolError('handshake must complete before issuing a session ticket')
1324
+ ticket_lifetime = _MAX_TICKET_LIFETIME_SECONDS
1325
+ ticket_age_add = int.from_bytes(os.urandom(4), 'big')
1326
+ ticket_nonce = os.urandom(8)
1327
+ early_data_value = QUIC_EARLY_DATA_SENTINEL if max_early_data_size else 0
1328
+ resumption_secret = self._key_schedule.resumption_psk(self._resumption_master_secret, ticket_nonce)
1329
+ payload = {
1330
+ 'v': 2,
1331
+ 'i': _current_time_ms(),
1332
+ 'l': ticket_lifetime,
1333
+ 'a': ticket_age_add,
1334
+ 'n': _b64(ticket_nonce),
1335
+ 's': self.server_name,
1336
+ 'h': self.selected_alpn,
1337
+ 'p': _b64(self.transport_parameters.to_bytes()),
1338
+ 'c': self._selected_cipher_suite,
1339
+ 'r': _b64(resumption_secret),
1340
+ 'e': early_data_value,
1341
+ }
1342
+ opaque_ticket = _seal_ticket(self._ticket_key, payload)
1343
+ ticket = QuicSessionTicket(
1344
+ ticket=opaque_ticket,
1345
+ resumption_secret=resumption_secret,
1346
+ server_name=self.server_name,
1347
+ alpn=self.selected_alpn,
1348
+ transport_parameters=self.transport_parameters,
1349
+ ticket_age_add=ticket_age_add,
1350
+ ticket_nonce=ticket_nonce,
1351
+ ticket_lifetime=ticket_lifetime,
1352
+ issued_at=int(payload['i']),
1353
+ cipher_suite=self._selected_cipher_suite,
1354
+ max_early_data_size=early_data_value,
1355
+ )
1356
+ self.issued_session_ticket = ticket
1357
+ extensions: list[TlsExtension] = []
1358
+ if early_data_value:
1359
+ extensions.append(TlsExtension(ExtensionType.EARLY_DATA, early_data_value))
1360
+ message = NewSessionTicket(
1361
+ ticket_lifetime=ticket_lifetime,
1362
+ ticket_age_add=ticket_age_add,
1363
+ ticket_nonce=ticket_nonce,
1364
+ ticket=opaque_ticket,
1365
+ extensions=tuple(extensions),
1366
+ )
1367
+ return message.encode()
1368
+
1369
+
1370
+ TLS13_HANDSHAKE_STATE_TABLE: tuple[dict[str, object], ...] = (
1371
+ {
1372
+ 'from': 'client_idle',
1373
+ 'event': 'start() / outbound ClientHello',
1374
+ 'to': 'client_wait_server',
1375
+ 'notes': 'client has emitted ClientHello and waits for the server flight',
1376
+ },
1377
+ {
1378
+ 'from': 'server_idle',
1379
+ 'event': 'ClientHello accepted without HRR',
1380
+ 'to': 'server_wait_client_finished',
1381
+ 'notes': 'server selected parameters and waits for the client Finished',
1382
+ },
1383
+ {
1384
+ 'from': 'server_idle',
1385
+ 'event': 'ClientHello requires HRR',
1386
+ 'to': 'server_wait_client_hello_retry',
1387
+ 'notes': 'server issued HelloRetryRequest and waits for a replacement ClientHello',
1388
+ },
1389
+ {
1390
+ 'from': 'server_wait_client_hello_retry',
1391
+ 'event': 'replacement ClientHello accepted',
1392
+ 'to': 'server_wait_client_finished',
1393
+ 'notes': 'retry path converges on the same post-ServerHello wait state',
1394
+ },
1395
+ {
1396
+ 'from': 'client_wait_server',
1397
+ 'event': 'server flight validated and Finished processed',
1398
+ 'to': 'complete',
1399
+ 'notes': 'client completed certificate verification, Finished, and traffic secret installation',
1400
+ },
1401
+ {
1402
+ 'from': 'server_wait_client_finished',
1403
+ 'event': 'client Finished validated',
1404
+ 'to': 'complete',
1405
+ 'notes': 'server completed handshake and may issue session tickets',
1406
+ },
1407
+ )
1408
+
1409
+
1410
+ def tls13_handshake_state_table() -> tuple[dict[str, object], ...]:
1411
+ return tuple(dict(entry) for entry in TLS13_HANDSHAKE_STATE_TABLE)