TLSv1.2 客户端加密完整示例解析
1. 计算client端的共享密钥
ECDH 算法协商secp256r1
shareKey = client_privateKey * server_publicKey = client_privateKey * server_privateKey *G
上图的就是server_publicKey
只需要代入client私钥按照secp256r1算法就能得到共享密钥
这个有开源现成的库可以计算 本着学习的目的 可以直接手算 // 这里需要了解椭圆曲线的加密原理基础才能理解。椭圆曲线的基本原理不复杂 而且应用非常广泛 有很多变种 在区块链等领域广泛应用。
# ----------------------------
# 你的密钥
# ----------------------------
CLIENT_PRIV_HEX = "508a5e1df188b3826e31e9515e79417815302197bffb946cbdff7afa2c0b5d02"
SERVER_PUB_HEX = "04083ecbf3dd57b4974283a5b86b8f1511fbe87c430590d79b1698b96343c919f8ae74da9c879619239a8832862820b0cc0cc5d0ad6490684a157a5651583e9aa7"
# ----------------------------
# secp256r1 (NIST P-256) 椭圆曲线参数
# ----------------------------
p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
a = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC
b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
Gx = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296
Gy = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162CBCE6B8E1F612886EF3DB3791549063
n = 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) % p
else:
m = (y2 - y1) * pow(x2 - x1, p-2, p) % p
x3 = (m * m - x1 - x2) % p
y3 = (m * (x1 - x3) - y1) % p
return (x3, y3)
def point_mul(x, y, scalar):
rx, ry = 0, 0
while scalar > 0:
if scalar & 1:
rx, ry = point_add(rx, ry, x, y)
x, y = point_add(x, y, x, y)
scalar >>= 1
return (rx, ry)
# ----------------------------
# 解析服务器公钥
# ----------------------------
pub_bytes = bytes.fromhex(SERVER_PUB_HEX)
qx = int.from_bytes(pub_bytes[1:33], byteorder='big')
qy = int.from_bytes(pub_bytes[33:65], byteorder='big')
# ----------------------------
# 私钥
# ----------------------------
priv = int(CLIENT_PRIV_HEX, 16)
# ----------------------------
# 计算共享密钥 = 私钥 × 服务端公钥
# ----------------------------
sx, sy = point_mul(qx, qy, priv)
# ----------------------------
# 输出结果(32字节 X坐标)
# ----------------------------
shared_secret = sx.to_bytes(32, byteorder='big').hex()
print("✅ ECDH 共享密钥 =", shared_secret)
///////////////////////////////////////////////////////////////////////////////////////////////////
ubuntu22@NYX:~/ssl/openssl-3.3.0$ python3 ec.py
✅ ECDH 共享密钥 = 0f6392ef6f17b753363a56c4a930f3c5476781bd0d6ec554cf5d209a41a0d56f
2. 但是真正用于加密的密钥远没有这么简单,还要进一步加工
以下只做具体的数值计算演示, 具体原理读者自行查看相关PRF计算相关资料.
TLS 1.2 PRF = 双哈希 HMAC + 异或
PRF(secret, label, seed) = P_SHA256(secret, label + seed)

这里输入参数为client_random, server_random, 再加上 Label: master secret
PRF(
secret, → 共享密钥 / master secret
label, → "master secret" / "key expansion"
seed → client_random + server_random
)
import hmac
import hashlib
# ====================== 你的真实数据 ======================
secret = bytes.fromhex("0f6392ef6f17b753363a56c4a930f3c5476781bd0d6ec554cf5d209a41a0d56f")
label = b"master secret"
random_bytes = bytes.fromhex("d20af594826b3b98a81be4944b66f90a62e50b0d1521986452fe9460e07d0e2c69b0180277ae71f7378365f6f3a745e04407c8c239da486b0499b1d26f9fd8d1")
# ====================== TLS 1.2 PRF 函数 ======================
def tls12_prf(secret, label, seed, output_len):
def p_sha256(secret, seed, out_len):
result = b""
a = seed # A(0) = seed
while 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())
✅ 计算结果:
e6f95a022f09297c6901d67dd57e19c17e32a49a39a959c011a5a0d7b6a8aaa38e85eb1559d0e03fc8efc35493eacb34
3. 你以为这样就结束了? 真正用于加密的key还要再来一轮PRF
Input 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 种子): 24a4e06e
4. 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其实是seq=1nonce: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.c(16字节,直接作为 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 (ctx=0x55555571f850,
in=0x55555571fc30 "GET / HTTP/1.1\\r\\nHost: www.baidu.com\\r\\nConnection: close\\r\\n\\r\\n\n", len=67,
out=0x55555571fc30 "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:75
openssl 下如果是x86 走的是aesni-gcm-x86_64.s
如果是armv8走的是aes-gcm-armv8_64.pl //如果支持UNROLL8_EOR3 走ciphers/cipher_aes_gcm_hw_armv8.inc
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
nonce: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....

所有评论(0)