TLSv1.2 客户端加密完整示例解析
1. 计算client端的共享密钥ECDH 算法协商secp256r1shareKey client_privateKey * server_publicKey client_privateKey * server_privateKey *G上图的就是server_publicKey只需要代入client私钥按照secp256r1算法就能得到共享密钥这个有开源现成的库可以计算 本着学习的目的 可以直接手算 // 这里需要了解椭圆曲线的加密原理基础才能理解。椭圆曲线的基本原理不复杂 而且应用非常广泛 有很多变种 在区块链等领域广泛应用。# ----------------------------# 你的密钥# ----------------------------CLIENT_PRIV_HEX 508a5e1df188b3826e31e9515e79417815302197bffb946cbdff7afa2c0b5d02SERVER_PUB_HEX 04083ecbf3dd57b4974283a5b86b8f1511fbe87c430590d79b1698b96343c919f8ae74da9c879619239a8832862820b0cc0cc5d0ad6490684a157a5651583e9aa7# ----------------------------# secp256r1 (NIST P-256) 椭圆曲线参数# ----------------------------p 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFa 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFCb 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604BGx 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296Gy 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162CBCE6B8E1F612886EF3DB3791549063n 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551# ----------------------------# 点加法、点倍乘ECDSA核心# ----------------------------def point_add(x1, y1, x2, y2):if x1 0 and y1 0:return (x2, y2)if x2 0 and y2 0:return (x1, y1)if x1 x2 and (y1 ! y2 or y1 0):return (0, 0)if x1 x2:m (3 * x1 * x1 a) * pow(2 * y1, p-2, p) % pelse:m (y2 - y1) * pow(x2 - x1, p-2, p) % px3 (m * m - x1 - x2) % py3 (m * (x1 - x3) - y1) % preturn (x3, y3)def point_mul(x, y, scalar):rx, ry 0, 0while scalar 0:if scalar 1:rx, ry point_add(rx, ry, x, y)x, y point_add(x, y, x, y)scalar 1return (rx, ry)# ----------------------------# 解析服务器公钥# ----------------------------pub_bytes bytes.fromhex(SERVER_PUB_HEX)qx int.from_bytes(pub_bytes[1:33], byteorderbig)qy int.from_bytes(pub_bytes[33:65], byteorderbig)# ----------------------------# 私钥# ----------------------------priv int(CLIENT_PRIV_HEX, 16)# ----------------------------# 计算共享密钥 私钥 × 服务端公钥# ----------------------------sx, sy point_mul(qx, qy, priv)# ----------------------------# 输出结果32字节 X坐标# ----------------------------shared_secret sx.to_bytes(32, byteorderbig).hex()print(✅ ECDH 共享密钥 , shared_secret)///////////////////////////////////////////////////////////////////////////////////////////////////ubuntu22NYX:~/ssl/openssl-3.3.0$ python3 ec.py✅ ECDH 共享密钥 0f6392ef6f17b753363a56c4a930f3c5476781bd0d6ec554cf5d209a41a0d56f2. 但是真正用于加密的密钥远没有这么简单还要进一步加工以下只做具体的数值计算演示, 具体原理读者自行查看相关PRF计算相关资料.TLS 1.2 PRF 双哈希 HMAC 异或PRF(secret, label, seed) P_SHA256(secret, label seed)这里输入参数为client_random, server_random, 再加上 Label: master secretPRF(secret, → 共享密钥 / master secretlabel, → master secret / key expansionseed → client_random server_random)import hmacimport hashlib# 你的真实数据 secret bytes.fromhex(0f6392ef6f17b753363a56c4a930f3c5476781bd0d6ec554cf5d209a41a0d56f)label bmaster secretrandom_bytes bytes.fromhex(d20af594826b3b98a81be4944b66f90a62e50b0d1521986452fe9460e07d0e2c69b0180277ae71f7378365f6f3a745e04407c8c239da486b0499b1d26f9fd8d1)# TLS 1.2 PRF 函数 def tls12_prf(secret, label, seed, output_len):def p_sha256(secret, seed, out_len):result ba seed # A(0) seedwhile len(result) out_len:a hmac.new(secret, a, hashlib.sha256).digest()result hmac.new(secret, a seed, hashlib.sha256).digest()return result[:out_len]return p_sha256(secret, label seed, output_len)# 计算 master_secret tls12_prf(secret, label, random_bytes, 48)# 输出结果 print(✅ 计算结果:)print(master_secret.hex())✅ 计算结果:e6f95a022f09297c6901d67dd57e19c17e32a49a39a959c011a5a0d7b6a8aaa38e85eb1559d0e03fc8efc35493eacb343. 你以为这样就结束了 真正用于加密的key还要再来一轮PRFInput Secret是上一轮的计算结果e6f95a022f09297c6901d67dd57e19c17e32a49a39a959c011a5a0d7b6a8aaa38e85eb1559d0e03fc8efc35493eacb34这次的label 是这个 Label: key expansion计算参考上面一样 只是参数变了TLS PRF 最终会话密钥 (key expansion 结果)ae80f49558a0363a72008dd9b874fc5fc83b7a8675ef7a2841751618d711eaa7070fa0dd24a4e06e--- 拆分后真正加密用的密钥 --- Client Write Key (AES 密钥): ae80f49558a0363a72008dd9b874fc5f Server Write Key (AES 密钥): c83b7a8675ef7a2841751618d711eaa7 Client Nonce (IV 种子): 070fa0dd Server Nonce (IV 种子): 24a4e06e4. OK 现在已经有了真正可以用来加密的key了按照约定的加密算法该算法需要传入4个参数GCM KEY(16字节): ae80f49558a0363a72008dd9b874fc5f //Client Write Key (AES 密钥)GCM TAG(12字节): 00 00 00 00 00 00 00 01 17 03 03 00 3a 就是包头上的这些 为了防止被串改也要弄进去. 01其实是seq1nonce:Yi.c (12字节): 070fa0dd2388010c52713d94 070fa0dd 是Client Nonce (IV 种子) 2388010c52713d94 是client端随机生成 但是会发给server端准备好要加密的数据[OpenSSL] 加密前 HTTP 明文 (58 字节): 0000 - 47 45 54 20 2f 20 48 54-54 50 2f 31 2e 31 0d 0a GET / HTTP/1.1.. 0010 - 48 6f 73 74 3a 20 77 77-77 2e 62 61 69 64 75 2e Host: www.baidu. 0020 - 63 6f 6d 0d 0a 43 6f 6e-6e 65 63 74 69 6f 6e 3a com..Connection: 0030 - 20 63 6c 6f 73 65 0d 0a-0d 0a close....材料都准备好了 就可以调用加密算法了同样的这里只演示数值计算。from cryptography.hazmat.primitives.ciphers.aead import AESGCM# # 1. 你真实的 KEY# key bytes.fromhex(ae80f49558a0363a72008dd9b874fc5f)# # 2. 你真实的 Yi.c16字节直接作为 NONCE# yi_c bytes.fromhex(070fa0dd2388010c52713d94)nonce yi_c[:12] # GCM 只用前 12 字节# # 3. 你真实的明文# plaintext bytes.fromhex(474554202f20485454502f312e310d0a486f73743a207777772e62616964752e636f6d0d0a436f6e6e656374696f6e3a20636c6f73650d0a0d0a)# # 4. 你真实的 AAD# aad bytes.fromhex(0000000000000001170303003a)# # 5. AES-GCM 加密# aesgcm AESGCM(key)cipher_with_tag aesgcm.encrypt(nonce, plaintext, aad)# # 输出结果# print(✅ 前12字节 NONCE , nonce.hex())print(✅ AAD , aad.hex())print(✅ 密文TAG , cipher_with_tag.hex())print(✅ 长度 , len(cipher_with_tag))✅ 密文TAG dd1de5bdb594317f26045e43c3504c086a5fb454cc88acdddc88988bedf193b3c0734cd146deaa6205e3b4da9bd8931544cd788c25ae54e147f44f10b6e971a808fb6c9db595a11b42df查看抓包 去掉前面2388010c52713d94 是client端随机生成 但是会发给server端完全一致.最后补充一点 关于最后这个加密的调用栈#0 aesni_gcm_encrypt () at crypto/modes/aesni-gcm-x86_64.s:516#1 0x00007ffff7c4d48c in generic_aes_gcm_cipher_update (ctx0x55555571f850,in0x55555571fc30 GET / HTTP/1.1\\r\\nHost: www.baidu.com\\r\\nConnection: close\\r\\n\\r\\n\n, len67,out0x55555571fc30 GET / HTTP/1.1\\r\\nHost: www.baidu.com\\r\\nConnection: close\\r\\n\\r\\n\n)at providers/implementations/ciphers/cipher_aes_gcm_hw.c:75openssl 下如果是x86 走的是aesni-gcm-x86_64.s如果是armv8走的是aes-gcm-armv8_64.pl //如果支持UNROLL8_EOR3 走ciphers/cipher_aes_gcm_hw_armv8.inc