Itext7智能卡签名PDF报错:文档自签名后已被篡改或损坏
问题分析与解决方案
核心问题定位
出现「文档自签名后已被篡改或损坏」的核心原因是签名哈希处理重复,以及PKCS#11签名机制的使用不符合规范,具体如下:
1. 双重哈希导致签名验证不匹配
在SmartCardSignaturePDF的sign方法中,你对iText传入的message做了二次SHA-256哈希:
return signData(MessageDigest.getInstance("SHA-256").digest(message));
但iText的signDetached方法已经通过BouncyCastleDigest完成了哈希计算,传入sign方法的message本身就是最终要签名的SHA-256哈希值。二次哈希后,智能卡签名的是错误的内容,导致PDF验证时哈希不匹配,触发篡改报错。
修复代码:
@Override public byte[] sign(byte[] message) throws GeneralSecurityException { try { // 直接传入iText计算好的哈希值,禁止二次哈希 return signData(message); } catch (HardwareException | SignatureException e) { // 不要吞异常,抛出后便于排查问题 throw new GeneralSecurityException("智能卡签名失败", e); } }
2. PKCS#11签名机制的规范适配
你使用的CKM_RSA_PKCS机制要求输入是符合PKCS#1 v1.5标准的DigestInfo结构,而非单纯的哈希值。SHA-256对应的DigestInfo固定格式为:
30 31 30 0D 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 + 32字节SHA-256哈希
如果你的智能卡PKCS#11实现不会自动构建这个结构,直接传入哈希值会导致签名内容不符合规范,验证失败。
修复方案:
在signData方法中,先将哈希值包装为DigestInfo结构再传入签名:
private byte[] signData(byte[] hash) throws HardwareException { // 构建SHA-256的DigestInfo结构 byte[] digestInfoPrefix = Hex.decodeHex("3031300D060960864801650304020105000420"); byte[] digestInfo = new byte[digestInfoPrefix.length + hash.length]; System.arraycopy(digestInfoPrefix, 0, digestInfo, 0, digestInfoPrefix.length); System.arraycopy(hash, 0, digestInfo, digestInfoPrefix.length, hash.length); // 后续签名逻辑不变,使用digestInfo替代原input_data Mechanism signMechanism = Mechanism.get(PKCS11Constants.CKM_RSA_PKCS); char[] signTemplate = "Sign".toCharArray(); byte[] id = "Sign".getBytes(); RSAPrivateKey rsaPrivateKeyTemplate = new RSAPrivateKey(); rsaPrivateKeyTemplate.getToken().setBooleanValue(Boolean.TRUE); rsaPrivateKeyTemplate.getId().setByteArrayValue(id); rsaPrivateKeyTemplate.getLabel().setCharArrayValue(signTemplate); try { this.cegerCardsession.findObjectsInit(rsaPrivateKeyTemplate); PKCS11Object[] key = this.cegerCardsession.findObjects(1024); PrivateKey pKey; if(key.length == 0) { Mechanism keyGenMechanism = Mechanism.get(PKCS11Constants.CKM_RSA_PKCS_KEY_PAIR_GEN); RSAPublicKey rsaPublicKeyTemplate = new RSAPublicKey(); rsaPublicKeyTemplate.getToken().setBooleanValue(Boolean.TRUE); rsaPublicKeyTemplate.getId().setByteArrayValue(id); rsaPublicKeyTemplate.getLabel().setCharArrayValue(signTemplate); KeyPair keyPair = this.cegerCardsession.generateKeyPair(keyGenMechanism, rsaPublicKeyTemplate, rsaPrivateKeyTemplate); pKey = keyPair.getPrivateKey(); } else { pKey = (PrivateKey) key[0]; } this.cegerCardsession.signInit(signMechanism, pKey); return this.cegerCardsession.sign(digestInfo); } catch (TokenException e) { throw new HardwareException(e.getMessage(), e.getCause()); } }
3. 其他优化建议
- 完善证书链:原代码只传入了单个证书,建议传入完整的证书链(包括中间CA证书),避免验证时因证书信任链不完整出现问题:
Certificate[] certChain = uc.getCertChain(); // 假设UserCertificates提供获取完整链的方法 if (certChain == null || certChain.length == 0) { certChain = new Certificate[]{uc.getCert()}; } - 不要吞异常:原代码中
pdfSigner.signDetached的catch块为空,会掩盖签名过程中的错误,建议添加日志或抛出异常,便于排查问题。
为什么其他智能卡无此问题
旧智能卡使用直接APDU命令,可能在APDU层面自动处理了DigestInfo包装或忽略了双重哈希的问题;而新卡的IAIK实现严格遵循PKCS#11规范,对输入格式要求更严格,因此暴露了代码中的哈希处理错误。
内容的提问来源于stack exchange,提问作者Miguel SIlva
相关产品推荐
相关产品推荐

