文章有点长(一共 2300 字), 但最后一个故事最有意思, 看不完的话可以直接拉到底
从面试题说起好了。
在考察到网络这一块的时候,可能会问问 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
另一个有趣的事情是 2017 年,当时在钱厂,对接某银行系统的时候,在通信协议的加密这块,对方给了一个 jar 包(不给源码),以及不知什么编码的公钥、私钥文件,既不是 PEM 也不是 X509,是个奇怪的二进制文件。
然鹅,钱厂用的是 PHP,这就有点尴尬了。
幸好这个 jar 包没有经过混淆,用安卓开发小伙伴提供的反编译工具,得到了源码,并经过一番努力重写成了 PHP 的源码。
然后发现那个公私钥是 java object 序列化后得到的字节码。
更有趣的还在后面。
为了方便测试,我们按银行给的 API 写了一套 mock 系统,这样就可以在不依赖银行在内部完成全流程自测,大幅提高了开发效率。
在部署 mock 系统的时候,没想太多,就用银行提供的这对公私钥,然后竟然调通了
也就是说,银行给的公钥和私钥文件竟然是一对,把他们的私钥直接给我们了……
我猜,应该是银行的安全审计部门在项目需求中要求用非对称加密,但是又没有对最终代码进行审查吧
顺便一提,正式上线时,对方给的 API url 是 https 的,但是 url 中的域名是 IP,钱厂在代码中只能把 CURLOPT_SSL_VERIFYHOST 设为 false 。
过了一段时间,他们决定用个正式的域名,才给安上了 https 证书。
又过了一段时间,他们的证书过期了。
并且在故障期间不允许我们忽略证书进行访问。
故事 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.
经过进一步的搜索,终于找到了如何在 PHP 中解决这个问题。
具体解决办法后面说,这里先介绍一些背景知识。
RSA 加密的基本流程是:将一个和密钥长度相同的输入(明文),通过一系列运算(加密),得到一个和密钥长度相同的输出(密文)。
以 1024 bit 的 RSA 密钥为例,每次输入 128 字节,输出 128 字节。
对于超过 128 字节的情况,就需要将原始数据切成 128 字节的块,分别加密后再拼起来;解密时,按 128 字节拆开解密。
但是不足 128 字节的情况,比如像密码这种短数据,或者长数据也并不总是 128 的倍数,会留下小尾巴,这就有点尴尬。
因此我们还需要用某种方法,将不足 128 字节的数据拼( padding )到 128 字节,再进行加密;解密得到的数据,需要把 padding 的数据去掉,才能得原始数据。
真是让人头秃。
继续。
对于普通文本,一个简单的做法是用 ASCII 0 进行填充。
但这会带来 2 个问题:
如果原文中包含了 ASCII 0,就无法有效识别
对于相同的输入,总是能得到相同的密文。
问题 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 ?
其实关于 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
1
wildlynx 2020-03-22 16:05:17 +08:00 via iPhone
除了 RSA,还有 ECC 椭圆曲线加密。
|
2
neilp 2020-03-22 16:14:53 +08:00 via iPhone
握手最开始是 dh 或者 ecdh, 然后才是 rsa 或者 ecc 。 最后才是 aes
|
5
marcoxuu 2020-03-22 16:44:24 +08:00 1
同信安毕业,感觉问题一老师讲过很多遍。握手过程用 wireshark 抓包可以看到完整的算法选择协商过程。RSA 和 ecdsa 的签名因为两者的 Sign 和 Verify 算法消耗差距也适用于不同的场景 :P
|
6
wdlth 2020-03-22 16:52:56 +08:00 1
应该说说密钥交换
|
7
mywaiting 2020-03-22 16:55:01 +08:00
对接 rsa 都这么多趣事了,对接 sm2 岂不是..........
|
8
iamundefined 2020-03-22 17:07:07 +08:00
@mywaiting 手动狗头,你调皮 hhh
|
9
azenk 2020-03-22 18:05:25 +08:00
你这个还好吧,有很多开源库可以研究,比如 openssl,embed-tls 等等,我做嵌入式的,48MHz 主频的处理器,用 RSA 算法必须得用内置的硬件 RSA 计算单元,那才让人头秃呢
|
10
stevenhawking 2020-03-22 18:12:01 +08:00
讲完了故事, 有不忘安利招聘, 给楼主点赞
|
11
wuwentao1998 2020-03-22 18:18:17 +08:00
问个问题:既然第一阶段通过 DH 算法生成了对称秘钥,整个通信过程已经加密了,为什么使用密码登录的时候还需要用服务器的公钥对密码加密?
|
12
msg7086 2020-03-22 18:40:22 +08:00
|
13
ThirdFlame 2020-03-22 19:25:55 +08:00
忽略证书,并不是不用证书,而是不验证证书的有效性吧。
那银行怎么知道你没有验证证书的有效性呢? |
14
yulihao 2020-03-22 19:26:56 +08:00
我只能说我爱 ecc❀
|
15
aneureka 2020-03-22 20:42:23 +08:00 via iPhone
关于 https 握手的细节楼主讲的大概过程我都还清楚(因为最近春招实习哈哈哈),不过还是很有趣,等有天来抓包详细看下过程;期待楼主的下一个故事
|
16
justin2018 2020-03-22 20:50:54 +08:00
😅 看成 RNA 了 😅
|
17
felix021 OP @ThirdFlame 确实不知道,就是个沟通问题,不让用就不让用吧。。。毕竟确实安全点儿
|
21
shuianfendi6 2020-03-22 21:11:58 +08:00
@mywaiting sm9 密钥交换.....
|
23
shawnsh 2020-03-22 23:31:09 +08:00 via Android
原文加点 salt,增加破解难度?
|
24
whoami9894 2020-03-22 23:33:59 +08:00 via Android
公钥密码最佳实践:别用 RSA
|
25
lostpg 2020-03-22 23:58:07 +08:00 via Android
现在打开看浏览器证书,新的加密套件用的是 x25519 了,私钥公钥都好短啊
|
27
felix021 OP @whoami9894 用来学习比较好,普通人能看懂,代码写起来也简单,实用性上确实和 ECC 不能比了
|
28
hacher 2020-03-23 01:30:27 +08:00
@msg7086 请问为什么一开始要用 DH 生成 session key? 客户端可以直接用 RSA 公钥加密 session key,发给服务器私钥解密.这样两端也有相同 session key 进行加密通信了
|
29
snnn 2020-03-23 01:32:34 +08:00 via Android
AES 是基于 block 的,block 怎么做 padding 有专门的规范。
|
30
jinliming2 2020-03-23 08:50:17 +08:00 via iPhone 2
> 顺便一提,正式上线时,对方给的 API url 是 https 的,但是 url 中的域名是 IP,钱厂在代码中只能把 CURLOPT_SSL_VERIFYHOST 设为 false 。
这一段描述的不太准确吧?需要忽略证书原因应该是自签名或是证书与访问域名 / IP 不匹配,而不是 IP 访问的缘故…… 因为 IP 也是可以颁发证书的,比如 https://1.1.1.1 |
31
est 2020-03-23 10:13:51 +08:00
其实还有个客户端证书。
|
32
farseeraliens 2020-03-23 12:14:29 +08:00 via iPhone
头一次知道 byte dance 叫钱厂……
|
33
felix021 OP @farseeraliens 不是,那是我的前东家……
|
35
felix021 OP @jinliming2 对,但是他们没有提供针对 IP 颁发的证书,所以只能是先忽略
|
36
0x5e 2020-03-23 12:47:55 +08:00
「在 PHP 代码中给数据手动填充前导 ASCII 0 」,是说把 ASCII 0 填充在前面吗?最近要用一个老的后端接口,也是 no padding 的,用 oc 和 java 都可以调通,但是用 pycrypto 或者 openssl 命令行就不行,原文尾部填充 ASCII 0 也不行
|
39
warcraft1236 2020-03-23 18:18:01 +08:00
我之前做自动化的时候也是,后端用的 Java groovy,我们用 Python,对于 aes 加密的数据,也是学习了一下 padding 这个事情然后才成功的用 Python 实现了加解密
|
41
lechain 2020-03-23 19:04:45 +08:00 via Android 1
故事不错,不过我还是得挑一个错误 🐶
>> 以 RSA 为例,在加解密的时候,需要对大整数(典型值是 2048bit,256 字节)做大量乘法、取模等运算;相比之下如 AES 这样的非对称加密算法会简单很多,一些 XOR 、移位,以及在 4x4 的矩阵上做些变换,还可以通过查表来加速。 道理我都懂,但是为什么 AES 是非对称加密,我如果没有记错,RSA 才是非对称加密吧?🐶 |
44
felix021 OP @0x5e 之前 php 的代码供参考: https://www.felix021.com/blog/read.php?2169
|
45
CRVV 2020-03-23 23:42:29 +08:00
> 大多数候选人知道非对称加密,了解客户端会用 RSA 公钥进行加密。
> 那么,服务器在返回响应报文之前,会用什么来进行加密呢? 1. "服务器在返回响应报文之前,会用什么来进行加密呢" 这句话有歧义,另一种理解方式是,服务器在返回之后,客户端要用什么来加密。 因为你的前一句话说客户端要用 RSA 公钥,那么必须让服务器先给一个公钥,那么很自然的问题就是在拿到公钥之前要怎么做。 2. 按照你想表达的含义,这个问题是要怎么加密服务器发送的报文。 你给的答案居然是用 AES,所以客户端用 RSA 加密,服务器用 AES 加密,你自己不觉得这里有问题么?为啥两边要用不同的算法? 3. 既然你要用这个来当面试题,至少把 wikipedia 的 TLS 浏览一下吧。 |
46
felix021 OP @CRVV
1. 有没有歧义是在面试中可以沟通的问题。大部分候选人并不认为有歧义,因为他们认为客户端全程都在用 RSA 加密,这个问法无非是确认下他们是否这么想。 2. 我给的答案不是 AES,我只是以 AES 为例对比对称加密和非对称加密。 3. 你怎么知道我没看呢?以及为什么非要看 wikipedia 呢?再者,我面试中也会用我不熟悉的问题和候选人探讨,并不都是“碾压式”的提问。 |
47
balamiao 2020-03-24 11:20:02 +08:00
|