Challenge Handlers¶
Handlers for completing ACME challenges. The library supports four challenge types: DNS-01, HTTP-01, TLS-ALPN-01, and the draft DNS-PERSIST-01.
ChallengeHandler (Base)¶
- class acmeow.ChallengeHandler[source]¶
Bases:
ABCAbstract base class for ACME challenge handlers.
Challenge handlers are responsible for deploying and cleaning up challenge responses. Implementations must handle both setup and cleanup to ensure proper resource management.
Example
>>> class MyDnsHandler(ChallengeHandler): ... def setup(self, domain, token, key_authorization): ... # Create DNS TXT record ... pass ... def cleanup(self, domain, token): ... # Remove DNS TXT record ... pass
- abstractmethod setup(domain, token, key_authorization)[source]¶
Deploy the challenge response.
This method is called before notifying the ACME server that the challenge is ready for validation. It should set up whatever resource is needed for the challenge type (DNS record, HTTP file, etc.).
- Parameters:
- Raises:
Exception – If setup fails and the challenge cannot proceed.
- Return type:
- abstractmethod cleanup(domain, token)[source]¶
Remove the challenge response.
This method is called after challenge validation completes (whether successful or not) to clean up deployed resources.
- Parameters:
- Return type:
Note
This method should not raise exceptions even if cleanup fails, as it’s called during error handling paths.
DNS-01 Handlers¶
CallbackDnsHandler¶
- class acmeow.CallbackDnsHandler[source]¶
Bases:
ChallengeHandlerDNS-01 handler using user-provided callbacks.
This handler delegates DNS record management to user-provided callback functions, allowing integration with any DNS provider.
- Parameters:
create_record (
Callable[[str,str,str],None]) – Callback to create a DNS TXT record. Signature: (domain: str, record_name: str, record_value: str) -> None - domain: The domain being validated (e.g., “example.com”) - record_name: The full record name (e.g., “_acme-challenge.example.com”) - record_value: The TXT record value (base64url SHA-256 hash)delete_record (
Callable[[str,str],None]) – Callback to delete a DNS TXT record. Signature: (domain: str, record_name: str) -> Nonepropagation_delay (
int) – Seconds to wait after creating record. Default 60.
Example
>>> def create_txt(domain, name, value): ... dns_api.create_record(name, "TXT", value) >>> def delete_txt(domain, name): ... dns_api.delete_record(name, "TXT") >>> handler = CallbackDnsHandler(create_txt, delete_txt)
HTTP-01 Handlers¶
CallbackHttpHandler¶
- class acmeow.CallbackHttpHandler[source]¶
Bases:
ChallengeHandlerHTTP-01 handler using user-provided callbacks.
This handler delegates challenge file management to user-provided callback functions, allowing integration with any HTTP serving method.
- Parameters:
Example
>>> def setup_challenge(domain, token, key_auth): ... redis.set(f"acme:{token}", key_auth) >>> def cleanup_challenge(domain, token): ... redis.delete(f"acme:{token}") >>> handler = CallbackHttpHandler(setup_challenge, cleanup_challenge)
FileHttpHandler¶
- class acmeow.FileHttpHandler[source]¶
Bases:
ChallengeHandlerHTTP-01 handler that writes challenge files to a webroot directory.
This handler writes challenge response files to the standard .well-known/acme-challenge/ directory structure. The web server must be configured to serve this directory.
- Parameters:
webroot (
Path) – Path to the web server’s document root. Files will be written to {webroot}/.well-known/acme-challenge/
Example
>>> handler = FileHttpHandler(Path("/var/www/html")) >>> # Challenge file will be at: >>> # /var/www/html/.well-known/acme-challenge/{token}
TLS-ALPN-01 Handlers¶
CallbackTlsAlpnHandler¶
- class acmeow.CallbackTlsAlpnHandler[source]¶
Bases:
ChallengeHandlerTLS-ALPN-01 handler using user-provided callbacks.
This handler delegates certificate deployment to user-provided callback functions, allowing integration with any TLS server.
- Parameters:
Example
>>> def deploy_cert(domain, cert_pem, key_pem): ... # Configure TLS server with certificate ... server.set_certificate(domain, cert_pem, key_pem) >>> def cleanup_cert(domain): ... server.remove_certificate(domain) >>> handler = CallbackTlsAlpnHandler(deploy_cert, cleanup_cert)
FileTlsAlpnHandler¶
- class acmeow.FileTlsAlpnHandler[source]¶
Bases:
ChallengeHandlerTLS-ALPN-01 handler that writes certificates to files.
This handler writes the validation certificate and key to files, which can then be loaded by a TLS server. Optionally calls a reload callback to signal the server to reload certificates.
- Parameters:
cert_dir (
Path) – Directory to write certificate files.cert_pattern (
str) – Pattern for certificate filename. {domain} is replaced.key_pattern (
str) – Pattern for key filename. {domain} is replaced.reload_callback (
Callable[[],None] |None) – Optional callback to reload the TLS server. Signature: () -> None
Example
>>> handler = FileTlsAlpnHandler( ... cert_dir=Path("/etc/tls/acme"), ... cert_pattern="{domain}.crt", ... key_pattern="{domain}.key", ... reload_callback=lambda: subprocess.run(["nginx", "-s", "reload"]), ... )
- __init__(cert_dir, cert_pattern='{domain}.alpn.crt', key_pattern='{domain}.alpn.key', reload_callback=None)[source]¶
DNS-PERSIST-01 Handlers¶
Handlers for the persistent DNS validation method defined by
draft-ietf-acme-dns-persist. These implement DnsPersistHandler rather than
ChallengeHandler, because the challenge uses no token or key authorization
and its record is meant to outlive the order.
DnsPersistHandler (Base)¶
- class acmeow.DnsPersistHandler[source]¶
Bases:
ABCAbstract base class for DNS-PERSIST-01 challenge handlers.
This is deliberately a separate interface from
ChallengeHandler: DNS-PERSIST-01 has no token and no key authorization, and its record is meant to outlive the validation, so thesetup/cleanupcontract of the other challenge types does not apply.Example
>>> class MyPersistHandler(DnsPersistHandler): ... def setup(self, domain, record_name, record_value): ... dns_api.upsert(record_name, "TXT", record_value)
- abstractmethod setup(domain, record_name, record_value)[source]¶
Publish the persistent validation record.
Implementations should treat this as an upsert: the record name is stable per domain, so a record left over from an earlier issuance may already exist and should be replaced rather than duplicated.
- cleanup(domain, record_name)[source]¶
Remove the validation record.
The default implementation does nothing. The record authorizes future issuance, so removing it after a successful validation would discard the benefit of using this challenge type at all. Override only when the record is genuinely meant to be single-use.
CallbackDnsPersistHandler¶
- class acmeow.CallbackDnsPersistHandler[source]¶
Bases:
DnsPersistHandlerDNS-PERSIST-01 handler backed by user-provided callbacks.
- Parameters:
create_record (
Callable[[str,str,str],None]) – Callback publishing the TXT record. Signature:(domain, record_name, record_value) -> None.delete_record (
Callable[[str,str],None] |None) – Optional callback removing the TXT record. Signature:(domain, record_name) -> None. Only invoked whenpersistis False.propagation_delay (
int) – Seconds to wait after publishing. Default 60.persist (
bool) – Whether to keep the record after validation. Default True, matching the intent of the challenge type.
Example
>>> def upsert(domain, name, value): ... dns_api.upsert(name, "TXT", value) >>> handler = CallbackDnsPersistHandler(upsert)
ManualDnsPersistHandler¶
- class acmeow.ManualDnsPersistHandler[source]¶
Bases:
DnsPersistHandlerDNS-PERSIST-01 handler that prints the record and waits for confirmation.
Intended for domains whose DNS is managed by hand or by another team.
- Parameters:
Example
>>> client.complete_dns_persist_challenges(ManualDnsPersistHandler())
DnsProviderPersistHandler¶
- class acmeow.DnsProviderPersistHandler[source]¶
Bases:
DnsPersistHandlerDNS-PERSIST-01 handler that publishes records through a DNS provider.
- Parameters:
provider (
DnsProvider) – The DNS provider used to manage records.ttl (
int) – Time to live for the published record. Default 3600. This is higher than the DNS-01 default because the record is long-lived.persist (
bool) – Whether to keep the record after validation. Default True.replace_existing (
bool) – Whether to delete a pre-existing record at the validation name before publishing. Default True, so that a record left by an earlier issuance does not accumulate duplicates. Requires the provider to implementlist_records.
Example
>>> from acmeow.dns import get_dns_provider >>> provider = get_dns_provider("cloudflare", api_token="...") >>> handler = DnsProviderPersistHandler(provider) >>> client.complete_dns_persist_challenges(handler)
- property provider: DnsProvider¶
The underlying DNS provider.
PersistRecordValue¶
- class acmeow.PersistRecordValue[source]¶
Bases:
objectA parsed DNS-PERSIST-01 TXT record value.
- issuer_domain_name¶
The Issuer Domain Name authorized by this record.
- accounturi¶
URI of the ACME account authorized to request issuance.
- policy¶
Optional
policyparameter (e.g."wildcard").
- persist_until¶
Optional expiry as a UNIX timestamp, after which the record should no longer be honoured.
- parameters¶
Any additional parameters, preserved in encounter order.
Helper Functions¶
generate_tls_alpn_certificate¶
- acmeow.generate_tls_alpn_certificate(domain, key_authorization, key=None, validity_days=1)[source]¶
Generate a TLS-ALPN-01 validation certificate.
Creates a self-signed certificate with the acmeIdentifier extension containing the SHA-256 hash of the key authorization, as required by RFC 8737.
- Parameters:
domain (
str) – The domain name to validate.key_authorization (
str) – The key authorization string (token.thumbprint).key (
Union[DHPrivateKey,Ed25519PrivateKey,Ed448PrivateKey,MLDSA44PrivateKey,MLDSA65PrivateKey,MLDSA87PrivateKey,MLKEM768PrivateKey,MLKEM1024PrivateKey,RSAPrivateKey,DSAPrivateKey,EllipticCurvePrivateKey,X25519PrivateKey,X448PrivateKey,None]) – Private key to use. If None, generates an EC P-256 key.validity_days (
int) – Certificate validity period in days. Default 1.
- Return type:
- Returns:
Tuple of (certificate_pem, private_key_pem) as bytes.
Example
>>> cert_pem, key_pem = generate_tls_alpn_certificate( ... "example.com", ... "token.thumbprint", ... )
validation_domain_name¶
- acmeow.validation_domain_name(domain)[source]¶
Build the Validation Domain Name for a domain.
A wildcard identifier is validated through the record for its base domain; the wildcard itself is authorized by
policy=wildcardin the record value rather than by a separate record name.- Parameters:
domain (
str) – The domain being validated, optionally a*.wildcard.- Return type:
- Returns:
The Validation Domain Name (e.g.
_validation-persist.example.com).
Example
>>> validation_domain_name("example.com") '_validation-persist.example.com' >>> validation_domain_name("*.example.com") '_validation-persist.example.com'
build_record_value¶
- acmeow.build_record_value(issuer_domain_name, accounturi, policy=None, persist_until=None, parameters=None)[source]¶
Build a DNS-PERSIST-01 TXT record value.
The result follows the
issue-valuesyntax of RFC 8659 section 4.2.- Parameters:
issuer_domain_name (
str) – An Issuer Domain Name from the challenge’sissuer-domain-nameslist.accounturi (
str) – The challenge’saccounturivalue.policy (
str|None) – Optional policy, e.g.WILDCARD_POLICY.persist_until (
int|datetime|None) – Optional expiry, as a UNIX timestamp or a datetime. Naive datetimes are interpreted as UTC.parameters (
dict[str,str] |None) – Optional additional parameters.
- Return type:
- Returns:
The record value, e.g.
"ca.example; accounturi=https://ca.example/acct/1".- Raises:
DnsPersistError – If any component is not representable in issue-value syntax. Both
issuer_domain_nameandaccounturioriginate from the CA, so they are validated rather than trusted: a value containing";"would otherwise inject extra parameters into the published record.
parse_record_value¶
- acmeow.parse_record_value(value)[source]¶
Parse a DNS-PERSIST-01 TXT record value.
- Parameters:
value (
str) – The raw TXT record value.- Return type:
- Returns:
The parsed record.
- Raises:
DnsPersistError – If the value is not a well-formed issue-value or is missing the mandatory
accounturiparameter.
Example
>>> parsed = parse_record_value("ca.example; accounturi=https://ca.example/a/1") >>> parsed.issuer_domain_name 'ca.example'
select_issuer_domain_name¶
- acmeow.select_issuer_domain_name(issuer_domain_names, preferred=None)[source]¶
Choose which Issuer Domain Name to publish.
- Parameters:
- Return type:
- Returns:
preferredwhen the CA offers it, otherwise the first offered name.- Raises:
DnsPersistError – If the CA offered no names, or if
preferredwas given but is not among them. Publishing a name the CA did not offer would fail validation, so this is reported rather than silently falling back.