一、加密算法
1. 非对称加密
RSA:将乘积公开作为加密密钥,即公钥,而两个大素数组合成私钥。 为了提升安全性,需要不断加长密钥长度,而这又会使加解密负担加重,因此需要一个新的算法来替代。
ECC :根据有限域上的椭圆曲线上的点群中的离散对数问题 ECDLP,ECDLP 是比因子分解问题更难的问题,它是指数级的难度。相比RSA有优势: 抗攻击性强 CPU 占用少 内容使用少 网络消耗低 加密速度快
ECC = 一种数学曲线(椭圆曲线密码学),是"原料"
DH = 一种密钥交换协议(Diffie-Hellman),是"用法"
ECDH = 在椭圆曲线上做 DH 密钥交换,是"具体产品"
非对称加密两大用途:
js
┌─────────────────────────────────────────┐
│ 非对称密码的两大用途 │
│ │
│ 1. 密钥协商 2. 数字签名 │
│ ├─ DH ├─ DSA │
│ ├─ ECDH ✓ ├─ ECDSA ✓ │
│ └─ X25519 └─ Ed25519 │
│ (现代首选) (现代首选) │
│ │
│ 底层数学基础:ECC(椭圆曲线) │
│ 曲线名:P-256, P-384, Curve25519... │
└─────────────────────────────────────────┘
2. 对称加密
对称加密的特点:加解密使用同一个密钥,速度相比非对称加密要快。 常见的对称加密算法:DES、3DES、AES
-
DES:已破解,不再安全,基本没有企业在用了,是对称加密算法的基石,具有学习价值
-
DESede(三重DES):替代DES出现的,计算密钥时间太长、加密效率不高,所以也基本上不用
-
AES:最常用的对称加密算法。密钥建立时间短、灵敏性好、内存需求低(不管怎样,反正就是好)
3. 哈希算法
散列解密算法的特点:不可逆/唯一/定长。 MD5速度更快,无法抵御碰撞攻击,但不适合用来做安全性校验; SHA1已被攻破,目前SHA256是目前更好的选择,安全性更高,但消耗时间也更多。
4. DSA数字签名算法(Digital Signature Algorithm)
js
非对称密码学两大用途:
┌────────────────────────────────────────────────┐
│ │
│ 非对称密钥对 (公钥 + 私钥) │
│ │ │
│ ├── 用途1: 加密/解密 │
│ │ └─ RSA 可以,ECC 不方便 │
│ │ │
│ └── 用途2: 数字签名 ← DSA 就是这个 │
│ ├─ DSA (基于离散对数,已过时) │
│ ├─ ECDSA (基于椭圆曲线) ← 你在用这个 │
│ └─ Ed25519 (更现代的椭圆曲线签名) │
│ │
└────────────────────────────────────────────────┘
简单介绍ECDSA:
js
ECDSA = EC + DSA
│ │
│ └─ DSA: 签名算法(怎么做签名和验证)
│ · 生成签名: 私钥 + 消息 → (r, s)
│ · 验证签名: 公钥 + 消息 + (r, s) → 真/假
│
└─ EC: 底层数学基础(椭圆曲线)
· 换成其他数学基础也行:
- RSA 签名 (用的是大数分解难题)
- DSA 原版 (用的是模幂运算离散对数)
- ECDSA (用的是椭圆曲线离散对数)
5. 相关的编码算法
Base64编码
加解密时经常会遇到有一个Base64编码的转换,所以这里也做一个简单介绍。
- 输入:任意二进制数据
- 输出:只用 64 个"安全字符"表示的文本
js
┌─────────────────────────────────────────┐
│ Base64 字符表(64个) │
│ │
│ A-Z (26) a-z (26) 0-9 (10) + / (2) │
│ │
│ 全是可打印 ASCII 字符,且避开: │
│ · 控制字符(\n, \r, \0...) │
│ · 特殊 URL 字符(?, #, &...通过Base64URL)│
│ · JSON 转义字符(", \) │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ Base64 字符串的识别特征 │
│ │
│ 1. 只包含这些字符: │
│ A-Z a-z 0-9 + / = │
│ │
│ 2. 经常以 = 或 == 结尾(填充符) │
│ │
│ 3. 长度永远是 4 的倍数 │
│ │
│ 4. 看起来像乱码,但没有奇怪的控制字符 │
└─────────────────────────────────────────────┘
相关编码对比:
js
┌──────────┬──────────────┬─────────────────┬──────────────┐
│ │ 标准 Base64 │ Base64URL │ 十六进制 │
├──────────┼──────────────┼─────────────────┼──────────────┤
│ 字符集 │ A-Za-z0-9+/ │ A-Za-z0-9-_ │ 0-9a-f │
│ 填充 │ 用 = 补齐 │ 可以去掉 = │ 不需要 │
│ URL安全 │ ❌ +/= 需转义 │ ✅ 直接放URL │ ✅ 天然安全 │
│ 空间效率 │ 增加约 33% │ 增加约 33% │ 增加 100% │
│ 典型用途 │ 邮件附件、证书│ JWT、JSON、URL │ 调试、指纹 │
└──────────┴──────────────┴─────────────────┴──────────────┘
实例(同一二进制数据):
原始: [0xB9, 0x4D, 0x27, ...](32字节)
标准: "uU0nuZNOPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=" ← +和=在URL中麻烦
URL安全: "uU0nuZNOPgilLlLX2n2r-sSE7-N6U4DukIj3rOLvzek" ← JSON中首选
十六进制: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" ← 64字符
DER 编码
DER(Distinguished Encoding Rules)就是把结构化数据编码成"唯一确定的二进制格式",确保同一份数据在任何机器上编码结果完全一致。
为什么签名/证书需要它
js
签名值本身是 (r, s) 两个大整数:
r = 0x8A2B...(32字节)
s = 0x3C7D...(32字节)
传输时怎么打包?
❌ 直接拼一起:接收方怎么知道从哪分开?
❌ JSON: {"r":"0x8A2B...","s":"0x3C7D..."} → 太长,浪费空间
✅ DER: 用 TLV 格式编码,确定、唯一、紧凑
TLV 结构
js
DER 的核心:TLV = Tag + Length + Value
┌──────┬──────────┬──────────────┐
│ Tag │ Length │ Value │
│ 类型 │ 长度 │ 内容 │
├──────┼──────────┼──────────────┤
│ 1字节 │ 1~N字节 │ Length字节 │
└──────┴──────────┴──────────────┘
实际例子(ECDSA 签名):
签名 = 序列(r, s)
┌─ SEQUENCE ────────────────────────────────────────┐
│ Tag: 30 (SEQUENCE) │
│ Length: 44 (后面总共44字节) │
│ │
│ ┌─ r 值 ──────────────┐ ┌─ s 值 ──────────────┐ │
│ │ Tag: 02 (INTEGER) │ │ Tag: 02 (INTEGER) │ │
│ │ Length: 20 │ │ Length: 20 │ │
│ │ Value: 8A2B... │ │ Value: 3C7D... │ │
│ └─────────────────────┘ └─────────────────────┘ │
└────────────────────────────────────────────────────┘
最终二进制(十六进制):
30 2C 02 14 8A2B... 02 14 3C7D...
│ │ │ │ │ │
│ │ │ │ │ └─ s 的 Value
│ │ │ │ └─ s 的 Length
│ │ │ └─ r 的 Value
│ │ └─ r 的 Length
│ └─ 总 Length
└─ 总 Tag
和 PEM 的关系
js
DER = 纯二进制,人是读不懂的
PEM = DER 的 Base64 包装,加上 -----BEGIN xxx----- 头尾
同一个数据:
DER: 30 44 02 20 8A 2B...(二进制,文件后缀 .der)
PEM: -----BEGIN PUBLIC KEY-----
MFkwEwYHKoZI...
-----END PUBLIC KEY-----
(文本,文件后缀 .pem)
二、数字证书中学习摘要、签名、验签
先来看一个假设的签名和验签的过程,然后再逐个说明。
Plain
┌──────────── 签名过程(客户端) ─────────────┐
│ │
│ 请求JSON │
│ {"user":"alice","amount":100} │
│ │ │
│ ▼ │
│ SHA256 哈希 │
│ a1b2c3d4e5f6...(32字节摘要) │
│ │ │
│ ▼ │
│ Secure Enclave 内 │
│ 用私钥 + ECDSA 算法计算签名 │
│ │ │
│ ▼ │
│ 签名值(r, s)DER编码 ≈ 70-72字节 │
│ 30440220...base64url │
└──────────────────────────────────────────────┘
┌──────────── 验签过程(服务器) ─────────────┐
│ │
│ 收到请求: │
│ { "payload": "加密的JSON密文", │
│ "signature": "30440220..." } │
│ │ │
│ ▼ │
│ 解密 payload → 得到原文 │
│ {"user":"alice","amount":100} │
│ │ │
│ ▼ │
│ 对原文 SHA256 哈希 │
│ a1b2c3d4e5f6... │
│ │ │
│ ▼ │
│ 用客户端公钥 + 签名值 + 哈希值 │
│ → ECDSA 验证函数 │
│ │ │
│ ▼ │
│ ✅ true / ❌ false │
└──────────────────────────────────────────────┘
摘要
任意长度输入 ──哈希函数──► 固定长度输出(摘要/指纹)
签名
js
┌──────────────────────────────────────────────┐
│ 数字签名的三大作用 │
│ │
│ 1. 完整性 证明消息没被篡改 │
│ 2. 认证 证明消息确实来自声称的发送者 │
│ 3. 不可否认 发送者事后不能抵赖"这不是我发的" │
└──────────────────────────────────────────────┘
签名及验签的的过程
arduino
明文 ──哈希──► 摘要 ──私钥签名──► 签名值
验签时: 签名值 + 公钥 → 能验证"这个摘要确实是用对应私钥对这份明文产生的"
CA证书
如下是从浏览器查看的一个CA证书: 
证书的主要内容:
js
┌─────────────────────────────────────────────┐
│ X.509 数字证书 │
│ │
│ ┌────────────────────────────────────┐ │
│ │ 版本号: v3 │ │
│ │ 序列号: 0x1A2B3C... │ │
│ │ 签名算法: sha256WithRSAEncryption │ │
│ │ 颁发者: CN=My CA, O=My Company │ │
│ │ 有效期: 2025-01-01 ~ 2026-01-01 │ │
│ │ 持有者: CN=app.example.com │ │
│ ├────────────────────────────────────┤ │
│ │ │ │
│ │ ┌──────────────┐ │ │
│ │ │ 公钥部分 │ ◄── 公钥指纹│ │
│ │ │ 256 字节 │ 计算这个 │ │
│ │ └──────────────┘ │ │
│ │ │ │
│ ├────────────────────────────────────┤ │
│ │ 扩展: SAN, Key Usage... │ │
│ ├────────────────────────────────────┤ │
│ │ CA 签名值: 30440220... │ │
│ └────────────────────────────────────┘ │
│ │
│ 证书指纹 = SHA256(整个文件) │
│ 公钥指纹 = SHA256(只有公钥部分) │
└─────────────────────────────────────────────┘
从权威CA证书颁发机构申请的证书链验证原理如下:
js
┌─────────────────────────────────────────────────────────┐
│ 信任链(Chain of Trust) │
│ │
│ 根CA(系统预置,绝对信任) │
│ ┌──────────────────────────────┐ │
│ │ Root CA 自签名证书 │ │
│ │ 公钥: root_pub │ │
│ │ 签名: root_priv(root_pub) │ ← 自己签自己 │
│ └──────────────┬───────────────┘ │
│ │ 用 root_priv 签名 │
│ ▼ │
│ 中间CA │
│ ┌──────────────────────────────┐ │
│ │ Intermediate CA 证书 │ │
│ │ 持有者: inter_pub │ │
│ │ 签发者: root_pub │ │
│ │ 签名: root_priv(inter_pub) │ │
│ └──────────────┬───────────────┘ │
│ │ 用 inter_priv 签名 │
│ ▼ │
│ 终端证书(你的App) │
│ ┌──────────────────────────────┐ │
│ │ App 证书 │ │
│ │ 持有者: app_pub │ │
│ │ 签发者: inter_pub │ │
│ │ 签名: inter_priv(app_pub) │ │
│ └──────────────────────────────┘ │
│ │
│ 验证路径: │
│ app_pub ← 用 inter_pub 验签名 ← 用 root_pub 验签名 ✓ │
└─────────────────────────────────────────────────────────┘
自签证书与CA证书对比:
| 对比维度 | 自签 CA | 商业 CA |
|---|---|---|
| 签发者 | 你自己(服务器管理员) | 受信的第三方机构(DigiCert、Let's Encrypt 等) |
| 费用 | 免费 | 免费(Let's Encrypt)到数万元/年不等 |
| 浏览器/系统信任 | 默认不信任(红色警告) | 默认信任(绿色锁) |
| 适用场景 | 内部系统、App 间通信 | 公网网站、用户浏览器访问 |
| 信任建立方式 | 手动预埋证书或指纹 | 操作系统/浏览器预置根证书 |
| 管理复杂度 | 自己维护 CA 基础设施 | 到期续费,平台自动验证域名 |
为什么现在很多CA证书(包括上面图中的)写的是TLS RSA CA?
这是历史惯性,不是因为它更安全。它们往往是十几年前就建好的基础设施:
最开始建根证书时: → 只有 RSA(1980s就有了) → ECC 还没成熟 / 支持度不够
于是根证书用了 RSA 4096 位密钥
到现在,它一直在给你签 TLS 证书(可能用 RSA,也可能用 ECDSA) 但它的"根"永远是那个老的 RSA 只要根证书还在有效期内,就一直这么标 (即使叶子证书用了更先进的 ECDSA 或 Ed25519,只要根证书是 RSA 的,证书链上依旧会看到
TLS RSA CA标志。)
三、设计一个安全通信的规则
以下是一个不一定是HTTPS请求的一个安全通信设计:
js
┌────────────────── 首次安装/注册 ──────────────────┐
│ 客户端生成 ECDH 密钥对 │
│ 生成 Client_Identity_KeyPair (长期身份密钥) │
│ Public_Identity_Key ──────► 服务器存储并签发证书 │
│ Private_Identity_Key 安全存储在 Keychain/Enclave │
└────────────────────────────────────────────────────┘
┌────────────────── 建立安全会话 ─────────────────────────────┐
│ │
│ 客户端 服务器 │
│ │ │ │
│ │ 1. 生成 ECDH 临时密钥对 │ │
│ │ ephemeral_public_key ──────────► │ │
│ │ │ 2. 生成临时密钥对 │
│ │ ◄─────────── server_ephemeral_pub + 证书 │
│ │ │ │
│ │ 3. ECDH 计算共享密钥 │ ECDH 计算共享密钥 │
│ │ shared_secret = X25519( │ shared_secret = │
│ │ client_eph_priv, │ X25519(server_ │
│ │ server_eph_pub) │ eph_priv, │
│ │ │ client_eph_pub)│
│ │ 4. HKDF 派生 session_key │ HKDF 派生 │
│ │ │ session_key │
│ │ │ │
│ │ 5. session_key 对称加密通信内容 │ │
│ │ + HMAC 完整性校验 │ │
│ │ ────────► verify & decrypt │ │
│ │ │ │
│ │ 6. 身份签名(用长期身份密钥) │ │
│ │ 签名关键操作,防止中间人 │ 验证身份签名 │
│ │
└──────────────────────────────────────────────────────────────┘
10年前老项目的HTTP通信方式:(现在看来是不够完善的)
js
┌─────────── 首次安装 ───────────┐
│ 客户端生成 RSA 公私钥对 │
│ Client_Public_Key ──────► 服务器存储
│ Client_Private_Key 保留本地
└────────────────────────────────┘
┌─────────── 登录时 ────────────┐
│ 客户端生成密钥Secret( UUID + 随机) │
│ UUID 作为对称加密密钥 │
└────────────────────────────────┘
┌─────────── 关键请求 ──────────────────────────────────┐
│ │
│ ① 用 Server_Public_Key 加密 (Secret+偏移字节) → key 字段 │
│ ② 用 UUID 对称加密 请求JSON → msg 字段 │
│ ③ 用 Client_Private_Key 签名 请求JSON → check 字段 │
│ │
│ POST /api/secure │
│ { key, msg, check } ─────────────────────► 服务器 │
└────────────────────────────────────────────────────────┘
┌─────────── 服务器处理 ────────────────────────────────-─┐
│ │
│ ① Server_Private_Key 解密 key → Secret │
│ ② Secret 解密 msg → json --》并计算Hash1 │
│ ③ Client_Public_Key 验签 check → JSON的哈希值HASH2 │
│ ④ 比对 Hash1 === Hash2 │
│ │
└────────────────────────────────────────────────────────┘
四、HTTPS的HMAC
Hash-based Message Authentication Code(基于哈希的消息认证码) 一句话:带密码的哈希。普通哈希谁都能算,HMAC 只有知道密钥的人才能算出来并验证。
js
普通哈希 SHA256:
任何人拿到 "hello" → 都能算出 b94d27b9...
用于:验证数据有没有损坏(完整性)
不能防:恶意篡改(攻击者改了数据,重新算个哈希就行)
HMAC-SHA256:
只有知道密钥的人才能算出正确的 HMAC 值
用于:验证数据有没有被篡改 + 证明它来自持有密钥的人(认证)
能防:篡改、伪造
工作原理
js
┌────────────── HMAC 计算过程 ──────────────┐
│ │
│ 输入: │
│ · 密钥 K (你的 secret) │
│ · 消息 M (要保护的数据) │
│ │
│ 步骤: │
│ │
│ 1. 如果 K 比 block-size 长 → 先哈希缩短 │
│ 如果 K 比 block-size 短 → 用 0 填充 │
│ │
│ 2. 用 0x36 填充到 block-size → ipad │
│ 用 0x5c 填充到 block-size → opad │
│ │
│ 3. inner = SHA256((K ⊕ ipad) || M) │
│ │
│ 4. outer = SHA256((K ⊕ opad) || inner) │
│ │
│ 5. 输出: outer (32字节,和SHA256一样长) │
│ │
└─────────────────────────────────────────────┘
图解流程:
密钥K 消息M
│ │
│ ▼
│ ┌─────────┐
│ │ (K⊕ipad)│ 异或内层填充
│ └────┬────┘
│ │
│ K⊕ipad || M ← 拼接
│ │
│ ▼
│ ┌─────────┐
│ │ SHA256 │ 第一次哈希
│ └────┬────┘
│ │ inner
│ ▼
│ (K⊕opad) || inner ← 拼接外层
│ │
│ ▼
│ ┌─────────┐
│ │ SHA256 │ 第二次哈希
│ └────┬────┘
│ │
│ ▼
│ HMAC 值(32字节)
│
└── 输出
五、CRC数据校验
CRC = Cyclic Redundancy Check(循环冗余校验): 用多项式除法算出固定长度的校验码,贴在数据末尾,用来检测数据传输/存储中是否发生了意外损坏。
js
┌──────────┬──────────────┬──────────────┬────────────────┐
│ │ CRC │ SHA256 │ HMAC │
├──────────┼──────────────┼──────────────┼────────────────┤
│ 目的 │ 检测意外损坏 │ 数据指纹 │ 防篡改+认证 │
│ 防意外 │ ✅ 极好 │ ✅ 极好 │ ✅ 极好 │
│ 防恶意 │ ❌ 一秒钟破解 │ ❌ 能重算 │ ✅ 没有密钥不行 │
│ 速度 │ 极快(硬件级)│ 较快 │ 较快 │
│ 输出长度 │ 8/16/32位 │ 256位 │ 256位 │
│ 典型场景 │ 网络包/硬盘 │ 文件指纹 │ API签名 │
└──────────┴──────────────┴──────────────┴────────────────┘
工作原理------多项式除法
js
CRC 本质是把数据看成一个巨大的二进制数,除以一个固定的"生成多项式",
得到的余数就是 CRC 值。
┌────────── CRC-8 简化示例 ──────────┐
│ │
│ 数据: 11010011101100 │
│ 多项式: x³ + x + 1 → 除数: 1011 │
│ │
│ 1. 数据末尾补 3 个零(多项式次数) │
│ 11010011101100 000 │
│ │
│ 2. 长除法(异或操作) │
│ 11010011101100000 │
│ ÷ 1011 │
│ ───────────── │
│ ...(反复异或) │
│ ───────────── │
│ 100 ← 余数 = CRC 值 │
│ │
│ 3. 发送: 原始数据 + CRC │
│ 11010011101100 100 │
│ │
└────────────────────────────────────┘
接收端验证
┌────────── 验证流程 ──────────┐
│ │
│ 收到: 数据 + CRC │
│ 11010011101100 100 │
│ │
│ 对整个数据 + CRC 做除法: │
│ 11010011101100100 │
│ ÷ 1011 │
│ │
│ 如果余数 = 0 → ✅ 数据完好 │
│ 如果余数 ≠ 0 → ❌ 数据损坏 │
│ │
│ 为什么余数为零就对? │
│ 因为发送方特意选了 CRC 值 │
│ 让 "数据+CRC" 能被多项式整除 │
└──────────────────────────────┘
实际应用场景
js
┌──────────────────────────────────────────┐
│ CRC 在你平时看不见的地方大量存在 │
│ │
│ 1. 网络传输 │
│ ┌─────────────────────────────────┐ │
│ │ 以太网帧: │ │
│ │ [头部][数据][CRC-32] ← 每帧都校验 │ │
│ │ CRC 不对 → 直接丢弃,请对方重传 │ │
│ └─────────────────────────────────┘ │
│ │
│ 2. 文件压缩 │
│ ┌─────────────────────────────────┐ │
│ │ ZIP 文件: │ │
│ │ 每个压缩块末尾都有 CRC-32 │ │
│ │ 解压时比对 → 确认没损坏 │ │
│ └─────────────────────────────────┘ │
│ │
│ 3. 存储系统 │
│ ┌─────────────────────────────────┐ │
│ │ 硬盘/SSD 每个扇区: │ │
│ │ [512/4096字节数据][CRC] │ │
│ │ 读出时校验 → 发现坏块 │ │
│ └─────────────────────────────────┘ │
│ │
│ 4. 嵌入式/IoT │
│ ┌─────────────────────────────────┐ │
│ │ 传感器数据包: │ │
│ │ 环境噪声大,容易比特翻转 │ │
│ │ CRC 检测 → 丢弃错误数据 │ │
│ └─────────────────────────────────┘ │
└──────────────────────────────────────────┘