上周帮一个做后端的朋友看代码,他在一个内部服务的配置里直接塞了一把私钥,然后问我这样是不是就稳了。我只追问了一句:这把私钥是用来解密的,还是用来签名的?他愣了几秒,说不知道,随手生成的。这种场景我实在见得太多了。非对称加密这几个字,大部分人能背出"公钥加密、私钥解密"八个字,但只要往下一层问——公钥凭什么能随便发、私钥到底该干哪件事、为什么光有加密还不够还得搞证书——脑子立刻变成一团浆糊。
我打算把非对称加密这件事彻底讲透,从"为什么需要它"到"数学上它靠什么成立",再到"命令行里怎么生成、怎么用、用错了会怎样"。这篇东西写给三类人:刚接触后端或安全岗、被面试问过公私钥却答不利索的同学;写过调用加解密接口但没深究过原理的开发者;以及单纯好奇"为什么我网购时密码没被偷走"的技术爱好者。全文不假设你有数论基础,但也不糊弄你,该有的公式和命令一个不少。
1. 先把"锁和钥匙"的直觉建立起来
我习惯先讲一个物理场景,因为这个类比能撑住后面 80% 的疑问。想象你要给朋友寄一个箱子,里面装着贵重物品。你手上有一把挂锁和唯一一把能打开它的钥匙。你把锁打开着寄给朋友,钥匙自己留在兜里。朋友收到锁,把东西放进箱子,用这把打开的锁"咔哒"扣上,再寄回给你。整个过程里,锁在路上一度是完全公开的,任何人都能拿到它、检查它、甚至复制一把一模一样的锁——但没用,因为复制锁不等于复制钥匙。
这就是非对称加密的全部直觉:加密的能力可以公开,解密的能力必须私有。这两件事在对称加密里是被绑死在同一把钥匙上的,而非对称加密第一次把它们拆开了。拆开这件事看着简单,但它是整个现代互联网信任体系的地基。
1.1 对称加密的困境:一把钥匙的麻烦
在非对称加密出现之前,所有加密都是对称的:加密和解密用同一把密钥。这东西本身没问题,今天的 AES 依然是这个思路,而且快得离谱——普通笔记本上用 AES-NI 指令集,吞吐能跑到每秒几个 GB。问题不在算法,在分发。
你和对方要通信,得先让双方都有这把钥匙。可你怎么把钥匙送过去?打电话说?电话可能被监听。发邮件?邮件走的是明文链路。派人送?成本高得离谱,而且人本身也可能被收买。这就是经典的"先有鸡还是先有蛋":要安全地传密钥,你得先有一条安全的通道;要建立安全的通道,你又得有密钥。
我做过一个粗糙的估算:一个中等规模的系统,如果每对服务之间都要维护一把对称密钥,n 个节点就是 n(n-1)/2 把。50 个服务就是 1225 把密钥,每一把都要独立分发、轮换、吊销。这个运维成本基本不可接受。更麻烦的是,密钥一旦泄露,你没法知道是谁泄的——因为双方手里是同一把东西。
注意:对称加密不是"落后的技术",恰恰相反,今天你所有真正的大流量数据,几乎全是它加密的。非对称加密解决的是"开局"和"身份"问题,不是"干活"问题。这个分工后面会反复提到。
1.2 非对称加密的关键反转:锁可以公开,钥匙藏在兜里
非对称加密生成的不是一个密钥,而是一对:公钥和私钥。它们数学上关联,但从公钥推不出私钥——这是整套体系的命门,后面所有安全性都架在这句话上。
具体怎么用,有两个方向,方向搞反是新手最容易犯的错:
| 用途 | 发送方用什么 | 接收方用什么 | 达成的效果 |
|---|---|---|---|
| 加密 | 对方的公钥 | 对方的私钥 | 只有对方能看懂 |
| 签名 | 自己的私钥 | 自己的公钥 | 任何人能确认是你发的 |
第一行是保密,第二行是认证。很多人只记住了第一行,然后写出"用私钥加密、公钥解密"这种说法——严格讲这是不准确的表述,虽然在某些教科书式 RSA 实现里能跑通,但现代密码库基本都会拒绝你这么干,原因我在第 4 章细说。
第二行才是真正被低估的。你想想,公钥是公开的,谁都能拿到;那么"用私钥做出来的东西",只要能用对应的公钥验证通过,就反过来说明做这个东西的人手里确实有私钥。这是个极其巧妙的思路转换:我不需要向你证明我是谁,我只需要向你证明"我手里有某个只有我该有的东西"。
1.3 三个常见误解先扫掉
在往下走之前,有三个误解必须先清掉,否则后面会越听越乱。
误解一:非对称加密比对称加密安全。不对。安全强度是可比较的,2048 位 RSA 和 128 位 AES 大致在同一量级。非对称的优势是密钥分发,不是强度。而且 RSA 的老实现有填充相关的攻击面,AES 反而更"干净"。
误解二:公钥公开就等于不安全。公钥公开是设计意图,不是漏洞。真正的问题不是"公钥被看到",而是"你看到的公钥是不是真的属于你以为的那个人"。这是两件完全不同的事,后者就是中间人攻击的入口,第 5 章专门讲。
误解三:有了非对称加密就不需要对称加密了。现实里几乎不存在"纯非对称"的加密系统。原因很朴素:太慢。慢到什么程度,我第 6 章给具体数字。
把这三条捋清楚,你在脑子里就已经能画出基本框架了:非对称负责开局和验身份,对称负责真正搬数据,证书负责回答"这个公钥是谁的"。剩下四章就是把这三句话拆开,填上血肉。
2. 拆开一把"公钥锁":数学上到底发生了什么
很多人学到这一步会卡住,因为教材一上来就甩欧拉定理和模逆元,把直觉全冲没了。我先给一个"搅拌机"的比喻,再补公式。
把一勺白糖和一勺盐倒进水里搅匀,这一步极其容易,一秒钟的事。但你要把糖和盐从这杯水里重新分出来,难到近乎不可能。密码学管这类操作叫单向函数:正着算便宜,反着算贵。光有单向还不够,因为解密本身也需要能算回来——所以真正需要的是单向陷门函数:正着算便宜,反着算贵,但如果你手里有一个秘密信息(陷门),反着算又变得便宜了。
那个"秘密信息"就是私钥。公钥是搅拌后的那杯水,私钥是那台能分离糖盐的离心机。你把这杯水举给全世界看,没人能还原出原料;但你有离心机,随时能还原。
2.1 单向陷门函数:能算过去,算不回来
判断一个数学问题能不能当陷门用,业界一般看三条:正向计算要足够快、逆向在没有陷门时要足够慢、陷门本身要容易生成和保存。这三条缺一不可——有些问题逆向确实难,但正向也慢得没法用,那就只能停留在论文里。
"足够慢"这个标准是动态的。所谓 2048 位 RSA 安全,意思是按当前公开的最优算法和算力水平,分解这个数大概需要难以承受的时间。它不是一个数学上的绝对结论,而是一个工程上的成本判断。这也是为什么密钥长度每隔若干年就要往上调一次:不是算法破了,是算力涨了。
我常跟人举一个更贴近生活的例子:你把一本 500 页的书随机打乱页码顺序,这个动作几秒钟就能做完;但给你一本页码全乱的书,让你还原成正确顺序,你得试到天荒地老。打乱是单向的,而"原始页码对照表"就是陷门。
2.2 RSA:大数分解的难度
RSA 的陷门建立在一个非常朴素的事实上:两个大质数相乘很容易,把乘积拆回两个质数很难。
具体到操作层面,流程是这样:随机找两个大质数 p 和 q,算出 n = p × q,再根据 p、q 算出欧拉函数 φ(n) = (p-1)(q-1),然后选一个和 φ(n) 互质的数 e,再算出 e 关于 φ(n) 的模逆 d。公钥是 (n, e),私钥是 (n, d)。加密就是 c = m^e mod n,解密就是 m = c^d mod n。
你不需要记住这些式子,但有一点必须理解:公钥里带着 n,而 n 是 p 乘 q 的结果。攻击者拿到 n,如果能分解出 p 和 q,就能算出 d,私钥就废了。所以 RSA 的安全边界,精确地等于"分解这个大数的难度"。
这里有个容易忽略的细节:如果 p 和 q 选得不好,分解难度会断崖式下跌。比如两个质数太接近,费马分解法几下就能搞定;比如 p-1 的所有质因子都很小,可以用 Pollard p-1 算法快速攻破。所以正经的密钥生成器不会让你随便填两个质数,它内部有一套随机性和素性检测的流程。
2.3 ECC:更短的密钥,同样的强度
椭圆曲线密码学(ECC)是另一条路线。它的陷门叫椭圆曲线离散对数问题:在一条特定曲线上,从一个点出发反复做"点加法",走很多步得到一个终点,这个正向计算很快;但只给你起点和终点,让你反推走了多少步,几乎不可能。
ECC 最大的卖点是单位安全强度对应的密钥短得多。这个差距不是一点点:
| 安全强度(对称等效) | RSA 密钥长度 | ECC 密钥长度 | 密钥体积差距 |
|---|---|---|---|
| 112 位 | 2048 位 | 224 位 | 约 9 倍 |
| 128 位 | 3072 位 | 256 位 | 约 12 倍 |
| 192 位 | 7680 位 | 384 位 | 约 20 倍 |
| 256 位 | 15360 位 | 521 位 | 约 29 倍 |
这个表在实际工程里的意义非常大。密钥短意味着握手时传输的字节少、签名验证的运算量小、证书体积小。移动端和物联网设备上,这个差别直接决定握手要花 30 毫秒还是 300 毫秒。所以你会看到新一点的系统默认都在往 ECC 迁移。
提示:选 RSA 还是 ECC,不是"哪个更安全"的问题,是兼容性和性能的权衡。老系统、老客户端、某些行业的遗留规范可能只认 RSA;新系统没有历史包袱的话,ECC 基本是更优解。
3. 密钥对是怎么造出来的(附实操)
理论讲完,该动手了。这一章我用最常见的通用命令行工具来演示,命令你在任何一台 Linux 或 macOS 上都能直接跑。Windows 用户装个 WSL 或者用 Git 自带的终端也一样。
先建一个干净的目录,后面的文件都放里面,免得把自己搞晕。
mkdir -p ~/keydemo && cd ~/keydemo3.1 生成一对密钥:命令与参数解读
RSA 密钥对的生成命令:
# 生成 2048 位 RSA 私钥,采用 PKCS#8 格式 openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private.pem # 从私钥导出对应的公钥 openssl pkey -in private.pem -pubout -out public.pemECC 的话更简单,因为参数不需要你选,曲线是固定的:
# 生成基于 P-256 曲线的私钥 openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ec_private.pem openssl pkey -in ec_private.pem -pubout -out ec_public.pem几个参数值得说清楚,因为它们经常被抄来抄去却没人解释:
rsa_keygen_bits:2048:密钥位数。2048 是当前的最低推荐线,低于这个数的产品级使用基本等于自找麻烦。3072 更稳,但生成时间和运算开销都会涨,自己权衡。-pubout:输出公钥。不加这个参数,原命令会输出私钥,很多人第一次就是在这里把私钥当公钥发出去了。PKCS#8:私钥的封装格式。老格式叫 PKCS#1,现在新工具默认走 PKCS#8,兼容性更好,也能统一装 EC 和 RSA 的私钥。
生成完看一眼文件权限,这一步千万别省:
ls -l private.pem # 如果显示 -rw-r--r--,说明组用户和其他用户都能读,必须改 chmod 600 private.pem我在实际项目里见过不止一次私钥权限 644 的情况,而且往往还躺在 Web 目录下面。这种配置被扫到只是时间问题。
3.2 密钥文件里到底存了什么
打开private.pem你会看到一堆以-----BEGIN PRIVATE KEY-----开头和结尾的 Base64 文本。别被吓到,这只是二进制数据做了 Base64 编码,方便在文本环境里传输和粘贴,它本身没有任何加密——任何拿到这个文件的人都能直接用。
想看里面到底是什么结构,可以这样:
openssl pkey -in private.pem -text -noout输出里你能看到 modulus(n)、publicExponent(通常是 65537)、privateExponent(d)、prime1(p)、prime2(q) 等等。看到这里你应该反应过来:私钥文件里同时包含了公钥信息,所以从私钥导出公钥是必然可行的。反过来不成立,公钥文件里没有 p 和 q。
这也是一个排查技巧:如果某个系统里公钥和私钥对不上,直接用私钥重新导出一份公钥去比对,比对着两堆 Base64 猜要快得多。
# 对比两个公钥是否一致 openssl pkey -pubin -in public.pem -text -noout > a.txt openssl pkey -in private.pem -pubout -text -noout > b.txt diff a.txt b.txt && echo "匹配"3.3 密钥长度怎么选
这个问题没有标准答案,但我可以给一个基于实际约束的判断框架。
第一看使用年限。一份密钥如果只用于几个月的临时通信,2048 位 RSA 完全够;如果是要嵌进硬件里、用十年不换的根密钥,就得往 3072 甚至更长考虑,因为算力会涨。
第二看对端能力。有些老旧的嵌入式设备或特定行业的终端,ECC 支持不全,握手直接失败。这种时候不是技术选型问题,是现实妥协。
第三看性能预算。握手阶段的签名验证是每次连接都要做的,如果 QPS 高、连接数大,ECC 带来的 CPU 节省会非常可观。我自己压测过一个内部网关,把签名算法从 RSA-2048 换成 ECDSA P-256 之后,握手阶段的 CPU 占用下降了大约一半,这个量级在高峰时段是能救命的。
第四看合规要求。某些行业规范会明确要求或者排除某些算法。这一条不看技术看规定,先确认再动手。
4. 加密只是一半:签名才是被低估的主角
前面说的都是加密。但如果你去看真实的系统,非对称密钥被用得最多的场景其实是签名,而不是加密。这个比例差距大到什么程度?我粗略统计过一个项目里所有非对称密钥的用途,签名相关的大概占八成,真正直接做加密的不到两成,而且那两成里还有一半最终是为了协商对称密钥。
4.1 加密与签名的方向差别
再强调一次那个方向表,因为反着用是最高频的错误:
- 加密:谁想保密,就用对方的公钥加。接收方用自己的私钥解。
- 签名:谁想证明身份,就用自己的私钥签。验证方用签名者的公钥验。
注意主语不一样。加密时你用的是别人的公钥,签名时你用的是自己的私钥。写代码时把这两个搞混,轻则功能报错,重则密钥泄露。
我见过最离谱的一次,是某同学想实现"服务端给客户端下发加密配置",结果让客户端拿服务端公钥去"加密",服务端拿自己的私钥去"解密"——这个方向倒是对的,但他把服务端私钥也打包进了客户端安装包里,理由是"客户端也得能验证"。他真正需要的其实是签名功能,却用了加密的接口。这种错误不是水平问题,是概念没理清。
4.2 签名流程拆解
完整的签名流程其实不是"对原文签名",而是分两步:先对原文算摘要,再对摘要做签名运算。
第一步:hash = SHA-256(原文) 第二步:signature = RSA_Sign(私钥, hash)为什么不直接对原文签?两个原因。一是性能:原文可能有几百 MB,直接做模幂运算慢到不可接受;摘要固定 32 字节,运算量恒定。二是安全性:直接签原文在数学上有一些已知的攻击面,而先做哈希能有效规避。
验证端流程对称:
第一步:hash1 = SHA-256(收到的原文) 第二步:hash2 = RSA_Verify(公钥, signature) 第三步:比对 hash1 == hash2命令行实操如下,你可以直接抄:
# 准备一份文件 echo "这是一份需要签名的内容" > doc.txt # 用私钥签名,先生成 SHA-256 摘要再签 openssl dgst -sha256 -sign private.pem -out doc.sig doc.txt # 用公钥验签 openssl dgst -sha256 -verify public.pem -signature doc.sig doc.txt # 输出 Verified OK 表示通过现在试着改一个字,再验一次:
echo "这是一份需要签名的內容" > doc.txt # 注意"容"改成了"內" openssl dgst -sha256 -verify public.pem -signature doc.sig doc.txt # 输出 Verification Failure这一改一验,你就摸到了数字签名的核心价值:内容变了一个字节,签名就失效。它同时提供了完整性校验和身份确认,这才是它被大量使用的真正原因。
4.3 为什么签名的实现更容易踩坑
签名用起来简单,但坑比加密多,我说三个最典型的。
坑一:把"签名"和"加密"的 API 混用。RSA 的底层运算m^d mod n和m^e mod n形式上是对称的,所以在某些底层接口里你可以用私钥去"加密"、公钥去"解密"。这样确实能跑出结果,但它不具备安全的填充方案,存在被伪造的风险,而且现代密码库通常直接禁用。正确做法是用专门的签名接口,填充方案用 PSS。
坑二:验签时跳过了内容校验。有些实现拿到签名后只验"签名本身能不能通过",却没把原文一起纳入校验范围。这会导致攻击者把 A 的签名挪到 B 的内容上用。标准做法是签名和原文绑定,验证时必须同时提供两者。
坑三:时间戳和重放。签名只能证明"这段内容是这个人生成的",不能证明"这段内容是什么时候生成的"。攻击者可以把你三个月前签过的一条支付指令重新发一遍,签名依然有效。解法是在签名内容里加入时间戳和一次性随机数,服务端维护一个短期去重缓存。这一条在接口设计中极其重要,但绝大多数入门材料不会讲。
注意:签名保证的是"来源可信"和"内容未改",不保证"新鲜"。新鲜性需要额外机制,不要指望签名本身解决。
5. 中间人:非对称加密挡不住的那个洞
现在到了最容易被忽略的一章。前面所有内容都建立在一个隐含前提上:你拿到的公钥,确实属于你以为的那个人。这个前提如果不成立,整套体系瞬间崩塌,而且崩得毫无征兆——你的代码不会报任何错。
5.1 公钥是谁的公钥
设想一个场景:你连上了一个网络,想访问某个服务。你请求对方公钥,攻击者在中间把请求拦下来,把自己的公钥发给你。你以为这是服务器的公钥,用它加密了自己的数据发出去。攻击者用自己的私钥解开,读到全部内容,再用真正的服务器公钥重新加密转发。服务器完全正常响应,你完全正常收到结果,双方都觉得自己在安全通信。
这个场景的可怕之处在于:从密码学角度看,整个过程没有任何一步出错。加密是对的,解密是对的,签名也是对的。错的只有一件事——你信任了一个不该信任的公钥。
所以非对称加密本身解决不了"公钥归属"问题,这个问题必须靠另一套机制来补。
5.2 证书链在做什么
常见的解法是引入一个第三方信任锚。这个第三方给公钥开一张"证明书",内容包括:这个公钥属于谁、有效期到什么时候、由谁签发,然后用自己的私钥在上面盖章。这张证明书就是数字证书。
验证逻辑是一条链:
你信任的根证书(内置在系统或浏览器里) └─ 签发了中间证书 └─ 签发了服务器的证书 └─ 里面装着服务器的公钥你收到服务器证书后,顺着这条链往上验,每一环都用上一环的公钥去验证下一环的签名,一直验到根证书为止。根证书是你主动信任的,链条就闭合了。
这个设计有个明显的好处:你不需要预先认识世界上每一个服务器,只需要信任少数几个根,就能验证海量的站点。代价是引入了中心化依赖,根证书一旦出问题影响面极大——这也是为什么系统的根证书列表更新是件很谨慎的事。
5.3 证书校验的常见失误
证书这块,工程上的坑比理论上的多。我按踩坑频率排个序:
| 失误 | 表现 | 后果 |
|---|---|---|
| 直接关掉证书校验 | 代码里写"验证失败就跳过" | 完全失去中间人防护 |
| 只校验证书存在,不校验链 | 拿到证书就用,不做链式验证 | 自签证书可冒充 |
| 不校验域名 | 证书有效但签发给了别的域名 | 可被跨站冒充 |
| 不校验有效期 | 过期多年的证书照样通过 | 长期泄露的私钥持续可用 |
| 用固定公钥代替完整校验 | 硬编码指纹但不更新机制 | 轮换密钥时服务中断 |
第一条尤其普遍。开发阶段因为自签证书报错,很多人图省事就在代码里加一句"跳过验证",然后这句话就跟着代码一路上了生产。我在代码评审里看到这种写法,一律要求改成"把自签证书加入信任列表"而不是"关掉校验"——前者是配置,后者是漏洞。
还有一个进阶做法值得提:公钥固定。思路是把服务端公钥的指纹硬编码在客户端里,只接受这一个。这样即使某个证书颁发机构被攻破,攻击者也没法冒充。代价是密钥轮换时必须同步更新客户端,要做双指纹过渡,运维复杂度上去了。这个方案适合客户端可控、安全要求高的场景,不适合开放给任意第三方接入的系统。
6. 混合加密:真实系统里没人只用非对称
终于到了"为什么不能全用非对称"这个问题。答案很直接:慢。但慢多少,得给数据。
6.1 性能账:为什么不能全用非对称
大致量级上,RSA-2048 的加解密吞吐在几万次操作每秒的量级(具体取决于 CPU 和实现),而 AES-128 用硬件指令集能做到每秒几个 GB 的吞吐。这个差距不是几倍,是几个数量级。如果你的服务要传输 1 GB 数据,用 RSA 分块加密,光运算时间就能以小时计。
还有个硬限制:非对称加密一次能处理的数据长度受密钥长度约束。RSA-2048 的模长是 256 字节,填充还要占掉一部分:
- 用 PKCS#1 v1.5 填充,固定占 11 字节,可加密上限 = 256 - 11 =245 字节
- 用 OAEP 填充配合 SHA-256,占 2 × 32 + 2 = 66 字节,可加密上限 = 256 - 66 =190 字节
也就是说,一段超过 190 字节的文本,RSA-2048 + OAEP 就直接报错了。我见过不少人在这一步卡住,报错信息还挺含糊,查半天才发现是长度问题。这时候不要想着"换更长的密钥"——就算换成 4096 位,上限也就 446 字节,依然不解决问题。正确解法是换思路,而不是换参数。
6.2 TLS 握手简版拆解
真实系统的做法叫混合加密:用非对称加密协商出一把临时对称密钥,然后用这把对称密钥加密全部业务数据。非对称只干开头那一小段活,大头交给对称。
简化后的流程大概是这样:
1. 客户端发起连接,声明支持的算法 2. 服务端返回证书(含公钥)和选定的算法 3. 客户端验证证书链、域名、有效期 4. 双方通过密钥交换算法协商出一个共享秘密 5. 双方各自用共享秘密推导出会话密钥 6. 后续所有数据用会话密钥做对称加密第 4 步在最常见的实现里用的是临时密钥交换:双方各自生成一对临时的密钥对,交换公钥,各自算出同一个共享秘密,然后把这个临时私钥立刻丢弃。服务端的长期私钥在这个过程中只用来签名,证明"我确实是这个域名的主人",不参与密钥计算。
这个设计的意义在第 6.3 节说。
6.3 前向安全
临时密钥交换带来的性质叫前向安全:即使攻击者某天拿到了服务端的长期私钥,他也无法解密之前录下来的历史流量。因为那些流量用的会话密钥是由早已销毁的临时私钥算出来的,补不回来。
反过来,如果密钥交换用的是长期私钥直接加密,那么一旦私钥泄露,历史上所有被录下的流量全部可解。这个差别在真实世界里非常要命——流量录制成本极低,长期保存也不难,所以"先录下来,以后再想办法"是一种现实的威胁模型。
判断一个服务有没有前向安全,看它协商的密钥交换算法里有没有"临时"这个特征。运维层面能做的就一件事:把服务端的算法配置往临时密钥交换方向调,同时保留少量兼容项以防老客户端连不上。
提示:前向安全是配置出来的,不是默认就有的。老版本的配置模板里可能还留着一批不带前向安全的套件,升级时顺手清一遍。
7. 常见问题与排查实录
这一章是我这些年攒下来的实际记录,按出现频率排。
7.1 高频问题速查表
| 现象 | 最可能的原因 | 排查动作 |
|---|---|---|
| 加密时报 data too large | 明文超过密钥可加密上限 | 改用混合加密,不要直接加密大段数据 |
| 验签一直失败 | 摘要算法两端不一致 | 确认双方都用 SHA-256,不要一边 SHA-1 |
| 验签一直失败 | 换行符被转换 | 检查签名和验证时的文件是否二进制一致 |
| 解密出来是乱码 | 填充方案不匹配 | 确认加密和解密用同一套填充 |
| 证书报域名不匹配 | 证书签发给了别的域名 | 检查请求的域名和证书 SAN 字段 |
| 本地能跑,线上报错 | 线上环境缺根证书 | 检查容器镜像的证书包是否完整 |
| 密钥轮换后服务中断 | 客户端硬编码了旧公钥 | 检查是否有公钥固定且没做过渡 |
表里第三行那个换行符问题,我踩过不止一次。在 Windows 上编辑的文件带 CRLF,在 Linux 上读取被转成 LF,内容差了一个字节,哈希就完全不一样。排查时不要盯着密码学看,先确认"参与签名的字节流是不是完全一致",用十六进制对比一下,经常五分钟就定位了。
7.2 几个真实排查过程
案例一:加密大文件失败。有个同学要把一份 2 MB 的配置文件加密后传给另一个服务,用的是 RSA 直接加密,一直报错。他以为是密钥太短,把 2048 换成 4096,还是报错。真正的问题在于 RSA 从设计上就不是干这个的。改法很明确:随机生成一把 32 字节的对称密钥,用这把密钥加密文件,再用对方的公钥加密这把对称密钥,一起发过去。对方先用私钥解出对称密钥,再用它解文件。这就是混合加密,代码量比他原来的写法还少。
案例二:签名验证偶发失败。表现是同一份内容,大部分时候验签成功,偶尔失败。这个"偶发"是最有迷惑性的。后来定位到原因是签名端对内容做了一次 JSON 序列化再算哈希,而验证端是把原始字符串直接算哈希,两边的字段顺序和空格处理不一致,某些数据下恰好一致,某些就崩了。修法是把签名范围定义成一个规范化的字节串,两端都按同一规则生成,不要依赖序列化库的默认行为。
案例三:私钥混用。有个内部服务把同一对密钥既用于加密又用于签名。这在密码学上叫密钥复用,是个应该避免的做法,原因是不同用途对密钥的生命周期、存储安全等级、轮换节奏要求都不一样;一旦某一个用途泄露,另一个用途也跟着完蛋。后来拆成两对密钥,部署上多了一个配置项,但风险边界清晰了。
7.3 交流中常被问到的几个问题
问:公钥能不能用来解密?技术上在纯 RSA 的数学层面,"用公钥做一次模幂运算"是可以算出东西的,但那不是正常意义上的解密。现代实现用专门接口和填充方案把这条路堵死了。所以正确的回答是:不要这么设计,用签名接口。
问:密钥越长越安全吗?安全强度确实随长度上升,但成本和兼容性也在上升。而且长度不是唯一变量,填充方案、随机数质量、私钥保管方式,任何一环出问题,长度再长也没用。我见过 4096 位密钥配 644 权限躺在共享目录里的,这种配置等于没加密。
问:对称加密和非对称加密到底怎么选?不是二选一。标准答案是:握手和身份验证用非对称,业务数据用对称。你日常写的代码里,真正调用非对称加密的机会其实很少,大部分情况下你是在用某个库封好的 TLS 连接,而那个连接内部已经替你做好了全部分工。
问:自己实现一套非对称加密算法靠谱吗?强烈不建议。这不是"造轮子锻炼能力"的范畴,而是"任何一个细节错误都可能导致全部失效且不报错"的范畴。时序攻击、填充预言、随机数偏差,这些东西的坑深到超出一般人的想象。用成熟库,把精力花在密钥管理和使用流程上。
最后分享一个我自己的习惯:每次涉及密钥的改动,我都会在评审时问三个问题——这把私钥谁能读到、它几点轮换一次、泄露了怎么发现。这三个问题答不上来的方案,我一般不会让它上线。非对称加密的数学部分其实很稳,出事的几乎永远是私钥放在哪、谁有权拿、什么时候换这些工程细节。