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