This document describes test cases for the ACME client using Pebble as the test CA.
All tests assume Pebble is running locally. See README.md for setup instructions.
Note: Pebble uses a self-signed TLS certificate. All commands below use
--insecureto skip TLS verification. In production, never use--insecure.
# docker-compose.yml from README
docker compose up -dVerify Pebble is reachable:
curl -sk https://localhost:14000/dir | python -m json.toolExpected output (URL paths may vary):
{
"keyChange": "https://localhost:14000/rollover-account-key",
"meta": { "termsOfService": "data:text/plain,..." },
"newAccount": "https://localhost:14000/sign-me-up",
"newNonce": "https://localhost:14000/nonce-plz",
"newOrder": "https://localhost:14000/order-plz",
"renewalInfo": "https://localhost:14000/renewal-info",
"revokeCert": "https://localhost:14000/revoke-cert"
}Since every example in this document targets Pebble (self-signed TLS), the alias bakes in --insecure so individual command lines stay short. In production, run the binary directly without --insecure.
# Linux/macOS
alias acme='./target/release/acme-client-rs --insecure'
# PowerShell
function acme { & .\target\release\acme-client-rs.exe --insecure @args }All commands below use acme as the alias for brevity, and inherit --insecure from it.
Goal: Generate a new ES256 PKCS#8 PEM key pair.
acme generate-key --account-key test-account.keyExpected:
- Exit code: 0
- Output:
ES256 account key saved to test-account.key - File
test-account.keyis created containing a PEM-encoded EC private key
Verify:
# Check it's a valid EC key
openssl ec -in test-account.key -text -nooutShould show ASN1 OID: prime256v1 (P-256).
Goal: Generate account keys for all supported algorithms.
for alg in es256 es384 es512 rsa2048 rsa4096; do
acme generate-key --algorithm ${alg} --account-key test-${alg}.key
doneNote: Ed25519 is excluded from E2E testing because Pebble only supports RS256, ES256, ES384, ES512.
Expected:
- Exit code: 0 for each algorithm
- Each key file created with valid PEM header (
BEGIN)
Goal: Re-running generate-key overwrites the existing file.
acme generate-key --account-key test-account.key
acme generate-key --account-key test-account.keyExpected:
- Exit code: 0 both times
- The key file contents change between runs (new random key)
Goal: Register a new ACME account with the CA.
acme --insecure --account-key test-account.key account --contact test@example.comExpected:
- Exit code: 0
- Output includes:
Account status: valid Account URL: https://localhost:14000/my-account/<id>
Goal: Calling account again with the same key returns the existing account.
acme --insecure --account-key test-account.key account --contact test@example.com
acme --insecure --account-key test-account.key account --contact test@example.comExpected:
- Both calls succeed with exit code 0
- Both return the same Account URL
Goal: Account creation works without a contact email.
acme --insecure --account-key test-account.key accountExpected:
- Exit code: 0
- Output:
Account status: valid
Goal: Submit a new certificate order for one domain.
acme --account-key test-account.key --account-url <account-url-from-TC-03> order test.example.comExpected:
- Exit code: 0
- Output includes:
Order URL: https://localhost:14000/my-order/<id> Status: pending Finalize URL: https://localhost:14000/finalize-order/<id> authz: https://localhost:14000/authZ/<id>
Goal: Order a certificate with multiple Subject Alternative Names.
acme --account-key test-account.key --account-url <account-url> order test.example.com www.example.com api.example.comExpected:
- Exit code: 0
- Three authorization URLs listed (one per domain)
Goal: Retrieve authorization details and available challenges.
acme --account-key test-account.key --account-url <account-url> get-authz <authz-url-from-TC-06>Expected:
- Exit code: 0
- Output includes:
Identifier: test.example.com (dns) Status: pending http-01 [pending] url=https://localhost:14000/chalZ/<id> token: <base64url-token> dns-01 [pending] url=... token: ... tls-alpn-01 [pending] url=... token: ...
Goal: Tell the CA a challenge is ready.
acme --account-key test-account.key --account-url <account-url> respond-challenge <challenge-url-from-TC-08>Expected:
- Exit code: 0
- Output:
Challenge status: pendingorChallenge status: processing
Note: With
PEBBLE_VA_ALWAYS_VALID=1, the challenge should transition tovalidshortly.
Goal: Start the built-in HTTP-01 validation server.
# Terminal 1: start server
acme --account-key test-account.key serve-http-01 --token <token> --port 5002
# Terminal 2: simulate validation request
curl http://localhost:5002/.well-known/acme-challenge/<token>Expected:
- Terminal 1:
HTTP-01 server listening on 0.0.0.0:5002, thenHTTP-01: served challenge response, then exits - Terminal 2: receives the key authorization string
<token>.<thumbprint>
Goal: Write the challenge file to a directory instead of starting a server.
acme --account-key test-account.key serve-http-01 --token <token> --challenge-dir /var/www/acmeExpected:
- Exit code: 0
- File created at
/var/www/acme/.well-known/acme-challenge/<token>
Goal: Binding to an occupied port produces a clear error.
# Terminal 1: occupy port
python -m http.server 5002
# Terminal 2: try to serve
acme --account-key test-account.key serve-http-01 --token dummy --port 5002Expected:
- Exit code: 1
- Error message:
Error: port 5002 is already in use (reverse proxy or other server?) Hint: use --challenge-dir <DIR> to write the challenge file to a directory your existing web server already serves, e.g.: acme-client-rs run example.com --challenge-dir /var/www/html
Goal: Display the TXT record value for DNS-01 validation.
acme --account-key test-account.key show-dns-01 --domain test.example.com --token <token>Expected:
- Exit code: 0
- Output (exact value depends on key+token):
=== DNS-01 Challenge === Create a DNS TXT record: Name: _acme-challenge.test.example.com Type: TXT Value: <base64url-encoded-sha256> - Command exits immediately (display only, no interactive wait)
Goal: Submit CSR to finalize a ready order.
First, ensure all authorizations are valid (use PEBBLE_VA_ALWAYS_VALID=1 + respond to challenges).
acme --account-key test-account.key --account-url <account-url> finalize --finalize-url <finalize-url> test.example.comExpected:
- Exit code: 0
- Output:
Order status: valid(orprocessing) - If valid:
Certificate URL: https://localhost:14000/certZ/<id>
Goal: Check order status until it becomes valid.
acme --account-key test-account.key --account-url <account-url> poll-order <order-url>Expected:
- Exit code: 0
- Output:
Order status: valid(after finalization) withCertificate URL: ...
Goal: Download the issued certificate chain.
acme --account-key test-account.key --account-url <account-url> download-cert <certificate-url> --output test-cert.pemExpected:
- Exit code: 0
- Output:
Certificate saved to test-cert.pem - File contains PEM-encoded certificate(s)
Verify:
openssl x509 -in test-cert.pem -text -noout | head -20Should show the domain in Subject Alternative Names.
Goal: Revoke a previously issued certificate.
--account-urlis optional - the client auto-discovers the account if omitted.
acme --account-key test-account.key revoke-cert test-cert.pemExpected:
- Exit code: 0
- Output:
Certificate revoked
Goal: Revoke with an explicit reason (e.g., 4 = superseded).
acme --account-key test-account.key revoke-cert test-cert.pem --reason 4Expected:
- Exit code: 0
- Output:
Certificate revoked
Goal: Deactivate the ACME account (irreversible).
acme --account-key test-account.key --account-url <account-url> deactivate-accountExpected:
- Exit code: 0
- Output:
Account status: deactivated
Goal: Verify that operations fail after account deactivation.
acme --account-key test-account.key --account-url <account-url> order test.example.comExpected:
- Exit code: 1
- Error message referencing
unauthorizedor account being deactivated
Goal: Run the complete automated flow with HTTP-01 challenge.
Requires
PEBBLE_VA_ALWAYS_VALID=1so Pebble doesn't need to reach the HTTP server.
# Fresh key
acme generate-key --account-key e2e-account.key
# Full flow
acme --insecure --account-key e2e-account.key run --contact e2e@example.com --challenge-type http-01 --http-port 5002 e2e-test.example.comExpected:
- Exit code: 0
- Output shows all 6 steps completing:
Account status: valid Order URL: ... Order status: pending Authorization for e2e-test.example.com - status: pending HTTP-01 server listening on 0.0.0.0:5002 Challenge response sent - waiting for validation… Authorization status: valid Order status: valid Certificate saved to certificate.pem certificate.pemfile created with valid PEM content
Goal: Run the automated flow with DNS-01 (interactive - pauses for DNS record setup).
acme --account-key e2e-account.key run --contact e2e@example.com --challenge-type dns-01 dns-test.example.comExpected:
- Prints DNS TXT record instructions
- Waits for Enter keypress
- After pressing Enter, proceeds through finalization
- With
PEBBLE_VA_ALWAYS_VALID=1, completes successfully
Goal: Issue a multi-SAN certificate through the automated flow.
acme --account-key e2e-account.key run --contact e2e@example.com --challenge-type http-01 --http-port 5002 san1.example.com san2.example.com san3.example.comExpected:
- Three separate authorizations processed
- Single certificate issued with all three SANs
Verify:
openssl x509 -in certificate.pem -text -noout | grep -A5 "Subject Alternative Name"Goal: Verify all three environment variables work as alternatives to flags.
export ACME_DIRECTORY_URL=https://localhost:14000/dir
export ACME_ACCOUNT_KEY_FILE=e2e-account.key
export ACME_ACCOUNT_URL=<account-url>
# No flags needed
acme order test.example.comExpected:
- Exit code: 0
- Uses the directory, key, and account URL from environment
Goal: Verify global options work when placed after the subcommand.
acme generate-key --account-key after-sub.key
acme account --account-key after-sub.key --contact test@example.comExpected:
- Both succeed (exit code 0)
--account-keyis recognized after the subcommand name
Goal: Clear error when the account key file doesn't exist.
acme --account-key nonexistent.key accountExpected:
- Exit code: 1
- Error:
Error: failed to read account key from nonexistent.key: ...
Goal: Clear error for an unreachable or wrong directory URL.
acme --directory https://localhost:9999/nope generate-key --account-key x.key
# generate-key doesn't contact the server - try account instead:
acme --directory https://localhost:9999/nope --account-key test-account.key accountExpected:
generate-key: succeeds (doesn't need the directory)account: exit code 1, error about connection refused or ACME directory request failed
Goal: generate-key succeeds even with an unreachable directory URL (no server contact needed).
acme --directory https://localhost:59999/nope generate-key --account-key offline.keyExpected:
- Exit code: 0
- Key file created (offline operation — no directory access required)
Goal: Clear error when directory URL returns a non-ACME response.
acme --directory https://localhost:14000/nonexistent --account-key test-account.key accountExpected:
- Exit code: 1
- Error:
Error: ACME directory request failed (HTTP 404): ...
Goal: Proper error when the CA rejects a domain name.
This test requires a CA with name constraints (not default Pebble, which allows any domain).
acme --directory https://constrained-ca/directory --account-key test-account.key --account-url <account-url> order not-allowed-domain.comExpected:
- Exit code: 1
- Error:
Error: ACME error (HTTP 400 Bad Request): The server will not issue certificates for the identifier (urn:ietf:params:acme:error:rejectedIdentifier) - No stack trace
Goal: Debug-level logging provides detailed protocol information.
RUST_LOG=debug acme --account-key test-account.key account --contact test@example.comExpected:
- Additional log lines showing:
- Nonce fetch requests
- JWS signing details
- Full HTTP request/response flow
- Directory structure
Goal: HTTP-01 server binds to a non-default port.
acme --account-key test-account.key serve-http-01 --token test-token --port 8080Expected:
- Output:
HTTP-01 server listening on 0.0.0.0:8080 - Server accepts connections on port 8080
Goal: Subcommands that operate on an existing account (order, get-authz, finalize, poll-order, download-cert, revoke-cert, deactivate-account, key-rollover, pre-authorize, renewal-info) require --account-url (or ACME_ACCOUNT_URL). Without it, the request is signed in JWK mode and the CA rejects it with unauthorized / malformed.
# Skip --account-url - request signed JWK-only
acme --insecure --account-key test-account.key order test.example.comExpected:
- Exit code: non-zero
- Error message indicates the order endpoint rejected the JWK-signed request (Pebble returns
urn:ietf:params:acme:error:malformed)
To make the call succeed, first register the account and pass its URL:
acme --insecure --account-key test-account.key account --contact test@example.com
# -> copy "Account URL: https://localhost:14000/my-account/<id>"
acme --insecure --account-key test-account.key --account-url <account-url> order test.example.comNote: The
runsubcommand sidesteps this by callingcreate_accountinternally before issuing the order, so--account-urlis optional there.
Goal: Verify the client retries on badNonce errors (RFC 8555 §6.5).
Use
PEBBLE_WFE_NONCEREJECT=50to make Pebble randomly reject 50% of nonces.
# docker-compose.yml override
environment:
- PEBBLE_WFE_NONCEREJECT=50acme --account-key test-account.key run --contact test@example.com --challenge-type http-01 --http-port 5002 nonce-test.example.comExpected:
- Flow completes despite nonce rejections
- With
RUST_LOG=debug, log showsReceived badNonce - retrying with fresh noncemessages
Goal: Verify --key-password encrypts the issued private key with PKCS#8 AES-256-CBC + scrypt KDF.
acme --insecure --account-key enc-test.key run --contact enc@example.com --challenge-type http-01 --http-port 5002 --cert-output enc-cert.pem --key-output enc-private.key --key-password "TestP@ssw0rd!2026" test.example.comExpected:
- Exit code: 0
enc-private.keycontainsENCRYPTED PRIVATE KEYPEM headeropenssl pkey -in enc-private.key -passin pass:TestP@ssw0rd!2026 -nooutsucceeds- Wrong password is rejected
Goal: Verify --key-password-file reads the encryption password from a file.
echo "FileP@ssw0rd!2026" > key-password.txt
acme --insecure --account-key enc2.key run --contact enc2@example.com --challenge-type http-01 --http-port 5002 --cert-output enc2-cert.pem --key-output enc2-private.key --key-password-file key-password.txt test.example.comExpected:
- Exit code: 0
enc2-private.keycontainsENCRYPTED PRIVATE KEYPEM header- Key is decryptable with the file password
Goal: Providing both password flags produces a CLI error.
acme --insecure --account-key test.key run --key-password "pw1" --key-password-file pw.txt test.example.comExpected:
- Exit code: non-zero
- Error mentions conflict/mutual exclusivity
Goal: Without --key-password, the private key is unencrypted PKCS#8 PEM.
Expected:
- Key file starts with
BEGIN PRIVATE KEY(notENCRYPTED)
Goal: run --days 1 skips issuance when the existing certificate has more than 1 day remaining.
# Step 1: Issue a certificate first
acme --insecure --account-key renewal.key run --contact renewal@example.com --challenge-type http-01 --http-port 5002 --cert-output renewal-cert.pem --key-output renewal-key.pem test.example.com
# Step 2: Re-run with --days 1 (should skip - cert has many days left)
acme --insecure --account-key renewal.key run --contact renewal@example.com --challenge-type http-01 --http-port 5002 --cert-output renewal-cert.pem --key-output renewal-key.pem --days 1 test.example.comExpected:
- Both commands exit with code 0
- Step 2 output contains
skipping renewal(case-insensitive)
Goal: run --days 9999 forces renewal because the certificate has fewer than 9999 days remaining.
acme --insecure --account-key renewal.key run --contact renewal@example.com --challenge-type http-01 --http-port 5002 --cert-output renewal-cert.pem --key-output renewal-key.pem --days 9999 test.example.comExpected:
- Exit code: 0
- Certificate file content changes (new certificate issued)
Goal: Rotate the account key to a new key pair.
acme generate-key --account-key rollover-new.key
acme --insecure --account-key rollover-old.key --account-url <account-url> key-rollover --new-key rollover-new.keyExpected:
- Exit code: 0
- Output contains
key rolled overorrolled over successfully(case-insensitive) - New key works for subsequent operations (e.g., placing an order)
Goal: Verify --dns-hook is called with ACME_ACTION=create before validation and ACME_ACTION=cleanup after.
acme --insecure --account-key dns-hook.key run --contact dns-hook@example.com --challenge-type dns-01 --dns-hook /path/to/hook.sh --cert-output dns-hook-cert.pem --key-output dns-hook-key.pem test.example.comExpected:
- Hook called with
action=create(at least once) - Hook called with
action=cleanup(at least once) - Hook receives the correct domain name
- Certificate issued (with
PEBBLE_VA_ALWAYS_VALID=1)
Goal: When --dns-wait times out (DNS record never appears), the cleanup hook is still called.
acme --insecure --account-key dns-hook2.key run --contact dns-hook2@example.com --challenge-type dns-01 --dns-hook /path/to/hook.sh --dns-wait 1 test.example.comExpected:
- Exit code: non-zero (propagation timeout)
- Hook log shows at least 1
action=createand at least 1action=cleanup - Cleanup is called on the error path (not only on success)
Goal: Multi-SAN order with --dns-hook and --dns-propagation-concurrency 1 processes all domains. The semaphore serializes the 3 domains through 1 permit.
acme --insecure --account-key dns-hook3.key run --contact dns-hook3@example.com --challenge-type dns-01 --dns-hook /path/to/hook.sh --dns-propagation-concurrency 1 --cert-output dns-hook3-cert.pem --key-output dns-hook3-key.pem domain1.example.com domain2.example.com domain3.example.comExpected:
- Hook
action=createcalled at least 3 times (one per domain) - Hook
action=cleanupcalled at least 3 times (one per domain) - At least 3 unique domains in hook log
- Exit code: 0 (certificate issued)
Goal: Multi-SAN order with --dns-wait 1 and --dns-propagation-concurrency 1 — propagation times out, but cleanup hooks are called for all 3 domains.
acme --insecure --account-key dns-hook4.key run --contact dns-hook4@example.com --challenge-type dns-01 --dns-hook /path/to/hook.sh --dns-wait 1 --dns-propagation-concurrency 1 domain1.example.com domain2.example.com domain3.example.comExpected:
- Exit code: non-zero (propagation timeout)
- Hook
action=createcalled at least 3 times - Hook
action=cleanupcalled at least 3 times (on error path)
Goal: Verify certificates and keys are saved to custom paths.
acme --insecure --account-key custom-out.key run --contact custom@example.com --challenge-type http-01 --http-port 5002 --cert-output /custom/dir/my-cert.pem --key-output /custom/dir/my-key.pem test.example.comExpected:
- Exit code: 0
- Certificate at the custom cert path
- Private key at the custom key path
Goal: JSON output from generate-key.
acme --output-format json generate-key --account-key json-key.keyExpected:
- Output is valid JSON containing
"command": "generate-key"and"algorithm"field
Goal: JSON output from account.
acme --insecure --output-format json --account-key json-key.key account --contact json@example.comExpected:
- Valid JSON with
"command": "account","status": "valid","url"present
Goal: JSON output from order.
acme --insecure --output-format json --account-key json-key.key --account-url <url> order test.example.comExpected:
- Valid JSON with
"command": "order","order_url"present,"authorizations"array with length > 0
Goal: JSON output from show-dns-01.
acme --output-format json --account-key json-key.key show-dns-01 --domain test.example.com --token test-tokenExpected:
- Valid JSON with
"record_name"containing_acme-challengeand"record_value"present
Goal: JSON output from a full run flow.
acme --insecure --output-format json --account-key json-e2e.key run --contact json-e2e@example.com --challenge-type http-01 --http-port 5002 --cert-output json-cert.pem --key-output json-key.pem test.example.comExpected:
- Exit code: 0
- JSON output contains
"action":"issued","cert_path","key_path","key_encrypted": false,"profile": null
Goal: JSON output when renewal is skipped.
acme --insecure --output-format json --account-key json-e2e.key run --contact json-e2e@example.com --challenge-type http-01 --http-port 5002 --cert-output json-cert.pem --key-output json-key.pem --days 1 test.example.comExpected:
- JSON output contains
"action":"skip","days_remaining","threshold": 1
Goal: Default text output has no JSON.
acme generate-key --account-key text-check.keyExpected:
- Output contains
account key saved to - Output does NOT contain
"command"
Goal: acme run --help lists the --on-challenge-ready flag.
Expected:
- Help output contains
on-challenge-ready
Goal: acme run --help lists the --on-cert-issued flag.
Expected:
- Help output contains
on-cert-issued
Goal: No panic when --on-challenge-ready points to a nonexistent script.
acme --insecure --account-key hook.key run --on-challenge-ready /nonexistent/hook.sh example.comExpected:
- No panic (may fail for other reasons)
Goal: No panic when --on-cert-issued points to a nonexistent script.
acme --insecure --account-key hook.key run --on-cert-issued /nonexistent/deploy.sh example.comExpected:
- No panic (may fail for other reasons)
Goal: --on-challenge-ready and --on-cert-issued can be used simultaneously.
acme --insecure --account-key hook.key run --on-challenge-ready /nonexistent/hook.sh --on-cert-issued /nonexistent/deploy.sh example.comExpected:
- No "cannot be used with" conflict error
Goal: acme account --help shows --eab-kid and --eab-hmac-key.
Expected:
- Help output contains both
eab-kidandeab-hmac-key
Goal: acme run --help shows --eab-kid and --eab-hmac-key.
Expected:
- Help output contains both
eab-kidandeab-hmac-key
Goal: Providing --eab-kid alone is rejected by clap.
acme --insecure --account-key eab.key account --eab-kid test-kidExpected:
- Exit code: non-zero
- Error mentions
eab-hmac-keyis required
Goal: Providing --eab-hmac-key alone is rejected by clap.
acme --insecure --account-key eab.key account --eab-hmac-key dGVzdAExpected:
- Exit code: non-zero
- Error mentions
eab-kidis required
Goal: Non-base64url HMAC key is rejected.
acme --insecure --account-key eab.key account --eab-kid test-kid --eab-hmac-key "not!!!valid===base64"Expected:
- Exit code: non-zero
- Error mentions base64 decode failure
Goal: Server rejects invalid EAB credentials (or accepts if EAB is not required).
acme --insecure --account-key eab.key account --contact eab-test@example.com --eab-kid fake-kid-12345 --eab-hmac-key dGVzdGtleWZvcmhtYWN0ZXN0aW5nExpected:
- Server rejects (error), OR
- Server accepts (EAB not required — ignored per RFC)
Goal: No panic when EAB flags are used with run.
acme --insecure --account-key eab.key run --eab-kid fake-kid --eab-hmac-key dGVzdA example.comExpected:
- No panic
Goal: acme --help lists the pre-authorize subcommand.
Expected:
- Help output contains
pre-authorize
Goal: acme pre-authorize --help shows --domain and --challenge-type.
Expected:
- Help output contains both
--domainand--challenge-type
Goal: pre-authorize without --domain is rejected.
acme --insecure --account-key preauth.key pre-authorizeExpected:
- Exit code: non-zero
- Error mentions
--domainis required
Goal: Graceful handling when the server does not advertise newAuthz.
acme --insecure --account-key preauth.key pre-authorize --domain preauth-test.example.comExpected:
- Clear error about pre-authorization not supported, OR
- Authorization URL returned (if server supports it)
- No panic
Goal: acme run --help shows --pre-authorize.
Expected:
- Help output contains
--pre-authorize
Goal: No panic when --pre-authorize is used with run.
acme --insecure --account-key preauth.key run --pre-authorize --challenge-type http-01 preauth-test.example.comExpected:
- No panic
Goal: JSON output from pre-authorize.
acme --insecure --output-format json --account-key preauth.key pre-authorize --domain preauth-test.example.comExpected:
- No panic
- If supported: valid JSON with
"command": "pre-authorize" - If not supported: error message
Goal: Requesting different domains than the existing certificate should skip with a helpful message.
# Requires an existing cert for a DIFFERENT domain
acme --output-format json --account-key e2e.key run \
--contact e2e@example.com \
--challenge-type http-01 --http-port 5002 \
--cert-output existing-cert.pem --key-output existing-key.pem \
--days 1 \
different-domain.example.comExpected:
- Exit code 0
- JSON output with
"action": "skip","reason": "domain_mismatch" - Hint suggesting
--reissue-on-mismatch
Goal: With --reissue-on-mismatch, domain mismatch triggers reissuance instead of skip.
acme --output-format json --account-key e2e.key run \
--contact e2e@example.com \
--challenge-type http-01 --http-port 5002 \
--cert-output existing-cert.pem --key-output existing-key.pem \
--days 1 --reissue-on-mismatch \
different-domain.example.comExpected:
- Exit code 0
- JSON output with
"action": "reissue","reason": "domain_mismatch" - Followed by normal certificate issuance
Goal: With --print-cert, the issued certificate PEM is printed to stdout after saving to file.
acme --account-key e2e.key run \
--contact e2e@example.com \
--challenge-type http-01 --http-port 5002 \
--cert-output cert78.pem --key-output key78.pem \
--print-cert \
tc78.example.comExpected:
- Exit code 0
- Certificate saved to
cert78.pem - Certificate PEM printed to stdout (contains
-----BEGIN CERTIFICATE-----) - Without
--print-cert, no PEM is printed to stdout
Goal: The --silent global flag suppresses all stdout output across all subcommands.
# 79a: silent run — no stdout, exit code only
acme --silent --account-key e2e.key run \
--contact e2e@example.com \
--challenge-type http-01 --http-port 5002 \
--cert-output cert79.pem --key-output key79.pem \
tc79.example.com
# Capture: stdout must be empty, exit code 0, cert79.pem exists
# 79b: silent with --output-format json — still no stdout
acme --silent --output-format json --account-key e2e.key run \
--contact e2e@example.com \
--challenge-type http-01 --http-port 5002 \
--cert-output cert79b.pem --key-output key79b.pem \
tc79b.example.com
# Capture: stdout must be empty, exit code 0
# 79c: silent generate-key — no stdout
acme --silent generate-key --account-key silent-test.key
# Capture: stdout must be empty, exit code 0, silent-test.key exists
# 79d: silent show-config — no stdout
acme --silent show-config
# Capture: stdout must be empty, exit code 0Expected:
- All sub-tests: exit code 0
- All sub-tests: stdout is completely empty (zero bytes)
- Files are still created correctly
- stderr (tracing) is unaffected by
--silent
Goal: The list-profiles subcommand displays available certificate profiles from the ACME server directory.
acme --directory https://acme-v02.api.letsencrypt.org/directory list-profilesExpected:
- Output starts with
Available certificate profiles: - Lists profile names with URLs (e.g.,
classic: https://letsencrypt.org/docs/profiles#classic) - Does NOT require an account key file
- Exit code 0
Goal: JSON output for list-profiles returns structured data.
acme --directory https://acme-v02.api.letsencrypt.org/directory --output-format json list-profilesExpected:
- JSON object with
"command": "list-profiles"and"profiles"map - Each profile is a key-value pair (name → description URL)
- Exit code 0
Goal: When the server does not advertise profiles, a clear message is shown.
acme --directory https://localhost:14000/dir list-profilesExpected (if Pebble has no profiles):
- Output:
Server does not advertise any profiles. - Exit code 0
Goal: The --profile flag appears in help for order and run subcommands.
acme order --help | grep -i profile
acme run --help | grep -i profileExpected:
- Both show
--profile <PROFILE>with description and[env: ACME_PROFILE=]
Goal: The --profile flag is accepted on the order subcommand and the profile is echoed in the response.
# Use a profile advertised by the server (e.g., "classic" for Let's Encrypt,
# "default" for Pebble). Query available profiles with list-profiles first.
acme --directory https://acme-server/directory \
--account-url <account-url> \
order --profile <server-profile> example.comExpected:
- Order placed successfully
- Text output includes
Profile: <server-profile> - JSON output includes
"profile": "<server-profile>" - Exit code 0
Goal: Using a profile not advertised by the server emits a warning but proceeds.
RUST_LOG=warn acme --directory https://acme-server/directory \
--account-url <account-url> \
order --profile nonexistent example.comExpected:
- stderr warning:
Profile "nonexistent" is not advertised by the server - Order still attempted (server may reject with ACME error)
The following test cases require special server configurations and are not included in the automated test script (tests/test.sh):
| TC | Description | Requirements |
|---|---|---|
| 28 | Rejected identifier — CA rejects a domain | CA with name constraints |
| 31 | Missing account URL — operations fail clearly | Manual |
| 32 | badNonce retry — client retries automatically | PEBBLE_WFE_NONCEREJECT=50 |
| 42 | run --ari — ARI-guided renewal |
Server with ARI support |
| 43 | run --ari --days — ARI with days fallback |
Server with ARI support |
| 44 | renewal-info on server without ARI |
Server without ARI |
| 45–49 | DNS-PERSIST-01 tests | Pebble with dns-persist-01 support |
| TC | Command | Challenge | Expectation | Automated |
|---|---|---|---|---|
| 01 | generate-key |
- | Key file created | Yes |
| 01b | generate-key (all algorithms) |
- | All key types generated | Yes |
| 02 | generate-key (overwrite) |
- | File replaced | Yes |
| 03 | account |
- | Account created | Yes |
| 04 | account (idempotent) |
- | Same account returned | Yes |
| 05 | account (no contact) |
- | Account created | Yes |
| 06 | order (single) |
- | Order pending | Yes |
| 07 | order (multi SAN) |
- | Multiple authz URLs | Yes |
| 08 | get-authz |
- | Challenges listed | Yes |
| 09 | respond-challenge |
- | Challenge progresses | Yes |
| 10 | serve-http-01 (standalone) |
HTTP-01 | Token served | Yes |
| 10b | serve-http-01 (challenge-dir) |
HTTP-01 | File written | Yes |
| 11 | serve-http-01 (port busy) |
HTTP-01 | Clear error | Yes |
| 12 | show-dns-01 |
DNS-01 | TXT instructions | Yes |
| 13 | finalize |
- | CSR submitted | Yes |
| 14 | poll-order |
- | Status returned | Yes |
| 15 | download-cert |
- | PEM saved | Yes |
| 16 | revoke-cert |
- | Cert revoked | Yes |
| 17 | revoke-cert (reason) |
- | Revoked with code | Yes |
| 18 | deactivate-account |
- | Account deactivated | Yes |
| 19 | Operations post-deactivation | - | Rejected | Yes |
| 20 | run (e2e, all key algorithms) |
HTTP-01 | Full flow succeeds | Yes |
| 21 | run (e2e) |
DNS-01 | Interactive flow | Yes |
| 22 | run (multi-SAN) |
HTTP-01 | Multi-domain cert | Yes |
| 23 | Env vars | - | Config from env | Yes |
| 24 | Global args after subcmd | - | Args accepted | Yes |
| 25 | Missing key file | - | Clear error | Yes |
| 26 | Invalid directory URL | - | Clear error | Yes |
| 26b | generate-key offline |
- | No directory needed | Yes |
| 27 | Directory 404 | - | Clear error | Yes |
| 28 | Rejected identifier | - | Clean ACME error | Manual |
| 29 | RUST_LOG=debug |
- | Verbose output | Yes |
| 30 | Custom HTTP port | HTTP-01 | Binds correctly | Yes |
| 31 | Missing account URL | - | Handled gracefully | Manual |
| 32 | badNonce retry | - | Auto-retry works | Manual |
| 33 | run + --key-password |
HTTP-01 | Encrypted key | Yes |
| 34 | run + --key-password-file |
HTTP-01 | Password from file | Yes |
| 35 | Password flags conflict | - | Mutual exclusivity | Yes |
| 36 | No password (unencrypted) | - | Plain PKCS#8 PEM | Yes |
| 37 | Renewal skipped (--days 1) |
HTTP-01 | Skips when not due | Yes |
| 38 | Renewal proceeds (--days 9999) |
HTTP-01 | Renews when due | Yes |
| 39 | key-rollover |
- | Key rotated | Yes |
| 40 | DNS-01 hook (create/cleanup) | DNS-01 | Hook called both ways | Yes |
| 40b | DNS-01 hook cleanup on timeout | DNS-01 | Cleanup on error path | Yes |
| 40c | Multi-domain DNS-01 hook (concurrency=1) | DNS-01 | All domains processed | Yes |
| 40d | Multi-domain DNS-01 cleanup on timeout | DNS-01 | Cleanup for all domains | Yes |
| 41 | Custom output paths | HTTP-01 | Files at custom paths | Yes |
| 42 | run --ari |
HTTP-01 | ARI-guided renewal | Manual |
| 43 | run --ari --days |
HTTP-01 | ARI with days fallback | Manual |
| 44 | renewal-info (no ARI server) |
- | Clear error | Manual |
| 45–49 | DNS-PERSIST-01 tests | DNS-PERSIST-01 | Various | Manual |
| 50 | generate-key JSON |
- | Structured JSON | Yes |
| 51 | account JSON |
- | Structured JSON | Yes |
| 52 | order JSON |
- | Structured JSON | Yes |
| 53 | show-dns-01 JSON |
- | Structured JSON | Yes |
| 54 | run JSON (e2e) |
HTTP-01 | action: issued |
Yes |
| 55 | run JSON (renewal skip) |
HTTP-01 | action: skip |
Yes |
| 56 | Text mode unchanged | - | No JSON in text | Yes |
| 57 | --on-challenge-ready in help |
- | Flag listed | Yes |
| 58 | --on-cert-issued in help |
- | Flag listed | Yes |
| 59 | --on-challenge-ready nonexistent |
- | No panic | Yes |
| 60 | --on-cert-issued nonexistent |
- | No panic | Yes |
| 61 | Both hooks together | - | No conflict | Yes |
| 62 | EAB flags in account help |
- | Flags listed | Yes |
| 63 | EAB flags in run help |
- | Flags listed | Yes |
| 64 | --eab-kid alone rejected |
- | Requires --eab-hmac-key |
Yes |
| 65 | --eab-hmac-key alone rejected |
- | Requires --eab-kid |
Yes |
| 66 | Invalid base64url HMAC key | - | Decode error | Yes |
| 67 | Fake EAB credentials | - | Server rejects or ignores | Yes |
| 68 | EAB on run (no panic) |
- | No panic | Yes |
| 69 | pre-authorize in help |
- | Subcommand listed | Yes |
| 70 | pre-authorize --help flags |
- | --domain, --challenge-type |
Yes |
| 71 | pre-authorize requires --domain |
- | Required arg | Yes |
| 72 | pre-authorize missing newAuthz |
- | Graceful error | Yes |
| 73 | --pre-authorize in run help |
- | Flag listed | Yes |
| 74 | --pre-authorize on run |
- | No panic | Yes |
| 75 | pre-authorize JSON output |
- | Structured JSON | Yes |
| 76 | run domain mismatch (skip) |
- | Skip with reason | Yes |
| 77 | run domain mismatch (reissue) |
- | Reissue triggered | Yes |
| 78 | run --print-cert |
HTTP-01 | PEM printed to stdout | Yes |
| 79 | --silent suppresses stdout |
- | No stdout output | Yes |
| 80 | list-profiles (text) |
- | Profiles listed | Yes |
| 81 | list-profiles (JSON) |
- | Structured JSON | Yes |
| 82 | list-profiles (no profiles) |
- | Clear message | Yes |
| 83 | --profile in help |
- | Flag listed | Yes |
| 84 | --profile on order |
- | Profile echoed | Yes |
| 85 | --profile unknown warning |
- | Warning emitted | Yes |
Tested against a step-ca ACME server on 2026-03-11 using version 1.7.0.
| TC | Test | Key | Challenge | Result |
|---|---|---|---|---|
| 01 | Generate key (ES256) | ES256 | - | ✅ Pass |
| 01 | Generate key (ES512) | ES512 | - | ✅ Pass |
| 01 | Generate key (RSA2048) | RSA2048 | - | ✅ Pass |
| 01 | Generate key (Ed25519) | Ed25519 | - | ✅ Pass |
| 02 | Generate key - overwrite | ES256 | - | ✅ Pass |
| 03 | Account creation (standalone) | ES256 | - | ✅ Pass |
| 04 | Account idempotent lookup | ES256 | - | ✅ Pass |
| 05 | Account - no contact | ES256 | - | ✅ Pass |
| 06 | Place an order (single domain) | ES256 | - | ✅ Pass |
| 07 | Place an order - multiple domains (SAN) | ES256 | - | ✅ Pass |
| 08 | Fetch authorization | ES256 | - | ✅ Pass |
| 09 | Respond to challenge | ES256 | HTTP-01 | ✅ Pass |
| 10 | Serve HTTP-01 standalone + curl | ES256 | HTTP-01 | ✅ Pass |
| 11 | Serve HTTP-01 - port busy | ES256 | HTTP-01 | ✅ Pass |
| 12 | Show DNS-01 instructions | ES256 | DNS-01 | ✅ Pass |
| 13 | Finalize order | ES256 | - | ✅ Pass |
| 14 | Poll order status | ES256 | - | ✅ Pass |
| 15 | Download certificate | ES256 | - | ✅ Pass |
| 16 | Revoke cert + double-revoke error | ES256 | - | ✅ Pass |
| 17 | Revoke with reason code (4=superseded) | ES256 | - | ✅ Pass |
| 18 | Account deactivation | ES256 | - | ✅ Pass |
| 19 | Order after deactivation (rejected) | ES256 | - | ✅ Pass |
| 20 | Full e2e run + cert issuance | ES256 | HTTP-01 | ✅ Pass |
| 20 | Full e2e run + cert issuance | ES384 | HTTP-01 | ✅ Pass |
| 20 | Full e2e run + revoke | ES512 | HTTP-01 | ✅ Pass |
| 20 | Full e2e run + revoke | RSA2048 | HTTP-01 | ✅ Pass |
| 20 | Full e2e run + revoke | Ed25519 | HTTP-01 | ✅ Pass |
| 23 | Environment variable configuration | ES256 | - | ✅ Pass |
| 24 | Global args after subcommand | ES256 | - | ✅ Pass |
| 25 | Missing key file | - | - | ✅ Pass |
| 26 | Invalid directory URL | ES256 | - | ✅ Pass |
| 27 | Directory 404 | ES256 | - | ✅ Pass |
| 28 | Rejected identifier (name constraints) | ES256 | HTTP-01 | ✅ Pass |
| 29 | RUST_LOG=debug verbose output |
ES256 | - | ✅ Pass |
| 30 | Wrong port - warning + clean failure | ES256 | HTTP-01 | ✅ Pass |
| 31 | Missing account URL - handled gracefully | ES256 | - | ✅ Pass |
| 22 | Multi-SAN e2e (2 domains) | ES256 | HTTP-01 | ✅ Pass |
| 21 | DNS-01 e2e (interactive) | ES256 | DNS-01 | ✅ Pass |