7. Keystore签名验签:sign与verify接口、摘要算法选择、PKCS#1 v1.5与PSS填充模式

签名验签,说白了就是给数据盖个「防伪章」。你发出去的数据,别人能验证这章是不是你盖的、有没有被篡改。Android Keystore 提供的 sign()verify() 接口,就是干这个事的。

我个人习惯把签名验签分成三步:选算法 → 算摘要 → 做填充。每一步都有坑,咱们一个一个说。

7.1 sign 与 verify 接口:怎么用?

先看代码,这是最标准的用法。我项目中写过不下十次这个模板,几乎没变过。

// 签名
private fun signData(key: PrivateKey, data: ByteArray): ByteArray {
    val signature = Signature.getInstance("SHA256withRSA")
    signature.initSign(key)
    signature.update(data)
    return signature.sign()
}

// 验签
private fun verifyData(key: PublicKey, data: ByteArray, signatureBytes: ByteArray): Boolean {
    val signature = Signature.getInstance("SHA256withRSA")
    signature.initVerify(key)
    signature.update(data)
    return signature.verify(signatureBytes)
}

这里有个细节:update() 可以分多次调用。比如你数据很大,可以分段喂给签名器。但要注意,update 的顺序必须和验签时完全一致。我曾经有个同事,签名时先 update 了 header 再 update body,验签时顺序反了,查了一下午 bug……

小提示:如果数据不大,直接用 sign(data) 一步到位也行。但分步 update 更灵活,适合流式处理。

7.2 摘要算法选择:SHA256 还是 SHA512?

签名不是直接签原始数据,而是先算一个摘要(hash),再对摘要签名。所以摘要算法的选择,直接影响安全性和性能。

算法 摘要长度 安全性 性能 推荐场景
SHA1 160 bit ❌ 已不推荐 兼容旧系统(不建议新项目用)
SHA256 256 bit ✅ 安全 较快 通用场景,默认选择
SHA384 384 bit ✅ 更安全 中等 高安全需求
SHA512 512 bit ✅ 最安全 较慢 金融、政务等极端场景

我个人建议:无脑选 SHA256withRSA。为什么?SHA256 目前没有实际碰撞攻击,性能也够用。SHA512 虽然更安全,但签名长度更长,网络传输和存储成本都高。除非你是在做银行核心系统,否则没必要。

注意:不要用 SHA1!Google 在 2017 年就宣布 SHA1 碰撞攻击可行。虽然 Android 上还没大规模出事,但别赌。

7.3 PKCS#1 v1.5 与 PSS 填充模式

嗯,这里要重点讲。RSA 签名不是直接把摘要塞进去,而是先做填充。填充模式有两种:PKCS#1 v1.5PSS

7.3.1 PKCS#1 v1.5:老牌选手

PKCS#1 v1.5 是 RSA 签名最早的标准。它的填充结构很简单:

填充后数据 = 0x00 || 0x01 || 0xFF...0xFF || 0x00 || DigestInfo

其中 0xFF 填充到目标长度,DigestInfo 包含摘要算法标识和摘要值。

优点:兼容性好,几乎所有 RSA 库都支持。缺点:存在一些理论攻击,比如 Bleichenbacher 攻击。虽然实际利用条件苛刻,但安全社区已经不太推荐了。

我记得有一次做安全审计,对方直接要求「不能用 PKCS#1 v1.5,必须上 PSS」。从那以后,我新项目都默认用 PSS。

7.3.2 PSS:现代选择

PSS(Probabilistic Signature Scheme)是更安全的填充方案。它引入了随机盐值,每次签名结果都不一样——即使签同样的数据。

PSS 的填充结构:

填充后数据 = maskedDB || salt || 0xBC

其中 maskedDB 是经过掩码处理的摘要和填充,salt 是随机数。

用代码指定 PSS 模式:

val signature = Signature.getInstance("SHA256withRSA/PSS")
// 或者更精细的控制:
val pssSignature = Signature.getInstance("SHA256withRSA")
pssSignature.setParameter(PSSParameterSpec(
    "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1
))

这里 32 是盐值长度(字节),一般和摘要长度一致。SHA256 就是 32 字节。

核心对比:
  • PKCS#1 v1.5:确定性签名,兼容性好,有理论风险
  • PSS:概率性签名,安全性更高,推荐新项目使用

7.4 避坑指南:我踩过的三个坑

讲几个真实案例,你遇到了能少走弯路。

坑一:签名长度不一致

我曾经用 PSS 签名,每次结果都不一样。测试同学说「你这签名不稳定啊」。其实 PSS 就是这样——随机盐值导致每次签名不同。验签时只要公钥对、数据对,就能通过。所以 不要用签名结果做缓存 key

坑二:算法字符串写错

"SHA256withRSA""SHA-256withRSA" 差一个横线,前者是 Android 标准写法,后者在某些 JDK 实现里才认。我见过有人从后端复制代码,横线没去掉,在 Android 上直接抛 NoSuchAlgorithmException

坑三:盐值长度不匹配

PSS 签名时设了 32 字节盐值,验签时也要用同样的参数。如果验签端(比如服务器)默认盐值是 20 字节,就会验签失败。所以 跨平台签名一定要约定好 PSS 参数

7.5 知识体系图

下面这张图总结了签名验签的核心流程和选择逻辑。我建议你保存下来,做方案设计时对照着看。

Keystore 签名验签核心流程 原始数据 (Data) 摘要算法 (SHA256) 填充模式 PKCS#1 v1.5 PSS (推荐) 签名结果 (Signature) 确定性签名 概率性签名(带盐值)

7.6 总结

签名验签这件事,说起来就三个要点:

  • 接口sign()verify() 成对使用,注意 update 顺序
  • 摘要:SHA256 是甜点选择,别碰 SHA1
  • 填充:新项目无脑上 PSS,兼容老系统才考虑 PKCS#1 v1.5

最后说一句:签名算法选型,宁可保守一点,别为了省那几毫秒性能去冒险。安全攻防是军备竞赛,你永远不知道对手手里有什么工具。


公众号:蓝海资料掘金营,微信deep3321