acme.sh申请证书流程解读
acme.sh 是一个非常了不起的项目,因为它完全用 POSIX Shell(即最基础的 Shell 脚本)实现了复杂的 ACME 协议(RFC 8555),没有依赖 Python 或 Go 等高级语言环境。
要理解它如何实现证书申请,我们需要深入其核心文件 acme.sh。整个流程严格遵循 ACME v2 协议。
以下是基于源码逻辑的深度分析,我们将证书申请流程拆解为 入口、核心调度、协议交互、验证逻辑、后续处理 五个部分。
1. 入口与参数解析:issue
当你执行 acme.sh --issue -d example.com -w /var/www/html 时,脚本的执行流程如下:
- 参数解析:脚本最底部的
main()函数处理命令行参数,识别到--issue,将_cmd变量设为issue。 - 分发执行:脚本调用
issue()函数(这是对外的主入口)。 - 环境检查:
issue()会做一系列初始化:- 解析所有域名(
-d参数)。 - 检查是否已经有账户(ACME 协议需要账户注册),如果没有则自动调用
register_account。 - 确定验证模式(DNS 模式、Webroot 模式、Standalone 模式等)。
- 解析所有域名(
最终,issue() 会调用核心的内部函数 _issue(注意前面的下划线),这是真正干活的地方。
2. 核心循环:_issue 函数
_issue 函数实现了 ACME 协议定义的标准“订单(Order)”流程。源码中这个函数非常长,其逻辑步骤如下:
A. 创建订单 (New Order)
- 源码对应:
_newOrder - 逻辑:
- 它构造一个 JSON Payload,包含你申请的所有域名。
- 调用
_post函数将请求发送给 CA(如 Let's Encrypt)的/new-order接口。 - 关键点:所有的网络请求都封装在
_post和_get中,底层使用curl或wget。 - 返回:CA 返回一个 JSON,包含
authorizations(验证地址列表)和finalize(最终提交 CSR 的地址)。
B. 获取验证详情 (Fetch Authorization)
- 源码对应:
_getAuthz - 逻辑:
- 遍历订单中的每个域名,去获取它们的验证要求。
- CA 会告诉脚本:“对于
example.com,你需要证明你拥有它。你可以选择http-01或dns-01方式,这是你需要填写的token和keyAuthorization。”
C. 触发验证动作 (Run Validation)
这是最复杂的部分,脚本根据你选的模式(HTTP 或 DNS)执行具体操作。
- 源码对应:
_run_validation - 逻辑:
- HTTP-01 模式:
- 调用
_start_http_verify。 - 它会在你的网站根目录创建
.well-known/acme-challenge/目录。 - 写入一个包含 Token 的文件。
- 调用
- DNS-01 模式:
- 调用
_start_dns_verify。 - 这里用到了著名的 DNS API 机制。源码中会
source导入dnsapi/目录下的对应脚本(例如dns_cf.sh)。 - 执行
dns_myapi_add函数,调用云厂商 API 添加 TXT 记录。
- 调用
- HTTP-01 模式:
D. 触发 CA 验证 (Trigger Challenge)
- 源码对应:
_respondToChallenge - 逻辑:
- 当你把文件放好,或者 TXT 记录加好后,
acme.sh会向 CA 发送一个 POST 请求,说:“我准备好了,你来查吧”。
- 当你把文件放好,或者 TXT 记录加好后,
E. 轮询状态 (Polling Status)
- 源码对应:
_check_authz_status - 逻辑:
- CA 的验证是异步的。脚本进入一个
while循环。 - 每隔几秒查询一次验证状态 (
pending->processing->valid或invalid)。 - 如果变更为
valid,则进入下一步;如果invalid,则报错退出。
- CA 的验证是异步的。脚本进入一个
F. 生成与提交 CSR (Finalize Order)
- 源码对应:
_createCSR,_finalizeOrder - 逻辑:
- 生成 CSR:调用
_createCSR。这里acme.sh直接调用系统的openssl命令生成私钥(如果需要)和 CSR 文件。 - 提交:将 CSR 内容通过
_post发送到之前获取的finalize接口。
- 生成 CSR:调用
G. 下载证书 (Download Cert)
- 源码对应:
_downloadCert - 逻辑:
- 一旦 Order 状态变为
valid,CA 会提供证书下载链接。 - 脚本下载 PEM 格式的证书,并处理证书链(Fullchain)。
- 一旦 Order 状态变为
3. 技术难点与实现细节
你可能会好奇,Shell 怎么处理 JSON 和加密签名?这正是 acme.sh 的黑科技之处。
1. JWS 签名 (JSON Web Signature)
ACME 协议要求所有请求必须经过 JWS 签名。
- 源码对应:
_jws() - 实现:
- 它利用
openssl dgst -sha256 -sign对 JSON 头和 Payload 进行签名。 - 然后利用自定义的
_base64函数将签名结果进行 URL 安全的 Base64 编码。 - 最后拼接成 ACME 需要的 JWS 格式结构体。
- 它利用
2. JSON 解析
Shell 没有原生的 JSON 解析器。
- 实现:
acme.sh大量使用了grep,sed,awk来提取 JSON 字段。- 例如函数
_egrep_o,它是对grep -o的封装,配合正则表达式提取 Token 或 URL。 - 这种方式虽然看起来“笨拙”,但保证了极高的兼容性,不需要安装
jq。
3. 插件化架构 (DNS API)
- 实现:
- 主程序不包含具体的 DNS 厂商逻辑。
dnsapi/目录下的每个.sh文件都遵循统一的命名规范(dns_provider_add,dns_provider_rm)。- 在运行时,主程序使用
source "$_api_file"动态加载这些函数,实现了类似于“接口”和“实现”的分离。
4. 源码阅读简易图解
如果你打开源码,可以按照这个调用栈去追踪:
issue() <-- 用户入口
|
+-> _checkAccount() <-- 确保有账户
|
+-> _issue() <-- 核心逻辑开始
|
+-> _newOrder() <-- 1. 向 CA 下单
|
+-> _getAuthz() <-- 2. 获取验证要求
|
+-> _run_validation() <-- 3. 部署验证文件或 DNS 记录 (调用插件)
|
+-> _respondToChallenge() <-- 4. 告诉 CA "我好了"
|
+-> _check_authz_status() <-- 5. 循环等待 CA 验证结果
|
+-> _createCSR() <-- 6. 调用 OpenSSL 生成 CSR
|
+-> _finalizeOrder() <-- 7. 提交 CSR
|
+-> _downloadCert() <-- 8. 下载证书
总结
acme.sh 申请证书的核心在于:它将 ACME 协议的 HTTP 交互流程,翻译成了标准的 Shell 函数调用。
它最大的技术亮点在于:
- 纯 Shell 实现 JWS 签名:绕过了编程语言库的依赖。
- 文本处理黑魔法:用基础 Linux 命令处理复杂的 JSON 交互。
- 插件系统:通过
source机制实现了强大的 DNS API 扩展性。
如果你想学习 Shell 编程的极限,阅读 acme.sh 的 _jws 函数和 _post 函数会让你大开眼界。