From d04535784434fd40088987bc49ef3fe0a31321f3 Mon Sep 17 00:00:00 2001 From: JGZYES Date: Fri, 14 Aug 2026 12:51:01 +0800 Subject: [PATCH] =?UTF-8?q?session=20ticket=20key=20=E8=BD=AE=E6=8D=A2:=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=202=20=E4=BB=A3=20key(paze=5Ftls=5Fconfig=5F?= =?UTF-8?q?rotate=5Fticket=5Fkey,=20=E5=BD=93=E5=89=8D=E8=BD=AC=E4=B8=8A?= =?UTF-8?q?=E4=B8=80=E4=BB=A3+=E6=96=B0key=E8=A3=85=E5=BD=93=E5=89=8D,=20?= =?UTF-8?q?=E5=86=99=E7=9B=982x32),=20=E9=AA=8C=E8=AF=81=E7=94=A8=E6=89=80?= =?UTF-8?q?=E6=9C=89=E5=B0=B1=E7=BB=AAkey=E9=81=8D=E5=8E=86MAC=E4=B8=8D?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E6=A7=BD=E4=BD=8D;=20pazessl=20s=5Fserver=20?= =?UTF-8?q?-rotate=5Fticket=5Fkey;=20=E9=AA=8C=E8=AF=81:=20=E8=BD=AE?= =?UTF-8?q?=E6=8D=A2=E5=90=8E=E6=97=A7=E4=BC=9A=E8=AF=9D=E4=BB=8D=E6=81=A2?= =?UTF-8?q?=E5=A4=8D+=E6=96=B0=E4=BC=9A=E8=AF=9D=E8=B7=A8=E8=BF=9B?= =?UTF-8?q?=E7=A8=8B=E6=81=A2=E5=A4=8D,=20=E6=97=A2=E6=9C=89=E5=9B=9E?= =?UTF-8?q?=E5=BD=92=E6=97=A0=E5=BD=B1=E5=93=8D;=20STATUS/AGENT/CHANGELOG?= =?UTF-8?q?=20=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENT.MD | 4 +++ CHANGELOG.md | 5 +++ STATUS.md | 1 + apps/pazessl/main.c | 5 ++- include/paze/tls.h | 5 +++ src/ssl/tls_conn.c | 65 ++++++++++++++++++++++++++++++--------- src/ssl/tls_handshake13.c | 32 ++++++++++--------- src/ssl/tls_internal.h | 8 +++-- 8 files changed, 92 insertions(+), 33 deletions(-) diff --git a/AGENT.MD b/AGENT.MD index 3c5b551..9cd0730 100644 --- a/AGENT.MD +++ b/AGENT.MD @@ -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 `,端到端验证(重启后 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`,验证:轮换后旧会话仍恢复 + 新会话跨进程恢复。 + diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d2ae78..1ee711c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `。端到端验证:重启服务端(同 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 字节,常量时间标量乘;非整字节对齐,标量乘按大端位遍历)。 diff --git a/STATUS.md b/STATUS.md index e618c46..acf25ba 100644 --- a/STATUS.md +++ b/STATUS.md @@ -34,6 +34,7 @@ - ✅ **secp384r1(TLS 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 双向交叉验证 - ✅ **secp521r1(TLS 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_SHA512(0x0603),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 ` 端到端验证(重启后 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(✅ 完成) diff --git a/apps/pazessl/main.c b/apps/pazessl/main.c index 42ccdd1..de40de9 100644 --- a/apps/pazessl/main.c +++ b/apps/pazessl/main.c @@ -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 diff --git a/include/paze/tls.h b/include/paze/tls.h index 203b7a1..2977b33 100644 --- a/include/paze/tls.h +++ b/include/paze/tls.h @@ -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); diff --git a/src/ssl/tls_conn.c b/src/ssl/tls_conn.c index 86da316..df333a1 100644 --- a/src/ssl/tls_conn.c +++ b/src/ssl/tls_conn.c @@ -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; } diff --git a/src/ssl/tls_handshake13.c b/src/ssl/tls_handshake13.c index f8a8aa7..ddff6c1 100644 --- a/src/ssl/tls_handshake13.c +++ b/src/ssl/tls_handshake13.c @@ -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)); diff --git a/src/ssl/tls_internal.h b/src/ssl/tls_internal.h index 2c6e987..89fdfde 100644 --- a/src/ssl/tls_internal.h +++ b/src/ssl/tls_internal.h @@ -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。