每次使用 HTTPS,浏览器都会在毫秒级完成一次 TLS 握手。TLS 1.3(RFC 8446)是这套协议的最新版本,相比 1.2 做了大刀阔斧的简化。本文用 不到 500 行 C 从零实现一次完整的 TLS 1.3 握手——不链接 OpenSSL,4 个密码学模块自己写,最后连上真正的 TLS 1.3 服务端拿到 HTTP 200。
完整代码:GitHub - examples/tls13/ | 下载 zip
1. 为什么要从零写 TLS 1.3
TLS 1.3 vs 1.2:关键变化
TLS 1.3 相比 1.2 有四个根本性改进:
| 特性 | TLS 1.2 | TLS 1.3 |
|---|---|---|
| 握手延迟 | 2-RTT | 1-RTT(首次),0-RTT(恢复) |
| 密钥交换 | RSA / DHE / ECDHE 可选 | ECDHE 必选(强制前向保密) |
| 对称加密 | CBC+HMAC / GCM / CCM | 仅 AEAD(GCM / CCM / ChaCha20-Poly1305) |
| 密码套件 | 数十种(含 RC4、3DES 等已知弱套件) | 5 种(全部安全) |
TLS 1.2 允许 RSA 静态密钥交换——服务端私钥泄漏即可解密所有历史会话。TLS 1.3 砍掉了这一模式,所有连接都使用临时椭圆曲线密钥交换(ECDHE),即使长期密钥泄漏也无法追溯解密,这就是前向保密(Forward Secrecy)。
TLS 1.2 的 CBC+HMAC 组合容易遭受 Padding Oracle 攻击(详见 CBC 攻击),TLS 1.3 彻底移除了非 AEAD 模式。
我们的目标
用 C 写一个 TLS 1.3 客户端:
- 一个
tls13.c搞定握手全流程,4 个独立密码学模块 - 只支持一个密码套件:
TLS_AES_128_GCM_SHA256(0x1301) - 只支持一条曲线:
X25519 - 不依赖任何第三方库,从 TCP socket 开始
- 能对接标准 TLS 1.3 实现(OpenSSL s_server),完成握手并发送 HTTP 请求
⚠️ 教学代码,不可用于生产环境。 我们跳过了证书链验证、0-RTT、PSK 等关键安全机制。
2. TLS 1.3 握手全景
TLS 1.3 的 1-RTT 握手分为三个阶段:
明文阶段
- Client → Server:ClientHello — 支持的密码套件、X25519 公钥、TLS 版本
- Server → Client:ServerHello — 选定的密码套件、Server 的 X25519 公钥
此时双方都能计算出 ECDHE 共享密钥,派生出 handshake traffic keys。
Handshake 加密阶段(使用 handshake keys 加密)
- Server → Client:{EncryptedExtensions}
- Server → Client:{Certificate}
- Server → Client:{CertificateVerify}
- Server → Client:{Finished} — 验证整个握手的完整性
- Client → Server:[Finished]
Application 加密阶段(切换到 application keys)
- 双向传输应用层数据(HTTP、gRPC 等)
{} 表示用 server handshake key
加密,[] 表示用 client handshake key 加密。
Record Layer
所有 TLS 消息都封装在 Record 中:
ContentType (1B) | ProtocolVersion (2B: 0x0303) | Length (2B) | Fragment
TLS 1.3 的 encrypted record 外层始终标记为
ApplicationData (23),真正的 content type
藏在解密后的 payload 末尾。
Handshake 消息格式
HandshakeType (1B) | Length (3B) | Body
我们会用到的类型:ClientHello (1)、ServerHello (2)、EncryptedExtensions (8)、Certificate (11)、CertificateVerify (15)、Finished (20)。
3. 密码学积木——逐个击破
实现 TLS 1.3 需要四块积木:哈希(SHA-256)、密钥交换(X25519)、密钥派生(HKDF)、认证加密(AES-128-GCM)。
3.1 X25519 ECDHE 密钥交换
X25519(Curve25519 上的 Diffie-Hellman)是 TLS 1.3 最常用的密钥交换算法。核心操作是标量乘法:给定私钥标量 \(k\) 和曲线上的点 \(u\),计算 \(k \times u\)。
安全性来自椭圆曲线离散对数问题(ECDLP):已知公钥 \(A = a \times G\),在计算上无法反推出私钥 \(a\)。
Montgomery Ladder
Curve25519 使用 Montgomery 形式,只需要 \(x\) 坐标即可完成计算。标量乘法通过 Montgomery Ladder 实现——遍历私钥的每一位,执行 conditional swap + differential addition:
static void x25519_scalar_mult(uint8_t out[32],
const uint8_t scalar[32],
const uint8_t point[32]) {
fe u, x1, x2, z2, x3, z3;
fe_unpack(u, point);
fe_copy(x1, u); /* x_1 = u */
fe_one(x2); fe_zero(z2); /* (x_2, z_2) = (1, 0) */
fe_copy(x3, u); fe_one(z3); /* (x_3, z_3) = (u, 1) */
uint8_t k[32];
memcpy(k, scalar, 32);
k[0] &= 248; k[31] &= 127; k[31] |= 64; /* clamp */
int swap = 0;
for (int t = 254; t >= 0; t--) {
int kt = (k[t >> 3] >> (t & 7)) & 1;
swap ^= kt;
fe_cswap(x2, x3, swap);
fe_cswap(z2, z3, swap);
swap = kt;
/* ... differential addition & doubling ... */
}
/* out = x2 * z2^(p-2) */
}clamp 操作确保标量具有正确的位结构:清除低 3
位(保证被 8 整除,cofactor
safety)、设置最高位(防止时序侧信道)、清除最高位(保证
< \(2^{255}\))。
我们的实现使用 5×51-bit limb 表示 \(\mathbb{F}_p\) 中的元素(\(p = 2^{255} - 19\)),完整代码约 295 行,通过 RFC 7748 §6.1 的两个测试向量。
3.2 HKDF 密钥派生
TLS 1.3 的密钥调度是一棵精心设计的树,所有加密密钥都从 ECDHE 共享密钥这个根推导而来。
整个密钥调度基于两个原语:
- HKDF-Extract(salt, IKM) → PRK:从输入密钥材料中提取固定长度的伪随机密钥
- HKDF-Expand(PRK, info, L) → OKM:从 PRK 扩展出任意长度的密钥
TLS 1.3 在上层封装了两个函数:
/* HKDF-Expand-Label 构造特定的 info 结构 */
void hkdf_expand_label(const uint8_t *secret,
const char *label,
const uint8_t *context, size_t ctx_len,
uint8_t *out, size_t out_len) {
uint8_t info[512];
size_t p = 0;
/* HkdfLabel = length(2) + "tls13 " + label + context */
put_u16(info, (uint16_t)out_len); p += 2;
size_t llen = 6 + strlen(label); /* "tls13 " prefix */
info[p++] = (uint8_t)llen;
memcpy(info + p, "tls13 ", 6); p += 6;
memcpy(info + p, label, strlen(label)); p += strlen(label);
info[p++] = (uint8_t)ctx_len;
if (ctx_len > 0) { memcpy(info + p, context, ctx_len); p += ctx_len; }
hkdf_expand(secret, 32, info, p, out, out_len);
}
/* Derive-Secret = HKDF-Expand-Label(secret, label, transcript_hash) */
void derive_secret(const uint8_t *secret, const char *label,
const uint8_t *transcript_hash, uint8_t *out) {
hkdf_expand_label(secret, label, transcript_hash, 32, out, 32);
}密钥派生流程:
Early Secret = HKDF-Extract(0, PSK=0) // 无 PSK 时为全零
↓ Derive-Secret("derived", "")
Handshake Secret = HKDF-Extract(derived, ECDHE_shared_secret)
├─→ client_handshake_traffic_secret ──→ key + iv
├─→ server_handshake_traffic_secret ──→ key + iv
↓ Derive-Secret("derived", "")
Master Secret = HKDF-Extract(derived, 0)
├─→ client_application_traffic_secret ──→ key + iv
└─→ server_application_traffic_secret ──→ key + iv
每个 traffic secret 通过 HKDF-Expand-Label 提取出 16 字节 AES 密钥和 12 字节 IV。
3.3 AES-128-GCM(AEAD)
AES-GCM 是一种认证加密(AEAD),同时提供保密性和完整性。它由两部分组成:
- AES-CTR:用递增计数器生成密钥流,与明文 XOR 得到密文
- GHASH:在 \(\operatorname{GF}(2^{128})\) 上计算认证标签,覆盖 AAD + 密文
void aes128_gcm_encrypt(const aes128_ctx *ctx,
const uint8_t nonce[12],
const uint8_t *aad, size_t aad_len,
const uint8_t *plain, size_t plain_len,
uint8_t *cipher, uint8_t tag[16]) {
/* 1. H = AES(K, 0^128) */
uint8_t H[16] = {0};
aes128_encrypt_block(ctx, H, H);
/* 2. AES-CTR with initial counter = 2 */
aes_ctr(ctx, nonce, 2, plain, cipher, plain_len);
/* 3. GHASH(H, AAD, cipher) */
ghash(H, aad, aad_len, cipher, plain_len, tag);
/* 4. Tag = GHASH_result XOR AES(K, nonce||0x00000001) */
uint8_t E0[16];
/* ... counter=1 block encryption and XOR ... */
}在 TLS 1.3 中,每条记录的 nonce 由 IV 和序列号 XOR 构成:
uint8_t nonce[12];
memcpy(nonce, iv, 12);
for (int i = 0; i < 8; i++)
nonce[4 + i] ^= (uint8_t)(seq >> (56 - 8 * i));AAD(Additional Authenticated Data)是 Record 头的 5 个字节。解密时,如果 GHASH 标签不匹配,整条记录将被拒绝——这杜绝了 CBC Padding Oracle 攻击的可能。
3.4 SHA-256
SHA-256 是整个 TLS 1.3 密钥体系的底座——HMAC、HKDF、Transcript Hash 全部依赖它。我们实现的是标准的 Merkle-Damgård 结构,遵循 FIPS 180-4:
- 消息按 512-bit 分块
- 每块通过 64 轮压缩函数更新 8 个 32-bit 状态变量
- 最后一块添加 1-bit padding + 64-bit 长度
实现约 130 行,通过 NIST 标准测试向量验证。
4. 核心握手代码走读
有了四块密码学积木,完整的握手逻辑就是把它们按 RFC 8446 规定的顺序串起来。
以下代码片段均为
tls13.c的裁剪版,省略了错误处理和部分重复逻辑,只保留关键路径。完整可编译代码见 examples/tls13/tls13.c。
4.1 TCP 连接 & ClientHello
/* TCP connect */
struct addrinfo hints = {.ai_socktype = SOCK_STREAM}, *res;
getaddrinfo(hostname, port, &hints, &res);
sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
connect(sock, res->ai_addr, res->ai_addrlen);ClientHello 是客户端发送的第一条消息,它需要声明:
- 支持的 TLS 版本(通过
supported_versions扩展,设为 0x0304 即 TLS 1.3) - 支持的密码套件(
TLS_AES_128_GCM_SHA256) - 临时 X25519 公钥(通过
key_share扩展) - 支持的签名算法(
signature_algorithms扩展) - 服务端域名(
server_name扩展,即 SNI)
static void send_client_hello(const char *hostname,
uint8_t priv[32],
uint8_t *ch_buf, size_t *ch_len) {
uint8_t pub[32];
/* 从 /dev/urandom 生成 X25519 临时私钥 */
FILE *f = fopen("/dev/urandom", "rb");
fread(priv, 1, 32, f); fclose(f);
x25519_base(pub, priv); /* 计算公钥 pub = priv × G */
uint8_t msg[512];
size_t p = 0;
/* Handshake type + length 占位(4 字节) */
size_t hs_start = p; p += 4;
put_u16(msg + p, TLS_VERSION_12); p += 2; /* legacy version */
/* 32 字节随机数(完整代码从 /dev/urandom 读取) */
/* fread(msg + p, 1, 32, rng) */ p += 32;
/* session_id(中间件兼容) */
msg[p++] = 32; memset(msg + p, 0xAB, 32); p += 32;
/* cipher_suites: 只有一个 */
put_u16(msg + p, 2); p += 2;
put_u16(msg + p, TLS_AES_128_GCM_SHA256); p += 2;
/* compression: null */
msg[p++] = 1; msg[p++] = 0;
/* ── Extensions ── */
/* supported_versions: TLS 1.3 */
/* key_share: X25519 public key (32 bytes) */
/* supported_groups: X25519 */
/* signature_algorithms: ECDSA + RSA-PSS + RSA-PKCS1 */
/* server_name (SNI) */
/* ... (每个扩展都是 type(2) + length(2) + data) */
}每条发出的 Handshake 消息都要加入 transcript hash——一个跨越整个握手的 SHA-256 滚动哈希。后续的密钥派生全部依赖它。
4.2 解析 ServerHello
ServerHello 的结构与 ClientHello
类似。我们需要提取的关键信息是 server 在
key_share 扩展中返回的 X25519 公钥:
static void recv_server_hello(uint8_t server_pub[32]) {
uint8_t type, buf[16384];
int len = record_recv(&type, buf, sizeof(buf));
sha256_update(&transcript, buf, len); /* 加入 transcript */
/* 跳过固定字段,解析 extensions */
size_t p = 4 + 2 + 32; /* header + version + random */
p += 1 + buf[p]; /* session_id */
p += 2 + 1; /* cipher_suite + compression */
size_t ext_len = get_u16(buf + p); p += 2;
size_t ext_end = p + ext_len;
while (p < ext_end) {
uint16_t etype = get_u16(buf + p); p += 2;
uint16_t elen = get_u16(buf + p); p += 2;
if (etype == EXT_KEY_SHARE) {
p += 4; /* group(2) + key_len(2) */
memcpy(server_pub, buf + p, 32);
return;
}
p += elen;
}
}4.3 派生 Handshake Keys
收到 ServerHello 后,双方都有了对方的 X25519 公钥,可以计算共享密钥并派生 handshake traffic keys:
static void derive_handshake_keys(const uint8_t priv[32],
const uint8_t server_pub[32]) {
/* ECDHE 共享密钥 */
uint8_t shared[32];
x25519(shared, priv, server_pub);
/* Early Secret (无 PSK) */
uint8_t zeros[32] = {0}, early_secret[32];
hkdf_extract(NULL, 0, zeros, 32, early_secret);
/* derived → Handshake Secret */
uint8_t empty_hash[32], derived[32];
sha256(NULL, 0, empty_hash);
derive_secret(early_secret, "derived", empty_hash, derived);
hkdf_extract(derived, 32, shared, 32, hs_secret);
/* Transcript hash(到 ServerHello 为止) */
sha256_ctx tmp = transcript;
uint8_t th[32];
sha256_final(&tmp, th);
/* Traffic secrets → key + iv */
derive_secret(hs_secret, "c hs traffic", th, c_hs_secret);
derive_secret(hs_secret, "s hs traffic", th, s_hs_secret);
uint8_t key_buf[16];
hkdf_expand_label(s_hs_secret, "key", NULL, 0, key_buf, 16);
aes128_init(&s_key, key_buf);
hkdf_expand_label(s_hs_secret, "iv", NULL, 0, s_iv, 12);
/* ... 同理 client side ... */
}4.4 解密握手消息 & 验证 Server Finished
从这里开始,server 发来的所有消息都用
server_handshake_traffic_secret
加密。我们需要逐条解密并加入 transcript:
static int decrypt_record(uint8_t *inner_type,
uint8_t *plain, size_t *plain_len) {
uint8_t type, buf[16384 + 256];
int len = record_recv(&type, buf, sizeof(buf));
/* 构造 nonce: IV XOR sequence_number */
uint8_t nonce[12];
memcpy(nonce, s_iv, 12);
for (int i = 0; i < 8; i++)
nonce[4 + i] ^= (uint8_t)(s_seq >> (56 - 8 * i));
s_seq++;
/* AAD = record header (5 bytes) */
uint8_t aad[5] = {CT_APPLICATION_DATA, 0x03, 0x03, len>>8, len};
aes128_gcm_decrypt(&s_key, nonce, aad, 5,
buf, len - 16, plain, buf + len - 16);
/* 去除填充,找到真正的 content type */
size_t plen = len - 16;
while (plen > 0 && plain[plen-1] == 0) plen--;
*inner_type = plain[--plen];
*plain_len = plen;
return 0;
}解密后的握手消息流:EncryptedExtensions → Certificate → CertificateVerify → Finished。前三个直接加入 transcript,重点是验证 Server Finished:
/* Server Finished = HMAC(finished_key, transcript_hash) */
uint8_t finished_key[32];
hkdf_expand_label(s_hs_secret, "finished", NULL, 0, finished_key, 32);
sha256_ctx tmp = transcript; /* transcript 到 CertificateVerify 为止 */
uint8_t th[32];
sha256_final(&tmp, th);
uint8_t expected[32];
hmac_sha256(finished_key, 32, th, 32, expected);
if (memcmp(plain + off + 4, expected, 32) != 0) {
/* 握手被篡改!中止连接。*/
exit(1);
}这一步是 TLS 1.3 握手完整性的核心保障:如果任何一条握手消息被篡改,transcript hash 就会不同,Finished 验证就会失败。
但要注意:Finished 验证只能证明对端持有正确的握手密钥、且 transcript 没被篡改;它不能证明”对端就是你想连接的那个网站”。 身份认证依赖于 Certificate + CertificateVerify——服务端用自己的私钥签署 transcript hash,客户端验证签名链直到受信任的根 CA。跳过这一步,中间人攻击者可以用自己的密钥对完成整个握手,Finished 一样能通过。
简化说明:我们跳过了证书链验证和 CertificateVerify 签名校验。生产环境中这两步必须完成,否则 TLS 握手的身份认证形同虚设。
4.5 发送 Client Finished
static void send_client_finished(void) {
uint8_t finished_key[32];
hkdf_expand_label(c_hs_secret, "finished", NULL, 0, finished_key, 32);
sha256_ctx tmp = transcript;
uint8_t th[32];
sha256_final(&tmp, th);
uint8_t verify_data[32];
hmac_sha256(finished_key, 32, th, 32, verify_data);
uint8_t msg[36];
msg[0] = HT_FINISHED;
put_u24(msg + 1, 32);
memcpy(msg + 4, verify_data, 32);
encrypt_send(CT_HANDSHAKE, msg, 36);
sha256_update(&transcript, msg, 36);
}注意:Client Finished 用的是
client_handshake_traffic_secret 加密,但 derive
application keys 时使用的 transcript hash 只到
Server Finished 为止(不包含 Client
Finished)。这是 RFC 8446 §7.1 的明确要求。
⚠️ 最容易踩的坑:如果你在发送 Client Finished 之后才取 transcript hash 去派生 application keys,hash 里会多出 Client Finished 这条消息,导出的 key/iv 与服务端不一致,后续所有应用数据都会解密失败。这是笔者实际调试时遇到的第一个”握手成功但应用数据全挂”的 bug——症状是 AES-GCM tag 校验报错,但根因在密钥派生的输入不对。正确做法是在
send_client_finished()之前就把 transcript hash 快照下来。
4.6 切换到 Application Keys
/* 在发送 Client Finished 之前,保存 transcript hash */
sha256_ctx sf_ctx = transcript; /* transcript 到 Server Finished */
uint8_t sf_hash[32];
sha256_final(&sf_ctx, sf_hash);
send_client_finished(); /* 用 handshake keys 加密发送 */
/* 从 Master Secret 派生 application keys */
derive_app_keys(sf_hash);derive_app_keys 沿着 Key Schedule
继续向下:
static void derive_app_keys(const uint8_t sf_transcript_hash[32]) {
uint8_t empty_hash[32], derived[32], master_secret[32];
sha256(NULL, 0, empty_hash);
derive_secret(hs_secret, "derived", empty_hash, derived);
uint8_t zeros[32] = {0};
hkdf_extract(derived, 32, zeros, 32, master_secret);
uint8_t s_app_secret[32], c_app_secret[32];
derive_secret(master_secret, "s ap traffic", sf_transcript_hash, s_app_secret);
derive_secret(master_secret, "c ap traffic", sf_transcript_hash, c_app_secret);
/* 从 traffic secrets 提取 key + iv,重新初始化加密上下文 */
/* ... */
}握手完成!此后发送一个 HTTP GET 验证连接可用:
char http_req[256];
int http_len = snprintf(http_req, sizeof(http_req),
"GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", hostname);
encrypt_send(CT_APPLICATION_DATA, (uint8_t *)http_req, http_len);4.7 工程上的”杂音消息”
实际对接时,你还会遇到两类协议规范之外的”干扰”:
- ChangeCipherSpec 兼容记录(content type
20)——TLS 1.3 已经废弃了 CCS,但为了穿透某些只认 TLS 1.2
流量模式的中间件(middlebox),服务端可能在 ServerHello
之后发一条空的 CCS。我们的
decrypt_record直接跳过它。 - NewSessionTicket(handshake type 4)——握手完成后,服务端通常会立刻用 application keys 加密发送一到两条 session ticket,用于后续连接的 PSK 快速恢复。它们的外层 content type 是 23(ApplicationData),解密后 inner type 是 22(Handshake),很容易和真正的应用数据混淆。我们的做法是循环读取,跳过所有非 ApplicationData 的 inner type,直到拿到 HTTP 响应。
这两个问题在 RFC 里都有交代(§5.1 CCS 兼容、§4.6.1 NewSessionTicket),但如果你只照着握手流程图写代码,十有八九会在这里卡住。
5. 跑起来
编译
cd examples/tls13
make # 编译 tls13 和 test_crypto
make test # 运行 14 个 RFC 测试向量所有密码学原语通过以下测试向量验证:
- SHA-256:NIST FIPS 180-4(3 个用例)
- HMAC-SHA256:RFC 4231
- X25519:RFC 7748 §6.1(2 个用例)
- HKDF:RFC 5869 Appendix A
- AES-128-GCM:NIST SP 800-38D Test Case 3(加密、解密、篡改检测)
- TLS 1.3 HKDF-Expand-Label:RFC 8448
运行
由于我们跳过了证书验证,直接连公网站点(如 google.com:443)握手能成功,但无法确认对端身份——这恰恰印证了第 4.4 节的讨论。因此我们用本地 OpenSSL s_server 做端到端验证,它是一个标准的 TLS 1.3 实现,足以证明协议逻辑的正确性:
# 生成自签名证书
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
-keyout key.pem -out cert.pem -days 365 -nodes \
-subj "/CN=localhost"
# 启动服务端
openssl s_server -accept 14433 -cert cert.pem -key key.pem \
-tls1_3 -www另一个终端运行我们的客户端:
./tls13 localhost 14433输出:
[+] TCP connected to localhost:14433
[+] ClientHello sent (199 bytes)
[+] ServerHello received
[+] Handshake keys derived
[+] Server Finished verified
[+] Client Finished sent
[+] Application keys derived
[+] HTTP request sent
HTTP/1.0 200 ok
Content-type: text/html
<HTML><BODY BGCOLOR="#ffffff">
<pre>
s_server -accept 14433 -cert cert.pem -key key.pem -tls1_3 -www
Secure Renegotiation IS NOT supported
...
[+] TLS 1.3 handshake complete!
HTTP 200——握手成功,应用数据正常收发。
6. 总结 & 延伸
我们简化了什么
| 简化项 | 生产环境要求 |
|---|---|
| 跳过证书验证 | 必须验证证书链到受信根 CA + CertificateVerify 签名 |
| 不支持 0-RTT | Early Data 可进一步降低首次连接延迟 |
| 不支持 PSK | Session Resumption 减少完整握手开销 |
| 不支持 HelloRetryRequest | 密码套件协商失败时的重试机制 |
| 不支持 Post-handshake 认证 | 客户端证书(mTLS)在握手后请求 |
| 固定一个密码套件 | 协商支持多种密码套件和曲线 |
仅 read/write 系统调用 |
需要非阻塞 I/O、超时、重试、缓冲区管理 |
代码量统计
| 模块 | 文件 | 行数 | 说明 |
|---|---|---|---|
| SHA-256 | sha256.c | 128 | FIPS 180-4 |
| X25519 | x25519.c | 295 | RFC 7748,Montgomery Ladder |
| HKDF | hkdf.c | 142 | HMAC-SHA256 + HKDF + Derive-Secret |
| AES-128-GCM | aes_gcm.c | 325 | AES 核心 + GCM (CTR + GHASH) |
| 握手逻辑 | tls13.c | 459 | Record Layer + 6 个握手函数(261 行) + main |
| 测试 | test_crypto.c | 216 | 14 个 RFC 测试向量 |
tls13.c 的 459
行中,核心握手函数(send_client_hello →
derive_app_keys)占 261
行,去掉空行和注释后净逻辑约 180 行;其余是 Record Layer I/O
和 main。
延伸阅读
- CBC Padding Oracle 攻击 — 理解为什么 TLS 1.3 只保留 AEAD
- 一次一密(OTP) — 密码学第一课:信息论安全
- 后量子密码学简介 — TLS 1.3 的未来:Kyber/ML-KEM 后量子密钥封装可以直接嵌入 key_share 扩展
- mTLS 双向认证实战 — 从应用层使用 TLS:客户端证书、证书轮转
参考资料
- RFC 8446 - The Transport Layer Security (TLS) Protocol Version 1.3
- RFC 7748 - Elliptic Curves for Security (X25519, X448)
- RFC 5869 - HMAC-based Extract-and-Expand Key Derivation Function (HKDF)
- NIST SP 800-38D - Recommendation for Block Cipher Modes of Operation: GCM
- RFC 8448 - Example Handshake Traces for TLS 1.3