读一遍 ACME 协议
读一遍 ACME 协议
最近用 typescript 自己实现了一个 ACME 客户端:https://github.com/shiny/HandyAcme 读了一遍协议,一点点做了实现,然后把理解的内容记录到下文中。
2023 更新:最近又基于以上 library 写了一个 SSL 证书自动签发、部署的 Web 面板:https://github.com/shiny/late.ink
ACME 协议全名为 “Automatic Certificate Management Environment”,为 RFC 8555,地址位于:
RFC 8555 - Automatic Certificate Management Environment (ACME)
常见支持 ACME 协议的 Ca 有
- Let’s Encrypt https://letsencrypt.org/
- BuyPass https://www.buypass.com/
- ZeroSSL https://zerossl.com/
消息传输
ACME 客户端和服务端通过 HTTPS 通信,签名格式为 JWS:JSON Web Signature(RFC 7515)。JWS 提供了客户端请求的认证,并能防止重放攻击。
必须使用 HTTPS 连接。
ACME 客户端必须包含 User-Agent
头。http header 中必须包含依赖的 http 客户端名称和版本号。
ACME 客户端必须发送 Accept-Language
header(RFC 7231),以便实现本地化错误信息。
JSON 对象二进制字段必须用 URL安全的 base64 协议。必须过滤末尾的=
符号。如果末尾包含了=
则必须视为错误编码而拒绝。
ACME 资源关系图
directory
|
+--> newNonce
|
+----------+----------+-----+-----+------------+
| | | | |
| | | | |
V V V V V
newAccount newAuthz newOrder revokeCert keyChange
| | |
| | |
V | V
account | order --+--> finalize
| | |
| | +--> cert
| V
+---> authorization
| ^
| | "up"
V |
challenge
目录 Directory
它是客户端唯一必须的配置,提供 ACME 服务器操作的正确 URL 地址。
提供了以下字段
字段 | URL 含义 |
---|---|
newNonce | 新的 nonce |
newAccount | 新的 account |
newOrder | 新的订单 |
newAuthz | 新的 authorization |
revokeCert | 吊销证书 |
keyChange | key change |
除此之外还有一个可选的 meta
字段
meta 里包含如下字段
-
termsOfService
: string 服务协议 URL -
website
: string 网址 -
caaIdentities
: string[] 我不理解 -
externalAccountRequired
: boolean 重要,CA 是否需要 externalAccountBinding 字段绑定账号。ZeroSSL 需要,Let's Encrypt 不需要。
请求方式
-
获取目录为 GET
-
获取 nonce 为 HEAD
-
其他均为 POST
-
POST-as-GET
是什么:https://datatracker.ietf.org/doc/html/rfc8555#section-6.3即 http Method 为 POST,但 payload 字段为空字符串。注意:空字符串和空对象
{}
是有区别的,用错会导致请求失败。
1. 新建账户 new Account
POST /acme/new-account HTTP/1.1
Host: example.com
Content-Type: application/jose+json
{
"protected": base64url({
"alg": "ES256",
"jwk": {...},
"nonce": "6S8IqOGY7eL2lsGoTZYifg",
"url": "https://example.com/acme/new-account"
}),
"payload": base64url({
"termsOfServiceAgreed": true,
"contact": [
"mailto:[email protected]",
"mailto:[email protected]"
]
}),
"signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I"
}
alg: 算法
jwk:JSON Web Key,包含私钥公钥
nonce:从 newNonce
获取到的随机字符串
url:服务的地址
externalAccountBinding
-
如果像 ZeroSSL 这样的 CA 需要 externalAccountBinding,则在 Payload 中需要提供该字段。
-
你可以在后台直接复制
-
分析 acme.sh 代码可以得知,它通过 ZeroSSL 的 API,获取了邮箱对应的 EAB credentials。
URL: api.zerossl.com/acme/eab-credentials-email
Method: Post
字段: email
2. 创建订单 newOrder
有了账号之后需要创建订单
订单结构如下:
{
status: 'pending',
expires: '2022-08-05T10:00:13Z',
identifiers: [
{ type: 'dns', value: 'example.com' },
{ type: 'dns', value: 'test1.example.com' }
],
authorizations: [
'https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/3166415814',
'https://acme-staging-v02.api.letsencrypt.org/acme/authz-v3/3166415824'
],
finalize: 'https://acme-staging-v02.api.letsencrypt.org/acme/finalize/62584554/3411423254'
}
关于 identifiers 的定义:https://datatracker.ietf.org/doc/html/rfc8555#section-9.7.8
根据这里的 authorizations 可以进入下一步:获取验证方式 challenges
2.1 获取验证方式 challenges
往上一步的 authorizations url 发送 POST-as-GET 的请求(payload 是空字符串)。
POST /acme/authz-v3/3188690644 HTTP/1.1
Host: example.com
Content-Type: application/jose+json
{
"protected": base64url({
"alg": "ES256",
"jwk": {...},
"nonce": "6S8IqOGY7eL2lsGoTZYifg",
"url": "https://example.com/acme/authz-v3/3188690644"
}),
"payload": '',
"signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I"
}
返回验证方式,示例如下:
{
"identifier": {
"type": "dns",
"value": "example.com"
},
"status": "pending",
"expires": "2022-08-05T10:00:13Z",
"challenges": [
{
"type": "http-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/3166415814/NPuHTg",
"token": "Jyq2Kxs8rbwGOPAPMOiHMhj3X_Y9cjqYIDcuKss0tTk"
},
{
"type": "dns-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/3166415814/-NH__g",
"token": "Jyq2Kxs8rbwGOPAPMOiHMhj3X_Y9cjqYIDcuKss0tTk"
},
{
"type": "tls-alpn-01",
"status": "pending",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/3166415814/zlVqNQ",
"token": "Jyq2Kxs8rbwGOPAPMOiHMhj3X_Y9cjqYIDcuKss0tTk"
}
]
}
dns-01 验证方式
dns 的 key 是 _acme-challenge
,value 是 token + jwk指纹 sha256 摘要
然后再次 sha256,并以 url 安全的 base64 编码。
urlsafebase64(sha256({token}.{key}))
http-01 验证方式
相对于 dns-01,http-01 验证方式更简单一些;缺点是不能验证 wildcard 证书(*.example.com)。
要求在域名解析出来的 IPv4 或 IPv6 地址上监听 80 端口(且必须是 80 端口),请求的地址为:http://{domain}/.well-known/acme-challenge/{token}
返回的数据为:
{token}.{key}
这次只需明文文本,无需 sha256 也无需 urlsafebase64。
tls-alpn-01 验证方式
它的好处是可以通过 443 端口来验证。不了解更详细的验证细节。
2.2 通知验证已完成
完成验证操作后,请求上述数据包中的 challenges.url
通知验证已经完成。
状态变化需要一点时间,可以轮询状态,等到所有 challenge
的 status
为 valid
时即可完成订单。
{
type: 'http-01',
status: 'valid',
url: 'https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/3188690644/15PaJg',
token: 'AXDybThqwiYOKqimUQmN7g4o4zlv2ZkpQMgEEC7_XMo',
validationRecord: [
{
url: 'http://test.example.com/.well-known/acme-challenge/AXDybThqwiYOKqimUQmN7g4o4zlv2ZkpQMgEEC7_XMo',
hostname: 'test.example.com',
port: '80',
addressesResolved: ['1.1.1.1'],
addressUsed: '1.1.1.1'
}
],
validated: '2022-07-31T14:29:08Z'
}
2.3 检测订单状态
当订单状态为 ready 时可以 finalize 订单(传入 csr)
当订单状态为 valid 时可以下载证书
2.4 完成订单 finalizeOrder
第一步创建订单中的 finalize
字段即是通知订单完成的 URL。当所有验证均已完成后即可完成订单。
3. 获取证书
快速验证申请的证书
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('<YOUR-SSL-KEY>.key'),
cert: fs.readFileSync('<YOUR-SSL-PEM>.crt')
};
https.createServer(options, function (req, res) {
res.writeHead(200);
res.end("hello world\n");
}).listen(8000);
console.log(`Open https://127.0.0.1:8000`);
替换代码中的 key 和 crt 文件位置,在浏览器中打开并查看证书是否和申请的一致。
错误
-
当 http 状态码为 4xx、5xx 即为出错
出错时的数据结构:
interface Error { // 清单:[https://datatracker.ietf.org/doc/html/rfc8555#section-6.7](https://datatracker.ietf.org/doc/html/rfc8555#section-6.7) // 例如:"urn:ietf:params:acme:error:malformed" type: string // 人类友好的描述,例如:"Request payload did not parse as JSON" detail: string // 例如:400 status: number }
-
频率限制
错误类型为:
urn:ietf:params:acme:error:rateLimited
detail
字段为Header 中会带上
Retry-After
字段,提示延迟的时间
【未完待续】
参考资源 Reference
- ACME 协议 https://datatracker.ietf.org/doc/html/rfc8555
- 一个 node 实现的 acme client https://github.com/publishlab/node-acme-client
- WebCrypto 加解密 https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto
- x.509 证书生成的 node 实现 https://github.com/PeculiarVentures/x509