tiny-ca 0.1.0__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.
- tiny_ca/__init__.py +41 -0
- tiny_ca/ca_factory/__init__.py +15 -0
- tiny_ca/ca_factory/factory.py +658 -0
- tiny_ca/ca_factory/utils/__init__.py +10 -0
- tiny_ca/ca_factory/utils/afile_loader.py +157 -0
- tiny_ca/ca_factory/utils/file_loader.py +290 -0
- tiny_ca/ca_factory/utils/life_time.py +199 -0
- tiny_ca/ca_factory/utils/serial.py +151 -0
- tiny_ca/const.py +81 -0
- tiny_ca/db/__init__.py +14 -0
- tiny_ca/db/async_db_manager.py +358 -0
- tiny_ca/db/base_db.py +173 -0
- tiny_ca/db/const.py +92 -0
- tiny_ca/db/models.py +144 -0
- tiny_ca/db/sync_db_manager.py +415 -0
- tiny_ca/exc.py +103 -0
- tiny_ca/managers/__init__.py +3 -0
- tiny_ca/managers/async_lifecycle_manager.py +702 -0
- tiny_ca/managers/sync_lifecycle_manager.py +722 -0
- tiny_ca/models/__init__.py +3 -0
- tiny_ca/models/certtificate.py +300 -0
- tiny_ca/settings.py +9 -0
- tiny_ca/storage/__init__.py +4 -0
- tiny_ca/storage/async_local_storage.py +218 -0
- tiny_ca/storage/base_storage.py +166 -0
- tiny_ca/storage/const.py +39 -0
- tiny_ca/storage/local_storage.py +448 -0
- tiny_ca/utils/__init__.py +0 -0
- tiny_ca/utils/serial_generator.py +530 -0
- tiny_ca-0.1.0.dist-info/METADATA +430 -0
- tiny_ca-0.1.0.dist-info/RECORD +33 -0
- tiny_ca-0.1.0.dist-info/WHEEL +5 -0
- tiny_ca-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
"""
|
|
2
|
+
serial_generator.py
|
|
3
|
+
|
|
4
|
+
X.509 certificate serial number generation and parsing utilities.
|
|
5
|
+
|
|
6
|
+
Two implementations are provided:
|
|
7
|
+
|
|
8
|
+
- ``SerialGenerator`` — stateful generator that maintains an internal
|
|
9
|
+
mapping between serial numbers and their source
|
|
10
|
+
identifiers (int or str). Suitable when an
|
|
11
|
+
authoritative in-process registry is acceptable.
|
|
12
|
+
|
|
13
|
+
- ``SerialWithEncoding`` — stateless generator that encodes a short name
|
|
14
|
+
prefix and a random UUID fragment into a single
|
|
15
|
+
integer. No shared state; safe for concurrent use.
|
|
16
|
+
|
|
17
|
+
SOLID compliance
|
|
18
|
+
----------------
|
|
19
|
+
SRP : Each class has a single reason to change.
|
|
20
|
+
``SerialGenerator`` → manages serial ↔ id mapping.
|
|
21
|
+
``SerialWithEncoding`` → encodes/decodes name + random bits.
|
|
22
|
+
``_PrefixRegistry`` → owns the CertType ↔ hex-prefix mapping.
|
|
23
|
+
OCP : New ``CertType`` variants require only a new entry in ``_PrefixRegistry``;
|
|
24
|
+
no existing logic changes.
|
|
25
|
+
LSP : Both generators expose compatible ``generate`` / ``parse`` signatures,
|
|
26
|
+
so they can be swapped behind an ``ISerialGenerator`` Protocol.
|
|
27
|
+
ISP : ``ISerialGenerator`` declares only the two methods callers actually need.
|
|
28
|
+
DIP : ``CertificateFactory`` (and other consumers) depend on ``ISerialGenerator``,
|
|
29
|
+
not on concrete classes.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from typing import Protocol, runtime_checkable
|
|
35
|
+
import uuid
|
|
36
|
+
|
|
37
|
+
from tiny_ca.const import CertType
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# ISP / DIP: minimal interface that callers depend on
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@runtime_checkable
|
|
45
|
+
class ISerialGenerator(Protocol):
|
|
46
|
+
"""
|
|
47
|
+
Minimal contract for serial-number generators.
|
|
48
|
+
|
|
49
|
+
Any class that exposes ``generate`` and ``parse`` with compatible
|
|
50
|
+
signatures satisfies this Protocol and can be injected wherever
|
|
51
|
+
certificate serial numbers are needed.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def generate(name: str, serial_type: CertType) -> int:
|
|
56
|
+
"""
|
|
57
|
+
Generate a unique serial number for the given name and certificate type.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
name : str
|
|
62
|
+
Human-readable identifier (e.g. CN or service name).
|
|
63
|
+
serial_type : CertType
|
|
64
|
+
Category of the certificate (CA, USER, SERVICE, …).
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
int
|
|
69
|
+
A non-negative integer suitable for use as an X.509 serial number.
|
|
70
|
+
"""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def parse(serial: int) -> tuple[CertType, str]:
|
|
75
|
+
"""
|
|
76
|
+
Decode a previously generated serial number.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
serial : int
|
|
81
|
+
Integer serial number produced by ``generate``.
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
tuple[CertType, str]
|
|
86
|
+
``(cert_type, name)`` recovered from the serial.
|
|
87
|
+
"""
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# OCP: centralised prefix registry — add new types here only
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class _PrefixRegistry:
|
|
97
|
+
"""
|
|
98
|
+
Bidirectional mapping between :class:`CertType` and 2-byte hex prefixes.
|
|
99
|
+
|
|
100
|
+
The prefixes are chosen to be human-readable in hex dumps:
|
|
101
|
+
|
|
102
|
+
========== ======= ========
|
|
103
|
+
CertType Prefix ASCII
|
|
104
|
+
========== ======= ========
|
|
105
|
+
CA 0x4341 "CA"
|
|
106
|
+
USER 0x5553 "US"
|
|
107
|
+
SERVICE 0x5356 "SV"
|
|
108
|
+
DEVICE 0x4445 "DE"
|
|
109
|
+
INTERNAL 0x494E "IN"
|
|
110
|
+
========== ======= ========
|
|
111
|
+
|
|
112
|
+
Class is intentionally non-instantiable (all attributes are class-level).
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
#: ``CertType`` → 2-byte integer prefix (big-endian ASCII representation).
|
|
116
|
+
TYPE_TO_PREFIX: dict[CertType, int] = {
|
|
117
|
+
CertType.CA: 0x4341, # "CA"
|
|
118
|
+
CertType.USER: 0x5553, # "US"
|
|
119
|
+
CertType.SERVICE: 0x5356, # "SV"
|
|
120
|
+
CertType.DEVICE: 0x4445, # "DE"
|
|
121
|
+
CertType.INTERNAL: 0x494E, # "IN"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#: Reverse mapping: 2-byte prefix → ``CertType``.
|
|
125
|
+
PREFIX_TO_TYPE: dict[int, CertType] = {v: k for k, v in TYPE_TO_PREFIX.items()}
|
|
126
|
+
|
|
127
|
+
def __init_subclass__(cls, **kwargs: object) -> None: # pragma: no cover
|
|
128
|
+
raise TypeError("_PrefixRegistry is not meant to be subclassed.")
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def prefix_for(cls, cert_type: CertType) -> int:
|
|
132
|
+
"""
|
|
133
|
+
Return the numeric prefix for *cert_type*.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
cert_type : CertType
|
|
138
|
+
The certificate category to look up.
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
int
|
|
143
|
+
2-byte integer prefix.
|
|
144
|
+
|
|
145
|
+
Raises
|
|
146
|
+
------
|
|
147
|
+
KeyError
|
|
148
|
+
If *cert_type* has no registered prefix.
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
return cls.TYPE_TO_PREFIX[cert_type]
|
|
152
|
+
except KeyError:
|
|
153
|
+
raise KeyError(
|
|
154
|
+
f"No prefix registered for CertType.{cert_type.name}. "
|
|
155
|
+
"Add it to _PrefixRegistry.TYPE_TO_PREFIX."
|
|
156
|
+
) from None
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def type_for(cls, prefix: int) -> CertType | None:
|
|
160
|
+
"""
|
|
161
|
+
Return the :class:`CertType` associated with *prefix*, or ``None``.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
prefix : int
|
|
166
|
+
2-byte integer prefix extracted from a serial number.
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
CertType | None
|
|
171
|
+
Matching certificate type, or ``None`` if the prefix is unknown.
|
|
172
|
+
"""
|
|
173
|
+
return cls.PREFIX_TO_TYPE.get(prefix)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
# SRP: stateful serial generator with id ↔ serial mapping
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class SerialGenerator:
|
|
182
|
+
"""
|
|
183
|
+
Stateful serial-number generator with a bidirectional id ↔ serial registry.
|
|
184
|
+
|
|
185
|
+
Serial number layout (64-bit integer)
|
|
186
|
+
::
|
|
187
|
+
|
|
188
|
+
[ 16-bit prefix ][ 48-bit data ]
|
|
189
|
+
|
|
190
|
+
- **prefix** — 2-byte ASCII code derived from :class:`CertType`
|
|
191
|
+
(see :class:`_PrefixRegistry`).
|
|
192
|
+
- **data** — for ``int`` inputs: the value itself (truncated to 48 bits);
|
|
193
|
+
for ``str`` inputs: an auto-incrementing counter per type.
|
|
194
|
+
|
|
195
|
+
The mapping between serial numbers and their original identifiers is kept
|
|
196
|
+
in instance-level dictionaries, making this class unsuitable for concurrent
|
|
197
|
+
multi-process use without external synchronisation.
|
|
198
|
+
|
|
199
|
+
Attributes
|
|
200
|
+
----------
|
|
201
|
+
_id_map : dict[int, int | str]
|
|
202
|
+
Maps ``serial`` → original identifier (int or str).
|
|
203
|
+
_name_map : dict[str, int]
|
|
204
|
+
Maps ``"<cert_type_value>_<name>"`` → serial; populated for string ids.
|
|
205
|
+
_last_serial : dict[CertType, int]
|
|
206
|
+
Per-type auto-increment counter for string-based identifiers.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
#: Bit-width reserved for the data portion of the serial.
|
|
210
|
+
_DATA_BITS: int = 48
|
|
211
|
+
|
|
212
|
+
#: Bitmask for the data portion: ``(1 << 48) - 1``.
|
|
213
|
+
_DATA_MASK: int = (1 << _DATA_BITS) - 1
|
|
214
|
+
|
|
215
|
+
def __init__(self) -> None:
|
|
216
|
+
self._id_map: dict[int, int | str] = {}
|
|
217
|
+
self._name_map: dict[str, int] = {}
|
|
218
|
+
self._last_serial: dict[CertType, int] = {}
|
|
219
|
+
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
# ISerialGenerator interface
|
|
222
|
+
# ------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
def generate(self, id_value: int | str, serial_type: CertType) -> int:
|
|
225
|
+
"""
|
|
226
|
+
Generate a serial number for *id_value* and register the mapping.
|
|
227
|
+
|
|
228
|
+
For **integer** inputs the value is embedded directly into the data
|
|
229
|
+
field (truncated to 48 bits). For **string** inputs a per-type
|
|
230
|
+
auto-increment counter is used and the original string is stored in
|
|
231
|
+
``_id_map`` for later retrieval by :meth:`parse`.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
id_value : int | str
|
|
236
|
+
Source identifier. Integers are encoded directly; strings trigger
|
|
237
|
+
the auto-increment path.
|
|
238
|
+
serial_type : CertType
|
|
239
|
+
Certificate category; determines the 2-byte prefix.
|
|
240
|
+
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
int
|
|
244
|
+
64-bit serial number: ``(prefix << 48) | data``.
|
|
245
|
+
|
|
246
|
+
Raises
|
|
247
|
+
------
|
|
248
|
+
KeyError
|
|
249
|
+
If *serial_type* has no registered prefix.
|
|
250
|
+
"""
|
|
251
|
+
prefix = _PrefixRegistry.prefix_for(serial_type)
|
|
252
|
+
|
|
253
|
+
if isinstance(id_value, int):
|
|
254
|
+
data = id_value & self._DATA_MASK
|
|
255
|
+
serial = (prefix << self._DATA_BITS) | data
|
|
256
|
+
self._id_map[serial] = id_value
|
|
257
|
+
else:
|
|
258
|
+
counter = self._last_serial.get(serial_type, 1)
|
|
259
|
+
data = counter & self._DATA_MASK
|
|
260
|
+
serial = (prefix << self._DATA_BITS) | data
|
|
261
|
+
|
|
262
|
+
self._id_map[serial] = id_value
|
|
263
|
+
self._name_map[f"{serial_type.value}_{id_value}"] = serial
|
|
264
|
+
self._last_serial[serial_type] = counter + 1
|
|
265
|
+
|
|
266
|
+
return serial
|
|
267
|
+
|
|
268
|
+
def parse(self, serial: int) -> tuple[CertType | None, int | str | None]:
|
|
269
|
+
"""
|
|
270
|
+
Decode a serial number back to its type and original identifier.
|
|
271
|
+
|
|
272
|
+
For ``CertType.USER`` the numeric data portion is returned directly.
|
|
273
|
+
For all other types the original identifier is looked up from the
|
|
274
|
+
internal registry; ``None`` is returned if not found.
|
|
275
|
+
|
|
276
|
+
Parameters
|
|
277
|
+
----------
|
|
278
|
+
serial : int
|
|
279
|
+
64-bit serial number previously produced by :meth:`generate`.
|
|
280
|
+
|
|
281
|
+
Returns
|
|
282
|
+
-------
|
|
283
|
+
tuple[CertType | None, int | str | None]
|
|
284
|
+
``(cert_type, original_id)``. Both members may be ``None`` if
|
|
285
|
+
the serial was not generated by this instance.
|
|
286
|
+
"""
|
|
287
|
+
prefix = serial >> self._DATA_BITS
|
|
288
|
+
data = serial & self._DATA_MASK
|
|
289
|
+
cert_type = _PrefixRegistry.type_for(prefix)
|
|
290
|
+
|
|
291
|
+
if cert_type == CertType.USER:
|
|
292
|
+
return cert_type, data
|
|
293
|
+
|
|
294
|
+
return cert_type, self._id_map.get(serial)
|
|
295
|
+
|
|
296
|
+
def get_serial_by_name(self, name: str, serial_type: CertType) -> int | None:
|
|
297
|
+
"""
|
|
298
|
+
Look up the serial number previously assigned to a string identifier.
|
|
299
|
+
|
|
300
|
+
Parameters
|
|
301
|
+
----------
|
|
302
|
+
name : str
|
|
303
|
+
Original string identifier passed to :meth:`generate`.
|
|
304
|
+
serial_type : CertType
|
|
305
|
+
Certificate type under which the name was registered.
|
|
306
|
+
|
|
307
|
+
Returns
|
|
308
|
+
-------
|
|
309
|
+
int | None
|
|
310
|
+
Registered serial number, or ``None`` if not found.
|
|
311
|
+
"""
|
|
312
|
+
return self._name_map.get(f"{serial_type.value}_{name}")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ---------------------------------------------------------------------------
|
|
316
|
+
# SRP: stateless serial generator with name encoding + UUID randomness
|
|
317
|
+
# ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class SerialWithEncoding:
|
|
321
|
+
"""
|
|
322
|
+
Stateless serial-number generator that encodes a short name prefix and
|
|
323
|
+
a UUID-derived random fragment into a single integer.
|
|
324
|
+
|
|
325
|
+
Serial number layout
|
|
326
|
+
::
|
|
327
|
+
|
|
328
|
+
[ 16-bit prefix ][ 80-bit encoded name ][ 64-bit random ]
|
|
329
|
+
|
|
330
|
+
Total width: **160 bits** (well within Python's arbitrary-precision int;
|
|
331
|
+
X.509 allows up to 20 bytes / 160 bits per RFC 5280 §4.1.2.2).
|
|
332
|
+
|
|
333
|
+
- **prefix** — 2-byte ASCII code from :class:`_PrefixRegistry`.
|
|
334
|
+
- **encoded name** — up to 4 ASCII characters packed into 32 bits
|
|
335
|
+
(little-endian byte order, zero-padded).
|
|
336
|
+
- **random** — lower 64 bits of a fresh ``uuid.uuid4()`` ensuring
|
|
337
|
+
global uniqueness without shared state.
|
|
338
|
+
|
|
339
|
+
Because no mutable state is kept, this class is safe to use from multiple
|
|
340
|
+
threads or processes simultaneously.
|
|
341
|
+
|
|
342
|
+
Class Attributes
|
|
343
|
+
----------------
|
|
344
|
+
RANDOM_BITS : int
|
|
345
|
+
Number of bits reserved for the random (UUID) portion. Default: ``64``.
|
|
346
|
+
NAME_BITS : int
|
|
347
|
+
Number of bits reserved for the encoded name portion. Default: ``32``
|
|
348
|
+
(4 bytes × 8 bits).
|
|
349
|
+
MAX_NAME_LENGTH : int
|
|
350
|
+
Maximum number of ASCII characters that can be encoded. Default: ``4``.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
#: Bit-width of the random (UUID) segment.
|
|
354
|
+
RANDOM_BITS: int = 64
|
|
355
|
+
|
|
356
|
+
#: Bit-width of the encoded-name segment (10 ASCII chars × 8 bits).
|
|
357
|
+
NAME_BITS: int = 80
|
|
358
|
+
|
|
359
|
+
#: Maximum number of characters accepted by :meth:`_encode_name`.
|
|
360
|
+
MAX_NAME_LENGTH: int = 10
|
|
361
|
+
|
|
362
|
+
# ------------------------------------------------------------------
|
|
363
|
+
# ISerialGenerator interface
|
|
364
|
+
# ------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
@classmethod
|
|
367
|
+
def generate(cls, name: str, serial_type: CertType) -> int:
|
|
368
|
+
"""
|
|
369
|
+
Generate a globally unique serial number for *name* and *serial_type*.
|
|
370
|
+
|
|
371
|
+
Only the first :attr:`MAX_NAME_LENGTH` characters of *name* are encoded;
|
|
372
|
+
uniqueness is guaranteed by the UUID random segment, not by the name.
|
|
373
|
+
|
|
374
|
+
Parameters
|
|
375
|
+
----------
|
|
376
|
+
name : str
|
|
377
|
+
Human-readable identifier. Only the first 4 ASCII characters are
|
|
378
|
+
embedded; the remainder is ignored (not hashed or truncated with loss).
|
|
379
|
+
serial_type : CertType
|
|
380
|
+
Certificate category; determines the 2-byte prefix.
|
|
381
|
+
|
|
382
|
+
Returns
|
|
383
|
+
-------
|
|
384
|
+
int
|
|
385
|
+
Non-negative integer serial suitable for X.509 certificates.
|
|
386
|
+
|
|
387
|
+
Raises
|
|
388
|
+
------
|
|
389
|
+
KeyError
|
|
390
|
+
If *serial_type* has no registered prefix.
|
|
391
|
+
|
|
392
|
+
Examples
|
|
393
|
+
--------
|
|
394
|
+
>>> serial = SerialWithEncoding.generate("nginx", CertType.SERVICE)
|
|
395
|
+
>>> cert_type, name = SerialWithEncoding.parse(serial)
|
|
396
|
+
>>> assert cert_type == CertType.SERVICE
|
|
397
|
+
>>> assert name == "ngin" # only first 4 chars are stored
|
|
398
|
+
"""
|
|
399
|
+
prefix: int = _PrefixRegistry.prefix_for(serial_type)
|
|
400
|
+
encoded_name: int = cls._encode_name(name[: cls.MAX_NAME_LENGTH])
|
|
401
|
+
random_part: int = uuid.uuid4().int & cls._random_mask()
|
|
402
|
+
|
|
403
|
+
serial: int = (
|
|
404
|
+
(prefix << (cls.NAME_BITS + cls.RANDOM_BITS))
|
|
405
|
+
| (encoded_name << cls.RANDOM_BITS)
|
|
406
|
+
| random_part
|
|
407
|
+
)
|
|
408
|
+
return serial
|
|
409
|
+
|
|
410
|
+
@classmethod
|
|
411
|
+
def parse(cls, serial: int) -> tuple[CertType | None, str]:
|
|
412
|
+
"""
|
|
413
|
+
Decode a serial number produced by :meth:`generate`.
|
|
414
|
+
|
|
415
|
+
Parameters
|
|
416
|
+
----------
|
|
417
|
+
serial : int
|
|
418
|
+
Integer serial number to decode.
|
|
419
|
+
|
|
420
|
+
Returns
|
|
421
|
+
-------
|
|
422
|
+
tuple[CertType | None, str]
|
|
423
|
+
``(cert_type, name_prefix)`` where *name_prefix* is the up-to-4-char
|
|
424
|
+
string recovered from the encoded-name segment.
|
|
425
|
+
*cert_type* is ``None`` if the prefix is unrecognised.
|
|
426
|
+
|
|
427
|
+
Examples
|
|
428
|
+
--------
|
|
429
|
+
>>> serial = SerialWithEncoding.generate("ca-root", CertType.CA)
|
|
430
|
+
>>> cert_type, name = SerialWithEncoding.parse(serial)
|
|
431
|
+
>>> assert cert_type == CertType.CA
|
|
432
|
+
>>> assert name == "ca-r"
|
|
433
|
+
"""
|
|
434
|
+
random_part: int = serial & cls._random_mask() # noqa: F841 — kept for clarity
|
|
435
|
+
encoded_name: int = (serial >> cls.RANDOM_BITS) & cls._name_mask()
|
|
436
|
+
prefix: int = serial >> (cls.RANDOM_BITS + cls.NAME_BITS)
|
|
437
|
+
|
|
438
|
+
cert_type: CertType | None = _PrefixRegistry.type_for(prefix)
|
|
439
|
+
name: str = cls._decode_name(encoded_name, cls.MAX_NAME_LENGTH)
|
|
440
|
+
|
|
441
|
+
return cert_type, name
|
|
442
|
+
|
|
443
|
+
# ------------------------------------------------------------------
|
|
444
|
+
# Private encoding helpers
|
|
445
|
+
# ------------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
@classmethod
|
|
448
|
+
def _encode_name(cls, text: str) -> int:
|
|
449
|
+
"""
|
|
450
|
+
Pack up to :attr:`MAX_NAME_LENGTH` ASCII characters into a single integer.
|
|
451
|
+
|
|
452
|
+
Characters are stored in **little-endian** byte order:
|
|
453
|
+
``result |= ord(char) << (8 * position)``.
|
|
454
|
+
|
|
455
|
+
Parameters
|
|
456
|
+
----------
|
|
457
|
+
text : str
|
|
458
|
+
ASCII string of length ≤ :attr:`MAX_NAME_LENGTH`.
|
|
459
|
+
|
|
460
|
+
Returns
|
|
461
|
+
-------
|
|
462
|
+
int
|
|
463
|
+
Non-negative integer with characters packed into the lower bytes.
|
|
464
|
+
|
|
465
|
+
Raises
|
|
466
|
+
------
|
|
467
|
+
ValueError
|
|
468
|
+
If *text* is longer than :attr:`MAX_NAME_LENGTH`.
|
|
469
|
+
"""
|
|
470
|
+
if len(text) > cls.MAX_NAME_LENGTH:
|
|
471
|
+
raise ValueError(
|
|
472
|
+
f"Name too long: {len(text)} chars > {cls.MAX_NAME_LENGTH} max. "
|
|
473
|
+
f"Truncate before calling or increase MAX_NAME_LENGTH."
|
|
474
|
+
)
|
|
475
|
+
result: int = 0
|
|
476
|
+
for position, char in enumerate(text):
|
|
477
|
+
result |= ord(char) << (8 * position)
|
|
478
|
+
return result
|
|
479
|
+
|
|
480
|
+
@classmethod
|
|
481
|
+
def _decode_name(cls, value: int, length: int) -> str:
|
|
482
|
+
"""
|
|
483
|
+
Unpack a little-endian-encoded integer back into an ASCII string.
|
|
484
|
+
|
|
485
|
+
Null bytes (``0x00``) terminate the string early, so zero-padding
|
|
486
|
+
introduced by :meth:`_encode_name` is stripped automatically.
|
|
487
|
+
|
|
488
|
+
Parameters
|
|
489
|
+
----------
|
|
490
|
+
value : int
|
|
491
|
+
Integer produced by :meth:`_encode_name`.
|
|
492
|
+
length : int
|
|
493
|
+
Maximum number of characters to attempt to decode.
|
|
494
|
+
|
|
495
|
+
Returns
|
|
496
|
+
-------
|
|
497
|
+
str
|
|
498
|
+
Decoded ASCII string with trailing null characters removed.
|
|
499
|
+
"""
|
|
500
|
+
chars: list[str] = []
|
|
501
|
+
for position in range(length):
|
|
502
|
+
byte: int = (value >> (8 * position)) & 0xFF
|
|
503
|
+
if byte == 0:
|
|
504
|
+
break
|
|
505
|
+
chars.append(chr(byte))
|
|
506
|
+
return "".join(chars)
|
|
507
|
+
|
|
508
|
+
@classmethod
|
|
509
|
+
def _random_mask(cls) -> int:
|
|
510
|
+
"""
|
|
511
|
+
Bitmask for the random segment: ``(1 << RANDOM_BITS) - 1``.
|
|
512
|
+
|
|
513
|
+
Returns
|
|
514
|
+
-------
|
|
515
|
+
int
|
|
516
|
+
Integer with the lower :attr:`RANDOM_BITS` set to 1.
|
|
517
|
+
"""
|
|
518
|
+
return (1 << cls.RANDOM_BITS) - 1
|
|
519
|
+
|
|
520
|
+
@classmethod
|
|
521
|
+
def _name_mask(cls) -> int:
|
|
522
|
+
"""
|
|
523
|
+
Bitmask for the name segment: ``(1 << NAME_BITS) - 1``.
|
|
524
|
+
|
|
525
|
+
Returns
|
|
526
|
+
-------
|
|
527
|
+
int
|
|
528
|
+
Integer with the lower :attr:`NAME_BITS` set to 1.
|
|
529
|
+
"""
|
|
530
|
+
return (1 << cls.NAME_BITS) - 1
|