RSA的PSCK1 和 PSS 加密、签名封装以及填充方式
SSA = Signature Scheme with Appendix
PSS = Probabilistic Signature Scheme
ES = Encryption Schemes
SSA是填充、封装格式
PSS是私钥签名流程。
ES 是公钥加密流程。
https://tools.ietf.org/html/rfc8017 定义了PKCS规范
这里描述其中 和本文有关的的内容
为什么RSA加密、签名需要填充?
主要还是为了安全性考虑。
1:
例如TLS流程中的RSA公钥加密(client key exchange),client会使用随即算法生成2+46=48字节的pre_master_key。
若不进行填充而直接加密,那么显然相同的pre_master_key,会得到相同的密文。这种在语义上来说,是不安全的。
2:
加密流程
加密方加密m:c = m^e mod n
,传输c
解密方解密c:m = c^d mod n
,还原m
由于c在网络上传输,如果网络上有人对其进行c' = c*k^e mod n
,这样的替换
那么解密方得到的结果是
(c*k^e)^d mod n
= c^d mod n * k^ed mod n
= m*k
即中间人有办法控制m。
RSAES-PKCS1-v1_5 加密流程
作用:RSA公钥加密
加密
1:待加密数据为M,规范要求M必须不大于k-11,其中k是模数n的字节数。
2:若1满足,则计算不存在字节0的随机值PS,显然根据下面的等式可以推算出,PS的长度是 k - M_len - 3
EM = 0x00 || 0x02 || PS || 0x00 || M
EM作为RSA运算的底数M,进行运算。C = EM ^e mod n
解密
1:校验C的长度,C必须是k字节长度。
2:C ^d mod n得到EM
EM理论上是0x00 || 0x02 || PS || 0x00 || M
这种格式的,所以校验的方法也相对比较简单。
先判断开头2字节是否是0x00 0x02,然后找到第一个0x00,这个0x00后面的值就是解密后的明文。
RSASSA-PKCS1-V1_5-SIGN 签名流程
该签名流程,使用了EMSA-PKCS1-v1_5 封装格式
RSA签名填充
例如需要签名一段数据 M,其长度m_len。
(下面1-3 是 EMSA-PKCS1-v1_5封装流程,4 是 RSASSA-PKCS1-V1_5-SIGN 签名流程。)
1:计算M的哈希值,H = hash(M),哈希可能是MD5、SHA1、SHA2等算法。
2:H并不会简单的进行模幂运算,而是需要进行封装后才会进行。
RFC上这么描述,它需要ASN1编码
DigestInfo ::= SEQUENCE {
digestAlgorithm AlgorithmIdentifier,
digest OCTET STRING
}
翻译成C语言的话,就是下面段数据T的数据组织(OID指的具体HASH算法的id,oid_size表示这个id的长度)
p = T
*p++ = ASN1_SEQUENCE | ASN1_CONSTRUCTED;
*p++ = (unsigned char) ( 0x08 + oid_size + hashlen );
*p++ = ASN1_SEQUENCE | ASN1_CONSTRUCTED;
*p++ = (unsigned char) ( 0x04 + oid_size );
*p++ = ASN1_OID;
*p++ = oid_size & 0xFF;
*memcpy( p, oid, oid_size );
*p += oid_size;
*p++ = ASN1_NULL;
*p++ = 0x00;
*p++ = ASN1_OCTET_STRING;
*p++ = hashlen;
*memcpy( p, H, hashlen );//哈希值在这里
熟悉ASN1格式的同学必然觉得这个很简单,但是不熟悉ASN1格式的同学肯定看到了一头雾水。
换个角度来看T,如果哈希算法确定,即oid确定,那么hashlen之前的数据都是确定的值。的确如此:
RFC中举例了各个常用算法组织成的 T
MD2: (0x)30 20 30 0c 06 08 2a 86 48 86 f7 0d 02 02 05 00 04
10 || H.
MD5: (0x)30 20 30 0c 06 08 2a 86 48 86 f7 0d 02 05 05 00 04
10 || H.
SHA-1: (0x)30 21 30 09 06 05 2b 0e 03 02 1a 05 00 04 14 || H.
SHA-224: (0x)30 2d 30 0d 06 09 60 86 48 01 65 03 04 02 04
05 00 04 1c || H.
SHA-256: (0x)30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00
04 20 || H.
SHA-384: (0x)30 41 30 0d 06 09 60 86 48 01 65 03 04 02 02 05 00
04 30 || H.
SHA-512: (0x)30 51 30 0d 06 09 60 86 48 01 65 03 04 02 03 05 00
04 40 || H.
SHA-512/224: (0x)30 2d 30 0d 06 09 60 86 48 01 65 03 04 02 05
05 00 04 1c || H.
SHA-512/256: (0x)30 31 30 0d 06 09 60 86 48 01 65 03 04 02 06
05 00 04 20 || H.
不熟悉ASN1格式的新同学,就认为H需要如上的由HASH算法决定的定值进行填充即可,但是这不会对拓展你的知识面有帮助。
3:EM = 0x00 || 0x01 || PS || 0x00 || T
到此,EMSA-PKCS1-v1_5规范定义完毕,下一步才是真正的签名
4:计算C = EM^d mod n
,得到C就是签名的最后结果。
验证RSA签名
这里不赘述了,将上面的流程逆过就行了。
RSASSA-PSS 签名流程
作用:RSA私钥签名
RSASSA-PSS 签名流程 使用了 EMSA-PSS 封装格式。
我们先描述 EMSA-PSS 封装格式,因为这是 RSASSA-PSS签名流程的一部分。
签名
若要对数据M进行签名
步骤:
1:计算 mHash = Hash(M)
,其长度为hashlen
2:计算slen长度的随机值salt,这里slen取hashlen。
3:计算 M' = Hash(00 00 00 00 00 00 00 00 || mHash || salt)
4:计算 DB = PS || 0x01 || salt
,PS是字节0。PS的长度是nLen - 2*hashlen - 2
。然后
到这里,待签名数据的内存布局如下
ptr = 00 00 00 ... 00 00 || 0x01 || salt || M' || 0xBC
看上面这种内存布局,也就好理解为什么PS的长度是 nLen - 2*hashlen - 2
了。
5:进行MGF运算
5-1:
计算hash值mask
unsigned char counter[4] = {0};
mask = Hash(M' || counter);
counter++;
5-2:
ptr ^= mask;
ptr += hashlen;
5-3:
执行5-1,5-2,直到 ptr 中的 salt 也被 mask 亦或运算 为止。最后的结果是 EM。
EM = maskDB || M' || 0xBC
。
换句话说,不考虑counter以及hash函数,maskDB, 是由M’亦或得到的。
6:执行 EM^d mod n 得到签名结果C。
注意:RFC上,对EM的首字节还需要特殊处理,但是实际应用中,都是一个固定的操作,这里不说了。(具体见 9.1.1 节的 step 11 )。
上述1-5的步骤,我们称之为 EMSA-PSS 封装格式,使用流程图描述:
+-----------+
| M |
+-----------+
|
V
Hash
|
V
+--------+----------+----------+
M' = |Padding1| mHash | salt |
+--------+----------+----------+
|
+--------+----------+ V
DB = |Padding2| salt | Hash
+--------+----------+ |
| |
V |
xor <--- MGF <---|
| |
| |
V V
+-------------------+----------+--+
EM = | maskedDB | H |bc|
+-------------------+----------+--+
验签
1:解密C,得到的结果是 EM。
2:解封装EM。EM解密出的结果是EM = maskDB || M' || 0xBC
验证签名没有EMSA-PKCS1-v1_5那么简单,因为EMSA-PKCS1-v1_5封装格式中没有随机值。所以这里需要进行文字描述。
EMSA-PSS解封装的核心就是需要恢复salt,就是封装时的那个随机值。
如何恢复salt, 我们回顾签名第四步的ptr,salt就是M’前的hashlen字节的值,只是被mask亦或了。我们只需要再被mask亦或一次,就能恢复salt。
那mask是什么,我们回顾签名第5-1步,mask = Hash(M' || counter);
,我们的EM
中是存在M’的,所以,验证签名的一方能够计算mask。
换句话说,dataA ^ dataB = dataC,dataC ^ dataB = dataA,这个是简单的数学原理。既然我们有 dataC(EM), dataB(hash(M’+counter)),自然能得到 DataA。
3:执行MGF, 也就是签名流程的第5步,他的结果就是我们能够得到 签名流程第四步的ptr。即能够得到salt。
4:至此我们从EM得到salt,M’,然后我们的入参又有 M,接下来的步骤就是使用 salt 和 M 生成, M”,理论上
M” 和 M’ 是相等的。这就是验证的流程。
示例程序
下面是RSASSA-PSS签名使用的填充方式;
说明:
1:示例程序没有进行签名操作,只进行了
EMSA-PSS的encode和decode。
2:示例程序固定使用了sha256进行hash计算。
3:示例程序的输出256字节。
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
unsigned char tosigned[] = "aaaaaaaaaaaa\n";
unsigned char encoded[256]={0};
unsigned int hash_len;
const EVP_MD *md;
void mgf1(unsigned char *dst, unsigned int dst_len, unsigned char *src, unsigned int src_len)
{
unsigned char tmp[EVP_MAX_MD_SIZE] = {0}, tmp2[EVP_MAX_MD_SIZE];
unsigned char *p = dst, *ctr;
unsigned int mask_len, hash_len = src_len, i;
memcpy(tmp, src, src_len);
ctr = tmp + src_len;
while((int)dst_len > 0)
{
SHA256(tmp, src_len + 4, tmp2);
mask_len = dst_len < hash_len ? dst_len : hash_len;
for(i = 0; i < mask_len; i++)
{
*p++ ^= tmp2[i];
}
dst_len -= mask_len;
ctr[3]++;
}
}
void decode(unsigned char *rsa2048)
{
unsigned int encodelen = 256;
unsigned char mHash[EVP_MAX_MD_SIZE], selfmap[EVP_MAX_MD_SIZE];
unsigned char tmp[8 + EVP_MAX_MD_SIZE*2]={0};
unsigned char *map, *salt;
/*这个函数应该是decode前被调用,而不是decode中*/
SHA256(tosigned, strlen(tosigned), mHash);
/*get M' */
map = rsa2048 + encodelen - 1 - hash_len;
/*use M' to do xor*/
mgf1(rsa2048, encodelen - 1 - hash_len, map, hash_len);
/*recover the salt in encoded buffer*/
salt = rsa2048 + encodelen - 1 - hash_len*2;
/*calc self M' */
memcpy(tmp + 8, mHash, hash_len);
memcpy(tmp + 8 + hash_len, salt, hash_len);
SHA256(tmp, 8 + hash_len + hash_len, selfmap);
if(!memcmp(map, selfmap, hash_len))
{
printf("decode SUCCESS\n");
}
}
void encode(unsigned char *rsa2048)
{
unsigned char mHash[EVP_MAX_MD_SIZE], map[EVP_MAX_MD_SIZE];
unsigned char *salt;
unsigned char tmp[8 + EVP_MAX_MD_SIZE*2]={0};
unsigned char *p = rsa2048;
unsigned int rsa_size = 256;
/*这个函数应该是encode前被调用,而不是decode中*/
/*step 1 mHash*/
SHA256(tosigned, strlen(tosigned), mHash);
/*step 2 radnom salt*/
salt = (p + rsa_size - 2 - hash_len*2);
*salt++ = 0x01;
/*Random*/
memset(salt, 0x12, hash_len);
memcpy(tmp + 8, mHash, hash_len);
memcpy(tmp + 8 + hash_len, salt, hash_len);
SHA256(tmp, 8 + hash_len+hash_len, p + rsa_size - hash_len - 1);
mgf1(rsa2048, rsa_size - hash_len - 1, p + rsa_size - hash_len - 1, hash_len);
rsa2048[rsa_size - 1] = 0xBC;
/*Set the leftmost 8emLen - emBits bits of the leftmost octet in maskedDB to zero.*/
rsa2048[0] &= 0xFF >> 1;
}
void main()
{
unsigned char rsa2048[256]={0};
md = EVP_sha256();
hash_len = EVP_MD_size(md);
encode(rsa2048);
//private_key_enc(rsa2048);
//private_key_edc(rsa2048);
decode(rsa2048);
}