session ticket key 轮换: 支持 2 代 key(paze_tls_config_rotate_ticket_key, 当前转上一代+新key装当前, 写盘2x32), 验证用所有就绪key遍历MAC不依赖槽位; pazessl s_server -rotate_ticket_key; 验证: 轮换后旧会话仍恢复+新会话跨进程恢复, 既有回归无影响; STATUS/AGENT/CHANGELOG 更新

这个提交包含在:
JGZYES
2026-08-14 12:51:01 +08:00
父节点 225a339a0e
当前提交 d045357844
修改 8 个文件,包含 92 行新增33 行删除
+4
查看文件
@@ -82,3 +82,7 @@ STATUS.md 完成度总表 AGENT.md 本文件
session ticket key 支持持久化到文件:新增 `paze_tls_config_set_session_ticket_file`(设文件路径)与 `paze_tls_ensure_ticket_key`(首次生成时读取/写盘;PSK 验证入口 `server_try_psk` 亦调用以加载),实现服务端**跨进程重启**后会话恢复仍然有效。`pazessl s_server -ticket_file <file>`,端到端验证(重启后 resumed=YES)。
### 6. session ticket key 轮换(✅ 已完成 2026-08-14
支持 2 代 ticket key:新增 `paze_tls_config_rotate_ticket_key`(当前 key 转上一代、新 key 装当前,写盘 2×32 字节);验证用所有已就绪 key 遍历匹配 MAC(不依赖 ticket 内槽位),轮换后旧 ticket 仍由上一代 key 平滑验证。`pazessl s_server -rotate_ticket_key`,验证:轮换后旧会话仍恢复 + 新会话跨进程恢复。
+5
查看文件
@@ -9,6 +9,11 @@
- session ticket key:新增 `paze_tls_config_set_session_ticket_file`(持久化文件)与 `paze_tls_ensure_ticket_key`(首次生成时读取/写盘,PSK 验证入口亦加载),服务端**跨进程重启**后 ticket 仍可校验、会话恢复继续有效。
- CLI`pazessl s_server -ticket_file <file>`。端到端验证:重启服务端(同 ticket 文件)后 `resumed=YES`
### 新增 —— session ticket key 轮换
- 支持 2 代 ticket key:新增 `paze_tls_config_rotate_ticket_key`(当前 key 转上一位、新 key 装当前;写盘 2×32 字节);验证用所有已就绪 key 遍历匹配 MAC,轮换后旧 ticket 仍由上一代 key 平滑验证。
- CLI`pazessl s_server -rotate_ticket_key`。验证:轮换后旧会话仍恢复 + 新会话跨进程恢复。
### 新增 —— TLS secp521r1 (P-521) 完整链路
- 新增 P-521 椭圆曲线:`src/crypto/p521.c` + `include/paze/p521.h`(域 p = 2^521-1,标量/仿射点 66/133 字节,常量时间标量乘;非整字节对齐,标量乘按大端位遍历)。
+1
查看文件
@@ -34,6 +34,7 @@
-**secp384r1TLS P-384**:新增 P-384 曲线点运算(`p384.c`,Jacobian,常量时间标量乘);ECDSA 支持 P-384 签名/验签(RFC 6979 确定性 k,`paze_ecdsa_p384_*`);X.509 支持 secp384r1 证书解析/验签/签发(含 ecdsa-with-SHA384 与 P-384 公钥,`paze_x509_build_spki384`);TLS 1.3 支持 secp384r1 组(key_share/supported_groups/ECDH 按 SEC1 x 坐标)、签名算法 ECDSA_SECP384R1_SHA384;TLS 1.2 ECDHE 亦支持 P-384。`s_client -key_share P-384` 端到端验证(含 HRR 触发与 P-384 证书链/主机名验证);曲线与 ECDSA 经 OpenSSL 双向交叉验证
-**secp521r1TLS P-521**:新增 P-521 曲线点运算(`p521.c`,域 p=2^521-1,标量/点 66/133 字节,常量时间标量乘);ECDSA 支持 P-521 签名/验签(RFC 6979,配 SHA-512,`paze_ecdsa_p521_*`);X.509 支持 secp521r1 证书解析/验签/签发(含 ecdsa-with-SHA512,`paze_x509_build_spki521`);TLS 1.3 支持 secp521r1 组与签名算法 ECDSA_SECP521R1_SHA5120x0603),TLS 1.2 ECDHE 亦支持。`s_client -key_share P-521` 端到端验证(HRR + P-521 证书链/主机名验证,应用数据收发);曲线(n·G=∞)、ECDSA 双向、X.509 证书均经 OpenSSL 交叉验证
-**TLS 1.3 会话恢复持久化**session ticket key 支持持久化到文件(`paze_tls_config_set_session_ticket_file` + `paze_tls_ensure_ticket_key`),服务端跨进程重启后 ticket 仍可校验、PSK 会话恢复继续有效;`pazessl s_server -ticket_file <file>` 端到端验证(重启后 resumed=YES
-**session ticket key 轮换**:支持 2 代 ticket key`paze_tls_config_rotate_ticket_key`),轮换后旧 ticket 仍由上一代 key 验证(MAC 遍历匹配,平滑过渡);`s_server -rotate_ticket_key` 验证(轮换后旧会话仍恢复 + 新会话跨进程恢复)
-**修复**ECDSA `bits2int` 输入短于群阶时错误左移(影响 P-384+SHA-256 验签);TLS 1.3/1.2 对端证书 `issuer_dn/subject_dn` 等指针指向被记录层复用缓冲导致的链验证失败(`paze_tls_store_peer_cert` 拷贝 DER
## PazeSSH — SSH-2(✅ 完成)
+4 -1
查看文件
@@ -73,7 +73,7 @@ static int usage(void) {
"Usage:\n"
" pazessl s_client -connect host:port [-CAfile roots.pem] [-servername SNI] [-tls1_2]\n"
" [-sess_in file] [-sess_out file] [-msg text] [-early] [-key_share X25519|P-256|P-384|P-521]\n"
" pazessl s_server -accept port -cert cert.pem -key key.pem [-tls1_2] [-reuse N] [-early] [-ticket_file f]\n"
" pazessl s_server -accept port -cert cert.pem -key key.pem [-tls1_2] [-reuse N] [-early] [-ticket_file f] [-rotate_ticket_key]\n"
" pazessl verify -CAfile roots.pem cert.pem\n"
" pazessl genrsa -out key.pem [2048]\n"
" pazessl req -x509 -newkey rsa:2048 -keyout k.pem -out c.pem -days D -subj /CN=X\n"
@@ -379,6 +379,7 @@ int main(int argc, char **argv) {
int only_12 = 0, do_early = 0;
int reuse = 1;
const char *ticketfile = NULL;
int do_rotate = 0;
for (int i = 2; i < argc; i++) {
if (strcmp(argv[i], "-accept") == 0 && i + 1 < argc) port = atoi(argv[++i]);
else if (strcmp(argv[i], "-cert") == 0 && i + 1 < argc) certf = argv[++i];
@@ -387,6 +388,7 @@ int main(int argc, char **argv) {
else if (strcmp(argv[i], "-tls1_2") == 0) only_12 = 1;
else if (strcmp(argv[i], "-early") == 0) do_early = 1;
else if (strcmp(argv[i], "-ticket_file") == 0 && i + 1 < argc) ticketfile = argv[++i];
else if (strcmp(argv[i], "-rotate_ticket_key") == 0) do_rotate = 1;
else if (strcmp(argv[i], "-v") == 0) paze_log_set_level(PAZE_LOG_DEBUG);
}
if (!certf || !keyf) {
@@ -407,6 +409,7 @@ int main(int argc, char **argv) {
paze_tls_config_set_cert_chain(cfg, (const char *)cdata, (const char *)kdata);
paze_tls_config_set_verify(cfg, PAZE_TLS_VERIFY_NONE, NULL, NULL);
if (ticketfile) paze_tls_config_set_session_ticket_file(cfg, ticketfile);
if (do_rotate) paze_tls_config_rotate_ticket_key(cfg, NULL); /* 启动时轮换当前 key */
/* Listen */
#ifdef _WIN32
+5
查看文件
@@ -255,6 +255,11 @@ paze_status_t paze_tls_config_set_session_ticket_key(paze_tls_config_t *cfg,
paze_status_t paze_tls_config_set_session_ticket_file(paze_tls_config_t *cfg,
const char *path);
/* 服务端:轮换 session ticket key(当前 key 变为上一代、新 key 装为当前;
* 上一代仍可短暂验证旧 ticket,实现平滑过渡)。newkey 为 NULL 时随机生成。 */
paze_status_t paze_tls_config_rotate_ticket_key(paze_tls_config_t *cfg,
const uint8_t newkey[32]);
paze_tls_t *paze_tls_new(const paze_tls_config_t *cfg);
void paze_tls_free(paze_tls_t *tls);
+50 -15
查看文件
@@ -294,21 +294,37 @@ int paze_tls_session_resumed(const paze_tls_t *tls) {
return tls ? (tls->psk_mode && tls->handshake_done) : 0;
}
/* 服务端:导出 session ticket key(位于 cfg,cfg_owned 在 paze_tls_new 时拷贝) */
/* 服务端:导出 session ticket key(位于 cfg;返回已就绪组数与当前(第 0)组) */
paze_status_t paze_tls_get_session_ticket_key(const paze_tls_t *tls,
uint8_t key[32], int *ready) {
if (!tls || !key || !ready) return PAZE_ERR_INVALID;
paze_tls_config_t *cfg = tls->cfg_owned ? tls->cfg_owned : (paze_tls_config_t *)tls->cfg;
*ready = cfg->ticket_key_ready;
if (cfg->ticket_key_ready) memcpy(key, cfg->ticket_key, 32);
*ready = cfg->ticket_keys > 0;
if (cfg->ticket_keys > 0) memcpy(key, cfg->ticket_key[0], 32);
return PAZE_OK;
}
paze_status_t paze_tls_config_set_session_ticket_key(paze_tls_config_t *cfg,
const uint8_t key[32]) {
if (!cfg || !key) return PAZE_ERR_INVALID;
memcpy(cfg->ticket_key, key, 32);
cfg->ticket_key_ready = 1;
memcpy(cfg->ticket_key[0], key, 32);
cfg->ticket_keys = 1;
return PAZE_OK;
}
/* 轮换:当前 key 移到上一代,新 key 装为当前。两代均就绪可同时验证旧 ticket。 */
paze_status_t paze_tls_config_rotate_ticket_key(paze_tls_config_t *cfg,
const uint8_t newkey[32]) {
if (!cfg) return PAZE_ERR_INVALID;
paze_tls_ensure_ticket_key(cfg); /* 先加载持久化的当前 key,再轮换 */
if (cfg->ticket_keys > 0)
memcpy(cfg->ticket_key[1], cfg->ticket_key[0], 32);
if (newkey) memcpy(cfg->ticket_key[0], newkey, 32);
else {
if (paze_random(cfg->ticket_key[0], 32) != PAZE_OK) return PAZE_ERR_PLATFORM;
}
if (cfg->ticket_keys < 2) cfg->ticket_keys++;
paze_tls_persist_ticket_keys(cfg);
return PAZE_OK;
}
@@ -321,23 +337,42 @@ paze_status_t paze_tls_config_set_session_ticket_file(paze_tls_config_t *cfg,
return PAZE_OK;
}
/* 服务端首次需要 ticket key 时调用:若配置了持久化文件则加载/生成并写盘。 */
/* 把已就绪的 ticket key 组写盘(2×32 字节) */
void paze_tls_persist_ticket_keys(paze_tls_config_t *cfg) {
if (!cfg->ticket_key_file[0]) return;
FILE *f = fopen(cfg->ticket_key_file, "wb");
if (!f) return;
uint8_t blob[64];
memcpy(blob, cfg->ticket_key[0], 32);
memcpy(blob + 32, cfg->ticket_key[0], 32); /* 仅 1 组时第 2 组同 current */
if (cfg->ticket_keys >= 2) memcpy(blob + 32, cfg->ticket_key[1], 32);
fwrite(blob, 1, sizeof(blob), f);
fclose(f);
}
/* 服务端首次需要 ticket key 时调用:若配置了持久化文件则加载,否则生成并写盘。 */
paze_status_t paze_tls_ensure_ticket_key(paze_tls_config_t *cfg) {
if (cfg->ticket_key_ready) return PAZE_OK;
if (cfg->ticket_keys > 0) return PAZE_OK;
if (cfg->ticket_key_file[0]) {
FILE *f = fopen(cfg->ticket_key_file, "rb");
if (f) {
size_t n = fread(cfg->ticket_key, 1, 32, f);
uint8_t blob[64];
size_t n = fread(blob, 1, sizeof(blob), f);
fclose(f);
if (n == 32) { cfg->ticket_key_ready = 1; return PAZE_OK; }
if (n >= 32) {
memcpy(cfg->ticket_key[0], blob, 32);
cfg->ticket_keys = 1;
if (n == 64 && memcmp(blob + 32, blob, 32) != 0) {
memcpy(cfg->ticket_key[1], blob + 32, 32);
cfg->ticket_keys = 2;
}
return PAZE_OK;
}
}
}
if (paze_random(cfg->ticket_key, 32) != PAZE_OK) return PAZE_ERR_PLATFORM;
cfg->ticket_key_ready = 1;
if (cfg->ticket_key_file[0]) {
FILE *f = fopen(cfg->ticket_key_file, "wb");
if (f) { fwrite(cfg->ticket_key, 1, 32, f); fclose(f); }
}
if (paze_random(cfg->ticket_key[0], 32) != PAZE_OK) return PAZE_ERR_PLATFORM;
cfg->ticket_keys = 1;
paze_tls_persist_ticket_keys(cfg);
return PAZE_OK;
}
+18 -14
查看文件
@@ -1111,33 +1111,36 @@ static int server_try_psk(paze_tls_t *tls, const uint8_t *exts, size_t exts_len,
return 0;
}
/* 校验 ticket 内部格式与 MAC */
/* 校验 ticket 内部格式与 MAC(结构: VER || kidx || psk_hash || psk_len || psk || time4 || mac) */
paze_tls_config_t *cfgc = tls->cfg_owned ? tls->cfg_owned : (paze_tls_config_t *)tls->cfg;
if (idlen < 3 + 1 + 4 + PAZE_TLS_TICKET_MAC_LEN) { PAZE_LOGD("psk: id too short %zu", idlen); return 0; }
if (idlen < 4 + 1 + 4 + PAZE_TLS_TICKET_MAC_LEN) { PAZE_LOGD("psk: id too short %zu", idlen); return 0; }
if (id[0] != PAZE_TLS_TICKET_VER) { PAZE_LOGD("psk: bad ver %d", id[0]); return 0; }
size_t pl = id[2];
if (idlen != 3 + pl + 4 + PAZE_TLS_TICKET_MAC_LEN) { PAZE_LOGD("psk: id len mismatch %zu vs %zu", idlen, 3 + pl + 4 + PAZE_TLS_TICKET_MAC_LEN); return 0; }
size_t pl = id[3];
if (idlen != 4 + pl + 4 + PAZE_TLS_TICKET_MAC_LEN) { PAZE_LOGD("psk: id len mismatch %zu vs %zu", idlen, 4 + pl + 4 + PAZE_TLS_TICKET_MAC_LEN); return 0; }
if (pl == 0 || pl > sizeof(tls->psk)) { PAZE_LOGD("psk: bad pl %zu", pl); return 0; }
/* 跨进程重启后 ticket_key 初始未就绪:若配置了持久化文件则从文件加载 */
paze_tls_ensure_ticket_key(cfgc);
if (!cfgc->ticket_key_ready) { PAZE_LOGD("psk: ticket_key not ready"); return 0; }
uint8_t mac[32];
paze_hmac(paze_hash_sha256(), cfgc->ticket_key, 32, id, 3 + pl + 4, mac);
if (paze_ct_equal(mac, id + 3 + pl + 4, PAZE_TLS_TICKET_MAC_LEN) != 0) {
PAZE_LOGD("psk: ticket MAC mismatch");
return 0;
if (cfgc->ticket_keys == 0) { PAZE_LOGD("psk: ticket_key not ready"); return 0; }
/* 用所有已就绪 key 试 MAC(支持轮换后旧 ticket 仍由上一代 key 验证,
* 不依赖 ticket 里的 kidx 槽位,避免轮换导致槽位错位) */
int mac_ok = 0;
for (int k = 0; k < cfgc->ticket_keys; k++) {
uint8_t mac[32];
paze_hmac(paze_hash_sha256(), cfgc->ticket_key[k], 32, id, 4 + pl + 4, mac);
if (paze_ct_equal(mac, id + 4 + pl + 4, PAZE_TLS_TICKET_MAC_LEN) == 0) { mac_ok = 1; break; }
}
uint32_t lifetime = paze_load32_be(id + 3 + pl);
if (!mac_ok) { PAZE_LOGD("psk: ticket MAC mismatch"); return 0; }
uint32_t lifetime = paze_load32_be(id + 4 + pl);
if (lifetime == 0 || (uint32_t)(int32_t)time(NULL) - lifetime >= PAZE_TLS_TICKET_LIFETIME) {
PAZE_LOGD("psk: ticket expired (lifetime=%u now=%d)", lifetime, (int32_t)time(NULL));
return 0;
}
/* 提取 PSK 与关联 hash */
tls->psk_hash = id[1];
tls->psk_hash = id[2];
if (tls->psk_hash != 0 && tls->psk_hash != 1) { PAZE_LOGD("psk: bad hash %d", tls->psk_hash); return 0; }
tls->psk_len = pl;
memcpy(tls->psk, id + 3, pl);
memcpy(tls->psk, id + 4, pl);
/* 验证 binder:
* binder = HMAC(binder_key, Hash(CH 去掉 binders 列表))
@@ -1205,12 +1208,13 @@ static paze_status_t send_new_session_ticket(paze_tls_t *tls) {
if (paze_tls_ensure_ticket_key(cfgc) != PAZE_OK) return PAZE_ERR_PLATFORM;
uint8_t body[512]; size_t bp = 0;
body[bp++] = PAZE_TLS_TICKET_VER;
body[bp++] = 0; /* kidx:使用第 0 组(当前)ticket key 签发 */
body[bp++] = (uint8_t)tls->psk_hash;
body[bp++] = (uint8_t)psk_len;
memcpy(body + bp, psk, psk_len); bp += psk_len;
paze_store32_be(body + bp, (uint32_t)time(NULL)); bp += 4; /* 签发时间 */
uint8_t mac[32];
paze_hmac(paze_hash_sha256(), cfgc->ticket_key, 32, body, bp, mac);
paze_hmac(paze_hash_sha256(), cfgc->ticket_key[0], 32, body, bp, mac);
memcpy(body + bp, mac, PAZE_TLS_TICKET_MAC_LEN); bp += PAZE_TLS_TICKET_MAC_LEN;
size_t tlen = bp;
paze_memzero(psk, sizeof(psk));
+5 -3
查看文件
@@ -138,11 +138,11 @@ struct paze_tls_config {
/* 客户端:要用于会话恢复的 PSK 会话(值拷贝) */
paze_tls_session_t session;
/* 服务端:session ticket 加密密钥(首次使用生成)。
/* 服务端:session ticket 加密密钥(定长 2 组,支持轮换;第 0 组为当前签发用)。
* cfg( paze_tls_config_set_session_ticket_key)
* */
uint8_t ticket_key[32];
int ticket_key_ready;
uint8_t ticket_key[2][32];
int ticket_keys; /* 已就绪的 key 组数(1 或 2) */
/* 若非空:session ticket key 持久化到该文件(服务端首次生成时读取/写盘),
* 使 */
char ticket_key_file[1024];
@@ -336,6 +336,8 @@ paze_status_t paze_tls_bio_write_all(paze_tls_t *tls, const uint8_t *buf, size_t
/* 确保 session ticket key 就绪:若配置了持久化文件则加载/生成并写盘 */
paze_status_t paze_tls_ensure_ticket_key(paze_tls_config_t *cfg);
/* 把已就绪的 ticket key 组写盘(2×32 字节) */
void paze_tls_persist_ticket_keys(paze_tls_config_t *cfg);
/* ---- tls_record.c ---- */
/* 读取并(若已加密)解密下一条 record。