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.
- tigrcorn_security/__init__.py +1 -0
- tigrcorn_security/alpn.py +29 -0
- tigrcorn_security/certs.py +10 -0
- tigrcorn_security/policies.py +68 -0
- tigrcorn_security/py.typed +1 -0
- tigrcorn_security/tls.py +583 -0
- tigrcorn_security/tls13/__init__.py +95 -0
- tigrcorn_security/tls13/extensions.py +759 -0
- tigrcorn_security/tls13/handshake.py +1411 -0
- tigrcorn_security/tls13/key_schedule.py +108 -0
- tigrcorn_security/tls13/messages.py +428 -0
- tigrcorn_security/tls13/transcript.py +51 -0
- tigrcorn_security/tls_cipher_policy.py +43 -0
- tigrcorn_security/x509/__init__.py +31 -0
- tigrcorn_security/x509/path.py +1284 -0
- tigrcorn_security-0.3.16.dev5.dist-info/METADATA +239 -0
- tigrcorn_security-0.3.16.dev5.dist-info/RECORD +20 -0
- tigrcorn_security-0.3.16.dev5.dist-info/WHEEL +5 -0
- tigrcorn_security-0.3.16.dev5.dist-info/licenses/LICENSE +163 -0
- tigrcorn_security-0.3.16.dev5.dist-info/top_level.txt +1 -0
|
@@ -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)
|