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