# How the ACME protocol works: RFC 8555, challenges and ARI

> ACME (RFC 8555) step by step: accounts, JWS requests, orders, http-01/dns-01/tls-alpn-01 challenges, ARI renewal windows and certificate profiles.

Source: https://ssl.sh/learn/acme-protocol/
Last updated: 2026-11-10

ACME (Automatic Certificate Management Environment) is the protocol that lets software request, validate, renew and revoke TLS certificates without a person in the loop. Standardized as [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555) in March 2019, it is a JSON API over HTTPS: the account key signs every request, challenges such as http-01 and dns-01 prove domain control, and a CSR completes the order.

## What ACME automates

ACME replaces the manual steps of buying a certificate with an API that a cron job can drive. Before ACME, a request meant generating a CSR, pasting it into a web form, clicking a link in an email or uploading a file by hand, and installing the result manually. That took minutes per certificate and failed silently when someone forgot.

Let's Encrypt built ACME for its 2015 launch, and the IETF finished the standard in 2019. Every major free CA and most commercial CAs now support it. The CA/Browser Forum [Baseline Requirements](https://ssl.sh/glossary/baseline-requirements/) recognise the ACME validation methods directly: section 3.2.2.4.19 covers the ACME http-01 method, 3.2.2.4.7 covers DNS changes such as dns-01, and 3.2.2.4.20 covers TLS-ALPN. With certificate lifetimes falling to 47 days by 2029, ACME has gone from convenient to effectively mandatory; see [automating renewal](https://ssl.sh/learn/automate-certificate-renewal/).

## The directory and its resources

Every ACME server publishes a single **directory** URL, and clients discover everything else from it. This is the Let's Encrypt production directory as of November 2026:

```json
{
  "keyChange": "https://acme-v02.api.letsencrypt.org/acme/key-change",
  "meta": {
    "caaIdentities": ["letsencrypt.org"],
    "profiles": {
      "classic": "https://letsencrypt.org/docs/profiles#classic",
      "shortlived": "https://letsencrypt.org/docs/profiles#shortlived",
      "tlsserver": "https://letsencrypt.org/docs/profiles#tlsserver"
    },
    "termsOfService": "https://letsencrypt.org/documents/LE-SA-v1.8-July-06-2026.pdf",
    "website": "https://letsencrypt.org"
  },
  "newAccount": "https://acme-v02.api.letsencrypt.org/acme/new-acct",
  "newNonce": "https://acme-v02.api.letsencrypt.org/acme/new-nonce",
  "newOrder": "https://acme-v02.api.letsencrypt.org/acme/new-order",
  "renewalInfo": "https://acme-v02.api.letsencrypt.org/acme/renewal-info",
  "revokeCert": "https://acme-v02.api.letsencrypt.org/acme/revoke-cert"
}
```

| Resource | Purpose |
|---|---|
| `newNonce` | Returns a fresh anti-replay nonce in the `Replay-Nonce` header |
| `newAccount` | Creates or finds an account bound to a public key |
| `newOrder` | Starts a request for a certificate covering one or more identifiers |
| Authorization | One per identifier; holds the challenges for that name |
| Challenge | A specific way to prove control (http-01, dns-01, tls-alpn-01) |
| `finalize` | Per-order URL where the client submits the CSR |
| Certificate | Per-order URL to download the issued chain |
| `revokeCert` | Revokes a certificate |
| `keyChange` | Rolls the account over to a new key |
| `renewalInfo` | ARI endpoint added by RFC 9773 |

The `caaIdentities` value is the string you put in a [CAA record](https://ssl.sh/learn/caa-records/) to authorize this CA.

## Every request is a signed JWS

ACME authenticates requests by signature, not by password or API token. Each POST body is a JSON Web Signature whose protected header names the algorithm, a nonce, the exact URL being called, and the account:

```text
{
  "protected": base64url({
    "alg": "ES256",
    "kid": "https://acme-v02.api.letsencrypt.org/acme/acct/1234567890",
    "nonce": "918MM8jNCGhHSIKmADOgmJbHgyvZRtj4QF5fM6dr95rPfCqLw7A",
    "url": "https://acme-v02.api.letsencrypt.org/acme/new-order"
  }),
  "payload": base64url({"identifiers": [{"type": "dns", "value": "example.com"}]}),
  "signature": "..."
}
```

Three rules follow from this design. First, the `newAccount` request carries the full public key in a `jwk` field, and every later request refers to the account by its URL in `kid`. Second, each nonce is single-use; if the server rejects one with a `badNonce` error, the client takes the new nonce from the response and retries. Third, reads are also signed: ACME uses **POST-as-GET**, a POST with an empty payload, to fetch orders and certificates, so the server always knows which account is asking.

## Accounts and External Account Binding

An ACME account is a key pair registered with the CA. The `newAccount` request can include contact addresses and must signal agreement to the terms of service. Let's Encrypt no longer sends expiry emails since June 4, 2025, so the contact address matters less than it used to.

Commercial CAs often require **External Account Binding (EAB)**, defined in RFC 8555 section 7.3.4. The CA gives you a key identifier and an HMAC key from its web console, and the client uses them to prove that the new ACME account belongs to a paying customer. The account key is separate from any certificate key and can be rotated with `keyChange` without reissuing certificates.

## Orders, authorizations and the state machine

An order is a request for one certificate, and it moves through a fixed set of states. The client posts the identifiers it wants to `newOrder`, receives one authorization per identifier, completes a challenge for each, then finalizes with a CSR.

```text
              newOrder
                 |
                 v
   +---------> pending --------------------+
   |             |  all authorizations     |  any authorization
   |             |  become valid           |  fails or expires
   |             v                         v
   |           ready  --(finalize + CSR)--> invalid
   |             |                         ^
   |             v                         |
   |         processing ---(CA error)------+
   |             |
   |             v
   |           valid  --> download certificate chain
   |
 Authorization: pending -> valid | invalid ; valid -> expired | deactivated | revoked
 Challenge:     pending -> processing -> valid | invalid
```

A valid authorization can be reused for later orders from the same account for a limited time. Under Ballot SC-081v3 the Baseline Requirements cap that reuse period at 200 days, falling to 100 days from March 15, 2027 and 10 days from March 15, 2029. Let's Encrypt is stricter: its `tlsserver` and `shortlived` profiles reuse authorizations for only 7 hours.

## The three challenge types

All three challenges are built on the same **key authorization** string, which ties the CA's random token to your account key:

```text
keyAuthorization = token + "." + base64url( SHA-256 JWK thumbprint of the account key )
```

| | http-01 | dns-01 | tls-alpn-01 |
|---|---|---|---|
| Where the proof lives | `http://<domain>/.well-known/acme-challenge/<token>` | TXT record at `_acme-challenge.<domain>` | Self-signed certificate served on port 443 |
| Content | The key authorization itself | base64url(SHA-256(keyAuthorization)) | Critical `acmeIdentifier` extension (OID 1.3.6.1.5.5.7.1.31) containing SHA-256(keyAuthorization) |
| Port | 80 only | DNS | 443, ALPN `acme-tls/1` |
| Wildcards | No | Yes | No |
| IP addresses | Yes | No | Yes |
| Typical user | Single web server | Wildcards, internal hosts, many servers | TLS proxies and hosting platforms |

**http-01** is the default for most clients. Let's Encrypt follows up to 10 redirects, but only to ports 80 or 443, and does not validate the certificate when redirected to HTTPS. Every server behind a load balancer must serve the same file.

**dns-01** proves control of the zone. It is the only way to obtain a [wildcard certificate](https://ssl.sh/glossary/wildcard-certificate/), and it works for hosts that are not exposed to the internet. Because Let's Encrypt resolves TXT records normally, you can delegate `_acme-challenge` with a CNAME to a zone that is easier to automate. The cost is that DNS API credentials end up in your issuance pipeline.

**tls-alpn-01**, specified in [RFC 8737](https://www.rfc-editor.org/rfc/rfc8737), replaced the insecure tls-sni-01 method that Let's Encrypt removed in March 2019. The server must recognise the special ALPN protocol and present a validation certificate only for those connections, which is why it suits reverse proxies more than ordinary web servers.

Let's Encrypt validates from several network locations at once. That practice became an industry rule through Ballot SC-067, which requires **Multi-Perspective Issuance Corroboration** ([MPIC](https://ssl.sh/glossary/mpic/)) for domain validation and CAA checks, making localized BGP hijacks much harder to exploit.

## Finalize, download and revoke

Once every authorization is valid, the order becomes `ready` and the client posts a CSR to the `finalize` URL. The CSR's names must match the order exactly. The server moves the order to `processing`, then `valid`, and the client downloads the chain from the certificate URL as `application/pem-certificate-chain`. If a CA offers alternate chains, it lists them in `Link: rel="alternate"` headers.

Revocation goes to `revokeCert` and can be signed either by the account that ordered the certificate or by the certificate's own private key. The second option lets anyone who holds a leaked key revoke the certificate even without the account.

## ARI: the CA tells you when to renew

ACME Renewal Information, published as [RFC 9773](https://www.rfc-editor.org/rfc/rfc9773), adds a `renewalInfo` endpoint that returns a suggested renewal window for a specific certificate. The certificate is identified by the base64url key identifier from its Authority Key Identifier, a dot, and its base64url serial number:

```text
GET /acme/renewal-info/aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE

HTTP/1.1 200 OK
Retry-After: 21600
{
  "suggestedWindow": {
    "start": "2025-01-02T04:00:00Z",
    "end":   "2025-01-03T04:00:00Z"
  },
  "explanationURL": "https://acme.example.com/docs/ari"
}
```

The client picks a random time inside the window, renews then, and re-checks after the `Retry-After` interval. When it orders the replacement it includes a `replaces` field naming the old certificate. A CA can move the window earlier when it needs to revoke certificates in bulk, so clients that honour ARI renew before the revocation instead of after an outage. Let's Encrypt recommends checking ARI at least twice a day.

## Profiles: choosing what kind of certificate you get

Profiles let a client choose between certificate variants that a CA offers. The CA lists them under `meta.profiles` in the directory, and the client adds `"profile": "tlsserver"` to its `newOrder` request. The mechanism is the IETF ACME working group draft *ACME Profiles Extension* (draft-ietf-acme-profiles), which Let's Encrypt implements.

| Let's Encrypt profile | Validity | Authorization reuse | Notes |
|---|---|---|---|
| `classic` (default) | 90 days; 64 days from Feb 10, 2027; 45 days from Feb 16, 2028 | 30 days | Includes Common Name; Key Encipherment for RSA keys |
| `tlsserver` | 45 days since May 13, 2026 | 7 hours | No Common Name, no Subject Key ID, max 25 names |
| `shortlived` | 160 hours | 7 hours | Same as tlsserver; also allows IP addresses |

Certificates from Let's Encrypt's Generation Y hierarchy carry only the server-authentication EKU, so ACME certificates from Let's Encrypt are no longer suitable for TLS client authentication; see [mTLS](https://ssl.sh/learn/mtls/).

## ACME clients

In practice a client such as Certbot, acme.sh or lego speaks ACME for you, or the web server does it itself. The table lists widely used open-source clients and servers with built-in ACME. ssl.sh is not affiliated with any of them.

| Client | Language / form | Notes |
|---|---|---|
| Certbot | Python CLI | `--preferred-profile` since Certbot 4.0; many DNS plugins |
| acme.sh | POSIX shell | Large set of DNS API hooks; no root required |
| lego | Go CLI and library | Many DNS providers |
| Caddy | Web server | Automatic HTTPS by default |
| nginx `ngx_http_acme_module` | Web server module | Official module announced in August 2025 |
| cert-manager | Kubernetes controller | `Issuer` and `Certificate` resources |

## Errors you will meet

ACME errors are problem documents with types under `urn:ietf:params:acme:error:`. The most common ones map to concrete fixes:

| Error type | Usual cause |
|---|---|
| `badNonce` | Stale nonce; the client should retry automatically |
| `unauthorized` | Challenge content wrong, or wrong server answered |
| `connection` | CA could not reach port 80 or 443; see [certbot timeout](https://ssl.sh/errors/certbot-timeout-during-connect/) |
| `dns` | NXDOMAIN, SERVFAIL or missing TXT record |
| `caa` | A CAA record forbids this CA; see [CAA errors](https://ssl.sh/errors/caa-record-prevents-issuance/) |
| `rateLimited` | Too many orders or failures; see [rate limits](https://ssl.sh/errors/lets-encrypt-rate-limit/) |
| `rejectedIdentifier` | The CA will not issue for this name |

Test against the CA's staging directory first. Staging certificates are not trusted, but staging has higher rate limits and shows the same errors, so a broken setup fails there instead of burning production limits.
