07:非对称加密的饼——RSA 和 ECDHE 到底在干什么
大家好,我是毛衣哥。上一期我们把 HTTP 扒光了,这一期来看看加了锁的 HTTPS 又是怎么被一步步拧开的。准备好瓜子,七步拆完。前六篇一直在说"非对称加密"这个词,但一直没有认真地解释它到底是什么。原因很简单:非对称加密是这个系列里最难讲的概念——难的不是怎么用,是直觉上很难接受"两个不共享秘密的人,怎么能商量出一个只有他俩知道的秘密"这个事实。这篇不用一个数学公式,用饼的比喻把它讲清楚。对称加密 vs 非对称加密:一个超短前置对称加密:你和朋友共识了一把钥匙,用这把钥匙加密和解密。优点是快(AES 加密 1GB 数据毫秒级),缺点是你俩得想办法把钥匙安全地交给对方。非对称加密:有两把钥匙——公钥和私钥。公钥可以公开,私钥自己藏着。公钥加密的东西只有私钥能解。优点是安全(公钥随便给),缺点是慢(RSA 加密几 KB 数据可能都要毫秒级)。TLS 的做法是:用非对称加密来安全地商量一把对称密钥,然后用对称密钥来加密大量的 HTTP 数据。取两者之长。RSA:一个可以随便发但是只有你能开的锁怎么理解 RSA?想象你发明了一种特殊的锁:这种锁不需要钥匙来锁上,任何人都能锁——但开锁需要一把唯一的钥匙。你批量生产了无数把这种锁(公钥),到处发——给你朋友一把、在公司网站上放一把、贴在大街上也可以。现在你朋友想给你送一个秘密消息。他找到一个你发的锁,锁在一个盒子上,把消息放进去,锁上盒子。然后寄给你。在运输过程中,任何人都能截获这个盒子。但他们打不开。因为没有你的钥匙。只有你口袋里的那把钥匙(私钥)能打开这个锁。这就是 RSA:公钥加密、私钥解密。RSA 在 TLS 中的角色(1.2 时代):在 TLS 握手第五步(ClientKeyExchange),客户端生成了 48 字节的 Pre-Master Secret。然后用服务器的 RSA 公钥(从第三步的证书里拿到的)加密这 48 字节,发送。服务器用自己的 RSA 私钥解密,拿到 48 字节的 Pre-Master Secret。在代码里的样子:// RSA 密钥交换 function rsa_key_exchange(server_cert): // 客户端侧 premaster = generate_random(48) // 生成 48 字节随机数 premaster[0..1] = TLS_VERSION // 前 2 字节是协议版本 encrypted = rsa_encrypt( // 用服务器公钥加密 server_cert.public_key, premaster ) send_to_server(encrypted) // 发过去 // 服务器侧 encrypted = receive_from_client() premaster = rsa_decrypt( server_private_key, // 用自己的私钥解密 encrypted ) // ECDHE 密钥交换 function ecdhe_key_exchange(): // 客户端侧 client_keypair = generate_ec_keypair() // 生成临时密钥对 client_public = client_keypair.public client_private = client_keypair.private // 用完就丢 send_to_server(client_public) // 发公钥 // 服务器侧 server_keypair = generate_ec_keypair() server_public = server_keypair.public server_private = server_keypair.private send_to_client(server_public) // 双方各自计算共享密钥 // 客户端算:client_private * server_public // 服务器算:server_private * client_public // 结果相同!而且网络上没有传输这个密钥 client_premaster = ec_multiply(client_private, server_public) server_premaster = ec_multiply(server_private, client_public) // client_premaster == server_premaster ← 数学保证这有什么问题?问题在于Forward Secrecy(前向安全性)。想象五年后,攻击者记录了五年前的所有 TLS 握手数据(包括被 RSA 加密的 Pre-Master Secret)。五年后的某一天,服务器的 RSA 私钥泄露了(因为服务器运维不当、员工离职、旧备份泄露等等)。攻击者可以用这个私钥去解密五年前的 Pre-Master Secret——然后推导出五年前的会话密钥——然后解密切开五年前的通信记录。RSA 没有 Forward Secrecy:一旦私钥泄露,过去所有的加密通信都能被解密。ECDHE——不需要把钥匙送过去的魔法ECDHE 解决了 RSA 没有 Forward Secrecy 的问题——而且用一种更神奇的方式。它的核心思想是:Pre-Master Secret 不在网络上传输。双方在本地各自计算出来。这就太神奇了——两个人在两处,各自计算出一个完全相同的数字。这堆数字在网络上的唯一交换信息,是两个看起来完全随机的"公钥"。怎么做到的?颜料混合比喻(这是我在网上找到的、解释 DH 最好的类比):你和朋友面前有一桶公开的红色颜料(这就是算法参数,所有人都能看到)你私底下选了一种黄色颜料(你的私钥)你朋友私底下选了一种蓝色颜料(朋友的私钥)你把黄色 + 红色混合 →橙色(你的公钥,可以公开)你朋友把蓝色 + 红色混合 →紫色(朋友的公钥,可以公开)你们交换颜色:你把橙色给朋友,朋友把紫色给你你把紫色 + 你的黄色 →褐色(这就是你们的共享秘密)你朋友把橙色 + 他的蓝色 →一样的褐色(同样的共享秘密)为什么中间人混不出那个褐色?中间人看到了:红色(公开参数)橙色(你的公钥)紫色(朋友公钥)但中间人不知道黄色(你的私钥)和蓝色(朋友的私钥)。他最多把红色 + 橙色 + 紫色混在一起——但那跟褐色是不一样的。椭圆曲线版本(ECDHE):把颜料混合替换成椭圆曲线上的点乘运算。本质完全一样——选一个私钥(随机数),乘以一个公开的基点(G),得到公钥(P1 = d1 * G)。交换公钥后,各自算共享秘密(S = d1 * d2 * G)。为什么有 Forward Secrecy?因为每次 TLS 连接,双方都生成新的临时密钥对。这个 DH 密钥对只在这个连接中使用。连接结束后立即丢弃。所以即使五年后服务器的主私钥泄露了:要解密五年前的通信,需要拿到五年前那次连接的临时 DH 私钥而那个临时私钥早在五年前就被销毁了所以永远无法解密五年前的通信这就是 Forward Secrecy——前向安全性。一个表格对比RSA ECDHE/DHE Pre-Master 客户端生成后用公钥加密发送 双方各自计算,不在网络上传输 Secret 传输 Forward 无:私钥泄露后过去通信可解密 有:每次连接用临时密钥 Secrecy 速度 加密慢(大数模幂) 密钥交换快 TLS 1.2 可用 可用(推荐) TLS 1.3 不可用(已移除) 强制使用“既然 ECDHE 更好,为什么 TLS 1.2 还保留 RSA?”历史原因。RSA 公钥加密的概念在 1977 年就提出来了,Elliptic Curve Diffie-Hellman 的大规模部署要晚得多。而且 RSA 有一个"心理上的简单":客户端生成 Pre-Master Secret → 用公钥加密发送 → 服务器私钥解密。这个过程很容易理解。而 DH:双方交换公钥后在本地各自计算出一个共享秘密——这听起来像魔法,需要更多的数学直觉才能接受。所以 TLS 设计者在早期选择了更"直观"的 RSA 作为默认密钥交换方式。直到后来大家发现没有 Forward Secrecy 的严重危害,才在 TLS 1.3 中彻底清除了 RSA 密钥交换。下期预告:HTTPS 抓包的八类盲区——中间人也有看不透的东西。Certificate Pinning、双向 TLS、应用层加密、QUIC……这些是中间人最头疼的问题。DumpAny 怎么做:RSA 和 ECDHE 的差异对 DumpAny 这样的中间人来说意味着不同的解密路径。在 ECDHE 模式下,中间人需要完整参与密钥交换——不只是拿到 Pre-Master Secret,还要在握手过程中实时计算会话密钥。DumpAny 的 TLS 层需要同时支持两种模式,以兼容不同版本的服务端配置。好在 TLS 1.3 普及后,RSA 模式不再需要了,实现复杂度也有所下降。