Skip to main content

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.

Project description

tiny_ca

Python PyPI Coverage Status Docs License

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.


Table of Contents


Features

Category Capability
CA bootstrap Self-signed root CA and intermediate (sub) CA with configurable path_length
Issuance Leaf certificates with SANs (DNS + IP), EKU (Server/Client Auth), email attribute
Lifecycle Revoke (RFC 5280 reasons), renew (same key), rotate (new key), hard-delete
CRL Build, sign, and verify Certificate Revocation Lists with configurable validity
Inspection Structured CertificateDetails snapshot from any x509.Certificate — fully serialisable
Export PKCS#12 (.p12/.pfx) bundles with full CA chain
Co-signing Re-sign third-party certificates under your CA, preserving Subject and extensions
Chain Build [leaf, ca] fullchain ready for nginx, Apache, or Envoy
Monitoring List certs with filters, find expiring-soon, bulk-expire stale records
Storage LocalStorage (sync) and AsyncLocalStorage (async, aiofiles) with UUID-based isolation
Database SyncDBHandler (SQLAlchemy) and AsyncDBHandler (aiosqlite) — SQLite, PostgreSQL, MySQL
API parity CertLifecycleManager and AsyncCertLifecycleManager are feature-identical
Serial numbers SerialWithEncoding — 160-bit RFC 5280-compliant, type-prefixed, name-encoded
Test coverage 100 % line and branch coverage across all modules

Requirements

  • Python 3.11 or higher
  • Core: cryptography >= 46, sqlalchemy >= 2, pydantic >= 2
  • Async extras: aiofiles, aiosqlite
  • PostgreSQL: psycopg2-binary (sync) / asyncpg (async)
  • MySQL: pymysql (sync) / aiomysql (async)

Architecture

CertLifecycleManager / AsyncCertLifecycleManager   ← application entry-point
        │
        ├── CertificateFactory                      ← crypto-only, zero I/O
        │       ├── ICALoader (Protocol)
        │       │       ├── CAFileLoader            ← sync PEM file loader
        │       │       └── AsyncCAFileLoader       ← async PEM file loader
        │       ├── CertLifetime                    ← validity window helpers
        │       ├── CertSerialParser                ← read serials from certs
        │       └── SerialWithEncoding              ← encode / decode serial numbers
        │
        ├── BaseStorage (ABC)
        │       ├── LocalStorage                    ← sync filesystem (UUID-isolated)
        │       └── AsyncLocalStorage               ← async filesystem (aiofiles)
        │
        └── BaseDB (ABC)
                ├── SyncDBHandler                   ← SQLAlchemy sync
                └── AsyncDBHandler                  ← SQLAlchemy async (aiosqlite)

Every component is injected at construction time — no global singletons, trivially testable.


Installation

# Core sync-only
pip install tiny-ca

# With async support (recommended)
pip install tiny-ca[async]

# PostgreSQL
pip install tiny-ca[postgres]           # sync (psycopg2)
pip install tiny-ca[postgres-async]     # async (asyncpg)

# MySQL
pip install tiny-ca[mysql]              # sync (pymysql)
pip install tiny-ca[mysql-async]        # async (aiomysql)

# Everything
pip install tiny-ca[all]

Quick Start

1. Bootstrap a Root CA

from tiny_ca.managers.sync_lifecycle_manager import CertLifecycleManager
from tiny_ca.storage.local_storage import LocalStorage
from tiny_ca.db.sync_db_manager import SyncDBHandler
from tiny_ca.models.certificate import CAConfig

storage = LocalStorage(base_folder="./pki")
db = SyncDBHandler(db_url="sqlite:///pki.db")
mgr = CertLifecycleManager(storage=storage, db_handler=db)

cert_path, key_path = mgr.create_self_signed_ca(
    CAConfig(common_name="My Root CA", organization="ACME Corp",
             country="UA", key_size=4096, days_valid=3650)
)

Attach the factory so the manager can issue certificates:

from tiny_ca.ca_factory.utils.file_loader import CAFileLoader
from tiny_ca.ca_factory.factory import CertificateFactory

loader = CAFileLoader(ca_cert_path=cert_path, ca_key_path=key_path)
mgr.factory = CertificateFactory(loader)

2. Issue a Leaf Certificate

from tiny_ca.models.certificate import ClientConfig
from tiny_ca.const import CertType

cert, key, csr = mgr.issue_certificate(
    ClientConfig(
        common_name="nginx.internal",
        serial_type=CertType.SERVICE,
        key_size=2048,
        days_valid=365,
        is_server_cert=True,
        san_dns=["nginx.internal", "www.nginx.internal"],
        san_ip=["192.168.1.10"],
    ),
    cert_path="services",
)

3. Renew a Certificate (same key)

Keeps the existing public key — only validity window and serial number change. Use when the key has not been compromised.

renewed = mgr.renew_certificate(serial=cert.serial_number, days_valid=365)

4. Rotate a Certificate (new key)

Atomically revokes the old cert and issues a replacement with a fresh key pair.

new_cert, new_key, new_csr = mgr.rotate_certificate(
    serial=cert.serial_number,
    config=ClientConfig(common_name="nginx.internal",
                        serial_type=CertType.SERVICE, days_valid=365,
                        is_server_cert=True),
)

5. Revoke a Certificate

from cryptography import x509
mgr.revoke_certificate(serial=cert.serial_number, reason=x509.ReasonFlags.key_compromise)

6. Generate and Verify a CRL

crl = mgr.generate_crl(days_valid=7)   # written to <base_folder>/crl.pem
mgr.verify_crl(crl)                     # raises ValidationCertError on failure

7. Issue an Intermediate CA

sub_ca_cert, sub_ca_key = mgr.issue_intermediate_ca(
    common_name="Issuing CA", key_size=4096, days_valid=1825,
    path_length=0, organization="ACME Corp", country="UA",
    cert_path="intermediate",
)

8. Export PKCS#12

p12_bytes = mgr.export_pkcs12(cert=cert, private_key=key,
                               password=b"strong-passphrase", name="nginx.internal")
with open("nginx.p12", "wb") as f:
    f.write(p12_bytes)

9. Co-sign a Third-Party Certificate

from cryptography import x509
third_party = x509.load_pem_x509_certificate(open("partner.pem", "rb").read())
cosigned = mgr.cosign_certificate(cert=third_party, days_valid=365)

10. Inspect a Certificate

details = mgr.inspect_certificate(cert)
print(details.common_name, details.fingerprint_sha256, details.public_key_size)

# Build fullchain.pem for nginx
from cryptography.hazmat.primitives.serialization import Encoding
chain = mgr.get_cert_chain(cert)
fullchain_pem = b"".join(c.public_bytes(Encoding.PEM) for c in chain)

11. Monitor Certificates

records  = mgr.list_certificates(status="valid", key_type="service", limit=50)
expiring = mgr.get_expiring_soon(within_days=30)
updated  = mgr.refresh_expired_statuses()   # run periodically
mgr.delete_certificate(serial=cert.serial_number)

12. Async Usage

import asyncio
from tiny_ca.managers.async_lifecycle_manager import AsyncCertLifecycleManager
from tiny_ca.storage.async_local_storage import AsyncLocalStorage
from tiny_ca.db.async_db_manager import AsyncDBHandler
from tiny_ca.models.certificate import CAConfig, ClientConfig
from tiny_ca.const import CertType

async def main():
    storage = AsyncLocalStorage(base_folder="./pki_async")
    db = AsyncDBHandler(db_url="sqlite+aiosqlite:///pki_async.db")
    await db._db.init_db()

    mgr = AsyncCertLifecycleManager(storage=storage, db_handler=db)
    cert_path, key_path = await mgr.create_self_signed_ca(
        CAConfig(common_name="Async CA", organization="ACME",
                 country="UA", key_size=2048, days_valid=3650)
    )

    from tiny_ca.ca_factory.utils.afile_loader import AsyncCAFileLoader
    from tiny_ca.ca_factory.factory import CertificateFactory
    loader = await AsyncCAFileLoader.create(cert_path, key_path)
    mgr.factory = CertificateFactory(loader)

    cert, key, csr = await mgr.issue_certificate(
        ClientConfig(common_name="api.internal", serial_type=CertType.SERVICE,
                     key_size=2048, days_valid=365, is_server_cert=True)
    )
    details = await mgr.inspect_certificate(cert)
    p12 = await mgr.export_pkcs12(cert, key, password=b"secret")

asyncio.run(main())

Complete Examples

File Description
examples/complete_example.py Sync API — full lifecycle
examples/acomplete_example.py Async API — full lifecycle
python examples/complete_example.py
python examples/acomplete_example.py

Configuration Models

CAConfig

Field Type Default Description
common_name str "Internal CA" CA Common Name (CN)
organization str "My Company" Organization (O)
country str "UA" Two-letter ISO 3166-1 alpha-2 code
key_size int 2048 RSA key length in bits
days_valid int 3650 Validity period in days
valid_from datetime | None None Explicit UTC start; None = now

ClientConfig

Field Type Default Description
common_name str Certificate CN
serial_type CertType SERVICE Certificate category
key_size int 2048 RSA key length
days_valid int 3650 Validity period
email EmailStr | None None emailAddress Subject attribute
is_server_cert bool True Adds ServerAuth EKU + CN as DNS SAN
is_client_cert bool False Adds ClientAuth EKU
san_dns list[str] | None None Extra DNS Subject Alternative Names
san_ip list[str] | None None IP address SANs
name str | None None Override output file base name

CertType

Value String Description
CertType.CA "CA" Root or intermediate CA
CertType.USER "USR" User / human certificate
CertType.SERVICE "SVC" Service / server certificate
CertType.DEVICE "DEV" IoT / device certificate
CertType.INTERNAL "INT" Internal infrastructure certificate

Storage Backends

LocalStorage (sync)

from tiny_ca.storage.local_storage import LocalStorage
storage = LocalStorage(base_folder="./pki")
./pki/
└── [cert_path/]
    └── <uuid>/
        ├── nginx.pem    ← x509.Certificate
        ├── nginx.key    ← RSA private key
        └── nginx.csr    ← CSR

AsyncLocalStorage (async)

Drop-in async replacement — same constructor, same layout, all I/O via aiofiles.


Database Adapters

# Sync
db = SyncDBHandler(db_url="sqlite:///pki.db")
db = SyncDBHandler(db_url="postgresql+psycopg2://user:pass@host/pki")

# Async
db = AsyncDBHandler(db_url="sqlite+aiosqlite:///pki.db")
db = AsyncDBHandler(db_url="postgresql+asyncpg://user:pass@host/pki")
await db._db.init_db()

BaseDB contract

Method Description
get_by_serial(serial) Fetch any record by serial number
get_by_name(cn) Fetch active VALID record by CN
register_cert_in_db(cert, uuid, key_type) Persist a new certificate
revoke_certificate(serial, reason) Mark as revoked (RFC 5280)
get_revoked_certificates() Yield rows for CRL generation
list_all(status, key_type, limit, offset) Paginated listing with filters
get_expiring(within_days) VALID certs expiring within N days
delete_by_uuid(uuid) Hard-delete a record
update_status_expired() Bulk-mark stale VALID records as EXPIRED

Serial Number Encoding

SerialWithEncoding packs three fields into a 160-bit integer (RFC 5280):

┌──────────────┬──────────────────────┬────────────────────┐
│  16-bit type │  80-bit name         │  64-bit random     │
│  prefix      │  (up to 10 chars)    │  (UUID fragment)   │
└──────────────┴──────────────────────┴────────────────────┘
from tiny_ca.utils.serial_generator import SerialWithEncoding
from tiny_ca.const import CertType

serial = SerialWithEncoding.generate("nginx", CertType.SERVICE)
cert_type, name = SerialWithEncoding.parse(serial)
# cert_type == CertType.SERVICE, name == "nginx"

Error Reference

Exception When raised Resolution
DBNotInitedError db_handler is None Pass db_handler to the manager
NotUniqueCertOwner CN conflict, is_overwrite=False Use is_overwrite=True
CertNotFound renew/rotate for unknown serial Verify the serial exists
ValidationCertError Bad issuer, expired, invalid signature Check cert origin and CA
InvalidRangeTimeCertificate not_after already in the past Fix valid_from or days_valid
FileAlreadyExists File exists, is_overwrite=False Use is_overwrite=True
NotExistCertFile CA PEM path missing Check the file path
IsNotFile CA PEM path is a directory Provide a file, not a directory
WrongType Unsupported file extension Use .pem, .key, or .csr
ErrorLoadCert PEM deserialisation failed Check file format and integrity

FAQ

Can I use an existing CA? Yes — load it with CAFileLoader or AsyncCAFileLoader.

What's the difference between renew and rotate? renew keeps the key pair and extends validity. rotate generates a new key and revokes the old cert.

How do I schedule CRL regeneration? Use APScheduler or any cron solution:

scheduler.add_job(mgr.generate_crl, "cron", day_of_week="mon", hour=0)

Can I use a custom storage backend (S3, Redis)? Yes — subclass BaseStorage and implement save_certificate and delete_certificate_folder.

How do I protect the CA private key with a password?

loader = CAFileLoader(ca_cert_path="ca.pem", ca_key_path="ca.key",
                      ca_key_password=b"passphrase")

Migrating from Other CAs

From OpenSSL

openssl x509 -in ca.crt -out ca.pem -outform PEM
openssl rsa  -in ca.key -out ca-key.pem -outform PEM
loader = CAFileLoader(ca_cert_path="ca.pem", ca_key_path="ca-key.pem")
mgr.factory = CertificateFactory(loader)

From Easy-RSA / CFSSL

Both output standard PEM files — follow the OpenSSL migration steps.


Benchmark Results

Linux 6.17, Python 3.11.15, 32-core CPU, NVMe SSD. 5 iterations each.

Operation Sync Async
CA creation (2048-bit) 0.037 s 0.067 s
CA creation (4096-bit) 0.317 s 0.411 s
Leaf issuance (2048-bit) 0.055 s 0.052 s
Leaf issuance (4096-bit) 0.476 s 0.712 s
CRL generation 0.001 s 0.002 s
Certificate verification 0.0003 s 0.0008 s
PKCS#12 export 0.0005 s 0.0006 s

Key generation dominates issuance time. For >1 000 certs/hour use PostgreSQL, the async API, and connection pooling.


Project Structure

tiny_ca/
├── const.py                        # CertType, ALLOWED_CERT_EXTENSIONS
├── exc.py                          # all custom exceptions
├── settings.py                     # DEFAULT_LOGGER
├── ca_factory/
│   ├── factory.py                  # CertificateFactory — all crypto
│   └── utils/
│       ├── file_loader.py          # CAFileLoader + ICALoader Protocol
│       ├── afile_loader.py         # AsyncCAFileLoader
│       ├── life_time.py            # CertLifetime
│       └── serial.py               # CertSerialParser
├── db/
│   ├── base_db.py                  # BaseDB — 9 abstract methods
│   ├── models.py                   # CertificateRecord ORM model
│   ├── const.py                    # RevokeStatus, CertificateStatus
│   ├── sync_db_manager.py          # SyncDBHandler
│   └── async_db_manager.py         # AsyncDBHandler
├── managers/
│   ├── sync_lifecycle_manager.py   # CertLifecycleManager (20+ ops)
│   └── async_lifecycle_manager.py  # AsyncCertLifecycleManager
├── models/
│   └── certificate.py              # CAConfig, ClientConfig, CertificateDetails
├── storage/
│   ├── base_storage.py             # BaseStorage ABC
│   ├── const.py                    # CryptoObject type alias
│   ├── local_storage.py            # LocalStorage
│   └── async_local_storage.py      # AsyncLocalStorage
└── utils/
    └── serial_generator.py         # SerialWithEncoding

Security Policy

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.


License

MIT © 2025 Denis Shchutskyi

Dependency License
cryptography BSD 3-Clause
SQLAlchemy MIT
Pydantic MIT
aiosqlite MIT
aiofiles Apache 2.0

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tiny_ca-0.2.0.tar.gz (119.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tiny_ca-0.2.0-py3-none-any.whl (88.0 kB view details)

Uploaded Python 3

File details

Details for the file tiny_ca-0.2.0.tar.gz.

File metadata

  • Download URL: tiny_ca-0.2.0.tar.gz
  • Upload date:
  • Size: 119.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"25.10","id":"questing","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for tiny_ca-0.2.0.tar.gz
Algorithm Hash digest
SHA256 49847c8c4938479697e881522cb4f2b71582877b4e1d5983b4bf3a275eb2e649
MD5 2bf68b7ceef0fb6b36b4f8050ee1396e
BLAKE2b-256 5f71f0d9dafcbff8874124c38723b008560c3002f24fcc1e6920ac53b760f768

See more details on using hashes here.

File details

Details for the file tiny_ca-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: tiny_ca-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 88.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.6 {"installer":{"name":"uv","version":"0.11.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"25.10","id":"questing","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for tiny_ca-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2a9cf6b4f3937cb652dfaff0441c7580338adfb7b60f20d725c0ec003c7721e4
MD5 40daafb3f8758719f9142545e05a95c0
BLAKE2b-256 9385c8b8638dbef289246d5048ef5a7a84dafe0a517c5e73a88ef3eb8135986d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page