8.5.3 方案三:全功能(机密性 + 认证 + 完整性)(Figure 8.21)

把方案一和方案二结合起来。
Alice 的完整操作:

第一步(认证+完整性,参考方案二):
  m --> H(·) --> K_A^-(H(m))  [数字签名]
  打包:{m, K_A^-(H(m))}  = 预备包
第二步(加密,参考方案一):
  随机生成 K_S
  K_S(预备包)  [对称加密整个预备包]
  K_B^+(K_S)  [用Bob公钥加密会话密钥]
最终发送:{K_S(预备包), K_B^+(K_S)}

Mermaid 流程图(Alice端):

消息 m

H 哈希

H m

K_A- 私钥签名

签名

拼接

预备包: m + 签名

K_S 对称加密

K_S 预备包

会话密钥 K_S

K_B+ 公钥加密

K_B+ K_S

拼接

发送到 Internet

Bob 的操作(反向):

1. 用 K_B^- 解密得到 K_S
2. 用 K_S 解密得到预备包 {m, K_A^-(H(m))}
3. 用 K_A^+ 验证签名,与本地计算的 H(m) 比对

Alice 使用公钥密码学的次数:

  • 一次用自己的私钥 K A − K_A^- KA(签名)
  • 一次用 Bob 的公钥 K B + K_B^+ KB+(加密会话密钥)
    Bob 使用公钥密码学的次数:
  • 一次用自己的私钥 K B − K_B^- KB(解密会话密钥)
  • 一次用 Alice 的公钥 K A + K_A^+ KA+(验证签名)

三个方案的能力对比

+---------------+--------+--------+----------+
| 方案          | 机密性 | 认证   | 完整性   |
+---------------+--------+--------+----------+
| 方案一(8.19)  |  YES   |  NO    |  NO      |
| 方案二(8.20)  |  NO    |  YES   |  YES     |
| 方案三(8.21)  |  YES   |  YES   |  YES     |
+---------------+--------+--------+----------+

公钥分发问题

方案三还有一个遗留问题:如何确保 Alice 拿到的公钥真的是 Bob 的?

  • Trudy 可以冒充 Bob,把自己的公钥发给 Alice,说"这是 Bob 的公钥"
  • Alice 用 Trudy 的公钥加密,Trudy 就能解密
    解决方案:CA(Certificate Authority,证书颁发机构)
    由受信任的第三方对公钥进行背书签名,防止公钥被冒充。

8.5.2 PGP(Pretty Good Privacy)

PGP 是 Phil Zimmermann 于 1991 年编写的电子邮件加密软件,是方案三(Figure 8.21)的实际实现。
PGP 使用的算法:

功能 使用的算法
消息摘要 MD5 或 SHA
对称加密 CAST、Triple-DES 或 IDEA
公钥加密 RSA

PGP 的特点:

  • 安装时自动生成公私钥对
  • 私钥用密码口令保护
  • 公钥可以发布到公钥服务器
  • 用户可以选择只签名、只加密、或两者都做
    PGP 的信任网络(Web of Trust):
    PGP 不依赖传统 CA,而是用"信任网络":
  • Alice 可以为她认识的任何 key/用户名 对背书签名
  • 用户之间可以举办"密钥签名聚会",当面交换公钥并互相签名
  • 信任是分布式传播的,不依赖单一权威机构
    PGP 签名消息格式(Figure 8.22):
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1
Bob:
Can I see you tonight?
Passionately yours, Alice
-----BEGIN PGP SIGNATURE-----
Version: PGP for Personal Privacy 5.0
Charset: noconv
yhHJRHhGJGhgg/12EpJ+lo8gE4vB3mqJhFEvZP9t6n7G6m5Gw2
-----END PGP SIGNATURE-----

签名数据是: K A − ( H ( m ) ) K_A^-(H(m)) KA(H(m)),即用 Alice 私钥对消息哈希的签名。
PGP 加密消息格式(Figure 8.23):

-----BEGIN PGP MESSAGE-----
Version: PGP for Personal Privacy 5.0
u2R4d+/jKmn8Bc5+hgDsqAewsDfrGdszX68liKm5F6Gc4sDfcXyt
RfdS10juHgbcfDssWe7/K=lKhnMikLo0+1/BvcX4t==Ujk9PbcD4
Thdf2awQfgHbnmKlok8iy6gThlp
-----END PGP MESSAGE

内容是经过加密的完整包,明文不出现在消息中。

C++ 完整代码演示

以下代码使用 OpenSSL 演示:

  • 对称加密(AES-256-CBC)模拟 K S ( m ) K_S(m) KS(m)
  • RSA 公钥加密模拟 K B + ( K S ) K_B^+(K_S) KB+(KS)
  • SHA-256 哈希 + RSA 签名模拟数字签名
#include <iostream>
#include <iomanip>
#include <vector>
#include <string>
#include <stdexcept>
#include <openssl/evp.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/rand.h>
#include <openssl/sha.h>
#include <openssl/err.h>
// ============================================================
// 工具:打印十六进制
// ============================================================
void printHex(const std::string& label, const std::vector<unsigned char>& data, int maxBytes = 32) {
    std::cout << label << " (" << data.size() << " bytes): ";
    int show = std::min((int)data.size(), maxBytes);
    for (int i = 0; i < show; ++i)
        std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)data[i];
    if ((int)data.size() > maxBytes) std::cout << "...";
    std::cout << std::dec << "\n";
}
// ============================================================
// 步骤1:生成随机会话密钥 Ks 和 IV
// 对应公式:随机选取 K_S
// ============================================================
void generateSessionKey(std::vector<unsigned char>& Ks,
                        std::vector<unsigned char>& IV) {
    Ks.resize(32); // AES-256 需要 32 字节密钥
    IV.resize(16); // AES 块大小 16 字节
    if (RAND_bytes(Ks.data(), 32) != 1 ||
        RAND_bytes(IV.data(), 16) != 1)
        throw std::runtime_error("随机数生成失败");
}
// ============================================================
// 步骤2:用对称密钥 Ks 加密消息
// 对应公式:K_S(m)
// ============================================================
std::vector<unsigned char> symmetricEncrypt(
    const std::vector<unsigned char>& plaintext,
    const std::vector<unsigned char>& Ks,
    const std::vector<unsigned char>& IV)
{
    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
    // 初始化 AES-256-CBC 加密,传入密钥 Ks 和初始向量 IV
    EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, Ks.data(), IV.data());
    std::vector<unsigned char> ct(plaintext.size() + 16);
    int len = 0, total = 0;
    EVP_EncryptUpdate(ctx, ct.data(), &len, plaintext.data(), plaintext.size());
    total = len;
    EVP_EncryptFinal_ex(ctx, ct.data() + total, &len);
    total += len;
    EVP_CIPHER_CTX_free(ctx);
    ct.resize(total);
    return ct;
}
// ============================================================
// 步骤3:用对称密钥 Ks 解密消息
// 对应公式:K_S^{-1}(K_S(m)) = m
// ============================================================
std::vector<unsigned char> symmetricDecrypt(
    const std::vector<unsigned char>& ciphertext,
    const std::vector<unsigned char>& Ks,
    const std::vector<unsigned char>& IV)
{
    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
    EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, Ks.data(), IV.data());
    std::vector<unsigned char> pt(ciphertext.size());
    int len = 0, total = 0;
    EVP_DecryptUpdate(ctx, pt.data(), &len, ciphertext.data(), ciphertext.size());
    total = len;
    if (EVP_DecryptFinal_ex(ctx, pt.data() + total, &len) != 1)
        throw std::runtime_error("解密失败(密钥错误或数据损坏)");
    total += len;
    EVP_CIPHER_CTX_free(ctx);
    pt.resize(total);
    return pt;
}
// ============================================================
// 步骤4:生成 RSA 密钥对(模拟 Bob 的公私钥)
// 返回 RSA* 对象(包含公钥和私钥)
// ============================================================
EVP_PKEY* generateRSAKeyPair() {
    // 使用 EVP 接口生成 2048 位 RSA 密钥对
    EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr);
    EVP_PKEY_keygen_init(ctx);
    EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048);
    EVP_PKEY* pkey = nullptr;
    EVP_PKEY_keygen(ctx, &pkey);
    EVP_PKEY_CTX_free(ctx);
    return pkey;
}
// ============================================================
// 步骤5:用接收方公钥加密会话密钥 Ks
// 对应公式:K_B^+(K_S)
// ============================================================
std::vector<unsigned char> rsaEncryptKey(
    const std::vector<unsigned char>& Ks,
    EVP_PKEY* publicKey)
{
    // RSA-OAEP 填充方式加密
    EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(publicKey, nullptr);
    EVP_PKEY_encrypt_init(ctx);
    EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING);
    size_t outLen = 0;
    // 先查询输出长度
    EVP_PKEY_encrypt(ctx, nullptr, &outLen, Ks.data(), Ks.size());
    std::vector<unsigned char> out(outLen);
    // 实际加密
    EVP_PKEY_encrypt(ctx, out.data(), &outLen, Ks.data(), Ks.size());
    EVP_PKEY_CTX_free(ctx);
    out.resize(outLen);
    return out;
}
// ============================================================
// 步骤6:用接收方私钥解密会话密钥
// 对应公式:K_B^-(K_B^+(K_S)) = K_S
// ============================================================
std::vector<unsigned char> rsaDecryptKey(
    const std::vector<unsigned char>& encryptedKs,
    EVP_PKEY* privateKey)
{
    EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(privateKey, nullptr);
    EVP_PKEY_decrypt_init(ctx);
    EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING);
    size_t outLen = 0;
    EVP_PKEY_decrypt(ctx, nullptr, &outLen, encryptedKs.data(), encryptedKs.size());
    std::vector<unsigned char> out(outLen);
    EVP_PKEY_decrypt(ctx, out.data(), &outLen, encryptedKs.data(), encryptedKs.size());
    EVP_PKEY_CTX_free(ctx);
    out.resize(outLen);
    return out;
}
// ============================================================
// 步骤7:计算 SHA-256 哈希(消息摘要)
// 对应公式:H(m)
// ============================================================
std::vector<unsigned char> sha256Hash(const std::vector<unsigned char>& data) {
    std::vector<unsigned char> hash(SHA256_DIGEST_LENGTH);
    SHA256(data.data(), data.size(), hash.data());
    return hash;
}
// ============================================================
// 步骤8:用发送方私钥对哈希值签名
// 对应公式:K_A^-(H(m))
// ============================================================
std::vector<unsigned char> rsaSign(
    const std::vector<unsigned char>& hash,
    EVP_PKEY* privateKey)
{
    // EVP_DigestSign 系列:直接对数据签名(内部会哈希,这里我们传已哈希的值)
    EVP_MD_CTX* ctx = EVP_MD_CTX_new();
    // 使用 RSA-PSS 填充 + SHA-256
    EVP_DigestSignInit(ctx, nullptr, EVP_sha256(), nullptr, privateKey);
    EVP_DigestSignUpdate(ctx, hash.data(), hash.size());
    size_t sigLen = 0;
    EVP_DigestSignFinal(ctx, nullptr, &sigLen);
    std::vector<unsigned char> sig(sigLen);
    EVP_DigestSignFinal(ctx, sig.data(), &sigLen);
    EVP_MD_CTX_free(ctx);
    sig.resize(sigLen);
    return sig;
}
// ============================================================
// 步骤9:用发送方公钥验证签名
// 对应公式:K_A^+(K_A^-(H(m))) == H(m)
// ============================================================
bool rsaVerify(
    const std::vector<unsigned char>& hash,
    const std::vector<unsigned char>& signature,
    EVP_PKEY* publicKey)
{
    EVP_MD_CTX* ctx = EVP_MD_CTX_new();
    EVP_DigestVerifyInit(ctx, nullptr, EVP_sha256(), nullptr, publicKey);
    EVP_DigestVerifyUpdate(ctx, hash.data(), hash.size());
    int result = EVP_DigestVerifyFinal(ctx, signature.data(), signature.size());
    EVP_MD_CTX_free(ctx);
    return result == 1; // 1=验证成功,0=失败,-1=错误
}
// ============================================================
// 主程序:演示安全电子邮件全流程(Figure 8.21)
// 实现:机密性 + 发送方认证 + 消息完整性
// ============================================================
int main() {
    std::cout << "=================================================\n";
    std::cout << "  安全电子邮件完整演示 (Figure 8.21)\n";
    std::cout << "  机密性 + 发送方认证 + 消息完整性\n";
    std::cout << "=================================================\n\n";
    // ---------- 原始消息 ----------
    std::string msg = "Bob, I love you! Meet me tonight. -- Alice";
    std::vector<unsigned char> message(msg.begin(), msg.end());
    std::cout << "[原始消息] " << msg << "\n\n";
    // ---------- 生成密钥对(模拟 Alice 和 Bob 各自的 RSA 密钥) ----------
    std::cout << "-- 生成 Alice 的 RSA 密钥对(用于签名)...\n";
    EVP_PKEY* aliceKey = generateRSAKeyPair();
    std::cout << "-- 生成 Bob 的 RSA 密钥对(用于加密 Ks)...\n\n";
    EVP_PKEY* bobKey = generateRSAKeyPair();
    // ======================================================
    // Alice 发送端
    // ======================================================
    std::cout << "========== Alice 发送端 ==========\n\n";
    // --- 第一步:计算消息哈希(H(m))---
    auto hashValue = sha256Hash(message);
    printHex("Step1 H(m) 消息哈希", hashValue);
    // --- 第二步:用 Alice 私钥签名(K_A^-(H(m)))---
    auto signature = rsaSign(hashValue, aliceKey);
    printHex("Step2 K_A^-(H(m)) 数字签名", signature);
    // --- 第三步:拼接消息和签名(预备包)---
    // 预备包 = message + signature_length(4字节) + signature
    std::vector<unsigned char> pkg;
    pkg.insert(pkg.end(), message.begin(), message.end());
    // 存储签名长度(4字节,大端序)
    uint32_t sigLen = (uint32_t)signature.size();
    pkg.push_back((sigLen >> 24) & 0xFF);
    pkg.push_back((sigLen >> 16) & 0xFF);
    pkg.push_back((sigLen >>  8) & 0xFF);
    pkg.push_back((sigLen      ) & 0xFF);
    pkg.insert(pkg.end(), signature.begin(), signature.end());
    std::cout << "Step3 预备包大小: " << pkg.size() << " bytes\n\n";
    // --- 第四步:生成随机会话密钥 Ks ---
    std::vector<unsigned char> Ks, IV;
    generateSessionKey(Ks, IV);
    printHex("Step4 Ks 随机会话密钥", Ks);
    // --- 第五步:用 Ks 对称加密预备包(K_S(pkg))---
    auto encryptedPkg = symmetricEncrypt(pkg, Ks, IV);
    printHex("Step5 K_S(pkg) 对称加密后", encryptedPkg);
    // --- 第六步:用 Bob 公钥加密 Ks(K_B^+(Ks))---
    auto encryptedKs = rsaEncryptKey(Ks, bobKey);
    printHex("Step6 K_B^+(K_S) RSA加密会话密钥", encryptedKs);
    std::cout << "\n[发送到网络] { K_S(pkg), K_B^+(K_S), IV }\n\n";
    // ======================================================
    // Bob 接收端
    // ======================================================
    std::cout << "========== Bob 接收端 ==========\n\n";
    // --- 第一步:用 Bob 私钥解密得到 Ks(K_B^-(K_B^+(Ks)) = Ks)---
    auto recoveredKs = rsaDecryptKey(encryptedKs, bobKey);
    printHex("Step1 解密得到 K_S", recoveredKs);
    // --- 第二步:用 Ks 解密预备包 ---
    auto decryptedPkg = symmetricDecrypt(encryptedPkg, recoveredKs, IV);
    std::cout << "Step2 对称解密成功,预备包大小: " << decryptedPkg.size() << " bytes\n\n";
    // --- 第三步:分离消息和签名 ---
    // 签名长度在倒数 sigLen+4 字节处
    size_t sigLenRecv = 0;
    size_t sigStart = decryptedPkg.size() - sigLen - 4;
    sigLenRecv  = (uint32_t)decryptedPkg[sigStart]     << 24;
    sigLenRecv |= (uint32_t)decryptedPkg[sigStart + 1] << 16;
    sigLenRecv |= (uint32_t)decryptedPkg[sigStart + 2] <<  8;
    sigLenRecv |= (uint32_t)decryptedPkg[sigStart + 3];
    std::vector<unsigned char> recvMessage(decryptedPkg.begin(),
                                           decryptedPkg.begin() + sigStart);
    std::vector<unsigned char> recvSignature(decryptedPkg.begin() + sigStart + 4,
                                             decryptedPkg.end());
    std::string recvMsg(recvMessage.begin(), recvMessage.end());
    std::cout << "Step3 分离出消息: " << recvMsg << "\n\n";
    // --- 第四步:计算收到消息的哈希 H(m) ---
    auto recvHash = sha256Hash(recvMessage);
    printHex("Step4 H(m) 本地计算哈希", recvHash);
    // --- 第五步:用 Alice 公钥验证签名 ---
    bool valid = rsaVerify(recvHash, recvSignature, aliceKey);
    std::cout << "Step5 签名验证: " << (valid ? "[OK] 签名有效!消息来自Alice且未被篡改" 
                                              : "[FAIL] 签名无效!消息可能被篡改") << "\n\n";
    // ======================================================
    // 演示:篡改消息后签名验证失败
    // ======================================================
    std::cout << "========== 防篡改演示 ==========\n";
    std::string tamperedMsg = "Bob, I hate you! -- Trudy";
    std::vector<unsigned char> tamperedMessage(tamperedMsg.begin(), tamperedMsg.end());
    auto tamperedHash = sha256Hash(tamperedMessage);
    bool tamperedValid = rsaVerify(tamperedHash, recvSignature, aliceKey);
    std::cout << "篡改后的消息: " << tamperedMsg << "\n";
    std::cout << "签名验证: " << (tamperedValid ? "通过(不应发生)" : "[FAIL] 签名不匹配!检测到篡改!") << "\n\n";
    // 清理
    EVP_PKEY_free(aliceKey);
    EVP_PKEY_free(bobKey);
    std::cout << "=================================================\n";
    std::cout << "  安全保证总结:\n";
    std::cout << "  机密性:只有Bob的私钥能解出 K_S,进而解密内容\n";
    std::cout << "  认证  :只有Alice的私钥能生成有效签名\n";
    std::cout << "  完整性:篡改消息会导致哈希不匹配,签名验证失败\n";
    std::cout << "=================================================\n";
    return 0;
}

https://godbolt.org/z/Y5c8qP9eY

=================================================
  安全电子邮件完整演示 (Figure 8.21)
  机密性 + 发送方认证 + 消息完整性
=================================================
[原始消息] Bob, I love you! Meet me tonight. -- Alice
-- 生成 Alice 的 RSA 密钥对(用于签名)...
-- 生成 Bob 的 RSA 密钥对(用于加密 Ks)...
========== Alice 发送端 ==========
Step1 H(m) 消息哈希 (32 bytes): b51010720683a422c039adb305d70c7735d4b6cbc79df95eaa1c77e887993c8b
Step2 K_A^-(H(m)) 数字签名 (256 bytes): 8062be79d259572189ab5ed08421914ab208e4f6dccb277247b366b0b35cd03d...
Step3 预备包大小: 302 bytes
Step4 Ks 随机会话密钥 (32 bytes): 2306fb0f74d43d758467defabedff8bae3e3ddf099cee96ba9cf8c2881f406f0
Step5 K_S(pkg) 对称加密后 (304 bytes): 85eba81838e59b95ab5e61e26a5399be546b7bcb60632bffc9fa819115a87b8a...
Step6 K_B^+(K_S) RSA加密会话密钥 (256 bytes): 9c19faa207a1fabbc7b73932ca01557ba05a67faa6e8c39133a86e81054e9528...
[发送到网络] { K_S(pkg), K_B^+(K_S), IV }
========== Bob 接收端 ==========
Step1 解密得到 K_S (32 bytes): 2306fb0f74d43d758467defabedff8bae3e3ddf099cee96ba9cf8c2881f406f0
Step2 对称解密成功,预备包大小: 302 bytes
Step3 分离出消息: Bob, I love you! Meet me tonight. -- Alice
Step4 H(m) 本地计算哈希 (32 bytes): b51010720683a422c039adb305d70c7735d4b6cbc79df95eaa1c77e887993c8b
Step5 签名验证: [OK] 签名有效!消息来自Alice且未被篡改
========== 防篡改演示 ==========
篡改后的消息: Bob, I hate you! -- Trudy
签名验证: [FAIL] 签名不匹配!检测到篡改!
=================================================
  安全保证总结:
  机密性:只有Bob的私钥能解出 K_S,进而解密内容
  认证  :只有Alice的私钥能生成有效签名
  完整性:篡改消息会导致哈希不匹配,签名验证失败
=================================================

编译命令:

g++ -o secure_email secure_email.cpp -lssl -lcrypto -std=c++17
./secure_email

关键概念速查表


概念 符号 含义
会话密钥 K S K_S KS 随机生成的对称密钥,每次通信不同
Bob的公钥 K B + K_B^+ KB+ 公开,任何人可用它加密
Bob的私钥 K B − K_B^- KB 只有Bob有,用于解密
Alice的私钥 K A − K_A^- KA 只有Alice有,用于签名
Alice的公钥 K A + K_A^+ KA+ 公开,任何人可用它验证Alice的签名
哈希函数 H ( ⋅ ) H(\cdot) H() 将任意长度消息压缩成固定长度摘要
数字签名 K A − ( H ( m ) ) K_A^-(H(m)) KA(H(m)) 用Alice私钥对哈希值加密
Nonce R R R 一次性随机数,防重放攻击
CA - 证书颁发机构,公钥可信第三方
PGP - Pretty Good Privacy,实用邮件加密系统

网络安全核心术语:从零理解

前言:为什么需要这些?

想象一下:Alice 要给 Bob 发一封情书,但坏人 Trudy 在网络上偷听。
我们需要解决三个问题:

1. 保密:Trudy 看不到信的内容
2. 认证:Bob 确认信真的是 Alice 写的
3. 完整:信在路上没有被 Trudy 偷改

下面的每个术语,都是为了解决这三个问题中的某一部分。

一、加密的两种方式

在学具体符号之前,先搞清楚加密有两大流派。

1.1 对称加密(一把钥匙开一把锁)

加密和解密用的是同一把钥匙
Alice ──[钥匙K加密]──> 密文 ──[钥匙K解密]──> Bob
比喻:一把锁配一把钥匙,Alice 和 Bob 各持一把复制品

优点: 速度极快(AES 可以加密大文件)
缺点: 钥匙怎么安全地传给对方?(密钥分发问题)

1.2 非对称加密(公钥/私钥,两把配对的钥匙)

加密用公钥,解密用私钥(或反过来用于签名)
Bob 生成一对钥匙:
  公钥 K_B^+  ──── 贴在门口,任何人都能看到
  私钥 K_B^-  ──── 藏在保险箱里,只有 Bob 有
Alice 想给 Bob 发秘密:
  用 Bob 的公钥加密 ──> 只有 Bob 的私钥才能解开
比喻:Bob 在门口放了一个开口朝上的信箱(公钥)
      任何人都能投信进去(加密)
      但只有 Bob 有钥匙打开信箱取信(解密)

优点: 不需要提前共享秘密钥匙
缺点: 速度慢,不适合加密大量数据

二、逐个术语从零理解

K S K_S KS:会话密钥(Session Key)

是什么: 一个随机生成的对称密钥,只用于这一次通信。
为什么需要它:

问题:非对称加密(RSA)太慢,直接用来加密一封长邮件要等很久。
解决方案:
  Step 1:随机生成一个短小的对称密钥 K_S(只有 32 字节)
  Step 2:用 K_S 快速加密长消息(AES,毫秒级)
  Step 3:用 RSA 只加密这个短小的 K_S(RSA 加密小数据很快)
这样:速度快(AES加密消息)又安全(RSA保护K_S)

生命周期:

通信开始 ──> 随机生成 K_S ──> 用完这次通信 ──> 丢弃
下次通信 ──> 重新生成新的 K_S
每次都不同,即使一个 K_S 泄露,也不影响其他通信

公式含义:
K S ( m ) = 用 K S 加密消息 m 得到的密文 K_S(m) = \text{用} K_S \text{加密消息} m \text{得到的密文} KS(m)=KS加密消息m得到的密文
K S − 1 ( K S ( m ) ) = m (用同一个 K S 解密还原) K_S^{-1}(K_S(m)) = m \quad \text{(用同一个} K_S \text{解密还原)} KS1(KS(m))=m(用同一个KS解密还原)

K B + K_B^+ KB+:Bob 的公钥(Public Key)

是什么: Bob 的非对称密钥对中的公开部分,可以告诉全世界。
用来干什么:加密(锁上)

场景:Alice 想给 Bob 发只有 Bob 能看到的东西
Alice 用 K_B^+ 加密  ──────>  Bob 用 K_B^- 解密
(任何人都能做这步)           (只有 Bob 能做这步)

公式含义:
K B + ( K S ) = 用 Bob 的公钥加密会话密钥 K S K_B^+(K_S) = \text{用 Bob 的公钥加密会话密钥} K_S KB+(KS)= Bob 的公钥加密会话密钥KS
现实类比:

K_B^+ 就像 Bob 的邮箱地址:
  - 公开的,谁都知道
  - 任何人都能往这个地址投信(加密)
  - 但只有 Bob 能开这个邮箱取信(解密)

K B − K_B^- KB:Bob 的私钥(Private Key)

是什么: Bob 的非对称密钥对中的私密部分,绝对不能泄露。
用来干什么:解密(开锁)

只有 Bob 持有 K_B^-,所以只有 Bob 能解开用 K_B^+ 加密的内容
K_B^-(K_B^+(K_S)) = K_S   ← 解密还原出会话密钥

泄露后果:

K_B^- 泄露 = 所有用 K_B^+ 加密发给 Bob 的内容都能被解读
                          = Bob 的通信安全完全崩溃

现实类比:

K_B^- 就像 Bob 邮箱的钥匙:
  - 只有 Bob 有
  - 丢了(泄露)就要换一对新的公私钥

K A − K_A^- KA:Alice 的私钥(用于签名)

是什么: Alice 的非对称密钥对中的私密部分。
用来干什么:签名(证明"这是我写的")

私钥有个特殊用法(与加密方向相反):
正常加密方向:公钥加密 → 私钥解密(保密)
签名方向:    私钥签名 → 公钥验证(认证)
Alice 用私钥 K_A^- 对消息"签名":
  因为世界上只有 Alice 有 K_A^-
  所以这个签名只有 Alice 能产生
  任何人用 K_A^+ 验证通过,就能证明签名确实来自 Alice

现实类比:

K_A^- 就像 Alice 的亲笔签名能力:
  - 只有 Alice 能写出"Alice的签名"(私钥签名)
  - 任何人都能对照样本验真假(公钥验证)
  - 但没人能伪造这个签名(私钥不泄露的前提下)

K A + K_A^+ KA+:Alice 的公钥(用于验证签名)

是什么: Alice 的非对称密钥对中的公开部分。
用来干什么:验证签名

Bob 收到自称 Alice 发来的消息,想验证真假:
Step 1:取出消息中的数字签名
Step 2:用 Alice 的公钥 K_A^+ 解开签名,得到哈希值
Step 3:自己计算消息的哈希值
Step 4:两个哈希值一样 = 签名有效 = 消息确实来自 Alice

公式含义:
K A + ( K A − ( H ( m ) ) ) = ? H ( m ) K_A^+(K_A^-(H(m))) \stackrel{?}{=} H(m) KA+(KA(H(m)))=?H(m)
左边:用公钥解开签名 右边:本地计算的哈希 \text{左边:用公钥解开签名} \quad \text{右边:本地计算的哈希} 左边:用公钥解开签名右边:本地计算的哈希
相等 = 验证通过 \text{相等 = 验证通过} 相等 = 验证通过

H ( ⋅ ) H(\cdot) H():哈希函数(Hash Function)

是什么: 一个"压缩机",把任意长度的消息变成固定长度的摘要。
特性:

输入:任意长度的消息 m(可以是 1GB 的文件)
输出:固定长度的摘要(SHA-256 输出 32 字节,永远是 32 字节)
特性1:单向性
  已知 H(m),无法反推出 m
特性2:抗碰撞性
  找不到两个不同的消息 m1 ≠ m2,使得 H(m1) = H(m2)
特性3:雪崩效应
  消息改动一个字节,输出的哈希值完全不同

雪崩效应演示:

原始:H("Bob, I love you")    = a3f8c2d1...
篡改:H("Bob, I hate you")    = 9b2e7f4a...
                                  完全不同!

为什么签名前先哈希:

问题:RSA 签名只能处理固定长度的数据,而且对长数据很慢
解决:先哈希,把任意长度的消息压缩成 32 字节
     再对这 32 字节签名(快!)
H(m) 代表了整个消息 m
     改动消息任何一处,H(m) 就会完全变化
     所以对 H(m) 签名 = 对整个消息 m 负责

公式含义:
H ( m ) = 消息 m 的哈希摘要(固定 32 字节,SHA-256) H(m) = \text{消息 m 的哈希摘要(固定 32 字节,SHA-256)} H(m)=消息 m 的哈希摘要(固定 32 字节,SHA-256

K A − ( H ( m ) ) K_A^-(H(m)) KA(H(m)):数字签名(Digital Signature)

是什么: Alice 用私钥对消息哈希值加密的结果,等同于电子签名。
完整流程:

消息 m

H(·)
哈希函数

H(m)
32字节摘要

K_A^-(·)
Alice私钥加密

K_A^-(H(m))
数字签名

为什么能证明身份:

世界上只有 Alice 有 K_A^-
  → 只有 Alice 能产生 K_A^-(H(m))
  → 任何人用 K_A^+ 能验证这个签名
  → 验证通过 = 确实是 Alice 签的

为什么能证明内容未篡改:

签名绑定了 H(m)(消息的哈希)
  → 如果消息被改成 m',H(m') ≠ H(m)
  → 签名验证就会失败
  → Bob 能发现消息被篡改了

类比:

数字签名 = 指纹 + 公证
K_A^-(H(m)) 同时证明了:
  1. 这封信是 Alice 写的(指纹 = 只有她有私钥)
  2. 信的内容没被改过(公证 = 哈希绑定原始内容)

R R R:Nonce(一次性随机数)

是什么: 一个只用一次的随机数,用来证明对方"此刻在线"。
解决的问题——重放攻击(Replay Attack):

场景:
  时刻1:Alice 向 Bob 发送认证消息(含加密密码)
         Trudy 在旁边录下这个消息
  时刻2:Trudy 把录下的消息原样重发给 Bob
         Bob 无法区分:这是 Alice 现在发的?还是录音?
攻击成功!Trudy 冒充了 Alice,但她根本不知道密码是什么。

Nonce 如何防止重放攻击:

Bob Alice Bob Alice 解密得到R 与自己发出的R比对 一致 = Alice此刻在线 我是Alice R = 9a3f7c2b(随机数,此刻生成) K_AB(R)(用共享密钥加密R)

为什么有效:

R 每次都是新随机数,Trudy 录下的是 K_AB(R_old)
下次 Bob 发出的是新的 R_new
Trudy 拿 K_AB(R_old) 来应答 ≠ K_AB(R_new)
Bob 验证失败 → 识破了 Trudy
Alice 能正确回答 K_AB(R_new),因为她知道共享密钥 K_AB

名字的含义:

Nonce = Number used ONCE(只用一次的数)
用完就扔,下次通信再生成新的

CA:证书颁发机构(Certificate Authority)

是什么: 可信的第三方权威机构,负责为公钥"背书"。
解决的问题——公钥冒充:

Alice 想给 Bob 发加密邮件,需要 Bob 的公钥 K_B^+
问题:Alice 怎么知道她拿到的公钥真的是 Bob 的?
攻击:
  Trudy 把自己的公钥 K_T^+ 发给 Alice,谎称"这是 Bob 的公钥"
  Alice 用 K_T^+ 加密消息
  Trudy 用自己的私钥 K_T^- 解密,读到了消息
  Alice 完全不知道!

CA 如何解决:

Step 1:Bob 把自己的公钥 K_B^+ 和身份信息提交给 CA
Step 2:CA 核实 Bob 的身份(就像护照认证)
Step 3:CA 用自己的私钥对 {Bob的身份, K_B^+} 签名
        → 生成数字证书(Certificate)
Step 4:Alice 收到证书,用 CA 的公钥验证签名
        → 验证通过 = 这个公钥确实是 Bob 的
Trudy 无法伪造 CA 的签名(没有 CA 的私钥)

信任链:

Alice 信任 CA
CA 为 Bob 的公钥背书
所以 Alice 可以信任 Bob 的公钥
就像:Alice 信任政府 → 政府颁发护照 → Alice 相信护照上的身份

现实中的 CA:

DigiCert、Let's Encrypt、GlobalSign...
你的浏览器内置了几十个受信任的根 CA 证书
HTTPS 网站的锁头图标,就是 CA 认证的结果

PGP:Pretty Good Privacy

是什么: 1991年由 Phil Zimmermann 编写的邮件加密系统,把上面所有技术整合在一起。
PGP 用到的算法:

哈希(消息摘要):MD5 或 SHA
对称加密(消息体):CAST、Triple-DES 或 IDEA
非对称加密(密钥保护):RSA

PGP 完整流程(既加密又签名):

Alice 发送端:
消息 m
  │
  ├──> H(m) ──> K_A^-(H(m)) ─────────────┐
  │                                        │
  └────────────────────────────────────── (+) 拼接预备包
                                           │
                                    K_S 加密预备包
                                           │
                                   K_B^+(K_S) ──┐
                                                 │
                                          (+) 拼接最终包
                                                 │
                                        ──────> 发送

PGP 的独特之处——信任网络(Web of Trust):

传统 CA 模式(中心化):
  所有人都信任同一个权威机构(CA)
  CA 说谁的公钥是真的,大家就信
PGP 模式(去中心化):
  Alice 信任 Bob → Bob 为 Carol 的公钥背书 → Alice 也可以信任 Carol
  用户之间互相签名公钥,形成"信任网"
  极端情况:举办"密钥签名聚会",面对面交换并签名彼此的公钥
优点:不依赖单一权威,更民主
缺点:信任关系复杂,普通用户难以管理

三、把所有术语串联起来

完整的安全邮件流程(Figure 8.21)

Alice 给 Bob 发一封"既加密、又签名"的邮件:

============ Alice 操作 ============
原始消息 m = "Bob, meet me tonight!"
第1步:签名(认证 + 完整性)
  H(m)           → 计算哈希,得到 32 字节摘要
  K_A^-(H(m))    → 用 Alice 私钥签名
第2步:打包预备包
  预备包 = { m, K_A^-(H(m)) }
第3步:加密(机密性)
  随机生成 K_S   → 会话密钥
  K_S(预备包)    → 对称加密整个预备包(快!)
  K_B^+(K_S)     → 用 Bob 公钥加密会话密钥
第4步:发送
  发送 { K_S(预备包), K_B^+(K_S) }
============ Bob 操作 ============
收到 { K_S(预备包), K_B^+(K_S) }
第1步:解密(拿回 K_S)
  K_B^-(K_B^+(K_S)) = K_S
第2步:解密预备包
  K_S^{-1}(K_S(预备包)) = 预备包 = { m, K_A^-(H(m)) }
第3步:验证签名(认证 + 完整性)
  K_A^+(K_A^-(H(m))) = H(m)      ← 解开签名得到哈希
  H(m) 本地计算                   ← 自己算一遍
  两者比对:相等 = 验证通过!

各术语的安全职责

+------------------+------------------+------------------+
| 术语             | 解决的问题       | 在流程中的位置   |
+------------------+------------------+------------------+
| K_S(会话密钥)  | 快速加密大消息   | 加密预备包       |
| K_B^+(Bob公钥) | 保护 K_S 传输   | 加密 K_S         |
| K_B^-(Bob私钥) | 解密恢复 K_S    | Bob端解密        |
| K_A^-(Alice私钥)| 证明身份       | 对哈希签名       |
| K_A^+(Alice公钥)| 验证签名真假   | Bob端验证        |
| H(·)(哈希函数) | 压缩消息+检测篡改| 签名前预处理    |
| K_A^-(H(m))      | 认证 + 完整性   | 数字签名本体     |
| R(Nonce)       | 防重放攻击      | 认证协议握手     |
| CA               | 公钥可信分发    | 公钥基础设施     |
| PGP              | 整合以上所有    | 完整邮件系统     |
+------------------+------------------+------------------+

四、三大安全目标速查

机密性(Confidentiality)
  谁保证:K_S + K_B^+(混合加密)
  谁破坏:窃听攻击(Eavesdropping)
  防御:只有 Bob 的私钥才能解开 K_S
认证(Authentication)
  谁保证:K_A^- + K_A^+(数字签名)
  谁破坏:冒充攻击(Impersonation)
  防御:只有 Alice 有私钥,签名无法伪造
完整性(Integrity)
  谁保证:H(m)(哈希函数)
  谁破坏:篡改攻击(Tampering)
  防御:改动消息任何一位,哈希值完全不同,签名验证失败

五、常见攻击与对应防御

攻击类型          攻击内容                      防御手段
──────────────────────────────────────────────────────────
窃听              截获明文消息                  K_S 对称加密
IP欺骗            伪造源IP地址                  更强的认证协议
重放攻击          录下认证消息重放              Nonce(R)一次性随机数
中间人攻击        冒充双方,替换公钥            CA 证书,公钥背书
篡改消息          修改传输中的消息内容          H(m) 哈希 + 数字签名
冒充身份          假装是 Alice 发消息           数字签名 K_A^-(H(m))
公钥冒充          谎称某公钥属于 Bob            CA 证书认证
──────────────────────────────────────────────────────────

第8章 8.6~8.7:TLS 与 IPsec 从零理解

前言:为什么需要 TLS 和 IPsec?

Bob 在网上买东西,输入了信用卡号……

没有安全保护时:
Bob ──[信用卡号 明文]──> 互联网 ──> Alice的服务器
                              |
                           Trudy 在这里偷看/篡改/冒充

三种威胁:

威胁 后果
没有加密(机密性) Trudy 截获信用卡号,盗刷
没有完整性 Trudy 把订单数量从1改成100
没有服务器认证 Trudy 假冒 Alice 的网站,骗走钱和信息

TLS 和 IPsec 就是为了解决这三个问题。

8.6 TLS(Transport Layer Security,传输层安全协议)

TLS 是什么?

TLS 在 TCP 之上提供安全服务,让 HTTP 变成 HTTPS。

协议栈对比:
普通 HTTP:
  应用层  HTTP
  传输层  TCP
  网络层  IP
HTTPS(有TLS):
  应用层  HTTP
  安全层  TLS  ← 插在这里,提供加密/认证/完整性
  传输层  TCP
  网络层  IP

你浏览器地址栏有"https://"和小锁图标,就是TLS在工作。
TLS 历史:SSL(Netscape) → TLS 1.0 → TLS 1.1 → TLS 1.2 → TLS 1.3(当前版本,RFC 8446)

8.6.1 Almost-TLS(简化版 TLS,帮助理解原理)

Almost-TLS 分三个阶段:握手 → 密钥派生 → 数据传输

阶段一:握手(Handshake)

目标:

  1. 建立 TCP 连接
  2. Bob 验证 Alice 的身份(服务器认证)
  3. 双方协商出一个共同的主密钥(Master Secret,MS)
Alice(服务器) Bob(客户端) Alice(服务器) Bob(客户端) (a) TCP三次握手 (b) TLS身份认证 Bob验证证书, 生成主密钥MS (c) 传递主密钥 用私钥K_A^-解密 得到MS TCP SYN TCP SYNACK TCP ACK TLS hello(我想建立TLS连接) 证书(含公钥 K_A^+,由CA签名) EMS = K_A^+(MS)(用Alice公钥加密MS)

关键步骤说明:

  • 证书:Alice 把自己的公钥 K A + K_A^+ KA+ 打包进证书,CA 对其签名。Bob 验证 CA 的签名,确认公钥真的属于 Alice。
  • EMS(Encrypted Master Secret):Bob 随机生成主密钥 M S MS MS,用 Alice 的公钥加密:
    E M S = K A + ( M S ) EMS = K_A^+(MS) EMS=KA+(MS)
  • Alice 用私钥解密:
    M S = K A − ( E M S ) = K A − ( K A + ( M S ) ) MS = K_A^-(EMS) = K_A^-(K_A^+(MS)) MS=KA(EMS)=KA(KA+(MS))
    握手结束后,只有 Bob 和 Alice 知道 M S MS MS,Trudy 完全不知道。
阶段二:密钥派生(Key Derivation)

为什么不直接用 MS 加密?

  • 更安全:双向通信各用不同密钥
  • 加密和完整性校验也要用不同密钥
    从 MS 派生出 4 把密钥:
MS(主密钥)
  │
  ├──> E_B^session:Bob→Alice 方向的加密密钥
  ├──> M_B^session:Bob→Alice 方向的 HMAC 密钥(完整性)
  ├──> E_A^session:Alice→Bob 方向的加密密钥
  └──> M_A^session:Alice→Bob 方向的 HMAC 密钥(完整性)

类比:MS 是一把原材料,派生出 4 把专用工具,各司其职。

阶段三:数据传输(Data Transfer)

为什么要分 Record(记录)?
TCP 是字节流,没有天然的消息边界。TLS 把数据切成一段段的 Record,每段单独加密+校验。
Bob 发送一条 TLS Record 的步骤:

原始数据
    │
    ├──[计算 HMAC]── record数据 + M_B + 序列号 ──> HMAC值
    │
    └──[加密]──── record数据 + HMAC ──> 用 E_B 对称加密
                                            │
                                       TLS Record
                                            │
                                        交给 TCP 发送

HMAC 计算公式:
H M A C = H ( r e c o r d ∥ M B s e s s i o n ∥ s e q N u m ) HMAC = H(record \| M_B^{session} \| seqNum) HMAC=H(recordMBsessionseqNum)
其中 ∥ \| 表示拼接, s e q N u m seqNum seqNum 是序列号。
为什么要加序列号?

如果没有序列号:
  Bob 发送了两个 Record:[R1, R2]
  Trudy 把顺序调换:[R2, R1]
  Alice 收到后 HMAC 各自验证通过(每个 Record 本身完整)
  但内容乱序了!
加入序列号后:
  HMAC 绑定了序列号
  R1 的 HMAC 包含 seqNum=1
  Trudy 把 R2 放到位置1,seqNum=1 对应的 HMAC 验证失败
  攻击被检测到!
TLS Record 格式(Figure 8.25)
+--------+---------+--------+------------------+------+
|  Type  | Version | Length |       Data       | HMAC |
| (1字节)| (2字节) | (2字节)|    (变长)        |(变长)|
+--------+---------+--------+------------------+------+
                            |<--- 用 E_B 加密 -------->|

字段 含义
Type Record类型:握手消息 / 应用数据 / 关闭连接
Version TLS版本号
Length 帮助接收方从TCP字节流中切出这条Record
Data 加密后的应用数据
HMAC 完整性校验码(也被加密)

Type、Version、Length 不加密(接收方需要先读这些才知道怎么处理)。

连接关闭(防截断攻击)

截断攻击(Truncation Attack):

正常情况:
  Bob 发完所有数据 → 发 TCP FIN → 连接关闭
攻击:
  Bob 发到一半 → Trudy 伪造一个 TCP FIN 插入
  Alice 以为 Bob 发完了,实际上只收到了一部分!

解决方法: TLS Record 的 Type 字段有专门的"关闭连接"类型。Alice 必须收到带有关闭类型的 TLS Record 才认为连接正常结束;如果先收到 TCP FIN 而没有收到关闭 Record,说明有问题。

8.6.2 真实 TLS 1.3

TLS 1.3 相比旧版的改进

方面 TLS 1.2 TLS 1.3
握手RTT 2 RTT 1 RTT
密钥交换 RSA 或 DH 只用 DH(前向保密)
加密+完整性 分开 AEAD 合一
旧算法 保留兼容 删除不安全算法

TLS 1.3 握手(3条消息,1个RTT)
Server(Alice) Client(Bob) Server(Alice) Client(Bob) 1 准备DH参数 收到Client的DH参数 生成自己的DH值 计算共享Master Secret 验证证书和签名 用DH计算Master Secret 握手完成,开始传输应用数据 Client Hello (TLS版本, 密码套件, nonce_1, DH公钥参数) Server Hello(密码套件, nonce_2, 证书, DH公钥) + Finished(握手消息的哈希,已加密) Client Finished(握手消息哈希,已加密) HTTP GET(受TLS保护)

密码套件(Cipher Suite)包含:

密码套件 = 密钥交换算法 + AEAD加密算法 + 哈希算法
例子:TLS_AES_256_GCM_SHA384
  密钥交换:DH(Diffie-Hellman)
  加密:AES-256-GCM(AEAD,同时提供加密+完整性)
  哈希:SHA-384(用于密钥派生)

AEAD(Authenticated Encryption with Associated Data):
旧方法:先加密,再单独计算 HMAC(两步)。
AEAD:一个算法同时提供加密和认证(更简洁、更安全)。
A E A D ( K , n o n c e , p l a i n t e x t , A A D ) → ( c i p h e r t e x t , t a g ) AEAD(K, nonce, plaintext, AAD) \rightarrow (ciphertext, tag) AEAD(K,nonce,plaintext,AAD)(ciphertext,tag)
其中 t a g tag tag 既验证密文完整性,也验证关联数据(AAD,如 IP 头)的完整性。

Nonce 的必要性(防连接重放 vs 防包重放)
两种不同的重放攻击:
1. 包重放攻击(Packet Replay)
   Trudy 把某个数据包重发一次
   防御:TLS Record 中的序列号(seqNum)
2. 连接重放攻击(Connection Replay)
   Trudy 录下 Bob 昨天的整个连接(所有包)
   今天原样重放给 Alice
   Alice 会以为 Bob 又下了一笔一模一样的订单!
   防御:Nonce!
   每次连接,Alice 发不同的 nonce
   nonce 参与密钥派生,导致两次会话密钥不同
   昨天的加密包用今天的密钥验证 → 失败!
TLS over TCP vs TLS over QUIC
TLS 1.3 + TCP:
Client                           Server
  │──TCP SYN──────────────────>│  }
  │<──TCP SYNACK────────────────│  } TCP握手:1 RTT
  │──TCP ACK──────────────────>│  }
  │──Client Hello─────────────>│  }
  │<──Server Hello + Finished───│  } TLS握手:1 RTT
  │──Client Finished──────────>│  }
  │──HTTP GET(开始!)─────>│  ← 需要 2 RTT 才能发数据
TLS 1.3 + QUIC(HTTP/3):
Client                           Server
  │──QUIC Init + Client Hello─>│  }
  │<──Server Hello + Finished───│  } 合并:只需 1 RTT
  │──Client Finished + HTTP GET>│  ← 1 RTT 就能发数据
Zero RTT(0-RTT):
  如果之前连接过,可以直接发数据,无需等待握手!

8.7 IPsec 与 VPN(网络层安全)

IPsec 是什么?

IPsec 在网络层提供安全,对整个 IP 数据报加密。

TLS 保护:应用层进程之间的通信(HTTP会话)
IPsec 保护:任意两个网络实体之间的所有IP流量
比喻:
  TLS = 给信封里的信加密(只有特定收信人能看)
  IPsec = 给整辆邮车加密(车里所有信都被保护,连送信路线都保密)

IPsec 提供的服务:

  • 机密性(Confidentiality)
  • 数据完整性(Data Integrity)
  • 来源认证(Source Authentication)
  • 防重放攻击(Replay-Attack Prevention)

8.7.1 VPN(Virtual Private Network,虚拟私有网络)

问题: 企业总部和分公司要安全通信,但专用物理网络太贵。
解决: 用公共互联网传输,但把数据加密——这就是 VPN。

VPN 示意图(Figure 8.27):
[总部内网]──[网关路由器R1]──(加密)──[公共互联网]──(解密)──[网关路由器R2]──[分公司内网]
 172.16.1.x    200.168.1.100                              193.68.2.23    172.16.2.x
[出差销售员的笔记本]──(IPsec加密)──[公共互联网]──(IPsec解密)──[总部]

内网内部通信:普通 IPv4,不加密(内网是可信的)。
跨公网通信:加密成 IPsec 数据报,互联网路由器看到的是加密内容,不知道原始数据。

8.7.2 AH 和 ESP 协议

IPsec 有两个核心协议:

协议 全称 认证 完整性 机密性(加密)
AH Authentication Header YES YES NO
ESP Encapsulating Security Payload YES YES YES

实际中 ESP 用得更多(因为 VPN 必须要加密)。

8.7.3 安全关联(Security Association,SA)

SA 是什么?
IPsec 发送数据前,源和目的之间要先建立一条"逻辑安全通道",这就是 SA。
SA 的关键特性:单向!

双向通信需要两条 SA:
R1 ─────────SA1(R1→R2)──────────> R2
R1 <─────────SA2(R2→R1)─────────── R2

SA 存储的信息:

一条 SA 包含:
  SPI(Security Parameter Index):32位标识符,SA的"编号"
  源地址:200.168.1.100
  目的地址:193.68.2.23
  加密算法:如 3DES-CBC 或 AES-256-GCM
  加密密钥:[具体密钥值]
  完整性算法:如 HMAC-MD5 或 HMAC-SHA256
  认证密钥:[具体密钥值]

SAD(Security Association Database):
每个 IPsec 实体(路由器/主机)维护一个数据库,存放所有 SA 的信息。

VPN 例子(总部 + 1个分公司 + n个销售员):
SA数量 = 2(总部↔分公司 双向)+ 2n(总部↔每个销售员 双向)
       = 2(n+1) 条 SA

SPD(Security Policy Database):
SAD 说明"怎么"加密,SPD 说明"哪些"流量需要 IPsec 处理。

SPD 规则示例:
  源IP=172.16.1.x,目的IP=172.16.2.x → 使用 SA#1 进行 IPsec 处理
  源IP=任意,目的IP=8.8.8.8 → 普通 IPv4,不做 IPsec

8.7.4 IPsec 数据报格式(ESP 隧道模式)

打包过程(“卷墨西哥卷饼 Enchilada”)

教材用制作墨西哥卷饼(Enchilada)来比喻打包过程,非常生动:

Step 1:在原始 IPv4 数据报后面加 ESP Trailer
         [原始IP头 | 原始数据] + [ESP Trailer]
Step 2:加密上面整体(用SA指定的加密算法和密钥)
         encrypt([原始IP头 | 原始数据 | ESP Trailer])
Step 3:在加密内容前面加 ESP Header
         [ESP Header] + encrypted([原始IP头 | 原始数据 | ESP Trailer])
         ↑ 这整个叫 "Enchilada"(墨西哥卷饼)
Step 4:对整个 Enchilada 计算 MAC(认证码)
         MAC = HMAC(Enchilada, 认证密钥)
Step 5:把 MAC 附到 Enchilada 后面
         payload = [Enchilada] + [ESP MAC]
Step 6:加上新的 IPv4 头(新IP头指向隧道端点)
         最终:[新IP头 | payload]
IPsec 数据报结构(Figure 8.29)
+----------+-----------+---------------+--------------------+-----------+---------+
| New IP   | ESP       | Original IP   | Original IP        | ESP       | ESP     |
| header   | header    | header        | datagram payload   | trailer   | MAC     |
| (明文)   | (明文)    |               |                    |           |         |
+----------+-----------+---------------+--------------------+-----------+---------+
           |<──────────── "Enchilada" 整体做 MAC 认证 ───────────────────────────>|
                        |<──────────── 这部分加密 ────────────────────────────────>|
ESP header 展开:
  +-------+-------+
  |  SPI  | Seq # |
  +-------+-------+
ESP trailer 展开:
  +---------+------------+-------------+
  | Padding | Pad length | Next header |
  +---------+------------+-------------+

各字段解释:

字段 位置 作用
New IP header 最外层,明文 隧道端点地址(R1→R2),公网路由器用这个转发
ESP header(SPI) 明文 告诉接收方这属于哪条SA,从而知道用什么密钥
ESP header(Seq #) 明文 序列号,防止重放攻击
Original IP header 加密 原始数据报的头部(含真实源/目的地址)
Original payload 加密 原始数据(HTTP、TCP等内容)
ESP trailer(Padding) 加密 块密码需要数据长度是块大小整数倍,补齐用
ESP trailer(Next header) 加密 标明原始数据类型(TCP/UDP/ICMP)
ESP MAC 末尾,明文 对整个 Enchilada 的 HMAC,验证完整性和真实性

新 IP 头 vs 原始 IP 头
原始数据报:
  源IP = 172.16.1.17(总部某台主机)
  目的IP = 172.16.2.48(分公司某台主机)
  → 这两个地址被加密,Trudy 看不到!
IPsec 新IP头:
  源IP = 200.168.1.100(R1,总部网关)
  目的IP = 193.68.2.23(R2,分公司网关)
  协议号 = 50(表示这是 ESP 协议的 IPsec 包)
  → 公网路由器看到这个,把包转发到 R2

IPsec 的机密性远超 TLS!
TLS 只加密 HTTP 内容,但 IP 头(源/目的地址)是明文的。
IPsec 连原始 IP 头都加密了,Trudy 不知道通信的是哪两台机器。

R2 收到后的处理步骤
1. 看到目的IP是自己(R2),接收数据报
2. 看到协议号=50,知道是 IPsec ESP 包
3. 用 SPI 查 SAD,找到对应的 SA
4. 用 SA 中的认证密钥计算 Enchilada 的 MAC,与 ESP MAC 字段比对
   → 不一致 = 数据被篡改或伪造,丢弃!
5. 检查序列号,确认不是重放的旧包
6. 用 SA 中的加密密钥解密 Enchilada
7. 去掉 Padding,取出原始 IP 数据报
8. 把原始 IP 数据报转发进分公司内网

8.7.5 IKE:IPsec 的密钥管理

问题: SA 需要加密密钥、认证密钥……这些密钥从哪来?

  • 小型VPN(2台路由器):管理员手动配置 → “Manual Keying”
  • 大型VPN(几百台设备):手动不现实 → 需要 IKE(Internet Key Exchange) 自动协商
IKE 两阶段协议
路由器 R2(分公司) 路由器 R1(总部) 路由器 R2(分公司) 路由器 R1(总部) Phase 1 第一轮:建立 IKE SA(安全信道) 双方用DH计算共享密钥 建立加密+认证的 IKE SA 信道 同时生成后续IPsec SA的 master secret Phase 1 第二轮:在IKE SA信道内互相认证 协商 IPsec SA 使用的加密/认证算法 Phase 2:建立 IPsec SA 派生两条 SA 各自的加密/认证密钥 DH参数交换(不暴露身份,无签名) DH参数交换 身份 + 签名(通过IKE SA加密传输) 身份 + 签名(通过IKE SA加密传输) 创建 IPsec SA(R1→R2方向) 创建 IPsec SA(R2→R1方向)

IKE SA vs IPsec SA:

IKE SA:
  用于 IKE 协议消息本身的保护(双向,临时使用)
  类比:谈合同时的保密协议
IPsec SA:
  用于实际数据传输的保护(单向)
  类比:正式签订的合同条款

两阶段设计的好处:

Phase 1 第一轮:不用公钥签名(计算量小),建立基础信道
Phase 1 第二轮:在安全信道内做身份认证(身份不被外部窃听)
Phase 2:Phase 2 不用公钥加密,基于 Phase 1 的 master secret 派生密钥
         → 速度快,一个 IKE SA 可以快速创建大量 IPsec SA

TLS vs IPsec 完整对比

+------------------+--------------------+--------------------+
| 特性             | TLS                | IPsec              |
+------------------+--------------------+--------------------+
| 工作层次         | 传输层(之上)      | 网络层             |
| 保护对象         | 特定应用的TCP连接  | 所有IP数据报        |
| 加密范围         | 应用数据           | 原始IP头+数据       |
| 是否隐藏IP地址   | 否                 | 是(隧道模式)      |
| 典型使用场景     | HTTPS网页          | VPN企业内网         |
| 密钥协商         | TLS握手            | IKE协议             |
| 典型协议标识     | https://           | 协议号=50(ESP)    |
+------------------+--------------------+--------------------+

C++ 完整演示代码

以下代码演示 TLS/IPsec 涉及的核心操作:

  • HMAC 计算(完整性校验)
  • TLS Record 的构建与解析
  • AEAD 加密(AES-GCM)
#include <iostream>
#include <iomanip>
#include <vector>
#include <string>
#include <cstring>
#include <stdexcept>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/rand.h>
#include <openssl/sha.h>
// ============================================================
// 工具函数:打印十六进制
// ============================================================
void printHex(const std::string& label, const std::vector<unsigned char>& data) {
    std::cout << label << " [" << data.size() << " bytes]: ";
    for (size_t i = 0; i < data.size() && i < 24; ++i)
        std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)data[i];
    if (data.size() > 24) std::cout << "...";
    std::cout << std::dec << "\n";
}
// ============================================================
// 生成随机字节(用于密钥、Nonce、IV 等)
// ============================================================
std::vector<unsigned char> randomBytes(int n) {
    std::vector<unsigned char> buf(n);
    if (RAND_bytes(buf.data(), n) != 1)
        throw std::runtime_error("随机数生成失败");
    return buf;
}
// ============================================================
// HMAC-SHA256 计算
// 对应 TLS 中的完整性校验:HMAC = H(data || key || seqNum)
// ============================================================
std::vector<unsigned char> computeHMAC(
    const std::vector<unsigned char>& key,    // M_B^session:HMAC 密钥
    const std::vector<unsigned char>& data,   // record 数据
    uint64_t seqNum)                          // 序列号(防重放/重排序)
{
    // 把序列号编码成 8 字节大端序,附在数据后面一起计算 HMAC
    std::vector<unsigned char> input = data;
    for (int i = 7; i >= 0; --i)
        input.push_back((seqNum >> (8 * i)) & 0xFF);
    unsigned char result[EVP_MAX_MD_SIZE];
    unsigned int resultLen = 0;
    // OpenSSL HMAC 函数:
    //   key:HMAC 密钥
    //   input:要认证的数据(含序列号)
    //   EVP_sha256:使用 SHA-256 作为底层哈希
    HMAC(EVP_sha256(),
         key.data(), (int)key.size(),
         input.data(), input.size(),
         result, &resultLen);
    return std::vector<unsigned char>(result, result + resultLen);
}
// ============================================================
// AES-256-GCM 加密(AEAD 模式)
// 同时提供加密(机密性)和认证(完整性)
// 对应 TLS 1.3 中的 AEAD 算法
// ============================================================
struct AEADResult {
    std::vector<unsigned char> ciphertext; // 加密后的密文
    std::vector<unsigned char> tag;        // 认证 tag(16字节)
    std::vector<unsigned char> iv;         // 随机 IV(12字节)
};
AEADResult aeadEncrypt(
    const std::vector<unsigned char>& key,       // 加密密钥(E_B^session)
    const std::vector<unsigned char>& plaintext, // 明文数据
    const std::vector<unsigned char>& aad)       // 关联数据(如 TLS Record 头部)
{
    AEADResult result;
    result.iv = randomBytes(12); // GCM 标准 IV 长度 12 字节
    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
    // 初始化 AES-256-GCM 加密
    EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr);
    // 设置 IV 长度(GCM 标准 12 字节)
    EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, nullptr);
    // 设置密钥和 IV
    EVP_EncryptInit_ex(ctx, nullptr, nullptr, key.data(), result.iv.data());
    // 处理关联数据(AAD):不加密但纳入认证范围
    // AAD 通常是 TLS Record 的头部(Type, Version, Length)
    int len = 0;
    if (!aad.empty())
        EVP_EncryptUpdate(ctx, nullptr, &len, aad.data(), (int)aad.size());
    // 加密明文
    result.ciphertext.resize(plaintext.size());
    EVP_EncryptUpdate(ctx, result.ciphertext.data(), &len,
                      plaintext.data(), (int)plaintext.size());
    // 收尾(GCM 不需要 padding)
    std::vector<unsigned char> finalBlock(16);
    int finalLen = 0;
    EVP_EncryptFinal_ex(ctx, finalBlock.data(), &finalLen);
    // 提取认证 tag(16字节)
    result.tag.resize(16);
    EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, result.tag.data());
    EVP_CIPHER_CTX_free(ctx);
    return result;
}
// ============================================================
// AES-256-GCM 解密(AEAD 模式)
// 同时解密并验证认证 tag
// ============================================================
std::vector<unsigned char> aeadDecrypt(
    const std::vector<unsigned char>& key,
    const std::vector<unsigned char>& ciphertext,
    const std::vector<unsigned char>& tag,
    const std::vector<unsigned char>& iv,
    const std::vector<unsigned char>& aad)
{
    EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
    EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr);
    EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, nullptr);
    EVP_DecryptInit_ex(ctx, nullptr, nullptr, key.data(), iv.data());
    // 设置期望的认证 tag(用于验证)
    EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16,
                        const_cast<unsigned char*>(tag.data()));
    int len = 0;
    if (!aad.empty())
        EVP_DecryptUpdate(ctx, nullptr, &len, aad.data(), (int)aad.size());
    std::vector<unsigned char> plaintext(ciphertext.size());
    EVP_DecryptUpdate(ctx, plaintext.data(), &len,
                      ciphertext.data(), (int)ciphertext.size());
    // 收尾时同时验证 tag:返回 1 = 验证通过,否则 = 数据被篡改
    int ret = EVP_DecryptFinal_ex(ctx, plaintext.data() + len, &len);
    EVP_CIPHER_CTX_free(ctx);
    if (ret != 1)
        throw std::runtime_error("AEAD 认证失败!数据可能被篡改!");
    return plaintext;
}
// ============================================================
// TLS Record 结构
// 对应 Figure 8.25
// ============================================================
struct TLSRecord {
    uint8_t  type;      // 0x17 = 应用数据, 0x15 = 关闭连接, 0x16 = 握手
    uint16_t version;   // TLS 1.3 = 0x0304
    uint16_t length;    // data 字段长度
    std::vector<unsigned char> data; // 加密后的数据 + tag(AEAD模式)
    std::vector<unsigned char> iv;   // 传输时附带 IV
};
// ============================================================
// 构建 TLS Record(发送方)
// 模拟 TLS 数据传输阶段:
//   1. 构造 Record 头(Type, Version, Length)作为 AAD
//   2. 用 AEAD 加密数据(同时认证 AAD)
// ============================================================
TLSRecord buildTLSRecord(
    const std::vector<unsigned char>& sessionKey, // E_B^session:加密密钥
    const std::string& appData,                    // 应用数据(如 HTTP GET)
    uint8_t type = 0x17)                           // 0x17 = 应用数据
{
    TLSRecord rec;
    rec.type    = type;
    rec.version = 0x0304; // TLS 1.3
    std::vector<unsigned char> plaintext(appData.begin(), appData.end());
    // AAD = Record 头部(不加密但纳入认证,防止头部被篡改)
    std::vector<unsigned char> aad = {
        rec.type,
        (uint8_t)(rec.version >> 8),
        (uint8_t)(rec.version & 0xFF),
        (uint8_t)((plaintext.size() >> 8) & 0xFF),
        (uint8_t)(plaintext.size() & 0xFF)
    };
    // AEAD 加密
    auto result = aeadEncrypt(sessionKey, plaintext, aad);
    // data = ciphertext + tag(接收方需要两者才能解密+验证)
    rec.data = result.ciphertext;
    rec.data.insert(rec.data.end(), result.tag.begin(), result.tag.end());
    rec.length = (uint16_t)rec.data.size();
    rec.iv = result.iv;
    return rec;
}
// ============================================================
// 解析 TLS Record(接收方)
// ============================================================
std::string parseTLSRecord(
    const std::vector<unsigned char>& sessionKey,
    const TLSRecord& rec)
{
    // 分离密文和 tag(最后 16 字节是 tag)
    size_t ciphertextLen = rec.data.size() - 16;
    std::vector<unsigned char> ciphertext(rec.data.begin(),
                                          rec.data.begin() + ciphertextLen);
    std::vector<unsigned char> tag(rec.data.begin() + ciphertextLen,
                                   rec.data.end());
    // 重建 AAD(与发送方一致)
    std::vector<unsigned char> aad = {
        rec.type,
        (uint8_t)(rec.version >> 8),
        (uint8_t)(rec.version & 0xFF),
        (uint8_t)((ciphertextLen >> 8) & 0xFF),
        (uint8_t)(ciphertextLen & 0xFF)
    };
    // AEAD 解密并验证
    auto plaintext = aeadDecrypt(sessionKey, ciphertext, tag, rec.iv, aad);
    return std::string(plaintext.begin(), plaintext.end());
}
// ============================================================
// 演示密钥派生(从 Master Secret 派生 4 把密钥)
// 真实 TLS 使用 HKDF,这里用 SHA-256 简化演示
// ============================================================
struct SessionKeys {
    std::vector<unsigned char> E_B; // Bob→Alice 加密密钥(32字节)
    std::vector<unsigned char> M_B; // Bob→Alice HMAC 密钥(32字节)
    std::vector<unsigned char> E_A; // Alice→Bob 加密密钥(32字节)
    std::vector<unsigned char> M_A; // Alice→Bob HMAC 密钥(32字节)
};
SessionKeys deriveKeys(const std::vector<unsigned char>& masterSecret) {
    // 简化:对 MS 加不同"标签"后哈希,派生各密钥
    // 真实 TLS 1.3 使用 HKDF(RFC 5869)
    auto derive = [&](const std::string& label) {
        std::vector<unsigned char> input = masterSecret;
        input.insert(input.end(), label.begin(), label.end());
        std::vector<unsigned char> result(SHA256_DIGEST_LENGTH);
        SHA256(input.data(), input.size(), result.data());
        return result;
    };
    SessionKeys keys;
    keys.E_B = derive("encryption_key_B"); // Bob→Alice 加密密钥
    keys.M_B = derive("mac_key_B");        // Bob→Alice HMAC 密钥
    keys.E_A = derive("encryption_key_A"); // Alice→Bob 加密密钥
    keys.M_A = derive("mac_key_A");        // Alice→Bob HMAC 密钥
    return keys;
}
// ============================================================
// 主程序:演示 TLS 数据传输的完整流程
// ============================================================
int main() {
    std::cout << "===========================================\n";
    std::cout << "   TLS 核心机制演示(C++/OpenSSL)\n";
    std::cout << "===========================================\n\n";
    // ---------- Step 1:模拟握手——生成 Master Secret ----------
    std::cout << "===== Step 1: 握手阶段(生成 Master Secret)=====\n";
    // 真实 TLS 中 MS 通过 DH 密钥协商得出,这里用随机数模拟
    auto masterSecret = randomBytes(48); // TLS 中 MS 通常 48 字节
    printHex("Master Secret (MS)", masterSecret);
    // ---------- Step 2:密钥派生——从 MS 派生 4 把密钥 ----------
    std::cout << "\n===== Step 2: 密钥派生(从 MS 派生 4 把密钥)=====\n";
    auto keys = deriveKeys(masterSecret);
    printHex("E_B (Bob→Alice 加密密钥)", keys.E_B);
    printHex("M_B (Bob→Alice HMAC密钥)", keys.M_B);
    printHex("E_A (Alice→Bob 加密密钥)", keys.E_A);
    printHex("M_A (Alice→Bob HMAC密钥)", keys.M_A);
    // ---------- Step 3:Bob 发送数据——构建 TLS Record ----------
    std::cout << "\n===== Step 3: Bob 发送 TLS Record =====\n";
    std::string httpRequest = "GET /index.html HTTP/1.1\r\nHost: alice.com\r\n\r\n";
    std::cout << "原始 HTTP 请求: " << httpRequest.substr(0, 40) << "...\n";
    auto record = buildTLSRecord(keys.E_B, httpRequest);
    std::cout << "TLS Record Type: 0x" << std::hex << (int)record.type
              << std::dec << " (应用数据)\n";
    std::cout << "TLS Record Version: 0x" << std::hex << record.version
              << std::dec << " (TLS 1.3)\n";
    printHex("加密后的 Data+Tag", record.data);
    // ---------- Step 4:演示 HMAC 序列号机制(almost-TLS)----------
    std::cout << "\n===== Step 4: HMAC + 序列号(防重排序)=====\n";
    std::string msg1 = "Record #0: 请转账100元";
    std::string msg2 = "Record #1: 收款方:Bob";
    std::vector<unsigned char> d1(msg1.begin(), msg1.end());
    std::vector<unsigned char> d2(msg2.begin(), msg2.end());
    auto hmac0 = computeHMAC(keys.M_B, d1, 0); // seqNum=0
    auto hmac1 = computeHMAC(keys.M_B, d2, 1); // seqNum=1
    printHex("HMAC(Record#0, seqNum=0)", hmac0);
    printHex("HMAC(Record#1, seqNum=1)", hmac1);
    // 验证:如果 Trudy 调换顺序,seqNum 不匹配就会失败
    auto hmac_tampered = computeHMAC(keys.M_B, d2, 0); // d2 但用 seqNum=0
    std::cout << "Trudy 把 Record#1 放到位置0,用 seqNum=0 验证:";
    std::cout << (hmac_tampered == hmac1 ? "通过(不应发生)" : "FAIL!检测到重排序攻击!") << "\n";
    // ---------- Step 5:Alice 接收并解密 ----------
    std::cout << "\n===== Step 5: Alice 接收并解密 TLS Record =====\n";
    try {
        auto decrypted = parseTLSRecord(keys.E_B, record);
        std::cout << "解密成功!HTTP 请求: " << decrypted.substr(0, 40) << "...\n";
    } catch (const std::exception& e) {
        std::cout << "解密失败: " << e.what() << "\n";
    }
    // ---------- Step 6:篡改数据后解密失败 ----------
    std::cout << "\n===== Step 6: 篡改攻击演示 =====\n";
    TLSRecord tamperedRecord = record;
    if (!tamperedRecord.data.empty())
        tamperedRecord.data[0] ^= 0xFF; // 翻转第一个字节,模拟篡改
    try {
        auto decrypted = parseTLSRecord(keys.E_B, tamperedRecord);
        std::cout << "解密成功(不应发生!)\n";
    } catch (const std::exception& e) {
        std::cout << "AEAD 检测到篡改:" << e.what() << "\n";
        std::cout << "Trudy 的篡改被发现!\n";
    }
    std::cout << "\n===========================================\n";
    std::cout << "  总结:\n";
    std::cout << "  1. MS 通过 DH 协商,Trudy 不知道\n";
    std::cout << "  2. 4 把密钥各司其职,加密/认证分离\n";
    std::cout << "  3. AEAD 同时保证机密性和完整性\n";
    std::cout << "  4. 序列号防止 Trudy 重排/重放数据包\n";
    std::cout << "===========================================\n";
    return 0;
}

https://godbolt.org/z/34h6sfMWr

===========================================
   TLS 核心机制演示(C++/OpenSSL)
===========================================
===== Step 1: 握手阶段(生成 Master Secret)=====
Master Secret (MS) [48 bytes]: 134ed66ac3a95e794c700456562f75bf1169a89fa81c45b3...
===== Step 2: 密钥派生(从 MS 派生 4 把密钥)=====
E_B (Bob→Alice 加密密钥) [32 bytes]: 35f7ce305de111fd567846f7d31714af6d919d902b58c92a...
M_B (Bob→Alice HMAC密钥) [32 bytes]: aa8802373ad767ad830180888fb99c2a805bce1837198e89...
E_A (Alice→Bob 加密密钥) [32 bytes]: 1627d7662d05cc14f5c97def4e77725f006228b3fa2356c7...
M_A (Alice→Bob HMAC密钥) [32 bytes]: 729d47996d456e175781e06989c237ebef48e65357766fab...
===== Step 3: Bob 发送 TLS Record =====
原始 HTTP 请求: GET /index.html HTTP/1.1
Host: alice.co...
TLS Record Type: 0x17 (应用数据)
TLS Record Version: 0x304 (TLS 1.3)
加密后的 Data+Tag [61 bytes]: 7a892ccff2bdceb99fafdcb74218fa2ec579cc44eb2257ee...
===== Step 4: HMAC + 序列号(防重排序)=====
HMAC(Record#0, seqNum=0) [32 bytes]: 3f16f421bcdb26742dde3046e68f2137284d45316225f999...
HMAC(Record#1, seqNum=1) [32 bytes]: 58d49e53d6140a7f387d8c820652475b666c3d8f688e4104...
Trudy 把 Record#1 放到位置0,用 seqNum=0 验证:FAIL!检测到重排序攻击!
===== Step 5: Alice 接收并解密 TLS Record =====
解密成功!HTTP 请求: GET /index.html HTTP/1.1
Host: alice.co...
===== Step 6: 篡改攻击演示 =====
AEAD 检测到篡改:AEAD 认证失败!数据可能被篡改!
Trudy 的篡改被发现!
===========================================
  总结:
  1. MS 通过 DH 协商,Trudy 不知道
  2. 4 把密钥各司其职,加密/认证分离
  3. AEAD 同时保证机密性和完整性
  4. 序列号防止 Trudy 重排/重放数据包
===========================================

编译命令:

g++ -o tls_demo tls_demo.cpp -lssl -lcrypto -std=c++17
./tls_demo

速查总结

TLS 三阶段

握手(Handshake)
  → 建立 TCP 连接
  → 服务器出示证书(CA签名的公钥)
  → 双方通过 DH 协商出 Master Secret(MS)
密钥派生(Key Derivation)
  MS → 派生 4 把密钥(E_B, M_B, E_A, M_A)
数据传输(Data Transfer)
  数据 → 切成 Record → AEAD加密(含序列号)→ 交给 TCP

IPsec 核心概念

SA(Security Association)
  → 单向的逻辑安全通道
  → 存在 SAD 中,包含:SPI、算法、密钥
ESP 数据报格式(隧道模式)
  → [新IP头 | ESP头(SPI+Seq#) | 加密(原始IP头+数据+ESP尾) | MAC]
IKE(密钥管理)
  → Phase 1:建立安全的 IKE SA 信道(DH协商)
  → Phase 2:在 IKE SA 内建立 IPsec SA,派生会话密钥

防御对照

攻击              TLS防御              IPsec防御
窃听            AEAD加密             ESP加密(连IP头都加密)
篡改数据        AEAD tag验证         ESP MAC(Enchilada HMAC)
重放单包        TLS序列号            ESP序列号
重放整个连接    握手中的 Nonce       IKE Nonce
伪造服务器      CA证书验证           IKE证书验证
截断连接        TLS关闭类型字段      不适用(连接级别)

从零理解 TLS 与 IPsec:通俗易懂的网络安全协议指南

目录

  1. 为什么需要加密通信?
  2. 核心密码学概念(必读基础)
  3. TLS 深度解析
  4. IPsec 深度解析
  5. TLS vs IPsec:怎么选?
  6. 常见问题 FAQ

1. 为什么需要加密通信?

1.1 明文通信的危险

想象你在咖啡馆用 Wi-Fi,你向银行发送了:

POST /transfer HTTP/1.1
账号: 622848xxxx
密码: mypassword123
转账金额: 10000

同一个 Wi-Fi 下的任何人都能抓到这条消息。 这就是"中间人攻击"(Man-in-the-Middle, MITM)。
攻击者能做三件坏事:

攻击类型 说明 例子
窃听(Eavesdropping) 读取你的数据 看到你的密码
篡改(Tampering) 修改数据内容 把转账金额改成 100000
伪装(Impersonation) 假冒服务器 钓鱼网站骗你的信息

1.2 安全通信的三个目标

机密性(Confidentiality)── 别人看不到内容
完整性(Integrity)─────── 内容没有被篡改
认证性(Authentication)── 确认对方是真实身份

TLS 和 IPsec 都是为了实现这三个目标,只是工作在不同的层次。

2. 核心密码学概念(必读基础)

理解 TLS 和 IPsec 之前,必须先懂这几个概念。

2.1 对称加密:共享一把钥匙

发送方          相同的密钥           接收方
  │   ─── 密钥 K ──────────────→   │
  │                                 │
  │  明文 → [加密 K] → 密文 →→→→ [解密 K] → 明文
  • 特点:速度快,适合加密大量数据
  • 问题:密钥怎么安全地传给对方?
  • 常见算法:AES(目前主流)、ChaCha20

2.2 非对称加密:公钥 + 私钥

每个人都有一对钥匙:
  公钥(Public Key)  ── 公开给所有人
  私钥(Private Key) ── 只有自己知道
加密流程:
  发送方用接收方的【公钥】加密 → 只有接收方用【私钥】才能解密
签名流程:
  发送方用【私钥】签名 → 任何人用【公钥】验证签名真实性
  • 特点:不需要提前共享密钥
  • 问题:速度慢,只适合加密少量数据(如密钥本身)
  • 常见算法:RSA、ECDSA、ECDH

2.3 Diffie-Hellman 密钥交换(DH)

这是一个神奇的数学协议,让双方在公开信道上协商出一个共同的秘密,而监听者无法得知。
通俗比喻——颜色混合法

Alice 和 Bob 公开约定一个"基础颜色":黄色
Alice 选秘密颜色:红色   →  黄色 + 红色 = 橙色  →  发给 Bob
Bob  选秘密颜色:蓝色   →  黄色 + 蓝色 = 绿色  →  发给 Alice
Alice 收到绿色 + 自己的红色 = 棕色(最终共享密钥)
Bob  收到橙色 + 自己的蓝色 = 棕色(最终共享密钥)
窃听者只看到:黄色、橙色、绿色 ── 无法还原出棕色!
  • ECDH(椭圆曲线 DH)是现代版本,更安全更快速

2.4 消息认证码(MAC)与哈希

哈希函数:任意长度的数据 → 固定长度的"指纹"
  "hello"       → a94f56c3...(SHA-256)
  "hello!"      → 5891b5b5...(完全不同!)
MAC = Hash(数据 + 密钥)
  作用:既能验证数据完整性,又能验证发送者身份
  常用:HMAC-SHA256

2.5 数字证书与 CA

问题:我怎么知道这个"公钥"真的属于 google.com,而不是黑客伪造的?
解决方案:数字证书
  ┌─────────────────────────────────────┐
  │ 证书内容:                           │
  │   所有者:google.com                 │
  │   公钥:abc123...                   │
  │   有效期:2024-01 至 2025-01         │
  │   颁发机构(CA):DigiCert           │
  │   CA 的数字签名:xyz789...           │
  └─────────────────────────────────────┘
CA(证书颁发机构)是被广泛信任的机构(如 DigiCert、Let's Encrypt)
你的操作系统/浏览器预装了这些 CA 的根证书

3. TLS 深度解析

Transport Layer Security,工作在传输层之上、应用层之下

3.1 TLS 是什么?在哪里工作?

OSI 模型                    TLS 的位置
┌─────────────┐
│  应用层      │  ← HTTP、FTP、SMTP 等(使用 TLS 后变成 HTTPS、FTPS、SMTPS)
├─────────────┤
│  表示层      │  ← TLS 工作在这里(保护应用数据)
│  会话层      │
├─────────────┤
│  传输层      │  ← TCP(TLS 建立在 TCP 之上)
├─────────────┤
│  网络层      │  ← IP
├─────────────┤
│  数据链路层   │
│  物理层      │
└─────────────┘

TLS 保护的是「一条应用连接」,比如你的浏览器和 google.com 之间。

3.2 TLS 版本演进


版本 发布年份 现状
SSL 2.0 1995 禁用,有严重漏洞
SSL 3.0 1996 禁用(POODLE 攻击)
TLS 1.0 1999 已废弃
TLS 1.1 2006 已废弃
TLS 1.2 2008 ⚠️ 仍在使用,逐步退出
TLS 1.3 2018 当前标准,强烈推荐

3.3 TLS 1.2 握手流程(经典版)

客户端(浏览器)                        服务器(google.com)
     │                                        │
     │──── ① ClientHello ─────────────────→  │
     │     (支持的加密套件列表、随机数 C)        │
     │                                        │
     │  ←── ② ServerHello ─────────────────  │
     │     (选定的加密套件、随机数 S)            │
     │                                        │
     │  ←── ③ Certificate ─────────────────  │
     │     (服务器的数字证书,含公钥)            │
     │                                        │
     │  ←── ④ ServerHelloDone ─────────────  │
     │                                        │
     │   [客户端验证证书合法性]                  │
     │                                        │
     │──── ⑤ ClientKeyExchange ───────────→  │
     │     (用服务器公钥加密的"预主密钥")        │
     │                                        │
     │──── ⑥ ChangeCipherSpec ────────────→  │
     │──── ⑦ Finished(加密) ────────────→  │
     │                                        │
     │  ←── ⑧ ChangeCipherSpec ────────────  │
     │  ←── ⑨ Finished(加密) ────────────  │
     │                                        │
     │════════ 应用数据(加密传输)════════════  │

密钥是怎么生成的?

预主密钥(PreMasterSecret)  ← 客户端生成,用服务器公钥加密传过去
主密钥(MasterSecret)      = PRF(PreMasterSecret, 随机数C, 随机数S)
会话密钥                    = 从主密钥派生出多个对称密钥
                               ├── 客户端加密密钥
                               ├── 服务器加密密钥
                               ├── 客户端 MAC 密钥
                               └── 服务器 MAC 密钥

问题:TLS 1.2 需要 2 个 RTT(往返时延),握手慢!

3.4 TLS 1.3 握手流程(现代版)

TLS 1.3 的最大改进是:把握手从 2 RTT 压缩到 1 RTT(甚至 0 RTT)

客户端                                      服务器
     │                                        │
     │──── ① ClientHello ─────────────────→  │
     │     (加密套件 + DH 公钥 key_share)      │
     │     ← 一步搞定,直接带 DH 参数!         │
     │                                        │
     │  ←── ② ServerHello ─────────────────  │
     │     (选定套件 + 服务器 DH 公钥)          │
     │  ←── ③ {Certificate} 加密             │
     │  ←── ④ {CertificateVerify} 加密       │
     │  ←── ⑤ {Finished} 加密               │
     │                                        │
     │   [双方已经可以计算出会话密钥!]           │
     │                                        │
     │──── ⑥ {Finished} 加密 ─────────────→  │
     │                                        │
     │════════ 应用数据(加密传输)════════════  │

TLS 1.3 的核心改进:

改进 1:强制使用 ECDH 密钥交换(前向保密)
  即使私钥将来泄露,历史数据也无法被解密
改进 2:删除所有不安全的算法
  不再支持 RSA 密钥交换、RC4、MD5、SHA-1...
改进 3:证书加密传输
  TLS 1.2 中证书是明文的,1.3 中加密,避免泄露身份信息
改进 4:0-RTT 会话恢复(Session Resumption)
  对于已经连接过的服务器,客户端可以在第一个数据包就发送应用数据!
  (安全性注意:0-RTT 数据不具备重放保护,仅适合幂等操作)

3.5 TLS 加密套件(Cipher Suite)

加密套件是一组算法的组合名称,例如:

TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
分解:
  TLS          ── 协议
  ECDHE        ── 密钥交换算法(椭圆曲线 DH,临时密钥)
  RSA          ── 身份认证算法(验证证书)
  AES_128_GCM  ── 对称加密算法(128位AES,GCM模式)
  SHA256       ── 消息认证哈希算法

TLS 1.3 精简为 5 个套件(全部强制前向保密):

TLS_AES_128_GCM_SHA256         ← 最常用
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256   ← 移动设备友好
TLS_AES_128_CCM_SHA256
TLS_AES_128_CCM_8_SHA256

3.6 HTTPS 实战:访问 https://example.com 发生了什么?

1. DNS 解析:example.com → 93.184.216.34
2. TCP 三次握手:建立 TCP 连接
3. TLS 握手(如上流程):
   ├── 协商加密算法
   ├── 验证服务器证书
   └── 生成会话密钥
4. HTTP 请求(加密传输):
   加密前:GET / HTTP/1.1\r\nHost: example.com\r\n
   加密后:一段看不懂的字节流
5. HTTP 响应(加密传输):
   加密后:一段看不懂的字节流
   解密后:<html>...</html>

4. IPsec 深度解析

Internet Protocol Security,工作在网络层,保护 IP 数据包

4.1 IPsec 是什么?在哪里工作?

OSI 模型                    IPsec 的位置
┌─────────────┐
│  应用层      │  ← HTTP、FTP、SSH 等(应用程序无需修改!)
├─────────────┤
│  表示层      │
│  会话层      │
├─────────────┤
│  传输层      │  ← TCP、UDP(对 IPsec 透明)
├─────────────┤
│  网络层      │  ← IPsec 工作在这里(直接保护 IP 数据包)
├─────────────┤
│  数据链路层   │
│  物理层      │
└─────────────┘

IPsec 保护的是「IP 数据包」,对上层应用完全透明!
典型用途:VPN(虚拟私人网络)

4.2 IPsec 的组成模块

IPsec 不是单一协议,而是一套协议框架:

IPsec 框架
├── 两种协议
│   ├── AH(Authentication Header)─── 只做认证,不加密
│   └── ESP(Encapsulating Security Payload)── 认证 + 加密(常用)
│
├── 两种模式
│   ├── 传输模式(Transport Mode)── 保护 IP 数据包的"内容"
│   └── 隧道模式(Tunnel Mode)──── 保护整个原始 IP 数据包(含头部)
│
└── 密钥管理
    └── IKE(Internet Key Exchange)── 协商建立 SA(安全关联)

4.3 AH vs ESP

AH(Authentication Header)- 只认证不加密
原始 IP 包:
[ IP 头 | 数据 ]
AH 处理后:
[ IP 头 | AH 头 | 数据 ]
         └── 包含对整个包的签名(但不加密数据!)
  • 防止数据被篡改
  • 防止 IP 头被篡改(包括源/目IP)
  • 不加密,数据内容可见
  • 与 NAT 不兼容(NAT 会修改 IP 头,破坏签名)
  • 实际很少使用
ESP(Encapsulating Security Payload)- 加密 + 认证
原始 IP 包:
[ IP 头 | 数据 ]
ESP 处理后:
[ IP 头 | ESP 头 | 加密(数据 + ESP 尾) | ESP 认证 ]
          └─────────────────────────────┘
          这一段是加密和认证保护的范围
  • 加密数据,内容保密
  • 认证,防止篡改
  • 与 NAT 可兼容(NAT-T 技术,封装在 UDP 4500 端口)
  • 实际 VPN 的主力协议

4.4 传输模式 vs 隧道模式

传输模式(Transport Mode)
适用场景:两台主机之间直接通信(端到端)
原始数据包:
[ 原始IP头: src=A, dst=B | TCP头 | 数据 ]
传输模式处理后:
[ 原始IP头: src=A, dst=B | ESP头 | 加密(TCP头 + 数据) | ESP认证 ]
特点:保护的是"内容",IP头不变(A→B 的路由信息可见)
用途:主机到主机的通信(如两台服务器之间)
隧道模式(Tunnel Mode)
适用场景:网关到网关,建立 VPN 隧道(最常见!)
原始数据包:
[ 内网IP头: src=192.168.1.10, dst=10.0.0.5 | TCP头 | 数据 ]
隧道模式处理后:
[ 新IP头: src=GW-A, dst=GW-B | ESP头 | 加密(原始IP头 + TCP头 + 数据) | ESP认证 ]
  └─────外层IP头────┘          └──────────────内层(完全加密)──────────────┘
特点:整个原始 IP 包被加密包裹,对外只看到网关的 IP 地址
用途:企业 VPN(路由器/防火墙之间)、远程接入 VPN

4.5 IKE:IPsec 的密钥协商协议

IKEv2 是目前标准,分两个阶段:

阶段一:IKE_SA(建立控制通道)
目的:建立一条安全的"控制通道",用于协商后续密钥
发起方                               响应方
    │                                   │
    │── IKE_SA_INIT (请求) ──────────→  │
    │   含:DH 公钥、支持的算法、随机数    │
    │                                   │
    │  ← IKE_SA_INIT (响应) ──────────  │
    │   含:DH 公钥、选定算法、随机数     │
    │                                   │
    │  [双方计算出共享密钥,以下消息加密]  │
    │                                   │
    │── IKE_AUTH (请求) 加密 ─────────→  │
    │   含:身份标识、证书/预共享密钥、签名 │
    │                                   │
    │  ← IKE_AUTH (响应) 加密 ─────────  │
    │   含:服务器身份、证书、签名         │
    │                                   │
    │  ========= IKE SA 建立完成 ======= │
阶段二:Child SA(建立数据通道)
目的:在 IKE SA 的保护下,建立真正传输数据的 IPsec SA
发起方                               响应方
    │                                   │
    │── CREATE_CHILD_SA (请求) 加密 ──→  │
    │   含:要保护的流量范围(如 TCP 443)  │
    │       新的 DH 交换(可选)           │
    │       提议的加密算法                │
    │                                   │
    │  ← CREATE_CHILD_SA (响应) 加密 ──  │
    │   含:选定的算法、SPI(安全参数索引) │
    │                                   │
    │  ========= IPsec SA 建立完成 ===== │
    │  现在可以用 ESP/AH 传输数据了!      │

4.6 安全关联(SA):IPsec 的核心概念

SA(Security Association)是 IPsec 的"连接"单元:

每个 SA 包含:
  SPI(Security Parameter Index)── 32位标识符,接收方用来查找对应SA
  加密算法 + 密钥                 ── 如 AES-256-GCM + 密钥K
  认证算法 + 密钥                 ── 如 HMAC-SHA256 + 密钥M
  序列号                          ── 防止重放攻击
  有效期                          ── 定时更新密钥
注意:SA 是单向的!
  A→B 通信 需要一个 SA
  B→A 通信 需要另一个 SA(反向)

SA 存储在本地的 SAD(Security Association Database,安全关联数据库) 中。
还有一个 SPD(Security Policy Database,安全策略数据库) 定义哪些流量需要 IPsec 保护:

SPD 示例规则:
  源IP: 192.168.1.0/24  目的IP: 10.0.0.0/24  → 使用 IPsec(ESP,隧道模式)
  源IP: any             目的IP: 8.8.8.8        → 直接通过(不加密)
  其他流量                                     → 丢弃

4.7 完整的企业 VPN 流量流程

场景:员工(192.168.1.10)通过 VPN 访问公司服务器(10.0.0.5)
员工 PC                VPN 网关 A              VPN 网关 B            公司服务器
192.168.1.10          1.2.3.4                 5.6.7.8              10.0.0.5
  │                      │                       │                      │
  │ 1. 普通数据包         │                       │                      │
  │  [src:192.168.1.10  │                       │                      │
  │   dst:10.0.0.5 | 数据]                       │                      │
  │─────────────────────→│                       │                      │
  │                      │                       │                      │
  │                      │ 2. 隧道封装(加密)    │                      │
  │                      │  [src:1.2.3.4        │                      │
  │                      │   dst:5.6.7.8        │                      │
  │                      │   | ESP | 加密(原始包)]│                      │
  │                      │───────────────────────→                      │
  │                      │                       │                      │
  │                      │                       │ 3. 解封装(解密)    │
  │                      │                       │  恢复原始数据包       │
  │                      │                       │──────────────────────→
  │                      │                       │                      │
  │                      │         ← 4. 响应包(同样加密传回)           │

5. TLS vs IPsec:怎么选?

5.1 核心区别对比


特性 TLS IPsec
工作层次 传输层以上(第5-6层) 网络层(第3层)
保护范围 单个应用连接 所有 IP 流量
应用感知 应用需要支持(如 HTTPS) 对应用完全透明
典型用途 HTTPS、API、邮件加密 企业 VPN、站点互联
配置复杂度 相对简单 较复杂
NAT 兼容性 天然兼容 需要 NAT-T 处理
前向保密 TLS 1.3 强制支持 IKEv2 支持(PFS)
性能开销 较低 稍高(隧道封装)

5.2 使用场景决策树

我需要保护哪类通信?
        │
        ├─→ 网站/API/Web 应用 ─────────────── → 用 TLS(HTTPS)
        │
        ├─→ 两个办公室网络互联 ──────────────── → 用 IPsec(站点到站点 VPN)
        │
        ├─→ 远程员工接入企业内网 ─────────────→ 用 IPsec(远程接入 VPN)
        │                                        也可用 TLS-based VPN(如 OpenVPN、WireGuard)
        │
        ├─→ 邮件加密 ────────────────────────→ 用 TLS(STARTTLS/SMTPS)
        │
        └─→ 数据库连接加密 ──────────────────→ 用 TLS

5.3 两者可以同时使用吗?

可以!而且很常见。

员工电脑  ──── IPsec VPN ────  公司内网  ──── TLS(HTTPS) ────  内部服务器
外层:IPsec 保护网络连接(员工←→公司)
内层:TLS  保护应用数据  (浏览器←→服务器)
双重加密,安全性更高。

6. 常见问题 FAQ

Q1:HTTPS 为什么不用 IPsec?

HTTPS 的服务器面向所有互联网用户,IPsec 需要双方都配置 SA,不适合大规模公共互联网服务。TLS 只需要服务器有证书,客户端(浏览器)直接支持,更灵活。

Q2:VPN 一定用 IPsec 吗?

不是。常见 VPN 协议有:

协议 底层 特点
IPsec/IKEv2 IPsec 原生系统支持,快速
OpenVPN TLS 开源,穿透性好
WireGuard 自定义 极简,高性能,新一代
L2TP/IPsec IPsec 旧式,仍广泛使用

Q3:为什么 TLS 1.3 比 1.2 更安全?

  1. 强制前向保密(PFS):每次会话密钥独立,私钥泄露不影响历史记录
  2. 删除所有已知不安全算法(RSA 密钥交换、RC4、MD5 等)
  3. 证书信息加密传输,防止身份泄露
  4. 握手流程简化,减少攻击面

Q4:什么是"前向保密"(Perfect Forward Secrecy, PFS)?

没有 PFS 的情况:
  用服务器的长期私钥加密会话密钥
  → 如果私钥将来被盗,攻击者可以解密所有历史会话
有 PFS 的情况(ECDH/DH):
  每次会话用临时密钥对(用完即丢)
  → 即使私钥将来被盗,也无法解密历史会话
  → 因为临时私钥早就销毁了!

Q5:证书过期了会怎样?

浏览器/客户端会:

  1. 检查证书有效期
  2. 如果过期 → 显示安全警告,阻止访问
  3. 用户强行继续 → 仍能连接,但失去身份验证保证
    Let’s Encrypt 提供免费 90 天证书,可配置自动续期(certbot)。

小结:一图理解 TLS 和 IPsec

┌─────────────────────────────────────────────────────────────────┐
│                         你的应用数据                              │
├─────────────────────────────────────────────────────────────────┤
│         TLS 保护:加密 + 认证单个应用连接                          │
│         工具:对称加密(AES) + 密钥交换(ECDH) + 证书(X.509)         │
├─────────────────────────────────────────────────────────────────┤
│                          TCP / UDP                               │
├─────────────────────────────────────────────────────────────────┤
│  IPsec 保护:加密 + 认证整个 IP 包(对应用透明)                    │
│  工具:ESP/AH + IKEv2 密钥协商 + 隧道/传输模式                     │
├─────────────────────────────────────────────────────────────────┤
│                         物理网络                                  │
└─────────────────────────────────────────────────────────────────┘
共同目标:机密性 + 完整性 + 认证性
核心武器:对称加密 + 非对称加密 + DH 密钥交换 + 数字证书
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐