Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,51 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [1.1.0] - 2026-02-12
### Added
- Command-line interface (`certapi` / `cli`) exposing common workflows: `issue`, `renew`, `list`, and `revoke`.
- Config file support and environment variable overrides for local/CI usage.
- Enhanced logging, debug flags, and more informative CLI error messages.
### Changed
- Improved CLI-friendly output formats (plain text and JSON) for scripting and automation.
### Fixed
- Various integration and usability issues discovered during CLI testing.

## [1.0.5] - 2026-02-08
### Added
- Packaging and CI improvements: `pyproject.toml` / `requirements.txt` updates and release automation tweaks.
### Fixed
- Docker image tagging and Dockerfile fixes for reproducible builds.
- Miscellaneous minor bugfixes and documentation tweaks.

## [1.0.4] - 2026-02-02
### Added
- Postgres keystore robustness improvements and better sqlite fallback handling.
### Fixed
- Packaging metadata and dependency pinning issues causing install-time warnings.

## [1.0.3] - 2026-01-28
### Added
- Improved DNS provider integrations (Cloudflare/DigitalOcean) for TXT record cleanup.
### Fixed
- Race conditions during challenge creation and cleanup under heavy concurrency.
- Robustness fixes for order certificate retrieval and decoding.

## [1.0.2] - 2026-01-22
### Added
- Additional sanity checks when loading keys and certificates from keystores.
### Fixed
- Retry/backoff handling for transient HTTP and DNS provider errors.
- Test stability fixes for challenge cleanup routines.

## [1.0.1] - 2026-01-20
### Added
- Small improvements to logging and diagnostic output for ACME flows.
### Fixed
- Keystore path handling edge-cases that caused certificate lookups to fail.
- Minor bugfixes in ACME error parsing to avoid missing-detail exceptions.

## [1.0.0] - 2026-01-15
### Added
- Production Docker image (multi-arch, rootless, Gunicorn, port `8080`).
Expand All @@ -15,6 +60,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Standardized environment variables, error handling, and minor typos.


## [0.6.0] - 2026-01-12
### ToDo
- [] Certapi api and docker image
Expand Down
82 changes: 82 additions & 0 deletions Developer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
Developer Guide
==============

Using CertApi as a Library
--------------------------

The library supports both low-level ACME operations and a higher-level manager that handles
storage and renewals. Pick the approach that fits your integration needs.

1.  Low-Level API: Certificate with Cloudflare
------------------------------------------

```python
import json
from certapi import CertApiException, CloudflareChallengeSolver, Key, AcmeCertIssuer


# Initialize the Cloudflare challenge solver
# The API key is read from the CLOUDFLARE_API_KEY environment variable, or you can set it below.
challenge_solver = CloudflareChallengeSolver(api_key=None)

# Initialize cert issuer with a new account key
cert_issuer = AcmeCertIssuer(Key.generate("ecdsa"), challenge_solver)

# Perform setup i.e. fetching directory and registering ACME account
cert_issuer.setup()

try:
# Obtain a certificate for your domain
(key, cert) = cert_issuer.generate_key_and_cert_for_domain("your-domain.com")

print("------ Private Key -----")
print(key.to_pem())
print("------- Certificate ------")
print(cert)
except CertApiException as e:
print("An error occurred:", json.dumps(e.json_obj(), indent=2))
```

2.  High-Level API: AcmeCertManager
-------------------------------

The `AcmeCertManager` provides a high-level interface that handles certificate storage,
automatic renewal checks, and multi-solver management.

```python
from certapi import (
AcmeCertManager,
FileSystemKeyStore,
AcmeCertIssuer,
CloudflareChallengeSolver,
)

# 1. Setup KeyStore to persist keys and certificates
key_store = FileSystemKeyStore("db")


# DNS-01 via Cloudflare (e.g. for wildcard certs or internal domains)
dns_solver = CloudflareChallengeSolver(api_key="your-cloudflare-token")

# 3. Initialize and Setup AcmeCertManager
# Create cert issuer with the default challenge solver
cert_issuer = AcmeCertIssuer.with_keystore(key_store, dns_solver)

cert_manager = AcmeCertManager(
key_store=key_store,
cert_issuer=cert_issuer,
challenge_solvers=[dns_solver], # other solvers can be used
)
cert_manager.setup()

# 4. Issue or Reuse Certificate
# Automatically checks and saves to keystore. Renews only if necessary.
response = cert_manager.issue_certificate(["example.com", "www.example.com"])

for cert_data in response.issued:
print(f"Newly issued for: {cert_data.domains}")
print(cert_data.cert)

for cert_data in response.existing:
print(f"Reusing existing for: {cert_data.domains}")
```
93 changes: 33 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ CertApi is a base library for building other tools, or to integrate Certificate
[![codecov](https://codecov.io/github/mesudip/certapi/graph/badge.svg?token=NYTNCH29IT)](https://codecov.io/github/mesudip/certapi)
[![PyPI version](https://img.shields.io/pypi/v/certapi.svg)](https://pypi.org/project/certapi/)

## Why another library?

I designed this library so that it can be imported and plugged in to other python projects. Goal is not to provide CLIs or quick working demo, but to be versatile for any use case.

- Pluggable keystores for keys and certificates
- Pluggable Challenge solvers for DNS and Http challenge solving
- High-level manager with renewal checks and multi-solver support
- Same interface for working locally, or requesting certificate from certapi server.

See the developer guide in [Developer.md](Developer.md) for library usage and workflows.


## Installation
Expand All @@ -19,75 +29,38 @@ You can install CertApi using pip
pip install certapi
```

## Example: Low Leve API : Certificate with Cloudflare

```python
import json
from certapi import CertApiException, CloudflareChallengeSolver, Key, AcmeCertIssuer


# Initialize the Cloudflare challenge solver
# The API key is read from the CLOUDFLARE_API_KEY environment variable, or you can set it below.
challenge_solver = CloudflareChallengeSolver(api_key=None)

## initialize cert issuer with a new account key
cert_issuer = AcmeCertIssuer(Key.generate('ecdsa'), challenge_solver)

# Preform setup i.e. fetching directory and registering ACME account
cert_issuer.setup()

try:
# Obtain a certificate for your domain
(key, cert) = cert_issuer.generate_key_and_cert_for_domain("your-domain.com")
## CLI

print("------ Private Key -----")
print(key.to_pem())
print("------- Certificate ------")
print(cert)
except CertApiException as e:
print(f"An error occurred:", json.dumps(e.json_obj(), indent=2))
CertApi also ships with a CLI for quick verification and certificate issuance.

```bash
## Certapi's dependencies are already included in the python installation. This doesn't affect the system.
sudo python3 -m pip install certapi --break-system-packages
```

### 1. With HTTP Challenge

## Example: High Level API (with AcmeCertManager)

The `AcmeCertManager` provides a high-level interface that handles certificate storage, automatic renewal checks, and multi-solver management.

```python
from certapi import (
AcmeCertManager,
FileSystemKeyStore,
AcmeCertIssuer,
CloudflareChallengeSolver
)

# 1. Setup KeyStore to persist keys and certificates
key_store = FileSystemKeyStore("db")
HTTP challenge requires you to have seup the DNS correctly. The ACME server will verify your domain by making an HTTP request.

```bash
# Verify environment and HTTP routing
sudo certapi verify example.com www.example.com

# DNS-01 via Cloudflare (e.g. for wildcard certs or internal domains)
dns_solver = CloudflareChallengeSolver(api_token="your-cloudflare-token")
# Obtain a certificate (requires root for HTTP-01)
sudo certapi obtain example.com www.example.com
```

# 3. Initialize and Setup AcmeCertManager
# Create cert issuer with the default challenge solver
cert_issuer = AcmeCertIssuer.with_keystore(key_store, dns_solver)
### 2. With DNS Provider Key

cert_manager = AcmeCertManager(
key_store=key_store,
cert_issuer=cert_issuer,
challenge_solvers=[dns_solver], # other solvers can be used
)
cert_manager.setup()
Using DNS-01 challenge with Cloudflare doesn't require the DNS to be setup. Set your Cloudflare API key as an environment variable.

# 4. Issue or Reuse Certificate
# Automatically checks sand saves to keystore. Renews only if necessary.
response = cert_manager.issue_certificate(["example.com", "www.example.com"])
```bash
# Set Cloudflare API key or token
export CLOUDFLARE_API_KEY=... # or CLOUDFLARE_API_TOKEN

for cert_data in response.issued:
print(f"Newly issued for: {cert_data.domains}")
print(cert_data.cert)
# Verify DNS configuration
sudo certapi verify example.com

for cert_data in response.existing:
print(f"Reusing existing for: {cert_data.domains}")
```
# Obtain a certificate using DNS-01
sudo certapi obtain example.com www.example.com
```
7 changes: 6 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name="certapi",
version="1.0.5",
version="1.1.0",
packages=find_packages(where="src"),
package_dir={"": "src"},
install_requires=[
Expand All @@ -22,4 +22,9 @@
"Operating System :: OS Independent",
],
python_requires=">=3.6",
entry_points={
"console_scripts": [
"certapi=certapi.cli:main",
],
},
)
6 changes: 6 additions & 0 deletions src/certapi/challenge_solver/InmemoryChallengeSolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ def __init__(self):
def supported_challenge_type(self) -> Literal["http-01"]:
return "http-01"

def supports_domain(self, domain: str) -> bool:
return "*" not in domain

def save_challenge(self, key: str, value: str, domain: str = None):
self.challenges[key] = value

Expand All @@ -23,6 +26,9 @@ def delete_challenge(self, key: str, domain: str = None):
if key in self.challenges:
del self.challenges[key]

def cleanup_old_challenges(self):
self.challenges.clear()

def __iter__(self):
return iter(self.challenges)

Expand Down
Loading
Loading