RSA-PKCS1-v1_5
目录
- 简介
- PKCS1-v1_5 的核心思想
- 术语说明
- EMSA-PKCS1-v1_5 编码
- 签名流程
- 验签流程
- PKCS1-v1_5 和 PSS 的区别
- Python 示例
- OpenSSL 示例
- C 语言实现时的关键点
- 常见问题
- 总结
简介
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)
术语说明
| 符号 | 含义 |
|---|---|
M | Message,待签名消息 |
S | Signature,签名结果 |
n | RSA modulus,模数 |
e | RSA public exponent,公钥指数 |
d | RSA private exponent,私钥指数 |
k | RSA 模数 n 的字节长度 |
H | Hash(M) 的结果 |
T | DigestInfo 的 DER 编码 |
EM | Encoded Message,编码后的消息 |
m | EM 转成的大整数 |
s | 签名大整数 |
OS2IP | Octet String to Integer Primitive,字节串转整数 |
I2OSP | Integer to Octet String Primitive,整数转字节串 |
RSASP1 | RSA 签名原语 |
RSAVP1 | RSA 验签原语 |
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,再执行 OS2IP、RSASP1、I2OSP。
验签流程
验签函数可以表示为:
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 的验签流程也是先检查签名长度,然后执行 OS2IP、RSAVP1、I2OSP,再重新编码消息生成 EM',最后比较 EM 和 EM'。
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")
结果如下:

工具验证结果:

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
如下图所示:

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 私钥运算。