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,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
base_storage.py
|
|
3
|
+
|
|
4
|
+
Abstract base class that defines the storage contract for certificate artefacts.
|
|
5
|
+
|
|
6
|
+
Any class that inherits from ``BaseStorage`` and implements both abstract methods
|
|
7
|
+
can be used wherever the application expects a storage backend, without requiring
|
|
8
|
+
any changes to the calling code (LSP / DIP).
|
|
9
|
+
|
|
10
|
+
SOLID notes
|
|
11
|
+
-----------
|
|
12
|
+
SRP : ``BaseStorage`` declares *what* the storage layer must do; concrete
|
|
13
|
+
subclasses decide *how* (local filesystem, S3, database BLOB, etc.).
|
|
14
|
+
OCP : New backends are added by subclassing; existing implementations are
|
|
15
|
+
not modified.
|
|
16
|
+
LSP : Subclasses must honour the documented return contracts (e.g. returning
|
|
17
|
+
``True`` for idempotent deletes rather than raising) so callers can
|
|
18
|
+
substitute them freely.
|
|
19
|
+
ISP : The interface is intentionally narrow — exactly the two operations that
|
|
20
|
+
the application layer requires.
|
|
21
|
+
DIP : ``CertLifecycleManager`` depends on ``BaseStorage``, never on
|
|
22
|
+
``LocalStorage`` or any other concrete class.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from abc import ABC, abstractmethod
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from cryptography.hazmat.primitives import serialization
|
|
31
|
+
|
|
32
|
+
from .const import CryptoObject
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BaseStorage(ABC):
|
|
36
|
+
"""
|
|
37
|
+
Abstract contract for certificate artefact storage backends.
|
|
38
|
+
|
|
39
|
+
Subclasses persist cryptographic objects (certificates, keys, CSRs, CRLs)
|
|
40
|
+
to some durable medium and provide a way to remove an entire issuance
|
|
41
|
+
folder in a single atomic call.
|
|
42
|
+
|
|
43
|
+
The two operations map directly to the two storage events in the
|
|
44
|
+
certificate lifecycle:
|
|
45
|
+
|
|
46
|
+
- **Issuance** — ``save_certificate`` is called once per artefact
|
|
47
|
+
(``.pem``, ``.key``, ``.csr``) with the same *uuid_str* so all three
|
|
48
|
+
files end up in the same directory.
|
|
49
|
+
- **Revocation / overwrite** — ``delete_certificate_folder`` removes the
|
|
50
|
+
entire UUID directory in one call, avoiding orphaned files.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def save_certificate(
|
|
55
|
+
self,
|
|
56
|
+
cert: CryptoObject,
|
|
57
|
+
file_name: str,
|
|
58
|
+
cert_path: str | Path | None = None,
|
|
59
|
+
uuid_str: str | None = None,
|
|
60
|
+
encoding: serialization.Encoding | None = None,
|
|
61
|
+
private_format: serialization.PrivateFormat | None = None,
|
|
62
|
+
public_format: serialization.PublicFormat | None = None,
|
|
63
|
+
encryption_algorithm: serialization.KeySerializationEncryption | None = None,
|
|
64
|
+
is_add_uuid: bool = True,
|
|
65
|
+
is_overwrite: bool = False,
|
|
66
|
+
) -> tuple[Path, str | None]:
|
|
67
|
+
"""
|
|
68
|
+
Serialise *cert* and persist it to the storage backend.
|
|
69
|
+
|
|
70
|
+
The implementation must:
|
|
71
|
+
1. Determine the correct serialisation format and file extension from
|
|
72
|
+
the type of *cert* (certificate → ``.pem``, private key → ``.key``,
|
|
73
|
+
CSR → ``.csr``, public key → ``.pub``, CRL → ``.pem``).
|
|
74
|
+
2. Assemble the output path from *cert_path*, the UUID directory
|
|
75
|
+
(when *is_add_uuid* is ``True``), and *file_name* + extension.
|
|
76
|
+
3. Auto-generate a UUID when *uuid_str* is ``None`` and *is_add_uuid*
|
|
77
|
+
is ``True``; reuse *uuid_str* when provided (so that multiple calls
|
|
78
|
+
for the same issuance all land in the same folder).
|
|
79
|
+
4. Honour *is_overwrite*: raise ``FileAlreadyExists`` when ``False``
|
|
80
|
+
and the target path already exists; silently replace when ``True``.
|
|
81
|
+
5. Return the absolute path to the saved file and the UUID used.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
cert : CryptoObject
|
|
86
|
+
The cryptographic object to serialise and save.
|
|
87
|
+
file_name : str
|
|
88
|
+
Base filename without extension (e.g. ``"ca"``, ``"my-service"``).
|
|
89
|
+
cert_path : str | Path | None
|
|
90
|
+
Optional sub-directory appended after the backend's root folder.
|
|
91
|
+
``None`` saves directly under the root.
|
|
92
|
+
uuid_str : str | None
|
|
93
|
+
Explicit UUID for the issuance sub-directory. Pass the UUID
|
|
94
|
+
returned by a previous call to group multiple artefacts in the
|
|
95
|
+
same folder. ``None`` triggers auto-generation.
|
|
96
|
+
Ignored when *is_add_uuid* is ``False``.
|
|
97
|
+
encoding : serialization.Encoding | None
|
|
98
|
+
Serialisation encoding override. ``None`` falls back to the
|
|
99
|
+
implementation's default (typically PEM).
|
|
100
|
+
private_format : serialization.PrivateFormat | None
|
|
101
|
+
Format for private-key serialisation
|
|
102
|
+
(e.g. ``TraditionalOpenSSL``, ``PKCS8``).
|
|
103
|
+
``None`` uses the implementation default.
|
|
104
|
+
public_format : serialization.PublicFormat | None
|
|
105
|
+
Format for public-key serialisation
|
|
106
|
+
(e.g. ``SubjectPublicKeyInfo``).
|
|
107
|
+
``None`` uses the implementation default.
|
|
108
|
+
encryption_algorithm : serialization.KeySerializationEncryption | None
|
|
109
|
+
Encryption wrapper for private keys
|
|
110
|
+
(e.g. ``NoEncryption``, ``BestAvailableEncryption``).
|
|
111
|
+
``None`` uses the implementation default.
|
|
112
|
+
is_add_uuid : bool
|
|
113
|
+
When ``True`` (default), a UUID subdirectory is inserted into the
|
|
114
|
+
path. Set to ``False`` for singleton files such as CRLs that are
|
|
115
|
+
regenerated in-place (``crl.pem``).
|
|
116
|
+
is_overwrite : bool
|
|
117
|
+
When ``True``, silently replace an existing file at the computed
|
|
118
|
+
path. When ``False`` (default), raise ``FileAlreadyExists``.
|
|
119
|
+
|
|
120
|
+
Returns
|
|
121
|
+
-------
|
|
122
|
+
tuple[Path, str | None]
|
|
123
|
+
``(absolute_path_to_saved_file, uuid_str_used)``.
|
|
124
|
+
The second element is the UUID that was used (auto-generated or
|
|
125
|
+
the value of *uuid_str*), or ``None`` when *is_add_uuid* is
|
|
126
|
+
``False``.
|
|
127
|
+
|
|
128
|
+
Raises
|
|
129
|
+
------
|
|
130
|
+
FileAlreadyExists
|
|
131
|
+
If the target file already exists and *is_overwrite* is ``False``.
|
|
132
|
+
TypeError
|
|
133
|
+
If *cert* is not a recognised cryptographic type.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def delete_certificate_folder(
|
|
138
|
+
self,
|
|
139
|
+
uuid_str: str,
|
|
140
|
+
cert_path: str | Path | None = None,
|
|
141
|
+
) -> bool:
|
|
142
|
+
"""
|
|
143
|
+
Remove the directory that contains all artefacts for a given UUID.
|
|
144
|
+
|
|
145
|
+
Implementations must be idempotent: if the target directory does not
|
|
146
|
+
exist, return ``True`` (no action was needed) rather than raising an
|
|
147
|
+
error. Only genuine unexpected failures (permission errors, I/O
|
|
148
|
+
errors) should return ``False``.
|
|
149
|
+
|
|
150
|
+
Parameters
|
|
151
|
+
----------
|
|
152
|
+
uuid_str : str
|
|
153
|
+
UUID string that identifies the sub-directory to delete. This is
|
|
154
|
+
the value returned as the second element by ``save_certificate``.
|
|
155
|
+
cert_path : str | Path | None
|
|
156
|
+
Optional sub-path prepended before *uuid_str*, matching the
|
|
157
|
+
*cert_path* used when the artefacts were saved. ``None`` means
|
|
158
|
+
*uuid_str* is directly under the backend's root folder.
|
|
159
|
+
|
|
160
|
+
Returns
|
|
161
|
+
-------
|
|
162
|
+
bool
|
|
163
|
+
``True`` — the directory was deleted, or did not exist (idempotent).
|
|
164
|
+
``False`` — an unexpected error occurred during deletion; the
|
|
165
|
+
implementation must log the exception.
|
|
166
|
+
"""
|
tiny_ca/storage/const.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
storage/const.py
|
|
3
|
+
|
|
4
|
+
Shared type aliases for the storage layer.
|
|
5
|
+
|
|
6
|
+
Exports ``CryptoObject``, a union type alias covering every cryptographic
|
|
7
|
+
object that ``BaseStorage.save_certificate`` is capable of serialising and
|
|
8
|
+
persisting. Declaring it once here prevents duplication across
|
|
9
|
+
``base_storage.py``, ``local_storage.py``, and any future storage backends.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import cryptography
|
|
15
|
+
from cryptography import x509
|
|
16
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
17
|
+
|
|
18
|
+
#: Union of all cryptographic object types accepted by the storage layer.
|
|
19
|
+
#:
|
|
20
|
+
#: ======================================= =========
|
|
21
|
+
#: Type Extension
|
|
22
|
+
#: ======================================= =========
|
|
23
|
+
#: ``x509.Certificate`` ``.pem``
|
|
24
|
+
#: ``x509.CertificateRevocationList`` ``.pem``
|
|
25
|
+
#: ``CertificateSigningRequest`` (Rust) ``.csr``
|
|
26
|
+
#: ``rsa.RSAPrivateKey`` ``.key``
|
|
27
|
+
#: ``rsa.RSAPublicKey`` ``.pub``
|
|
28
|
+
#: ======================================= =========
|
|
29
|
+
#:
|
|
30
|
+
#: The internal Rust-backed ``CertificateSigningRequest`` type is included
|
|
31
|
+
#: because the ``cryptography`` library exposes CSR objects through its Rust
|
|
32
|
+
#: bindings rather than a pure-Python class.
|
|
33
|
+
CryptoObject = (
|
|
34
|
+
x509.Certificate
|
|
35
|
+
| x509.CertificateRevocationList
|
|
36
|
+
| cryptography.hazmat.bindings._rust.x509.CertificateSigningRequest # type: ignore
|
|
37
|
+
| rsa.RSAPrivateKey
|
|
38
|
+
| rsa.RSAPublicKey
|
|
39
|
+
)
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""
|
|
2
|
+
local_storage.py
|
|
3
|
+
|
|
4
|
+
Local filesystem implementation of ``BaseStorage``.
|
|
5
|
+
|
|
6
|
+
Module-level contents
|
|
7
|
+
---------------------
|
|
8
|
+
``_CertSerializer`` — stateless helper that converts a cryptographic object
|
|
9
|
+
to raw bytes and determines the correct file extension.
|
|
10
|
+
Private to this module; not part of the public API.
|
|
11
|
+
``LocalStorage`` — concrete ``BaseStorage`` that writes artefacts to
|
|
12
|
+
a configurable directory tree on the local filesystem.
|
|
13
|
+
|
|
14
|
+
SOLID notes
|
|
15
|
+
-----------
|
|
16
|
+
SRP : ``_CertSerializer`` is responsible for serialisation only.
|
|
17
|
+
``LocalStorage`` is responsible for path resolution and file I/O only.
|
|
18
|
+
Neither class performs cryptographic operations or database access.
|
|
19
|
+
OCP : New crypto object types are handled by adding a branch in
|
|
20
|
+
``_CertSerializer.serialise`` without modifying ``LocalStorage``.
|
|
21
|
+
LSP : ``LocalStorage`` fully honours the ``BaseStorage`` contract, including
|
|
22
|
+
the idempotent-delete and ``FileAlreadyExists`` raise semantics.
|
|
23
|
+
DIP : Consumers depend on ``BaseStorage``; ``LocalStorage`` is injected
|
|
24
|
+
at construction time.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from logging import Logger
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
import shutil
|
|
32
|
+
from uuid import uuid4
|
|
33
|
+
import warnings
|
|
34
|
+
|
|
35
|
+
import cryptography
|
|
36
|
+
from cryptography import x509
|
|
37
|
+
from cryptography.hazmat.primitives import serialization
|
|
38
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
39
|
+
|
|
40
|
+
from tiny_ca.exc import FileAlreadyExists
|
|
41
|
+
from tiny_ca.settings import DEFAULT_LOGGER
|
|
42
|
+
|
|
43
|
+
from .base_storage import BaseStorage
|
|
44
|
+
from .const import CryptoObject
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# SRP: serialisation logic isolated from storage logic
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class _CertSerializer:
|
|
52
|
+
"""
|
|
53
|
+
Converts a ``CryptoObject`` to raw bytes and the matching file extension.
|
|
54
|
+
|
|
55
|
+
This is a private, stateless helper used exclusively by ``LocalStorage``.
|
|
56
|
+
Isolating serialisation here keeps ``LocalStorage`` free of type-dispatch
|
|
57
|
+
logic and makes it trivial to add support for new cryptographic types
|
|
58
|
+
(OCP: add one branch here, nothing else changes).
|
|
59
|
+
|
|
60
|
+
Supported types
|
|
61
|
+
~~~~~~~~~~~~~~~
|
|
62
|
+
======================================= ======== ============================
|
|
63
|
+
Type Ext Method called
|
|
64
|
+
======================================= ======== ============================
|
|
65
|
+
``x509.Certificate`` ``.pem`` ``public_bytes``
|
|
66
|
+
``x509.CertificateRevocationList`` ``.pem`` ``public_bytes``
|
|
67
|
+
``CertificateSigningRequest`` (Rust) ``.csr`` ``public_bytes``
|
|
68
|
+
``rsa.RSAPrivateKey`` ``.key`` ``private_bytes``
|
|
69
|
+
``rsa.RSAPublicKey`` ``.pub`` ``public_bytes``
|
|
70
|
+
======================================= ======== ============================
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def serialise(
|
|
75
|
+
cert: CryptoObject,
|
|
76
|
+
encoding: serialization.Encoding,
|
|
77
|
+
private_format: serialization.PrivateFormat,
|
|
78
|
+
public_format: serialization.PublicFormat,
|
|
79
|
+
encryption_algorithm: serialization.KeySerializationEncryption,
|
|
80
|
+
) -> tuple[bytes, str]:
|
|
81
|
+
"""
|
|
82
|
+
Serialise *cert* to raw bytes and return its file extension.
|
|
83
|
+
|
|
84
|
+
Dispatches to the appropriate serialisation method based on the
|
|
85
|
+
runtime type of *cert*. All format/encoding parameters are passed
|
|
86
|
+
through regardless of type; the ones irrelevant to the object type
|
|
87
|
+
(e.g. *private_format* for a certificate) are silently ignored by the
|
|
88
|
+
cryptography library's own methods.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
cert : CryptoObject
|
|
93
|
+
The cryptographic object to serialise.
|
|
94
|
+
encoding : serialization.Encoding
|
|
95
|
+
Target encoding, e.g. ``Encoding.PEM`` or ``Encoding.DER``.
|
|
96
|
+
private_format : serialization.PrivateFormat
|
|
97
|
+
Format for RSA private-key output
|
|
98
|
+
(``TraditionalOpenSSL`` / ``PKCS8``).
|
|
99
|
+
public_format : serialization.PublicFormat
|
|
100
|
+
Format for RSA public-key output
|
|
101
|
+
(``SubjectPublicKeyInfo`` / ``PKCS1``).
|
|
102
|
+
encryption_algorithm : serialization.KeySerializationEncryption
|
|
103
|
+
Encryption wrapper applied to private-key output
|
|
104
|
+
(``NoEncryption`` / ``BestAvailableEncryption``).
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
tuple[bytes, str]
|
|
109
|
+
``(raw_bytes, file_extension)`` where *file_extension* is one of
|
|
110
|
+
``".pem"``, ``".csr"``, ``".key"``, or ``".pub"``, including the
|
|
111
|
+
leading dot.
|
|
112
|
+
|
|
113
|
+
Raises
|
|
114
|
+
------
|
|
115
|
+
TypeError
|
|
116
|
+
If *cert* is not one of the five supported cryptographic types.
|
|
117
|
+
"""
|
|
118
|
+
if isinstance(cert, (x509.Certificate, x509.CertificateRevocationList)):
|
|
119
|
+
return cert.public_bytes(encoding=encoding), ".pem"
|
|
120
|
+
|
|
121
|
+
if isinstance(
|
|
122
|
+
cert,
|
|
123
|
+
cryptography.hazmat.bindings._rust.x509.CertificateSigningRequest, # type: ignore
|
|
124
|
+
):
|
|
125
|
+
return cert.public_bytes(encoding=encoding), ".csr"
|
|
126
|
+
|
|
127
|
+
if isinstance(cert, rsa.RSAPrivateKey):
|
|
128
|
+
return (
|
|
129
|
+
cert.private_bytes(
|
|
130
|
+
encoding=encoding,
|
|
131
|
+
format=private_format,
|
|
132
|
+
encryption_algorithm=encryption_algorithm,
|
|
133
|
+
),
|
|
134
|
+
".key",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if isinstance(cert, rsa.RSAPublicKey):
|
|
138
|
+
return cert.public_bytes(encoding=encoding, format=public_format), ".pub"
|
|
139
|
+
|
|
140
|
+
raise TypeError(
|
|
141
|
+
f"Unsupported crypto object type: {type(cert)!r}. "
|
|
142
|
+
"Expected one of: x509.Certificate, x509.CertificateRevocationList, "
|
|
143
|
+
"CertificateSigningRequest, rsa.RSAPrivateKey, rsa.RSAPublicKey."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
# SRP: filesystem persistence
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class LocalStorage(BaseStorage):
|
|
153
|
+
"""
|
|
154
|
+
Local filesystem storage backend for certificate artefacts.
|
|
155
|
+
|
|
156
|
+
Writes serialised cryptographic objects to a configurable directory tree.
|
|
157
|
+
Each issuance group (certificate + key + CSR) is placed in a dedicated
|
|
158
|
+
UUID subdirectory so that all artefacts for a given certificate can be
|
|
159
|
+
found and deleted together.
|
|
160
|
+
|
|
161
|
+
Directory layout
|
|
162
|
+
~~~~~~~~~~~~~~~~
|
|
163
|
+
::
|
|
164
|
+
|
|
165
|
+
<base_folder>/
|
|
166
|
+
└── [cert_path/]
|
|
167
|
+
└── [<uuid>/]
|
|
168
|
+
├── <file_name>.pem # x509.Certificate or CRL
|
|
169
|
+
├── <file_name>.key # RSA private key
|
|
170
|
+
├── <file_name>.csr # certificate signing request
|
|
171
|
+
└── <file_name>.pub # RSA public key (if applicable)
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
base_folder : str | Path
|
|
176
|
+
Root directory under which all certificates are stored. The directory
|
|
177
|
+
is created on first write if it does not exist.
|
|
178
|
+
Default: ``"./certs"``.
|
|
179
|
+
base_encoding : serialization.Encoding
|
|
180
|
+
Default encoding for all serialised objects.
|
|
181
|
+
Default: ``Encoding.PEM``.
|
|
182
|
+
base_private_format : serialization.PrivateFormat
|
|
183
|
+
Default format for RSA private-key files.
|
|
184
|
+
Default: ``PrivateFormat.TraditionalOpenSSL`` (PKCS#1, OpenSSL-compatible).
|
|
185
|
+
base_public_format : serialization.PublicFormat
|
|
186
|
+
Default format for RSA public-key files.
|
|
187
|
+
Default: ``PublicFormat.SubjectPublicKeyInfo`` (PKCS#8 / X.509 SubjectPublicKeyInfo).
|
|
188
|
+
base_encryption_algorithm : serialization.KeySerializationEncryption
|
|
189
|
+
Default encryption applied to private-key files.
|
|
190
|
+
Default: ``NoEncryption()`` — keys are stored in plaintext.
|
|
191
|
+
logger : Logger | None
|
|
192
|
+
Logger for diagnostic messages. Falls back to ``DEFAULT_LOGGER``.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
def __init__(
|
|
196
|
+
self,
|
|
197
|
+
base_folder: str | Path = "./certs",
|
|
198
|
+
base_encoding: serialization.Encoding = serialization.Encoding.PEM,
|
|
199
|
+
base_private_format: serialization.PrivateFormat = serialization.PrivateFormat.TraditionalOpenSSL,
|
|
200
|
+
base_public_format: serialization.PublicFormat = serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
201
|
+
base_encryption_algorithm: serialization.KeySerializationEncryption = serialization.NoEncryption(),
|
|
202
|
+
logger: Logger | None = None,
|
|
203
|
+
) -> None:
|
|
204
|
+
self._base_folder = Path(base_folder)
|
|
205
|
+
self._base_encoding = base_encoding
|
|
206
|
+
self._base_private_format = base_private_format
|
|
207
|
+
self._base_public_format = base_public_format
|
|
208
|
+
self._base_encryption_algorithm = base_encryption_algorithm
|
|
209
|
+
self._logger = logger or DEFAULT_LOGGER
|
|
210
|
+
|
|
211
|
+
# ------------------------------------------------------------------
|
|
212
|
+
# BaseStorage interface
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
def save_certificate(
|
|
216
|
+
self,
|
|
217
|
+
cert: CryptoObject,
|
|
218
|
+
file_name: str,
|
|
219
|
+
cert_path: str | Path | None = None,
|
|
220
|
+
uuid_str: str | None = None,
|
|
221
|
+
encoding: serialization.Encoding | None = None,
|
|
222
|
+
private_format: serialization.PrivateFormat | None = None,
|
|
223
|
+
public_format: serialization.PublicFormat | None = None,
|
|
224
|
+
encryption_algorithm: serialization.KeySerializationEncryption | None = None,
|
|
225
|
+
is_add_uuid: bool = True,
|
|
226
|
+
is_overwrite: bool = False,
|
|
227
|
+
) -> tuple[Path, str | None]:
|
|
228
|
+
"""
|
|
229
|
+
Serialise *cert* and write the result to the local filesystem.
|
|
230
|
+
|
|
231
|
+
Assembles the output path as::
|
|
232
|
+
|
|
233
|
+
<base_folder> / [cert_path/] / [<uuid>/] / <file_name><ext>
|
|
234
|
+
|
|
235
|
+
Where *ext* is determined automatically from the type of *cert*.
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
cert : CryptoObject
|
|
240
|
+
Cryptographic object to serialise and persist.
|
|
241
|
+
file_name : str
|
|
242
|
+
Base filename without extension (e.g. ``"ca"``, ``"nginx"``).
|
|
243
|
+
cert_path : str | Path | None
|
|
244
|
+
Optional sub-directory appended after *base_folder*.
|
|
245
|
+
uuid_str : str | None
|
|
246
|
+
Reuse an existing UUID directory by passing the value returned by
|
|
247
|
+
a previous ``save_certificate`` call. ``None`` auto-generates a
|
|
248
|
+
new UUID. Ignored when *is_add_uuid* is ``False``.
|
|
249
|
+
encoding : serialization.Encoding | None
|
|
250
|
+
Encoding override. ``None`` uses *base_encoding*.
|
|
251
|
+
private_format : serialization.PrivateFormat | None
|
|
252
|
+
Private-key format override. ``None`` uses *base_private_format*.
|
|
253
|
+
public_format : serialization.PublicFormat | None
|
|
254
|
+
Public-key format override. ``None`` uses *base_public_format*.
|
|
255
|
+
encryption_algorithm : serialization.KeySerializationEncryption | None
|
|
256
|
+
Private-key encryption override. ``None`` uses
|
|
257
|
+
*base_encryption_algorithm*.
|
|
258
|
+
is_add_uuid : bool
|
|
259
|
+
When ``True`` (default), a UUID subdirectory is inserted.
|
|
260
|
+
Set to ``False`` for singleton files such as CRL that are
|
|
261
|
+
regenerated in-place.
|
|
262
|
+
is_overwrite : bool
|
|
263
|
+
When ``True``, silently replace an existing file.
|
|
264
|
+
When ``False`` (default), raise ``FileAlreadyExists``.
|
|
265
|
+
|
|
266
|
+
Returns
|
|
267
|
+
-------
|
|
268
|
+
tuple[Path, str | None]
|
|
269
|
+
``(absolute_path_to_written_file, uuid_used)``.
|
|
270
|
+
*uuid_used* is ``None`` when *is_add_uuid* is ``False``.
|
|
271
|
+
|
|
272
|
+
Raises
|
|
273
|
+
------
|
|
274
|
+
FileAlreadyExists
|
|
275
|
+
If the computed target path already exists and *is_overwrite*
|
|
276
|
+
is ``False``.
|
|
277
|
+
TypeError
|
|
278
|
+
If *cert* is not a supported cryptographic type.
|
|
279
|
+
"""
|
|
280
|
+
output_dir, effective_uuid = self._resolve_output_dir(
|
|
281
|
+
cert_path=cert_path,
|
|
282
|
+
uuid_str=uuid_str,
|
|
283
|
+
is_add_uuid=is_add_uuid,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
cert_data, extension = _CertSerializer.serialise(
|
|
287
|
+
cert=cert,
|
|
288
|
+
encoding=encoding or self._base_encoding,
|
|
289
|
+
private_format=private_format or self._base_private_format,
|
|
290
|
+
public_format=public_format or self._base_public_format,
|
|
291
|
+
encryption_algorithm=encryption_algorithm
|
|
292
|
+
or self._base_encryption_algorithm,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
output_path = output_dir / f"{file_name}{extension}"
|
|
296
|
+
self._write_file(path=output_path, data=cert_data, is_overwrite=is_overwrite)
|
|
297
|
+
|
|
298
|
+
self._logger.debug(
|
|
299
|
+
"Saved %s → %s (uuid=%s)", type(cert).__name__, output_path, effective_uuid
|
|
300
|
+
)
|
|
301
|
+
return output_path, effective_uuid
|
|
302
|
+
|
|
303
|
+
def delete_certificate_folder(
|
|
304
|
+
self,
|
|
305
|
+
uuid_str: str,
|
|
306
|
+
cert_path: str | Path | None = None,
|
|
307
|
+
) -> bool:
|
|
308
|
+
"""
|
|
309
|
+
Recursively remove the directory identified by *uuid_str*.
|
|
310
|
+
|
|
311
|
+
The target path is resolved as::
|
|
312
|
+
|
|
313
|
+
<base_folder> / [cert_path/] / <uuid_str>
|
|
314
|
+
|
|
315
|
+
The operation is idempotent: if the directory does not exist a
|
|
316
|
+
``UserWarning`` is emitted and ``True`` is returned (no action needed).
|
|
317
|
+
If the path exists but is a regular file rather than a directory, a
|
|
318
|
+
``UserWarning`` is emitted and ``True`` is returned (not our directory).
|
|
319
|
+
Only a genuine ``OSError`` during ``shutil.rmtree`` causes ``False``.
|
|
320
|
+
|
|
321
|
+
Parameters
|
|
322
|
+
----------
|
|
323
|
+
uuid_str : str
|
|
324
|
+
UUID sub-directory name to remove.
|
|
325
|
+
cert_path : str | Path | None
|
|
326
|
+
Optional sub-path under *base_folder* containing *uuid_str*.
|
|
327
|
+
|
|
328
|
+
Returns
|
|
329
|
+
-------
|
|
330
|
+
bool
|
|
331
|
+
``True`` — directory removed, or path was already absent.
|
|
332
|
+
``False`` — ``OSError`` occurred; check logs for details.
|
|
333
|
+
|
|
334
|
+
Warns
|
|
335
|
+
-----
|
|
336
|
+
UserWarning
|
|
337
|
+
If the target path does not exist or is not a directory.
|
|
338
|
+
"""
|
|
339
|
+
target = self._base_folder
|
|
340
|
+
if cert_path:
|
|
341
|
+
target = target / cert_path
|
|
342
|
+
target = target / uuid_str
|
|
343
|
+
|
|
344
|
+
if not target.exists():
|
|
345
|
+
warnings.warn(
|
|
346
|
+
f"Certificate folder not deleted — path does not exist: {target}",
|
|
347
|
+
UserWarning,
|
|
348
|
+
stacklevel=2,
|
|
349
|
+
)
|
|
350
|
+
return True
|
|
351
|
+
|
|
352
|
+
if not target.is_dir():
|
|
353
|
+
warnings.warn(
|
|
354
|
+
f"Certificate folder not deleted — path is not a directory: {target}",
|
|
355
|
+
UserWarning,
|
|
356
|
+
stacklevel=2,
|
|
357
|
+
)
|
|
358
|
+
return True
|
|
359
|
+
|
|
360
|
+
try:
|
|
361
|
+
shutil.rmtree(target)
|
|
362
|
+
self._logger.info("Deleted certificate folder: %s", target)
|
|
363
|
+
return True
|
|
364
|
+
except OSError as exc:
|
|
365
|
+
self._logger.error(
|
|
366
|
+
"Failed to delete certificate folder %s: %s", target, exc
|
|
367
|
+
)
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
# ------------------------------------------------------------------
|
|
371
|
+
# Private helpers
|
|
372
|
+
# ------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
def _resolve_output_dir(
|
|
375
|
+
self,
|
|
376
|
+
cert_path: str | Path | None,
|
|
377
|
+
uuid_str: str | None,
|
|
378
|
+
is_add_uuid: bool,
|
|
379
|
+
) -> tuple[Path, str | None]:
|
|
380
|
+
"""
|
|
381
|
+
Compute the directory path where the certificate file will be written.
|
|
382
|
+
|
|
383
|
+
Builds the path incrementally:
|
|
384
|
+
1. Start from *base_folder*.
|
|
385
|
+
2. Append *cert_path* if provided.
|
|
386
|
+
3. Append a UUID segment if *is_add_uuid* is ``True``.
|
|
387
|
+
|
|
388
|
+
The UUID is taken from *uuid_str* if provided, otherwise a new UUID4
|
|
389
|
+
string is generated. When *is_add_uuid* is ``False`` the second return
|
|
390
|
+
value is always ``None``.
|
|
391
|
+
|
|
392
|
+
Parameters
|
|
393
|
+
----------
|
|
394
|
+
cert_path : str | Path | None
|
|
395
|
+
Optional sub-path under *base_folder*.
|
|
396
|
+
uuid_str : str | None
|
|
397
|
+
Explicit UUID for the sub-directory, or ``None`` to auto-generate.
|
|
398
|
+
is_add_uuid : bool
|
|
399
|
+
Whether to append a UUID segment to the path.
|
|
400
|
+
|
|
401
|
+
Returns
|
|
402
|
+
-------
|
|
403
|
+
tuple[Path, str | None]
|
|
404
|
+
``(resolved_output_directory, uuid_used_or_None)``.
|
|
405
|
+
"""
|
|
406
|
+
directory = self._base_folder
|
|
407
|
+
if cert_path:
|
|
408
|
+
directory = directory / cert_path
|
|
409
|
+
|
|
410
|
+
effective_uuid: str | None = None
|
|
411
|
+
if is_add_uuid:
|
|
412
|
+
effective_uuid = uuid_str or str(uuid4())
|
|
413
|
+
directory = directory / effective_uuid
|
|
414
|
+
|
|
415
|
+
return directory, effective_uuid
|
|
416
|
+
|
|
417
|
+
def _write_file(self, path: Path, data: bytes, is_overwrite: bool) -> None:
|
|
418
|
+
"""
|
|
419
|
+
Write *data* to *path*, creating any missing parent directories.
|
|
420
|
+
|
|
421
|
+
Parent directories are created with ``mkdir(parents=True,
|
|
422
|
+
exist_ok=True)`` so the caller never needs to pre-create the
|
|
423
|
+
directory tree manually.
|
|
424
|
+
|
|
425
|
+
Parameters
|
|
426
|
+
----------
|
|
427
|
+
path : Path
|
|
428
|
+
Absolute target path for the output file.
|
|
429
|
+
data : bytes
|
|
430
|
+
Raw serialised bytes to write.
|
|
431
|
+
is_overwrite : bool
|
|
432
|
+
When ``False``, raise ``FileAlreadyExists`` if *path* already
|
|
433
|
+
exists. When ``True``, the existing file is silently replaced.
|
|
434
|
+
|
|
435
|
+
Raises
|
|
436
|
+
------
|
|
437
|
+
FileAlreadyExists
|
|
438
|
+
If *path* already exists on disk and *is_overwrite* is ``False``.
|
|
439
|
+
"""
|
|
440
|
+
if path.exists() and not is_overwrite:
|
|
441
|
+
raise FileAlreadyExists(path_save_cert=path)
|
|
442
|
+
|
|
443
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
444
|
+
|
|
445
|
+
with open(path, "wb") as fh:
|
|
446
|
+
fh.write(data)
|
|
447
|
+
|
|
448
|
+
self._logger.debug("Written %d bytes → %s", len(data), path)
|
|
File without changes
|