源码HTTPS握手底层原理?

访客 源码剖析 2

本文目录导读:

  1. 核心概念:三个“秘密”的交换
  2. 流程拆解(TLS 1.3 简化 + 源码视角)
  3. HTTPS 底层源码链路总结(从你的代码到握手)
  4. 一个重要的“坑”:密码学与协议实现

这是一个非常核心的计算机安全问题,HTTPS 的本质就是 HTTP(应用层) 运行于 TLS/SSL(传输层安全协议)之上。

“握手”是客户端(如浏览器)和服务器建立安全连接的第一步,目的是协商加密算法、验证服务器身份、并生成只有双方知道的会话密钥

下面深入到源码级别(以最流行的 OpenSSL 库和 Linux 内核的通信逻辑为例),拆解 TLS 1.3(目前主流且更简洁的版本,TLS 1.2 为参考)的握手底层原理。

核心概念:三个“秘密”的交换

在握手过程中,所有操作都围绕下面三个值展开(源码里会以 struct 或变量形式存在):

  1. 随机数 (Random):双方生成,防止重放攻击。
  2. 预主密钥 (Pre-Master Secret):客户端生成,使用服务器公钥加密传输。
  3. 主密钥 (Master Secret):由预主密钥和两个随机数通过一个 PRF(伪随机函数)推导出来,是后续生成所有加密密钥的根。

流程拆解(TLS 1.3 简化 + 源码视角)

TLS 1.3 将握手从 2-RTT 降为 1-RTT,效率更高,假设是首次连接(非会话恢复)。

ClientHello (客户端问候)

概念:客户端发送支持的 TLS 版本、密码套件(如 TLS_AES_128_GCM_SHA256)、一个 32 字节的随机数 以及密钥交换的公共参数(椭圆曲线 Diffie-Hellman 的初始公钥)

源码级别演绎 (openssl/ssl/statem/statem_clnt.c 中的 tls_construct_client_hello()):

// 伪代码示意
int tls_construct_client_hello(SSL *s, WPACKET *pkt) {
    // 1. 生成 32 字节客户端随机数
    RAND_bytes(s->s3->client_random, 32); 
    // 2. 写入 TLS 版本 (0x0304 代表 TLS 1.3)
    WPACKET_put_bytes_u16(pkt, s->version);
    // 3. 写入随机数
    WPACKET_memcpy(pkt, s->s3->client_random, 32);
    // 4. 写入密码套件列表 (TLS_AES_256_GCM_SHA384)
    // ...
    // 5. **关键**: 生成客户端的 ECDHE (椭圆曲线 Diffie-Hellman Ephemeral) 密钥对
    //    并将公钥 (KeyShare) 放入扩展字段
    if (!tls_construct_extensions(s, pkt, ...)) {
        // 扩展里有一个 extension_type = key_share
        // 内容就是椭圆曲线类型 (如 x25519) 和公钥值
    }
    // 6. 发送给服务器
    return 1;
}

核心意义:客户端不仅告诉了服务器“我想用谁”,还提前把公钥材料KeyShare)发过去了,这是 TLS 1.3 减少一次 RTT 的关键。

ServerHello + 服务器参数 (服务器回复)

服务器收到后,也会做类似的步骤。

源码级别演绎 (openssl/ssl/statem/statem_srvr.c 中的 tls_construct_server_hello()):

int tls_construct_server_hello(SSL *s, WPACKET *pkt) {
    // 1. 生成 32 字节服务器随机数
    RAND_bytes(s->s3->server_random, 32);
    // 2. 写入 TLS 版本 (与客户端协商一致)
    WPACKET_put_bytes_u16(pkt, s->version);
    // 3. 写入随机数
    WPACKET_memcpy(pkt, s->s3->server_random, 32);
    // 4. **关键**: 发送服务器自己的 KeyShare (也是 ECDHE 公钥)
    //    服务器用客户端的 KeyShare 和自己的私钥,就能计算出共享密钥!
    // ...
    return 1;
}

服务器还会做一件极其重要的事:

源码演绎 (ossl_statem_server_post_process_message()):

// 伪代码 - 计算共享密钥
EC_POINT *client_pub = ...; // 从 ClientHello 中解析出客户端公钥
EC_KEY *server_priv = ...; // 服务器自己生成的临时私钥
// 重点: 两边各自用 (自己的私钥 + 对方的公钥) 计算出相同的共享密钥 "Z"
unsigned char *shared_secret_Z = ECDH_compute_key(client_pub, server_priv); 
//  这个 shared_secret_Z Pre-Master Secret 的现代版本
// 然后立即使用 HKDF (HMAC-based Extract-and-Expand Key Derivation Function)
// 从 shared_secret_Z 和两个随机数,派生出实际的会话加密密钥
tls13_generate_handshake_secret(s, shared_secret_Z, s->s3->client_random, s->s3->server_random);
// 重要: 使用这个握手密钥加密服务器即将发送的 Certificate/Finished 消息

核心意义

  • 即使攻击者截获了所有公钥(都是明文的),没有私钥也无法计算出共享密钥 Z
  • 完美前向保密 (PFS):因为服务器使用的 ECDHE 私钥是临时生成的,每次连接都不同,即使将来服务器的长期私钥(证书私钥)泄露,也无法解密过去记录的流量。

证书验证 + 握手完成 (Certificate + Finished)

服务器动作

  1. 发送自己的数字证书(包含公钥和 CA 签名),客户端会验证这个证书的合法性。
  2. 发送一个 CertificateVerify 消息:用证书私钥对到目前为止的所有握手消息进行签名,客户端用证书公钥验签,证明服务器确实拥有该证书的私钥
  3. 发送一个 Finished 消息:使用刚刚计算出的握手密钥,对 handshake 记录进行 MAC 计算,确保握手过程未被篡改,客户端收到后,也能用自己算出来的握手密钥验证 Finished 消息,证明密钥协商成功

客户端动作

  1. 验证服务器证书(路径、有效期、吊销状态)。
  2. 用证书公钥验证 CertificateVerify 签名。
  3. 用握手密钥验证 Finished 消息。
  4. 生成最终的会话密钥,并发送自己的 Finished 消息。

最终成果:双方现在持有完全相同的、只有它们知道的一组会话密钥(由 shared_secret_Z 派生而来)。

HTTPS 底层源码链路总结(从你的代码到握手)

当你用 https://example.com 访问时,底层发生了什么(以 curl + OpenSSL 为例):

  1. Socket 连接curl 调用 socket() -> connect()example.com:443,这是 TCP 三次握手,不加密
  2. 加载 SSL 上下文curl 调用 SSL_CTX_new(TLS_client_method()) 创建 SSL 上下文。
  3. 绑定 SocketSSL_new(ctx) -> SSL_set_fd(ssl, socket_fd) 将加密层绑定到已连接的 TCP 套接字上。
  4. 执行握手SSL_connect(ssl),这个函数内部就会触发上述 tls_construct_client_hello 等一堆复杂的 OpenSSL 源码调用。
    • 内核层面:数据包通过 sendmsg() 系统调用发送出去,经过 TCP 协议栈、IP 层,最终从网卡发出,接收也是同理。
    • OpenSSL 层面:处理协议状态机、加密/解密、内存分配、大数运算(ECC 计算)。
  5. 发送/接收数据:握手成功后,SSL_read(ssl, buf, len)SSL_write(ssl, buf, len) 对应用层透明,OpenSSL 会在底层自动使用协商好的密钥进行 AES-GCM 加密和解密。

一个重要的“坑”:密码学与协议实现

源码不是一个简单的函数调用,而是状态机加密工程网络协议的复杂结合。

  • 状态机:OpenSSL 有一张巨大的状态转换表,SSL_ST_BEFORE -> SSL_ST_OK -> SSL_ST_READ_BODY -> SSL_ST_READ_HEADER,错误的状态会导致连接中断。
  • 加密工程RAND_bytes 不是简单的随机,它可能调用 /dev/urandom 或 CPU 的 RDRAND 指令。ECDH_compute_key 底层涉及椭圆曲线点乘、Montgomery 域运算、快速模约减等高度优化的汇编代码(如 x86_64-mont5)。
  • 不可信赖的握手Finished 消息的目的是对所有握手消息进行 HMAC 验证,如果任何一个字节在传输过程中被篡改(比如中间人修改了 KeyShare 公钥),双方计算出的密钥将不一致,Finished 验证必然失败,连接立即终止,这是底层保障安全性的防线。

HTTPS 握手底层原理 = TCP 连接 + 基于 DH/ECDH 的密钥交换 + 证书链的 PKI 验证 + 状态机驱动的明文/密文协议切换,所有这一切,在 OpenSSL 源码中体现为数千行 C 代码的严密逻辑、状态流转和数学运算,理解了 ECDHE 密钥协商和 Finished 消息的完整性验证,就抓住了 HTTPS 安全的本质。

标签: 密钥交换

抱歉,评论功能暂时关闭!