ACME 协议流程与AllinSSL 的关系(三)

结合 route_acme.py(ACME 协议逻辑)和 route_dns.py(DNS 钩子逻辑)代码,我们可以把证书申请流程拆解成一个完整的"柜台办证"过程。

在这个过程中,你的代码扮演的是发证机关(CA 厂商)的角色,而 AllinSSL 扮演的是办证人(客户端)


第一阶段:初始化(进店咨询)

1. /directory ------ 咨询服务台
  • 功能作用:这是 AllinSSL 访问你的第一站。它像一张"服务导览图",告诉客户端后续每个步骤该去哪个 URL 办理。
  • 代码定位 :返回了 newNoncenewOrder 等关键接口的地址。
2. /new-nonce ------ 领取防伪号牌
  • 功能作用:ACME 协议为了防范重放攻击(防止别人截获你的报文再次发送),要求每个请求都带上一个随机数(Nonce)。
  • 代码定位 :你的代码通过 Responseheaders 返回了一个 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 里)发给你,求你盖章。
  • 代码定位
    1. 解析 CSR:代码从 Base64 编码中提取出 CSR。
    2. 动态签名 :调用 _sign_csr_to_pem 函数,用你伪造的 _ca_key 给这个 CSR 盖章,生成真正的 .pem 格式证书字符串。
    3. 缓存证书 :把生成的证书存入 _cert_store 内存字典中,并给出一个下载地址。
9. /cert/{cert_id} ------ 窗口领证
  • 功能作用:AllinSSL 顺着上一步给的地址,把签发好的证书下载回去。
  • 代码定位 :从 _cert_store 内存中取出 PEM 字符串并返回。

总结:你的代码逻辑串联

  1. route_acme.py 负责流程调度
    • 它设定了规矩(ACME 协议)。
    • 它负责盖章 (使用 cryptography 库进行 RSA 签名)。
  2. route_dns.py 负责辅助执行
    • 它是 AllinSSL 为了完成 route_acme.py 布置的任务(DNS 验证)而调用的工具接口。

代码内容

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"
        }
    }
相关推荐
网管NO.12 小时前
OpenClaw 多模型配置完整教程(WSL2 + Ubuntu)
运维·网络·人工智能·ubuntu
bai_lan_ya2 小时前
Linux 输入系统应用编程完全指南
linux·运维·服务器
漠月瑾-西安2 小时前
Cookie Secure 属性:守护网络传输安全的关键防线
网络安全·https·web开发·安全配置·cookie安全·会话保护
skywalk81632 小时前
参考paddlex的图像识别和目标检测,做一个精简的寻物小助手的推理服务器后台
服务器·人工智能·目标检测
昵称只能一个月修改一次。。。2 小时前
ARM基本知识
网络
坚定的共产主义生产设备永不宕机3 小时前
OSPF实操配置
网络·智能路由器
思茂信息3 小时前
CST软件加载 Pin 二极管的可重构电桥仿真研究
服务器·开发语言·人工智能·php·cst·电磁仿真·电磁辐射
FightingHg3 小时前
和claude、openclaw交互的一些杂七杂八记录
linux·运维·服务器
深念Y3 小时前
魅蓝Note5 Root + 改内核激活命名空间:让Docker跑在安卓上
android·linux·服务器·docker·容器·root·服务