tiny-ca 0.1.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 (56) hide show
  1. tiny_ca-0.1.0/PKG-INFO +430 -0
  2. tiny_ca-0.1.0/README.md +417 -0
  3. tiny_ca-0.1.0/pyproject.toml +121 -0
  4. tiny_ca-0.1.0/setup.cfg +4 -0
  5. tiny_ca-0.1.0/tests/test_afile_loader.py +352 -0
  6. tiny_ca-0.1.0/tests/test_async_db_handler.py +409 -0
  7. tiny_ca-0.1.0/tests/test_async_lifecycle_manager.py +561 -0
  8. tiny_ca-0.1.0/tests/test_async_local_storage.py +262 -0
  9. tiny_ca-0.1.0/tests/test_base_db.py +129 -0
  10. tiny_ca-0.1.0/tests/test_base_storage.py +108 -0
  11. tiny_ca-0.1.0/tests/test_database_manager.py +47 -0
  12. tiny_ca-0.1.0/tests/test_db_const.py +84 -0
  13. tiny_ca-0.1.0/tests/test_factory.py +554 -0
  14. tiny_ca-0.1.0/tests/test_file_loader.py +344 -0
  15. tiny_ca-0.1.0/tests/test_life_time.py +192 -0
  16. tiny_ca-0.1.0/tests/test_local_storage.py +406 -0
  17. tiny_ca-0.1.0/tests/test_models.py +218 -0
  18. tiny_ca-0.1.0/tests/test_serial.py +180 -0
  19. tiny_ca-0.1.0/tests/test_serial_generator.py +356 -0
  20. tiny_ca-0.1.0/tests/test_storage_const.py +111 -0
  21. tiny_ca-0.1.0/tests/test_sync_db_manager.py +358 -0
  22. tiny_ca-0.1.0/tests/test_sync_lifecycle_manager.py +569 -0
  23. tiny_ca-0.1.0/tiny_ca/__init__.py +41 -0
  24. tiny_ca-0.1.0/tiny_ca/ca_factory/__init__.py +15 -0
  25. tiny_ca-0.1.0/tiny_ca/ca_factory/factory.py +658 -0
  26. tiny_ca-0.1.0/tiny_ca/ca_factory/utils/__init__.py +10 -0
  27. tiny_ca-0.1.0/tiny_ca/ca_factory/utils/afile_loader.py +157 -0
  28. tiny_ca-0.1.0/tiny_ca/ca_factory/utils/file_loader.py +290 -0
  29. tiny_ca-0.1.0/tiny_ca/ca_factory/utils/life_time.py +199 -0
  30. tiny_ca-0.1.0/tiny_ca/ca_factory/utils/serial.py +151 -0
  31. tiny_ca-0.1.0/tiny_ca/const.py +81 -0
  32. tiny_ca-0.1.0/tiny_ca/db/__init__.py +14 -0
  33. tiny_ca-0.1.0/tiny_ca/db/async_db_manager.py +358 -0
  34. tiny_ca-0.1.0/tiny_ca/db/base_db.py +173 -0
  35. tiny_ca-0.1.0/tiny_ca/db/const.py +92 -0
  36. tiny_ca-0.1.0/tiny_ca/db/models.py +144 -0
  37. tiny_ca-0.1.0/tiny_ca/db/sync_db_manager.py +415 -0
  38. tiny_ca-0.1.0/tiny_ca/exc.py +103 -0
  39. tiny_ca-0.1.0/tiny_ca/managers/__init__.py +3 -0
  40. tiny_ca-0.1.0/tiny_ca/managers/async_lifecycle_manager.py +702 -0
  41. tiny_ca-0.1.0/tiny_ca/managers/sync_lifecycle_manager.py +722 -0
  42. tiny_ca-0.1.0/tiny_ca/models/__init__.py +3 -0
  43. tiny_ca-0.1.0/tiny_ca/models/certtificate.py +300 -0
  44. tiny_ca-0.1.0/tiny_ca/settings.py +9 -0
  45. tiny_ca-0.1.0/tiny_ca/storage/__init__.py +4 -0
  46. tiny_ca-0.1.0/tiny_ca/storage/async_local_storage.py +218 -0
  47. tiny_ca-0.1.0/tiny_ca/storage/base_storage.py +166 -0
  48. tiny_ca-0.1.0/tiny_ca/storage/const.py +39 -0
  49. tiny_ca-0.1.0/tiny_ca/storage/local_storage.py +448 -0
  50. tiny_ca-0.1.0/tiny_ca/utils/__init__.py +0 -0
  51. tiny_ca-0.1.0/tiny_ca/utils/serial_generator.py +530 -0
  52. tiny_ca-0.1.0/tiny_ca.egg-info/PKG-INFO +430 -0
  53. tiny_ca-0.1.0/tiny_ca.egg-info/SOURCES.txt +54 -0
  54. tiny_ca-0.1.0/tiny_ca.egg-info/dependency_links.txt +1 -0
  55. tiny_ca-0.1.0/tiny_ca.egg-info/requires.txt +6 -0
  56. tiny_ca-0.1.0/tiny_ca.egg-info/top_level.txt +1 -0
tiny_ca-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,430 @@
1
+ Metadata-Version: 2.4
2
+ Name: tiny-ca
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: aiofiles>=25.1.0
8
+ Requires-Dist: aiosqlite>=0.22.1
9
+ Requires-Dist: cryptography>=46.0.5
10
+ Requires-Dist: pydantic[email]>=2.12.5
11
+ Requires-Dist: sqlalchemy[mypy]>=2.0.48
12
+ Requires-Dist: types-aiofiles>=25.1.0.20251011
13
+
14
+ # tiny_ca
15
+
16
+ [![Coverage Status](https://img.shields.io/badge/%20Python%20Versions-%3E%3D3.11-informational)](https://github.com/Shchusia/tiny_ca)
17
+ [![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)
18
+
19
+ [![Coverage Status](https://img.shields.io/badge/Version-0.1.0-informational)](https://pypi.org/project/tiny_ca/)
20
+ [![Coverage Status](https://img.shields.io/badge/Docs-passed-green)](https://shchusia.github.io/tiny_ca/)
21
+
22
+ A lightweight Python library for managing the full lifecycle of X.509 certificates — from bootstrapping a self-signed root CA to issuing, revoking, and rotating end-entity certificates, generating CRLs, and persisting all artefacts to local storage backed by a relational database.
23
+
24
+ ---
25
+
26
+ ## Table of Contents
27
+
28
+ - [Features](#features)
29
+ - [Architecture Overview](#architecture-overview)
30
+ - [Installation](#installation)
31
+ - [Quick Start](#quick-start)
32
+ - [1. Bootstrap a Self-Signed Root CA](#1-bootstrap-a-self-signed-root-ca)
33
+ - [2. Issue an End-Entity Certificate](#2-issue-an-end-entity-certificate)
34
+ - [3. Revoke a Certificate](#3-revoke-a-certificate)
35
+ - [4. Generate a CRL](#4-generate-a-crl)
36
+ - [5. Verify a Certificate](#5-verify-a-certificate)
37
+ - [6. Rotate a Certificate](#6-rotate-a-certificate)
38
+ - [7. Async Usage](#7-async-usage)
39
+ - [Configuration Models](#configuration-models)
40
+ - [Storage Backends](#storage-backends)
41
+ - [Database Adapters](#database-adapters)
42
+ - [Serial Number Encoding](#serial-number-encoding)
43
+ - [Error Reference](#error-reference)
44
+ - [Running Tests](#running-tests)
45
+ - [Project Structure](#project-structure)
46
+
47
+ ---
48
+
49
+ ## Features
50
+
51
+ - **Self-signed CA bootstrap** — generate a root CA certificate and key in one call.
52
+ - **End-entity certificate issuance** — server, client, device, user, and service certificates with SANs (DNS + IP).
53
+ - **Certificate revocation** — mark certificates as revoked in the database with RFC 5280 reason codes.
54
+ - **CRL generation** — build and sign a Certificate Revocation List from current revocation records.
55
+ - **Certificate verification** — validate issuer, validity window, signature, and revocation status.
56
+ - **Certificate rotation** — atomically revoke an old certificate and issue its replacement.
57
+ - **Pluggable storage** — `LocalStorage` and `AsyncLocalStorage` write PEM/key/CSR/CRL files to a configurable directory tree.
58
+ - **Pluggable database** — `SyncDBHandler` (SQLAlchemy sync) and `AsyncDBHandler` (SQLAlchemy async/aiosqlite) back the certificate registry.
59
+ - **Sync and async APIs** — `CertLifecycleManager` (sync) and `AsyncCertLifecycleManager` (async) with identical feature sets.
60
+ - **Smart serial numbers** — `SerialWithEncoding` packs a CertType prefix + name fragment + UUID randomness into a single 160-bit integer; fully RFC 5280-compliant.
61
+
62
+ ---
63
+
64
+ ## Architecture Overview
65
+
66
+ ```
67
+ CertLifecycleManager / AsyncCertLifecycleManager
68
+ │
69
+ ├── CertificateFactory ← cryptographic operations only
70
+ │ ├── CAFileLoader / AsyncCAFileLoader ← load CA from PEM files
71
+ │ └── CertLifetime / CertSerialParser ← validity & serial helpers
72
+ │
73
+ ├── BaseStorage
74
+ │ ├── LocalStorage ← sync filesystem backend
75
+ │ └── AsyncLocalStorage ← async filesystem backend
76
+ │
77
+ └── BaseDB
78
+ ├── SyncDBHandler ← SQLAlchemy sync
79
+ └── AsyncDBHandler ← SQLAlchemy async (aiosqlite)
80
+ ```
81
+
82
+ Every component is injected at construction time — no global state, easy to test.
83
+
84
+ ---
85
+
86
+ ## Installation
87
+
88
+ ```bash
89
+ pip install tiny_ca
90
+ # async support (aiosqlite + aiofiles)
91
+ pip install tiny_ca[async]
92
+ ```
93
+
94
+ Dependencies: `cryptography`, `sqlalchemy`, `pydantic`.
95
+ Optional: `aiosqlite`, `aiofiles` (async backends).
96
+
97
+ ---
98
+
99
+ ## Quick Start
100
+
101
+ ### 1. Bootstrap a Self-Signed Root CA
102
+
103
+ ```python
104
+ from tiny_ca.managers.sync_lifecycle_manager import CertLifecycleManager
105
+ from tiny_ca.models.certtificate import CAConfig
106
+ from tiny_ca.storage.local_storage import LocalStorage
107
+ from tiny_ca.db.sync_db_manager import SyncDBHandler
108
+
109
+ storage = LocalStorage(base_folder="./pki")
110
+ db = SyncDBHandler(db_url="sqlite:///pki.db")
111
+
112
+ mgr = CertLifecycleManager(storage=storage, db_handler=db)
113
+
114
+ config = CAConfig(
115
+ common_name="My Internal CA",
116
+ organization="ACME Corp",
117
+ country="UA",
118
+ key_size=4096,
119
+ days_valid=3650,
120
+ )
121
+
122
+ cert_path, key_path = mgr.create_self_signed_ca(config)
123
+ print(f"CA certificate: {cert_path}")
124
+ print(f"CA private key: {key_path}")
125
+ ```
126
+
127
+ ### 2. Issue an End-Entity Certificate
128
+
129
+ After bootstrapping the CA you need to load it back and attach a `CertificateFactory`:
130
+
131
+ ```python
132
+ from tiny_ca.ca_factory.utils.file_loader import CAFileLoader
133
+ from tiny_ca.ca_factory.factory import CertificateFactory
134
+ from tiny_ca.models.certtificate import ClientConfig
135
+ from tiny_ca.const import CertType
136
+
137
+ loader = CAFileLoader(
138
+ ca_cert_path="./pki/<uuid>/ca.pem",
139
+ ca_key_path="./pki/<uuid>/ca.key",
140
+ )
141
+ mgr.factory = CertificateFactory(loader)
142
+
143
+ svc_config = ClientConfig(
144
+ common_name="nginx.internal",
145
+ serial_type=CertType.SERVICE,
146
+ key_size=2048,
147
+ days_valid=365,
148
+ is_server_cert=True,
149
+ san_dns=["nginx.internal", "www.nginx.internal"],
150
+ san_ip=["192.168.1.10"],
151
+ )
152
+
153
+ cert, key, csr = mgr.issue_certificate(svc_config, cert_path="services")
154
+ print(f"Issued: {cert.serial_number}")
155
+ ```
156
+
157
+ ### 3. Revoke a Certificate
158
+
159
+ ```python
160
+ from cryptography import x509
161
+
162
+ success = mgr.revoke_certificate(
163
+ serial=cert.serial_number,
164
+ reason=x509.ReasonFlags.key_compromise,
165
+ )
166
+ print("Revoked:", success)
167
+ ```
168
+
169
+ ### 4. Generate a CRL
170
+
171
+ ```python
172
+ crl = mgr.generate_crl(days_valid=7)
173
+ # Written to <base_folder>/crl.pem automatically
174
+ ```
175
+
176
+ ### 5. Verify a Certificate
177
+
178
+ ```python
179
+ from tiny_ca.exc import ValidationCertError
180
+
181
+ try:
182
+ mgr.verify_certificate(cert)
183
+ print("Certificate is valid")
184
+ except ValidationCertError as e:
185
+ print(f"Validation failed: {e}")
186
+ ```
187
+
188
+ ### 6. Rotate a Certificate
189
+
190
+ ```python
191
+ new_cert, new_key, new_csr = mgr.rotate_certificate(
192
+ serial=cert.serial_number,
193
+ config=svc_config,
194
+ )
195
+ print(f"Rotated to serial: {new_cert.serial_number}")
196
+ ```
197
+
198
+ ### 7. Async Usage
199
+
200
+ All operations are available as `async`/`await` via `AsyncCertLifecycleManager`:
201
+
202
+ ```python
203
+ import asyncio
204
+ from tiny_ca.managers.async_lifecycle_manager import AsyncCertLifecycleManager
205
+ from tiny_ca.storage.async_local_storage import AsyncLocalStorage
206
+ from tiny_ca.db.async_db_manager import AsyncDBHandler
207
+ from tiny_ca.models.certtificate import CAConfig, ClientConfig
208
+ from tiny_ca.const import CertType
209
+
210
+ async def main():
211
+ storage = AsyncLocalStorage(base_folder="./pki_async")
212
+ db = AsyncDBHandler(db_url="sqlite+aiosqlite:///pki_async.db")
213
+ await db._db.init_db()
214
+
215
+ mgr = AsyncCertLifecycleManager(storage=storage, db_handler=db)
216
+
217
+ # Bootstrap CA
218
+ cert_path, key_path = await mgr.create_self_signed_ca(
219
+ CAConfig(common_name="Async CA", organization="ACME", country="UA",
220
+ key_size=2048, days_valid=3650)
221
+ )
222
+
223
+ # Attach factory (after loading the CA)
224
+ from tiny_ca.ca_factory.utils.afile_loader import AsyncCAFileLoader
225
+ from tiny_ca.ca_factory.factory import CertificateFactory
226
+
227
+ loader = await AsyncCAFileLoader.create(cert_path.parent / "ca.pem",
228
+ cert_path.parent / "ca.key")
229
+ mgr.factory = CertificateFactory(loader)
230
+
231
+ # Issue
232
+ cert, key, csr = await mgr.issue_certificate(
233
+ ClientConfig(common_name="modules.internal", serial_type=CertType.SERVICE,
234
+ key_size=2048, days_valid=365, is_server_cert=True)
235
+ )
236
+ print("Issued:", cert.serial_number)
237
+
238
+ asyncio.run(main())
239
+ ```
240
+
241
+ ---
242
+
243
+ ## Configuration Models
244
+
245
+ Both models are Pydantic `BaseModel` instances — all fields are validated on construction.
246
+
247
+ ### `CAConfig`
248
+
249
+ | Field | Type | Default | Description |
250
+ |---|---|---|---|
251
+ | `common_name` | `str` | — | CA Common Name (CN) |
252
+ | `organization` | `str` | — | Organization (O) |
253
+ | `country` | `str` | — | Two-letter ISO country code |
254
+ | `key_size` | `int` | `2048` | RSA key length in bits |
255
+ | `days_valid` | `int` | `3650` | Validity period in days |
256
+
257
+ ### `ClientConfig`
258
+
259
+ | Field | Type | Default | Description |
260
+ |---|---|---|---|
261
+ | `common_name` | `str` | — | Certificate CN |
262
+ | `serial_type` | `CertType` | `SERVICE` | Certificate category |
263
+ | `key_size` | `int` | `2048` | RSA key length |
264
+ | `days_valid` | `int` | `365` | Validity period |
265
+ | `email` | `str \| None` | `None` | Optional emailAddress Subject attribute |
266
+ | `is_server_cert` | `bool` | `False` | Adds ServerAuth EKU + DNS SAN from CN |
267
+ | `is_client_cert` | `bool` | `False` | Adds ClientAuth EKU |
268
+ | `san_dns` | `list[str] \| None` | `None` | Extra DNS Subject Alternative Names |
269
+ | `san_ip` | `list[str] \| None` | `None` | IP address SANs |
270
+ | `name` | `str \| None` | `None` | Override output file base name |
271
+
272
+ ### `CertType` enum
273
+
274
+ | Value | Description |
275
+ |---|---|
276
+ | `CA` | Root or intermediate CA |
277
+ | `USER` | User / human certificate |
278
+ | `SERVICE` | Service / server certificate |
279
+ | `DEVICE` | IoT / device certificate |
280
+ | `INTERNAL` | Internal infrastructure certificate |
281
+
282
+ ---
283
+
284
+ ## Storage Backends
285
+
286
+ ### `LocalStorage` (sync)
287
+
288
+ ```python
289
+ from tiny_ca.storage.local_storage import LocalStorage
290
+ from cryptography.hazmat.primitives import serialization
291
+
292
+ storage = LocalStorage(
293
+ base_folder="./pki",
294
+ base_encoding=serialization.Encoding.PEM,
295
+ base_private_format=serialization.PrivateFormat.TraditionalOpenSSL,
296
+ base_encryption_algorithm=serialization.NoEncryption(),
297
+ )
298
+ ```
299
+
300
+ File layout:
301
+ ```
302
+ ./pki/
303
+ └── [cert_path/]
304
+ └── <uuid>/
305
+ ├── service.pem # x509.Certificate
306
+ ├── service.key # RSA private key
307
+ └── service.csr # CertificateSigningRequest
308
+ ```
309
+
310
+ ### `AsyncLocalStorage` (async)
311
+
312
+ Drop-in async replacement for `LocalStorage` — same constructor, same layout, all I/O methods are `async`.
313
+
314
+ ---
315
+
316
+ ## Database Adapters
317
+
318
+ ### `SyncDBHandler`
319
+
320
+ ```python
321
+ from tiny_ca.db.sync_db_manager import SyncDBHandler
322
+
323
+ db = SyncDBHandler(db_url="sqlite:///pki.db")
324
+ # PostgreSQL: "postgresql+psycopg2://user:pass@host/dbname"
325
+ ```
326
+
327
+ ### `AsyncDBHandler`
328
+
329
+ ```python
330
+ from tiny_ca.db.async_db_manager import AsyncDBHandler
331
+
332
+ db = AsyncDBHandler(db_url="sqlite+aiosqlite:///pki.db")
333
+ await db._db.init_db() # create schema on first run
334
+ ```
335
+
336
+ Both implement `BaseDB`:
337
+
338
+ | Method | Description |
339
+ |---|---|
340
+ | `get_by_serial(serial)` | Fetch record by X.509 serial number |
341
+ | `get_by_name(common_name)` | Fetch active VALID record by CN |
342
+ | `register_cert_in_db(cert, uuid, key_type)` | Persist new certificate |
343
+ | `revoke_certificate(serial, reason)` | Mark certificate as revoked |
344
+ | `get_revoked_certificates()` | Yield records for CRL generation |
345
+
346
+ ---
347
+
348
+ ## Serial Number Encoding
349
+
350
+ `SerialWithEncoding` packs three fields into a single 160-bit integer:
351
+
352
+ ```
353
+ [ 16-bit prefix ][ 80-bit name ][ 64-bit random ]
354
+ ```
355
+
356
+ - **prefix** — 2-byte ASCII code of the `CertType` (e.g. `"SV"` for `SERVICE`).
357
+ - **name** — up to 10 ASCII characters from the CN, zero-padded.
358
+ - **random** — lower 64 bits of a fresh `uuid.uuid4()`.
359
+
360
+ ```python
361
+ from tiny_ca.utils.serial_generator import SerialWithEncoding
362
+ from tiny_ca.const import CertType
363
+
364
+ serial = SerialWithEncoding.generate("nginx", CertType.SERVICE)
365
+ cert_type, name = SerialWithEncoding.parse(serial)
366
+ # cert_type == CertType.SERVICE
367
+ # name == "nginx"
368
+ ```
369
+
370
+ ---
371
+
372
+ ## Error Reference
373
+
374
+ | Exception | When raised |
375
+ |---|---|
376
+ | `DBNotInitedError` | A DB-required operation is called but `db_handler` is `None` |
377
+ | `NotUniqueCertOwner` | CN conflict detected and `is_overwrite=False` |
378
+ | `CertNotFound` | `rotate_certificate` called for a non-existent serial |
379
+ | `ValidationCertError` | Issuer mismatch, expired, or signature verification failure |
380
+ | `InvalidRangeTimeCertificate` | Computed `not_after` is already in the past |
381
+ | `FileAlreadyExists` | Target file exists and `is_overwrite=False` |
382
+ | `NotExistCertFile` | CA PEM file path does not exist |
383
+ | `IsNotFile` | CA PEM path exists but is not a regular file |
384
+ | `WrongType` | CA PEM file has an unsupported extension |
385
+ | `ErrorLoadCert` | PEM deserialisation failed |
386
+
387
+ ---
388
+
389
+ ## Running Tests
390
+
391
+ ```bash
392
+ pip install pytest pytest-cov aiosqlite aiofiles
393
+ pytest tests/ --cov=tiny_ca --cov-report=term-missing
394
+ ```
395
+
396
+ ---
397
+
398
+ ## Project Structure
399
+
400
+ ```
401
+ tiny_ca/
402
+ ├── ca_factory/
403
+ │ ├── factory.py # CertificateFactory — crypto generation
404
+ │ └── utils/
405
+ │ ├── file_loader.py # CAFileLoader + ICALoader protocol
406
+ │ ├── afile_loader.py # AsyncCAFileLoader
407
+ │ ├── life_time.py # CertLifetime — validity window helpers
408
+ │ └── serial.py # CertSerialParser
409
+ ├── db/
410
+ │ ├── base_db.py # BaseDB ABC
411
+ │ ├── models.py # CertificateRecord ORM model
412
+ │ ├── const.py # RevokeStatus, CertificateStatus
413
+ │ ├── sync_db_manager.py # SyncDBHandler + DatabaseManager
414
+ │ └── async_db_manager.py # AsyncDBHandler + async DatabaseManager
415
+ ├── managers/
416
+ │ ├── sync_lifecycle_manager.py # CertLifecycleManager
417
+ │ └── async_lifecycle_manager.py # AsyncCertLifecycleManager
418
+ ├── models/
419
+ │ └── certtificate.py # CAConfig, ClientConfig, CertificateInfo
420
+ ├── storage/
421
+ │ ├── base_storage.py # BaseStorage ABC
422
+ │ ├── const.py # CryptoObject type alias
423
+ │ ├── local_storage.py # LocalStorage + _CertSerializer
424
+ │ └── async_local_storage.py # AsyncLocalStorage
425
+ ├── utils/
426
+ │ └── serial_generator.py # SerialGenerator, SerialWithEncoding, _PrefixRegistry
427
+ ├── const.py # CertType enum
428
+ ├── exc.py # All custom exceptions
429
+ └── settings.py # DEFAULT_LOGGER
430
+ ```