关于 RSA 的一些趣事

2020-03-22 15:23:36 +08:00
 felix021

文章有点长(一共 2300 字), 但最后一个故事最有意思, 看不完的话可以直接拉到底

== 1 ==

从面试题说起好了。

在考察到网络这一块的时候,可能会问问 http 协议,聊安全相关问题时,就顺便聊聊 https 。

大多数候选人知道非对称加密,了解客户端会用 RSA 公钥进行加密。

那么,服务器在返回响应报文之前,会用什么来进行加密呢?

有些候选人回答:“用服务器私钥进行加密”。

内心呵呵一笑

接着问,那服务器返回的信息岂不是可以被中间人拦截并解密吗?

候选人一般就放弃挣扎,只能强颜欢笑了。

有进一步了解过 https 的同学,能够说出在 SSL/TLS 握手以后,会生成一个对称加密密钥。

那么,既然有非对称加密,为什么还需要使用对称加密呢?

有些候选人就回答不上来了,只能强颜欢笑+1 。

实际上这是因为非对称加密的性能通常比对称加密算法差几个数量级。

以 RSA 为例,在加解密的时候,需要对大整数(典型值是 2048bit,256 字节)做大量乘法、取模等运算;相比之下如 AES 这样的非对称加密算法会简单很多,一些 XOR 、移位,以及在 4x4 的矩阵上做些变换,还可以通过查表来加速。

此外,由于 AES 的广泛应用,主流 CPU ( Intel, AMD, ARM )都有相应的扩展指令集,可以将性能提升一个数量级,实际每秒能处理的数据在数百 MB 这个量级上。

有些硬盘号称有全盘加密功能,实际上就是硬盘的主控芯片在写入前通过 AES 进行加密,在电脑启动时 BIOS 会要求输入密码。这样即使电脑丢了,或者硬盘被人拆下挂到其他机器上也不用担心数据泄露。

关于 RSA 算法的实现细节,推荐阮一峰写的《 RSA 算法原理》

https://www.ruanyifeng.com/blog/2013/06/rsa_algorithm_part_one.html

== 2 ==

另一个有趣的事情是 2017 年,当时在钱厂,对接某银行系统的时候,在通信协议的加密这块,对方给了一个 jar 包(不给源码),以及不知什么编码的公钥、私钥文件,既不是 PEM 也不是 X509,是个奇怪的二进制文件。

然鹅,钱厂用的是 PHP,这就有点尴尬了。

幸好这个 jar 包没有经过混淆,用安卓开发小伙伴提供的反编译工具,得到了源码,并经过一番努力重写成了 PHP 的源码。

然后发现那个公私钥是 java object 序列化后得到的字节码。

更有趣的还在后面。

为了方便测试,我们按银行给的 API 写了一套 mock 系统,这样就可以在不依赖银行在内部完成全流程自测,大幅提高了开发效率。

在部署 mock 系统的时候,没想太多,就用银行提供的这对公私钥,然后竟然调通了

也就是说,银行给的公钥和私钥文件竟然是一对,把他们的私钥直接给我们了……

我猜,应该是银行的安全审计部门在项目需求中要求用非对称加密,但是又没有对最终代码进行审查吧

顺便一提,正式上线时,对方给的 API url 是 https 的,但是 url 中的域名是 IP,钱厂在代码中只能把 CURLOPT_SSL_VERIFYHOST 设为 false 。

过了一段时间,他们决定用个正式的域名,才给安上了 https 证书。

又过了一段时间,他们的证书过期了。

并且在故障期间不允许我们忽略证书进行访问。

== 3 ==

故事 2 里提到,把那段 java 代码“经过一番努力”重写成了 PHP,其实中间还是遇到了个不大不小的麻烦。

Java 代码里用了一个叫 bouncycastle 的库来进行 RSA 的加解密,而我用 PHP 的 openssl_private_encrypt 加密的文本,并不能被他们提供的 java 代码正常解密。

经过多次尝试,我发现了一个现象:对同一个消息,java 代码加密生成的密文,每次都一样,而 PHP 生成的密文,则总是在变化。

作为一个信息安全专业的毕业生,我竟然不知道这是为什么,真是愧对国家愧对党,只好默默点开桌面上的小飞机,在一些不存在的网站上摸索。

根据这个现象,我在 stackoverflow 找到了 "data encrypted with openssl_public_encrypt is different every time?" 这个问题,答案中给了个线索:

The PKCS#1 encryption algorithm uses some random seed to make the cipher-text different every time. This protects the cipher-text against several attacks, like frequency analysis, ciphertext matching.

FROM stackoverflow.com/questions/36627518

经过进一步的搜索,终于找到了如何在 PHP 中解决这个问题。

具体解决办法后面说,这里先介绍一些背景知识。

RSA 加密的基本流程是:将一个和密钥长度相同的输入(明文),通过一系列运算(加密),得到一个和密钥长度相同的输出(密文)。

以 1024 bit 的 RSA 密钥为例,每次输入 128 字节,输出 128 字节。

对于超过 128 字节的情况,就需要将原始数据切成 128 字节的块,分别加密后再拼起来;解密时,按 128 字节拆开解密。

但是不足 128 字节的情况,比如像密码这种短数据,或者长数据也并不总是 128 的倍数,会留下小尾巴,这就有点尴尬。

因此我们还需要用某种方法,将不足 128 字节的数据拼( padding )到 128 字节,再进行加密;解密得到的数据,需要把 padding 的数据去掉,才能得原始数据。

真是让人头秃。

继续。

对于普通文本,一个简单的做法是用 ASCII 0 进行填充。

但这会带来 2 个问题:

  1. 如果原文中包含了 ASCII 0,就无法有效识别

  2. 对于相同的输入,总是能得到相同的密文。

问题 2 可能招致某些类型的攻击,例如前面引用中提到的 "frequency analysis",以一个简化的场景为例,假设每个单词是单独加密的,在英文中单词 a 出现的次数最多,通过统计密文出现的频率,可以破译对应的明文。

一个改进的方案是,使用一些随机数进行填充,这样可以保证相同明文每次加密得到不同的密文。

基于这个思路,RFC 2313 制定了 RSA 的加密标准 PKCS #1: RSA Encryption Version 1.5,通过在 128 字节中的前 11 个字节里加入一些随机数,保证每次加密得到的密文不同。

回到最初的问题,通过查看 PHP 的 openssl_public_encrypt 文档,可以发现它有一个 $padding 参数,默认值是 OPENSSL_PKCS1_PADDING 。

而银行给的 Java 代码是 Cipher.getInstance("RSA", new BouncyCastleProvider()); 按照官方文档的说明,这里的 RSA 等于 "RSA/NONE/NoPadding"。

最后,通过在 PHP 代码中给数据手动填充前导 ASCII 0,并指定 OPENSSL_NO_PADDING,终于和 Java 代码兼容了。

问题圆满解决。

等等……

银行用的是 NoPadding ?

== THE END ==

其实关于 RSA 还有一些其他有趣的事情,这次就先写到这里,下次(如果我还记得的话),可以聊聊 RSA 和币圈的一点小八卦。

按照前几篇的套路,文末还是要贴一下招聘广告:

我在网盟广告业务线(穿山甲),由于业务持续高速发展,长期缺人、不限 HC 。关于字节跳动面试的详情,可参考我之前写的《程序员面试指北:面试官视角》

https://mp.weixin.qq .com/s/Byvu-w7kyby-L7FBCE24Uw ~ 投递链接 ~

后端开发(上海) https://job.toutiao.com/s/sBAvKe

后端开发(北京) https://job.toutiao.com/s/sBMyxk

广告策略研发(上海) https://job.toutiao.com/s/sBDMAK

其他地区、职能线 https://job.toutiao.com/s/sB9Jqk

5855 次点击
所在节点    程序员
49 条回复
shuianfendi6
2020-03-22 21:11:58 +08:00
@mywaiting sm9 密钥交换.....
zts1993
2020-03-22 23:22:13 +08:00
@mywaiting 那是简直是一场灾难。。。
shawnsh
2020-03-22 23:31:09 +08:00
原文加点 salt,增加破解难度?
whoami9894
2020-03-22 23:33:59 +08:00
公钥密码最佳实践:别用 RSA
lostpg
2020-03-22 23:58:07 +08:00
现在打开看浏览器证书,新的加密套件用的是 x25519 了,私钥公钥都好短啊
felix021
2020-03-23 00:49:49 +08:00
@lostpg 对,椭圆曲线的密钥短了很多,计算量也比 RSA 要小很多
felix021
2020-03-23 00:50:30 +08:00
@whoami9894 用来学习比较好,普通人能看懂,代码写起来也简单,实用性上确实和 ECC 不能比了
hacher
2020-03-23 01:30:27 +08:00
@msg7086 请问为什么一开始要用 DH 生成 session key? 客户端可以直接用 RSA 公钥加密 session key,发给服务器私钥解密.这样两端也有相同 session key 进行加密通信了
snnn
2020-03-23 01:32:34 +08:00
AES 是基于 block 的,block 怎么做 padding 有专门的规范。
jinliming2
2020-03-23 08:50:17 +08:00
> 顺便一提,正式上线时,对方给的 API url 是 https 的,但是 url 中的域名是 IP,钱厂在代码中只能把 CURLOPT_SSL_VERIFYHOST 设为 false 。

这一段描述的不太准确吧?需要忽略证书原因应该是自签名或是证书与访问域名 / IP 不匹配,而不是 IP 访问的缘故……
因为 IP 也是可以颁发证书的,比如 https://1.1.1.1
est
2020-03-23 10:13:51 +08:00
其实还有个客户端证书。
farseeraliens
2020-03-23 12:14:29 +08:00
头一次知道 byte dance 叫钱厂……
felix021
2020-03-23 12:24:59 +08:00
@farseeraliens 不是,那是我的前东家……
lostpg
2020-03-23 12:33:22 +08:00
@felix021 但安全性也依然很好,而且背景非常干净没有 NIST 的痕迹🤣
felix021
2020-03-23 12:38:35 +08:00
@jinliming2 对,但是他们没有提供针对 IP 颁发的证书,所以只能是先忽略
0x5e
2020-03-23 12:47:55 +08:00
「在 PHP 代码中给数据手动填充前导 ASCII 0 」,是说把 ASCII 0 填充在前面吗?最近要用一个老的后端接口,也是 no padding 的,用 oc 和 java 都可以调通,但是用 pycrypto 或者 openssl 命令行就不行,原文尾部填充 ASCII 0 也不行
felix021
2020-03-23 14:09:56 +08:00
@0x5e 对,填充在前面,更多细节你参考我提到的 RFC 2313
0x5e
2020-03-23 18:06:48 +08:00
@felix021 感谢,还是不行,我再试试
warcraft1236
2020-03-23 18:18:01 +08:00
我之前做自动化的时候也是,后端用的 Java groovy,我们用 Python,对于 aes 加密的数据,也是学习了一下 padding 这个事情然后才成功的用 Python 实现了加解密
msg7086
2020-03-23 18:28:23 +08:00
@hacher 初始信道不安全。
RSA 公钥哪来的,不是服务器给的么,那中间人把公钥换掉就行了。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/655096

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX