Skip to content

a-fni/ACME-Client

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

71 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

ACME Client

A from-scratch implementation of an ACME (Automatic Certificate Management Environment) client, written in Python, capable of autonomously obtaining, serving, and revoking X.509 certificates from an ACME-compliant CA. Built as the final project for the Network Security course at ETH Zurich.

The client implements the full RFC 8555 ACME protocol flow, including both http-01 and dns-01 challenge types, a custom authoritative DNS server, and a post-issuance HTTPS server that serves the obtained certificate.


Table of Contents


Overview

The ACME protocol (RFC 8555) defines a standard interface through which a certificate authority can autonomously validate domain ownership and issue TLS certificates. This project implements a full ACME client from scratch, with no reliance on existing ACME libraries such as acme or certbot.

Key capabilities:

  • Full ACME protocol state machine: directory bootstrap, account creation, order placement, challenge resolution, CSR submission, certificate download, and revocation.
  • Support for both http-01 (well-known HTTP token) and dns-01 (DNS TXT record) challenge types.
  • Custom UDP DNS server that serves both A and TXT records, enabling self-contained dns-01 challenge resolution without external DNS infrastructure.
  • JOSE (JSON Object Signing and Encryption) implementation including JWK construction, RS256 signing, and key thumbprint computation per RFC 7638.
  • Post-issuance HTTPS server that wraps the newly obtained certificate and serves it immediately.
  • Graceful shutdown via a dedicated HTTP control endpoint.
  • Tested against Pebble, the Let's Encrypt lightweight ACME testing CA.

Architecture

The application is composed of five concurrent network services and a central ACME client, all orchestrated from a single entry point via Python threads:

                        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                        β”‚               main.py                    β”‚
                        β”‚  (FSM orchestrator + thread coordinator) β”‚
                        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                     β”‚ spawns
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β”‚                          β”‚                               β”‚
   β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚  DNS Server β”‚          β”‚  HTTP Challenge β”‚            β”‚ HTTP Shutdown   β”‚
   β”‚  :10053/UDP β”‚          β”‚  Server :5002   β”‚            β”‚ Server  :5003   β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β–²                          β–²                               β”‚
          β”‚ TXT record injection     β”‚ token/key-auth injection      β”‚ /shutdown
          β”‚                          β”‚                               β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚                          ACMEClient                                    β”‚
   β”‚   getInitialNonce β†’ createNewAccount β†’ requestNewCertificate β†’         β”‚
   β”‚   solveChallenges β†’ finalizeRequest β†’ downloadCertificate              β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                     β”‚ after certificate obtained
                                     β–Ό
                          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                          β”‚  HTTPS Certificate  β”‚
                          β”‚  Server  :5001      β”‚
                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The ACME client drives a finite state machine. On a nonce rejection or transient validation failure the FSM restarts from the nonce acquisition step, ensuring resilience without manual intervention.


Project Structure

ACME-Client/
β”œβ”€β”€ main.py                          # Entry point and FSM orchestrator
β”œβ”€β”€ networking/
β”‚   β”œβ”€β”€ ACMEClient.py                # Core ACME protocol implementation
β”‚   β”œβ”€β”€ dnsServer.py                 # Custom authoritative DNS server
β”‚   β”œβ”€β”€ httpWebServer.py             # HTTP server for http-01 challenge responses
β”‚   β”œβ”€β”€ httpsWebServer.py            # HTTPS server serving the obtained certificate
β”‚   β”œβ”€β”€ httpShutdownServer.py        # HTTP control server for graceful shutdown
β”‚   β”œβ”€β”€ constructBasicHTTPResponse.py
β”‚   └── ValidationError.py
β”œβ”€β”€ challenge_solver/
β”‚   β”œβ”€β”€ challengeSolver.py           # Challenge type dispatcher
β”‚   β”œβ”€β”€ dnsResponder.py              # dns-01 challenge responder (TXT record injection)
β”‚   └── httpResponder.py             # http-01 challenge responder (token injection)
β”œβ”€β”€ utilities/
β”‚   β”œβ”€β”€ signing.py                   # RSA key generation, JWK, JOSE signing, CSR construction
β”‚   β”œβ”€β”€ encoding.py                  # URL-safe base64 encoding utilities
β”‚   └── parseArguments.py            # CLI argument parser
β”œβ”€β”€ scripts/
β”‚   β”œβ”€β”€ launcher.py                  # Remote launcher (reads config from HTTP endpoint)
β”‚   β”œβ”€β”€ docker-compile.sh            # Docker-based dependency installation
β”‚   └── docker-run.sh                # Docker-based execution wrapper
└── project/
    β”œβ”€β”€ pebble.minica.pem            # Pebble CA root certificate for TLS verification
    β”œβ”€β”€ compile                      # Dependency installation script
    └── run                          # Shell entrypoint wrapper

Protocol Flow

The ACME protocol interaction follows RFC 8555 and is implemented as a sequential state machine inside main.py:

  1. Directory bootstrap β€” On construction, ACMEClient fetches the ACME directory URL to resolve the newNonce, newAccount, newOrder, and revokeCert endpoints.

  2. Nonce acquisition (getInitialNonce) β€” A HEAD request to newNonce retrieves the first Replay-Nonce. Every subsequent nonce is extracted from the response header of the preceding request.

  3. Account creation (createNewAccount) β€” A JOSE-signed POST to newAccount registers a new account. A fresh RSA-2048 key pair is generated per session. The account URL returned in the Location header is stored and used as the kid in all subsequent protected headers.

  4. Order placement (requestNewCertificate) β€” A POST to newOrder with a list of DNS identifier objects initiates a certificate order. The response provides the finalize URL and a list of authorizations URLs, one per identifier.

  5. Challenge resolution (solveChallenges) β€” For each authorization URL, the client performs a GET-as-POST to retrieve the challenge object, then calls the appropriate responder (dnsResponder or httpResponder) to provision the proof. It then notifies the ACME server that the challenge is ready and polls the authorization URL until its status transitions to valid.

  6. Finalization (finalizeRequest) β€” A CSR is constructed with all requested identifiers as Subject Alternative Names and submitted to the finalize URL. The private key associated with the CSR is retained in memory.

  7. Certificate download (downloadCertificate) β€” The client performs a GET-as-POST against the account URL to retrieve the orders list, queries the latest order for the certificate URL, and downloads the PEM certificate chain.

  8. Certificate serving β€” The certificate and private key are written to cert.pem and key.pem respectively. The HTTPS server is started with these files and serves on port 5001.

  9. Revocation (optional, --revoke) β€” If requested, the PEM certificate is read back, converted to DER, base64url-encoded, and submitted to revokeCert in a JOSE-signed payload.


Challenge Types

http-01

The http-01 challenge proves domain control by serving a key authorization token at a well-known HTTP path:

GET http://<domain>:5002/.well-known/acme-challenge/<token>

The httpResponder injects the token and the computed key_authorization (<token>.<thumbprint>) as class-level attributes on HttpChallengeServer. The server, already running in a background thread, picks these up and serves the correct response to the ACME server's validation request.

dns-01

The dns-01 challenge proves domain control by serving a DNS TXT record:

_acme-challenge.<identifier>  TXT  <digest>

where <digest> is the URL-safe base64 encoding of SHA-256(<token>.<thumbprint>).

The dnsResponder computes this value and injects a new TXT entry directly into the DNSResolver.delegate.entries dictionary of the running DNS server instance. The DNS server, already listening on UDP port 10053, will serve the newly injected record immediately upon the next query from the ACME server.


Components

ACME Client

networking/ACMEClient.py

The central class driving all ACME interactions. Manages state across the protocol lifecycle: nonce, account location, authorization URLs, finalize URL, private key bytes, and certificate bytes. Constructs JOSE-protected requests by encoding and signing headers and payloads with RS256 using the Signing utility.

Notable design detail: the protected header switches between a jwk field (before account creation) and a kid field (after), as required by RFC 8555 Β§6.2.

DNS Server

networking/dnsServer.py

A lightweight authoritative DNS server built on top of dnslib. Binds to 0.0.0.0:10053 over UDP. Handles A and TXT query types. Record names support wildcard patterns, which are converted to regex at query resolution time. The DNSResolver instance is exposed via a class-level delegate reference, allowing challenge responders to inject records at runtime without restarting the server.

HTTP Challenge Server

networking/httpWebServer.py

A minimal http.server.HTTPServer subclass running on port 5002. Handles two resources: the root path (aliveness) and the ACME challenge path /.well-known/acme-challenge/<token>. The token and key authorization string are stored as class-level attributes, set by httpResponder prior to challenge notification.

HTTPS Certificate Server

networking/httpsWebServer.py

An HTTPServer instance whose socket is wrapped with ssl.wrap_socket using the freshly issued certificate and private key. Runs on port 5001 in a background thread after the certificate has been obtained. Serves a basic root response to confirm the certificate is live.

Shutdown Server

networking/httpShutdownServer.py

An HTTP server on port 5003 that accepts a GET /shutdown request and triggers a graceful teardown of the HTTP challenge server and HTTPS server before calling exit(0). Holds references to both servers as class-level attributes.

Cryptographic Layer

utilities/signing.py

Handles all cryptographic operations:

  • RSA-2048 key pair generation using cryptography.hazmat.primitives.asymmetric.rsa.
  • JWK object construction from the public exponent and modulus, both base64url-encoded per RFC 7517.
  • PKCS1v15 / SHA-256 signature computation over the <encoded_header>.<encoded_payload> string, as required by JOSE.
  • JWK thumbprint computation per RFC 7638: canonical JSON serialization of the JWK followed by SHA-256 hashing.
  • CSR construction using x509.CertificateSigningRequestBuilder with Subject Alternative Names for all requested domains.
  • DER certificate loading for revocation.

utilities/encoding.py

Provides the URL-safe, padding-stripped base64 encoding required throughout the JOSE and ACME specifications, along with URL manipulation helpers (removePortFromUrl, insertPortInUrl) needed to reconcile the port discrepancy between JOSE url header values and actual request URLs when running against Pebble.


Installation

Requirements: Python 3.10+

Install dependencies:

pip3 install cryptography dnslib requests

Or use the provided script:

bash project/compile

Usage

Command-Line Interface

python3 main.py <challenge_type> --dir <dir_url> --record <ip> --domain <domain> [--domain <domain> ...] [--revoke]
Argument Required Description
challenge_type Yes Challenge type to use: http01 or dns01
--dir Yes URL of the ACME server directory endpoint
--record Yes IPv4 address the DNS server will return for all registered A record queries
--domain Yes Domain name(s) to include in the certificate (repeatable)
--revoke No If set, revoke the certificate after issuance

Examples

Obtain a certificate for a single domain using http-01, against a local Pebble instance:

python3 main.py http01 \
  --dir https://localhost:14000/dir \
  --record 127.0.0.1 \
  --domain example.com

Obtain a wildcard-compatible certificate for multiple domains using dns-01:

python3 main.py dns01 \
  --dir https://localhost:14000/dir \
  --record 127.0.0.1 \
  --domain example.com \
  --domain www.example.com

Obtain and immediately revoke a certificate:

python3 main.py http01 \
  --dir https://localhost:14000/dir \
  --record 127.0.0.1 \
  --domain example.com \
  --revoke

On successful completion:

  • key.pem β€” RSA private key for the certificate (PEM)
  • cert.pem β€” Issued certificate chain (PEM)
  • HTTPS server live on port 5001
  • Shutdown endpoint available at http://localhost:5003/shutdown

Testing with Pebble

Pebble is a minimal ACME server intended for integration testing. The project/pebble.minica.pem file contains Pebble's root CA certificate and is passed to all requests calls via the verify= parameter to enable TLS verification against Pebble's self-signed certificate.

Start Pebble with the appropriate configuration (DNS resolver pointed to 0.0.0.0:10053 for dns-01 testing), then run the client as shown in the examples above.


Dependencies

Package Purpose
cryptography RSA key generation, JOSE signing, CSR construction, x509
dnslib DNS server and record type primitives
requests HTTP/HTTPS communication with the ACME server

All are standard, well-maintained packages available on PyPI.


License

See LICENSE.

About

ACME client implementing automated certificate request compliant with RFC8555.

Topics

Resources

License

Stars

Watchers

Forks

Contributors