结合 route_acme.py(ACME 协议逻辑)和 route_dns.py(DNS 钩子逻辑)代码,我们可以把证书申请流程拆解成一个完整的"柜台办证"过程。
在这个过程中,你的代码扮演的是发证机关(CA 厂商)的角色,而 AllinSSL 扮演的是办证人(客户端)。
第一阶段:初始化(进店咨询)
1. /directory ------ 咨询服务台
- 功能作用:这是 AllinSSL 访问你的第一站。它像一张"服务导览图",告诉客户端后续每个步骤该去哪个 URL 办理。
- 代码定位 :返回了
newNonce、newOrder等关键接口的地址。
2. /new-nonce ------ 领取防伪号牌
- 功能作用:ACME 协议为了防范重放攻击(防止别人截获你的报文再次发送),要求每个请求都带上一个随机数(Nonce)。
- 代码定位 :你的代码通过
Response的headers返回了一个Replay-Nonce。AllinSSL 拿到这个号牌后,才能在下一步请求里"盖章"签名。
3. /new-account ------ 注册会员
- 功能作用:AllinSSL 告诉 CA:"我是谁,这是我的账号公钥,以后我说的话都用我的私钥签名。"
- 代码定位 :你的代码返回了状态
valid(通过),并在 Header 中给了它一个账号 ID(account/1)。
第二阶段:下单与审核(提交申请)
4. /new-order ------ 填写办证申请表
- 功能作用 :AllinSSL 正式提交申请,说明要为哪个域名(
www.liu.com)办证。 - 代码定位 :
- 关键动作 :代码从请求体里解码出
identifiers(域名信息),并原样返回。 - 后续指引 :接口返回了两个非常重要的后续链接:
authorizations(去哪证明域名是你的)和finalize(审核通过后去哪领证)。
- 关键动作 :代码从请求体里解码出
5. /authz/{auth_id} ------ 领取身份挑战任务
- 功能作用:发证机关(你)给申请人(AllinSSL)出题。
- 代码定位 :你告诉 AllinSSL:"请使用
dns-01方式验证,并在 DNS 里填入我给你的这个token。"
第三阶段:证明与验证(自证清白)
6. /dnswebhook (在 route_dns.py 中) ------ 贴公告
- 功能作用 :这是 AllinSSL 平台在收到你给的
token后,转头来调用这个接口,让你帮它把"公告"贴到 DNS 上。 - 代码定位 :它接收
action(创建/删除)和value(验证码),并返回code: 200。虽然你这里只是模拟打印了日志,但 AllinSSL 只要看到成功返回,就会认为公告已经贴好了。
7. /chall/{chall_id} ------ 申请查看结果
- 功能作用:AllinSSL 贴完公告后,回来敲门:"任务做完了,你们快去查公告栏(DNS)吧!"
- 代码定位 :你的代码直接返回了
status: valid。这就相当于你根本没去查公告栏,直接给它开了"绿灯"。
第四阶段:签发与领证(打印证件)
8. /finalize/{order_id} ------ 盖章签发
- 功能作用 :这是最核心的一步。验证通过后,AllinSSL 会把它的证书公钥(封装在 CSR 里)发给你,求你盖章。
- 代码定位 :
- 解析 CSR:代码从 Base64 编码中提取出 CSR。
- 动态签名 :调用
_sign_csr_to_pem函数,用你伪造的_ca_key给这个 CSR 盖章,生成真正的.pem格式证书字符串。 - 缓存证书 :把生成的证书存入
_cert_store内存字典中,并给出一个下载地址。
9. /cert/{cert_id} ------ 窗口领证
- 功能作用:AllinSSL 顺着上一步给的地址,把签发好的证书下载回去。
- 代码定位 :从
_cert_store内存中取出 PEM 字符串并返回。
总结:你的代码逻辑串联
route_acme.py负责流程调度 :- 它设定了规矩(ACME 协议)。
- 它负责盖章 (使用
cryptography库进行 RSA 签名)。
route_dns.py负责辅助执行 :- 它是 AllinSSL 为了完成
route_acme.py布置的任务(DNS 验证)而调用的工具接口。
- 它是 AllinSSL 为了完成
代码内容
route_acme.py
python
import json
import base64
import datetime
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from fastapi import APIRouter, Request, Response
from src.model.result import Result
from src.utils.logger import get_logger
logger = get_logger()
router = APIRouter(tags=["acme"])
# --- 配置区 ---
BASE_URL = "https://came.pool7.yun100.cn"
ACME_PREFIX = "/acme"
FULL_BASE = f"{BASE_URL}{ACME_PREFIX}"
# --- 伪 CA 密钥(模块加载时生成一次,重启后不变签发历史会失效)---
_ca_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
_ca_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Fake CA")])
# 内存证书存储:order_id -> cert PEM str
_cert_store: dict[str, str] = {}
def _sign_csr_to_pem(csr: x509.CertificateSigningRequest) -> str:
"""用伪 CA 密钥对 CSR 签发证书,返回 PEM 字符串"""
now = datetime.datetime.utcnow()
cert = (
x509.CertificateBuilder()
.subject_name(csr.subject)
.issuer_name(_ca_name)
.public_key(csr.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now)
.not_valid_after(now + datetime.timedelta(days=365))
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
.sign(_ca_key, hashes.SHA256())
)
return cert.public_bytes(serialization.Encoding.PEM).decode()
# --- ACME 协议实现 ---
@router.get("/directory")
async def directory():
"""ACME 入口点:所有路径都带上 /acme 前缀"""
logger.info("ACME directory endpoint called")
return {
"newNonce": f"{FULL_BASE}/new-nonce",
"newAccount": f"{FULL_BASE}/new-account",
"newOrder": f"{FULL_BASE}/new-order",
"finalize": f"{FULL_BASE}/finalize",
"newAuthz": f"{FULL_BASE}/new-authz",
"meta": {"termsOfService": "http://example.com/tos"}
}
@router.head("/new-nonce")
@router.get("/new-nonce")
async def get_nonce():
return Response(
status_code=204, # No Content
headers={
"Replay-Nonce": "static-nonce-for-allinssl",
"Cache-Control": "no-store",
"Link": '<https://came.pool7.yun100.cn/acme/directory>;rel="index"' # 顺便返回索引链接
}
)
@router.post("/new-account")
async def new_account(response: Response):
logger.info("ACME new account endpoint called")
response.headers["Location"] = f"{FULL_BASE}/account/1"
return {"status": "valid"}
@router.post("/new-order")
async def create_order(request: Request):
# 1. 获取 lego 发过来的 JWS 请求体
body = await request.json()
# 2. 解码 payload (ACME 协议中 payload 是 base64url 编码的)
payload_b64 = body.get("payload", "")
# 注意:这里需要处理 base64url 的填充问题
missing_padding = len(payload_b64) % 4
if missing_padding:
payload_b64 += '=' * (4 - missing_padding)
payload_json = json.loads(base64.urlsafe_b64decode(payload_b64))
requested_identifiers = payload_json.get("identifiers", [])
logger.info(f"Client requested order for: {requested_identifiers}")
# 3. 构造符合 RFC 8555 标准的响应
order_data = {
"status": "pending",
"expires": "2026-12-31T23:59:59Z",
"identifiers": requested_identifiers, # 重点:这里必须和请求的一模一样
"authorizations": [
f"{FULL_BASE}/authz/some-random-id"
],
"finalize": f"{FULL_BASE}/finalize/some-random-id"
}
return Response(
content=json.dumps(order_data),
status_code=201, # ACME 创建资源通常返回 201
headers={
"Replay-Nonce": "next-nonce-value", # 别忘了每个响应都要给新 Nonce
"Location": "https://came.pool7.yun100.cn/acme/order/some-order-id"
}
)
@router.post("/authz/{auth_id}")
async def get_authorization(auth_id: str):
logger.info(f"Checking authorization for ID: {auth_id}")
# 构造响应数据
# 注意:identifier 的 value 最好和你 new-order 时的一致
auth_data = {
"status": "pending",
"expires": "2026-12-31T23:59:59Z",
"identifier": {
"type": "dns",
"value": "www.liu.com"
},
"challenges": [
{
"type": "dns-01", # 告诉 lego 使用 DNS 验证
"status": "pending",
"url": f"https://came.pool7.yun100.cn/acme/chall/{auth_id}", # 下一个坑位
"token": "this-is-a-fake-token-for-validation"
}
]
}
return Response(
content=json.dumps(auth_data),
status_code=200,
headers={
"Replay-Nonce": "even-more-nonces", # 核心:每个 POST 响应都要给新 Nonce
"Content-Type": "application/json"
}
)
@router.post("/chall/{chall_id}")
async def challenge_ack(chall_id: str):
"""当 allinssl 配置完 DNS 后来这里触发验证"""
logger.info(f"ACME challenge endpoint called, chall_id={chall_id}")
# 既然是伪造适配器,我们直接返回有效,让流程继续
return {"status": "valid"}
@router.post("/finalize/{order_id}")
async def finalize(order_id: str, request: Request):
"""关键环节:解析 AllinSSL 提交的 CSR,动态签发证书"""
logger.info(f"ACME finalize endpoint called, order_id={order_id}")
body = await request.json()
# 1. 解码 JWS payload,提取 CSR
payload_b64 = body.get("payload", "")
missing = len(payload_b64) % 4
if missing:
payload_b64 += '=' * (4 - missing)
payload = json.loads(base64.urlsafe_b64decode(payload_b64))
csr_b64 = payload.get("csr", "")
missing = len(csr_b64) % 4
if missing:
csr_b64 += '=' * (4 - missing)
csr_der = base64.urlsafe_b64decode(csr_b64)
# 2. 解析 CSR,用 CSR 里的公钥签发证书
csr = x509.load_der_x509_csr(csr_der)
cert_pem = _sign_csr_to_pem(csr)
# 3. 存入内存,供 get_cert 接口取用
_cert_store[order_id] = cert_pem
logger.info(f"证书已签发并缓存,order_id={order_id}")
return {
"status": "valid",
"certificate": f"{FULL_BASE}/cert/{order_id}"
}
@router.get("/cert/{cert_id}")
@router.post("/cert/{cert_id}")
async def get_cert(cert_id: str):
"""返回 finalize 阶段动态签发的证书"""
logger.info(f"ACME certificate download endpoint called, cert_id={cert_id}")
cert_pem = _cert_store.get(cert_id)
if not cert_pem:
logger.error(f"证书未找到,cert_id={cert_id}")
return Response(status_code=404, content="cert not found")
return Response(content=cert_pem, media_type="application/pem-certificate-chain")
@router.get("/test", response_model=Result)
async def test():
try:
return Result.success("successs, hehe")
except Exception as e:
logger.error(f"Collector endpoint error: {e}", exc_info=True)
return Result.fail(str(e))
route_dns.py
python
from fastapi import APIRouter, Request, Response
from src.utils.logger import get_logger
logger = get_logger()
router_dns = APIRouter(tags=["dns"])
@router_dns.post("/dnswebhook")
async def dns_web_hook(request: Request):
# 1. 获取 AllinSSL 发过来的数据
try:
data = await request.json()
except Exception:
return {"code": 400, "msg": "Invalid JSON"}
action = data.get("action") # create 或 delete
domain = data.get("full_domain") # _acme-challenge.www.liu.com
value = data.get("value") # 验证值
# 2. 打印日志(方便你观察 AllinSSL 到底传了什么)
if action == "create":
logger.info(f"[AllinSSL] 收到创建请求: 域名={domain}, 记录值={value}")
elif action == "delete":
logger.info(f"[AllinSSL] 收到删除请求: 域名={domain}")
# 3. 核心:返回 AllinSSL 预期的成功格式
# 大多数平台只要看到 code 200 或 success 就会继续下一步
return {
"code": 200,
"msg": "success",
"data": {
"status": "done"
}
}