RSA 经验之谈

  写代码这么多年了,在涉及RSA跨语言的功能时,总会让人经历一番折磨。所以有必要把现在我掌握的这些零碎的知识总结一下公布出来,免得组内同事再走弯路。由于RSA实在太复杂,还有很多内容没弄懂,也没精力全了解,暂且把问题留下,未来也许能回答。所以这篇文章的标题为“经验之谈”

  本质上是数字运算

  RSA算法的本质是找到几个非常大的数,然后做运算,加密解密都是运算而已。进行加解密之前你总要产生一个密钥对,这就是查找这几个数的过程。在产生的数字中,p,q是两个大质数,其乘积为n,经过挑选(你不要管到底是怎么挑选的)又会得到e和d,公钥就是(n, e),私钥就是(n, d),这个n的位数就是我们所说的密钥长度, 转换成为二进制,则要为64的倍数

  从上面你可以了解到,其实所谓密钥最初就是几个很大的数,给我们带来很多麻烦的其实是如何保存和传输这些数字。而所谓的加密解密,签名验签,也不过是指数操作然后取模的数字运算而已(这里排除了摘要运算)

  基于字节的存储

  平时我们看到被加密或者签名的明文都是字符串,公私钥保存成的也是字符串。实际上在进行RSA运算时,都是基于数字的。在编程语言中,大数字可能不好表示,所以我们看到的SDK API往往都是以字节数组体现(byte[]),这样一来,在保存和传输时,字节数组和字符串之间的转换就成了容易出错的地方。

  待加密/签名的明文的字符串要根据其编码方式转换成为字节数组,比如ascii或者UTF-8。解密之后的要做逆运算转换为明文。而公私钥则要看具体遵照的规范来决定如何转换,这其中涉及到不只一步编解码处理,不过基本上最后/第一步都是base64和字节之间的转换

  C#的那一大堆参数

  C#标准库的RSACryptoServiceProvider产生的密钥对儿要以xml字符串的形式导出,联调时候我们发现很多参数不知道是干什么用的,dp, dq, InverseQ...这些多出来的东西对公钥没影响,但一股脑的都放到私钥中了

  在PKCS#1 version 2.2中解释其为Chinese Remainder Theorem(CRT),具体这种CRT是怎么计算的我并没深入研究,但其不再是上面是说的n是两个大质数的乘积,而是多个的乘积

  最要命的是RSACryptoServiceProvider好像并没提供把这个xml转换成为我我们常用的字节数组的方法,所以我在demo中使用的BouncyCastle库

  公钥与私钥的关系

  公钥用来加密私钥解密。签名则是私钥签名,公钥验签。不用硬背,你记住私钥是要自己藏起来的,公钥是分配出去,然后自己推导就好:有人想把信息隐秘的发送给你,就要求只有你能解密。而你想把信息发出的同时证明自己的身份,则要求你能够签名

  也许你会因为你用过的一些框架而疑惑:私钥也可以用来加密,公钥也可以用来解密。我觉得用“加密”“解密”这个字眼并不合适,还是用运算比较好。理论上这一对密钥完全可以倒过来使用,但这样一来私钥就要公布出去,公钥反倒要妥善保存,可生成密钥对的实际情况是,各语言框架会考虑运算时的复杂程度,公钥中的e一般会取3或者65537, 而私钥中有n了,整个公钥很容易猜到了

  我听到过一种说法,是“私钥可以生成公钥”,这是错误的,上面一条说私钥可以“猜”出公钥,也并不是“生成”。我想这说法应该是看到pfx文件导入到IIS中可以再导出公钥,其实pfx文件里本就包含公钥信息,不过这种包含所有参数信息(n, e, d, p, q, dp, dq...)的密钥格式似乎还不少,很多“私钥文件可以导出公钥”

  签名操作

  签名操作近似于对明文做哈希操作,然后再进行基于私钥的加密,说近似是因为我曾尝试基于上面流程自己实现签名,结果不正确,也许是缺少某些步骤的处理比如填充

  要做哈希是因为一方面RSA运算比较费时,另一方面其没有AES那么多种分组算法,RSA只能做其长度(字节数)- 11的这么长的明文的运算(据说这个公式也和填充方式有关,未必准),超过的部分截断,再进行一次运算。所以基本上没人用RSA进行加密解密,都是把明文哈希之后缩短了,再进行签名验签

  PKCS是啥

  我们在调用SDK时候经常看到的PKCS#1,PKCS#8,甚至到PKCS#12,其实是“Public-Key Cryptography Standards”,其不同编号涉及的内容也不相同,之前写代码时候发现PKCS#1经常作为padding的一个参数,其实#1中规范的内容不止如此(这里的Public-Key我认为不是说这个标准仅涉及公钥),甚至包含了怎么把数字转化为字符串的方式

  每个编号都做了一些事情,你可以从wiki上看到列表

  我们常常能看见PKCS#1作为Padding的参数,对应还有常用的NoPadding,不填充。Java的Demo中,传入cipher algorithm 参数为"RSA"则会默认使用PKCS#1,C#的Demo对应RSAUtils类的Encrypt和Decrypt方法,使用了PKCS1Encoding类,而C的Demo则明确指定了PKCS1/SSL V23(鬼才知道啥是SSL V23),如果你想改成NoPadding,Java cipher algorithm 参数传入“RSA/ECB/NoPadding” (基于Java 8), C#则要去掉PKCS1Encoding类,直接使用RsaEngine类

  其他padding方式均未试验成功

  带padding的方式会导致每次加密结果不一样,因为填充了随机数

 

  RSA是否更安全

  RSA未必更难破解,据说等价于AES256安全级别的RSA,密钥长度要远大于现在我们常用的2048。反过来,同安全级别的RSA,据说,据说,要比其他算法慢1000倍。之所以我们要用它主要还是因为密钥泄露的几率很低

    还有一大堆没弄懂的事

  为什么Java读取公钥都用X509来封装,而读取私钥则用PKCS8封装?哪怕我生成密钥对根本没做指定?

  ASN.1规定了什么,DER和BER格式是怎样?

  还有一大堆crt,cer, pfx格式的证书文件都是啥?

  签名的流程到底是啥,为什么不用指定padding方式?我自己写的签名方法差了哪一步?

  C#标准库到底要怎么导出可供别的语言用的公私钥字符串而不是证书?

 

最后献上4个可以互通的demo(虽然我不太知道是怎么通的)仅供参考

posted @ 2018-01-31 16:45  Anti-Archs  阅读(322)  评论(0编辑  收藏  举报