(注意本文中出现的 C 代码只是一个大致的描述,并不是实际可运行的代码)
先大致介绍一下概率加密 (Probabilistic Encryption),不用严格的学术定义的话,可以这样说:
概率加密是指具有以下性质的公钥加密算法:对于相同的明文,生成的密文随机变化。
最基本的 RSA 算法不是概率加密函数,而它的衍生算法 RSA-OAEP 是一个概率加密函数。
使用概率加密有什么好处呢?
对于非概率加密的公钥加密算法,相同的明文在加密以后,生成的密文相同,那么监听者可以通过比较截获的密文得知同样的消息又被发送了一次,而概率加密就不会有这种问题。
举个例子,如果有一个人要买股票,他在网上发送了“买入一万股股票”这条消息,如果用非概率加密的算法,那么监听者不知道加密内容,但是可以把这条密文记下来。第二天,这个人又要买同样数量同样品种的股票,他再次发送“买入一万股股票”这条消息,监听者把这次截获的密文和昨天记录的密文比较一下,就能知道这个人今天做了和昨天一样的操作。这样虽然监听者没有能够破译密文,但是还是知道了一点有用的情况:这个人两天中都做过同样的操作,这样还是有信息泄露,是不安全的!
如果上面那个操作者使用概率加密算法,那么虽然他加密的消息都是“买入一万股股票”,但是加密后两次生成的密文是不同的,监听者虽然能比较两次截获的密文,但还是不知道这个人两天中做过的操作是否一样,所以概率加密更加安全。
由此可知为什么在实际应用中大量采用 RSA-OAEP 算法,而不是基本的 RSA 算法。
概率签名算法有同样的优点:即使对同一块数据做数字签名,每次生成的签名值也不相同。
编程时遇到概率加密或概率签名后,要注意每次生成的密文或签名都是变化的,如果当成固定值就可能出错!举例如下:
已有一个现成的公钥加密函数,定义是
int PublicKeyEncrypt(unsigned char *input_data,
unsigned int input_data_len,
unsigned char *encrypted_data,
unsigned int *encrypted_data_len);
对于这个函数,输出密文大小等于输入明文大小,所以不需要预计算存放密文的内存空间大小。
还有一个已经写好的对密文进行 ASN.1 编码的函数,定义是
int Asn1EncodeCiphertext(unsigned char *input_data,
unsigned int input_data_len,
unsigned char *encoded_data,
unsigned int *encoded_data_len);
对于编码函数,当参数 encoded_data 的值是空指针时,并不做编码操作,而是把编码结果的字节长度赋给指针 encoded_data_len 指向的无符号整型变量;当参数 encoded_data 的值不是空指针时,进行编码操作,把编码结果写入到以 encoded_data 为首地址的缓冲区中,把编码结果的字节长度赋给指针 encoded_data_len 指向的无符号整型变量。
现在要写一个函数,实现公钥加密并输出密文的 ASN.1 编码功能,函数定义为
int EncryptAndEncode(unsigned char *input_data,
unsigned int input_data_len,
unsigned char *encoded_data,
unsigned int *encoded_data_len);
希望当第三个参数 encoded_data 的值为空指针时,不对密文编码,而是将编码结果的字节长度赋给指针 encoded_data_len 指向的无符号整型变量。
对于 EncryptAndEncode() 一开始是这样设计的:
1. 调用 PublicKeyEncrypt() 获得加密结果;
2. 判断 EncryptAndEncode() 的第三个参数是否为空指针, 如果为空指针,调用 Asn1EncodeCiphertext() ,将获得的编码结果的字节长度赋给函数 EncryptAndEncode() 的第四个参数(即指针 encoded_data_len )指向的无符号整型变量,然后就返回。如果不是空指针,调用 Asn1EncodeCiphertext(),将编码结果存入EncryptAndEncode() 的第三个参数 encoded_data 指向的缓冲区,将编码结果的字节长度赋给函数 EncryptAndEncode() 的第四个参数(即指针 encoded_data_len )指向的无符号整型变量后返回。
程序使用 EncryptAndEncode() 的方式如下:
1. 调用 EncryptAndEncode(),通过给 EncryptAndEncode() 函数的第三个参数赋值为空指针的方式,预计算一下编码结果的长度;
2. 按照上一步算出的长度,动态开辟存储空间;
3. 再次调用 EncryptAndEncode(),把密文的编码结果写入到动态开辟的存储空间中。
在测试时发现有时会报告缓冲区溢出,但不是每次都会溢出。为什么会出现这种错误呢?
检查后发现原因在于两次调用了 EncryptAndEncode(),每次调用 EncryptAndEncode() 时都会调用到 PublicKeyEncrypt() 函数。PublicKeyEncrypt() 是一个概率加密函数,即使加密同一块输入数据,每次输出的加密结果都是不同的。所以第一次调用 EncryptAndEncode() 时获得的缓冲区大小仅适用于容纳第一次生成的密文 ANS.1 编码,第二次调用 EncryptAndEncode() 时,生成的密文值可能变化,对应的 ASN.1 编码结果长度也可能变化(一个例子是: ASN.1 编码规则中规定编码整数时,若整数的第一个字节的值大于 127,要在编码中的负载开始位置插入一个值为 0 的字节,整数 127 和整数 227 的编码长度是不同的),这就可能导致缓冲区不足以容纳第二次生成的密文编码,就出现了缓冲区溢出的错误。
那么如何解决这个问题呢?在实际中不需要非常精确地确定密文编码缓冲区的大小时,可以把缓冲区预留大一些,但是怎么把握尺度呢?下面举一个例子:
假设密文是一个 ASN.1 编码的 SEQUENCE,粗略描述的定义为
SEQUENCE{
x INTEGER,
y INTEGER
}
已知 x 和 y 都是 32 字节长的整数。
注意 ASN.1 编码分为三部分:类型、负载长度、负载。
类型占 1 个字节。
负载长度有短编码和长编码两种方式,采用短编码时占 1 字节,采用长编码时,最长占 128 字节(长编码的形式为 0x8*, **,**,... 其中 0x8* 占 1 个字节,后面最多还可以有 127 个字节);
负载长度的上限在编程时一般可以知道。如果是整数,编码后长度有可能会增加 1 个字节,所以负载为整数时,负载部分编码结果的长度上限是整数自身的字节长度再加上 1。
下面估算一下将 SEQUENCE 中的 x 编码为 ASN.1 INTEGER 后的长度上限:
编码结果字节长度上限 = 1 (类型的长度) + 1 (负载长度部分的长度,由于小于 128,所以肯定会采用短编码方式) + 33 (负载部分的长度) = 35。
同理,y 的编码结果长度上限也是 35。
SEQUENCE 的编码结果长度上限 = 1 (类型的长度) + 1 (负载长度部分的长度,这里肯定采用短编码方式) + 35 + 35 (后面两个 35 加起来是负载长度上限) = 72
当计算机操作数是 2 的幂时效率会高一些,所以可以把编码结果长度上限从 72 放大到 128。
知道密文 ASN.1 编码长度上限以后,对 EncryptAndEncode() 的设计做如下修改:
检查第三个参数 encoded_data 的值是否为 NULL,如果为空指针,将函数 EncryptAndEncode() 的第四个参数(即指针 encoded_data_len )指向的无符号整型变量的值赋为 128,然后返回。
如果不是空指针,调用 PublicKeyEncrypt() 获得加密结果,再调用 Asn1EncodeCiphertext() 对密文编码,将编码结果存入 EncryptAndEncode() 的第三个参数 encoded_data 指针指向的缓冲区,将编码结果长度赋给 EncryptAndEncode() 的第四个参数 encoded_data_len 指向的无符号整型变量,然后返回。
修改以后,使用 EncryptAndEncode() 函数时,如果通过将第三个参数 encoded_data 的值设为 NULL 指针的方式,得到的缓冲区大小总是固定值 128 比特,足以容纳密文编码结果,就不会出现缓冲区溢出的问题了。也可以不通过将第三个参数 encoded_data 的值设为 NULL 指针的方式计算缓冲区大小,只要知道密文的 ASN.1 编码的字节长度上限是 72,可以直接定义一个大于或等于72字节的字符数组,每当调用 EncryptAndEncode() 函数时,把密文编码结果放入这个数组,就不会遇到缓冲区溢出的错误了。