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
tiny_ca/db/base_db.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
Abstract base class that defines the contract for all certificate registry
|
|
4
|
+
database adapters.
|
|
5
|
+
|
|
6
|
+
Any class that inherits from ``BaseDB`` and implements all six abstract methods
|
|
7
|
+
can be used wherever the application expects a database layer, without requiring
|
|
8
|
+
any changes to the calling code (LSP / DIP).
|
|
9
|
+
|
|
10
|
+
SOLID notes
|
|
11
|
+
-----------
|
|
12
|
+
SRP : ``BaseDB`` declares *what* the registry must be able to do; concrete
|
|
13
|
+
subclasses decide *how*.
|
|
14
|
+
OCP : New query operations are added by extending ``BaseDB``; existing
|
|
15
|
+
implementations are not modified.
|
|
16
|
+
LSP : Every concrete subclass must honour the documented pre- and post-conditions
|
|
17
|
+
of each method so callers can substitute implementations freely.
|
|
18
|
+
ISP : The interface is intentionally narrow — only operations that callers
|
|
19
|
+
actually need are declared here.
|
|
20
|
+
DIP : High-level modules (e.g. ``CertLifecycleManager``) depend on ``BaseDB``,
|
|
21
|
+
never on ``SyncDBHandler`` or any other concrete implementation.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from abc import ABC, abstractmethod
|
|
27
|
+
from collections.abc import Generator
|
|
28
|
+
|
|
29
|
+
from cryptography import x509
|
|
30
|
+
|
|
31
|
+
from tiny_ca.const import CertType
|
|
32
|
+
|
|
33
|
+
from .models import CertificateRecord
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BaseDB(ABC):
|
|
37
|
+
"""
|
|
38
|
+
Abstract contract for the certificate registry database adapter.
|
|
39
|
+
|
|
40
|
+
Defines all operations that the application layer requires from the
|
|
41
|
+
persistence tier. Concrete implementations (e.g. ``SyncDBHandler``)
|
|
42
|
+
provide the actual SQL queries and transaction management.
|
|
43
|
+
|
|
44
|
+
All methods that return a ``CertificateRecord`` must return ``None``
|
|
45
|
+
(not raise an exception) when the requested record simply does not exist.
|
|
46
|
+
Exceptions are reserved for genuine infrastructure failures (connection
|
|
47
|
+
errors, constraint violations, etc.).
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def get_by_serial(self, serial: int) -> CertificateRecord | None:
|
|
52
|
+
"""
|
|
53
|
+
Retrieve a certificate record by its X.509 serial number.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
serial : int
|
|
58
|
+
Integer serial number to look up. Implementations are responsible
|
|
59
|
+
for any type conversion required by the underlying storage format
|
|
60
|
+
(e.g. converting to ``str`` for string-typed database columns).
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
CertificateRecord | None
|
|
65
|
+
The matching record regardless of its current status (VALID,
|
|
66
|
+
REVOKED, EXPIRED), or ``None`` if no record exists for *serial*.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def get_by_name(self, common_name: str) -> CertificateRecord | None:
|
|
71
|
+
"""
|
|
72
|
+
Retrieve the currently active certificate record for a given Common Name.
|
|
73
|
+
|
|
74
|
+
Implementations must filter to only ``VALID`` records so that the
|
|
75
|
+
caller always receives the live certificate or ``None`` — never a
|
|
76
|
+
revoked or expired one.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
common_name : str
|
|
81
|
+
The CN (Common Name) field from the certificate Subject to look up.
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
CertificateRecord | None
|
|
86
|
+
The active VALID record for *common_name*, or ``None`` if no such
|
|
87
|
+
record exists.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def register_cert_in_db(
|
|
92
|
+
self,
|
|
93
|
+
cert: x509.Certificate,
|
|
94
|
+
uuid: str,
|
|
95
|
+
key_type: CertType = CertType.DEVICE,
|
|
96
|
+
) -> bool:
|
|
97
|
+
"""
|
|
98
|
+
Persist a newly issued certificate to the registry.
|
|
99
|
+
|
|
100
|
+
Creates a new record with ``status=VALID`` populated from the
|
|
101
|
+
certificate metadata. The implementation must extract the CN from
|
|
102
|
+
``cert.subject`` and store the full PEM encoding for later retrieval.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
cert : x509.Certificate
|
|
107
|
+
The issued X.509 certificate object to register.
|
|
108
|
+
uuid : str
|
|
109
|
+
UUID that identifies the filesystem folder containing the
|
|
110
|
+
corresponding ``.pem``, ``.key``, and ``.csr`` artefact files.
|
|
111
|
+
key_type : CertType
|
|
112
|
+
Certificate category (CA, USER, SERVICE, DEVICE, INTERNAL).
|
|
113
|
+
Default: ``CertType.DEVICE``.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
bool
|
|
118
|
+
``True`` if the record was persisted successfully;
|
|
119
|
+
``False`` if the operation failed (the implementation must log
|
|
120
|
+
the reason and roll back any partial changes).
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def revoke_certificate(
|
|
125
|
+
self,
|
|
126
|
+
serial_number: int,
|
|
127
|
+
reason: x509.ReasonFlags = x509.ReasonFlags.unspecified,
|
|
128
|
+
) -> tuple[bool, object]:
|
|
129
|
+
"""
|
|
130
|
+
Mark a certificate as revoked and record the reason and timestamp.
|
|
131
|
+
|
|
132
|
+
Implementations must:
|
|
133
|
+
1. Look up the record by *serial_number* filtered to ``status=VALID``.
|
|
134
|
+
2. If not found, return ``(False, <NOT_FOUND status>)`` without error.
|
|
135
|
+
3. Update ``status``, ``revocation_reason``, and ``revocation_date``.
|
|
136
|
+
4. Commit atomically; roll back on any exception.
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
serial_number : int
|
|
141
|
+
Serial number of the certificate to revoke.
|
|
142
|
+
reason : x509.ReasonFlags
|
|
143
|
+
RFC 5280 §5.3.1 revocation reason code.
|
|
144
|
+
Default: ``x509.ReasonFlags.unspecified`` (code 0).
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
tuple[bool, object]
|
|
149
|
+
``(success, status_value)`` where *status_value* is implementation-
|
|
150
|
+
defined (typically a ``RevokeStatus`` enum member).
|
|
151
|
+
``True`` indicates the revocation was committed; ``False`` means it
|
|
152
|
+
was not (reason encoded in *status_value*).
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
@abstractmethod
|
|
156
|
+
def get_revoked_certificates(self) -> Generator[CertificateRecord, None, None]:
|
|
157
|
+
"""
|
|
158
|
+
Yield certificate records that should appear in the current CRL.
|
|
159
|
+
|
|
160
|
+
Implementations define their own freshness window (e.g. only records
|
|
161
|
+
revoked within the past 365 days and not yet expired), but must yield
|
|
162
|
+
objects that expose at minimum:
|
|
163
|
+
|
|
164
|
+
- ``serial_number`` — castable to ``int``
|
|
165
|
+
- ``revocation_date`` — a ``datetime`` object
|
|
166
|
+
- ``revocation_reason`` — an integer RFC 5280 reason code
|
|
167
|
+
|
|
168
|
+
Yields
|
|
169
|
+
------
|
|
170
|
+
CertificateRecord
|
|
171
|
+
Records (or row-like objects) for each revoked certificate that
|
|
172
|
+
falls within the implementation's CRL inclusion window.
|
|
173
|
+
"""
|
tiny_ca/db/const.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
db/const.py
|
|
3
|
+
|
|
4
|
+
Shared constants for the database layer.
|
|
5
|
+
|
|
6
|
+
Currently exports ``RevokeStatus``, the value object that encodes the
|
|
7
|
+
possible outcomes of a certificate revocation attempt. Keeping it in a
|
|
8
|
+
dedicated module prevents circular imports between ``base_db``, the ORM
|
|
9
|
+
models, and the concrete handler implementations.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from enum import Enum, StrEnum
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RevokeStatus(Enum):
|
|
18
|
+
"""
|
|
19
|
+
Enumerated outcomes for a certificate revocation attempt.
|
|
20
|
+
|
|
21
|
+
Used as the second element of the ``(bool, RevokeStatus)`` tuple returned
|
|
22
|
+
by ``BaseDB.revoke_certificate`` implementations. The boolean indicates
|
|
23
|
+
overall success; this enum provides the machine-readable reason when the
|
|
24
|
+
operation did not succeed, and a confirmation token when it did.
|
|
25
|
+
|
|
26
|
+
Members
|
|
27
|
+
-------
|
|
28
|
+
NOT_FOUND :
|
|
29
|
+
No active (``VALID``) certificate with the requested serial number
|
|
30
|
+
exists in the registry. The certificate may already be revoked,
|
|
31
|
+
expired, or was never registered.
|
|
32
|
+
UNKNOWN_ERROR :
|
|
33
|
+
An unexpected internal error (e.g. database constraint violation,
|
|
34
|
+
connection failure) prevented the revocation. The implementation must
|
|
35
|
+
log the underlying exception before returning this status.
|
|
36
|
+
OK :
|
|
37
|
+
The revocation was committed successfully. The certificate record has
|
|
38
|
+
been updated with ``status=REVOKED``, a ``revocation_date``, and the
|
|
39
|
+
provided ``revocation_reason``.
|
|
40
|
+
|
|
41
|
+
Examples
|
|
42
|
+
--------
|
|
43
|
+
>>> success, status = db.revoke_certificate(serial=12345, reason=ReasonFlags.key_compromise)
|
|
44
|
+
>>> if not success:
|
|
45
|
+
... if status == RevokeStatus.NOT_FOUND:
|
|
46
|
+
... logger.warning("Certificate not found")
|
|
47
|
+
... else:
|
|
48
|
+
... logger.error("Internal revocation error")
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
NOT_FOUND = (
|
|
52
|
+
"The certificate was not revoked because there is no valid certificate "
|
|
53
|
+
"with the specified serial number."
|
|
54
|
+
)
|
|
55
|
+
UNKNOWN_ERROR = (
|
|
56
|
+
"The certificate was not revoked due to an internal error. "
|
|
57
|
+
"Please review the service logs."
|
|
58
|
+
)
|
|
59
|
+
OK = "success"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class CertificateStatus(StrEnum):
|
|
63
|
+
"""
|
|
64
|
+
Lifecycle state of a certificate record in the registry.
|
|
65
|
+
|
|
66
|
+
Stored as a lowercase string in the ``status`` column of
|
|
67
|
+
``CertificateRecord`` so the value is human-readable in raw SQL output.
|
|
68
|
+
|
|
69
|
+
Members
|
|
70
|
+
-------
|
|
71
|
+
VALID :
|
|
72
|
+
The certificate was issued successfully and has not been revoked or
|
|
73
|
+
expired. Active certificates used for authentication or encryption
|
|
74
|
+
are expected to be in this state.
|
|
75
|
+
REVOKED :
|
|
76
|
+
The certificate was explicitly revoked before its natural expiry.
|
|
77
|
+
The ``revocation_date`` and ``revocation_reason`` columns on the
|
|
78
|
+
corresponding ``CertificateRecord`` row must be non-null.
|
|
79
|
+
EXPIRED :
|
|
80
|
+
The certificate's ``not_valid_after`` date has passed. This status
|
|
81
|
+
may be set by a background job; alternatively, callers can detect
|
|
82
|
+
expiry by comparing ``not_valid_after`` to the current time.
|
|
83
|
+
UNKNOWN :
|
|
84
|
+
The status could not be determined, typically because no record was
|
|
85
|
+
found for the requested serial number. Used as a safe sentinel value
|
|
86
|
+
by ``CertLifecycleManager.get_certificate_status``.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
VALID = "valid"
|
|
90
|
+
REVOKED = "revoked"
|
|
91
|
+
EXPIRED = "expired"
|
|
92
|
+
UNKNOWN = "unknown"
|
tiny_ca/db/models.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
|
|
3
|
+
SQLAlchemy ORM models and status enumerations for the certificate registry.
|
|
4
|
+
|
|
5
|
+
Module-level contents
|
|
6
|
+
---------------------
|
|
7
|
+
``CertificateStatus`` — ``StrEnum`` representing the lifecycle state of a
|
|
8
|
+
certificate row.
|
|
9
|
+
``CertificateRecord`` — ORM-mapped table that stores all metadata for issued,
|
|
10
|
+
revoked, and expired certificates.
|
|
11
|
+
|
|
12
|
+
Design notes
|
|
13
|
+
------------
|
|
14
|
+
- ``CertificateStatus`` uses ``StrEnum`` so that values are stored as plain
|
|
15
|
+
strings in the database, making the column human-readable and compatible
|
|
16
|
+
with non-Python tooling that queries the database directly.
|
|
17
|
+
- ``CertificateRecord`` stores the full PEM-encoded public certificate so the
|
|
18
|
+
certificate can be reconstructed independently of the filesystem artefacts.
|
|
19
|
+
- ``serial_number`` is stored as ``String`` (not ``Integer``) because X.509
|
|
20
|
+
serial numbers can be up to 20 bytes / 160 bits, exceeding the range of a
|
|
21
|
+
64-bit SQL integer on most backends.
|
|
22
|
+
- ``uuid`` links the database record to the filesystem folder managed by
|
|
23
|
+
``BaseStorage``, enabling clean deletion of both the DB row and the
|
|
24
|
+
corresponding files together.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from sqlalchemy import Column, DateTime, Integer, String
|
|
30
|
+
from sqlalchemy.orm import Mapped, declarative_base
|
|
31
|
+
|
|
32
|
+
from tiny_ca.const import CertType
|
|
33
|
+
|
|
34
|
+
from .const import CertificateStatus
|
|
35
|
+
|
|
36
|
+
Base = declarative_base()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CertificateRecord(Base): # type: ignore
|
|
40
|
+
"""
|
|
41
|
+
ORM model for a single certificate entry in the registry.
|
|
42
|
+
|
|
43
|
+
Maps to the ``certificates`` table. Each row represents one certificate
|
|
44
|
+
that has been issued by the CA, regardless of its current lifecycle state.
|
|
45
|
+
|
|
46
|
+
Columns
|
|
47
|
+
-------
|
|
48
|
+
id : int
|
|
49
|
+
Auto-incremented surrogate primary key. Not exposed to application
|
|
50
|
+
code; use ``serial_number`` as the business key.
|
|
51
|
+
serial_number : str
|
|
52
|
+
X.509 serial number stored as a decimal string. Unique and indexed.
|
|
53
|
+
String storage avoids integer overflow for 160-bit serials (RFC 5280).
|
|
54
|
+
common_name : str
|
|
55
|
+
Common Name (CN) extracted from the certificate Subject at issuance
|
|
56
|
+
time. Not unique; the same CN may appear across different certificate
|
|
57
|
+
generations (e.g. after rotation).
|
|
58
|
+
status : str
|
|
59
|
+
Current lifecycle state. One of the ``CertificateStatus`` values:
|
|
60
|
+
``"valid"``, ``"revoked"``, ``"expired"``, or ``"unknown"``.
|
|
61
|
+
Defaults to ``CertificateStatus.VALID`` on insertion.
|
|
62
|
+
not_valid_before : datetime
|
|
63
|
+
Start of the certificate's validity period (UTC, naive datetime as
|
|
64
|
+
stored by SQLAlchemy's ``DateTime`` column type).
|
|
65
|
+
not_valid_after : datetime
|
|
66
|
+
End of the certificate's validity period (UTC, naive datetime).
|
|
67
|
+
Indexed to allow efficient queries for expired certificates.
|
|
68
|
+
key_type : str
|
|
69
|
+
Certificate category stored as the ``CertType`` enum's string value
|
|
70
|
+
(e.g. ``"ca"``, ``"device"``, ``"service"``).
|
|
71
|
+
Defaults to ``CertType.DEVICE.value``.
|
|
72
|
+
certificate_pem : str
|
|
73
|
+
Full PEM-encoded public certificate. Allows reconstruction of the
|
|
74
|
+
``x509.Certificate`` object without accessing the filesystem.
|
|
75
|
+
revocation_date : datetime | None
|
|
76
|
+
UTC timestamp at which the certificate was revoked. ``None`` for
|
|
77
|
+
non-revoked certificates.
|
|
78
|
+
revocation_reason : int | None
|
|
79
|
+
RFC 5280 §5.3.1 revocation reason code stored as an integer.
|
|
80
|
+
``None`` for non-revoked certificates. Maps to the integer value
|
|
81
|
+
of the corresponding ``x509.ReasonFlags`` member.
|
|
82
|
+
uuid : str | None
|
|
83
|
+
UUID string that identifies the filesystem folder (managed by
|
|
84
|
+
``BaseStorage``) holding the ``.pem``, ``.key``, and ``.csr`` files
|
|
85
|
+
for this certificate. ``None`` if no filesystem artefacts exist.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
__tablename__ = "certificates"
|
|
89
|
+
|
|
90
|
+
id = Column(Integer, primary_key=True)
|
|
91
|
+
|
|
92
|
+
serial_number = Column(
|
|
93
|
+
String,
|
|
94
|
+
unique=True,
|
|
95
|
+
nullable=False,
|
|
96
|
+
index=True,
|
|
97
|
+
comment="X.509 serial number as a decimal string (up to 160-bit value).",
|
|
98
|
+
)
|
|
99
|
+
common_name = Column(
|
|
100
|
+
String,
|
|
101
|
+
nullable=False,
|
|
102
|
+
comment="CN extracted from the certificate Subject at issuance time.",
|
|
103
|
+
)
|
|
104
|
+
status: Mapped[str] = Column( # type: ignore
|
|
105
|
+
String,
|
|
106
|
+
default=CertificateStatus.VALID,
|
|
107
|
+
comment="Lifecycle state: valid | revoked | expired | unknown.",
|
|
108
|
+
)
|
|
109
|
+
not_valid_before = Column(
|
|
110
|
+
DateTime,
|
|
111
|
+
nullable=False,
|
|
112
|
+
comment="Start of the certificate validity window (UTC).",
|
|
113
|
+
)
|
|
114
|
+
not_valid_after = Column(
|
|
115
|
+
DateTime,
|
|
116
|
+
nullable=False,
|
|
117
|
+
index=True,
|
|
118
|
+
comment="End of the certificate validity window (UTC). Indexed for expiry queries.",
|
|
119
|
+
)
|
|
120
|
+
key_type = Column(
|
|
121
|
+
String,
|
|
122
|
+
default=CertType.DEVICE.value,
|
|
123
|
+
comment="Certificate category: ca | device | service | user | internal.",
|
|
124
|
+
)
|
|
125
|
+
certificate_pem = Column(
|
|
126
|
+
String,
|
|
127
|
+
nullable=False,
|
|
128
|
+
comment="Full PEM-encoded public certificate for offline reconstruction.",
|
|
129
|
+
)
|
|
130
|
+
revocation_date = Column(
|
|
131
|
+
DateTime,
|
|
132
|
+
nullable=True,
|
|
133
|
+
comment="UTC timestamp of revocation. NULL for non-revoked certificates.",
|
|
134
|
+
)
|
|
135
|
+
revocation_reason: Mapped[str] = Column( # type: ignore
|
|
136
|
+
String,
|
|
137
|
+
nullable=True,
|
|
138
|
+
comment="RFC 5280 §5.3.1 reason code integer. NULL for non-revoked certificates.",
|
|
139
|
+
)
|
|
140
|
+
uuid = Column(
|
|
141
|
+
String,
|
|
142
|
+
unique=True,
|
|
143
|
+
comment="Filesystem folder UUID linking this record to its storage artefacts.",
|
|
144
|
+
)
|