tiny-ca 0.1.2__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.
- tiny_ca-0.2.0/LICENSE +33 -0
- tiny_ca-0.2.0/PKG-INFO +589 -0
- tiny_ca-0.2.0/README.md +547 -0
- tiny_ca-0.2.0/docs/source/conf.py +61 -0
- tiny_ca-0.2.0/pyproject.toml +180 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tests/test_async_db_handler.py +287 -0
- tiny_ca-0.2.0/tests/test_async_lifecycle_manager.py +1191 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tests/test_base_db.py +35 -1
- tiny_ca-0.2.0/tests/test_factory.py +1251 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tests/test_file_loader.py +1 -1
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tests/test_life_time.py +33 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tests/test_sync_db_manager.py +233 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tests/test_sync_lifecycle_manager.py +213 -5
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/ca_factory/factory.py +366 -6
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/ca_factory/utils/afile_loader.py +1 -1
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/ca_factory/utils/file_loader.py +1 -1
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/ca_factory/utils/life_time.py +46 -14
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/ca_factory/utils/serial.py +12 -12
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/db/async_db_manager.py +160 -1
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/db/base_db.py +91 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/db/models.py +17 -15
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/db/sync_db_manager.py +157 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/managers/async_lifecycle_manager.py +153 -70
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/managers/sync_lifecycle_manager.py +149 -69
- tiny_ca-0.2.0/tiny_ca/models/__init__.py +3 -0
- tiny_ca-0.1.2/tiny_ca/models/certtificate.py → tiny_ca-0.2.0/tiny_ca/models/certificate.py +4 -4
- tiny_ca-0.2.0/tiny_ca.egg-info/PKG-INFO +589 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca.egg-info/SOURCES.txt +3 -2
- tiny_ca-0.2.0/tiny_ca.egg-info/requires.txt +23 -0
- tiny_ca-0.2.0/tiny_ca.egg-info/top_level.txt +3 -0
- tiny_ca-0.1.2/PKG-INFO +0 -431
- tiny_ca-0.1.2/README.md +0 -417
- tiny_ca-0.1.2/pyproject.toml +0 -123
- tiny_ca-0.1.2/tests/test_async_lifecycle_manager.py +0 -613
- tiny_ca-0.1.2/tests/test_factory.py +0 -554
- tiny_ca-0.1.2/tests/test_factory_missing_coverage.py +0 -548
- tiny_ca-0.1.2/tiny_ca/models/__init__.py +0 -3
- tiny_ca-0.1.2/tiny_ca.egg-info/PKG-INFO +0 -431
- tiny_ca-0.1.2/tiny_ca.egg-info/requires.txt +0 -7
- tiny_ca-0.1.2/tiny_ca.egg-info/top_level.txt +0 -1
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/setup.cfg +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tests/test_afile_loader.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tests/test_async_local_storage.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tests/test_base_storage.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tests/test_database_manager.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tests/test_db_const.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tests/test_local_storage.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tests/test_models.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tests/test_serial.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tests/test_serial_generator.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tests/test_storage_const.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/__init__.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/ca_factory/__init__.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/ca_factory/utils/__init__.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/const.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/db/__init__.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/db/const.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/exc.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/managers/__init__.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/settings.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/storage/__init__.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/storage/async_local_storage.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/storage/base_storage.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/storage/const.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/storage/local_storage.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/utils/__init__.py +0 -0
- {tiny_ca-0.1.2 → tiny_ca-0.2.0}/tiny_ca/utils/serial_generator.py +0 -0
- {tiny_ca-0.1.2 → 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
|
+
[](https://github.com/Shchusia/tiny_ca)
|
|
46
|
+
[](https://pypi.org/project/tiny-ca/)
|
|
47
|
+
[](https://coveralls.io/github/Shchusia/tiny_ca?branch=feature/docs)
|
|
48
|
+
[](https://shchusia.github.io/tiny_ca/)
|
|
49
|
+
[](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 |
|