
PKIJS 中签名后无法自验证,通常因 CryptoKey 导入时指定的算法与 SignedData.sign() 内部默认签名机制不一致所致;将私钥导入算法从 "RSA-PSS" 改为 "RSASSA-PKCS1-v1_5" 即可解决。
pkijs 中签名后无法自验证,通常因 `cryptokey` 导入时指定的算法与 `signeddata.sign()` 内部默认签名机制不一致所致;将私钥导入算法从 `"rsa-pss"` 改为 `"rsassa-pkcs1-v1_5"` 即可解决。
在使用 PKIJS(@peculiar/webcrypto + asn1js + pkijs)实现 CMS 签名与验证时,一个常见却隐蔽的问题是:本地签名后调用 .verify() 返回 false,而用 OpenSSL 生成的等效签名却能成功验证。这并非证书或数据问题,而是底层 Web Crypto API 的算法协商不一致导致的。
? 根本原因:密钥导入算法与签名算法不匹配
PKIJS 的 SignedData.sign() 方法在内部默认使用 RSASSA-PKCS1-v1_5(即传统 PKCS#1 v1.5 填充)进行签名,而非 RSA-PSS。但你在加载私钥时指定了:
await crypto.importKey("pkcs8", ber, {
name: "RSA-PSS", // ❌ 错误:PSS 是另一种签名模式
hash: "SHA-256",
}, true, ["sign"]);
该配置仅允许该 CryptoKey 用于 sign() 调用中的 PSS 模式,而 pkijs.SignedData.sign() 并未以 PSS 方式调用 subtle.sign() —— 它实际调用的是:
subtle.sign({ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, privateKey, data)
由于密钥被限制为仅支持 "RSA-PSS",浏览器/运行时会静默拒绝或降级处理(行为因环境而异),最终导致生成的签名结构异常(如 DigestAlgorithmIdentifier 缺失、SignatureAlgorithmIdentifier 不匹配),致使后续 .verify() 失败。
✅ 正确做法:导入时声明匹配的算法
将私钥导入逻辑修正为明确声明 RSASSA-PKCS1-v1_5:
case "PRIVATE KEY":
for (const ber of bers) {
const key = await crypto.importKey("pkcs8", ber, {
name: "RSASSA-PKCS1-v1_5", // ✅ 正确:与 pkijs 默认签名行为一致
hash: "SHA-256"
}, true, ["sign"]);
ret.push(key);
}
return ret;
同时确保签名流程保持简洁(你当前的 signData 实现已正确):
export async function signData(
data: ArrayBuffer,
certificate: Certificate,
privateKey: CryptoKey
): Promise<signeddata> {
const cmsSigned = new SignedData({
encapContentInfo: new EncapsulatedContentInfo({
eContentType: ContentInfo.DATA,
// eContent 可选:若需封装原始数据,取消注释并设置
// eContent: new OctetString({ valueHex: data })
}),
certificates: [certificate],
signerInfos: [
new SignerInfo({
sid: new IssuerAndSerialNumber({
issuer: certificate.issuer,
serialNumber: certificate.serialNumber
})
})
]
});
await cmsSigned.sign(privateKey, 0, "SHA-256", data); // ✅ 自动使用 RSASSA-PKCS1-v1_5
return cmsSigned;
}</signeddata>
⚠️ 注意事项
-
不要混用 PSS 和 PKCS1-v1_5:若需使用 RSA-PSS(例如要求更强的安全性或合规性),则必须:
- 显式传入 PSS 参数(如
saltLength)到sign(); - 在
SignerInfo中手动设置signatureAlgorithm为AlgorithmIdentifierforRSASSA-PSS; - 确保验证端也以完全相同参数解析。
- 显式传入 PSS 参数(如
-
证书链与时间验证:
checkChain: true要求trustedCerts至少包含根 CA 或中间 CA;若仅传入终端证书,链验证会失败(但你的案例中 OpenSSL 签名能过,说明证书本身有效,故重点仍在签名算法)。 -
Deno 环境兼容性:Deno 的
WebCrypto实现严格遵循规范,不支持跨算法“宽松匹配”,因此算法声明必须精确。
✅ 验证是否修复
修改后重新运行原逻辑:
const ok = await signedData.verify({
signer: 0,
checkChain: true,
trustedCerts: [certificate],
data: data.buffer
});
console.log(ok); // → true ✅
至此,PKIJS 签名与自验证将完全一致,且与 OpenSSL cms -sign 输出二进制 DER 签名兼容(二者均默认采用 RSASSA-PKCS1-v1_5 + SHA-256)。











