
我先说个习惯拿到一份加密协议文档我第一件事不是看代码而是先把里面的公钥算法加解密公式抄下来。公式虽然抽象但最能暴露一个算法有没有被用歪RSA 是不是把明文直接当整数做模幂ElGamal 的随机数 k 是不是每次重新生成ECC 体系里的“加密”究竟是直接把消息包进公钥还是只做了一次密钥协商。这些判断不看公式根本说不清。这篇文章就把常见公钥算法的加解密公式从头到尾拆一遍覆盖 RSA、ElGamal、ECC 体系里的 ECIES以及 SM2。每个算法我都会讲清公式来源、参数约束、填充细节和工程落地时要补的东西。适合刚接触密码工程、能调库但讲不清公式的开发者也适合做安全评审时想快速建立判断依据的人。1. 先想清楚一件事公钥加密的公式里哪部分能公开哪部分必须保密1.1 单向陷门加密容易解密需要一扇“后门”公钥算法看起来形态各异但骨架非常一致都存在一个容易计算、难以求逆的单向变换然后再在变换上开一扇只有私钥知道的小门。RSA 就是最典型的例子加密时求消息 m 的 e 次幂然后对模数 n 取模这件事任何人都能做但私钥 d 就是陷阱门拿着 d 对密文 c 再做一次 d 次幂取模消息又回来了。没有 d 的人想解出 m绕不开大整数分解这个困难问题。这个“单向 陷阱门”的结构决定了一个算法能不能叫公钥加密。反过来也提醒我们阅读公式时第一件事就是分清哪些参数必须公开哪些参数必须保密。公开参数算错了顶多无法解密私有参数泄露则是灾难。1.2 三类底层难题对应三种公式形态常见公钥算法其实分别踩在三条不同的数学难题上大整数分解难题给定两个大素数 p 和 q把它们乘成一个合数 n 很容易但拿到 n 再分解回 p、q 极难。RSA 就依赖这一点。有限域离散对数难题给定大素数 p 和生成元 g计算 y g^x mod p 很容易但知道 y 反求 x 极难。ElGamal 建立在这一点上。椭圆曲线离散对数难题给定椭圆曲线上的基点 G计算 Q kG 很容易但知道 Q 反求整数 k 极难。ECC 和 SM2 都建立在这一点上。理解了“难题类型”之后再去看公式你会更容易接受那些奇怪的位长数字RSA 动辄 3072 位ECC 却只要 256 位不是因为 RSA 更强而是因为它们踩的困难问题复杂度和攻击方式完全不一样。1.3 公式只是数学外壳实用系统还要做编码和填充公式只描述数学变换实际系统必须在公式前后接上编码、填充、校验、密钥派生等步骤。教科书上的 RSA 加密公式是 c m^e mod n但真实实现里 m 并不是你的原始消息而是经过填充后的编码消息ECIES 的公式里也隐藏着 KDF 派生对称密钥和认证加密的步骤。如果只盯着公式而忽略这些外围逻辑你很容易在设计评审时被带偏误以为算出 c m^e mod n 就算完成了加密。2. RSA一个模幂公式走天下但填充才是工程分水岭2.1 从两个大素数到一对密钥RSA 的密钥生成流程可以概括为随机选择两个大素数 p、q。计算模数 n p * q。计算 φ(n) (p - 1) * (q - 1)。选择一个公钥指数 e通常取 65537。计算 d满足 e * d ≡ 1 mod φ(n)。公钥是 (n, e)私钥是 (n, d)。这里 n 可以公开e 可以公开但 p、q、φ(n)、d 必须保密。d 一旦泄露任何人拿到公钥里的 n 和 e 都能反算出 d。加解密公式相当直观$$ c m^e \bmod n $$$$ m c^d \bmod n $$解密能成立的原理也很简单。因为 e * d 是 φ(n) 的倍数加 1所以对任意与 n 互素的 m都有$$ m^{ed} \equiv m \pmod n $$教科书里通常会说 m 必须小于 n 且与 n 互素。实际实现里填充过程会保证待加密的数据经过变换后处在这个安全范围内所以这条约束由填充层来兜底。关于 e 65537 这个数字很多人只是习惯性照抄。它其实是费马数 F4二进制是 10000000000000001只有两个 1用快速幂做模幂运算时效率很高。同时它和任意 φ(n) 碰撞出公因子 eb 的概率极低。相比之下如果 e 选得太小比如 e 3又会引入低指数攻击风险所以工程上固定用 65537 是很稳妥的选择。2.2 教科书 RSA 与工业 RSA隔着一层 OAEP如果你真的按教科书公式直接拿原始消息算 m^e mod n会遇到几个非常实际的问题。第一同一个明文加密结果永远相同完全没有随机性第二如果明文很短比如 m 小于 n 的 1/e 次方那么 c m^e 甚至不会超过 n攻击者直接开 e 次方根就能还原明文第三消息边界、编码、截断问题会搞得你非常难受。工业实现里RSA 加密的对象是“填充后的消息”。最常见的两种填充方式PKCS#1 v1.5 填充在消息前加固定格式的随机字节结构简单但历史上出现过填充格式预言相关的攻击风险。OAEP 填充基于散列函数的填充方式安全性更可控是现在推荐的选择。OAEP 加密时同样会引入随机数因此同一个明文每次加密得到的密文都不同。OAEP 填充之后还有一个直接影响开发的事情RSA 密文长度始终等于模长。2048 位 RSA 的模长是 256 字节所以密文固定 256 字节3072 位 RSA 的模长是 384 字节密文固定 384 字节。想要加密更长的数据就不是换算法的问题而是应该走混合加密。2.3 RSA 在实际生产中最常见的三个误用第一直接拿 RSA 加密文件或大块数据结果因为明文超过模长而失败。RSA 不适合加密业务数据公钥只应该用来加密对称密钥、会话密钥或极短的消息。大块数据用 AES 这类对称算法处理再用 RSA 把对称密钥包起来这就是混合加密。第二填充方式不一致导致跨部门联调失败。比如一方用 PKCS#1 v1.5另一方用 OAEP或者 OAEP 里 Hash 参数一个用 SHA-1、一个用 SHA-256看起来都能加密到解密端就会报“padding check failed”之类的错误。这类问题不是算法错而是协议参数没对齐。第三不检查密钥生成质量。自研素数生成器、随机数质量差、p 和 q 太接近都会让 RSA 的安全性大打折扣。现在成熟的密码学库内部已经处理了大量检测逻辑我始终建议用标准库生成密钥而不是自己写一段看似优美的素数搜索代码。3. ElGamal加密公式里多一个随机数 k安全性和运气绑在一起3.1 公钥与私钥的生成ElGamal 的数学基础是有限域离散对数。系统参数是一个大素数 p 和一个生成元 g。私钥是一个随机整数 x公钥是$$ y g^x \bmod p $$这里 p、g、y 都可以公开x 保密。攻击者就算拿到 y要求解 x本质上就是解离散对数问题。ElGamal 的核心思路和 DH 密钥交换非常接近区别在于 DH 只协商出一个共享秘密而 ElGamal 把共享秘密用乘法包装进了密文。3.2 加密是随机遮蔽解密是去掉遮蔽ElGamal 加密时发送方需要临时选择一个随机数 k然后计算两部分密文$$ c_1 g^k \bmod p $$$$ c_2 m \cdot y^k \bmod p $$密文是二元组 (c1, c2)。解密时接收方使用私钥 x$$ m c_2 \cdot c_1^{-x} \bmod p $$为什么能解出来因为 c1^{-x} g^{-kx}而 c2 里的 y^k (g^x)^k g^{kx}两者相乘指数抵消剩下的就是消息 m。整个过程可以理解成发送方先通过 DH 方式算出一个只有双方知道的共享遮蔽值 y^k把消息藏在这个遮蔽值背后接收方用自己的私钥把遮蔽去掉。ElGamal 的加密公式有一个显著特征同一个明文每次生成的密文都可能不同因为 k 是每次重新取的。这比无填充 RSA 安全很多但缺点也非常明显。3.3 随机数 k 一旦重用整个方案就塌了这是 ElGamal 最大的坑。如果两条消息使用了同一个 k设两条密文分别是 (c1, c2a) 和 (c1, c2b)对应的明文是 ma 和 mb。因为 y^k 完全一样直接做除法$$ \frac{c_{2a}}{c_{2b}} \equiv \frac{ma}{mb} \pmod p $$攻击者一旦知道其中一条明文立刻能还原另一条明文。如果这个随机数 k 还用在其他协议操作里后果可能更严重。更不用说如果 k 本身泄露攻击者直接算出 y^k 就能解开任何一条用这个 k 加密的消息。所以 k 必须是来自密码学安全随机源的不可预测值绝对不能是固定值、时间戳、计数器之类的东西。另外ElGamal 加密的明文 m 必须落在 1 到 p-1 的范围内m 0 时 c2 0同步失效。密文形态就更明显了消息长度约等于 p 的长度密文却是两个 p 长度膨胀了一倍。这也决定了 ElGamal 不适合加密大块数据更适合小消息或密钥封装类场景。3.4 密文膨胀与典型应用场景由于密文是两段ElGamal 在同等安全强度下会比 RSA 多占不少存储和带宽。早期邮件加密体系中能看到它的身影但现代新系统里直接用 ElGamal 加密的场景已经很少更多人会选择 ECC 体系的方案。如果你要做密码学实验拿 ElGamal 练手没问题但生产环境务必用标准库随机数 k 的处理交给库来做别自己写。4. ECC 与 SM2把“幂”换成“点乘”公式变成了点运算4.1 椭圆曲线上的一个点是如何“乘”起来的椭圆曲线加密不是直接在整数上做模幂而是在椭圆曲线点群上做标量乘法。曲线通常写作$$ y^2 x^3 ax b $$曲线上的点之间定义了一种“加法”运算两个点 P、Q 连线交曲线于第三点再取关于 x 轴的对称点结果是 P Q。当 P Q 时做的是切线叫二倍点。所谓的标量乘法 kP就是把 P 加到自己身上 k 次。写起来像乘法实际上一连串的点加和二倍运算。私钥就是一个随机大整数 d公钥是$$ Q dG $$G 是公开的基点。已知 d 和 G 求 Q 非常快但已知 Q 和 G 反求 d 非常困难这就是椭圆曲线离散对数难题。ECC 的所有安全性都建立在这个不对称性上。4.2 ECDH两个公钥一交换共同密钥就出来了严格来说 ECDH 不是加解密公式但它几乎所有 ECC 加密方案的基础。假设 Alice 的私钥是 a公钥是 Qa aGBob 的私钥是 b公钥是 Qb bG。Alice 用自己的私钥 a 乘以 Bob 的公钥 Qb得到$$ S aQ_b a(bG) abG $$Bob 用自己的私钥 b 乘以 Alice 的公钥 Qa得到$$ S bQ_a b(aG) abG $$两人最终得到同一个共享点 S。这个共享点的坐标经过密钥派生函数处理就能变成对称密钥。ECDH 本身不直接加密消息但它是 ECIES 等方案的中间公式。4.3 ECIES临时密钥加认证加密的组合框架ECIES 是“椭圆曲线集成加密方案”的缩写。它把椭圆曲线点运算和对称加密串起来。加密一条消息的过程大致是发送方生成临时私钥 r计算临时公钥 R rG。用接收方公钥 Q 计算共享点 S rQ (Sx, Sy)。用 KDF 从 Sx || Sy 派生对称密钥。用对称认证加密算法加密消息。输出 R 密文。接收方收到 R 后用自己的私钥 d 计算$$ S dR d(rG) r(dG) rQ $$得到的正是同一个共享点 S。之后用同样的 KDF 派生对称密钥再解密。这里最重要的细节是临时私钥 r 每次都不同因此同一个明文每次发送出来的 R 不同、密文也不同。ECIES 的安全性不依赖发送方公钥而是依赖临时密钥 r 的随机性。4.4 SM2 的加解密公式SM2 也是基于椭圆曲线的公钥算法整体思想和 ECIES 相似但密文格式更固定。设接收方私钥为 dB公钥为 PB dB·G。加密一条明文 M 的过程选择随机数 k。计算点 C1 kG。计算共享点 S kPB (x2, y2)。用 KDF 从 x2 || y2 派生字节流 t。计算 C2 M ⊕ t。计算 C3 Hash(x2 || M || y2)。输出密文 C1 || C3 || C2。解密时接收方先用自己的私钥 dB 乘 C1$$ S d_B \cdot C_1 d_B(kG) k(d_BG) kP_B $$恢复出共享点 (x2, y2)再用同样的 KDF 派生 t对 C2 做异或得到 M最后重新计算 C3 校验消息完整性。需要特别注意的是 SM2 输出的组合顺序我在这里写的是 C1 || C3 || C2接旧系统时要确认对方用的格式因为不同版本实现确实存在顺序差异。4.5 参数陷阱自定义曲线的坑ECC 公式成立的前提是每个点都真的在曲线上。如果攻击者传入一个不在曲线上的伪造点并且系统没有做校验就可能触发无效曲线攻击或非法点攻击导致私钥信息泄露。标准库通常会处理这些校验但如果你在做协议设计时允许用户传入曲线参数、基点或点坐标风险就非常大。我见过的处理原则很简单生产环境永远使用标准曲线永远不让用户自定义曲线参数。5. 四个算法的公式、位长和选型对照5.1 一张表把公式与特点串起来算法数学难题加解密公式核心密文形态推荐位长典型场景RSA大整数分解加密 c m^e mod n解密 m c^d mod n一个大整数长度等于模长3072短消息加密、密钥交换兼容场景ElGamal有限域离散对数加密 (c1, c2) (g^k, m·y^k)解密 m c2·c1^{-x}两个大整数3072小消息、特定邮件加密ECC / ECIES椭圆曲线离散对数临时点 R rGS rQ 派生对称密钥R密文一个椭圆曲线点加密文256移动端、物联网、端到端加密SM2椭圆曲线离散对数C1 kGC2 M ⊕ tC3 Hash密文 C1‖C3‖C2点加摘要加密文256需要标准密文格式的业务5.2 安全强度为何不能只看位长很多人看到 RSA 用 3072 位、ECC 用 256 位误以为 RSA 安全强度更高这是完全错误的。判断安全强度应该看它们对应的对称密钥等效位数。RSA 和有限域离散对数最好的攻击方法是目前已公开的亚指数复杂度算法例如数域筛。这意味着位长必须拉得很高才能维持安全。椭圆曲线离散对数目前最好的通用攻击还停留在指数级复杂度比如 Pollard rho 算法所以 256 位椭圆曲线就能达到 3072 位 RSA 的安全水平。日常开发我会按这个基准做判断RSA 2048 属于旧系统仍大量存在、新建系统尽量避开或升级RSA 3072 和 ECC 256 大约对应 128 位对称安全强度ECC 384 则能提供更好的长期安全冗余。5.3 选型思路优先考虑混合加密公钥算法有一个通病慢而且加密数据长度受限。RSA 直接加密长数据会越界ElGamal 密文会膨胀一倍ECIES 本质上也是在对称加密外面包了一层。所以真正的工程方案永远是混合加密。如果你的系统从零开始没有历史兼容负担我建议优先考虑 ECDHE 做密钥协商协商出的会话密钥再交给 AES-GCM 这类认证加密算法处理业务数据。需要“公钥直接加密一小段密文”时用 RSA-OAEP 或 SM2/ECIES 都行。ElGamal 作为学习教材很好但新系统没必要自己组装随机数 k 的管理和性能都会让你后悔。6. 用标准库验证公式命令、代码和我踩过的坑6.1 先用 OpenSSL 亲手跑一次 RSA-OAEP看公式不如动手验证。下面这一组命令用 OpenSSL 走一遍完整的 RSA 加密解密流程# 生成 2048 位 RSA 私钥 openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out rsa_key.pem # 从私钥导出公钥 openssl pkey -in rsa_key.pem -pubout -out rsa_pub.pem # 准备一条短消息 echo -n hello public key msg.bin # 使用 OAEP 填充加密 openssl pkeyutl -encrypt -pubin -inkey rsa_pub.pem \ -in msg.bin -out cipher.bin \ -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 # 使用私钥解密 openssl pkeyutl -decrypt -inkey rsa_key.pem \ -in cipher.bin -out dec.bin \ -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 # 确认解密结果一致 cmp msg.bin dec.bin echo OK # 观察密文长度 wc -c msg.bin cipher.bin执行完你会发现 msg.bin 只有十几字节cipher.bin 是 256 字节。这个对比很直观2048 位 RSA 的密文长度永远等于 256 字节模长。填充不仅让长度固定下来还堵住了无填充方案的确定性漏洞。我建议你在自己的机器上换几条不同长度的明文试试比如 16 字节、100 字节、190 字节再试 191 字节。超过 OAEP 上限之后加密命令会直接报错这个边界值会帮你理解“为何 RSA 只能加密小块数据”。6.2 用 OpenSSL 验证 ECDH 共享点ECC 的真实加解密流程比 RSA 复杂但核心的共享点公式可以用 OpenSSL 直接验证。先生成两对 EC 密钥然后互相用对方的公钥做派生# 生成 Alice 和 Bob 的 EC 密钥使用 prime256v1 曲线 openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out alice_key.pem openssl pkey -in alice_key.pem -pubout -out alice_pub.pem openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out bob_key.pem openssl pkey -in bob_key.pem -pubout -out bob_pub.pem # Alice 用自己私钥派和 Bob 公钥的共享秘密 openssl pkeyutl -derive -inkey alice_key.pem -peerkey bob_pub.pem -out alice_shared.bin # Bob 用自己私钥派和 Alice 公钥的共享秘密 openssl pkeyutl -derive -inkey bob_key.pem -peerkey alice_pub.pem -out bob_shared.bin # 比较是否一致 cmp alice_shared.bin bob_shared.bin echo SHARED如果输出 SHARED说明两边算出的共享点坐标一致。这时你再回头读 4.2 节那个公式会对 abG 这个点有一个非常具体的印象。这里的 derive 命令并不是完整加密但它恰好验证了 ECIES 和 SM2 加解密公式中“双方共享点一致”的那一环。6.3 实际项目里遇到的几个坑第一OAEP 的散列参数不一致。很多库默认 OAEP 使用 SHA-1另一方强行指定 SHA-256结果就是“decryption failure”。这是我在评审跨系统接口时最常看到的填充问题两地代码看起来都没错错在协议没约定清楚。第二把原始消息直接强转成整数做 RSA 公式计算。有个读者曾经把自己写的 RSA 代码发来排查问题就在于没有做填充直接把一段 UTF-8 字节流切成数字放进 m 的位置。短消息还能跑通长消息直接解密失败换成中文消息后字节数翻倍结果全是乱码。这正好验证了教科书公式和实际实现之间的巨大差异。第三ECC 公钥的点编码格式。SEC1 格式下非压缩点以 04 开头压缩点以 02 或 03 开头。两套系统如果一边输出压缩点、另一边只支持非压缩点解析就会报错。看起来像公钥损坏实际上是编码格式没对齐。类似的问题也会出现在 SM2 的 C1 点编码上。第四ElGamal 自己在实验里写取模运算时容易翻车。求 c1^{-x} 的正确做法是模逆运算Python 里可以直接用 pow(c1, -x, p)而不是先转浮点倒数再取模。浮点数参与取模结果一定有问题。这类细节在公式里不会体现但一进代码就会撞上。最后说一个我在代码评审里养成的习惯每接触一个公钥算法我会把它的公式抄成一张小卡片卡片上只填三行哪些参数是公开的、哪些是私有的、哪些是每次加密都必须变化的。RSA 的填充随机性、ElGamal 的 k、ECC 的临时私钥全部能在这个框架里对号入座。遇到新协议时把这三行填出来安全性的一半就已经看清了。