最近用 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 有
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 协议。必须过滤末尾的=
符号。如果末尾包含了=
则必须视为错误编码而拒绝。
directory
|
+--> newNonce
|
+----------+----------+-----+-----+------------+
| | | | |
| | | | |
V V V V V
newAccount newAuthz newOrder revokeCert keyChange
| | |
| | |
V | V
account | order --+--> finalize
| | |
| | +--> cert
| V
+---> authorization
| ^
| | "up"
V |
challenge
它是客户端唯一必须的配置,提供 ACME 服务器操作的正确 URL 地址。
提供了以下字段
字段 | URL 含义 |
---|---|
newNonce | 新的 nonce |
newAccount | 新的 account |
newOrder | 新的订单 |
newAuthz | 新的 authorization |
revokeCert | 吊销证书 |
keyChange | key change |
除此之外还有一个可选的 meta
字段
meta 里包含如下字段
termsOfService
: string 服务协议 URLwebsite
: 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 字段为空字符串。注意:空字符串和空对象 {}
是有区别的,用错会导致请求失败。
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:服务的地址
如果像 ZeroSSL 这样的 CA 需要 externalAccountBinding,则在 Payload 中需要提供该字段。
你可以在 后台直接复制
分析 acme.sh 代码可以得知,它通过 ZeroSSL 的 API,获取了邮箱对应的 EAB credentials。
URL: api.zerossl.com/acme/eab-credentials-email
Method: Post
字段: email
有了账号之后需要创建订单
订单结构如下:
{
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
往上一步的 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 的 key 是 _acme-challenge
,value 是 token + jwk指纹 sha256 摘要
然后再次 sha256,并以 url 安全的 base64 编码。
urlsafebase64(sha256({token}.{key}))
参考实现方式: https://github.com/publishlab/node-acme-client/blob/4437c8377c40558a869a23f55471dff1cc81f6c7/src/client.js#L465
相对于 dns-01,http-01 验证方式更简单一些;缺点是不能验证 wildcard 证书(*.example.com)。
要求在域名解析出来的 IPv4 或 IPv6 地址上监听 80 端口(且必须是 80 端口),请求的地址为:http://{domain}/.well-known/acme-challenge/{token}
返回的数据为:
{token}.{key}
这次只需明文文本,无需 sha256 也无需 urlsafebase64。
它的好处是可以通过 443 端口来验证。不了解更详细的验证细节。
完成验证操作后,请求上述数据包中的 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'
}
当订单状态为 ready 时可以 finalize 订单(传入 csr)
当订单状态为 valid 时可以下载证书
第一步创建订单中的 finalize
字段即是通知订单完成的 URL。当所有验证均已完成后即可完成订单。
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
字段,提示延迟的时间
【未完待续】
玻璃钢生产厂家促销独角兽玻璃钢雕塑平顶山玻璃钢花盆定做四川玻璃钢海豚雕塑价格肇庆玻璃钢卡通雕塑怎么样天津玻璃钢马雕塑制作方法台湾玻璃钢雕塑厂厂家绩溪玻璃钢雕塑厂家选购玻璃钢动物雕塑深圳玻璃钢雕塑定做厂家龙海玻璃钢雕塑玻璃钢雕塑工厂业务员弹弹兔玻璃钢雕塑晋中玻璃钢雕塑设计山东玻璃钢雕塑批发厂深圳玻璃钢雕塑大全泉州玻璃钢花盆厂玻璃钢狮身雕塑价格甘南玻璃钢雕塑制作影视卡通关羽玻璃钢雕塑荥阳玻璃钢广场雕塑玻璃钢雕塑定做价格多少钱汕尾玻璃钢熊猫雕塑济南动物玻璃钢雕塑安装商场橱窗美陈雕塑福建特色商场美陈多少钱武威户外玻璃钢雕塑哪家好河源玻璃钢雕塑厂商生产玻璃钢卡通雕塑尺寸玻璃钢仿真马景观雕塑摆件福州抽象玻璃钢雕塑供应商香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声单亲妈妈陷入热恋 14岁儿子报警汪小菲曝离婚始末遭遇山火的松茸之乡雅江山火三名扑火人员牺牲系谣言何赛飞追着代拍打萧美琴窜访捷克 外交部回应卫健委通报少年有偿捐血浆16次猝死手机成瘾是影响睡眠质量重要因素高校汽车撞人致3死16伤 司机系学生315晚会后胖东来又人满为患了小米汽车超级工厂正式揭幕中国拥有亿元资产的家庭达13.3万户周杰伦一审败诉网易男孩8年未见母亲被告知被遗忘许家印被限制高消费饲养员用铁锨驱打大熊猫被辞退男子被猫抓伤后确诊“猫抓病”特朗普无法缴纳4.54亿美元罚金倪萍分享减重40斤方法联合利华开始重组张家界的山上“长”满了韩国人?张立群任西安交通大学校长杨倩无缘巴黎奥运“重生之我在北大当嫡校长”黑马情侣提车了专访95后高颜值猪保姆考生莫言也上北大硕士复试名单了网友洛杉矶偶遇贾玲专家建议不必谈骨泥色变沉迷短剧的人就像掉进了杀猪盘奥巴马现身唐宁街 黑色着装引猜测七年后宇文玥被薅头发捞上岸事业单位女子向同事水杯投不明物质凯特王妃现身!外出购物视频曝光河南驻马店通报西平中学跳楼事件王树国卸任西安交大校长 师生送别恒大被罚41.75亿到底怎么缴男子被流浪猫绊倒 投喂者赔24万房客欠租失踪 房东直发愁西双版纳热带植物园回应蜉蝣大爆发钱人豪晒法院裁定实锤抄袭外国人感慨凌晨的中国很安全胖东来员工每周单休无小长假白宫:哈马斯三号人物被杀测试车高速逃费 小米:已补缴老人退休金被冒领16年 金额超20万