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,300 @@
|
|
|
1
|
+
"""
|
|
2
|
+
certtificate.py
|
|
3
|
+
|
|
4
|
+
Pydantic data models for certificate configuration and metadata.
|
|
5
|
+
|
|
6
|
+
These models act as typed, validated data-transfer objects between the
|
|
7
|
+
application layer and the cryptographic factory. They are intentionally
|
|
8
|
+
free of any cryptographic logic — validation (field constraints, type
|
|
9
|
+
coercion) is Pydantic's responsibility; generation is ``CertificateFactory``'s.
|
|
10
|
+
|
|
11
|
+
Class hierarchy
|
|
12
|
+
---------------
|
|
13
|
+
::
|
|
14
|
+
|
|
15
|
+
BaseModel
|
|
16
|
+
├── CommonNameCertificate — CN field only
|
|
17
|
+
│ └── BaseCertificateDataModel — CN + organization + country
|
|
18
|
+
├── BaseCertificateConfig — key_size, days_valid, valid_from
|
|
19
|
+
│ └── CAConfig — full CA bootstrap config (inherits both branches)
|
|
20
|
+
└── ClientConfig — end-entity cert config (CN + crypto params + SANs)
|
|
21
|
+
|
|
22
|
+
CertificateInfo — read-only metadata extracted from an
|
|
23
|
+
existing CA certificate's Subject field
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import datetime
|
|
29
|
+
|
|
30
|
+
from pydantic import BaseModel, ConfigDict, EmailStr, Field, IPvAnyAddress
|
|
31
|
+
|
|
32
|
+
from tiny_ca.const import CertType
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Reusable base fragments
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CommonNameCertificate(BaseModel):
|
|
40
|
+
"""
|
|
41
|
+
Minimal certificate identity fragment carrying only the Common Name.
|
|
42
|
+
|
|
43
|
+
Used as a mix-in base so that all certificate configuration models share
|
|
44
|
+
a consistent ``common_name`` field with the same default and description.
|
|
45
|
+
|
|
46
|
+
Attributes
|
|
47
|
+
----------
|
|
48
|
+
common_name : str
|
|
49
|
+
The ``CN`` (Common Name) field of the certificate Subject.
|
|
50
|
+
Default: ``"Internal CA"``.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
common_name: str = Field(
|
|
54
|
+
default="Internal CA",
|
|
55
|
+
description="Common Name (CN) for the certificate Subject.",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class BaseCertificateDataModel(CommonNameCertificate):
|
|
60
|
+
"""
|
|
61
|
+
Certificate Subject identity fields shared by all CA configurations.
|
|
62
|
+
|
|
63
|
+
Extends ``CommonNameCertificate`` with *organization* and *country* so
|
|
64
|
+
that all issued end-entity certificates can inherit the issuer's
|
|
65
|
+
organisational identity.
|
|
66
|
+
|
|
67
|
+
Attributes
|
|
68
|
+
----------
|
|
69
|
+
common_name : str
|
|
70
|
+
Inherited from ``CommonNameCertificate``.
|
|
71
|
+
organization : str
|
|
72
|
+
Organization (O) field of the certificate Subject.
|
|
73
|
+
Default: ``"My Company"``.
|
|
74
|
+
country : str
|
|
75
|
+
Two-letter ISO 3166-1 alpha-2 country code (C field).
|
|
76
|
+
Default: ``"UA"``.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
organization: str = Field(
|
|
80
|
+
default="My Company",
|
|
81
|
+
description="Organization (O) field of the certificate Subject.",
|
|
82
|
+
)
|
|
83
|
+
country: str = Field(
|
|
84
|
+
default="UA",
|
|
85
|
+
description="Two-letter ISO 3166-1 alpha-2 country code (C field).",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class BaseCertificateConfig(BaseModel):
|
|
90
|
+
"""
|
|
91
|
+
Cryptographic validity parameters shared by all certificate types.
|
|
92
|
+
|
|
93
|
+
Provides the three fields that control the key strength and the validity
|
|
94
|
+
window of a certificate. Used as a mix-in base to avoid duplication
|
|
95
|
+
between ``CAConfig`` and ``ClientConfig``.
|
|
96
|
+
|
|
97
|
+
Attributes
|
|
98
|
+
----------
|
|
99
|
+
key_size : int
|
|
100
|
+
RSA key length in bits. Use ``2048`` for standard security or
|
|
101
|
+
``4096`` for long-lived CA certificates. Default: ``2048``.
|
|
102
|
+
days_valid : int
|
|
103
|
+
Number of calendar days the certificate should remain valid.
|
|
104
|
+
Default: ``3650`` (≈10 years; appropriate for a root CA).
|
|
105
|
+
valid_from : datetime.datetime | None
|
|
106
|
+
Explicit start of the validity period as a timezone-aware
|
|
107
|
+
``datetime``. When ``None``, the cryptographic layer uses the
|
|
108
|
+
current UTC time at the moment of issuance. Default: ``None``.
|
|
109
|
+
|
|
110
|
+
Notes
|
|
111
|
+
-----
|
|
112
|
+
``model_config = ConfigDict(arbitrary_types_allowed=True)`` is required
|
|
113
|
+
because ``datetime.datetime`` is not a Pydantic-native field type.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
key_size: int = Field(
|
|
117
|
+
default=2048,
|
|
118
|
+
description="RSA key length in bits (2048 or 4096).",
|
|
119
|
+
)
|
|
120
|
+
days_valid: int = Field(
|
|
121
|
+
default=3650,
|
|
122
|
+
description="Certificate validity period in calendar days.",
|
|
123
|
+
)
|
|
124
|
+
valid_from: datetime.datetime | None = Field(
|
|
125
|
+
default=None,
|
|
126
|
+
description=(
|
|
127
|
+
"Explicit validity start as a timezone-aware datetime. "
|
|
128
|
+
"None defaults to the current UTC time at issuance."
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
# Concrete configuration models
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class CAConfig(BaseCertificateConfig, BaseCertificateDataModel):
|
|
141
|
+
"""
|
|
142
|
+
Complete configuration for bootstrapping a self-signed root CA certificate.
|
|
143
|
+
|
|
144
|
+
Combines all Subject identity fields (``BaseCertificateDataModel``) with
|
|
145
|
+
the cryptographic validity parameters (``BaseCertificateConfig``).
|
|
146
|
+
Passed directly to ``CertificateFactory.build_self_signed_ca`` via
|
|
147
|
+
``config.model_dump()``.
|
|
148
|
+
|
|
149
|
+
Inherited attributes
|
|
150
|
+
--------------------
|
|
151
|
+
common_name : str
|
|
152
|
+
CN for the CA. Default: ``"Internal CA"``.
|
|
153
|
+
organization : str
|
|
154
|
+
O field. Default: ``"My Company"``.
|
|
155
|
+
country : str
|
|
156
|
+
C field (ISO 3166-1 alpha-2). Default: ``"UA"``.
|
|
157
|
+
key_size : int
|
|
158
|
+
RSA key size in bits. Default: ``2048``.
|
|
159
|
+
days_valid : int
|
|
160
|
+
Validity in days. Default: ``3650``.
|
|
161
|
+
valid_from : datetime | None
|
|
162
|
+
Explicit validity start; ``None`` uses current UTC. Default: ``None``.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class ClientConfig(CommonNameCertificate, BaseCertificateConfig):
|
|
167
|
+
"""
|
|
168
|
+
Configuration for issuing an end-entity (client or server) certificate.
|
|
169
|
+
|
|
170
|
+
Passed to ``CertificateFactory.issue_certificate`` via
|
|
171
|
+
``config.model_dump(exclude={"name"})``.
|
|
172
|
+
|
|
173
|
+
Attributes
|
|
174
|
+
----------
|
|
175
|
+
common_name : str
|
|
176
|
+
CN for the certificate Subject. Inherited from
|
|
177
|
+
``CommonNameCertificate``. Default: ``"Internal CA"``.
|
|
178
|
+
key_size : int
|
|
179
|
+
RSA key size in bits. Inherited from ``BaseCertificateConfig``.
|
|
180
|
+
Default: ``2048``.
|
|
181
|
+
days_valid : int
|
|
182
|
+
Validity period in calendar days. Default: ``3650``.
|
|
183
|
+
valid_from : datetime | None
|
|
184
|
+
Explicit validity start. Default: ``None``.
|
|
185
|
+
serial_type : CertType
|
|
186
|
+
Certificate category used when encoding the serial number.
|
|
187
|
+
Default: ``CertType.CA``.
|
|
188
|
+
is_client_cert : bool
|
|
189
|
+
When ``True``, ``ClientAuth`` is added to the Extended Key Usage
|
|
190
|
+
extension. Default: ``False``.
|
|
191
|
+
is_server_cert : bool
|
|
192
|
+
When ``True``, ``ServerAuth`` is added to the Extended Key Usage
|
|
193
|
+
extension and the CN is included as a DNS Subject Alternative Name
|
|
194
|
+
(RFC 2818 compliance). Default: ``True``.
|
|
195
|
+
san_dns : list[str] | None
|
|
196
|
+
Additional DNS names for the Subject Alternative Name extension.
|
|
197
|
+
Default: ``None``.
|
|
198
|
+
san_ip : list[IPvAnyAddress] | None
|
|
199
|
+
IP addresses for the Subject Alternative Name extension. Accepts
|
|
200
|
+
both IPv4 and IPv6. Default: ``None``.
|
|
201
|
+
email : EmailStr | None
|
|
202
|
+
Optional email address added as an ``emailAddress`` Subject attribute.
|
|
203
|
+
Must be a valid RFC 5322 address if provided. Default: ``None``.
|
|
204
|
+
name : str | None
|
|
205
|
+
Override for the output file basename used by ``BaseStorage``.
|
|
206
|
+
When ``None``, the storage layer derives the name from *common_name*.
|
|
207
|
+
This field is excluded from ``model_dump`` calls to the factory.
|
|
208
|
+
Default: ``None``.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
serial_type: CertType = Field(
|
|
212
|
+
default=CertType.CA,
|
|
213
|
+
description="Certificate category encoded into the serial number.",
|
|
214
|
+
)
|
|
215
|
+
is_client_cert: bool = Field(
|
|
216
|
+
default=False,
|
|
217
|
+
description="Add ClientAuth to Extended Key Usage when True.",
|
|
218
|
+
)
|
|
219
|
+
is_server_cert: bool = Field(
|
|
220
|
+
default=True,
|
|
221
|
+
description=(
|
|
222
|
+
"Add ServerAuth to Extended Key Usage and include CN as a DNS SAN "
|
|
223
|
+
"when True (required for TLS server certificates per RFC 2818)."
|
|
224
|
+
),
|
|
225
|
+
)
|
|
226
|
+
san_dns: list[str] | None = Field(
|
|
227
|
+
default=None,
|
|
228
|
+
description="Additional DNS names for the Subject Alternative Name extension.",
|
|
229
|
+
)
|
|
230
|
+
san_ip: list[IPvAnyAddress] | None = Field(
|
|
231
|
+
default=None,
|
|
232
|
+
description="IP addresses (IPv4 or IPv6) for the Subject Alternative Name extension.",
|
|
233
|
+
)
|
|
234
|
+
email: EmailStr | None = Field(
|
|
235
|
+
default=None,
|
|
236
|
+
description="Optional email address added as an emailAddress Subject attribute.",
|
|
237
|
+
)
|
|
238
|
+
name: str | None = Field(
|
|
239
|
+
default=None,
|
|
240
|
+
description=(
|
|
241
|
+
"Explicit output file basename for storage artefacts. "
|
|
242
|
+
"Excluded from model_dump calls to the factory. "
|
|
243
|
+
"None causes the storage layer to derive the name from common_name."
|
|
244
|
+
),
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
# Read-only metadata extracted from an existing CA certificate
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class CertificateInfo(BaseModel):
|
|
254
|
+
"""
|
|
255
|
+
Structured metadata extracted from the Subject field of a CA certificate.
|
|
256
|
+
|
|
257
|
+
Populated by ``CAFileLoader._extract_info`` and made available via the
|
|
258
|
+
``ICALoader.base_info`` property. ``CertificateFactory`` uses these
|
|
259
|
+
values to populate the Subject of every end-entity certificate it issues,
|
|
260
|
+
ensuring that all leaf certificates share the same organisational identity
|
|
261
|
+
as the signing CA.
|
|
262
|
+
|
|
263
|
+
All fields are optional (``None``) because not every CA certificate
|
|
264
|
+
includes all Subject attributes.
|
|
265
|
+
|
|
266
|
+
Attributes
|
|
267
|
+
----------
|
|
268
|
+
organization : str | None
|
|
269
|
+
Organization (O) field. Default: ``"My company"``.
|
|
270
|
+
organizational_unit : str | None
|
|
271
|
+
Organizational Unit (OU) field. Default: ``"server"``.
|
|
272
|
+
country : str | None
|
|
273
|
+
Two-letter ISO 3166-1 alpha-2 country code (C field).
|
|
274
|
+
Default: ``"UA"``.
|
|
275
|
+
state : str | None
|
|
276
|
+
State or Province (ST) field. Default: ``None``.
|
|
277
|
+
locality : str | None
|
|
278
|
+
Locality or City (L) field. Default: ``None``.
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
organization: str | None = Field(
|
|
282
|
+
default="My company",
|
|
283
|
+
description="Organization (O) extracted from the CA certificate Subject.",
|
|
284
|
+
)
|
|
285
|
+
organizational_unit: str | None = Field(
|
|
286
|
+
default="server",
|
|
287
|
+
description="Organizational Unit (OU) extracted from the CA certificate Subject.",
|
|
288
|
+
)
|
|
289
|
+
country: str | None = Field(
|
|
290
|
+
default="UA",
|
|
291
|
+
description="Country (C) extracted from the CA certificate Subject.",
|
|
292
|
+
)
|
|
293
|
+
state: str | None = Field(
|
|
294
|
+
default=None,
|
|
295
|
+
description="State or Province (ST) extracted from the CA certificate Subject.",
|
|
296
|
+
)
|
|
297
|
+
locality: str | None = Field(
|
|
298
|
+
default=None,
|
|
299
|
+
description="Locality / City (L) extracted from the CA certificate Subject.",
|
|
300
|
+
)
|
tiny_ca/settings.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from logging import DEBUG, Formatter, StreamHandler, getLogger
|
|
2
|
+
|
|
3
|
+
DEFAULT_LOGGER = getLogger("TinyCA")
|
|
4
|
+
DEFAULT_LOGGER.setLevel(DEBUG)
|
|
5
|
+
_console_handler = StreamHandler()
|
|
6
|
+
_formatter = Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
7
|
+
_console_handler.setFormatter(_formatter)
|
|
8
|
+
DEFAULT_LOGGER.addHandler(_console_handler)
|
|
9
|
+
DT_STR_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Coroutine
|
|
5
|
+
from logging import Logger
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import shutil
|
|
8
|
+
from typing import Any
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
import warnings
|
|
11
|
+
|
|
12
|
+
import aiofiles
|
|
13
|
+
import cryptography
|
|
14
|
+
from cryptography import x509
|
|
15
|
+
from cryptography.hazmat.primitives import serialization
|
|
16
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
17
|
+
|
|
18
|
+
from tiny_ca.exc import FileAlreadyExists
|
|
19
|
+
from tiny_ca.settings import DEFAULT_LOGGER
|
|
20
|
+
|
|
21
|
+
from .base_storage import BaseStorage
|
|
22
|
+
from .const import CryptoObject
|
|
23
|
+
from .local_storage import LocalStorage, _CertSerializer
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AsyncLocalStorage(LocalStorage):
|
|
27
|
+
async def save_certificate( # type: ignore[override]
|
|
28
|
+
self,
|
|
29
|
+
cert: CryptoObject,
|
|
30
|
+
file_name: str,
|
|
31
|
+
cert_path: str | Path | None = None,
|
|
32
|
+
uuid_str: str | None = None,
|
|
33
|
+
encoding: serialization.Encoding | None = None,
|
|
34
|
+
private_format: serialization.PrivateFormat | None = None,
|
|
35
|
+
public_format: serialization.PublicFormat | None = None,
|
|
36
|
+
encryption_algorithm: serialization.KeySerializationEncryption | None = None,
|
|
37
|
+
is_add_uuid: bool = True,
|
|
38
|
+
is_overwrite: bool = False,
|
|
39
|
+
) -> tuple[Path, str | None]:
|
|
40
|
+
"""
|
|
41
|
+
Serialise *cert* and write the result to the local filesystem.
|
|
42
|
+
|
|
43
|
+
Assembles the output path as::
|
|
44
|
+
|
|
45
|
+
<base_folder> / [cert_path/] / [<uuid>/] / <file_name><ext>
|
|
46
|
+
|
|
47
|
+
Where *ext* is determined automatically from the type of *cert*.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
cert : CryptoObject
|
|
52
|
+
Cryptographic object to serialise and persist.
|
|
53
|
+
file_name : str
|
|
54
|
+
Base filename without extension (e.g. ``"ca"``, ``"nginx"``).
|
|
55
|
+
cert_path : str | Path | None
|
|
56
|
+
Optional sub-directory appended after *base_folder*.
|
|
57
|
+
uuid_str : str | None
|
|
58
|
+
Reuse an existing UUID directory by passing the value returned by
|
|
59
|
+
a previous ``save_certificate`` call. ``None`` auto-generates a
|
|
60
|
+
new UUID. Ignored when *is_add_uuid* is ``False``.
|
|
61
|
+
encoding : serialization.Encoding | None
|
|
62
|
+
Encoding override. ``None`` uses *base_encoding*.
|
|
63
|
+
private_format : serialization.PrivateFormat | None
|
|
64
|
+
Private-key format override. ``None`` uses *base_private_format*.
|
|
65
|
+
public_format : serialization.PublicFormat | None
|
|
66
|
+
Public-key format override. ``None`` uses *base_public_format*.
|
|
67
|
+
encryption_algorithm : serialization.KeySerializationEncryption | None
|
|
68
|
+
Private-key encryption override. ``None`` uses
|
|
69
|
+
*base_encryption_algorithm*.
|
|
70
|
+
is_add_uuid : bool
|
|
71
|
+
When ``True`` (default), a UUID subdirectory is inserted.
|
|
72
|
+
Set to ``False`` for singleton files such as CRL that are
|
|
73
|
+
regenerated in-place.
|
|
74
|
+
is_overwrite : bool
|
|
75
|
+
When ``True``, silently replace an existing file.
|
|
76
|
+
When ``False`` (default), raise ``FileAlreadyExists``.
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
tuple[Path, str | None]
|
|
81
|
+
``(absolute_path_to_written_file, uuid_used)``.
|
|
82
|
+
*uuid_used* is ``None`` when *is_add_uuid* is ``False``.
|
|
83
|
+
|
|
84
|
+
Raises
|
|
85
|
+
------
|
|
86
|
+
FileAlreadyExists
|
|
87
|
+
If the computed target path already exists and *is_overwrite*
|
|
88
|
+
is ``False``.
|
|
89
|
+
TypeError
|
|
90
|
+
If *cert* is not a supported cryptographic type.
|
|
91
|
+
"""
|
|
92
|
+
output_dir, effective_uuid = self._resolve_output_dir(
|
|
93
|
+
cert_path=cert_path,
|
|
94
|
+
uuid_str=uuid_str,
|
|
95
|
+
is_add_uuid=is_add_uuid,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
cert_data, extension = await asyncio.to_thread(
|
|
99
|
+
_CertSerializer.serialise,
|
|
100
|
+
cert=cert,
|
|
101
|
+
encoding=encoding or self._base_encoding,
|
|
102
|
+
private_format=private_format or self._base_private_format,
|
|
103
|
+
public_format=public_format or self._base_public_format,
|
|
104
|
+
encryption_algorithm=encryption_algorithm
|
|
105
|
+
or self._base_encryption_algorithm,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
output_path = output_dir / f"{file_name}{extension}"
|
|
109
|
+
await self._write_file(
|
|
110
|
+
path=output_path, data=cert_data, is_overwrite=is_overwrite
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
self._logger.debug(
|
|
114
|
+
"Saved %s → %s (uuid=%s)", type(cert).__name__, output_path, effective_uuid
|
|
115
|
+
)
|
|
116
|
+
return output_path, effective_uuid
|
|
117
|
+
|
|
118
|
+
async def _write_file( # type: ignore[override]
|
|
119
|
+
self, path: Path, data: bytes, is_overwrite: bool
|
|
120
|
+
) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Write *data* to *path*, creating any missing parent directories.
|
|
123
|
+
|
|
124
|
+
Parent directories are created with ``mkdir(parents=True,
|
|
125
|
+
exist_ok=True)`` so the caller never needs to pre-create the
|
|
126
|
+
directory tree manually.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
path : Path
|
|
131
|
+
Absolute target path for the output file.
|
|
132
|
+
data : bytes
|
|
133
|
+
Raw serialised bytes to write.
|
|
134
|
+
is_overwrite : bool
|
|
135
|
+
When ``False``, raise ``FileAlreadyExists`` if *path* already
|
|
136
|
+
exists. When ``True``, the existing file is silently replaced.
|
|
137
|
+
|
|
138
|
+
Raises
|
|
139
|
+
------
|
|
140
|
+
FileAlreadyExists
|
|
141
|
+
If *path* already exists on disk and *is_overwrite* is ``False``.
|
|
142
|
+
"""
|
|
143
|
+
if path.exists() and not is_overwrite:
|
|
144
|
+
raise FileAlreadyExists(path_save_cert=path)
|
|
145
|
+
|
|
146
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
|
|
148
|
+
async with aiofiles.open(path, "wb") as fh:
|
|
149
|
+
await fh.write(data)
|
|
150
|
+
|
|
151
|
+
self._logger.debug("Written %d bytes → %s", len(data), path)
|
|
152
|
+
|
|
153
|
+
async def delete_certificate_folder( # type: ignore[override]
|
|
154
|
+
self,
|
|
155
|
+
uuid_str: str,
|
|
156
|
+
cert_path: str | Path | None = None,
|
|
157
|
+
) -> bool:
|
|
158
|
+
"""
|
|
159
|
+
Recursively remove the directory identified by *uuid_str*.
|
|
160
|
+
|
|
161
|
+
The target path is resolved as::
|
|
162
|
+
|
|
163
|
+
<base_folder> / [cert_path/] / <uuid_str>
|
|
164
|
+
|
|
165
|
+
The operation is idempotent: if the directory does not exist a
|
|
166
|
+
``UserWarning`` is emitted and ``True`` is returned (no action needed).
|
|
167
|
+
If the path exists but is a regular file rather than a directory, a
|
|
168
|
+
``UserWarning`` is emitted and ``True`` is returned (not our directory).
|
|
169
|
+
Only a genuine ``OSError`` during ``shutil.rmtree`` causes ``False``.
|
|
170
|
+
|
|
171
|
+
Parameters
|
|
172
|
+
----------
|
|
173
|
+
uuid_str : str
|
|
174
|
+
UUID sub-directory name to remove.
|
|
175
|
+
cert_path : str | Path | None
|
|
176
|
+
Optional sub-path under *base_folder* containing *uuid_str*.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
bool
|
|
181
|
+
``True`` — directory removed, or path was already absent.
|
|
182
|
+
``False`` — ``OSError`` occurred; check logs for details.
|
|
183
|
+
|
|
184
|
+
Warns
|
|
185
|
+
-----
|
|
186
|
+
UserWarning
|
|
187
|
+
If the target path does not exist or is not a directory.
|
|
188
|
+
"""
|
|
189
|
+
target = self._base_folder
|
|
190
|
+
if cert_path:
|
|
191
|
+
target = target / cert_path
|
|
192
|
+
target = target / uuid_str
|
|
193
|
+
|
|
194
|
+
if not target.exists():
|
|
195
|
+
warnings.warn(
|
|
196
|
+
f"Certificate folder not deleted — path does not exist: {target}",
|
|
197
|
+
UserWarning,
|
|
198
|
+
stacklevel=2,
|
|
199
|
+
)
|
|
200
|
+
return True
|
|
201
|
+
|
|
202
|
+
if not target.is_dir():
|
|
203
|
+
warnings.warn(
|
|
204
|
+
f"Certificate folder not deleted — path is not a directory: {target}",
|
|
205
|
+
UserWarning,
|
|
206
|
+
stacklevel=2,
|
|
207
|
+
)
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
await asyncio.to_thread(shutil.rmtree, path=target)
|
|
212
|
+
self._logger.info("Deleted certificate folder: %s", target)
|
|
213
|
+
return True
|
|
214
|
+
except OSError as exc:
|
|
215
|
+
self._logger.error(
|
|
216
|
+
"Failed to delete certificate folder %s: %s", target, exc
|
|
217
|
+
)
|
|
218
|
+
return False
|