2. ACME协议交互流程详解
ACME 协议(RFC 8555)本质上是一套“客户端驱动、服务端裁决”的自动化证书生命周期协议。它把一次证书签发拆成若干可独立观察、可重试、可审计的交互阶段:客户端先读取目录资源确定各接口地址,再注册账户并完成密钥所有权证明,随后创建订单、响应授权挑战,最终提交 CSR 换取证书;吊销则是一条独立的、以证书为对象的操作路径。理解这条链路上每一步的报文形态与状态迁移,是排查“为什么卡在 pending”“为什么 challenge 一直 invalid”这类问题的前提。
目录资源:一切交互的入口
客户端启动后的第一个动作是 GET /directory。目录是一个 JSON 文档,不含任何业务状态,只提供各资源的 URL 映射。典型响应如下(示例为教学假设值):
{
"newNonce": "https://acme.example.com/acme/new-nonce",
"newAccount": "https://acme.example.com/acme/new-acct",
"newOrder": "https://acme.example.com/acme/new-order",
"revokeCert": "https://acme.example.com/acme/revoke-cert",
"keyChange": "https://acme.example.com/acme/key-change",
"meta": {
"termsOfService": "https://acme.example.com/terms/v1",
"website": "https://acme.example.com",
"caaIdentities": ["example.com"],
"externalAccountRequired": false
}
}
目录本身不参与状态机,但它决定了后续所有请求的目标地址。若 meta.externalAccountRequired 为 true,则账户注册必须携带外部账户绑定(EAB)的 kid 与 hmac,否则会直接返回 urn:ietf:params:acme:error:externalAccountRequired。因此排障时第一步永远是确认目录可访问、字段齐全、meta 约束是否满足。
Nonce 与 JWS:所有写操作的统一封装
除目录外,ACME 的写操作(newAccount、newOrder、challenge、finalize、revokeCert)都要求以 JWS(JSON Web Signature)形式提交,且必须携带一次性随机数 nonce。客户端通过 HEAD /acme/new-nonce 获取,服务端在响应头 Replay-Nonce 中返回。每个 nonce 只能使用一次,重放会得到 badNonce 错误,此时客户端应重新取 nonce 并重签请求。JWS 的受保护头包含 alg、nonce、url,账户建立后还需加入 kid(账户 URL);未注册时则使用 jwk 内嵌公钥。这一层封装是后续所有报文的基础,nonce 管理不当是自动化脚本最常见的失败原因之一。
账户注册与状态
向 newAccount 提交 JWS,payload 含 termsOfServiceAgreed 与可选的 contact。服务端返回 201 时表示新建账户,响应头 Location 即账户 URL(kid);若账户已存在则返回 200。账户对象状态只有 valid 与 deactivated 两种:前者可正常下单,后者由客户端向账户 URL 发送 {"status":"deactivated"} 触发,且不可逆。账户注册成功后,客户端应持久化 kid 与账户私钥,后续请求全部改用 kid 而非 jwk。
订单与授权:状态机的核心
创建订单时向 newOrder 提交 identifiers 数组,例如 [{"type":"dns","value":"app.example.com"}]。服务端返回订单对象,含 status、authorizations(授权 URL 列表)、finalize(提交 CSR 的地址)以及 expires。订单状态迁移为:pending → ready → processing → valid,异常分支为 invalid。只有当所有授权都变为 valid,订单才会从 pending 进入 ready。
每个授权对象包含一个或多个 challenge,常见类型为 http-01、dns-01、tls-alpn-01。授权状态为 pending → valid / invalid;challenge 状态为 pending → processing → valid / invalid。客户端先 GET 授权拿到 challenge 的 token 与 url,按类型部署验证材料(如 http-01 需在 /.well-known/acme-challenge/<token> 返回 token.thumbprint),再向 challenge URL 发送空 payload 的 POST 触发校验。服务端随后回连验证,客户端通过轮询授权 URL 观察状态变化。若返回 invalid,授权对象中的 error 字段会给出具体原因,如连接超时、内容不匹配、DNS 记录缺失等。
签发与吊销
订单进入 ready 后,客户端生成证书私钥与 CSR(DER 编码、Base64URL 后放入 payload 的 csr 字段),POST 到 finalize。服务端签发期间订单变为 processing,完成后变为 valid,此时订单对象的 certificate 字段给出证书下载 URL。客户端 GET 该 URL 可获得 PEM 链。若订单变为 invalid,通常意味着 CSR 与订单 identifiers 不匹配或签名算法不受支持。
吊销走独立接口 revokeCert,payload 含 certificate(DER Base64URL)与可选 reason(0–10 的 RFC 5280 原因码)。吊销请求可用账户私钥签名,也可用证书自身私钥签名。成功返回 200,重复吊销通常也返回 200 以保持幂等。吊销不影响已签发证书在客户端侧的存储,但会影响 OCSP/CRL 状态,因此自动化流程中应同步更新本地证书台账。
状态机视角下的排障要点
把上述流程映射为状态机后,排障就有了明确抓手。订单长期停在 pending,说明至少一个授权未 valid,应逐个 GET 授权 URL 查看 challenge 状态与 error;challenge 停在 processing 超过合理时间,通常是服务端回连被防火墙拦截或 DNS 尚未生效;订单进入 ready 后 finalize 返回 invalid,优先核对 CSR 中的 SAN 是否与订单 identifiers 完全一致。nonce 相关错误(badNonce)则应检查客户端是否在并发请求中复用了同一 nonce。吊销操作虽然独立,但同样需要 nonce 与 JWS 封装,且 reason 码要与实际场景匹配,否则可能被 CA 拒绝。
Link: rel="up" 头,这样在轮询和重试时无需重新推导资源路径,也能在日志中还原完整状态迁移轨迹。