HTTPS协议与HTTP协议的区别
一:差别对比
层级 | HTTP | HTTPS |
---|---|---|
应用层 | 明文文本(可直接 telnet 阅读) | 加密二进制(Wireshark 需导入私钥才能解密) |
传输层 | TCP 80 端口 | TCP 443 端口 |
安全层 | 无 | TLS 1.2/1.3(身份验证 + 机密性 + 完整性) |
维度 | 要点 | 示例/验证 |
---|---|---|
1. 端口 | 80 vs 443 | curl -v http://example.com vs https://example.com |
2. 握手 | 三次握手即可发报文 | HTTPS 额外 TLS 握手(1-RTT 或 0-RTT) |
3. 加密 | 明文可被中间人窃听 | Wireshark 抓 HTTP 直接看到密码;HTTPS 只能看到密文 |
4. 证书 | 无 | 服务器必须提供 CA 签发的 X.509 证书 |
5. SEO | 谷歌优先收录 HTTPS | Chrome 地址栏对 HTTP 显示“不安全” |
6. 性能 | 旧时代 HTTPS 慢 | TLS 1.3 + HTTP/2 使 HTTPS 更快(多路复用 + 0-RTT) |
7. 成本 | 免费 | Let’s Encrypt 提供零费用证书 |
二:HTTPS连接建立的详细过程
阶段1. TCP 三次握手 14:00:00.021-0.041 RTT≈20 ms
Client → SYN(seq=0xabcd1234, MSS=1460, SACK_PERM=1)
Server → SYN-ACK(seq=0x567890ef, ack=0xabcd1235)
Client → ACK(seq=0xabcd1235, ack=0x567890f0)
窗口 65535 → 28960 → 28960
阶段2 TLS1.3 握手 14:00:00.042-0.083 RTT≈41 ms
3.1 ClientHello(明文)
HandshakeType=01
Length=0x01c8
ClientVersion=0x0303(向下兼容)
Random=0x9f8e…(32字节)
SessionID=32字节(兼容 0x20)
CipherSuites=TLS_AES_128_GCM_SHA256(0x1301),TLS_CHACHA20_POLY1305_SHA256(0x1303)…
Extensions:server_name: "example.com"supported_groups: x25519(0x001d),secp256r1(0x0017)key_share: x25519 pub_A=0x1a2b…(32字节)signature_algorithms: ecdsa_secp256r1_sha256,rsa_pss_rsae_sha256application_layer_protocol_negotiation: h2,http/1.1
备注:主要数据就是支持的TLS协议版本,支持的加密套件,ClientRandom随机数,以及key_share
问题1:加密套件是什么,包含哪些东西?
加密套件(Cipher Suite)本质上是一份“算法清单”,告诉通信两端:
用什么算法得到共享密钥(Key Exchange)
用什么算法验证身份(Authentication)
用什么算法加密数据(Bulk Encryption / AEAD)
用什么算法做完整性校验(MAC / PRF / Hash)
TLS 1.2 与 TLS 1.3 的写法不同,下面分开讲,并给出可抓包看到的真实例子。
TLS1.2格式:
密钥交换_身份验证_批量加密_消息认证
ECDHE-ECDSA-AES256-GCM-SHA384
密钥交换:ECDHE
身份验证:ECDSA 证书(P-256/P-384)
加密:AES-256-GCM
MAC/PRF:SHA384
TLS 1.3 加密套件的 2 段式命名
TLS 1.3 把密钥交换和身份验证从套件里拿掉,改为“协商算法 + 证书公钥算法”独立字段
示例:
TLS_AES_128_GCM_SHA256
字段 | 含义 | |
---|---|---|
AEAD 算法 | 实际加密/完整性一次完成 | AES-128-GCM |
HKDF Hash | 用于密钥派生 | SHA256 |
问题2:如何使用密钥协商算法得到共享密钥,key_share和随机数有什么作用?
0. 预备
曲线:X25519(RFC 7748,素数 p = 2²⁵⁵ − 19,基点 G 已标准化)
私钥长度:32 字节随机数,需做掩码
输出:32 字节共享密钥 SS(也叫 K)
────────────────
生成私钥 & 公钥
客户端 Alice
随机 32 字节(示例)
priv_A = 0x6a2cb65d7b5a1e8c25f1e8c25f1e8c25f1e8c25f1e8c25f1e8c25f1e8c25f掩码(RFC 7748):
priv_A[0] &= 0xf8
priv_A[31] &= 0x7f
priv_A[31] |= 0x40
→ 最终 priv_A = 0x68acb65d7b5a1e8c25f1e8c25f1e8c25f1e8c25f1e8c25f1e8c25f1e8c240公钥 pub_A = X25519(priv_A, G)#备注:这个公钥就是key_share
→ pub_A = 0x1a2b3c4d5e6f7890123456789abcdef0123456789abcdef0123456789abcdef
服务端 Bob
随机 32 字节
priv_B = 0x2037437a4c0550b24c0550b24c0550b24c0550b24c0550b24c0550b24c0550b2掩码后 priv_B = 0x2037437a4c0550b24c0550b24c0550b24c0550b24c0550b24c0550b24c0550b2a
公钥 pub_B = X25519(priv_B, G)
→ pub_B = 0x3c4d5e6f7890123456789abcdef0123456789abcdef0123456789abcdef0e1f
──────────────── 2. 交换公钥
Alice 把 pub_A 发给 Bob
Bob 把 pub_B 发给 Alice
(网络字节序:小端 32 字节)
──────────────── 3. 计算共享密钥
Alice 侧
SS = X25519(priv_A, pub_B)= X25519(0x68acb65d..., 0x3c4d5e6f...)→ 32 字节结果= 0x4a5d9d5ba4ce3d8f6a0b2c8e7d1f7b8d
Bob 侧
SS = X25519(priv_B, pub_A)= X25519(0x2037437a..., 0x1a2b3c4d...)→ 32 字节结果= 0x4a5d9d5ba4ce3d8f6a0b2c8e7d1f7b8d
两侧结果完全一致,这就是 ECDHE 共享密钥 K。
0. 输入
SS:32 字节 ECDHE 共享密钥(上一题已算得
0x4a5d9d5b…
)。Hello.Random:ClientHello.random || ServerHello.random(64 字节)。
CipherSuite:TLS_AES_128_GCM_SHA256 → Hash=SHA256 → 输出长度 L=32 字节。
────────────────
提取(HKDF-Extract)
salt = 0x00 * 32
early_secret = HKDF-Extract(salt, SS)
得到 32 字节 early_secret
。
──────────────── 2. 派生握手密钥
handshake_secret = HKDF-Extract(early_secret, Derive-Secret(early_secret,"derived",""))
其中 Derive-Secret
调用:
Derive-Secret(Secret, Label, Messages) =HKDF-Expand-Label(Secret, Label, Transcript-Hash(Messages), Hash.length)
Transcript-Hash
= SHA256(ClientHello…ServerHello)
结果仍是 32 字节,记为 handshake_secret
。
──────────────── 3. 计算客户端/服务端握手密钥
client_handshake_key =HKDF-Expand-Label(handshake_secret, "client handshake key",Transcript-Hash(ClientHello…ServerHello), 32)server_handshake_key =HKDF-Expand-Label(handshake_secret, "server handshake key",Transcript-Hash(ClientHello…ServerHello), 32)
这两个 32 字节直接作为 AES-128-GCM 的 128-bit 密钥。
──────────────── 4. 计算应用数据密钥(真正加密 HTTP/2 流量)
master_secret = HKDF-Extract(handshake_secret, Derive-Secret(handshake_secret,"derived",""))client_application_traffic_secret =HKDF-Expand-Label(master_secret, "client application traffic secret",Transcript-Hash(ClientHello…ServerFinished), 32)server_application_traffic_secret =HKDF-Expand-Label(master_secret, "server application traffic secret",Transcript-Hash(ClientHello…ServerFinished), 32)
同样 32 字节 → AES-128-GCM 密钥。
──────────────── 5. 每字节公式(伪代码)
# HKDF-Expand-Label 定义
HKDF-Expand-Label(Secret, Label, Context, Length):info = 0x00 0x00 || Length(2B) || "tls13 " || Label || 0x00 || Contextreturn HKDF-Expand(Secret, info, Length)
OpenSSL 打印示例
client_application_traffic_secret=5d0c2e5f 0a4e8f6b 7b8a9c1e 2f3d4b5a 6e7f8c9d 0e1f2a3b 4c5d6e7f 8a9b0c1d
──────────────── 一句话总结
TLS 1.3 用 HKDF-Extract → HKDF-Expand-Label 把 32 字节共享密钥层层派生出 握手密钥 → 应用密钥,每一步都绑定握手摘要,确保“改一字节就全变”。
3.2ServerHello+EncryptedExtensions+Certificate+CertificateVerify+Finished(明文→加密)
HandshakeType=02
Random=0x4d5e…
CipherSuite=0x1301(TLS_AES_128_GCM_SHA256)
key_share: x25519 pub_B=0x3c4d…(32字节)
server hello返回的重要数据包含了选定的TLS版本,加密套件,server random,以及key_share。
问题3:明明到了server hello才选定了使用的加密套件,那凭什么clienthello的时候就发出了key_share呢,这个明明要经过加密套件中的密钥协商算法才能得出来的?
客户端在 ClientHello 里给出的 Key Share 并不是“最终答案”,而是一份 预选的提议(draft key share)。它遵循 “先送一个最常见曲线,避免往返” 的设计:
提前发送 ≠ 最终确定
ClientHello 可以同时列出:自己支持的所有加密套件(cipher suites)
同时也给出其中某一条曲线(如 X25519)的公钥(key_share 扩展)
服务端有否决权
如果服务端也支持这条曲线 → 直接用它算共享密钥,握手立刻成功(1-RTT)。
如果服务端想选别的曲线 → HelloRetryRequest 要求客户端重新发对应曲线的公钥(额外 1 个 RTT),但连接不会中断。
好处:减少 RTT
90% 的现代服务器都支持 X25519,因此客户端先送它的公钥,大概率一次往返就完成握手;只有在少数场景(强制 P-384、国密 SM2 等)才触发 HelloRetryRequest。
一句话:Key Share 只是“先发制人”的提议,最终曲线/套件仍由服务端决定,客户端随时可改。
certificate消息中主要包含的是证书验证链如下所示:
Certificate链(ASN.1 DER)
[0] leaf: CN=example.com, SAN=DNS:example.com, DNS:*.example.comPublicKey=ecdsa-p256 0x04ab…Signature=ecdsa-with-SHA256 0x021f…
[1] intermediate: CN=Let's Encrypt R3
[2] root: CN=ISRG Root X1(本地已信任)
问题4:如何快速了解什么叫做证书链,证书链的验证过程是什么?
把“证书链”想成一份可逐层验证的「官方介绍信」,就能立即理解它的作用与结构。下面用生活类比 → 技术细节 → 浏览器验证流程三步彻底讲清。
生活类比:介绍信的三级盖章 你(浏览器)拿到一份“Bob 的介绍信”:
第一层:Bob 自己写的
“我叫 Bob,身份证 123456,公钥是 0xABCD…”
但任何人都能伪造,因此需要第二层。第二层:派出所(中级 CA)盖章
“经核查,Bob 的身份证确实 123456,公章属实。”
派出所的公章大家信不过?那就再往上。第三层:市公安局(根 CA)再盖章
“派出所的公章是真的,特此证明。”
市公安局的章直接预装在每个人(浏览器/操作系统)的“信任保险箱”里,无需再验证。
这三张纸叠在一起就是“证书链”:Bob 的实体证书 + 中级 CA 证书 + 根 CA 证书。
证书中包含了这个被签目标的域名,有效期,公钥以及签发机构ca的数字签名(即用ca的私钥去对出数字签名外所有证书内容的一个加密)。
CertificateVerify
这个存在主要是为了确保握手过程中的身份验证,这里就要用到身份验证算法,所谓certificateverify包含了服务端用私钥把从clienthello到serverclient所有消息的哈希码进行加密的密文,然后客户端收到之后会事先把从clienthello到serverclient所有消息用哈希算法进行加密,然后用之前收到的证书上的公钥对密文进行解密,比对内容后就能确定对方是不是正确的对象,杜绝握手过程中的中间人攻击。
SignatureScheme=ecdsa_secp256r1_sha256
Signature=ECDSA_sign(SK_server, Hash(ClientHello..ServerHello))
Finished
verify_data=HMAC(handshake_secret, Hash(ClientHello..CertificateVerify))
这个过程主要服务端使用加密套件中的确保完整性算法HMAC算法对握手摘要进行了加密,然后客户端也会同步进行加密,服务端把密文发过去之后进行比对两边密文是否一致,来确保最后不存在中间人攻击。
3.3 客户端验证(下面很多过程贴合上面提到的)
链式验证:根证书信任 → 中间证书签名 → 叶子证书签名 OK
域名匹配:SAN 包含 example.com OK
有效期:2024-06-01 至 2024-08-30 OK
吊销检查:OCSP Stapling 已附带 OK
3.4 计算共享密钥
SS = X25519(priv_A, pub_B) = 32字节共享秘密
handshake_secret = HKDF-Extract(salt=0x00, SS)
client_handshake_key = HKDF-Expand(handshake_secret, "client handshake key", 32)
server_handshake_key = HKDF-Expand(handshake_secret, "server handshake key", 32)
3.5 ClientFinished(加密)
verify_data=HMAC(handshake_secret, Hash(ClientHello..ClientFinished-1))
至此握手完成,后续流量用 client_app_key
/ server_app_key
(AES-128-GCM) 加密。
问题5:https传输密文的时候如何确保完整性和真实性?
HTTPS 把“完整性 + 真实性”做成一道带密钥的防伪封条,贴在每一条加密记录上,任何比特级篡改都会立刻被检测并终止连接。下面用 TLS 1.3 为例,把机制拆成“三张牌”:
1️⃣ 只有双方知道的对称密钥
• 来源:TLS 1.3 派生出的 client_application_traffic_secret
/ server_application_traffic_secret
• 作用:既做加密也做 MAC 的密钥,绝不外传。
2️⃣ AEAD 模式:一次性完成“加密 + 认证”
TLS 1.3 强制使用 AEAD(AES-128-GCM、ChaCha20-Poly1305 等),每个加密记录包含:
[ 16-byte 随机数 nonce ]
[ 原始明文 ]
[ 16-byte 认证标签 Auth Tag ]
Auth Tag = AEAD_Encrypt(key, nonce, plaintext, 附加数据 AAD)
AAD 里固定包含记录序号 + 类型 + 版本 + 长度,哪怕改 1 bit,Auth Tag 立刻失效。
3️⃣ 隐式序号:防重放
每条记录都有一个单调递增的 64-bit 序号,参与 AAD 计算,但不随包发送。
→ 乱序、重复、丢包都会被 Auth Tag 拒绝。
一条 TLS 记录的格式分两层:
固定 5 字节的记录头(所有版本统一,也叫AAD)
载荷区(Fragment),根据握手阶段是否加密,内部结构完全不同。
固定 5 字节头(大端序)
字节 | 字段 | 长度 | 取值/说明 |
---|---|---|---|
0 | ContentType | 1 B | 0x14=ChangeCipherSpec |
1-2 | LegacyVersion | 2 B | 0x0303(TLS 1.3 也写 0x0303,兼容) |
3-4 | Length | 2 B | 载荷长度 0‒16383(不含这 5 B) |
2. Fragment(载荷区)——两种形态
A. 明文阶段 / 握手早期
+---------+--------------+
| 明文数据 | 无额外结构 |
| (变长) | |
+---------+--------------+
例:ClientHello 直接放在 Fragment 里。
B. TLS 1.3 加密阶段(AEAD 封装)
+----------------+------------------+----------------+
| 12 B IV/nonce | Ciphertext | 16 B Auth Tag |
| (显式) | (明文+填充) | (MAC) |
+----------------+------------------+----------------+
IV/nonce 由 TLS 1.3 规定 12 字节显式发送
Ciphertext 长度 = Length – 28
Auth Tag 固定 16 字节
它只需要用同一把密钥、同一次 nonce、同一份 AAD 把整条 fragment 丢进 AEAD 解密函数。
如果 Auth Tag 不对,函数会立即报错(bad_record_mac
),这就是完整性/真实性校验的全部动作。
────────────────
输入材料(全部来自刚才收到的 TLS 记录)
key = client_application_traffic_secret (32 B 对称密钥)
nonce = 12 B explicit IV + 8 B implicit seq (共 12 B)
aad = 5 B 固定结构:0x17 0x0303 Length
ciphertext_with_tag = IV 之后的所有字节(len-28)
int rc = EVP_AEAD_CTX_open(ctx, // 已装载 keyout, // 输出明文缓冲区&out_len, // 实际明文长度MAX_OUT, // 缓冲区大小nonce, 12, // 12 B nonceciphertext, len, // 完整 fragment(含 tag)aad, 5 // 5 B AAD);
成功:
rc=1
,out
里是原始明文,可直接交给 HTTP/2。失败:
rc=0
,TLS 立即发Alert(fatal: bad_record_mac)
,连接 RST。