Tenloy's Blog

数据加密 — 非对称加密(加签/加密,以RSA为例)

Word count: 4.3kReading time: 16 min
2020/12/20 Share

一、概述

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密钥长度、明文长度和密文长度

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加密算法的规则可知,一个安全个体的私钥只有自己才知道,公钥则是可以被多方知道,所以要起到签名的效果,需要私钥签名,公钥验签

01

步骤

  • 将原始数据哈希运算,得出标记,用 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
2
# 生成PKCS1格式RSA Private Key. 密钥长度为2048
$ openssl genrsa -out private-key.p1.pem 2048

4.1.2 私钥检查

1
2
# 校验私钥文件
$ openssl rsa -in private.pem -check

4.1.3 私钥格式转换

1
2
3
4
5
6
7
8
9
10
# PKCS #1 -> Unencrypted PKCS #8
openssl pkcs8 -topk8 -in private-key.p1.pem -out private-key.p8.pem -nocrypt

# PKCS #1 -> Encrypted PKCS #8
# 过程中会让你输入密码,你至少得输入4位,所以PKCS #8相比PKCS #1更安全。
openssl pkcs8 -topk8 -in private-key.p1.pem -out private-key.p8.pem

# PKCS #8 -> PKCS #1
# 如果这个PKCS #8是加密的,那么你得输入密码。
openssl rsa -in private-key.p8.pem -out private-key.p1.pem

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
2
3
4
5
# 提取X.509格式RSA Public Key
openssl rsa -in private-key.p1.pem -pubout -out public-key.x509.pem

# 提取PKCS #1格式RSA Public Key
openssl rsa -in private-key.p1.pem -out public-key.p1.pem -RSAPublicKey_out

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
2
3
4
5
# X.509 RSA Public Key -> PKCS #1 RSA Public Key
openssl rsa -pubin -in public-key.x509.pem -RSAPublicKey_out -out public-key.p1.pem

# PKCS #1 RSA Public Key -> X.509 RSA Public Key
openssl rsa -RSAPublicKey_in -in public-key.p1.pem -pubout -out public-key.x509.pem

4.3 证书操作命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 生成私钥
openssl genrsa -out ca.key 1024

# 创建证书请求
openssl req -new -key ca.key -out rsacert.csr

# 生成证书并签名,有效期10年
openssl x509 -req -days 3650 -in rsacert.csr -signkey ca.key -out rsacert.crt

# 将x509证书转 DER 格式
openssl x509 -in rsacert.crt -out rsacert.der -outform der

# 导出P12文件
openssl pkcs12 -export -out p.p12 -inkey ca.key -in rsacert.crt

4.4 编码格式: PEM转DER

1
2
3
4
5
# 将私钥转换成 DER 格式
$ openssl rsa -in private.pem -out private.der -outform der

# 将公钥转换成 DER 格式 (以RSA1024为例,216字节 -> 162字节,符合base64编码后,长度增加3/1的规则)
$ openssl rsa -in public.x509.pem -out public.der -pubin -outform der

4.5 加密、解密

1
2
3
4
5
# 使用公钥加密小文件
$ openssl rsautl -encrypt -pubin -inkey public.x509.pem -in msg.txt -out msg.bin

# 使用私钥解密小文件
$ openssl rsautl -decrypt -inkey private.pem -in msg.bin -out a.txt

4.6 其他

1
2
3
4
5
# 以纯文本格式输出私钥内容
$ openssl rsa -in private.pem -text -out private.txt

# 以纯文本格式输出公钥内容
$ openssl rsa -in public-key.x509.pem -out public.txt -pubin -pubout -text

五、iOS中的RSA

5.1 编程中常见的公私钥格式

在iOS中使用RSA加/验签、加/解密,首先需要拿到我们想要的公钥、私钥,在 上篇博客 中已经介绍过:

证书文件常见的两种编码方式:DER编码PEM编码

在iOS中经常接触到的证书格式标准:PKCS#1PKCS#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

分为两步(其实很简单):

  1. 将公私钥文件或者字符串转换成 SecKeyRef 对象, SecKeyRef 对象是一个密码学角度的抽象的密钥对象(也就是说它可以代表一个公钥、私钥或者某种对称加密的密钥)。无论是加解密还是签名,都会需要这个对象。
    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对象(私钥)
    // 生成代码与公钥过程大致相同,有一些细微差别
    我们在 上一章4.7 RSA私钥PKCS1与PKCS8格式区别? 中已经证明过 如果从后往前看的话,其实可以发现PKCS8仅比PKCS1多了一个26自己的头,剩余的内容均完全一致。所以我们在这里使用时,如果是pkcs8格式的公钥私钥就要多一步除去头部的步骤,如果是pkcs1则不用,区别仅仅只有这一点。
1
2
+ (NSData *)stripPublicKeyHeader:(NSData *)d_key;  // 去掉pkcs8公钥的头
+ (NSData *)stripPrivateKeyHeader:(NSData *)d_key; //去掉pkcs8私钥的头

注意,上面的方法不能对pkcs1格式的公钥、私钥进行操作。前者能生成SecKeyRef,但后续操作失败code=-9809;后者去除头失败,返回处理后的私钥字符串为空,导致无法生成SecKeyRef。

有兴趣可以看下这篇文章 — iOS 生成 SecKeyRef 的正规方式,文章有提到直接处理PEM编码格式的头时,由于对应的代码解析力不够强,经常会返回一个空的密钥对象,但是在我们APP内频繁测试没有发现这个问题(如果读到这里能为我解答这个疑问,麻烦评论留言一下吧,多谢)

  1. 调用相应的函数,实现功能
    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中生成SecKeyRef对象传入,数据传输两端确定padding填充方式即可。要确认两边使用的签名算法设置参数一致;详细代码看demo即可
    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 上,希望能有所帮助,有什么问题可以留言讨论。

Author:Tenloy

原文链接:https://tenloy.github.io/2020/12/20/asymmetric.html

发表日期:2020.12.20 , 5:49 AM

更新日期:2024.04.07 , 8:02 PM

版权声明:本文采用Crative Commons 4.0 许可协议进行许可

CATALOG
  1. 一、概述
    1. 1.1 非对称加密(公钥加密)
    2. 1.2 RSA算法
    3. 1.3 密钥、明文、密文长度
      1. 1.3.1 密钥长度
      2. 1.3.2 明文长度
      3. 1.3.3 密文长度
      4. 1.3.4 常见的RSA密钥长度
  2. 二、加密、解密
  3. 三、加签、验签
  4. 四、OpenSSL 常用操作命令
    1. 4.1 Private Key操作命令
      1. 4.1.1 私钥创建
      2. 4.1.2 私钥检查
      3. 4.1.3 私钥格式转换
    2. 4.2 Public Key操作命令
      1. 4.2.1 从PKCS #1、#8私钥中提取公钥
      2. 4.2.2 从X.509证书提取公钥
      3. 4.2.4 公钥格式转换
    3. 4.3 证书操作命令
    4. 4.4 编码格式: PEM转DER
    5. 4.5 加密、解密
    6. 4.6 其他
  5. 五、iOS中的RSA
    1. 5.1 编程中常见的公私钥格式
    2. 5.2 代码处理过程
  6. 六、常见问题
    1. 6.1 为什么RSA公钥加密使用PKCS1填充每次生成结果都不一样?
  7. 七、代码整理Demo