You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 02:30:52