acme.sh申请证书流程解读

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 时,脚本的执行流程如下:

  1. 参数解析:脚本最底部的 main() 函数处理命令行参数,识别到 --issue,将 _cmd 变量设为 issue
  2. 分发执行:脚本调用 issue() 函数(这是对外的主入口)。
  3. 环境检查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 中,底层使用 curlwget
    • 返回:CA 返回一个 JSON,包含 authorizations(验证地址列表)和 finalize(最终提交 CSR 的地址)。

B. 获取验证详情 (Fetch Authorization)

  • 源码对应_getAuthz
  • 逻辑
    • 遍历订单中的每个域名,去获取它们的验证要求。
    • CA 会告诉脚本:“对于 example.com,你需要证明你拥有它。你可以选择 http-01dns-01 方式,这是你需要填写的 tokenkeyAuthorization。”

C. 触发验证动作 (Run Validation)

这是最复杂的部分,脚本根据你选的模式(HTTP 或 DNS)执行具体操作。

  • 源码对应_run_validation
  • 逻辑
    1. HTTP-01 模式
      • 调用 _start_http_verify
      • 它会在你的网站根目录创建 .well-known/acme-challenge/ 目录。
      • 写入一个包含 Token 的文件。
    2. DNS-01 模式
      • 调用 _start_dns_verify
      • 这里用到了著名的 DNS API 机制。源码中会 source 导入 dnsapi/ 目录下的对应脚本(例如 dns_cf.sh)。
      • 执行 dns_myapi_add 函数,调用云厂商 API 添加 TXT 记录。

D. 触发 CA 验证 (Trigger Challenge)

  • 源码对应_respondToChallenge
  • 逻辑
    • 当你把文件放好,或者 TXT 记录加好后,acme.sh 会向 CA 发送一个 POST 请求,说:“我准备好了,你来查吧”。

E. 轮询状态 (Polling Status)

  • 源码对应_check_authz_status
  • 逻辑
    • CA 的验证是异步的。脚本进入一个 while 循环。
    • 每隔几秒查询一次验证状态 (pending -> processing -> validinvalid)。
    • 如果变更为 valid,则进入下一步;如果 invalid,则报错退出。

F. 生成与提交 CSR (Finalize Order)

  • 源码对应_createCSR, _finalizeOrder
  • 逻辑
    • 生成 CSR:调用 _createCSR。这里 acme.sh 直接调用系统的 openssl 命令生成私钥(如果需要)和 CSR 文件。
    • 提交:将 CSR 内容通过 _post 发送到之前获取的 finalize 接口。

G. 下载证书 (Download Cert)

  • 源码对应_downloadCert
  • 逻辑
    • 一旦 Order 状态变为 valid,CA 会提供证书下载链接。
    • 脚本下载 PEM 格式的证书,并处理证书链(Fullchain)。

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 函数调用。

它最大的技术亮点在于:

  1. 纯 Shell 实现 JWS 签名:绕过了编程语言库的依赖。
  2. 文本处理黑魔法:用基础 Linux 命令处理复杂的 JSON 交互。
  3. 插件系统:通过 source 机制实现了强大的 DNS API 扩展性。

如果你想学习 Shell 编程的极限,阅读 acme.sh_jws 函数和 _post 函数会让你大开眼界。