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: ABC

Abstract 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:
  • domain (str) – The domain being validated.

  • token (str) – The challenge token from the ACME server.

  • key_authorization (str) – The key authorization string (token.thumbprint). For DNS-01, this should be hashed with SHA-256 and base64url encoded. For HTTP-01, this is served directly.

Raises:

Exception – If setup fails and the challenge cannot proceed.

Return type:

None

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:
  • domain (str) – The domain that was validated.

  • token (str) – The challenge token.

Return type:

None

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: ChallengeHandler

DNS-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) -> None

  • propagation_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)
__init__(create_record, delete_record, propagation_delay=60)[source]
Parameters:
Return type:

None

property propagation_delay: int

Seconds to wait after creating DNS record for propagation.

setup(domain, token, key_authorization)[source]

Create DNS TXT record for the challenge.

The TXT record value is the base64url-encoded SHA-256 hash of the key authorization, as specified by RFC 8555.

Parameters:
  • domain (str) – The domain being validated.

  • token (str) – The challenge token (not used for DNS record value).

  • key_authorization (str) – The key authorization string to hash.

Return type:

None

cleanup(domain, token)[source]

Remove DNS TXT record.

Parameters:
  • domain (str) – The domain that was validated.

  • token (str) – The challenge token (not used).

Return type:

None

HTTP-01 Handlers

CallbackHttpHandler

class acmeow.CallbackHttpHandler[source]

Bases: ChallengeHandler

HTTP-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:
  • setup_callback (Callable[[str, str, str], None]) – Callback to deploy the challenge response. Signature: (domain: str, token: str, key_authorization: str) -> None

  • cleanup_callback (Callable[[str, str], None]) – Callback to remove the challenge response. Signature: (domain: str, token: str) -> None

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)
__init__(setup_callback, cleanup_callback)[source]
Parameters:
Return type:

None

setup(domain, token, key_authorization)[source]

Deploy challenge response using callback.

Parameters:
  • domain (str) – The domain being validated.

  • token (str) – The challenge token.

  • key_authorization (str) – The key authorization string.

Return type:

None

cleanup(domain, token)[source]

Remove challenge response using callback.

Parameters:
  • domain (str) – The domain that was validated.

  • token (str) – The challenge token.

Return type:

None

FileHttpHandler

class acmeow.FileHttpHandler[source]

Bases: ChallengeHandler

HTTP-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}
__init__(webroot)[source]
Parameters:

webroot (Path)

Return type:

None

property challenge_dir: Path

Directory where challenge files are written.

setup(domain, token, key_authorization)[source]

Write challenge response file.

Creates the challenge file containing the key authorization at the path expected by the ACME server: {webroot}/.well-known/acme-challenge/{token}

Parameters:
  • domain (str) – The domain being validated (for logging).

  • token (str) – The challenge token (becomes the filename).

  • key_authorization (str) – The key authorization (file content).

Return type:

None

cleanup(domain, token)[source]

Remove challenge response file.

Parameters:
  • domain (str) – The domain that was validated.

  • token (str) – The challenge token (the filename).

Return type:

None

TLS-ALPN-01 Handlers

CallbackTlsAlpnHandler

class acmeow.CallbackTlsAlpnHandler[source]

Bases: ChallengeHandler

TLS-ALPN-01 handler using user-provided callbacks.

This handler delegates certificate deployment to user-provided callback functions, allowing integration with any TLS server.

Parameters:
  • deploy_callback (Callable[[str, bytes, bytes], None]) – Callback to deploy the certificate. Signature: (domain: str, cert_pem: bytes, key_pem: bytes) -> None

  • cleanup_callback (Callable[[str], None]) – Callback to remove the certificate. Signature: (domain: str) -> None

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)
__init__(deploy_callback, cleanup_callback)[source]
Parameters:
Return type:

None

setup(domain, token, key_authorization)[source]

Generate and deploy TLS-ALPN-01 certificate.

Parameters:
  • domain (str) – The domain being validated.

  • token (str) – The challenge token (not used for TLS-ALPN-01).

  • key_authorization (str) – The key authorization string.

Return type:

None

cleanup(domain, token)[source]

Remove TLS-ALPN-01 certificate.

Parameters:
  • domain (str) – The domain that was validated.

  • token (str) – The challenge token (not used).

Return type:

None

FileTlsAlpnHandler

class acmeow.FileTlsAlpnHandler[source]

Bases: ChallengeHandler

TLS-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]
Parameters:
  • cert_dir (Path)

  • cert_pattern (str)

  • key_pattern (str)

  • reload_callback (Callable[[], None] | None)

Return type:

None

setup(domain, token, key_authorization)[source]

Generate and write TLS-ALPN-01 certificate files.

Parameters:
  • domain (str) – The domain being validated.

  • token (str) – The challenge token (not used for TLS-ALPN-01).

  • key_authorization (str) – The key authorization string.

Return type:

None

cleanup(domain, token)[source]

Remove TLS-ALPN-01 certificate files.

Parameters:
  • domain (str) – The domain that was validated.

  • token (str) – The challenge token (not used).

Return type:

None

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: ABC

Abstract 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 the setup/cleanup contract 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)
propagation_delay: int = 60

Seconds to wait after publishing the record before notifying the CA.

persist: bool = True

Whether the record is kept after validation. See cleanup().

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.

Parameters:
  • domain (str) – The domain being validated (may be a *. wildcard).

  • record_name (str) – The Validation Domain Name to publish at.

  • record_value (str) – The issue-value formatted TXT record value.

Raises:

Exception – If the record cannot be published.

Return type:

None

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.

Parameters:
  • domain (str) – The domain that was validated.

  • record_name (str) – The Validation Domain Name that was published.

Return type:

None

CallbackDnsPersistHandler

class acmeow.CallbackDnsPersistHandler[source]

Bases: DnsPersistHandler

DNS-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 when persist is 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)
__init__(create_record, delete_record=None, propagation_delay=60, persist=True)[source]
Parameters:
Return type:

None

setup(domain, record_name, record_value)[source]

Publish the persistent validation record via the callback.

Return type:

None

Parameters:
  • domain (str)

  • record_name (str)

  • record_value (str)

cleanup(domain, record_name)[source]

Remove the record, if persist is False and a callback was given.

Return type:

None

Parameters:
  • domain (str)

  • record_name (str)

ManualDnsPersistHandler

class acmeow.ManualDnsPersistHandler[source]

Bases: DnsPersistHandler

DNS-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:
  • propagation_delay (int) – Seconds to wait after confirmation. Default 0, since the operator confirms the record is already live.

  • prompt (bool) – Whether to block on input() until the operator confirms. Default True. Set False for unattended runs that only need the record printed to the log.

Example

>>> client.complete_dns_persist_challenges(ManualDnsPersistHandler())
__init__(propagation_delay=0, prompt=True)[source]
Parameters:
  • propagation_delay (int)

  • prompt (bool)

Return type:

None

setup(domain, record_name, record_value)[source]

Print the record and optionally wait for the operator.

Return type:

None

Parameters:
  • domain (str)

  • record_name (str)

  • record_value (str)

DnsProviderPersistHandler

class acmeow.DnsProviderPersistHandler[source]

Bases: DnsPersistHandler

DNS-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 implement list_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)
__init__(provider, ttl=3600, persist=True, replace_existing=True)[source]
Parameters:
  • provider (DnsProvider)

  • ttl (int)

  • persist (bool)

  • replace_existing (bool)

Return type:

None

property propagation_delay: int

Seconds to wait after publishing for DNS propagation.

property provider: DnsProvider

The underlying DNS provider.

setup(domain, record_name, record_value)[source]

Publish the persistent validation record via the provider.

Parameters:
  • domain (str) – The domain being validated.

  • record_name (str) – The Validation Domain Name.

  • record_value (str) – The issue-value formatted TXT record value.

Return type:

None

cleanup(domain, record_name)[source]

Remove the record, if persist is False.

Parameters:
  • domain (str) – The domain that was validated.

  • record_name (str) – The Validation Domain Name.

Return type:

None

PersistRecordValue

class acmeow.PersistRecordValue[source]

Bases: object

A 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 policy parameter (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.

issuer_domain_name: str
accounturi: str
policy: str | None
persist_until: int | None
parameters: tuple[tuple[str, str], ...]
property allows_wildcard: bool

Whether this record authorizes wildcard issuance.

property is_expired: bool

Whether persist_until is in the past.

Records without a persist_until parameter never expire.

__str__()[source]

Render the record value in RFC 8659 issue-value syntax.

Return type:

str

__init__(issuer_domain_name, accounturi, policy=None, persist_until=None, parameters=())
Parameters:
Return type:

None

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:
Return type:

tuple[bytes, bytes]

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=wildcard in the record value rather than by a separate record name.

Parameters:

domain (str) – The domain being validated, optionally a *. wildcard.

Return type:

str

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-value syntax of RFC 8659 section 4.2.

Parameters:
  • issuer_domain_name (str) – An Issuer Domain Name from the challenge’s issuer-domain-names list.

  • accounturi (str) – The challenge’s accounturi value.

  • 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:

str

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_name and accounturi originate 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:

PersistRecordValue

Returns:

The parsed record.

Raises:

DnsPersistError – If the value is not a well-formed issue-value or is missing the mandatory accounturi parameter.

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:
  • issuer_domain_names (Sequence[str]) – The list offered by the challenge.

  • preferred (str | None) – An Issuer Domain Name to prefer, if the CA offers it.

Return type:

str

Returns:

preferred when the CA offers it, otherwise the first offered name.

Raises:

DnsPersistError – If the CA offered no names, or if preferred was 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.