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,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
|
+
]
|