一、概述
1.1 非对称加密(公钥加密)
公开密钥密码学(Public-key cryptography)也称非对称式密码学(Asymmetric cryptography)是密码学的一种算法。
- 它需要两个密钥,一个是公开密钥,另一个是私有密钥;公钥用作加密,私钥则用作解密。由于加密和解密需要两个不同的密钥,故被称为非对称加密;不同于加密和解密都使用同一个密钥的对称加密。
- 公钥可以公开,可任意向外发布;私钥不可以公开,必须由用户自行严格秘密保管,绝不透过任何途径向任何人提供,也不会透露给被信任的要通信的另一方。
- 应用:
- 公钥加密,私钥加密:使用公钥把明文加密后所得的密文,只能用相对应的私钥才能解密并得到原本的明文,最初用来加密的公钥不能用作解密。
- 私钥签名,公钥验签:基于非对称加密的特性,它还能提供数字签名的功能,使电子文件可以得到如同在纸本文件上亲笔签署的效果。
- 优点:
- 安全:不论给出多少份明文和对应的密文,也无法根据已知的明文和密文的对应关系,破译出下一份密文。
- 灵活:可以产生很多的公钥E和私钥D的组合给不同的加密者
- 缺点:运算速度慢。
1.2 RSA算法
RSA加密算法是一种非对称加密算法。
RSA在相关应用的时候,是需要有一些标准的 — PKI(public key infrastructure)标准(见上篇)。最常用的是pkcs。现在的各种程序中,基本都是遵循这个标准来使用RSA的。
公钥加密标准(The Public-Key Cryptography Standards, PKCS)是由美国RSA数据安全公司及其合作伙伴制定的一组公钥密码学标准,其中包括证书申请、证书更新、证书作废表发布、扩展证书内容以及数字签名、数字信封的格式等方面的一系列相关协议。
1.3 密钥、明文、密文长度
RSA的三个重要大数:公钥指数e、私钥指数d和模值n。
1.3.1 密钥长度
- 密钥指谁?密钥长度指谁?模值的位长度。
- 由于RSA密钥是(公钥+模值)、(私钥+模值)分组分发的,单独给对方一个公钥或私钥是没有任何用处,所以我们说的“密钥”其实是它们两者中的其中一组。但我们说的“密钥长度”一般只是指模值的位长度。目前主流可选值:1024、2048、3072、4096…
- 模值 n 主流长度是多少?
- 目前主流密钥长度至少都是1024bits以上,低于1024bit的密钥已经不建议使用(安全问题)。那么上限在哪里?没有上限,多大都可以使用。
- 公钥指数 e 如何确定?
- 公钥指数是随意选的,但目前行业上公钥指数普遍选的都是65537(0x10001,5bits),该值是除了1、3、5、17、257之外的最小素数,为什么不选的大一点?当然可以,只是考虑到既要满足相对安全、又想运算的快一点(加密时),PKCS#1的一个建议值而已。
- 私钥指数 d 如何确定?
- 公钥指数随意选,那么私钥就不能再随意选了,只能根据算法公式(
ed%k=1,k=(p-1)(q-1)
)进行运算出来。那么私钥指数会是多少位?根据ed关系,私钥d=(x*k+1)/e
,所以单看这个公式,私钥指数似乎也不是唯一结果,可能大于也可能小于1024bits的,但我们习惯上也是指某个小于1024bits的大整数。 - 包括前文的公钥指数,在实际运算和存储时为方便一般都是按照标准位长进行使用,前面不足部分补0填充,所以,使用保存和转换这些密钥需要注意统一缓冲区的长度。
- 公钥指数随意选,那么私钥就不能再随意选了,只能根据算法公式(
1.3.2 明文长度
网上有说明文长度小于等于密钥长度(Bytes)-11,这说法本身不太准确,会给人感觉RSA 1024只能加密117字节长度明文。实际上,RSA算法本身要求加密内容也就是明文长度m必须 0 < m < n
,也就是说内容这个大整数不能超过n,否则就出错。那么如果m=0是什么结果?普遍RSA加密器会直接返回全0结果。如果m>n,运算就会出错?!那怎么办?且听下文分解。
所以,RSA实际可加密的明文长度最大也是1024bits,但问题就来了:
如果小于这个长度怎么办?就需要进行padding,因为如果没有padding,用户无法确分解密后内容的真实长度,字符串之类的内容问题还不大,以0作为结束符,但对二进制数据就很难理解,因为不确定后面的0是内容还是内容结束符。
只要用到padding,那么就要占用实际的明文长度,于是才有117字节的说法。我们一般使用的padding标准有NoPPadding、OAEPPadding、PKCS1Padding等,其中PKCS#1建议的padding就占用了11个字节。
如果大于这个长度怎么办?很多算法的padding往往是在后边的,但PKCS的padding则是在前面的,此为有意设计,有意的把第一个字节置0以确保m的值小于n。
这样,128字节(1024bits)-减去11字节正好是117字节,但对于RSA加密来讲,padding也是参与加密的,所以,依然按照1024bits去理解,但实际的明文只有117字节了。
关于PKCS#1 padding规范可参考:RFC2313 chapter 8.1,我们在把明文送给RSA加密器前,要确认这个值是不是大于n,也就是如果接近n位长,那么需要先padding再分段加密。除非我们是“定长定量自己可控可理解”的加密不需要padding。
1.3.3 密文长度
密文长度就是给定符合条件的明文加密出来的结果位长,这个可以确定,加密后的密文位长跟密钥的位长度是相同的,因为加密公式:
1 | C = (P^e) % n |
所以,C最大值就是n-1,所以不可能超过n的位数。尽管可能小于n的位数,但从传输和存储角度,仍然是按照标准位长来进行的,所以,即使我们加密一字节的明文,运算出来的结果也要按照标准位长来使用(当然了,除非我们能再采取措施区分真实的位长,一般不在考虑)。
至于明文分片多次加密,自然密文长度成倍增长,但已不属于一次加密的问题,不能放到一起考虑。
1.3.4 常见的RSA密钥长度
位数 | 私钥长度(X509 PEM格式) | 公钥长度(X509 PEM格式) | 明文长度 | 密文长度(非Base64) |
---|---|---|---|---|
512 | 428 | 128 | 1~53 | 54 |
1024 | 812 | 216 | 1~117 | 128 |
2048 | 1588 | 392 | 1~245 | 256 |
二、加密、解密
加密的目的是实现只有指定个体才能打开发送方发出的数据,所以公钥加密(使用指定接受者的公钥来加密),私钥解密,常用的加密算法如RSA
三、加签、验签
接收方可以通过签名来确认发送方的身份,并可进行数据完整性检查。由RSA加密算法的规则可知,一个安全个体的私钥只有自己才知道,公钥则是可以被多方知道,所以要起到签名的效果,需要私钥签名,公钥验签。
步骤:
- 将原始数据哈希运算,得出标记,用 A 的私钥进行一次非对称加密算法处理。(注意:签名是由原始数据的哈希值生成的,而不是原始数据本身。后者的体积可能很大,所以不可取。)
- B用A的公钥进行解密:
- 如果能解出来,表示:确实是 A 发的。
- 如果解出来的值与收到的原始文本算出的哈希值相同,表示:数据传输途中未被修改。
过程中出现的算法:
- 哈希算法:将任意长度的消息M映射成一个固定长度的散列值h(也称为消息摘要),常见的比如MD4、MD5、SHA-1、SHA-256、SHA-384、SHA-512
- 签名算法:RSA、DSA。其中RSA既能当做加密算法,也能当做签名算法来用,正反逆运算都是通的。DSA只能用作签名
- 本文代码签名算法为SHA1+RSA,Java中称
SHA1WithRSA
四、OpenSSL 常用操作命令
4.1 Private Key操作命令
4.1.1 私钥创建
1 | # 生成PKCS1格式RSA Private Key. 密钥长度为2048 |
4.1.2 私钥检查
1 | # 校验私钥文件 |
4.1.3 私钥格式转换
1 | # PKCS #1 -> Unencrypted PKCS #8 |
4.2 Public Key操作命令
4.2.1 从PKCS #1、#8私钥中提取公钥
提取指的是从Private Key中提取Public Key,openssl rsa
同时支持PKCS #1和PKCS #8的RSA Private Key,唯一的区别是如果PKCS #8是加密的,会要求你输入密码。
1 | # 提取X.509格式RSA Public Key |
x509 格式的公钥长度比 pkcs#1 格式的长一些,以RSA1024为例,前者的长度为216,后者的长度为188。(都是base64编码格式)。
4.2.2 从X.509证书提取公钥
1 | openssl x509 -in cert.crt -pubkey -noout > public-key.x509.pem |
4.2.4 公钥格式转换
1 | # X.509 RSA Public Key -> PKCS #1 RSA Public Key |
4.3 证书操作命令
1 | # 生成私钥 |
4.4 编码格式: PEM转DER
1 | # 将私钥转换成 DER 格式 |
4.5 加密、解密
1 | # 使用公钥加密小文件 |
4.6 其他
1 | # 以纯文本格式输出私钥内容 |
五、iOS中的RSA
5.1 编程中常见的公私钥格式
在iOS中使用RSA加/验签、加/解密,首先需要拿到我们想要的公钥、私钥,在 上篇博客 中已经介绍过:
证书文件常见的两种编码方式:DER编码、PEM编码。
在iOS中经常接触到的证书格式标准:PKCS#1、PKCS#8(java中经常使用)、PKCS#12,PKCS#12文件扩展名为**.p12或者.pfx**(可存储公钥+私钥),此外常见的还有.cer/.crt/.der
(存储的是公钥)。
注意:
- 加载 .p12 文件代码转换成私钥
- 加载.cer .crt .der文件代码转换成公钥
- 直接将PEM编码格式的、PKCS#1格式的公钥、PKCS#1 / PKCS#8标准的私钥硬编码,写在代码里使用。
以上都是可以的,但是首先需要先确定到底使用哪种方式,因为不同的数据加载方式、不同的证书格式,所要处理的过程是不一样的。详见下面代码。
注意:iOS中的相关API,一般都是需要使用公钥/私钥的DER格式的数据,如果现有的是PEM格式的,可以事先用openssl 进行转换一下。
5.2 代码处理过程
这里使用的是iOS SDK中的 Security.framework 库,非openssl库,多年以前苹果就弃用了 OpenSSL,转而推荐自有框架 Security 和 CommonCrypto。苹果官方示例程序,这个程序是 Xcode 3.x 写的,是 MRC 的。
当然你仍然可以使用 OpenSSL,比如说在 iOS 上使用开源库 OpenSSL for iPhone。
分为两步(其实很简单):
- 将公私钥文件或者字符串转换成 SecKeyRef 对象, SecKeyRef 对象是一个密码学角度的抽象的密钥对象(也就是说它可以代表一个公钥、私钥或者某种对称加密的密钥)。无论是加解密还是签名,都会需要这个对象。我们在 上一章4.7 RSA私钥PKCS1与PKCS8格式区别? 中已经证明过 如果从后往前看的话,其实可以发现PKCS8仅比PKCS1多了一个26自己的头,剩余的内容均完全一致。所以我们在这里使用时,如果是pkcs8格式的公钥私钥就要多一步除去头部的步骤,如果是pkcs1则不用,区别仅仅只有这一点。
1
2
3
4
5
6
7
8
9// pragma mark - '.der'公钥文件生成SecKeyRef对象(公钥)
// pragma mark - PKCS#1、PKCS#8 PEM编码公钥生成SecKeyRef对象(公钥)
// PKCS#8格式的证书如果在代码的处理上,比PKCS#1多了一步对header的处理,也就是demo中的stripPublicKeyHeader函数,如果是PKCS#1的证书,跳过这个函数即可
// pragma mark - '.12'私钥文件生成SecKeyRef对象(私钥)
// pragma mark - PKCS#1、PKCS#8 PEM编码公钥生成SecKeyRef对象(私钥)
// 生成代码与公钥过程大致相同,有一些细微差别
1 | + (NSData *)stripPublicKeyHeader:(NSData *)d_key; // 去掉pkcs8公钥的头 |
注意,上面的方法不能对pkcs1格式的公钥、私钥进行操作。前者能生成SecKeyRef,但后续操作失败code=-9809;后者去除头失败,返回处理后的私钥字符串为空,导致无法生成SecKeyRef。
有兴趣可以看下这篇文章 — iOS 生成 SecKeyRef 的正规方式,文章有提到直接处理PEM编码格式的头时,由于对应的代码解析力不够强,经常会返回一个空的密钥对象,但是在我们APP内频繁测试没有发现这个问题(如果读到这里能为我解答这个疑问,麻烦评论留言一下吧,多谢)
- 调用相应的函数,实现功能从上面的函数可以看到,函数参数并不复杂,将1中生成SecKeyRef对象传入,数据传输两端确定padding填充方式即可。要确认两边使用的签名算法设置参数一致;详细代码看demo即可
1
2
3
4
5
6
7
8
9// 使用私钥生成数字签名
OSStatus SecKeyRawSign(SecKeyRef key, SecPadding padding, const uint8_t *dataToSign, size_t dataToSignLen, uint8_t *sig, size_t *sigLen);
// 使用公钥对数字签名进行验证
OSStatus SecKeyRawVerify(SecKeyRef key, SecPadding padding, const uint8_t *signedData, size_t signedDataLen, const uint8_t *sig, size_t sigLen);
// 使用公钥对数据加密
OSStatus SecKeyEncrypt(SecKeyRef key, SecPadding padding, const uint8_t *plainText, size_t plainTextLen, uint8_t *cipherText, size_t *cipherTextLen);
// 使用私钥对数据解密
OSStatus SecKeyDecrypt(SecKeyRef key, SecPadding padding, const uint8_t *cipherText, size_t cipherTextLen, uint8_t *plainText, size_t *plainTextLen)1
2
3
4
5
6
7
8
9// digest message with sha1
+ (NSData *)sha1:(NSString *)str
{
const void *data = [str cStringUsingEncoding:NSUTF8StringEncoding];
CC_LONG len = (CC_LONG)strlen(data);
uint8_t * md = malloc( CC_SHA1_DIGEST_LENGTH * sizeof(uint8_t) );;
CC_SHA1(data, len, md);
return [NSData dataWithBytes:md length:CC_SHA1_DIGEST_LENGTH];
}
六、常见问题
6.1 为什么RSA公钥加密使用PKCS1填充每次生成结果都不一样?
在上一篇博客 — 常见的PKI标准(X.509、PKCS) 中已经介绍过PKCS1填充方式的过程,不再赘述,总结一下:
- PKCS1填充格式:加密块EB = 00 + 块类型BT + 填充字符PS + 00 + 数据D。
- 如果使用公钥操作,BT永远为02,而对于BT为02的,PS对应的填充字节的值随机产生但不能是0字节(非00)。
- 填充后,进行加密运算之前的数据不一致,得出的结果当然就不一样。
- (这篇博客的作者一步步验证了这个现象,感兴趣的可以看下)
七、代码整理Demo
在Objective-C-RSA项目代码的基础上,根据自己项目的使用场景,整理了一下代码,放在了 GitHub - RSAHandle 上,希望能有所帮助,有什么问题可以留言讨论。