RSA-PKCS1-v1_5

目录

简介

PKCS #1 是 RSA 密码算法相关的规范,包含 RSA 密钥格式、加密/解密原语、签名/验签原语、加密方案、签名方案以及 ASN.1 表示方式等内容。RFC 8017 对应的是 PKCS #1 v2.2。(IETF Datatracker)

本文主要整理 RSASSA-PKCS1-v1_5 签名方案,也就是常说的:

RSA + PKCS#1 v1.5 Signature
RSASSA-PKCS1-v1_5

它和 RSA-PSS 都是 RSA 签名方案,但两者的编码方式不同。RSASSA-PKCS1-v1_5 是把 RSASP1 / RSAVP1 这两个 RSA 原语和 EMSA-PKCS1-v1_5 编码方法组合起来使用。(IETF Datatracker),此文只介绍PKCS1_v1.5,Rsa相关的算法可参考 RSA2048


PKCS1-v1_5 的核心思想

RSA 签名不能直接对原始消息 M 做私钥运算,而是先要把消息处理成一个固定长度的编码块 EM

整体流程可以理解为:

M
↓
Hash(M)
↓
DigestInfo
↓
EMSA-PKCS1-v1_5 编码
↓
EM = 00 01 FF ... FF 00 DigestInfo
↓
RSA 私钥运算
↓
Signature

其中最关键的编码结构是:

EM = 0x00 || 0x01 || PS || 0x00 || T

其中:

PS = FF FF FF ... FF
T  = DigestInfo,也就是 Hash算法标识 + Hash值

RFC 8017 中定义 EMSA-PKCS1-v1_5 是确定性的编码方法,也就是同样的消息、同样的密钥、同样的 Hash 算法,生成的签名通常是一样的。(IETF Datatracker)


术语说明

符号含义
MMessage,待签名消息
SSignature,签名结果
nRSA modulus,模数
eRSA public exponent,公钥指数
dRSA private exponent,私钥指数
kRSA 模数 n 的字节长度
HHash(M) 的结果
TDigestInfo 的 DER 编码
EMEncoded Message,编码后的消息
mEM 转成的大整数
s签名大整数
OS2IPOctet String to Integer Primitive,字节串转整数
I2OSPInteger to Octet String Primitive,整数转字节串
RSASP1RSA 签名原语
RSAVP1RSA 验签原语

EMSA-PKCS1-v1_5 编码

编码过程如下,原文见 IETF Datatracker

1. H = Hash(M)

2. T = DigestInfo(Hash算法标识 + H)

3. 检查长度:
   emLen >= tLen + 11

4. 构造 PS:
   PS = FF FF FF ... FF
   长度为 emLen - tLen - 3
   且 PS 至少 8 字节

5. 拼接:
   EM = 00 || 01 || PS || 00 || T
   
6.输出EM   

RFC 8017 中对编码块的定义就是:

EM = 0x00 || 0x01 || PS || 0x00 || T

其中 PS 全部由 0xff 组成,并且长度至少 8 字节。

以 SHA-256 为例,DigestInfo 的前缀是:

3031300d060960864801650304020105000420

所以:

T = 3031300d060960864801650304020105000420 || SHA256(M)

SHA-256 的 Hash 长度是 32 字节,因此:

tLen = 19 + 32 = 51 字节

如果 RSA 是 2048 bit:

k = 2048 / 8 = 256 字节

则:

PS长度 = 256 - 51 - 3 = 202 字节

最终编码结构大概是:

00 01 FF FF FF ... FF 00 3031300d060960864801650304020105000420 || SHA256(M)

签名流程

签名函数可以表示为:

RSASSA-PKCS1-v1_5-SIGN(K, M)

输入:

K = RSA 私钥
M = 待签名消息

输出:

S = 签名,长度为 k 字节

签名步骤:

1. 对消息 M 做 EMSA-PKCS1-v1_5 编码:

   EM = EMSA-PKCS1-v1_5-ENCODE(M, k)

2. 把 EM 转换成整数:

   m = OS2IP(EM)

3. 使用 RSA 私钥做签名运算:

   s = RSASP1(K, m)

   普通形式可以理解为:

   s = m^d mod n

4. 把整数签名 s 转换成固定长度字节串:

   S = I2OSP(s, k)

5. 输出签名 S

RFC 8017 的签名流程也是先生成 EM,再执行 OS2IPRSASP1I2OSP


验签流程

验签函数可以表示为:

RSASSA-PKCS1-v1_5-VERIFY((n, e), M, S)

输入:

(n, e) = RSA 公钥
M      = 原始消息
S      = 待验证签名

输出:

valid signature
invalid signature

验签步骤:

1. 检查签名长度:

   len(S) == k

   如果长度不等于 k,验签失败。

2. 把签名 S 转换成整数:

   s = OS2IP(S)

3. 使用 RSA 公钥做验签运算:

   m = RSAVP1((n, e), s)

   普通形式可以理解为:

   m = s^e mod n

4. 把整数 m 转回编码消息:

   EM = I2OSP(m, k)

5. 对原始消息 M 再做一次 EMSA-PKCS1-v1_5 编码:

   EM' = EMSA-PKCS1-v1_5-ENCODE(M, k)

6. 比较 EM 和 EM':

   如果 EM == EM',验签成功。
   否则,验签失败。

RFC 8017 的验签流程也是先检查签名长度,然后执行 OS2IPRSAVP1I2OSP,再重新编码消息生成 EM',最后比较 EMEM'


Python 示例

需要安装:

pip install pycryptodome   

示例代码:

from Crypto.PublicKey import RSA
from Crypto.Signature import pkcs1_15
from Crypto.Hash import SHA256
from binascii import hexlify

# 生成 RSA 密钥
key = RSA.generate(2048)

private_key = key
public_key = key.publickey()

print("Public Key:")
print(key.publickey().export_key().decode())

message = b"hello pkcs1 v1.5"

# 计算 Hash
print("Hash:")
h = SHA256.new(message)
print(h.hexdigest())

# 签名
signature = pkcs1_15.new(private_key).sign(h)

print("Signature:")
print(hexlify(signature).decode())

# 验签
try:
    pkcs1_15.new(public_key).verify(h, signature)
    print("Verify: valid signature")
except (ValueError, TypeError):
    print("Verify: invalid signature")

结果如下:
rsa_python_result

工具验证结果:
rsa_tool_python_result


OpenSSL 示例

生成私钥:

openssl genrsa -out rsa_private.pem 2048

导出公钥:

openssl rsa -in rsa_private.pem -pubout -out rsa_public.pem

准备消息:

echo -n "hello pkcs1 v1.5" > message.txt

使用 SHA-256 + PKCS#1 v1.5 签名:

openssl dgst -sha256 -sign rsa_private.pem -out signature.bin message.txt

验签:

openssl dgst -sha256 -verify rsa_public.pem -signature signature.bin message.txt

如果验签成功,会输出:

Verified OK

如下图所示:
rsa_openssl_result


C 语言示例

参考代码:

#define SHA256_DIGEST_SIZE 32U
#define RSA_MIN_MODULUS_BITS 508
#define RSA_MAX_MODULUS_BITS 3072
#define RSA_MAX_MODULUS_LEN  ((RSA_MAX_MODULUS_BITS + 7) / 8)
#define RSA_MAX_PRIME_BITS   ((RSA_MAX_MODULUS_BITS + 1) / 2)
#define RSA_MAX_PRIME_LEN    ((RSA_MAX_PRIME_BITS + 7) / 8)

Std_ReturnType Crypto_Rsa_Rsassa_Pkcs1_v15_Sign(uint8 *sig, uint32 *sig_len,
                                                  const uint8 *msg_hash, uint32 hash_len,
                                                  const rsa_sk_t *sk)
{
    static const uint8 SHA256_DER_PREFIX[19U] = {
        0x30U, 0x31U, 0x30U, 0x0DU, 0x06U, 0x09U, 0x60U, 0x86U,
        0x48U, 0x01U, 0x65U, 0x03U, 0x04U, 0x02U, 0x01U, 0x05U,
        0x00U, 0x04U, 0x20U
    };

    uint8  em[RSA_MAX_MODULUS_LEN];
    uint32 em_len;
    uint32 ps_len;
    uint32 offset;
    int    status;

    if((sig == NULL) || (sig_len == NULL) || (msg_hash == NULL) || (sk == NULL))
        return E_NOT_OK;
    if(hash_len != SHA256_DIGEST_SIZE)
        return E_NOT_OK;

    em_len = (sk->bits + 7U) / 8U;
    if(em_len < (sizeof(SHA256_DER_PREFIX) + SHA256_DIGEST_SIZE + 11U))
        return E_NOT_OK;


    ps_len = em_len - sizeof(SHA256_DER_PREFIX) - SHA256_DIGEST_SIZE - 3U;

    em[0U] = 0x00U;
    em[1U] = 0x01U;
    memset(&em[2U], 0xFFU, ps_len);
    em[2U + ps_len] = 0x00U;
    offset = 3U + ps_len;
    memcpy(&em[offset], SHA256_DER_PREFIX, sizeof(SHA256_DER_PREFIX));
    offset += sizeof(SHA256_DER_PREFIX);
    memcpy(&em[offset], msg_hash, SHA256_DIGEST_SIZE);

    status = private_block_operation(sig, sig_len, em, em_len, (rsa_sk_t *)sk);


    memset(em, 0, sizeof(em));
    return (status == 0) ? E_OK : E_NOT_OK;
}

验签时:

Std_ReturnType Crypto_Rsa_Rsassa_Pkcs1_v15_Verify(const uint8 *signature, uint32 sig_len,
                                                    const uint8 *msg_hash, uint32 hash_len,
                                                    const rsa_pk_t *pk)
{
    /* SHA-256 DigestInfo DER prefix (RFC 8017, Appendix C):
     * 30 31 30 0D 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 */
    static const uint8 SHA256_DER_PREFIX[19U] = {
        0x30, 0x31, 0x30, 0x0D, 0x06, 0x09, 0x60, 0x86,
        0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05,
        0x00, 0x04, 0x20
    };

    uint8    em[RSA_MAX_MODULUS_LEN];
    uint32   em_len;
    uint32   out_len;
    uint32   i;
    uint32   ps_end;
    uint32   digest_info_len;

    if((signature == NULL) || (msg_hash == NULL) || (pk == NULL))
    {
        return E_NOT_OK;
    }

    if(hash_len != SHA256_DIGEST_SIZE)
    {
        return E_NOT_OK;
    }

    em_len = (pk->bits + 7U) / 8U;

    if(sig_len != em_len)
    {
        return E_NOT_OK;
    }


    out_len = em_len;
    if(public_block_operation(em, &out_len, (uint8 *)signature, sig_len, (rsa_pk_t *)pk) != 0)
    {
        return E_NOT_OK;
    }


    /* Verify PKCS#1 v1.5 padding: EM = 0x00 || 0x01 || PS || 0x00 || T */
    if((em[0] != 0x00U) || (em[1] != 0x01U))
    {
        return E_NOT_OK;
    }

    /* Find end of 0xFF padding. */
    ps_end = 2U;
    while(ps_end < em_len && em[ps_end] == 0xFFU)
    {
        ps_end++;
    }

    if(em[ps_end] != 0x00U)
    {
        return E_NOT_OK;
    }

    ps_end++;  /* Skip the 0x00 separator. */

    digest_info_len = sizeof(SHA256_DER_PREFIX) + SHA256_DIGEST_SIZE;

    if((ps_end + digest_info_len) != em_len)
    {
        return E_NOT_OK;
    }

    /* Verify DER prefix. */
    for(i = 0U; i < sizeof(SHA256_DER_PREFIX); ++i)
    {
        if(em[ps_end + i] != SHA256_DER_PREFIX[i])
        {
            return E_NOT_OK;
        }
    }

    /* Verify hash. */
    for(i = 0U; i < SHA256_DIGEST_SIZE; ++i)
    {
        if(em[ps_end + sizeof(SHA256_DER_PREFIX) + i] != msg_hash[i])
        {
            return E_NOT_OK;
        }
    }

    return E_OK;
}

工具的验证基于C语言,不再赘述验证结果.


常见问题

1. PKCS1-v1_5 是加密还是签名?

都有。

PKCS#1 里有:

RSAES-PKCS1-v1_5   // 加密方案
RSASSA-PKCS1-v1_5  // 签名方案

本文整理的是:

RSASSA-PKCS1-v1_5

也就是签名方案。注意不要把加密填充和签名填充混用。


2. 签名长度是多少?

签名长度等于 RSA 模数长度。

例如:

RSA-1024 -> 128 字节
RSA-2048 -> 256 字节
RSA-3072 -> 384 字节
RSA-4096 -> 512 字节

所以即使消息只有 1 字节,RSA-2048 的签名也是 256 字节。


3. 为什么需要 DigestInfo?

因为 PKCS1-v1_5 签名不仅要放 Hash 值,还要放 Hash 算法标识。

例如 SHA-256:

DigestInfo = SHA256算法标识 || SHA256(M)

这样验签方知道这个签名对应的是哪个 Hash 算法。


4. PKCS1-v1_5 和 RSA-PSS 可以互相验签吗?

不可以。

PKCS1-v1_5 签名不能用 PSS 验签
PSS 签名不能用 PKCS1-v1_5 验签

它们底层都是 RSA,但是编码结构不同。


5. 新项目还推荐 PKCS1-v1_5 吗?

如果是新协议设计,更推荐:

RSA-PSS + SHA-256

如果是兼容老系统、证书、已有协议,仍然经常会遇到:

RSA-PKCS1-v1_5 + SHA-256

RFC 8017 也提到,虽然没有已知攻击直接针对 EMSA-PKCS1-v1_5 编码方法,但作为预防未来发展的措施,建议逐步迁移到 EMSA-PSS。(IETF Datatracker)


总结

RSASSA-PKCS1-v1_5 的核心流程可以总结为:

签名:

M
↓
Hash(M)
↓
DigestInfo
↓
EM = 00 01 FF...FF 00 DigestInfo
↓
m = OS2IP(EM)
↓
s = m^d mod n
↓
S = I2OSP(s, k)
验签:

S
↓
s = OS2IP(S)
↓
m = s^e mod n
↓
EM = I2OSP(m, k)
↓
重新根据 M 生成 EM'
↓
比较 EM 和 EM'

PKCS1-v1_5 签名 = Hash 消息 + 加 DigestInfo + 构造固定格式填充块 + RSA 私钥运算。