如何验证SafetyNet返回的apkCertificateDigestSha256与Android应用证书SHA256的一致性?
解决SafetyNet apkCertificateDigestSha256与应用证书指纹匹配问题
我懂你现在卡在哪了——直接把SafetyNet返回的Base64编码证书哈希转成UTF-8字符串得到乱码,和keytool输出的SHA256指纹对不上。这是因为你误解了这个字段的本质:apkCertificateDigestSha256是证书SHA256哈希的原始二进制值的Base64编码,它不是可读的文本字符串,直接转UTF-8自然会出现乱码。
正确的验证步骤
要让两者匹配,你需要按以下步骤操作:
- 解码Base64字符串得到二进制字节数组(这一步你已经做对了)
- 将二进制字节数组转换成十六进制字符串
- 去除keytool输出的SHA256指纹中的冒号
- 对比两个十六进制字符串是否完全一致
具体代码实现(Kotlin)
这里给你一个直接可用的Kotlin工具函数,用来把字节数组转成不带冒号的十六进制字符串:
fun bytesToHex(bytes: ByteArray): String { val hexChars = CharArray(bytes.size * 2) for (i in bytes.indices) { val v = bytes[i].toInt() and 0xFF hexChars[i * 2] = "0123456789ABCDEF"[v ushr 4] hexChars[i * 2 + 1] = "0123456789ABCDEF"[v and 0x0F] } return String(hexChars) }
然后用这个函数处理你的解码结果:
// SafetyNet返回的证书哈希Base64字符串 val base64Digest = "AJRBzWCfJIY7QD2cp4sv9t0cCGMRGdxuID9VdPLV1H4=" // 解码得到二进制字节数组 val digestBytes = Base64.decode(base64Digest, Base64.DEFAULT) // 转成十六进制字符串 val hexDigest = bytesToHex(digestBytes) // 处理keytool输出的SHA256指纹:去掉冒号并转大写 val keytoolSha256 = "00:94:41:CD:60:9F:24:86:3B:40:3D:9C:A7:8B:2F:F6:DD:1C:08:63:11:19:DC:6E:20:3F:55:74:F2:D5:D4:7E" val cleanedKeytoolSha256 = keytoolSha256.replace(":", "").uppercase() // 最终对比 val isCertificateMatch = hexDigest == cleanedKeytoolSha256
额外注意事项
- 密钥库要对应:如果验证的是debug版本APK,必须用debug keystore生成的指纹;如果是release版本,一定要用对应的release keystore,别搞混环境。
- 别忘了验证Nonce:一定要对比响应中的
nonce和你服务器生成并发送的nonce,防止重放攻击。 - 完整的响应签名验证:除了证书哈希,你还得用Google的公钥验证整个SafetyNet响应的签名,确保响应没被篡改——这才是SafetyNet完整性验证的核心环节。
内容的提问来源于stack exchange,提问作者Sagar Nayak
相关产品推荐
相关产品推荐

