tiny-ca 0.2.0__tar.gz → 0.3.0__tar.gz
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-0.2.0 → tiny_ca-0.3.0}/LICENSE +1 -1
- {tiny_ca-0.2.0/tiny_ca.egg-info → tiny_ca-0.3.0}/PKG-INFO +125 -26
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/README.md +122 -21
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/pyproject.toml +44 -22
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tests/test_afile_loader.py +23 -4
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tests/test_async_db_handler.py +16 -14
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tests/test_async_lifecycle_manager.py +1 -1
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tests/test_async_local_storage.py +3 -3
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tests/test_database_manager.py +6 -2
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tests/test_factory.py +6 -4
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tests/test_file_loader.py +22 -6
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tests/test_life_time.py +17 -29
- tiny_ca-0.3.0/tests/test_review_regressions.py +851 -0
- tiny_ca-0.3.0/tests/test_review_regressions_async.py +269 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tests/test_serial.py +7 -19
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tests/test_sync_db_manager.py +20 -18
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/__init__.py +3 -3
- tiny_ca-0.3.0/tiny_ca/ca_factory/factory.py +1223 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/ca_factory/utils/afile_loader.py +10 -7
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/ca_factory/utils/file_loader.py +13 -10
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/ca_factory/utils/life_time.py +4 -2
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/const.py +28 -1
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/db/async_db_manager.py +93 -212
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/db/sync_db_manager.py +87 -171
- tiny_ca-0.3.0/tiny_ca/db/utils.py +67 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/exc.py +33 -0
- tiny_ca-0.3.0/tiny_ca/managers/async_lifecycle_manager.py +652 -0
- tiny_ca-0.3.0/tiny_ca/managers/sync_lifecycle_manager.py +945 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/models/certificate.py +42 -5
- tiny_ca-0.3.0/tiny_ca/settings.py +15 -0
- tiny_ca-0.3.0/tiny_ca/storage/async_local_storage.py +118 -0
- tiny_ca-0.3.0/tiny_ca/storage/const.py +40 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/storage/local_storage.py +227 -201
- tiny_ca-0.3.0/tiny_ca/utils/keys.py +101 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/utils/serial_generator.py +40 -21
- {tiny_ca-0.2.0 → tiny_ca-0.3.0/tiny_ca.egg-info}/PKG-INFO +125 -26
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca.egg-info/SOURCES.txt +4 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca.egg-info/requires.txt +1 -3
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca.egg-info/top_level.txt +1 -0
- tiny_ca-0.2.0/tiny_ca/ca_factory/factory.py +0 -1307
- tiny_ca-0.2.0/tiny_ca/managers/async_lifecycle_manager.py +0 -871
- tiny_ca-0.2.0/tiny_ca/managers/sync_lifecycle_manager.py +0 -885
- tiny_ca-0.2.0/tiny_ca/settings.py +0 -9
- tiny_ca-0.2.0/tiny_ca/storage/async_local_storage.py +0 -218
- tiny_ca-0.2.0/tiny_ca/storage/const.py +0 -39
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/docs/source/conf.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/setup.cfg +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tests/test_base_db.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tests/test_base_storage.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tests/test_db_const.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tests/test_local_storage.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tests/test_models.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tests/test_serial_generator.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tests/test_storage_const.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tests/test_sync_lifecycle_manager.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/ca_factory/__init__.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/ca_factory/utils/__init__.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/ca_factory/utils/serial.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/db/__init__.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/db/base_db.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/db/const.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/db/models.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/managers/__init__.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/models/__init__.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/storage/__init__.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/storage/base_storage.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca/utils/__init__.py +0 -0
- {tiny_ca-0.2.0 → tiny_ca-0.3.0}/tiny_ca.egg-info/dependency_links.txt +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tiny-ca
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A lightweight, 100%-tested Python library for managing the full lifecycle of X.509 certificates — issuance, revocation, rotation, CRL generation, PKCS#12 export, and more.
|
|
5
5
|
Author-email: Denis Shchutskyi <denisshchutskyi@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Repository, https://github.com/Shchusia/tiny_ca
|
|
8
8
|
Project-URL: Documentation, https://shchusia.github.io/tiny_ca/
|
|
9
9
|
Project-URL: Bug Tracker, https://github.com/Shchusia/tiny_ca/issues
|
|
10
|
-
Project-URL: Changelog, https://github.com/Shchusia/tiny_ca/blob/
|
|
10
|
+
Project-URL: Changelog, https://github.com/Shchusia/tiny_ca/blob/master/CHANGELOG.md
|
|
11
11
|
Keywords: pki,x509,certificate,ca,tls,ssl,cryptography,crl,pkcs12
|
|
12
12
|
Classifier: License :: OSI Approved :: MIT License
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -23,11 +23,9 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
License-File: LICENSE
|
|
24
24
|
Requires-Dist: cryptography>=46.0.0
|
|
25
25
|
Requires-Dist: pydantic[email]>=2.12.0
|
|
26
|
-
Requires-Dist: sqlalchemy>=2.0.0
|
|
26
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.0
|
|
27
27
|
Provides-Extra: async
|
|
28
|
-
Requires-Dist: aiofiles>=25.1.0; extra == "async"
|
|
29
28
|
Requires-Dist: aiosqlite>=0.22.0; extra == "async"
|
|
30
|
-
Requires-Dist: types-aiofiles>=25.1.0; extra == "async"
|
|
31
29
|
Provides-Extra: postgres
|
|
32
30
|
Requires-Dist: psycopg2-binary>=2.9.0; extra == "postgres"
|
|
33
31
|
Provides-Extra: postgres-async
|
|
@@ -42,8 +40,9 @@ Dynamic: license-file
|
|
|
42
40
|
|
|
43
41
|
# tiny_ca
|
|
44
42
|
|
|
45
|
-
[](https://pypi.org/project/tiny-ca/)
|
|
46
44
|
[](https://pypi.org/project/tiny-ca/)
|
|
45
|
+
|
|
47
46
|
[](https://coveralls.io/github/Shchusia/tiny_ca?branch=feature/docs)
|
|
48
47
|
[](https://shchusia.github.io/tiny_ca/)
|
|
49
48
|
[](LICENSE)
|
|
@@ -52,6 +51,43 @@ A lightweight, **100%-tested** Python library for managing the full lifecycle of
|
|
|
52
51
|
|
|
53
52
|
---
|
|
54
53
|
|
|
54
|
+
## REST API
|
|
55
|
+
|
|
56
|
+
> Looking for a ready-to-use HTTP server built on **tiny-ca**?
|
|
57
|
+
> Check out **[tiny-ca-gateway](https://github.com/Shchusia/tiny-ca-gateway)** — a framework-agnostic REST API adapter that exposes all 22 certificate lifecycle endpoints with no extra code.
|
|
58
|
+
|
|
59
|
+
| | |
|
|
60
|
+
|---|---|
|
|
61
|
+
| **GitHub** | [github.com/Shchusia/tiny-ca-gateway](https://github.com/Shchusia/tiny-ca-gateway) |
|
|
62
|
+
| **Integration guide** | [tiny-ca-gateway/blob/master/README.md](https://github.com/Shchusia/tiny-ca-gateway/blob/master/README.md) |
|
|
63
|
+
| **Supported frameworks** | FastAPI · Flask · aiohttp · Django Ninja |
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install "tiny-ca-gateway[fastapi]" # FastAPI + Uvicorn
|
|
67
|
+
pip install "tiny-ca-gateway[flask]" # Flask + Gunicorn
|
|
68
|
+
pip install "tiny-ca-gateway[aiohttp]" # aiohttp
|
|
69
|
+
pip install "tiny-ca-gateway[django]" # Django + Django Ninja
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
# FastAPI — all 22 CA endpoints in 5 lines
|
|
74
|
+
from fastapi import FastAPI
|
|
75
|
+
from contextlib import asynccontextmanager
|
|
76
|
+
from tiny_ca_gateway.fastapi.lifespan.manager import FastAPILifespanManager
|
|
77
|
+
from tiny_ca_gateway.fastapi.api.v1.ca_routes import router
|
|
78
|
+
|
|
79
|
+
@asynccontextmanager
|
|
80
|
+
async def lifespan(app: FastAPI):
|
|
81
|
+
await FastAPILifespanManager(common_name="My CA").on_startup()
|
|
82
|
+
yield
|
|
83
|
+
|
|
84
|
+
app = FastAPI(lifespan=lifespan)
|
|
85
|
+
app.include_router(router, prefix="/api/v1")
|
|
86
|
+
# → Swagger UI at http://localhost:8000/docs
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
55
91
|
## Table of Contents
|
|
56
92
|
|
|
57
93
|
- [Features](#features)
|
|
@@ -80,14 +116,16 @@ A lightweight, **100%-tested** Python library for managing the full lifecycle of
|
|
|
80
116
|
|---|---|
|
|
81
117
|
| **CA bootstrap** | Self-signed root CA and intermediate (sub) CA with configurable `path_length` |
|
|
82
118
|
| **Issuance** | Leaf certificates with SANs (DNS + IP), EKU (Server/Client Auth), email attribute |
|
|
83
|
-
| **
|
|
84
|
-
| **
|
|
119
|
+
| **Key algorithms** | RSA (2048+), ECDSA P-256 / P-384, Ed25519 — for the CA and for leaves |
|
|
120
|
+
| **Lifecycle** | Revoke (RFC 5280 reasons), renew (same key), rotate (new key), hard-delete — every CA-signed certificate is registered, so it can always be revoked |
|
|
121
|
+
| **CRL** | RFC 5280 CRLs with `CRLNumber`, AKI and per-entry reason codes; optional CRL Distribution Points / AIA in issued certificates |
|
|
122
|
+
| **Validation** | Direct (`validate_cert`) or through intermediates (`validate_chain`, with `BasicConstraints` / `pathLen` checks); registry check is fail-closed |
|
|
85
123
|
| **Inspection** | Structured `CertificateDetails` snapshot from any `x509.Certificate` — fully serialisable |
|
|
86
124
|
| **Export** | PKCS#12 (`.p12`/`.pfx`) bundles with full CA chain |
|
|
87
125
|
| **Co-signing** | Re-sign third-party certificates under your CA, preserving Subject and extensions |
|
|
88
126
|
| **Chain** | Build `[leaf, ca]` fullchain ready for nginx, Apache, or Envoy |
|
|
89
127
|
| **Monitoring** | List certs with filters, find expiring-soon, bulk-expire stale records |
|
|
90
|
-
| **Storage** | `LocalStorage` (sync) and `AsyncLocalStorage` (async
|
|
128
|
+
| **Storage** | `LocalStorage` (sync) and `AsyncLocalStorage` (async) with UUID-based isolation, `0600` private keys and path-traversal protection |
|
|
91
129
|
| **Database** | `SyncDBHandler` (SQLAlchemy) and `AsyncDBHandler` (aiosqlite) — SQLite, PostgreSQL, MySQL |
|
|
92
130
|
| **API parity** | `CertLifecycleManager` and `AsyncCertLifecycleManager` are feature-identical |
|
|
93
131
|
| **Serial numbers** | `SerialWithEncoding` — 160-bit RFC 5280-compliant, type-prefixed, name-encoded |
|
|
@@ -99,7 +137,7 @@ A lightweight, **100%-tested** Python library for managing the full lifecycle of
|
|
|
99
137
|
|
|
100
138
|
- **Python** 3.11 or higher
|
|
101
139
|
- **Core:** `cryptography >= 46`, `sqlalchemy >= 2`, `pydantic >= 2`
|
|
102
|
-
- **Async extras:** `
|
|
140
|
+
- **Async extras:** `aiosqlite`
|
|
103
141
|
- **PostgreSQL:** `psycopg2-binary` (sync) / `asyncpg` (async)
|
|
104
142
|
- **MySQL:** `pymysql` (sync) / `aiomysql` (async)
|
|
105
143
|
|
|
@@ -120,7 +158,7 @@ CertLifecycleManager / AsyncCertLifecycleManager ← application entry-point
|
|
|
120
158
|
│
|
|
121
159
|
├── BaseStorage (ABC)
|
|
122
160
|
│ ├── LocalStorage ← sync filesystem (UUID-isolated)
|
|
123
|
-
│ └── AsyncLocalStorage ← async filesystem (
|
|
161
|
+
│ └── AsyncLocalStorage ← async filesystem (worker threads)
|
|
124
162
|
│
|
|
125
163
|
└── BaseDB (ABC)
|
|
126
164
|
├── SyncDBHandler ← SQLAlchemy sync
|
|
@@ -164,7 +202,13 @@ from tiny_ca.storage.local_storage import LocalStorage
|
|
|
164
202
|
from tiny_ca.db.sync_db_manager import SyncDBHandler
|
|
165
203
|
from tiny_ca.models.certificate import CAConfig
|
|
166
204
|
|
|
167
|
-
|
|
205
|
+
from cryptography.hazmat.primitives.serialization import BestAvailableEncryption
|
|
206
|
+
|
|
207
|
+
# Private keys are always written with mode 0600; encrypt them at rest too.
|
|
208
|
+
storage = LocalStorage(
|
|
209
|
+
base_folder="./pki",
|
|
210
|
+
base_encryption_algorithm=BestAvailableEncryption(b"key-passphrase"),
|
|
211
|
+
)
|
|
168
212
|
db = SyncDBHandler(db_url="sqlite:///pki.db")
|
|
169
213
|
mgr = CertLifecycleManager(storage=storage, db_handler=db)
|
|
170
214
|
|
|
@@ -180,10 +224,19 @@ Attach the factory so the manager can issue certificates:
|
|
|
180
224
|
from tiny_ca.ca_factory.utils.file_loader import CAFileLoader
|
|
181
225
|
from tiny_ca.ca_factory.factory import CertificateFactory
|
|
182
226
|
|
|
183
|
-
loader = CAFileLoader(ca_cert_path=cert_path, ca_key_path=key_path
|
|
184
|
-
|
|
227
|
+
loader = CAFileLoader(ca_cert_path=cert_path, ca_key_path=key_path,
|
|
228
|
+
ca_key_password=b"key-passphrase")
|
|
229
|
+
mgr.factory = CertificateFactory(
|
|
230
|
+
loader,
|
|
231
|
+
# Optional: tell relying parties where to find the CRL / CA certificate.
|
|
232
|
+
crl_distribution_points=["http://pki.example/crl.pem"],
|
|
233
|
+
ca_issuers_urls=["http://pki.example/ca.pem"],
|
|
234
|
+
)
|
|
185
235
|
```
|
|
186
236
|
|
|
237
|
+
`CAConfig` / `ClientConfig` accept `key_algorithm=KeyAlgorithm.EC_P256`
|
|
238
|
+
(also `EC_P384`, `ED25519`, default `RSA`) — see [Configuration Models](#configuration-models).
|
|
239
|
+
|
|
187
240
|
### 2. Issue a Leaf Certificate
|
|
188
241
|
|
|
189
242
|
```python
|
|
@@ -207,7 +260,9 @@ cert, key, csr = mgr.issue_certificate(
|
|
|
207
260
|
### 3. Renew a Certificate (same key)
|
|
208
261
|
|
|
209
262
|
Keeps the existing public key — only validity window and serial number change.
|
|
210
|
-
Use when the key has **not** been compromised.
|
|
263
|
+
Use when the key has **not** been compromised. The renewed certificate is
|
|
264
|
+
stored and registered (so it can be revoked); the original is revoked with
|
|
265
|
+
reason `superseded`. The private key stays in the original folder.
|
|
211
266
|
|
|
212
267
|
```python
|
|
213
268
|
renewed = mgr.renew_certificate(serial=cert.serial_number, days_valid=365)
|
|
@@ -215,7 +270,9 @@ renewed = mgr.renew_certificate(serial=cert.serial_number, days_valid=365)
|
|
|
215
270
|
|
|
216
271
|
### 4. Rotate a Certificate (new key)
|
|
217
272
|
|
|
218
|
-
|
|
273
|
+
Issues a replacement with a fresh key pair, then revokes the old cert
|
|
274
|
+
(`superseded`). If the old cert cannot be revoked, the replacement is rolled
|
|
275
|
+
back and `CertRevocationError` is raised.
|
|
219
276
|
|
|
220
277
|
```python
|
|
221
278
|
new_cert, new_key, new_csr = mgr.rotate_certificate(
|
|
@@ -240,6 +297,10 @@ crl = mgr.generate_crl(days_valid=7) # written to <base_folder>/crl.pem
|
|
|
240
297
|
mgr.verify_crl(crl) # raises ValidationCertError on failure
|
|
241
298
|
```
|
|
242
299
|
|
|
300
|
+
Revoked certificates stay on the CRL until they expire. If the database
|
|
301
|
+
cannot be read, `generate_crl` raises `DBOperationError` and the previous
|
|
302
|
+
`crl.pem` is left untouched — an empty CRL is never published.
|
|
303
|
+
|
|
243
304
|
### 7. Issue an Intermediate CA
|
|
244
305
|
|
|
245
306
|
```python
|
|
@@ -248,6 +309,10 @@ sub_ca_cert, sub_ca_key = mgr.issue_intermediate_ca(
|
|
|
248
309
|
path_length=0, organization="ACME Corp", country="UA",
|
|
249
310
|
cert_path="intermediate",
|
|
250
311
|
)
|
|
312
|
+
|
|
313
|
+
# The intermediate is registered (revocable via the root's CRL). Verify a
|
|
314
|
+
# leaf issued by it through the chain:
|
|
315
|
+
mgr.verify_certificate(leaf_cert, intermediates=[sub_ca_cert])
|
|
251
316
|
```
|
|
252
317
|
|
|
253
318
|
### 8. Export PKCS#12
|
|
@@ -265,6 +330,7 @@ with open("nginx.p12", "wb") as f:
|
|
|
265
330
|
from cryptography import x509
|
|
266
331
|
third_party = x509.load_pem_x509_certificate(open("partner.pem", "rb").read())
|
|
267
332
|
cosigned = mgr.cosign_certificate(cert=third_party, days_valid=365)
|
|
333
|
+
# stored + registered like any other certificate issued by this CA
|
|
268
334
|
```
|
|
269
335
|
|
|
270
336
|
### 10. Inspect a Certificate
|
|
@@ -349,7 +415,8 @@ python examples/acomplete_example.py
|
|
|
349
415
|
| `common_name` | `str` | `"Internal CA"` | CA Common Name (CN) |
|
|
350
416
|
| `organization` | `str` | `"My Company"` | Organization (O) |
|
|
351
417
|
| `country` | `str` | `"UA"` | Two-letter ISO 3166-1 alpha-2 code |
|
|
352
|
-
| `key_size` | `int` | `2048` | RSA key length in bits |
|
|
418
|
+
| `key_size` | `int` | `2048` | RSA key length in bits (≥ 2048; ignored for EC / Ed25519) |
|
|
419
|
+
| `key_algorithm` | `KeyAlgorithm` | `RSA` | `RSA`, `EC_P256`, `EC_P384` or `ED25519` |
|
|
353
420
|
| `days_valid` | `int` | `3650` | Validity period in days |
|
|
354
421
|
| `valid_from` | `datetime \| None` | `None` | Explicit UTC start; `None` = now |
|
|
355
422
|
|
|
@@ -359,14 +426,15 @@ python examples/acomplete_example.py
|
|
|
359
426
|
|---|---|---|---|
|
|
360
427
|
| `common_name` | `str` | — | Certificate CN |
|
|
361
428
|
| `serial_type` | `CertType` | `SERVICE` | Certificate category |
|
|
362
|
-
| `key_size` | `int` | `2048` | RSA key length |
|
|
429
|
+
| `key_size` | `int` | `2048` | RSA key length (≥ 2048; ignored for EC / Ed25519) |
|
|
430
|
+
| `key_algorithm` | `KeyAlgorithm` | `RSA` | `RSA`, `EC_P256`, `EC_P384` or `ED25519` |
|
|
363
431
|
| `days_valid` | `int` | `3650` | Validity period |
|
|
364
432
|
| `email` | `EmailStr \| None` | `None` | `emailAddress` Subject attribute |
|
|
365
433
|
| `is_server_cert` | `bool` | `True` | Adds ServerAuth EKU + CN as DNS SAN |
|
|
366
434
|
| `is_client_cert` | `bool` | `False` | Adds ClientAuth EKU |
|
|
367
435
|
| `san_dns` | `list[str] \| None` | `None` | Extra DNS Subject Alternative Names |
|
|
368
436
|
| `san_ip` | `list[str] \| None` | `None` | IP address SANs |
|
|
369
|
-
| `name` | `str \| None` | `None` | Override output file base name |
|
|
437
|
+
| `name` | `str \| None` | `None` | Override output file base name (letters, digits, `.`, `-`, `_`; no leading dot) |
|
|
370
438
|
|
|
371
439
|
### `CertType`
|
|
372
440
|
|
|
@@ -393,14 +461,23 @@ storage = LocalStorage(base_folder="./pki")
|
|
|
393
461
|
./pki/
|
|
394
462
|
└── [cert_path/]
|
|
395
463
|
└── <uuid>/
|
|
396
|
-
├── nginx.pem ← x509.Certificate
|
|
397
|
-
├── nginx.key ←
|
|
398
|
-
└── nginx.csr ← CSR
|
|
464
|
+
├── nginx.pem ← x509.Certificate (0644)
|
|
465
|
+
├── nginx.key ← private key (0600)
|
|
466
|
+
└── nginx.csr ← CSR (0644)
|
|
399
467
|
```
|
|
400
468
|
|
|
469
|
+
- Private keys are written with mode `0600`. They are **unencrypted unless**
|
|
470
|
+
`base_encryption_algorithm=BestAvailableEncryption(password)` is passed — a
|
|
471
|
+
warning is logged once per storage instance otherwise.
|
|
472
|
+
- `file_name`, `cert_path` and `uuid_str` are validated: absolute paths, `..`
|
|
473
|
+
segments and backslashes raise `InvalidPathError`.
|
|
474
|
+
- Without `is_overwrite=True` files are created with `O_EXCL`, so an existing
|
|
475
|
+
file is never replaced.
|
|
476
|
+
|
|
401
477
|
### `AsyncLocalStorage` (async)
|
|
402
478
|
|
|
403
|
-
Drop-in async replacement — same constructor, same layout
|
|
479
|
+
Drop-in async replacement — same constructor, same layout and the same
|
|
480
|
+
security properties; blocking I/O runs in worker threads.
|
|
404
481
|
|
|
405
482
|
---
|
|
406
483
|
|
|
@@ -431,6 +508,13 @@ await db._db.init_db()
|
|
|
431
508
|
| `delete_by_uuid(uuid)` | Hard-delete a record |
|
|
432
509
|
| `update_status_expired()` | Bulk-mark stale VALID records as EXPIRED |
|
|
433
510
|
|
|
511
|
+
Read methods (and `update_status_expired`) raise `DBOperationError` on any
|
|
512
|
+
database failure instead of returning `None` / an empty list — a CA must fail
|
|
513
|
+
closed. `register_cert_in_db` returns `False` on failure; the managers turn
|
|
514
|
+
that into `DBOperationError` and remove the artefacts they wrote. Datetimes
|
|
515
|
+
are stored as naive UTC (`DateTime` columns without time zone), which keeps
|
|
516
|
+
PostgreSQL / asyncpg happy.
|
|
517
|
+
|
|
434
518
|
---
|
|
435
519
|
|
|
436
520
|
## Serial Number Encoding
|
|
@@ -440,7 +524,7 @@ await db._db.init_db()
|
|
|
440
524
|
```
|
|
441
525
|
┌──────────────┬──────────────────────┬────────────────────┐
|
|
442
526
|
│ 16-bit type │ 80-bit name │ 64-bit random │
|
|
443
|
-
│ prefix │ (up to 10 chars) │ (
|
|
527
|
+
│ prefix │ (up to 10 chars) │ (secrets.randbits)│
|
|
444
528
|
└──────────────┴──────────────────────┴────────────────────┘
|
|
445
529
|
```
|
|
446
530
|
|
|
@@ -453,6 +537,10 @@ cert_type, name = SerialWithEncoding.parse(serial)
|
|
|
453
537
|
# cert_type == CertType.SERVICE, name == "nginx"
|
|
454
538
|
```
|
|
455
539
|
|
|
540
|
+
Non-ASCII characters are stored as `?`, so a Cyrillic CN can never overflow
|
|
541
|
+
into the type prefix. The top bit is always zero, so the DER encoding fits the
|
|
542
|
+
20-octet limit of RFC 5280.
|
|
543
|
+
|
|
456
544
|
---
|
|
457
545
|
|
|
458
546
|
## Error Reference
|
|
@@ -469,6 +557,9 @@ cert_type, name = SerialWithEncoding.parse(serial)
|
|
|
469
557
|
| `IsNotFile` | CA PEM path is a directory | Provide a file, not a directory |
|
|
470
558
|
| `WrongType` | Unsupported file extension | Use `.pem`, `.key`, or `.csr` |
|
|
471
559
|
| `ErrorLoadCert` | PEM deserialisation failed | Check file format and integrity |
|
|
560
|
+
| `DBOperationError` | Registry could not be read / written | Check the database; nothing was trusted or published |
|
|
561
|
+
| `CertRevocationError` | Rotation/renewal could not revoke the old cert | Retry; the replacement was rolled back |
|
|
562
|
+
| `InvalidPathError` | Storage path would escape `base_folder` | Use plain names / relative sub-paths |
|
|
472
563
|
|
|
473
564
|
---
|
|
474
565
|
|
|
@@ -478,7 +569,16 @@ cert_type, name = SerialWithEncoding.parse(serial)
|
|
|
478
569
|
Yes — load it with `CAFileLoader` or `AsyncCAFileLoader`.
|
|
479
570
|
|
|
480
571
|
**What's the difference between `renew` and `rotate`?**
|
|
481
|
-
`renew` keeps the key pair and extends validity. `rotate` generates a new key
|
|
572
|
+
`renew` keeps the key pair and extends validity. `rotate` generates a new key. Both revoke the old cert with reason `superseded`.
|
|
573
|
+
|
|
574
|
+
**Why does `verify_certificate` reject a certificate signed by my CA?**
|
|
575
|
+
Since 0.3.0 a certificate issued directly by the CA must also be present in the
|
|
576
|
+
registry — otherwise its revocation status is unknown. Certificates created
|
|
577
|
+
with older versions via `renew` / `cosign` were not registered; re-issue them.
|
|
578
|
+
|
|
579
|
+
**How do I see the library's logs?**
|
|
580
|
+
`tiny_ca` only attaches a `NullHandler`; configure the `"TinyCA"` logger in your
|
|
581
|
+
application, e.g. `logging.getLogger("TinyCA").setLevel(logging.INFO)`.
|
|
482
582
|
|
|
483
583
|
**How do I schedule CRL regeneration?**
|
|
484
584
|
Use APScheduler or any cron solution:
|
|
@@ -586,4 +686,3 @@ Do **not** open public issues for security vulnerabilities. Email the maintainer
|
|
|
586
686
|
| SQLAlchemy | MIT |
|
|
587
687
|
| Pydantic | MIT |
|
|
588
688
|
| aiosqlite | MIT |
|
|
589
|
-
| aiofiles | Apache 2.0 |
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# tiny_ca
|
|
2
2
|
|
|
3
|
-
[](https://pypi.org/project/tiny-ca/)
|
|
4
4
|
[](https://pypi.org/project/tiny-ca/)
|
|
5
|
+
|
|
5
6
|
[](https://coveralls.io/github/Shchusia/tiny_ca?branch=feature/docs)
|
|
6
7
|
[](https://shchusia.github.io/tiny_ca/)
|
|
7
8
|
[](LICENSE)
|
|
@@ -10,6 +11,43 @@ A lightweight, **100%-tested** Python library for managing the full lifecycle of
|
|
|
10
11
|
|
|
11
12
|
---
|
|
12
13
|
|
|
14
|
+
## REST API
|
|
15
|
+
|
|
16
|
+
> Looking for a ready-to-use HTTP server built on **tiny-ca**?
|
|
17
|
+
> Check out **[tiny-ca-gateway](https://github.com/Shchusia/tiny-ca-gateway)** — a framework-agnostic REST API adapter that exposes all 22 certificate lifecycle endpoints with no extra code.
|
|
18
|
+
|
|
19
|
+
| | |
|
|
20
|
+
|---|---|
|
|
21
|
+
| **GitHub** | [github.com/Shchusia/tiny-ca-gateway](https://github.com/Shchusia/tiny-ca-gateway) |
|
|
22
|
+
| **Integration guide** | [tiny-ca-gateway/blob/master/README.md](https://github.com/Shchusia/tiny-ca-gateway/blob/master/README.md) |
|
|
23
|
+
| **Supported frameworks** | FastAPI · Flask · aiohttp · Django Ninja |
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install "tiny-ca-gateway[fastapi]" # FastAPI + Uvicorn
|
|
27
|
+
pip install "tiny-ca-gateway[flask]" # Flask + Gunicorn
|
|
28
|
+
pip install "tiny-ca-gateway[aiohttp]" # aiohttp
|
|
29
|
+
pip install "tiny-ca-gateway[django]" # Django + Django Ninja
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
# FastAPI — all 22 CA endpoints in 5 lines
|
|
34
|
+
from fastapi import FastAPI
|
|
35
|
+
from contextlib import asynccontextmanager
|
|
36
|
+
from tiny_ca_gateway.fastapi.lifespan.manager import FastAPILifespanManager
|
|
37
|
+
from tiny_ca_gateway.fastapi.api.v1.ca_routes import router
|
|
38
|
+
|
|
39
|
+
@asynccontextmanager
|
|
40
|
+
async def lifespan(app: FastAPI):
|
|
41
|
+
await FastAPILifespanManager(common_name="My CA").on_startup()
|
|
42
|
+
yield
|
|
43
|
+
|
|
44
|
+
app = FastAPI(lifespan=lifespan)
|
|
45
|
+
app.include_router(router, prefix="/api/v1")
|
|
46
|
+
# → Swagger UI at http://localhost:8000/docs
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
13
51
|
## Table of Contents
|
|
14
52
|
|
|
15
53
|
- [Features](#features)
|
|
@@ -38,14 +76,16 @@ A lightweight, **100%-tested** Python library for managing the full lifecycle of
|
|
|
38
76
|
|---|---|
|
|
39
77
|
| **CA bootstrap** | Self-signed root CA and intermediate (sub) CA with configurable `path_length` |
|
|
40
78
|
| **Issuance** | Leaf certificates with SANs (DNS + IP), EKU (Server/Client Auth), email attribute |
|
|
41
|
-
| **
|
|
42
|
-
| **
|
|
79
|
+
| **Key algorithms** | RSA (2048+), ECDSA P-256 / P-384, Ed25519 — for the CA and for leaves |
|
|
80
|
+
| **Lifecycle** | Revoke (RFC 5280 reasons), renew (same key), rotate (new key), hard-delete — every CA-signed certificate is registered, so it can always be revoked |
|
|
81
|
+
| **CRL** | RFC 5280 CRLs with `CRLNumber`, AKI and per-entry reason codes; optional CRL Distribution Points / AIA in issued certificates |
|
|
82
|
+
| **Validation** | Direct (`validate_cert`) or through intermediates (`validate_chain`, with `BasicConstraints` / `pathLen` checks); registry check is fail-closed |
|
|
43
83
|
| **Inspection** | Structured `CertificateDetails` snapshot from any `x509.Certificate` — fully serialisable |
|
|
44
84
|
| **Export** | PKCS#12 (`.p12`/`.pfx`) bundles with full CA chain |
|
|
45
85
|
| **Co-signing** | Re-sign third-party certificates under your CA, preserving Subject and extensions |
|
|
46
86
|
| **Chain** | Build `[leaf, ca]` fullchain ready for nginx, Apache, or Envoy |
|
|
47
87
|
| **Monitoring** | List certs with filters, find expiring-soon, bulk-expire stale records |
|
|
48
|
-
| **Storage** | `LocalStorage` (sync) and `AsyncLocalStorage` (async
|
|
88
|
+
| **Storage** | `LocalStorage` (sync) and `AsyncLocalStorage` (async) with UUID-based isolation, `0600` private keys and path-traversal protection |
|
|
49
89
|
| **Database** | `SyncDBHandler` (SQLAlchemy) and `AsyncDBHandler` (aiosqlite) — SQLite, PostgreSQL, MySQL |
|
|
50
90
|
| **API parity** | `CertLifecycleManager` and `AsyncCertLifecycleManager` are feature-identical |
|
|
51
91
|
| **Serial numbers** | `SerialWithEncoding` — 160-bit RFC 5280-compliant, type-prefixed, name-encoded |
|
|
@@ -57,7 +97,7 @@ A lightweight, **100%-tested** Python library for managing the full lifecycle of
|
|
|
57
97
|
|
|
58
98
|
- **Python** 3.11 or higher
|
|
59
99
|
- **Core:** `cryptography >= 46`, `sqlalchemy >= 2`, `pydantic >= 2`
|
|
60
|
-
- **Async extras:** `
|
|
100
|
+
- **Async extras:** `aiosqlite`
|
|
61
101
|
- **PostgreSQL:** `psycopg2-binary` (sync) / `asyncpg` (async)
|
|
62
102
|
- **MySQL:** `pymysql` (sync) / `aiomysql` (async)
|
|
63
103
|
|
|
@@ -78,7 +118,7 @@ CertLifecycleManager / AsyncCertLifecycleManager ← application entry-point
|
|
|
78
118
|
│
|
|
79
119
|
├── BaseStorage (ABC)
|
|
80
120
|
│ ├── LocalStorage ← sync filesystem (UUID-isolated)
|
|
81
|
-
│ └── AsyncLocalStorage ← async filesystem (
|
|
121
|
+
│ └── AsyncLocalStorage ← async filesystem (worker threads)
|
|
82
122
|
│
|
|
83
123
|
└── BaseDB (ABC)
|
|
84
124
|
├── SyncDBHandler ← SQLAlchemy sync
|
|
@@ -122,7 +162,13 @@ from tiny_ca.storage.local_storage import LocalStorage
|
|
|
122
162
|
from tiny_ca.db.sync_db_manager import SyncDBHandler
|
|
123
163
|
from tiny_ca.models.certificate import CAConfig
|
|
124
164
|
|
|
125
|
-
|
|
165
|
+
from cryptography.hazmat.primitives.serialization import BestAvailableEncryption
|
|
166
|
+
|
|
167
|
+
# Private keys are always written with mode 0600; encrypt them at rest too.
|
|
168
|
+
storage = LocalStorage(
|
|
169
|
+
base_folder="./pki",
|
|
170
|
+
base_encryption_algorithm=BestAvailableEncryption(b"key-passphrase"),
|
|
171
|
+
)
|
|
126
172
|
db = SyncDBHandler(db_url="sqlite:///pki.db")
|
|
127
173
|
mgr = CertLifecycleManager(storage=storage, db_handler=db)
|
|
128
174
|
|
|
@@ -138,10 +184,19 @@ Attach the factory so the manager can issue certificates:
|
|
|
138
184
|
from tiny_ca.ca_factory.utils.file_loader import CAFileLoader
|
|
139
185
|
from tiny_ca.ca_factory.factory import CertificateFactory
|
|
140
186
|
|
|
141
|
-
loader = CAFileLoader(ca_cert_path=cert_path, ca_key_path=key_path
|
|
142
|
-
|
|
187
|
+
loader = CAFileLoader(ca_cert_path=cert_path, ca_key_path=key_path,
|
|
188
|
+
ca_key_password=b"key-passphrase")
|
|
189
|
+
mgr.factory = CertificateFactory(
|
|
190
|
+
loader,
|
|
191
|
+
# Optional: tell relying parties where to find the CRL / CA certificate.
|
|
192
|
+
crl_distribution_points=["http://pki.example/crl.pem"],
|
|
193
|
+
ca_issuers_urls=["http://pki.example/ca.pem"],
|
|
194
|
+
)
|
|
143
195
|
```
|
|
144
196
|
|
|
197
|
+
`CAConfig` / `ClientConfig` accept `key_algorithm=KeyAlgorithm.EC_P256`
|
|
198
|
+
(also `EC_P384`, `ED25519`, default `RSA`) — see [Configuration Models](#configuration-models).
|
|
199
|
+
|
|
145
200
|
### 2. Issue a Leaf Certificate
|
|
146
201
|
|
|
147
202
|
```python
|
|
@@ -165,7 +220,9 @@ cert, key, csr = mgr.issue_certificate(
|
|
|
165
220
|
### 3. Renew a Certificate (same key)
|
|
166
221
|
|
|
167
222
|
Keeps the existing public key — only validity window and serial number change.
|
|
168
|
-
Use when the key has **not** been compromised.
|
|
223
|
+
Use when the key has **not** been compromised. The renewed certificate is
|
|
224
|
+
stored and registered (so it can be revoked); the original is revoked with
|
|
225
|
+
reason `superseded`. The private key stays in the original folder.
|
|
169
226
|
|
|
170
227
|
```python
|
|
171
228
|
renewed = mgr.renew_certificate(serial=cert.serial_number, days_valid=365)
|
|
@@ -173,7 +230,9 @@ renewed = mgr.renew_certificate(serial=cert.serial_number, days_valid=365)
|
|
|
173
230
|
|
|
174
231
|
### 4. Rotate a Certificate (new key)
|
|
175
232
|
|
|
176
|
-
|
|
233
|
+
Issues a replacement with a fresh key pair, then revokes the old cert
|
|
234
|
+
(`superseded`). If the old cert cannot be revoked, the replacement is rolled
|
|
235
|
+
back and `CertRevocationError` is raised.
|
|
177
236
|
|
|
178
237
|
```python
|
|
179
238
|
new_cert, new_key, new_csr = mgr.rotate_certificate(
|
|
@@ -198,6 +257,10 @@ crl = mgr.generate_crl(days_valid=7) # written to <base_folder>/crl.pem
|
|
|
198
257
|
mgr.verify_crl(crl) # raises ValidationCertError on failure
|
|
199
258
|
```
|
|
200
259
|
|
|
260
|
+
Revoked certificates stay on the CRL until they expire. If the database
|
|
261
|
+
cannot be read, `generate_crl` raises `DBOperationError` and the previous
|
|
262
|
+
`crl.pem` is left untouched — an empty CRL is never published.
|
|
263
|
+
|
|
201
264
|
### 7. Issue an Intermediate CA
|
|
202
265
|
|
|
203
266
|
```python
|
|
@@ -206,6 +269,10 @@ sub_ca_cert, sub_ca_key = mgr.issue_intermediate_ca(
|
|
|
206
269
|
path_length=0, organization="ACME Corp", country="UA",
|
|
207
270
|
cert_path="intermediate",
|
|
208
271
|
)
|
|
272
|
+
|
|
273
|
+
# The intermediate is registered (revocable via the root's CRL). Verify a
|
|
274
|
+
# leaf issued by it through the chain:
|
|
275
|
+
mgr.verify_certificate(leaf_cert, intermediates=[sub_ca_cert])
|
|
209
276
|
```
|
|
210
277
|
|
|
211
278
|
### 8. Export PKCS#12
|
|
@@ -223,6 +290,7 @@ with open("nginx.p12", "wb") as f:
|
|
|
223
290
|
from cryptography import x509
|
|
224
291
|
third_party = x509.load_pem_x509_certificate(open("partner.pem", "rb").read())
|
|
225
292
|
cosigned = mgr.cosign_certificate(cert=third_party, days_valid=365)
|
|
293
|
+
# stored + registered like any other certificate issued by this CA
|
|
226
294
|
```
|
|
227
295
|
|
|
228
296
|
### 10. Inspect a Certificate
|
|
@@ -307,7 +375,8 @@ python examples/acomplete_example.py
|
|
|
307
375
|
| `common_name` | `str` | `"Internal CA"` | CA Common Name (CN) |
|
|
308
376
|
| `organization` | `str` | `"My Company"` | Organization (O) |
|
|
309
377
|
| `country` | `str` | `"UA"` | Two-letter ISO 3166-1 alpha-2 code |
|
|
310
|
-
| `key_size` | `int` | `2048` | RSA key length in bits |
|
|
378
|
+
| `key_size` | `int` | `2048` | RSA key length in bits (≥ 2048; ignored for EC / Ed25519) |
|
|
379
|
+
| `key_algorithm` | `KeyAlgorithm` | `RSA` | `RSA`, `EC_P256`, `EC_P384` or `ED25519` |
|
|
311
380
|
| `days_valid` | `int` | `3650` | Validity period in days |
|
|
312
381
|
| `valid_from` | `datetime \| None` | `None` | Explicit UTC start; `None` = now |
|
|
313
382
|
|
|
@@ -317,14 +386,15 @@ python examples/acomplete_example.py
|
|
|
317
386
|
|---|---|---|---|
|
|
318
387
|
| `common_name` | `str` | — | Certificate CN |
|
|
319
388
|
| `serial_type` | `CertType` | `SERVICE` | Certificate category |
|
|
320
|
-
| `key_size` | `int` | `2048` | RSA key length |
|
|
389
|
+
| `key_size` | `int` | `2048` | RSA key length (≥ 2048; ignored for EC / Ed25519) |
|
|
390
|
+
| `key_algorithm` | `KeyAlgorithm` | `RSA` | `RSA`, `EC_P256`, `EC_P384` or `ED25519` |
|
|
321
391
|
| `days_valid` | `int` | `3650` | Validity period |
|
|
322
392
|
| `email` | `EmailStr \| None` | `None` | `emailAddress` Subject attribute |
|
|
323
393
|
| `is_server_cert` | `bool` | `True` | Adds ServerAuth EKU + CN as DNS SAN |
|
|
324
394
|
| `is_client_cert` | `bool` | `False` | Adds ClientAuth EKU |
|
|
325
395
|
| `san_dns` | `list[str] \| None` | `None` | Extra DNS Subject Alternative Names |
|
|
326
396
|
| `san_ip` | `list[str] \| None` | `None` | IP address SANs |
|
|
327
|
-
| `name` | `str \| None` | `None` | Override output file base name |
|
|
397
|
+
| `name` | `str \| None` | `None` | Override output file base name (letters, digits, `.`, `-`, `_`; no leading dot) |
|
|
328
398
|
|
|
329
399
|
### `CertType`
|
|
330
400
|
|
|
@@ -351,14 +421,23 @@ storage = LocalStorage(base_folder="./pki")
|
|
|
351
421
|
./pki/
|
|
352
422
|
└── [cert_path/]
|
|
353
423
|
└── <uuid>/
|
|
354
|
-
├── nginx.pem ← x509.Certificate
|
|
355
|
-
├── nginx.key ←
|
|
356
|
-
└── nginx.csr ← CSR
|
|
424
|
+
├── nginx.pem ← x509.Certificate (0644)
|
|
425
|
+
├── nginx.key ← private key (0600)
|
|
426
|
+
└── nginx.csr ← CSR (0644)
|
|
357
427
|
```
|
|
358
428
|
|
|
429
|
+
- Private keys are written with mode `0600`. They are **unencrypted unless**
|
|
430
|
+
`base_encryption_algorithm=BestAvailableEncryption(password)` is passed — a
|
|
431
|
+
warning is logged once per storage instance otherwise.
|
|
432
|
+
- `file_name`, `cert_path` and `uuid_str` are validated: absolute paths, `..`
|
|
433
|
+
segments and backslashes raise `InvalidPathError`.
|
|
434
|
+
- Without `is_overwrite=True` files are created with `O_EXCL`, so an existing
|
|
435
|
+
file is never replaced.
|
|
436
|
+
|
|
359
437
|
### `AsyncLocalStorage` (async)
|
|
360
438
|
|
|
361
|
-
Drop-in async replacement — same constructor, same layout
|
|
439
|
+
Drop-in async replacement — same constructor, same layout and the same
|
|
440
|
+
security properties; blocking I/O runs in worker threads.
|
|
362
441
|
|
|
363
442
|
---
|
|
364
443
|
|
|
@@ -389,6 +468,13 @@ await db._db.init_db()
|
|
|
389
468
|
| `delete_by_uuid(uuid)` | Hard-delete a record |
|
|
390
469
|
| `update_status_expired()` | Bulk-mark stale VALID records as EXPIRED |
|
|
391
470
|
|
|
471
|
+
Read methods (and `update_status_expired`) raise `DBOperationError` on any
|
|
472
|
+
database failure instead of returning `None` / an empty list — a CA must fail
|
|
473
|
+
closed. `register_cert_in_db` returns `False` on failure; the managers turn
|
|
474
|
+
that into `DBOperationError` and remove the artefacts they wrote. Datetimes
|
|
475
|
+
are stored as naive UTC (`DateTime` columns without time zone), which keeps
|
|
476
|
+
PostgreSQL / asyncpg happy.
|
|
477
|
+
|
|
392
478
|
---
|
|
393
479
|
|
|
394
480
|
## Serial Number Encoding
|
|
@@ -398,7 +484,7 @@ await db._db.init_db()
|
|
|
398
484
|
```
|
|
399
485
|
┌──────────────┬──────────────────────┬────────────────────┐
|
|
400
486
|
│ 16-bit type │ 80-bit name │ 64-bit random │
|
|
401
|
-
│ prefix │ (up to 10 chars) │ (
|
|
487
|
+
│ prefix │ (up to 10 chars) │ (secrets.randbits)│
|
|
402
488
|
└──────────────┴──────────────────────┴────────────────────┘
|
|
403
489
|
```
|
|
404
490
|
|
|
@@ -411,6 +497,10 @@ cert_type, name = SerialWithEncoding.parse(serial)
|
|
|
411
497
|
# cert_type == CertType.SERVICE, name == "nginx"
|
|
412
498
|
```
|
|
413
499
|
|
|
500
|
+
Non-ASCII characters are stored as `?`, so a Cyrillic CN can never overflow
|
|
501
|
+
into the type prefix. The top bit is always zero, so the DER encoding fits the
|
|
502
|
+
20-octet limit of RFC 5280.
|
|
503
|
+
|
|
414
504
|
---
|
|
415
505
|
|
|
416
506
|
## Error Reference
|
|
@@ -427,6 +517,9 @@ cert_type, name = SerialWithEncoding.parse(serial)
|
|
|
427
517
|
| `IsNotFile` | CA PEM path is a directory | Provide a file, not a directory |
|
|
428
518
|
| `WrongType` | Unsupported file extension | Use `.pem`, `.key`, or `.csr` |
|
|
429
519
|
| `ErrorLoadCert` | PEM deserialisation failed | Check file format and integrity |
|
|
520
|
+
| `DBOperationError` | Registry could not be read / written | Check the database; nothing was trusted or published |
|
|
521
|
+
| `CertRevocationError` | Rotation/renewal could not revoke the old cert | Retry; the replacement was rolled back |
|
|
522
|
+
| `InvalidPathError` | Storage path would escape `base_folder` | Use plain names / relative sub-paths |
|
|
430
523
|
|
|
431
524
|
---
|
|
432
525
|
|
|
@@ -436,7 +529,16 @@ cert_type, name = SerialWithEncoding.parse(serial)
|
|
|
436
529
|
Yes — load it with `CAFileLoader` or `AsyncCAFileLoader`.
|
|
437
530
|
|
|
438
531
|
**What's the difference between `renew` and `rotate`?**
|
|
439
|
-
`renew` keeps the key pair and extends validity. `rotate` generates a new key
|
|
532
|
+
`renew` keeps the key pair and extends validity. `rotate` generates a new key. Both revoke the old cert with reason `superseded`.
|
|
533
|
+
|
|
534
|
+
**Why does `verify_certificate` reject a certificate signed by my CA?**
|
|
535
|
+
Since 0.3.0 a certificate issued directly by the CA must also be present in the
|
|
536
|
+
registry — otherwise its revocation status is unknown. Certificates created
|
|
537
|
+
with older versions via `renew` / `cosign` were not registered; re-issue them.
|
|
538
|
+
|
|
539
|
+
**How do I see the library's logs?**
|
|
540
|
+
`tiny_ca` only attaches a `NullHandler`; configure the `"TinyCA"` logger in your
|
|
541
|
+
application, e.g. `logging.getLogger("TinyCA").setLevel(logging.INFO)`.
|
|
440
542
|
|
|
441
543
|
**How do I schedule CRL regeneration?**
|
|
442
544
|
Use APScheduler or any cron solution:
|
|
@@ -544,4 +646,3 @@ Do **not** open public issues for security vulnerabilities. Email the maintainer
|
|
|
544
646
|
| SQLAlchemy | MIT |
|
|
545
647
|
| Pydantic | MIT |
|
|
546
648
|
| aiosqlite | MIT |
|
|
547
|
-
| aiofiles | Apache 2.0 |
|