使用java和iText对PDF哈希进行签名

使用java和iText对PDF哈希进行签名,java,pdf,hash,itext,digital-signature,Java,Pdf,Hash,Itext,Digital Signature,我有一个生成PDF的应用程序,需要签名 我们没有签署文档的证书,因为它们位于HSM中,我们可以使用证书的唯一方法是使用Web服务 PdfReader reader = new PdfReader(src); reader.setAppendable(true); ByteArrayOutputStream baos = new ByteArrayOutputStream(); FileOutputStream fout = new FileOutputStream

我有一个生成PDF的应用程序,需要签名

我们没有签署文档的证书,因为它们位于HSM中,我们可以使用证书的唯一方法是使用Web服务

    PdfReader reader = new PdfReader(src);
    reader.setAppendable(true);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    FileOutputStream fout = new FileOutputStream(dest);
    PdfStamper stamper = PdfStamper.createSignature(reader, fout, '\0');
    PdfSignatureAppearance appearance  = stamper.getSignatureAppearance();

    appearance.setReason("Test");
    appearance.setLocation("footer");
    appearance.setVisibleSignature(new Rectangle(100, 100, 200, 200), 1, null);
    appearance.setCertificate(certChain[0]);

    PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
    dic.setReason(appearance.getReason());
    dic.setLocation(appearance.getLocation());
    dic.setContact(appearance.getContact());
    dic.setDate(new PdfDate(appearance.getSignDate()));
    appearance.setCryptoDictionary(dic);

    HashMap<PdfName, Integer> exc = new HashMap<PdfName, Integer>();
    exc.put(PdfName.CONTENTS, new Integer(8192 * 2 + 2));
    appearance.preClose(exc);

    ExternalDigest externalDigest = new ExternalDigest()
    {
        public MessageDigest getMessageDigest(String hashAlgorithm) throws GeneralSecurityException
        {
            return DigestAlgorithms.getMessageDigest(hashAlgorithm, null);
        }
    };

    PdfPKCS7 sgn = new PdfPKCS7(null, certChain, "SHA256", null, externalDigest, false);
    InputStream data = appearance.getRangeStream();
    byte[] hash = DigestAlgorithms.digest(data, externalDigest.getMessageDigest("SHA256"));
    Calendar cal = Calendar.getInstance();

    byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, cal, null, null, CryptoStandard.CMS);
    sh = MessageDigest.getInstance("SHA256", "BC").digest(sh);

    String hashPdf = new String(Base64.encodeBytes(sh));

    String hashSignat = hashPdf;
在这一点上,我们得到一个PDF签名,但签名无效。Adobe称“文档自签署以来已被修改或损坏”

我已经通过了,但是没有运气。

示例文件 您在对问题的评论中共享了由代码签名的

对该文件的分析(使用test
testpriyankasignaturesampleinformedapprovement\u Signed
执行)表明,实际签名的哈希是

1134ED96F42AC7352E546BE0E906C0BF5D4429AEAFAC39145DB40A0BB7E817B
它应该是经过身份验证的属性的散列,但它们的散列是

E7101D9770ABF5E58D43670AAC6E9418265AE80F74B3BBDFB67911C50CC5D949。
有符号字节范围散列为

D7917CF3BA9FE5D5B626ED825965D699F7C54DBF9F18DECE18EF8DD36DC4C28,
所以这也不是散列。因此,不清楚该签名散列来自何处

最终证明

web服务上的编码实用程序不同,原因是解码的哈希值错误,并且不需要
MessageDigest(sh=MessageDigest.getInstance(“SHA256”,“BC”).digest(sh))
。这一行给出了错误的哈希值

签名以前签名的文件 此后,出现了一个新问题:

当我对已经签名的文件进行签名时,该文件正在获得签名,但签名无效。其显示“签名字节范围无效”

对于已签名的PDF,您必须使用附加模式。为此,您需要一个不同的
PdfStamper.createSignature
重载,再加上一个参数
boolean
,将其设置为
true

原因是通常(即不激活附加模式)iText会重新组织内部PDF结构并删除未使用的对象。在已签名PDF中,这通常会移动(已存在)签名的位置,从而使签名结构无效。不过,使用追加模式,iText会保持原始PDF字节不变,只追加新数据

更改哈希 我只是不明白为什么字节“sh”的值每次都在同一个文件中改变?实际上,我想存储文件的哈希值。是因为“卡尔”吗

实际上,每次您开始操作PDF时,结果都会得到一个新的唯一ID。此外,修改时间也会被存储。对于签名用例,签名时间也不同

我可以一次获取散列并将其存储在db中,稍后对散列进行签名并附加到pdf中吗

你要么必须

  • 保持压模和外观暂时打开,否则你必须
  • 保留准备好的文件,否则你必须
  • 修补iText以允许您设置签名时间、操作时间和id的固定值
第一种选择对我来说是不可能的。我没有第二个第三个选择。你能详细说明这两个问题吗,或者给我一些参考资料吗

修补iText以对每个文件强制执行固定哈希 好的,首先是第三个选项,修补iText,通常是您不想做的事情,因为这会使以后合并iText更新变得困难

OpenPdf(一个较旧的iText fork)包含一个补丁,该补丁将
EnforcedModificationDate
OverrideFileId
,以及
IncludeFileID
属性添加到
PdfStamper
。(
PdfSignatureAppearance
已具有
SignDate
属性。)此修补程序已应用于允许eSignature DSS在其签名过程中使用OpenPdf(这还包括创建签名PDF两次,因此需要固定哈希值)

您可能不想切换到这个旧的iText fork,因为它错过了许多新版本的修复和新选项

保留已准备好的文件 因此,您可能应该将原始创建的文件保留为空签名,例如在某个临时文件夹中,并在检索最终签名后立即应用延迟签名

这本质上就是iText示例的全部内容,它首先创建一个包含所有内容的中间PDF,只缺少signatrue字节:

public void emptySignature(String src, String dest, String fieldname, Certificate[] chain) throws IOException, DocumentException, GeneralSecurityException {
    PdfReader reader = new PdfReader(src);
    FileOutputStream os = new FileOutputStream(dest);
    PdfStamper stamper = PdfStamper.createSignature(reader, os, '\0');
    PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
    appearance.setVisibleSignature(new Rectangle(36, 748, 144, 780), 1, fieldname);
    appearance.setCertificate(chain[0]);
    ExternalSignatureContainer external = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
    MakeSignature.signExternalContainer(appearance, external, 8192);
}
只有在第二步中,才会注入实际签名:

public void createSignature(String src, String dest, String fieldname, PrivateKey pk, Certificate[] chain) throws IOException, DocumentException, GeneralSecurityException {
    
    PdfReader reader = new PdfReader(src);
    FileOutputStream os = new FileOutputStream(dest);
    ExternalSignatureContainer external = new MyExternalSignatureContainer(pk, chain);
    MakeSignature.signDeferred(reader, fieldname, os, external);
}
使用

这里的has是在第二步计算的,但是您不需要这样做,您可以

  • 在第一步中,已通过不使用原始的
    ExternalBlankSignatureContainer
    来计算它,如上面的
    emptySignature()
    中所述,而是扩展它来计算散列,如
    MyExternalSignatureContainer
    所做
  • 将此值存储在数据库中,以及
  • (检索签名后)使用MyExternalSignatureContainer的变体注入该签名,该变体不计算哈希,而是精确注入返回的签名
示例文件 您在对问题的评论中共享了由代码签名的

对该文件的分析(使用test
testpriyankasignaturesampleinformedapprovement\u Signed
执行)表明,实际签名的哈希是

1134ED96F42AC7352E546BE0E906C0BF5D4429AEAFAC39145DB40A0BB7E817B
它应该是经过身份验证的属性的散列,但它们的散列是

E7101D9770ABF5E58D43670AAC6E9418265AE80F74B3BBDFB67911C50CC5D949。
有符号字节范围散列为

D7917CF3BA9FE5D5B626ED825965D699F7C54DBF9F18DECE18EF8DD36DC4C28,
所以这也不是散列。因此,不清楚该签名散列来自何处

最终证明

web服务上的编码实用程序不同,因此解码的哈希值
public void createSignature(String src, String dest, String fieldname, PrivateKey pk, Certificate[] chain) throws IOException, DocumentException, GeneralSecurityException {
    
    PdfReader reader = new PdfReader(src);
    FileOutputStream os = new FileOutputStream(dest);
    ExternalSignatureContainer external = new MyExternalSignatureContainer(pk, chain);
    MakeSignature.signDeferred(reader, fieldname, os, external);
}
class MyExternalSignatureContainer implements ExternalSignatureContainer {

    protected PrivateKey pk;
    protected Certificate[] chain;
    
    public MyExternalSignatureContainer(PrivateKey pk, Certificate[] chain) {
        this.pk = pk;
        this.chain = chain;
    }
    
    public byte[] sign(InputStream is) throws GeneralSecurityException {
        try {
            PrivateKeySignature signature = new PrivateKeySignature(pk, "SHA256", "BC");
            String hashAlgorithm = signature.getHashAlgorithm();
            BouncyCastleDigest digest = new BouncyCastleDigest();
            PdfPKCS7 sgn = new PdfPKCS7(null, chain, hashAlgorithm, null, digest, false);
            byte hash[] = DigestAlgorithms.digest(is, digest.getMessageDigest(hashAlgorithm));
            Calendar cal = Calendar.getInstance();
            byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, cal, null, null, CryptoStandard.CMS);
            byte[] extSignature = signature.sign(sh);
            sgn.setExternalDigest(extSignature, null, signature.getEncryptionAlgorithm());
            return sgn.getEncodedPKCS7(hash, cal, null, null, null, CryptoStandard.CMS);
        }
        catch (IOException ioe) {
            throw new ExceptionConverter(ioe);
        }
    }

    public void modifySigningDictionary(PdfDictionary signDic) {
    }
    
}