使用Java和iText对PDF哈希签名

时间:2019-03-07 11:15:43

标签: 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(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;

这是我们的代码,首先,我们获得签名外观,并计算哈希值

至此,我们有了文档的哈希码。然后,我们将哈希发送到Web服务,并获得签名的哈希代码。

最后,我们将签名的哈希值放入PDF:

    sgn.setExternalDigest(Base64.decode(hashSignat), null, "RSA");
    byte[] encodedSign = sgn.getEncodedPKCS7(hash, cal, null, null, 
    null, CryptoStandard.CMS);
    byte[] paddedSig = new byte[8192];
    System.arraycopy(encodedSign, 0, paddedSig, 0, 
    encodedSign.length);

    PdfDictionary dic2 = new PdfDictionary();
    dic2.put(PdfName.CONTENTS, new PdfString(paddedSig).setHexWriting(true));

    appearance.close(dic2);

在这一点上,我们获得了PDF签名,但是签名无效。 Adobe说“自签名以来,文档已被更改或损坏”。

我经历了Sign PDF using an external service and iTextPDF signature itext pkcs7 multi signIs it possible to sign a PDF document with hash and signed hash?,但是没有运气。

1 个答案:

答案 0 :(得分:0)

示例文件

您在问题的注释中共享了一个由代码签名的example file

对该文件的分析(使用AnalyzeSignatures测试testPriyankaSignatureSampleinformedconsent_Signed执行)显示,实际签名的哈希为

1134ED96F42AC7352E546BE0E906C0BF5D44229AEAFAC39145DB40A0BB7E817B

应该是经过身份验证的属性的哈希,但是它们的哈希是

E7101D9770ABF5E58D43670AAC6E9418265AE80F74B3BDFB67911C0CC5D5D949.

带符号的字节范围哈希是

D7917CF3BA9FE5D5B626ED825965D699F7C54DBBF9F18DECE18EF8DD36DC4C28,

所以也不是这个哈希。因此,不清楚该签名哈希来自何处。

最终发现

  

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

签名先前已签名的文件

此后弹出一个新问题:

  

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

要签署已签署的pdf文件,您必须使用追加模式。为此,您需要一个不同的PdfStamper.createSignature重载和一个另外的参数boolean,您将其设置为true

原因是通常iText(即不激活附加模式)重新组织内部PDF结构并丢弃未使用的对象。在已经签名的PDF中,这通常会移动(已经存在的)签名的位置,这会使签名结构无效。不过,iText使用追加模式,将原始PDF字节保留为原来的字节,并且仅追加新数据。

更改哈希

  

我只是感到困惑,为什么每次都对同一文件的字节“ sh”的值进行更改?实际上,我想存储文件的哈希值。是因为“ cal”吗?

实际上,每次您开始处理PDF时,结果都会获得一个新的唯一ID。此外,存储修改时间。而且在签名用例的情况下,签名时间也不同。

  

我可以一次获取哈希并将其存储在db中,稍后再对哈希签名并附加为pdf吗?

您要么必须

  • 暂时保留压模和外观开放,否则您必须
  • 保留准备好的文件,否则您必须
  • 修补iText,以使您可以为签名时间,操作时间和ID设置固定值。
  

我无法使用第一个选项。我没有第二个第三选择。您能详细说明一下还是给我参考?

修补iText以对每个文件实施固定的哈希值

好吧,第三个选择是对iText进行修补,通常这是您不希望做的事情,因为这会使以后合并iText更新变得困难。

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

您可能不想切换到旧的iText分支,因为它缺少许多修复程序和较新iText版本的新选项。

保留已准备好的文件

因此,您可能应该保留原始创建的文件带有空签名,例如在某些临时文件夹中,并在获取最终签名后立即应用延迟签名

这本质上就是iText示例C4_09_DeferredSigning所要解决的问题,它首先创建一个包含所有内容的中间PDF,仅缺少签名字节:

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);
}

使用

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) {
    }

}

此处是在第二步中计算的,但您不必这样做,可以

  • 已经通过第一步而不是像上面的ExternalBlankSignatureContainer那样使用原始的emptySignature()来进行计算,而是像MyExternalSignatureContainer那样将其扩展为计算哈希,
  • 将此值存储在数据库中,并且
  • (一旦检索到签名,则使用MyExternalSignatureContainer的变体注入该签名,该变体不计算哈希值,而是精确注入返回的签名。