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,108 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import hmac
5
+ from dataclasses import dataclass
6
+
7
+ from tigrcorn_security.tls13.transcript import HandshakeTranscript
8
+ from tigrcorn_transports.quic.crypto import hkdf_expand_label, hkdf_extract
9
+
10
+
11
+ @dataclass(slots=True)
12
+ class TrafficSecrets:
13
+ client_handshake_traffic_secret: bytes
14
+ server_handshake_traffic_secret: bytes
15
+ client_application_traffic_secret: bytes
16
+ server_application_traffic_secret: bytes
17
+ client_early_traffic_secret: bytes | None = None
18
+ exporter_master_secret: bytes | None = None
19
+ resumption_master_secret: bytes | None = None
20
+
21
+
22
+ class Tls13KeySchedule:
23
+ def __init__(self, *, hash_name: str = 'sha256') -> None:
24
+ self.hash_name = hash_name
25
+ self.hash_length = hashlib.new(hash_name).digest_size
26
+
27
+ def hash_empty(self) -> bytes:
28
+ return hashlib.new(self.hash_name).digest()
29
+
30
+ def transcript_hash(self, transcript: HandshakeTranscript | bytes) -> bytes:
31
+ if isinstance(transcript, HandshakeTranscript):
32
+ return transcript.digest()
33
+ return hashlib.new(self.hash_name, transcript).digest()
34
+
35
+ def extract(self, salt: bytes, ikm: bytes) -> bytes:
36
+ return hkdf_extract(salt, ikm, hash_name=self.hash_name)
37
+
38
+ def expand_label(self, secret: bytes, label: bytes | str, context: bytes = b'', length: int | None = None) -> bytes:
39
+ return hkdf_expand_label(
40
+ secret,
41
+ label,
42
+ context,
43
+ self.hash_length if length is None else length,
44
+ hash_name=self.hash_name,
45
+ )
46
+
47
+ def derive_secret(self, secret: bytes, label: bytes | str, transcript: HandshakeTranscript | bytes | None = None) -> bytes:
48
+ if transcript is None:
49
+ context = self.hash_empty()
50
+ else:
51
+ context = self.transcript_hash(transcript)
52
+ return self.expand_label(secret, label, context, self.hash_length)
53
+
54
+ def zero_secret(self) -> bytes:
55
+ return b'\x00' * self.hash_length
56
+
57
+ def make_early_secret(self, psk: bytes | None) -> bytes:
58
+ ikm = self.zero_secret() if psk is None else psk
59
+ return self.extract(self.zero_secret(), ikm)
60
+
61
+ def make_binder_key(self, early_secret: bytes, *, external: bool = False) -> bytes:
62
+ label = 'ext binder' if external else 'res binder'
63
+ return self.derive_secret(early_secret, label)
64
+
65
+ def client_early_traffic_secret(self, early_secret: bytes, transcript: HandshakeTranscript | bytes) -> bytes:
66
+ return self.derive_secret(early_secret, 'c e traffic', transcript)
67
+
68
+ def handshake_secret(self, early_secret: bytes, shared_secret: bytes) -> bytes:
69
+ derived = self.derive_secret(early_secret, 'derived')
70
+ return self.extract(derived, shared_secret)
71
+
72
+ def handshake_traffic_secrets(self, handshake_secret: bytes, transcript: HandshakeTranscript | bytes) -> tuple[bytes, bytes]:
73
+ return (
74
+ self.derive_secret(handshake_secret, 'c hs traffic', transcript),
75
+ self.derive_secret(handshake_secret, 's hs traffic', transcript),
76
+ )
77
+
78
+ def finished_key(self, base_key: bytes) -> bytes:
79
+ return self.expand_label(base_key, 'finished', b'', self.hash_length)
80
+
81
+ def finished_verify_data(self, base_key: bytes, transcript: HandshakeTranscript | bytes) -> bytes:
82
+ return hmac.new(self.finished_key(base_key), self.transcript_hash(transcript), getattr(hashlib, self.hash_name)).digest()
83
+
84
+ def verify_finished(self, verify_data: bytes, *, base_key: bytes, transcript: HandshakeTranscript | bytes) -> bool:
85
+ expected = self.finished_verify_data(base_key, transcript)
86
+ return hmac.compare_digest(expected, verify_data)
87
+
88
+ def master_secret(self, handshake_secret: bytes) -> bytes:
89
+ derived = self.derive_secret(handshake_secret, 'derived')
90
+ return self.extract(derived, self.zero_secret())
91
+
92
+ def application_traffic_secrets(self, master_secret: bytes, transcript: HandshakeTranscript | bytes) -> tuple[bytes, bytes]:
93
+ return (
94
+ self.derive_secret(master_secret, 'c ap traffic', transcript),
95
+ self.derive_secret(master_secret, 's ap traffic', transcript),
96
+ )
97
+
98
+ def exporter_master_secret(self, master_secret: bytes, transcript: HandshakeTranscript | bytes) -> bytes:
99
+ return self.derive_secret(master_secret, 'exp master', transcript)
100
+
101
+ def resumption_master_secret(self, master_secret: bytes, transcript: HandshakeTranscript | bytes) -> bytes:
102
+ return self.derive_secret(master_secret, 'res master', transcript)
103
+
104
+ def resumption_psk(self, resumption_master_secret: bytes, ticket_nonce: bytes) -> bytes:
105
+ return self.expand_label(resumption_master_secret, 'resumption', ticket_nonce, self.hash_length)
106
+
107
+ def update_application_traffic_secret(self, traffic_secret: bytes) -> bytes:
108
+ return self.expand_label(traffic_secret, 'traffic upd', b'', self.hash_length)
@@ -0,0 +1,428 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass, field
5
+ from enum import IntEnum
6
+ from typing import ClassVar, Sequence
7
+
8
+ from tigrcorn_core.errors import ProtocolError
9
+ from tigrcorn_security.tls13.extensions import (
10
+ ExtensionType,
11
+ TlsExtension,
12
+ TLS_LEGACY_VERSION,
13
+ encode_extensions,
14
+ decode_extensions,
15
+ )
16
+
17
+ HELLO_RETRY_REQUEST_RANDOM = bytes.fromhex(
18
+ 'CF21AD74E59A6111BE1D8C021E65B891'
19
+ 'C2A211167ABB8C5E079E09E2C8A8339C'
20
+ )
21
+
22
+
23
+ class HandshakeType(IntEnum):
24
+ CLIENT_HELLO = 1
25
+ SERVER_HELLO = 2
26
+ NEW_SESSION_TICKET = 4
27
+ END_OF_EARLY_DATA = 5
28
+ ENCRYPTED_EXTENSIONS = 8
29
+ CERTIFICATE = 11
30
+ CERTIFICATE_REQUEST = 13
31
+ CERTIFICATE_VERIFY = 15
32
+ FINISHED = 20
33
+ KEY_UPDATE = 24
34
+ MESSAGE_HASH = 254
35
+
36
+
37
+ class NeedMoreData(ProtocolError):
38
+ pass
39
+
40
+
41
+
42
+ def _u8_vector(payload: bytes) -> bytes:
43
+ if len(payload) > 255:
44
+ raise ValueError('u8 vector too large')
45
+ return bytes([len(payload)]) + payload
46
+
47
+
48
+
49
+ def _u16_vector(payload: bytes) -> bytes:
50
+ if len(payload) > 0xFFFF:
51
+ raise ValueError('u16 vector too large')
52
+ return len(payload).to_bytes(2, 'big') + payload
53
+
54
+
55
+
56
+ def _u24_vector(payload: bytes) -> bytes:
57
+ if len(payload) > 0xFFFFFF:
58
+ raise ValueError('u24 vector too large')
59
+ return len(payload).to_bytes(3, 'big') + payload
60
+
61
+
62
+
63
+ def _read_exact(data: bytes, offset: int, length: int) -> tuple[bytes, int]:
64
+ end = offset + length
65
+ if end > len(data):
66
+ raise NeedMoreData('incomplete TLS handshake payload')
67
+ return data[offset:end], end
68
+
69
+
70
+
71
+ def _read_u8(data: bytes, offset: int) -> tuple[int, int]:
72
+ raw, offset = _read_exact(data, offset, 1)
73
+ return raw[0], offset
74
+
75
+
76
+
77
+ def _read_u16(data: bytes, offset: int) -> tuple[int, int]:
78
+ raw, offset = _read_exact(data, offset, 2)
79
+ return int.from_bytes(raw, 'big'), offset
80
+
81
+
82
+
83
+ def _read_u24(data: bytes, offset: int) -> tuple[int, int]:
84
+ raw, offset = _read_exact(data, offset, 3)
85
+ return int.from_bytes(raw, 'big'), offset
86
+
87
+
88
+
89
+ def _read_u32(data: bytes, offset: int) -> tuple[int, int]:
90
+ raw, offset = _read_exact(data, offset, 4)
91
+ return int.from_bytes(raw, 'big'), offset
92
+
93
+
94
+
95
+ def _read_u8_vector(data: bytes, offset: int) -> tuple[bytes, int]:
96
+ length, offset = _read_u8(data, offset)
97
+ return _read_exact(data, offset, length)
98
+
99
+
100
+
101
+ def _read_u16_vector(data: bytes, offset: int) -> tuple[bytes, int]:
102
+ length, offset = _read_u16(data, offset)
103
+ return _read_exact(data, offset, length)
104
+
105
+
106
+
107
+ def _read_u24_vector(data: bytes, offset: int) -> tuple[bytes, int]:
108
+ length, offset = _read_u24(data, offset)
109
+ return _read_exact(data, offset, length)
110
+
111
+
112
+ @dataclass(slots=True)
113
+ class HandshakeMessage:
114
+ handshake_type: ClassVar[int]
115
+
116
+ def encode_body(self, **kwargs) -> bytes:
117
+ raise NotImplementedError
118
+
119
+ def encode(self, **kwargs) -> bytes:
120
+ body = self.encode_body(**kwargs)
121
+ return bytes([self.handshake_type]) + len(body).to_bytes(3, 'big') + body
122
+
123
+
124
+ @dataclass(slots=True)
125
+ class ClientHello(HandshakeMessage):
126
+ handshake_type: ClassVar[int] = HandshakeType.CLIENT_HELLO
127
+ random: bytes = field(default_factory=lambda: os.urandom(32))
128
+ legacy_session_id: bytes = field(default_factory=lambda: os.urandom(32))
129
+ cipher_suites: tuple[int, ...] = ()
130
+ compression_methods: bytes = b'\x00'
131
+ extensions: tuple[TlsExtension, ...] = ()
132
+ legacy_version: int = TLS_LEGACY_VERSION
133
+
134
+ def encode_body(self, *, message_context: str = 'client_hello', **kwargs) -> bytes:
135
+ if len(self.random) != 32:
136
+ raise ValueError('ClientHello.random must be 32 bytes')
137
+ if len(self.legacy_session_id) > 32:
138
+ raise ValueError('legacy_session_id must be <= 32 bytes')
139
+ cipher_payload = b''.join(cipher_suite.to_bytes(2, 'big') for cipher_suite in self.cipher_suites)
140
+ if len(cipher_payload) < 2:
141
+ raise ValueError('at least one cipher suite is required')
142
+ return (
143
+ self.legacy_version.to_bytes(2, 'big')
144
+ + self.random
145
+ + _u8_vector(self.legacy_session_id)
146
+ + _u16_vector(cipher_payload)
147
+ + _u8_vector(self.compression_methods)
148
+ + encode_extensions(self.extensions, message_context=message_context)
149
+ )
150
+
151
+ @classmethod
152
+ def decode_body(cls, body: bytes) -> 'ClientHello':
153
+ legacy_version, offset = _read_u16(body, 0)
154
+ random, offset = _read_exact(body, offset, 32)
155
+ legacy_session_id, offset = _read_u8_vector(body, offset)
156
+ cipher_suites_raw, offset = _read_u16_vector(body, offset)
157
+ compression_methods, offset = _read_u8_vector(body, offset)
158
+ extensions = decode_extensions(body[offset:], message_context='client_hello')
159
+ if len(cipher_suites_raw) % 2:
160
+ raise ProtocolError('invalid cipher_suites vector in ClientHello')
161
+ cipher_suites = tuple(int.from_bytes(cipher_suites_raw[index:index + 2], 'big') for index in range(0, len(cipher_suites_raw), 2))
162
+ return cls(
163
+ random=random,
164
+ legacy_session_id=legacy_session_id,
165
+ cipher_suites=cipher_suites,
166
+ compression_methods=compression_methods,
167
+ extensions=extensions,
168
+ legacy_version=legacy_version,
169
+ )
170
+
171
+ def with_extensions(self, extensions: Sequence[TlsExtension]) -> 'ClientHello':
172
+ return ClientHello(
173
+ random=self.random,
174
+ legacy_session_id=self.legacy_session_id,
175
+ cipher_suites=self.cipher_suites,
176
+ compression_methods=self.compression_methods,
177
+ extensions=tuple(extensions),
178
+ legacy_version=self.legacy_version,
179
+ )
180
+
181
+
182
+ @dataclass(slots=True)
183
+ class ServerHello(HandshakeMessage):
184
+ handshake_type: ClassVar[int] = HandshakeType.SERVER_HELLO
185
+ random: bytes
186
+ legacy_session_id_echo: bytes
187
+ cipher_suite: int
188
+ extensions: tuple[TlsExtension, ...]
189
+ legacy_version: int = TLS_LEGACY_VERSION
190
+ legacy_compression_method: int = 0
191
+
192
+ def encode_body(self, *, message_context: str = 'server_hello', **kwargs) -> bytes:
193
+ if len(self.random) != 32:
194
+ raise ValueError('ServerHello.random must be 32 bytes')
195
+ return (
196
+ self.legacy_version.to_bytes(2, 'big')
197
+ + self.random
198
+ + _u8_vector(self.legacy_session_id_echo)
199
+ + self.cipher_suite.to_bytes(2, 'big')
200
+ + bytes([self.legacy_compression_method])
201
+ + encode_extensions(self.extensions, message_context=message_context)
202
+ )
203
+
204
+ @property
205
+ def is_hello_retry_request(self) -> bool:
206
+ return self.random == HELLO_RETRY_REQUEST_RANDOM
207
+
208
+ @classmethod
209
+ def decode_body(cls, body: bytes) -> 'ServerHello':
210
+ legacy_version, offset = _read_u16(body, 0)
211
+ random, offset = _read_exact(body, offset, 32)
212
+ legacy_session_id_echo, offset = _read_u8_vector(body, offset)
213
+ cipher_suite, offset = _read_u16(body, offset)
214
+ legacy_compression_method, offset = _read_u8(body, offset)
215
+ context = 'hello_retry_request' if random == HELLO_RETRY_REQUEST_RANDOM else 'server_hello'
216
+ extensions = decode_extensions(body[offset:], message_context=context)
217
+ return cls(
218
+ random=random,
219
+ legacy_session_id_echo=legacy_session_id_echo,
220
+ cipher_suite=cipher_suite,
221
+ extensions=extensions,
222
+ legacy_version=legacy_version,
223
+ legacy_compression_method=legacy_compression_method,
224
+ )
225
+
226
+
227
+ @dataclass(slots=True)
228
+ class EncryptedExtensions(HandshakeMessage):
229
+ handshake_type: ClassVar[int] = HandshakeType.ENCRYPTED_EXTENSIONS
230
+ extensions: tuple[TlsExtension, ...]
231
+
232
+ def encode_body(self, *, message_context: str = 'encrypted_extensions', **kwargs) -> bytes:
233
+ return encode_extensions(self.extensions, message_context=message_context)
234
+
235
+ @classmethod
236
+ def decode_body(cls, body: bytes) -> 'EncryptedExtensions':
237
+ return cls(extensions=decode_extensions(body, message_context='encrypted_extensions'))
238
+
239
+
240
+ @dataclass(slots=True)
241
+ class CertificateRequest(HandshakeMessage):
242
+ handshake_type: ClassVar[int] = HandshakeType.CERTIFICATE_REQUEST
243
+ request_context: bytes = b''
244
+ extensions: tuple[TlsExtension, ...] = ()
245
+
246
+ def encode_body(self, *, message_context: str = 'certificate_request', **kwargs) -> bytes:
247
+ return _u8_vector(self.request_context) + encode_extensions(self.extensions, message_context=message_context)
248
+
249
+ @classmethod
250
+ def decode_body(cls, body: bytes) -> 'CertificateRequest':
251
+ request_context, offset = _read_u8_vector(body, 0)
252
+ return cls(request_context=request_context, extensions=decode_extensions(body[offset:], message_context='certificate_request'))
253
+
254
+
255
+ @dataclass(slots=True)
256
+ class CertificateEntry:
257
+ cert_data: bytes
258
+ extensions: tuple[TlsExtension, ...] = ()
259
+
260
+ def encode(self) -> bytes:
261
+ return _u24_vector(self.cert_data) + encode_extensions(self.extensions, message_context='certificate_entry')
262
+
263
+ @classmethod
264
+ def decode(cls, data: bytes, offset: int) -> tuple['CertificateEntry', int]:
265
+ cert_data, offset = _read_u24_vector(data, offset)
266
+ extensions_raw, offset = _read_u16_vector(data, offset)
267
+ extensions = decode_extensions(len(extensions_raw).to_bytes(2, 'big') + extensions_raw, message_context='certificate_entry')
268
+ return cls(cert_data=cert_data, extensions=extensions), offset
269
+
270
+
271
+ @dataclass(slots=True)
272
+ class Certificate(HandshakeMessage):
273
+ handshake_type: ClassVar[int] = HandshakeType.CERTIFICATE
274
+ request_context: bytes = b''
275
+ certificate_list: tuple[CertificateEntry, ...] = ()
276
+
277
+ def encode_body(self, **kwargs) -> bytes:
278
+ payload = bytearray()
279
+ for entry in self.certificate_list:
280
+ payload.extend(entry.encode())
281
+ return _u8_vector(self.request_context) + _u24_vector(bytes(payload))
282
+
283
+ @classmethod
284
+ def decode_body(cls, body: bytes) -> 'Certificate':
285
+ request_context, offset = _read_u8_vector(body, 0)
286
+ certificate_list_raw, offset = _read_u24_vector(body, offset)
287
+ if offset != len(body):
288
+ raise ProtocolError('invalid Certificate message length')
289
+ inner = 0
290
+ entries: list[CertificateEntry] = []
291
+ while inner < len(certificate_list_raw):
292
+ entry, inner = CertificateEntry.decode(certificate_list_raw, inner)
293
+ entries.append(entry)
294
+ return cls(request_context=request_context, certificate_list=tuple(entries))
295
+
296
+
297
+ @dataclass(slots=True)
298
+ class CertificateVerify(HandshakeMessage):
299
+ handshake_type: ClassVar[int] = HandshakeType.CERTIFICATE_VERIFY
300
+ algorithm: int
301
+ signature: bytes
302
+
303
+ def encode_body(self, **kwargs) -> bytes:
304
+ return self.algorithm.to_bytes(2, 'big') + _u16_vector(self.signature)
305
+
306
+ @classmethod
307
+ def decode_body(cls, body: bytes) -> 'CertificateVerify':
308
+ algorithm, offset = _read_u16(body, 0)
309
+ signature, offset = _read_u16_vector(body, offset)
310
+ if offset != len(body):
311
+ raise ProtocolError('invalid CertificateVerify message')
312
+ return cls(algorithm=algorithm, signature=signature)
313
+
314
+
315
+ @dataclass(slots=True)
316
+ class Finished(HandshakeMessage):
317
+ handshake_type: ClassVar[int] = HandshakeType.FINISHED
318
+ verify_data: bytes
319
+
320
+ def encode_body(self, **kwargs) -> bytes:
321
+ return self.verify_data
322
+
323
+ @classmethod
324
+ def decode_body(cls, body: bytes) -> 'Finished':
325
+ return cls(verify_data=body)
326
+
327
+
328
+ @dataclass(slots=True)
329
+ class NewSessionTicket(HandshakeMessage):
330
+ handshake_type: ClassVar[int] = HandshakeType.NEW_SESSION_TICKET
331
+ ticket_lifetime: int
332
+ ticket_age_add: int
333
+ ticket_nonce: bytes
334
+ ticket: bytes
335
+ extensions: tuple[TlsExtension, ...] = ()
336
+
337
+ def encode_body(self, **kwargs) -> bytes:
338
+ return (
339
+ self.ticket_lifetime.to_bytes(4, 'big')
340
+ + self.ticket_age_add.to_bytes(4, 'big')
341
+ + _u8_vector(self.ticket_nonce)
342
+ + _u16_vector(self.ticket)
343
+ + encode_extensions(self.extensions, message_context='new_session_ticket')
344
+ )
345
+
346
+ @classmethod
347
+ def decode_body(cls, body: bytes) -> 'NewSessionTicket':
348
+ ticket_lifetime, offset = _read_u32(body, 0)
349
+ ticket_age_add, offset = _read_u32(body, offset)
350
+ ticket_nonce, offset = _read_u8_vector(body, offset)
351
+ ticket, offset = _read_u16_vector(body, offset)
352
+ extensions = decode_extensions(body[offset:], message_context='new_session_ticket')
353
+ return cls(
354
+ ticket_lifetime=ticket_lifetime,
355
+ ticket_age_add=ticket_age_add,
356
+ ticket_nonce=ticket_nonce,
357
+ ticket=ticket,
358
+ extensions=extensions,
359
+ )
360
+
361
+
362
+ @dataclass(slots=True)
363
+ class KeyUpdate(HandshakeMessage):
364
+ handshake_type: ClassVar[int] = HandshakeType.KEY_UPDATE
365
+ request_update: int
366
+
367
+ def encode_body(self, **kwargs) -> bytes:
368
+ return bytes([self.request_update])
369
+
370
+ @classmethod
371
+ def decode_body(cls, body: bytes) -> 'KeyUpdate':
372
+ if len(body) != 1:
373
+ raise ProtocolError('invalid KeyUpdate message')
374
+ return cls(request_update=body[0])
375
+
376
+
377
+ @dataclass(slots=True)
378
+ class SyntheticMessageHash(HandshakeMessage):
379
+ handshake_type: ClassVar[int] = HandshakeType.MESSAGE_HASH
380
+ digest: bytes
381
+
382
+ def encode_body(self, **kwargs) -> bytes:
383
+ return self.digest
384
+
385
+
386
+ @dataclass(slots=True)
387
+ class UnknownHandshake(HandshakeMessage):
388
+ handshake_type: int
389
+ body: bytes
390
+
391
+ def encode_body(self, **kwargs) -> bytes:
392
+ return self.body
393
+
394
+
395
+ _HANDSHAKE_DECODERS: dict[int, type[HandshakeMessage]] = {
396
+ HandshakeType.CLIENT_HELLO: ClientHello,
397
+ HandshakeType.SERVER_HELLO: ServerHello,
398
+ HandshakeType.NEW_SESSION_TICKET: NewSessionTicket,
399
+ HandshakeType.ENCRYPTED_EXTENSIONS: EncryptedExtensions,
400
+ HandshakeType.CERTIFICATE_REQUEST: CertificateRequest,
401
+ HandshakeType.CERTIFICATE: Certificate,
402
+ HandshakeType.CERTIFICATE_VERIFY: CertificateVerify,
403
+ HandshakeType.FINISHED: Finished,
404
+ HandshakeType.KEY_UPDATE: KeyUpdate,
405
+ }
406
+
407
+
408
+
409
+ def decode_handshake_message(data: bytes, offset: int = 0) -> tuple[HandshakeMessage, int]:
410
+ handshake_type_raw, next_offset = _read_u8(data, offset)
411
+ body_length, next_offset = _read_u24(data, next_offset)
412
+ body, next_offset = _read_exact(data, next_offset, body_length)
413
+ decoder = _HANDSHAKE_DECODERS.get(handshake_type_raw)
414
+ if decoder is None:
415
+ message: HandshakeMessage = UnknownHandshake(handshake_type=handshake_type_raw, body=body)
416
+ else:
417
+ message = decoder.decode_body(body) # type: ignore[attr-defined]
418
+ return message, next_offset
419
+
420
+
421
+
422
+ def decode_handshake_messages(data: bytes) -> tuple[HandshakeMessage, ...]:
423
+ messages: list[HandshakeMessage] = []
424
+ offset = 0
425
+ while offset < len(data):
426
+ message, offset = decode_handshake_message(data, offset)
427
+ messages.append(message)
428
+ return tuple(messages)
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from dataclasses import dataclass, field
5
+
6
+ from tigrcorn_security.tls13.messages import SyntheticMessageHash
7
+
8
+
9
+ @dataclass(slots=True)
10
+ class HandshakeTranscript:
11
+ hash_name: str = 'sha256'
12
+ _messages: bytearray = field(default_factory=bytearray)
13
+
14
+ @property
15
+ def hash_length(self) -> int:
16
+ return hashlib.new(self.hash_name).digest_size
17
+
18
+ def copy(self) -> 'HandshakeTranscript':
19
+ transcript = HandshakeTranscript(hash_name=self.hash_name)
20
+ transcript._messages.extend(self._messages)
21
+ return transcript
22
+
23
+ def clear(self) -> None:
24
+ self._messages.clear()
25
+
26
+ def append(self, encoded_handshake_message: bytes) -> None:
27
+ self._messages.extend(encoded_handshake_message)
28
+
29
+ def extend(self, encoded_handshake_messages: bytes) -> None:
30
+ self._messages.extend(encoded_handshake_messages)
31
+
32
+ def digest(self) -> bytes:
33
+ hasher = hashlib.new(self.hash_name)
34
+ hasher.update(self._messages)
35
+ return hasher.digest()
36
+
37
+ def digest_with(self, *encoded_handshake_messages: bytes) -> bytes:
38
+ hasher = hashlib.new(self.hash_name)
39
+ hasher.update(self._messages)
40
+ for message in encoded_handshake_messages:
41
+ hasher.update(message)
42
+ return hasher.digest()
43
+
44
+ def as_bytes(self) -> bytes:
45
+ return bytes(self._messages)
46
+
47
+ def reset_with_message_hash(self, encoded_client_hello: bytes) -> None:
48
+ digest = hashlib.new(self.hash_name, encoded_client_hello).digest()
49
+ synthetic = SyntheticMessageHash(digest=digest).encode()
50
+ self._messages.clear()
51
+ self._messages.extend(synthetic)
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from tigrcorn_core.errors import ConfigError
4
+
5
+ CIPHER_TLS_AES_128_GCM_SHA256 = 0x1301
6
+ CIPHER_TLS_AES_256_GCM_SHA384 = 0x1302
7
+
8
+ SUPPORTED_TLS13_CIPHER_SUITES = (
9
+ CIPHER_TLS_AES_256_GCM_SHA384,
10
+ CIPHER_TLS_AES_128_GCM_SHA256,
11
+ )
12
+
13
+ CIPHER_SUITE_NAME_TO_ID = {
14
+ 'TLS_AES_128_GCM_SHA256': CIPHER_TLS_AES_128_GCM_SHA256,
15
+ 'TLS_AES_256_GCM_SHA384': CIPHER_TLS_AES_256_GCM_SHA384,
16
+ }
17
+
18
+
19
+ def tls13_cipher_suite_name(cipher_suite: int) -> str:
20
+ for name, value in CIPHER_SUITE_NAME_TO_ID.items():
21
+ if value == cipher_suite:
22
+ return name
23
+ return f'0x{cipher_suite:04x}'
24
+
25
+
26
+ def parse_tls13_cipher_allowlist(value: str | None) -> tuple[int, ...]:
27
+ if value is None:
28
+ return ()
29
+ tokens = [token.strip() for token in value.replace(',', ':').split(':') if token.strip()]
30
+ if not tokens:
31
+ raise ConfigError('ssl_ciphers must contain at least one TLS 1.3 cipher suite name')
32
+ resolved: list[int] = []
33
+ for token in tokens:
34
+ if token not in CIPHER_SUITE_NAME_TO_ID:
35
+ raise ConfigError(f'unsupported TLS 1.3 cipher suite: {token!r}')
36
+ cipher_suite = CIPHER_SUITE_NAME_TO_ID[token]
37
+ if cipher_suite not in resolved:
38
+ resolved.append(cipher_suite)
39
+ return tuple(resolved)
40
+
41
+
42
+ def format_tls13_cipher_allowlist(cipher_suites: tuple[int, ...] | list[int]) -> str:
43
+ return ':'.join(tls13_cipher_suite_name(cipher_suite) for cipher_suite in cipher_suites)
@@ -0,0 +1,31 @@
1
+ from .path import (
2
+ CertificatePurpose,
3
+ CertificateValidationPolicy,
4
+ RevocationCache,
5
+ RevocationCacheEntry,
6
+ RevocationFetchPolicy,
7
+ RevocationFreshnessPolicy,
8
+ RevocationMaterial,
9
+ RevocationMode,
10
+ VerifiedCertificatePath,
11
+ load_pem_certificates,
12
+ verify_certificate_chain,
13
+ verify_certificate_hostname,
14
+ verify_certificate_validity,
15
+ )
16
+
17
+ __all__ = [
18
+ 'CertificatePurpose',
19
+ 'CertificateValidationPolicy',
20
+ 'RevocationCache',
21
+ 'RevocationCacheEntry',
22
+ 'RevocationFetchPolicy',
23
+ 'RevocationFreshnessPolicy',
24
+ 'RevocationMaterial',
25
+ 'RevocationMode',
26
+ 'VerifiedCertificatePath',
27
+ 'load_pem_certificates',
28
+ 'verify_certificate_chain',
29
+ 'verify_certificate_hostname',
30
+ 'verify_certificate_validity',
31
+ ]