ACME protocol implementation in Python
The `acme` library is a low-level, comprehensive Python implementation of the Automated Certificate Management Environment (ACME) protocol, primarily maintained as part of the Certbot project. It enables programmatic certificate issuance, renewal, and management, supporting Let's Encrypt and other ACME-compatible certificate authorities. Currently at version 5.5.0, `acme` follows the release cadence of Certbot, typically seeing several updates per year, often driven by new ACME RFCs or internal Certbot architectural changes.
Warnings
- breaking Major architectural shift in version 5.0.0 (aligned with Certbot 5.0.0) removed `acme.crypto_util.SSLSocket`, `acme.crypto_util.probe_sni`, and TLS-ALPN related challenge classes (`acme.challenges.TLSALPN01Response`, `acme.challenges.TLSALPN01`, `acme.standalone.TLSServer`, `acme.standalone.TLSALPN01Server`). This also removed final `pyopenssl` x509 and PKey objects usage within `acme`'s core cryptography, favoring `cryptography` library objects.
- deprecated The function `acme.crypto_util.make_self_signed_cert` was deprecated in Certbot 5.1.0 and is slated for removal. This indicates a shift away from generating self-signed certificates directly within the `acme` library.
- breaking Version 2.0.0 (aligned with Certbot 2.0.0) removed support for pre-RFC 8555 ACME versions and related deprecated messaging constructs like `acme.messages.OLD_ERROR_PREFIX`. It also removed the `source_address` argument from `acme.client.ClientNetwork` and several deprecated attributes from `acme.messages.Directory` and `acme.messages.Authorization`.
- gotcha The `acme` library's cryptographic requirements are tightly coupled with `certbot`'s and have increased over time. As of Certbot 3.2.0, `acme` requires `cryptography>=43.0.0` and `pyOpenSSL>=25.0.0`.
Install
-
pip install acme
Imports
- ClientV2
from acme import client
- messages
from acme import messages
- challenges
from acme import challenges
- josepy
import josepy as jose
- crypto_util
from acme import crypto_util
Quickstart
import os
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
from acme import client
from acme import messages
import josepy as jose
# This is the staging ACME URL for Let's Encrypt
DIRECTORY_URL = os.environ.get('ACME_DIRECTORY_URL', 'https://acme-staging-v02.api.letsencrypt.org/directory')
def run_acme_client():
# 1. Create a new ACME account private key
acc_key_pem = rsa.generate_private_key(
public_exponent=65537, key_size=2048, backend=default_backend()
)
acc_key = jose.JWK.from_pem(acc_key_pem.private_bytes(
encoding=jose.serialization.Encoding.PEM,
format=jose.serialization.PrivateFormat.PKCS8,
encryption_algorithm=jose.serialization.NoEncryption()
))
# 2. Initialize the client network
net = client.ClientNetwork(acc_key, user_agent="my-acme-client/1.0")
# 3. Get the ACME directory object
directory = messages.Directory.from_json(net.get(DIRECTORY_URL).json())
# 4. Create the ACME client
acme_client = client.ClientV2(directory, net=net)
# 5. Register a new account (or load existing)
# email_contact is optional for staging/testing
new_reg = messages.NewRegistration(
terms_of_service_agreed=True,
contact=('mailto:test@example.com',) if 'test@example.com' in os.environ.get('ACME_EMAIL', '') else None
)
try:
regr = acme_client.new_account(new_reg)
print(f"Account created/loaded: {regr.uri}")
except client.errors.ConflictError as e:
# Account already exists, retrieve it
print(f"Account already exists, retrieving: {e.uri}")
regr = acme_client.query_registration(messages.RegistrationResource(uri=e.uri, body=new_reg))
print("ACME client initialized and account registered.")
return acme_client, regr
if __name__ == "__main__":
acme_client_instance, account_resource = run_acme_client()