Development
Project structure, testing, hooks, and contributing
Project Structure
src/acmeeh/
├── __init__.py # Version: 1.0.0
├── __main__.py # CLI entry point
├── cli/
│ ├── main.py # Argument parsing & dispatch
│ └── commands/
│ ├── admin.py # admin create-user
│ ├── ca.py # ca test-sign
│ ├── crl.py # crl rebuild
│ ├── db.py # db status, db migrate
│ ├── inspect.py # inspect order/certificate/account
│ └── serve.py # serve (start server)
├── config/
│ ├── __init__.py # Exports AcmeehConfig, get_config
│ ├── acmeeh_config.py # ConfigKit subclass
│ ├── settings.py # Frozen dataclasses (27 sections)
│ └── schema.json # JSON Schema validation
├── app/
│ ├── factory.py # create_app(config, database)
│ ├── context.py # DI container: get_container()
│ ├── errors.py # AcmeProblem exception
│ ├── middleware.py # Request/response middleware
│ ├── rate_limiter.py # Per-endpoint rate limiting
│ └── shutdown.py # Graceful shutdown handler
├── core/
│ ├── types.py # Enums: OrderStatus, ChallengeType, etc.
│ ├── state.py # State machine transitions
│ ├── jws.py # JWS/JWK/JWK Thumbprint (RFC 7515/7517/7638)
│ └── urls.py # URL builder utilities
├── api/
│ ├── __init__.py # register_blueprints()
│ ├── directory.py # GET /directory
│ ├── nonce.py # HEAD/GET /new-nonce
│ ├── account.py # POST /new-account, /acct/{id}
│ ├── order.py # POST /new-order, /order/{id}, /order/{id}/finalize
│ ├── authorization.py # POST /authz/{id}
│ ├── challenge_routes.py # POST /chall/{id}
│ ├── certificate.py # POST /cert/{id}, /revoke-cert
│ ├── key_change.py # POST /key-change
│ ├── new_authz.py # POST /new-authz
│ ├── crl.py # GET /crl (optional)
│ ├── renewal_info.py # GET /renewalInfo/{id} (optional)
│ ├── metrics.py # GET /metrics (optional)
│ ├── serializers.py # JSON serialization helpers
│ └── decorators.py # ACME response headers
├── models/ # Frozen dataclass models
│ ├── account.py
│ ├── authorization.py
│ ├── certificate.py
│ ├── challenge.py
│ ├── nonce.py
│ ├── notification.py
│ └── order.py
├── repositories/ # BaseRepository[T] subclasses
│ ├── account.py
│ ├── authorization.py
│ ├── certificate.py
│ ├── challenge.py
│ ├── nonce.py
│ ├── notification.py
│ └── order.py
├── services/ # Business logic
│ ├── account.py # Account creation, update, deactivation
│ ├── authorization.py # Authorization lifecycle
│ ├── certificate.py # Certificate issuance and revocation
│ ├── challenge.py # Challenge creation and validation
│ ├── cleanup_worker.py # Nonce GC, order expiry, data retention
│ ├── csr_validator.py # CSR validation against profiles
│ ├── expiration_worker.py # Certificate expiration warnings
│ ├── key_change.py # Account key rollover
│ ├── nonce.py # Nonce generation and tracking
│ ├── notification.py # Email notifications with retry
│ ├── order.py # Order creation and finalization
│ ├── renewal_info.py # ARI renewal information
│ └── workers.py # Worker orchestrator (starts background threads)
├── ca/ # CA backends
│ ├── base.py # CABackend ABC, IssuedCertificate, CAError
│ ├── registry.py # Backend loader/registry
│ ├── internal.py # File-based CA
│ ├── external.py # HTTP API CA
│ ├── hsm.py # PKCS#11 HSM CA
│ ├── acme_proxy.py # Upstream ACME CA
│ ├── failover.py # Multi-backend failover wrapper
│ ├── circuit_breaker.py # Circuit breaker for CA backends
│ ├── crl.py # CRL generation and management
│ ├── ct.py # Certificate Transparency submission
│ ├── caa.py # CAA record checking
│ ├── cert_utils.py # Certificate parsing utilities
│ └── upstream_handlers.py # Challenge handlers for ACME proxy
├── challenge/ # Challenge validators
│ ├── base.py # ChallengeValidator ABC
│ ├── registry.py # Validator registry
│ ├── http01.py # HTTP-01 validator
│ ├── dns01.py # DNS-01 validator
│ ├── tls_alpn01.py # TLS-ALPN-01 validator
│ └── auto_accept.py # Auto-accept validator (for ACME proxy)
├── hooks/ # Hook system
│ ├── base.py # Hook ABC
│ ├── events.py # Event definitions
│ ├── registry.py # Hook registry and dispatcher
│ ├── ct_hook.py # CT log submission hook
│ └── audit_export_hook.py # Audit event export hook
├── admin/ # Admin API
│ ├── routes.py # Flask blueprint (49 endpoints)
│ ├── auth.py # JWT auth, token blacklist, rate limiting
│ ├── service.py # AdminUserService
│ ├── repository.py # Admin repositories (users, audit, EAB, etc.)
│ ├── models.py # Admin data models
│ ├── serializers.py # JSON serializers
│ ├── pagination.py # Cursor-based pagination
│ └── password.py # Password hashing and generation
├── notifications/ # Email notification system
│ ├── renderer.py # Jinja2 template rendering
│ └── templates/ # 17 notification type templates
├── logging/ # Logging and audit
│ ├── setup.py # Log configuration (JSON/human format)
│ ├── sanitize.py # Log message sanitization
│ ├── security_events.py # Security event logging
│ └── audit_cleanup.py # Audit log retention cleanup
├── metrics/ # Prometheus metrics
│ └── collector.py # Metric definitions and collector
├── server/ # WSGI server
│ ├── gunicorn_app.py # Custom gunicorn application class
│ └── wsgi.py # WSGI entry point (ACMEEH_CONFIG env)
└── db/
├── __init__.py # Database initialization
├── init.py # Schema auto-setup
├── unit_of_work.py # Transaction unit of work
└── schema.sql # PostgreSQL schema
Running Tests
# Run all tests
PYTHONPATH=src python -m pytest tests/
# Run a specific test file
PYTHONPATH=src python -m pytest tests/test_config.py -v
# Run a single test
PYTHONPATH=src python -m pytest tests/test_config.py::test_name -v
# Run with coverage
PYTHONPATH=src python -m pytest tests/ --cov=acmeeh --cov-report=html
Note
Config Reset
The fresh_config autouse fixture in tests/conftest.py automatically resets the ConfigKit singleton before and after every test. You don’t need to worry about config leaking between tests.
Test Structure
tests/
├── conftest.py # Shared fixtures (fresh_config, etc.)
├── test_config.py
├── test_settings.py
├── test_jws.py
├── test_state.py
├── ...
└── integration/
├── conftest.py # Full app fixtures with mocked DB
├── test_directory.py
├── test_account.py
├── test_order.py
└── ...
Hook System
ACMEEH has a pluggable hook system that fires on lifecycle events. Hooks run asynchronously in a thread pool and don’t block the request.
For the full list of available events, context keys, and configuration options, see the Extensibility guide.
Writing a Hook
All custom hooks must inherit from Hook and override the event methods they need. Unimplemented methods are no-ops. The constructor receives an optional config dict from the hook entry in the YAML configuration.
from acmeeh.hooks.base import Hook
class SlackNotifier(Hook):
def __init__(self, config: dict | None = None):
super().__init__(config)
self.webhook_url = self.config["webhook_url"]
@classmethod
def validate_config(cls, config: dict) -> None:
# Called at load time — raise ValueError if invalid
if "webhook_url" not in config:
raise ValueError("webhook_url is required")
def on_certificate_issuance(self, ctx: dict):
# ctx contains: certificate_id, order_id, account_id,
# serial_number, domains, not_after, pem_chain
domains = ctx["domains"]
serial = ctx["serial_number"]
# POST to Slack webhook...
def on_certificate_revocation(self, ctx: dict):
# ctx contains: certificate_id, account_id,
# serial_number, reason
...
Register the hook in config (see Extensibility for the full YAML schema and additional examples):
hooks:
registered:
- class: my_hooks.SlackNotifier
events: [certificate.issuance, certificate.revocation]
config:
webhook_url: https://hooks.slack.com/...
Built-in Hooks
ACMEEH ships with two built-in hook implementations:
Hook |
Module |
Purpose |
|---|---|---|
CT Log Hook |
|
Submits issued certificates to Certificate Transparency logs (configured via |
Audit Export Hook |
|
Exports audit events to external systems via webhook or syslog (configured via |
Enum Reference
Core enumerated types from core/types.py used throughout the API and database.
AccountStatus
valid— Active accountdeactivated— Self-deactivated by account holderrevoked— Revoked by administrator
OrderStatus
pending— Awaiting challenge validationready— All authorizations valid, ready for finalizationprocessing— CSR submitted, certificate being issuedvalid— Certificate issuedinvalid— One or more authorizations failed
ChallengeStatus
pending— Waiting for client to respondprocessing— Validation in progressvalid— Validation succeededinvalid— Validation failed
RevocationReason (RFC 5280 u00a75.3.1)
Code |
Name |
Description |
|---|---|---|
0 |
|
No specific reason |
1 |
|
Private key compromised |
2 |
|
CA key compromised |
3 |
|
Subject’s affiliation changed |
4 |
|
Certificate replaced by a new one |
5 |
|
Subject no longer operates |
6 |
|
Certificate temporarily suspended |
8 |
|
Remove from CRL (delta CRL) |
9 |
|
Privilege for certificate withdrawn |
10 |
|
Attribute Authority compromised |
NotificationType
delivery_succeeded,delivery_failed— Certificate delivery eventsrevocation_succeeded,revocation_failed— Revocation eventsregistration_succeeded,registration_failed— Account registration eventsadmin_user_created,admin_password_reset— Admin user eventsexpiration_warning— Certificate expiration warningorder_rejected— Order rejected by policyorder_quota_exceeded— Account exceeded order quotaorder_stale_recovered— Stale processing order recoveredchallenge_failed— Challenge validation failedcsr_validation_failed— CSR validation failed against profileaccount_deactivated— Account deactivated by holderkey_rollover_succeeded— Account key rollover succeededauthorization_deactivated— Authorization deactivated
AdminRole
admin— Full access to all admin API endpointsauditor— Read-only access to users, audit logs, certificates, and CSR profiles
Adding a CA Backend
To add a new built-in CA backend, update all of the following:
``config/settings.py`` — Add a new frozen dataclass for the backend’s settings and a
_build_*function. Add the field toCASettings.``config/schema.json`` — Add the JSON Schema definition for the new backend’s configuration.
``config/acmeeh_config.py`` — Add any validation rules in
additional_checks().``ca/registry.py`` — Register the new backend name in the registry so it can be loaded.
``ca/your_backend.py`` — Implement the
CABackendsubclass.Tests — Update all test files that construct
CASettingsdirectly to include the new field.
Warning
Important
Many tests construct CASettings directly. If you add a field, you must update every test that does so, or tests will fail with missing argument errors.
Adding a Challenge Validator
Challenge validators live in challenge/ and implement the validation logic for each challenge type. To add a new type:
Add the challenge type to
core/types.pyin theChallengeTypeenumCreate a validator class in
challenge/Register it in the challenge registry
Add configuration in
config/settings.pyUpdate
config/schema.json
CLI Commands Reference
See Deployment for the full CLI reference (subcommands and global flags).
Code Conventions
Models — All model classes are frozen dataclasses (immutable after creation)
Repositories — Extend
BaseRepository[T]from PyPGKit. Return model instances.Services — Business logic layer. Coordinate between repositories and CA backends.
Errors — Use
AcmeProblemfor all user-facing errors. Follows RFC 7807 Problem Details.JWS — Custom implementation in
core/jws.pyusing thecryptographylibrary (no josepy dependency).Config — ConfigKit singleton via metaclass. Always reset between tests.
Contact validation — Email regex requires a dot in the domain part (
test@localhostis rejected).SQL schema — Tables use
IF NOT EXISTS; triggers useDROP IF EXISTS+CREATE.