cocoon/acme.h
xfy911 f38effff49
Some checks failed
CI / build (push) Failing after 26s
feat: ACME 配置集成 + 自动续期后台协程
- cocoon_config_t 新增 ACME 字段(enabled, directory_url, email, domains,
  cert_path, key_path, renew_days),支持最多 8 个域名
- config.c: JSON 解析 acme 配置块、校验逻辑、config_merge 支持
- main.c: 新增 --acme 命令行开关
- server.c: server_start 启动 ACME 自动续期后台协程(检查周期 24h,
  提前 renew_days 触发重新签发)
- acme.c: 实现 acme_save_certificate 证书持久化、
  acme_cert_days_until_expiry 过期天数检查
- acme.h: 新增 save_certificate / cert_days_until_expiry 接口声明
- 新增 tests/unit/test_acme_config.c:覆盖配置解析、校验、合并、
  证书过期检查、证书保存共 6 项测试
- 修复 test_config.c 中 config_merge 调用以匹配新签名
2026-06-16 12:45:08 +08:00

292 lines
8.4 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* acme.h - ACME / Let's Encrypt 自动证书模块接口
*
* 实现 RFC 8555 ACME 协议客户端,支持 HTTP-01 挑战。
* 自动完成证书签发和续期,无需手动配置。
*
* 设计:
* - 独立 ACME 客户端上下文,管理账户、订单、证书
* - JWS 签名使用 OpenSSL EVP 接口
* - HTTP-01 挑战响应通过回调集成到服务器
* - 后台协程定时检查证书过期并自动续期
*
* @author xfy
*/
#ifndef COCOON_ACME_H
#define COCOON_ACME_H
#include <stdbool.h>
#include <stddef.h>
#include <time.h>
/* ACME 目录端点结构 */
typedef struct {
char newNonce[512];
char newAccount[512];
char newOrder[512];
char revokeCert[512];
char keyChange[512];
} acme_directory_t;
/* ACME 账户信息 */
typedef struct {
char kid[512]; /* 账户 URL (Key ID) */
char contact[256]; /* 联系邮箱 */
bool termsAgreed; /* 是否同意服务条款 */
} acme_account_t;
/* ACME 授权 */
typedef struct {
char domain[256]; /* 授权域名 */
char status[32]; /* pending / valid / invalid / deactivated / expired / revoked */
char token[256]; /* HTTP-01 挑战 token */
char uri[512]; /* 授权 URI */
} acme_authz_t;
/* ACME 订单 */
typedef struct {
char uri[512]; /* 订单 URI */
char status[32]; /* pending / ready / processing / valid / invalid */
char finalize[512]; /* finalize 端点 */
char certificate[512]; /* 证书下载端点 */
time_t expires; /* 过期时间 */
acme_authz_t *authz; /* 授权数组 */
size_t num_authz; /* 授权数量 */
} acme_order_t;
/* ACME 客户端上下文 */
typedef struct acme_ctx acme_ctx_t;
/**
* acme_create - 创建 ACME 客户端上下文
*
* @param directory_url ACME 目录 URL如 Let's Encrypt 生产环境或测试环境)
* @param account_key_pem 账户私钥 PEM 字符串EC P-256NULL 则自动生成
* @return ACME 上下文,失败返回 NULL
*/
acme_ctx_t *acme_create(const char *directory_url, const char *account_key_pem);
/**
* acme_destroy - 销毁 ACME 客户端上下文
*
* @param ctx ACME 上下文
*/
void acme_destroy(acme_ctx_t *ctx);
/**
* acme_get_directory - 获取 ACME 服务器目录
*
* 向 directory_url 发送 GET 请求,解析返回的 JSON 目录。
*
* @param ctx ACME 上下文
* @param out 输出目录结构
* @return 0 成功,-1 失败
*/
int acme_get_directory(acme_ctx_t *ctx, acme_directory_t *out);
/**
* acme_get_nonce - 获取新的 nonce
*
* ACME 协议要求每个 POST 请求都包含一个 Replay-Nonce。
*
* @param ctx ACME 上下文
* @param nonce_url newNonce 端点 URL
* @return 0 成功,-1 失败
*/
int acme_get_nonce(acme_ctx_t *ctx, const char *nonce_url);
/**
* acme_create_account - 创建或查找 ACME 账户
*
* 如果账户已存在(服务器返回 200则复用现有账户。
*
* @param ctx ACME 上下文
* @param email 联系邮箱(如 "admin@example.com"
* @param terms_agreed 是否同意服务条款
* @return 0 成功,-1 失败
*/
int acme_create_account(acme_ctx_t *ctx, const char *email, bool terms_agreed);
/**
* acme_create_order - 创建证书订单
*
* @param ctx ACME 上下文
* @param domains 域名数组
* @param num_domains 域名数量
* @param out 输出订单结构(调用者负责释放 out->authz
* @return 0 成功,-1 失败
*/
int acme_create_order(acme_ctx_t *ctx, const char **domains, size_t num_domains,
acme_order_t *out);
/**
* acme_order_free - 释放订单结构中的动态内存
*
* @param order 订单结构
*/
void acme_order_free(acme_order_t *order);
/**
* acme_fetch_authz - 获取授权详情和挑战
*
* @param ctx ACME 上下文
* @param authz 授权结构(输入 uri输出 token 等)
* @return 0 成功,-1 失败
*/
int acme_fetch_authz(acme_ctx_t *ctx, acme_authz_t *authz);
/**
* acme_respond_challenge - 响应 HTTP-01 挑战
*
* 通知 ACME 服务器可以验证挑战了。
*
* @param ctx ACME 上下文
* @param authz 授权结构
* @return 0 成功,-1 失败
*/
int acme_respond_challenge(acme_ctx_t *ctx, const acme_authz_t *authz);
/**
* acme_poll_authz - 轮询授权状态直到完成或失败
*
* @param ctx ACME 上下文
* @param authz 授权结构
* @param timeout_ms 超时毫秒
* @return 0 成功valid-1 失败
*/
int acme_poll_authz(acme_ctx_t *ctx, acme_authz_t *authz, int timeout_ms);
/**
* acme_finalize_order - 发送 CSR 完成订单
*
* @param ctx ACME 上下文
* @param order 订单结构
* @param domains 域名数组
* @param num_domains 域名数量
* @return 0 成功,-1 失败
*/
int acme_finalize_order(acme_ctx_t *ctx, acme_order_t *order,
const char **domains, size_t num_domains);
/**
* acme_poll_order - 轮询订单状态直到证书就绪
*
* @param ctx ACME 上下文
* @param order 订单结构
* @param timeout_ms 超时毫秒
* @return 0 成功valid-1 失败
*/
int acme_poll_order(acme_ctx_t *ctx, acme_order_t *order, int timeout_ms);
/**
* acme_download_certificate - 下载签发的证书链
*
* @param ctx ACME 上下文
* @param cert_url 证书下载 URL
* @param out_pem 输出 PEM 证书链(调用者负责 free
* @return 0 成功,-1 失败
*/
int acme_download_certificate(acme_ctx_t *ctx, const char *cert_url, char **out_pem);
/**
* acme_get_thumbprint - 获取账户密钥的 JWK Thumbprint
*
* HTTP-01 挑战响应内容 = token + "." + base64url(thumbprint)
*
* @param ctx ACME 上下文
* @param out 输出 thumbprintbase64url 编码,调用者负责 free
* @return 0 成功,-1 失败
*/
int acme_get_thumbprint(acme_ctx_t *ctx, char **out);
/**
* acme_get_keyauth - 获取 HTTP-01 挑战的 keyAuthorization
*
* keyAuthorization = token + "." + base64url(JWK thumbprint)
* RFC 8555 §8.3
*
* @param ctx ACME 上下文
* @param token 挑战 token
* @param out 输出 keyAuthorization调用者负责 free
* @return 0 成功,-1 失败
*/
int acme_get_keyauth(acme_ctx_t *ctx, const char *token, char **out);
/**
* acme_issue_certificate - 一键签发证书(完整流程)
*
* 封装目录发现 → 创建账户 → 创建订单 → 挑战 → 下载证书 的完整流程。
* 此函数会阻塞,需要在协程中调用。
*
* @param ctx ACME 上下文
* @param domains 域名数组
* @param num_domains 域名数量
* @param email 联系邮箱
* @param cert_pem 输出证书 PEM调用者负责 free
* @param key_pem 输出私钥 PEM调用者负责 free
* @return 0 成功,-1 失败
*/
int acme_issue_certificate(acme_ctx_t *ctx, const char **domains, size_t num_domains,
const char *email, char **cert_pem, char **key_pem);
/**
* acme_save_certificate - 将证书和私钥保存到文件
*
* @param cert_pem 证书 PEM 字符串
* @param key_pem 私钥 PEM 字符串
* @param cert_path 证书文件路径
* @param key_path 私钥文件路径
* @return 0 成功,-1 失败
*/
int acme_save_certificate(const char *cert_pem, const char *key_pem,
const char *cert_path, const char *key_path);
/**
* acme_cert_days_until_expiry - 检查证书剩余有效天数
*
* @param cert_path 证书文件路径
* @return 剩余天数,<0 表示证书不存在或已过期0 表示今天过期
*/
int acme_cert_days_until_expiry(const char *cert_path);
/* HTTP 请求辅助(内部使用,暴露给单元测试) */
typedef struct {
int status;
char *body;
size_t body_len;
char location[512];
char replay_nonce[256];
} acme_http_response_t;
/**
* acme_http_post_jws - 发送带 JWS 签名的 POST 请求
*
* @param ctx ACME 上下文
* @param url 请求 URL
* @param payload JSON payload可为 NULL 表示空)
* @param out 输出响应
* @return 0 成功,-1 失败
*/
int acme_http_post_jws(acme_ctx_t *ctx, const char *url, const char *payload,
acme_http_response_t *out);
/**
* acme_http_get - 发送 GET 请求
*
* @param ctx ACME 上下文
* @param url 请求 URL
* @param out 输出响应
* @return 0 成功,-1 失败
*/
int acme_http_get(acme_ctx_t *ctx, const char *url, acme_http_response_t *out);
/**
* acme_http_response_free - 释放 HTTP 响应资源
*
* @param resp 响应结构
*/
void acme_http_response_free(acme_http_response_t *resp);
#endif /* COCOON_ACME_H */