【问题标题】:java iText7 deferred with sign prefix producing invalid signed pdfjava iText7 延迟,带有符号前缀,产生无效的签名 pdf
【发布时间】:2021-06-08 15:05:24
【问题描述】:

使用 iText7,我正在尝试签署 PDF 以获取外部实体的签名。 该过程必须按如下方式实现

  • 使用 Web 服务并获取证书,
  • 获取要签名的 PDF 哈希。在发送到外部实体进行签名之前,哈希必须有前缀。
  • 为此我创建了一个临时 PDF。
  • 在信息交换中使用 Web 服务。
  • 发送哈希;
  • 通过短信获得确认
  • 获取签名哈希。
  • 我使用已签名的哈希对最终 PDF 进行签名。

问题,我收到the document has been altered or corrupted after applying the signature 的错误。
项目实施是。

private final String _pdfToBeSigned = "C:/tmp/ama/PDF1.pdf";
private final String _temporaryPdf = "C:/tmp/ama/PDF1_empty.pdf";
private final String _finalPdf = "C:/tmp/ama/PDF1_assinado.pdf";

private final String _signatureFieldname = "sign1";

private static X509Certificate[] _chain = null;
private static Collection<byte[]> _crlBytesList = null;
private static Collection<byte[]> _ocspBytesList = null;

public static void main(String[] a) throws GeneralSecurityException, IOException {
    //Certificates (3) of the service are obtained. 
    _chain = getCertificates();
    
    TSAClientBouncyCastle tsaClient = new TSAClientBouncyCastle("https://freetsa.org/tsr");
    CrlClientOnline crlClients = new CrlClientOnline(_chain);
    OcspClientBouncyCastle ocspClient = new OcspClientBouncyCastle(null);
    
    _crlBytesList = getCrlByteList(crlClients);
    _ocspBytesList = getOcspBytesList(ocspClient);
    
    TestSign test = new TesteSign();        
    
    List<byte[]> hashs = test.getPreSignPDF(tsaClient, ocspClient);
    
    //Note: send hash to sign and wait for return of SMS with code
    //      if SMS code ok, the signed hash is obtained. 
    byte[] hashSignedForAma = getHashSignedByAma(hashs.get(0));
    
    //Finally sign the PDF 
    test.setFinalSignPDF(hashSignedForAma, hashs.get(1), tsaClient);
}

private static Collection<byte[]> getCrlByteList(CrlClientOnline crlClients){
     if(crlClients == null) return null;
     Collection<byte[]> coll = null;
     for(int i=0;i<_chain.length;i++) {
         Collection<byte[]> tmp = crlClients.getEncoded(_chain[i], null);
         if(null != tmp ) {
             coll = new ArrayList<byte[]>();
             coll.addAll(tmp);
         }
     }
     return coll;
}
      
private static Collection<byte[]> getOcspBytesList(OcspClientBouncyCastle ocspClient) {
   if(_chain.length <= 1 ||
      ocspClient == null) {
       return null;
   }
   Collection<byte[]> list = new ArrayList<byte[]>();
   for(var i = 0; i < _chain.length - 1; i++) {
       byte[] encoded = ocspClient.getEncoded(_chain[i], _chain[i + 1], null);
       if(encoded != null) {
           list.add(encoded);
       }
   }
   return list;

}

获取要签名的 PDF 哈希。在发送给外部实体进行签名之前,哈希必须有一个前缀,并为此创建一个临时 PDF。

public List<byte[]> getPreSignPDF(TSAClientBouncyCastle tsaClient_, OcspClientBouncyCastle ocspClient_) throws GeneralSecurityException, IOException {
    try (OutputStream ops = new FileOutputStream(_temporaryPdf);){
        PdfReader reader = new PdfReader(_pdfToBeSigned);
        StampingProperties prop = new StampingProperties();

        PdfSigner pdfSigner = new PdfSigner(reader, ops, prop);

        pdfSigner.setFieldName(_signatureFieldname);

        PdfSignatureAppearance  appearance = pdfSigner.getSignatureAppearance();

        appearance.setPageRect(new Rectangle(10,750,150,50))
                  .setPageNumber(1)
                  .setLayer2FontSize(6f)
                  .setReason("reason")
                  .setLocation("location")                    
                  .setCertificate(_chain[0]);

        Prepare4AmaSigningContainer container = new Prepare4AmaSigningContainer();
        
        //calculate estimed size
        int estimatedSize = 8192 + //initial base container size
                ( ocspClient_ != null ? 4192 : 0 ) +
                ( tsaClient_ != null  ? 4600 : 0 );
        if(_crlBytesList != null) {
            for (byte[] bs : _crlBytesList) {
                estimatedSize+= bs.length + 10;
            }
        }
        pdfSigner.signExternalContainer(container, estimatedSize); 

        byte[] HashForSigning = container.getHashToBeSignedByAma();
        byte[] NakeHash = container.getHashToBeSignedByAma();
        
        List<byte[]> array = new ArrayList<byte[]>();
        array.add(HashForSigning);  //idx 0
        array.add(NakeHash);        //idx 1
        
        return array;           
    }
}

//***** CLASS ****
class Prepare4AmaSigningContainer extends ExternalBlankSignatureContainer{

    private final byte[] _sha256SigPrefix = {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, (byte)0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20};
    private byte[] hashToBeSignedByAma;
    private byte[] nakeHash = null; 
    
    public Prepare4AmaSigningContainer(){
        super(PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached);
    }

    public byte[] getHashToBeSignedByAma() {
        return hashToBeSignedByAma;
    }
    
    public byte[] getNakeHash() {
        return nakeHash;
    }
    
    @Override
    public byte[] sign(InputStream data){
        try {
            // create pdf pkcs7 for signing the document
            BouncyCastleDigest digest = new BouncyCastleDigest();
            PdfPKCS7 sgn = new PdfPKCS7(null, _chain, DigestAlgorithms.SHA256, null, digest, false);
            
            // get hash for document bytes
            nakeHash = DigestAlgorithms.digest(data, digest.getMessageDigest(DigestAlgorithms.SHA256));

            // get attributes
            byte[] docBytes = sgn.getAuthenticatedAttributeBytes(nakeHash,  PdfSigner.CryptoStandard.CMS, _crlBytesList, _crlBytesList);
            // hash it again
            try(InputStream myInputStream = new ByteArrayInputStream(docBytes);){ 
                byte[] docBytesHash = DigestAlgorithms.digest(myInputStream, digest.getMessageDigest("SHA256"));

                //prepend sha256 prefix to hash for send signed
                hashToBeSignedByAma = new byte[_sha256SigPrefix.length + docBytesHash.length];
                System.arraycopy(_sha256SigPrefix, 0, hashToBeSignedByAma, 0, _sha256SigPrefix.length );
                System.arraycopy(docBytesHash, 0, hashToBeSignedByAma, _sha256SigPrefix.length, docBytesHash.length );
    
                return new byte[0]; // empty array
            } 
        }catch (IOException | GeneralSecurityException ioe) {
            throw new RuntimeException(ioe);
        }
    }
}

使用签名的哈希我签署临时 PDF

public void setFinalSignPDF(byte[] HashSignedForAma_, byte[] nakedHashFromIntermediaryPdf_, TSAClientBouncyCastle tsaClient_) throws IOException, GeneralSecurityException{     

    try (OutputStream writer = new FileOutputStream(_finalPdf);) {
        PdfReader pdfReader = new PdfReader(_temporaryPdf);
        PdfDocument document = new PdfDocument(pdfReader);

        InjectAmaSignatureContainer finalContainer = new InjectAmaSignatureContainer(
                                                                HashSignedForAma_,
                                                                nakedHashFromIntermediaryPdf_,
                                                                tsaClient_);
        PdfSigner.signDeferred(document, _signatureFieldname, writer, finalContainer);
    }
}

//***** CLASS ****
class InjectAmaSignatureContainer implements IExternalSignatureContainer {

    private byte[] documentHash;
    private byte[] signature;
    private TSAClientBouncyCastle tsaClient;
    private byte[] dados = null;

    public InjectAmaSignatureContainer(byte[] signature_, byte[] documentHash_, TSAClientBouncyCastle tsaClient_) {
        signature = signature_;
        documentHash = documentHash_;
        tsaClient = tsaClient_;
    }
    
    public byte[] getDados() {
        return dados;
    }

    @Override
    public byte[] sign(InputStream is) throws GeneralSecurityException {
        BouncyCastleDigest digest = new BouncyCastleDigest();
        
        PdfPKCS7 sgn = new PdfPKCS7(null, 
                                    _chain, 
                                    DigestAlgorithms.SHA256, 
                                    null, 
                                    digest, 
                                    false);

        sgn.setExternalDigest(signature, null, "RSA");

        byte[] encodedSig = sgn.getEncodedPKCS7(documentHash, PdfSigner.CryptoStandard.CMS, tsaClient, _ocspBytesList, _crlBytesList);

        return encodedSig;
    }

    @Override
    public void modifySigningDictionary(PdfDictionary signDic) {
    }
}

谢谢

编辑:
方法中的错误修复:Prepare4AmaSigningContainer

byte[] docBytes = sgn.getAuthenticatedAttributeBytes(nakeHash, PdfSigner.CryptoStandard.CMS, _ocspBytesList, _crlBytesList);

【问题讨论】:

  • 请分享一个由您的代码签名的示例 pdf 以供分析。
  • 嗨,谢谢。 link 用于 PDF 签名。
  • 好的,Prepare4AmaSigningContainer 中有一个错误,您的sgn.getAuthenticatedAttributeBytes 调用将_crlBytesList 用于两个参数;我认为其中之一应该是_ocspBytesList。但我认为还有其他问题。
  • 是的,我的错是对的。我已经修复了它,但它仍然有同样的问题。我尝试了几种方法(您建议了一些方法),但它们从未给我正确的签名。如果有更好的解决问题的方法...我会实施的,谢谢
  • “如果有更好的方法来解决这个问题......我会实施它,谢谢” - 也许。在main 中,您按顺序执行最后三个指令(test.getPreSignPDFgetHashSignedByAmatest.setFinalSignPDF)。如果这相当于您的最终设计,我们可以轻松地将其设为一步签名,并摆脱延迟签名的步骤。

标签: java pdf itext sign


【解决方案1】:

这是我对这个问题的解决方案。 感谢MKL 的提示...

private static final String input = "c:/tmp/ama/PDF-1.pdf";
private static final String tmp = "c:/tmp/ama/tmpPDF.pdf";
private static final String output =  "c:/tmp/ama/signed.pdf";

public static void main(String[] args) throws IOException, GeneralSecurityException {
    BouncyCastleProvider providerBC = new BouncyCastleProvider();
    Security.addProvider(providerBC);

    //get (via web service) all certificate (3 at all)
    Certificate[] chain = new CallAMA().getCertificates();

    StackOverflow app = new StackOverflow();
    
    //read the HASH to be signed (via web service) and create a tmp pdf. 
    byte[] hash4Sign = app.emptySignature(input, tmp, "sig", chain);
    
    //Concatenate the sha256SigPrefix to the hash and send it to be signed (via web service)
    //wait for the code received via SMS
    //(accepts the code via the keyboard and sends it via webservice)
    //finally receive the signed hash 
    byte[] signedHash = new CallAMA().getHashSignedByAma(hash4Sign);
    
    //insert hash in tmp pdf, and create a new pdf signed
    app.createSignature(signedHash, tmp, output, "sig", chain);
}

public byte[] emptySignature(String src, String dest, String fieldname, Certificate[] chain) throws IOException, GeneralSecurityException {
    PdfReader reader = new PdfReader(src);
    PdfSigner signer = new PdfSigner(reader, new FileOutputStream(dest), new StampingProperties().useAppendMode());
    PdfSignatureAppearance appearance = signer.getSignatureAppearance();

    appearance
            .setLayer2FontSize(6)
            .setPageRect(new Rectangle(36, 748, 250, 50))
            .setPageNumber(1)
            .setCertificate(chain[0])
            .setSignatureCreator("ORIGEM");
    signer.setFieldName(fieldname);
        
    /* ExternalBlankSignatureContainer constructor will create the PdfDictionary for the signature
     * information and will insert the /Filter and /SubFilter values into this dictionary.
     * It will leave just a blank placeholder for the signature that is to be inserted later.
     */
    MyExternalBlankSignatureContainer external = new MyExternalBlankSignatureContainer(chain, PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached);

   
    // Sign the document using an external container.
    // 8192 is the size of the empty signature placeholder.
    signer.signExternalContainer(external, 8192);
    byte[] hash4Sign = external.getHash4Sign();
    return hash4Sign;
}

public void createSignature(byte[] hashSigned, String src, String dest, String fieldName, Certificate[] chain)
        throws IOException, GeneralSecurityException {
    PdfReader reader = new PdfReader(src);
    try(FileOutputStream os = new FileOutputStream(dest)) {
        PdfSigner signer = new PdfSigner(reader, os, new StampingProperties());

        IExternalSignatureContainer external = new MyExternalSignatureContainer(hashSigned, chain, PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached);

        // Signs a PDF where space was already reserved. The field must cover the whole document.
        signer.signDeferred(signer.getDocument(), fieldName, os, external);
    }
}

class MyExternalSignatureContainer implements IExternalSignatureContainer {

    /* Signature dictionary. Filter and SubFilter.  */
    private PdfDictionary sigDic;
    private byte[] signedHash =null;
    private Certificate[] chain = null;

    public MyExternalSignatureContainer(byte[] _signedHash, Certificate[] _chain, PdfName filter, PdfName subFilter) {
        sigDic = new PdfDictionary();
        sigDic.put(PdfName.Filter, filter);
        sigDic.put(PdfName.SubFilter, subFilter);
        signedHash = _signedHash;
        chain = _chain;
    }

    @Override
    public byte[] sign(InputStream data) throws GeneralSecurityException {
        try {
            String hashAlgorithm = DigestAlgorithms.SHA256;//"SHA-256";
            BouncyCastleDigest digest = new BouncyCastleDigest();
            MessageDigest md = digest.getMessageDigest(hashAlgorithm);

            byte[]   hash = DigestAlgorithms.digest(data, md);
            PdfPKCS7 sgn  = new PdfPKCS7(null, chain, hashAlgorithm, null, digest, false);

            //Collection<byte[]> ocsp = new OcspClientBouncyCastle(null);
            OcspClientBouncyCastle ocspClient = new OcspClientBouncyCastle(null);
            Collection<byte[]> ocsp = new ArrayList<byte[]>();
            for(var i = 0; i < chain.length - 1; i++) {
                byte[] encoded = ocspClient.getEncoded((X509Certificate)chain[i], (X509Certificate)chain[i + 1], null);
                if(encoded != null) ocsp.add(encoded);
            }

            sgn.setExternalDigest(signedHash, null, "RSA");

            ITSAClient tsaClient = null;//new GSTSAClient(access);
            return sgn.getEncodedPKCS7(hash, PdfSigner.CryptoStandard.CADES, tsaClient, ocsp, null);
        } catch (IOException | GeneralSecurityException de) {
            de.printStackTrace();
            throw new GeneralSecurityException(de);
        }
    }

    @Override
    public void modifySigningDictionary(PdfDictionary signDic) {
        signDic.putAll(sigDic);
    }
}


class MyExternalBlankSignatureContainer implements IExternalSignatureContainer {

    /* Signature dictionary. Filter and SubFilter.  */
    private PdfDictionary sigDic;
    private byte[] hash4Sign = null;
    private Certificate[] chain = null;

    public MyExternalBlankSignatureContainer(Certificate[] _chain, PdfName filter, PdfName subFilter) {
        sigDic = new PdfDictionary();
        sigDic.put(PdfName.Filter, filter);
        sigDic.put(PdfName.SubFilter, subFilter);
        chain = _chain;
    }
    
    public byte[] getHash4Sign() {
        return hash4Sign;
    }

    @Override
    public byte[] sign(InputStream data) throws GeneralSecurityException {
        try {
            String hashAlgorithm = DigestAlgorithms.SHA256;//"SHA-256";
            BouncyCastleDigest digest = new BouncyCastleDigest();
            MessageDigest md = digest.getMessageDigest(hashAlgorithm);

            byte[]   hash = DigestAlgorithms.digest(data, md);
            PdfPKCS7 sgn  = new PdfPKCS7(null, chain, hashAlgorithm, null, digest, false);

            OcspClientBouncyCastle ocspClient = new OcspClientBouncyCastle(null);
            Collection<byte[]> ocsp = new ArrayList<byte[]>();
            for(var i = 0; i < chain.length - 1; i++) {
                byte[] encoded = ocspClient.getEncoded((X509Certificate)chain[i], (X509Certificate)chain[i + 1], null);
                if(encoded != null) ocsp.add(encoded);
            }

            byte[] attributeBytes = sgn.getAuthenticatedAttributeBytes(hash, PdfSigner.CryptoStandard.CADES, ocsp, null);

             //create sha256 message digest
            hash4Sign = MessageDigest.getInstance(hashAlgorithm).digest(attributeBytes);

            return new byte[0];
        } catch (IOException | GeneralSecurityException de) {
            de.printStackTrace();
            throw new GeneralSecurityException(de);
        }
    }

    @Override
    public void modifySigningDictionary(PdfDictionary signDic) {
        signDic.putAll(sigDic);
    }
}

【讨论】:

    【解决方案2】:

    经过多次尝试和资料分析,找到了这个链接sample iText code

    ***** 重要 *****
    该示例根本没有解决问题,因为函数应该在 GSSignatureContainer 类的 sign 方法中“停止”。 这种方法解决了问题:the document has been altered or corrupted after applying the signature

    PDF signed

    private static final String input = "c:/tmp/ama/PDF-1.pdf";
    private static final String output =  "c:/tmp/ama/signed_signed.pdf";
    
    
    public static void main(String[] args) throws IOException, GeneralSecurityException {
        Security.addProvider(new BouncyCastleProvider());
        PdfReader          reader             = new PdfReader(input);
        OutputStream       fos                = new FileOutputStream(output);
        StampingProperties stampingProperties = new StampingProperties();
        //For any signature in the Pdf  but the first one, you need to use appendMode
        //        stampingProperties.useAppendMode();
        PdfSigner pdfSigner = new PdfSigner(reader, fos, stampingProperties);
    
         /*you can modify the signature appearance */
        PdfSignatureAppearance appearance = pdfSigner.getSignatureAppearance();
        appearance.setPageRect(new Rectangle(36, 508, 254, 200));
        appearance.setPageNumber(1);
        appearance.setLayer2FontSize(10f);
        appearance.setReason("Teste Assinatura");
        appearance.setLocation("Lisboa");
    
        IExternalSignatureContainer gsContainer = new GSSignatureContainer(PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached);
        pdfSigner.signExternalContainer(gsContainer, 8049);
    }
    
    public class GSSignatureContainer implements IExternalSignatureContainer {
    
    /* Signature dictionary. Filter and SubFilter.  */
    private PdfDictionary sigDic;
    
    public GSSignatureContainer( PdfName filter, PdfName subFilter) {
        sigDic = new PdfDictionary();
        sigDic.put(PdfName.Filter, filter);
        sigDic.put(PdfName.SubFilter, subFilter);
    }
    
    @Override
    public byte[] sign(InputStream data) throws GeneralSecurityException {
        try {
            //get all certificates (3) from client via web service
            Certificate[] chain = new CallAMA().getCertificates();
    
            String hashAlgorithm = DigestAlgorithms.SHA256;//"SHA-256";
            BouncyCastleDigest digest = new BouncyCastleDigest();
            MessageDigest md = digest.getMessageDigest(hashAlgorithm);
    
            byte[]   hash = DigestAlgorithms.digest(data, md);
            PdfPKCS7 sgn  = new PdfPKCS7(null, chain, hashAlgorithm, null, digest, false);
    
            Collection<byte[]> ocsp = null;
    
            byte[] attributeBytes = sgn.getAuthenticatedAttributeBytes(hash, PdfSigner.CryptoStandard.CADES, ocsp, null);
    
             //criar sha256 message digest
            byte[] attributeBytesDigest = MessageDigest.getInstance(hashAlgorithm).digest(attributeBytes);
    
            /****************************************************
             * CALL client (AMA) -> receive SMS and signed hash *
             ****************************************************/
            byte[] signedHash = new CallAMA().getHashSignedByAma(attributeBytesDigest);
            sgn.setExternalDigest(signedHash, null, "RSA");
    
            ITSAClient tsaClient = null;//new GSTSAClient(access);
            return sgn.getEncodedPKCS7(hash, PdfSigner.CryptoStandard.CADES, tsaClient, ocsp, null);
        } catch (IOException | GeneralSecurityException de) {
            de.printStackTrace();
            throw new GeneralSecurityException(de);
        }
    }
    
    @Override
    public void modifySigningDictionary(PdfDictionary signDic) {
        signDic.putAll(sigDic);
    }
    

    }

    【讨论】:

    • “这个例子根本没有解决问题” - 那你为什么要把它作为答案发布。 "PDF signed" - 该链接要求下载方已获得您的访问权限;它不能按原样下载。
    • 您好已经更正了链接(谢谢)。答案并没有解决我的问题,但是它解决了问题应用签名后文档已被更改或损坏(答案可能对某人有所帮助)
    • 在这种情况下,是什么阻止您将 GSSignatureContainer.sign 方法的功能拆分为两种模式,在初始模式下,代码仅执行到您想要停止的点和一个空的返回数组。然后你等到可以继续,切换容器的模式,并将其用于延迟签名。在这种其他模式下,只有 分割点之后的代码会被执行。
    • 你好 mkl,我已经尝试过这个选项(它并不顺利)。我必须在 servlet 中实现此操作,这迫使我将变量保存在会话中,如果 pdf 太大,这会消耗大量内存。我正在尝试通过临时 pdf 实现该功能,但我遇到了哈希问题。我的方法是获取 pdf 的哈希,包括一个空签名并将其保存为临时的。我签署了哈希,再次阅读了临时 pdf 并使用 GSSignatureContainer.sign 方法(使用签名的哈希)我只需要包含它,但我又遇到了问题。任何想法或示例代码?谢谢。
    • 在此答案的代码中,您调用 new CallAMA().getHashSignedByAma 以获得从待签名属性计算的裸散列。在您问题的代码中,您调用getHashSignedByAma 来获取根据待签名属性_sha256SigPrefix 开头的裸散列。正如您所说,此答案中的代码生成的 PDF 验证正常,看起来 getHashSignedByAma 确实需要普通的裸散列,而不是带有一些前缀。所以你应该再次尝试你的两阶段代码,去掉前面的前缀。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-24
    • 1970-01-01
    • 1970-01-01
    • 2017-11-26
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多