tiny-ca 0.1.2__tar.gz → 0.2.1__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.
Files changed (68) hide show
  1. tiny_ca-0.2.1/LICENSE +33 -0
  2. tiny_ca-0.2.1/PKG-INFO +627 -0
  3. tiny_ca-0.2.1/README.md +585 -0
  4. tiny_ca-0.2.1/docs/source/conf.py +61 -0
  5. tiny_ca-0.2.1/pyproject.toml +203 -0
  6. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tests/test_async_db_handler.py +287 -0
  7. tiny_ca-0.2.1/tests/test_async_lifecycle_manager.py +1191 -0
  8. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tests/test_base_db.py +35 -1
  9. tiny_ca-0.2.1/tests/test_factory.py +1251 -0
  10. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tests/test_file_loader.py +1 -1
  11. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tests/test_life_time.py +33 -0
  12. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tests/test_sync_db_manager.py +233 -0
  13. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tests/test_sync_lifecycle_manager.py +213 -5
  14. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/__init__.py +1 -1
  15. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/ca_factory/factory.py +366 -6
  16. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/ca_factory/utils/afile_loader.py +1 -1
  17. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/ca_factory/utils/file_loader.py +1 -1
  18. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/ca_factory/utils/life_time.py +46 -14
  19. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/ca_factory/utils/serial.py +12 -12
  20. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/db/async_db_manager.py +160 -1
  21. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/db/base_db.py +91 -0
  22. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/db/models.py +17 -15
  23. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/db/sync_db_manager.py +157 -0
  24. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/managers/async_lifecycle_manager.py +153 -70
  25. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/managers/sync_lifecycle_manager.py +149 -69
  26. tiny_ca-0.2.1/tiny_ca/models/__init__.py +3 -0
  27. tiny_ca-0.1.2/tiny_ca/models/certtificate.py → tiny_ca-0.2.1/tiny_ca/models/certificate.py +4 -4
  28. tiny_ca-0.2.1/tiny_ca.egg-info/PKG-INFO +627 -0
  29. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca.egg-info/SOURCES.txt +3 -2
  30. tiny_ca-0.2.1/tiny_ca.egg-info/requires.txt +23 -0
  31. tiny_ca-0.2.1/tiny_ca.egg-info/top_level.txt +4 -0
  32. tiny_ca-0.1.2/PKG-INFO +0 -431
  33. tiny_ca-0.1.2/README.md +0 -417
  34. tiny_ca-0.1.2/pyproject.toml +0 -123
  35. tiny_ca-0.1.2/tests/test_async_lifecycle_manager.py +0 -613
  36. tiny_ca-0.1.2/tests/test_factory.py +0 -554
  37. tiny_ca-0.1.2/tests/test_factory_missing_coverage.py +0 -548
  38. tiny_ca-0.1.2/tiny_ca/models/__init__.py +0 -3
  39. tiny_ca-0.1.2/tiny_ca.egg-info/PKG-INFO +0 -431
  40. tiny_ca-0.1.2/tiny_ca.egg-info/requires.txt +0 -7
  41. tiny_ca-0.1.2/tiny_ca.egg-info/top_level.txt +0 -1
  42. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/setup.cfg +0 -0
  43. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tests/test_afile_loader.py +0 -0
  44. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tests/test_async_local_storage.py +0 -0
  45. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tests/test_base_storage.py +0 -0
  46. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tests/test_database_manager.py +0 -0
  47. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tests/test_db_const.py +0 -0
  48. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tests/test_local_storage.py +0 -0
  49. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tests/test_models.py +0 -0
  50. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tests/test_serial.py +0 -0
  51. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tests/test_serial_generator.py +0 -0
  52. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tests/test_storage_const.py +0 -0
  53. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/ca_factory/__init__.py +0 -0
  54. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/ca_factory/utils/__init__.py +0 -0
  55. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/const.py +0 -0
  56. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/db/__init__.py +0 -0
  57. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/db/const.py +0 -0
  58. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/exc.py +0 -0
  59. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/managers/__init__.py +0 -0
  60. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/settings.py +0 -0
  61. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/storage/__init__.py +0 -0
  62. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/storage/async_local_storage.py +0 -0
  63. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/storage/base_storage.py +0 -0
  64. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/storage/const.py +0 -0
  65. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/storage/local_storage.py +0 -0
  66. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/utils/__init__.py +0 -0
  67. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca/utils/serial_generator.py +0 -0
  68. {tiny_ca-0.1.2 → tiny_ca-0.2.1}/tiny_ca.egg-info/dependency_links.txt +0 -0
tiny_ca-0.2.1/LICENSE ADDED
@@ -0,0 +1,33 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Denis Shchutskyi (Shchusia)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ ## Third-Party Licenses
26
+
27
+ This software uses the following open source dependencies:
28
+
29
+ - **cryptography** (BSD 3-Clause) - https://github.com/pyca/cryptography
30
+ - **SQLAlchemy** (MIT) - https://github.com/sqlalchemy/sqlalchemy
31
+ - **Pydantic** (MIT) - https://github.com/pydantic/pydantic
32
+ - **aiosqlite** (MIT) - https://github.com/omnilib/aiosqlite
33
+ - **aiofiles** (Apache 2.0) - https://github.com/Tinche/aiofiles
tiny_ca-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,627 @@
1
+ Metadata-Version: 2.4
2
+ Name: tiny-ca
3
+ Version: 0.2.1
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
+ Author-email: Denis Shchutskyi <denisshchutskyi@gmail.com>
6
+ License: MIT
7
+ Project-URL: Repository, https://github.com/Shchusia/tiny_ca
8
+ Project-URL: Documentation, https://shchusia.github.io/tiny_ca/
9
+ Project-URL: Bug Tracker, https://github.com/Shchusia/tiny_ca/issues
10
+ Project-URL: Changelog, https://github.com/Shchusia/tiny_ca/blob/master/CHANGELOG.md
11
+ Keywords: pki,x509,certificate,ca,tls,ssl,cryptography,crl,pkcs12
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Security :: Cryptography
18
+ Classifier: Topic :: Internet
19
+ Classifier: Intended Audience :: Developers
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: cryptography>=46.0.0
25
+ Requires-Dist: pydantic[email]>=2.12.0
26
+ Requires-Dist: sqlalchemy>=2.0.0
27
+ Provides-Extra: async
28
+ Requires-Dist: aiofiles>=25.1.0; extra == "async"
29
+ Requires-Dist: aiosqlite>=0.22.0; extra == "async"
30
+ Requires-Dist: types-aiofiles>=25.1.0; extra == "async"
31
+ Provides-Extra: postgres
32
+ Requires-Dist: psycopg2-binary>=2.9.0; extra == "postgres"
33
+ Provides-Extra: postgres-async
34
+ Requires-Dist: asyncpg>=0.30.0; extra == "postgres-async"
35
+ Provides-Extra: mysql
36
+ Requires-Dist: pymysql>=1.1.0; extra == "mysql"
37
+ Provides-Extra: mysql-async
38
+ Requires-Dist: aiomysql>=0.2.0; extra == "mysql-async"
39
+ Provides-Extra: all
40
+ Requires-Dist: tiny-ca[async,mysql,mysql-async,postgres,postgres-async]; extra == "all"
41
+ Dynamic: license-file
42
+
43
+ # tiny_ca
44
+
45
+ [![Python](https://img.shields.io/pypi/pyversions/tiny-ca)](https://pypi.org/project/tiny-ca/)
46
+ [![PyPI](https://img.shields.io/pypi/v/tiny-ca?color=blue)](https://pypi.org/project/tiny-ca/)
47
+
48
+ [![Coverage Status](https://coveralls.io/repos/github/Shchusia/tiny_ca/badge.svg?branch=feature/docs)](https://coveralls.io/github/Shchusia/tiny_ca?branch=feature/docs)
49
+ [![Docs](https://img.shields.io/badge/Docs-passing-green)](https://shchusia.github.io/tiny_ca/)
50
+ [![License](https://img.shields.io/badge/License-MIT-blue)](LICENSE)
51
+
52
+ A lightweight, **100%-tested** Python library for managing the full lifecycle of X.509 certificates — from bootstrapping a self-signed root CA to issuing, revoking, rotating, renewing, and co-signing end-entity certificates, generating and verifying CRLs, exporting PKCS#12 bundles, and issuing intermediate CAs — all backed by pluggable sync/async storage and database adapters.
53
+
54
+ ---
55
+
56
+ ## REST API
57
+
58
+ > Looking for a ready-to-use HTTP server built on **tiny-ca**?
59
+ > 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.
60
+
61
+ | | |
62
+ |---|---|
63
+ | **GitHub** | [github.com/Shchusia/tiny-ca-gateway](https://github.com/Shchusia/tiny-ca-gateway) |
64
+ | **Integration guide** | [tiny-ca-gateway/blob/master/README.md](https://github.com/Shchusia/tiny-ca-gateway/blob/master/README.md) |
65
+ | **Supported frameworks** | FastAPI · Flask · aiohttp · Django Ninja |
66
+
67
+ ```bash
68
+ pip install "tiny-ca-gateway[fastapi]" # FastAPI + Uvicorn
69
+ pip install "tiny-ca-gateway[flask]" # Flask + Gunicorn
70
+ pip install "tiny-ca-gateway[aiohttp]" # aiohttp
71
+ pip install "tiny-ca-gateway[django]" # Django + Django Ninja
72
+ ```
73
+
74
+ ```python
75
+ # FastAPI — all 22 CA endpoints in 5 lines
76
+ from fastapi import FastAPI
77
+ from contextlib import asynccontextmanager
78
+ from tiny_ca_gateway.fastapi.lifespan.manager import FastAPILifespanManager
79
+ from tiny_ca_gateway.fastapi.api.v1.ca_routes import router
80
+
81
+ @asynccontextmanager
82
+ async def lifespan(app: FastAPI):
83
+ await FastAPILifespanManager(common_name="My CA").on_startup()
84
+ yield
85
+
86
+ app = FastAPI(lifespan=lifespan)
87
+ app.include_router(router, prefix="/api/v1")
88
+ # → Swagger UI at http://localhost:8000/docs
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Table of Contents
94
+
95
+ - [Features](#features)
96
+ - [Requirements](#requirements)
97
+ - [Architecture](#architecture)
98
+ - [Installation](#installation)
99
+ - [Quick Start](#quick-start)
100
+ - [Complete Examples](#complete-examples)
101
+ - [Configuration Models](#configuration-models)
102
+ - [Storage Backends](#storage-backends)
103
+ - [Database Adapters](#database-adapters)
104
+ - [Serial Number Encoding](#serial-number-encoding)
105
+ - [Error Reference](#error-reference)
106
+ - [FAQ](#faq)
107
+ - [Migrating from Other CAs](#migrating-from-other-cas)
108
+ - [Benchmark Results](#benchmark-results)
109
+ - [Project Structure](#project-structure)
110
+ - [Security Policy](#security-policy)
111
+ - [License](#license)
112
+
113
+ ---
114
+
115
+ ## Features
116
+
117
+ | Category | Capability |
118
+ |---|---|
119
+ | **CA bootstrap** | Self-signed root CA and intermediate (sub) CA with configurable `path_length` |
120
+ | **Issuance** | Leaf certificates with SANs (DNS + IP), EKU (Server/Client Auth), email attribute |
121
+ | **Lifecycle** | Revoke (RFC 5280 reasons), renew (same key), rotate (new key), hard-delete |
122
+ | **CRL** | Build, sign, and verify Certificate Revocation Lists with configurable validity |
123
+ | **Inspection** | Structured `CertificateDetails` snapshot from any `x509.Certificate` — fully serialisable |
124
+ | **Export** | PKCS#12 (`.p12`/`.pfx`) bundles with full CA chain |
125
+ | **Co-signing** | Re-sign third-party certificates under your CA, preserving Subject and extensions |
126
+ | **Chain** | Build `[leaf, ca]` fullchain ready for nginx, Apache, or Envoy |
127
+ | **Monitoring** | List certs with filters, find expiring-soon, bulk-expire stale records |
128
+ | **Storage** | `LocalStorage` (sync) and `AsyncLocalStorage` (async, aiofiles) with UUID-based isolation |
129
+ | **Database** | `SyncDBHandler` (SQLAlchemy) and `AsyncDBHandler` (aiosqlite) — SQLite, PostgreSQL, MySQL |
130
+ | **API parity** | `CertLifecycleManager` and `AsyncCertLifecycleManager` are feature-identical |
131
+ | **Serial numbers** | `SerialWithEncoding` — 160-bit RFC 5280-compliant, type-prefixed, name-encoded |
132
+ | **Test coverage** | **100 %** line and branch coverage across all modules |
133
+
134
+ ---
135
+
136
+ ## Requirements
137
+
138
+ - **Python** 3.11 or higher
139
+ - **Core:** `cryptography >= 46`, `sqlalchemy >= 2`, `pydantic >= 2`
140
+ - **Async extras:** `aiofiles`, `aiosqlite`
141
+ - **PostgreSQL:** `psycopg2-binary` (sync) / `asyncpg` (async)
142
+ - **MySQL:** `pymysql` (sync) / `aiomysql` (async)
143
+
144
+ ---
145
+
146
+ ## Architecture
147
+
148
+ ```
149
+ CertLifecycleManager / AsyncCertLifecycleManager ← application entry-point
150
+ │
151
+ ├── CertificateFactory ← crypto-only, zero I/O
152
+ │ ├── ICALoader (Protocol)
153
+ │ │ ├── CAFileLoader ← sync PEM file loader
154
+ │ │ └── AsyncCAFileLoader ← async PEM file loader
155
+ │ ├── CertLifetime ← validity window helpers
156
+ │ ├── CertSerialParser ← read serials from certs
157
+ │ └── SerialWithEncoding ← encode / decode serial numbers
158
+ │
159
+ ├── BaseStorage (ABC)
160
+ │ ├── LocalStorage ← sync filesystem (UUID-isolated)
161
+ │ └── AsyncLocalStorage ← async filesystem (aiofiles)
162
+ │
163
+ └── BaseDB (ABC)
164
+ ├── SyncDBHandler ← SQLAlchemy sync
165
+ └── AsyncDBHandler ← SQLAlchemy async (aiosqlite)
166
+ ```
167
+
168
+ Every component is injected at construction time — no global singletons, trivially testable.
169
+
170
+ ---
171
+
172
+ ## Installation
173
+
174
+ ```bash
175
+ # Core sync-only
176
+ pip install tiny-ca
177
+
178
+ # With async support (recommended)
179
+ pip install tiny-ca[async]
180
+
181
+ # PostgreSQL
182
+ pip install tiny-ca[postgres] # sync (psycopg2)
183
+ pip install tiny-ca[postgres-async] # async (asyncpg)
184
+
185
+ # MySQL
186
+ pip install tiny-ca[mysql] # sync (pymysql)
187
+ pip install tiny-ca[mysql-async] # async (aiomysql)
188
+
189
+ # Everything
190
+ pip install tiny-ca[all]
191
+ ```
192
+
193
+ ---
194
+
195
+ ## Quick Start
196
+
197
+ ### 1. Bootstrap a Root CA
198
+
199
+ ```python
200
+ from tiny_ca.managers.sync_lifecycle_manager import CertLifecycleManager
201
+ from tiny_ca.storage.local_storage import LocalStorage
202
+ from tiny_ca.db.sync_db_manager import SyncDBHandler
203
+ from tiny_ca.models.certificate import CAConfig
204
+
205
+ storage = LocalStorage(base_folder="./pki")
206
+ db = SyncDBHandler(db_url="sqlite:///pki.db")
207
+ mgr = CertLifecycleManager(storage=storage, db_handler=db)
208
+
209
+ cert_path, key_path = mgr.create_self_signed_ca(
210
+ CAConfig(common_name="My Root CA", organization="ACME Corp",
211
+ country="UA", key_size=4096, days_valid=3650)
212
+ )
213
+ ```
214
+
215
+ Attach the factory so the manager can issue certificates:
216
+
217
+ ```python
218
+ from tiny_ca.ca_factory.utils.file_loader import CAFileLoader
219
+ from tiny_ca.ca_factory.factory import CertificateFactory
220
+
221
+ loader = CAFileLoader(ca_cert_path=cert_path, ca_key_path=key_path)
222
+ mgr.factory = CertificateFactory(loader)
223
+ ```
224
+
225
+ ### 2. Issue a Leaf Certificate
226
+
227
+ ```python
228
+ from tiny_ca.models.certificate import ClientConfig
229
+ from tiny_ca.const import CertType
230
+
231
+ cert, key, csr = mgr.issue_certificate(
232
+ ClientConfig(
233
+ common_name="nginx.internal",
234
+ serial_type=CertType.SERVICE,
235
+ key_size=2048,
236
+ days_valid=365,
237
+ is_server_cert=True,
238
+ san_dns=["nginx.internal", "www.nginx.internal"],
239
+ san_ip=["192.168.1.10"],
240
+ ),
241
+ cert_path="services",
242
+ )
243
+ ```
244
+
245
+ ### 3. Renew a Certificate (same key)
246
+
247
+ Keeps the existing public key — only validity window and serial number change.
248
+ Use when the key has **not** been compromised.
249
+
250
+ ```python
251
+ renewed = mgr.renew_certificate(serial=cert.serial_number, days_valid=365)
252
+ ```
253
+
254
+ ### 4. Rotate a Certificate (new key)
255
+
256
+ Atomically revokes the old cert and issues a replacement with a fresh key pair.
257
+
258
+ ```python
259
+ new_cert, new_key, new_csr = mgr.rotate_certificate(
260
+ serial=cert.serial_number,
261
+ config=ClientConfig(common_name="nginx.internal",
262
+ serial_type=CertType.SERVICE, days_valid=365,
263
+ is_server_cert=True),
264
+ )
265
+ ```
266
+
267
+ ### 5. Revoke a Certificate
268
+
269
+ ```python
270
+ from cryptography import x509
271
+ mgr.revoke_certificate(serial=cert.serial_number, reason=x509.ReasonFlags.key_compromise)
272
+ ```
273
+
274
+ ### 6. Generate and Verify a CRL
275
+
276
+ ```python
277
+ crl = mgr.generate_crl(days_valid=7) # written to <base_folder>/crl.pem
278
+ mgr.verify_crl(crl) # raises ValidationCertError on failure
279
+ ```
280
+
281
+ ### 7. Issue an Intermediate CA
282
+
283
+ ```python
284
+ sub_ca_cert, sub_ca_key = mgr.issue_intermediate_ca(
285
+ common_name="Issuing CA", key_size=4096, days_valid=1825,
286
+ path_length=0, organization="ACME Corp", country="UA",
287
+ cert_path="intermediate",
288
+ )
289
+ ```
290
+
291
+ ### 8. Export PKCS#12
292
+
293
+ ```python
294
+ p12_bytes = mgr.export_pkcs12(cert=cert, private_key=key,
295
+ password=b"strong-passphrase", name="nginx.internal")
296
+ with open("nginx.p12", "wb") as f:
297
+ f.write(p12_bytes)
298
+ ```
299
+
300
+ ### 9. Co-sign a Third-Party Certificate
301
+
302
+ ```python
303
+ from cryptography import x509
304
+ third_party = x509.load_pem_x509_certificate(open("partner.pem", "rb").read())
305
+ cosigned = mgr.cosign_certificate(cert=third_party, days_valid=365)
306
+ ```
307
+
308
+ ### 10. Inspect a Certificate
309
+
310
+ ```python
311
+ details = mgr.inspect_certificate(cert)
312
+ print(details.common_name, details.fingerprint_sha256, details.public_key_size)
313
+
314
+ # Build fullchain.pem for nginx
315
+ from cryptography.hazmat.primitives.serialization import Encoding
316
+ chain = mgr.get_cert_chain(cert)
317
+ fullchain_pem = b"".join(c.public_bytes(Encoding.PEM) for c in chain)
318
+ ```
319
+
320
+ ### 11. Monitor Certificates
321
+
322
+ ```python
323
+ records = mgr.list_certificates(status="valid", key_type="service", limit=50)
324
+ expiring = mgr.get_expiring_soon(within_days=30)
325
+ updated = mgr.refresh_expired_statuses() # run periodically
326
+ mgr.delete_certificate(serial=cert.serial_number)
327
+ ```
328
+
329
+ ### 12. Async Usage
330
+
331
+ ```python
332
+ import asyncio
333
+ from tiny_ca.managers.async_lifecycle_manager import AsyncCertLifecycleManager
334
+ from tiny_ca.storage.async_local_storage import AsyncLocalStorage
335
+ from tiny_ca.db.async_db_manager import AsyncDBHandler
336
+ from tiny_ca.models.certificate import CAConfig, ClientConfig
337
+ from tiny_ca.const import CertType
338
+
339
+ async def main():
340
+ storage = AsyncLocalStorage(base_folder="./pki_async")
341
+ db = AsyncDBHandler(db_url="sqlite+aiosqlite:///pki_async.db")
342
+ await db._db.init_db()
343
+
344
+ mgr = AsyncCertLifecycleManager(storage=storage, db_handler=db)
345
+ cert_path, key_path = await mgr.create_self_signed_ca(
346
+ CAConfig(common_name="Async CA", organization="ACME",
347
+ country="UA", key_size=2048, days_valid=3650)
348
+ )
349
+
350
+ from tiny_ca.ca_factory.utils.afile_loader import AsyncCAFileLoader
351
+ from tiny_ca.ca_factory.factory import CertificateFactory
352
+ loader = await AsyncCAFileLoader.create(cert_path, key_path)
353
+ mgr.factory = CertificateFactory(loader)
354
+
355
+ cert, key, csr = await mgr.issue_certificate(
356
+ ClientConfig(common_name="api.internal", serial_type=CertType.SERVICE,
357
+ key_size=2048, days_valid=365, is_server_cert=True)
358
+ )
359
+ details = await mgr.inspect_certificate(cert)
360
+ p12 = await mgr.export_pkcs12(cert, key, password=b"secret")
361
+
362
+ asyncio.run(main())
363
+ ```
364
+
365
+ ---
366
+
367
+ ## Complete Examples
368
+
369
+ | File | Description |
370
+ |---|---|
371
+ | `examples/complete_example.py` | Sync API — full lifecycle |
372
+ | `examples/acomplete_example.py` | Async API — full lifecycle |
373
+
374
+ ```bash
375
+ python examples/complete_example.py
376
+ python examples/acomplete_example.py
377
+ ```
378
+
379
+ ---
380
+
381
+ ## Configuration Models
382
+
383
+ ### `CAConfig`
384
+
385
+ | Field | Type | Default | Description |
386
+ |---|---|---|---|
387
+ | `common_name` | `str` | `"Internal CA"` | CA Common Name (CN) |
388
+ | `organization` | `str` | `"My Company"` | Organization (O) |
389
+ | `country` | `str` | `"UA"` | Two-letter ISO 3166-1 alpha-2 code |
390
+ | `key_size` | `int` | `2048` | RSA key length in bits |
391
+ | `days_valid` | `int` | `3650` | Validity period in days |
392
+ | `valid_from` | `datetime \| None` | `None` | Explicit UTC start; `None` = now |
393
+
394
+ ### `ClientConfig`
395
+
396
+ | Field | Type | Default | Description |
397
+ |---|---|---|---|
398
+ | `common_name` | `str` | — | Certificate CN |
399
+ | `serial_type` | `CertType` | `SERVICE` | Certificate category |
400
+ | `key_size` | `int` | `2048` | RSA key length |
401
+ | `days_valid` | `int` | `3650` | Validity period |
402
+ | `email` | `EmailStr \| None` | `None` | `emailAddress` Subject attribute |
403
+ | `is_server_cert` | `bool` | `True` | Adds ServerAuth EKU + CN as DNS SAN |
404
+ | `is_client_cert` | `bool` | `False` | Adds ClientAuth EKU |
405
+ | `san_dns` | `list[str] \| None` | `None` | Extra DNS Subject Alternative Names |
406
+ | `san_ip` | `list[str] \| None` | `None` | IP address SANs |
407
+ | `name` | `str \| None` | `None` | Override output file base name |
408
+
409
+ ### `CertType`
410
+
411
+ | Value | String | Description |
412
+ |---|---|---|
413
+ | `CertType.CA` | `"CA"` | Root or intermediate CA |
414
+ | `CertType.USER` | `"USR"` | User / human certificate |
415
+ | `CertType.SERVICE` | `"SVC"` | Service / server certificate |
416
+ | `CertType.DEVICE` | `"DEV"` | IoT / device certificate |
417
+ | `CertType.INTERNAL` | `"INT"` | Internal infrastructure certificate |
418
+
419
+ ---
420
+
421
+ ## Storage Backends
422
+
423
+ ### `LocalStorage` (sync)
424
+
425
+ ```python
426
+ from tiny_ca.storage.local_storage import LocalStorage
427
+ storage = LocalStorage(base_folder="./pki")
428
+ ```
429
+
430
+ ```
431
+ ./pki/
432
+ └── [cert_path/]
433
+ └── <uuid>/
434
+ ├── nginx.pem ← x509.Certificate
435
+ ├── nginx.key ← RSA private key
436
+ └── nginx.csr ← CSR
437
+ ```
438
+
439
+ ### `AsyncLocalStorage` (async)
440
+
441
+ Drop-in async replacement — same constructor, same layout, all I/O via `aiofiles`.
442
+
443
+ ---
444
+
445
+ ## Database Adapters
446
+
447
+ ```python
448
+ # Sync
449
+ db = SyncDBHandler(db_url="sqlite:///pki.db")
450
+ db = SyncDBHandler(db_url="postgresql+psycopg2://user:pass@host/pki")
451
+
452
+ # Async
453
+ db = AsyncDBHandler(db_url="sqlite+aiosqlite:///pki.db")
454
+ db = AsyncDBHandler(db_url="postgresql+asyncpg://user:pass@host/pki")
455
+ await db._db.init_db()
456
+ ```
457
+
458
+ ### `BaseDB` contract
459
+
460
+ | Method | Description |
461
+ |---|---|
462
+ | `get_by_serial(serial)` | Fetch any record by serial number |
463
+ | `get_by_name(cn)` | Fetch active VALID record by CN |
464
+ | `register_cert_in_db(cert, uuid, key_type)` | Persist a new certificate |
465
+ | `revoke_certificate(serial, reason)` | Mark as revoked (RFC 5280) |
466
+ | `get_revoked_certificates()` | Yield rows for CRL generation |
467
+ | `list_all(status, key_type, limit, offset)` | Paginated listing with filters |
468
+ | `get_expiring(within_days)` | VALID certs expiring within N days |
469
+ | `delete_by_uuid(uuid)` | Hard-delete a record |
470
+ | `update_status_expired()` | Bulk-mark stale VALID records as EXPIRED |
471
+
472
+ ---
473
+
474
+ ## Serial Number Encoding
475
+
476
+ `SerialWithEncoding` packs three fields into a 160-bit integer (RFC 5280):
477
+
478
+ ```
479
+ ┌──────────────┬──────────────────────┬────────────────────┐
480
+ │ 16-bit type │ 80-bit name │ 64-bit random │
481
+ │ prefix │ (up to 10 chars) │ (UUID fragment) │
482
+ └──────────────┴──────────────────────┴────────────────────┘
483
+ ```
484
+
485
+ ```python
486
+ from tiny_ca.utils.serial_generator import SerialWithEncoding
487
+ from tiny_ca.const import CertType
488
+
489
+ serial = SerialWithEncoding.generate("nginx", CertType.SERVICE)
490
+ cert_type, name = SerialWithEncoding.parse(serial)
491
+ # cert_type == CertType.SERVICE, name == "nginx"
492
+ ```
493
+
494
+ ---
495
+
496
+ ## Error Reference
497
+
498
+ | Exception | When raised | Resolution |
499
+ |---|---|---|
500
+ | `DBNotInitedError` | `db_handler is None` | Pass `db_handler` to the manager |
501
+ | `NotUniqueCertOwner` | CN conflict, `is_overwrite=False` | Use `is_overwrite=True` |
502
+ | `CertNotFound` | `renew`/`rotate` for unknown serial | Verify the serial exists |
503
+ | `ValidationCertError` | Bad issuer, expired, invalid signature | Check cert origin and CA |
504
+ | `InvalidRangeTimeCertificate` | `not_after` already in the past | Fix `valid_from` or `days_valid` |
505
+ | `FileAlreadyExists` | File exists, `is_overwrite=False` | Use `is_overwrite=True` |
506
+ | `NotExistCertFile` | CA PEM path missing | Check the file path |
507
+ | `IsNotFile` | CA PEM path is a directory | Provide a file, not a directory |
508
+ | `WrongType` | Unsupported file extension | Use `.pem`, `.key`, or `.csr` |
509
+ | `ErrorLoadCert` | PEM deserialisation failed | Check file format and integrity |
510
+
511
+ ---
512
+
513
+ ## FAQ
514
+
515
+ **Can I use an existing CA?**
516
+ Yes — load it with `CAFileLoader` or `AsyncCAFileLoader`.
517
+
518
+ **What's the difference between `renew` and `rotate`?**
519
+ `renew` keeps the key pair and extends validity. `rotate` generates a new key and revokes the old cert.
520
+
521
+ **How do I schedule CRL regeneration?**
522
+ Use APScheduler or any cron solution:
523
+ ```python
524
+ scheduler.add_job(mgr.generate_crl, "cron", day_of_week="mon", hour=0)
525
+ ```
526
+
527
+ **Can I use a custom storage backend (S3, Redis)?**
528
+ Yes — subclass `BaseStorage` and implement `save_certificate` and `delete_certificate_folder`.
529
+
530
+ **How do I protect the CA private key with a password?**
531
+ ```python
532
+ loader = CAFileLoader(ca_cert_path="ca.pem", ca_key_path="ca.key",
533
+ ca_key_password=b"passphrase")
534
+ ```
535
+
536
+ ---
537
+
538
+ ## Migrating from Other CAs
539
+
540
+ ### From OpenSSL
541
+
542
+ ```bash
543
+ openssl x509 -in ca.crt -out ca.pem -outform PEM
544
+ openssl rsa -in ca.key -out ca-key.pem -outform PEM
545
+ ```
546
+ ```python
547
+ loader = CAFileLoader(ca_cert_path="ca.pem", ca_key_path="ca-key.pem")
548
+ mgr.factory = CertificateFactory(loader)
549
+ ```
550
+
551
+ ### From Easy-RSA / CFSSL
552
+
553
+ Both output standard PEM files — follow the OpenSSL migration steps.
554
+
555
+ ---
556
+
557
+ ## Benchmark Results
558
+
559
+ *Linux 6.17, Python 3.11.15, 32-core CPU, NVMe SSD. 5 iterations each.*
560
+
561
+ | Operation | Sync | Async |
562
+ |---|---|---|
563
+ | CA creation (2048-bit) | 0.037 s | 0.067 s |
564
+ | CA creation (4096-bit) | 0.317 s | 0.411 s |
565
+ | Leaf issuance (2048-bit) | 0.055 s | 0.052 s |
566
+ | Leaf issuance (4096-bit) | 0.476 s | 0.712 s |
567
+ | CRL generation | 0.001 s | 0.002 s |
568
+ | Certificate verification | 0.0003 s | 0.0008 s |
569
+ | PKCS#12 export | 0.0005 s | 0.0006 s |
570
+
571
+ Key generation dominates issuance time. For >1 000 certs/hour use PostgreSQL, the async API, and connection pooling.
572
+
573
+ ---
574
+
575
+ ## Project Structure
576
+
577
+ ```
578
+ tiny_ca/
579
+ ├── const.py # CertType, ALLOWED_CERT_EXTENSIONS
580
+ ├── exc.py # all custom exceptions
581
+ ├── settings.py # DEFAULT_LOGGER
582
+ ├── ca_factory/
583
+ │ ├── factory.py # CertificateFactory — all crypto
584
+ │ └── utils/
585
+ │ ├── file_loader.py # CAFileLoader + ICALoader Protocol
586
+ │ ├── afile_loader.py # AsyncCAFileLoader
587
+ │ ├── life_time.py # CertLifetime
588
+ │ └── serial.py # CertSerialParser
589
+ ├── db/
590
+ │ ├── base_db.py # BaseDB — 9 abstract methods
591
+ │ ├── models.py # CertificateRecord ORM model
592
+ │ ├── const.py # RevokeStatus, CertificateStatus
593
+ │ ├── sync_db_manager.py # SyncDBHandler
594
+ │ └── async_db_manager.py # AsyncDBHandler
595
+ ├── managers/
596
+ │ ├── sync_lifecycle_manager.py # CertLifecycleManager (20+ ops)
597
+ │ └── async_lifecycle_manager.py # AsyncCertLifecycleManager
598
+ ├── models/
599
+ │ └── certificate.py # CAConfig, ClientConfig, CertificateDetails
600
+ ├── storage/
601
+ │ ├── base_storage.py # BaseStorage ABC
602
+ │ ├── const.py # CryptoObject type alias
603
+ │ ├── local_storage.py # LocalStorage
604
+ │ └── async_local_storage.py # AsyncLocalStorage
605
+ └── utils/
606
+ └── serial_generator.py # SerialWithEncoding
607
+ ```
608
+
609
+ ---
610
+
611
+ ## Security Policy
612
+
613
+ Do **not** open public issues for security vulnerabilities. Email the maintainer (see GitHub profile). Acknowledgement within 48 hours; public advisory only after a fix is available.
614
+
615
+ ---
616
+
617
+ ## License
618
+
619
+ [MIT](LICENSE) © 2025 Denis Shchutskyi
620
+
621
+ | Dependency | License |
622
+ |---|---|
623
+ | cryptography | BSD 3-Clause |
624
+ | SQLAlchemy | MIT |
625
+ | Pydantic | MIT |
626
+ | aiosqlite | MIT |
627
+ | aiofiles | Apache 2.0 |