简体   繁体   English

PDFBox 2.0.15 的外部签名

[英]External Signature with PDFBox 2.0.15

I'm implementing an application to sign PDF files in the server, with the follow scenario (to make long history, short):我正在实现一个应用程序来签署服务器中的 PDF 文件,使用以下场景(制作长历史,短历史):

  1. Client start signature sending to server, date/time and watermark客户端开始向服务器发送签名、日期/时间和水印
  2. Server add signature dictionaries into file and send data to be signed服务器将签名字典添加到文件中并发送要签名的数据
  3. Client sign content客户签收内容
  4. Server finish the signature服务器完成签名

I'm using PDFBox 2.0.15, and making use of new feature saveIncrementalForExternalSigning as shown in the code below:我正在使用 PDFBox 2.0.15,并使用新功能saveIncrementalForExternalSigning ,如下面的代码所示:

try {
        String name = document.getID();
        File signedFile = new File(workingDir.getAbsolutePath() + sep + name + "_Signed.pdf");
        this.log("[SIGNATURE] Creating signed version of the document");
        if (signedFile.exists()) {
            signedFile.delete();
        }
        FileOutputStream tbsFos = new FileOutputStream(signedFile);
        ExternalSigningSupport externalSigning = pdfdoc.saveIncrementalForExternalSigning(tbsFos);

        byte[] content = readExternalSignatureContent(externalSigning);
        if (postparams.get("action").equalsIgnoreCase("calc_hash")) {
            this.log("[SIGNATURE] Calculating hash of the document");
            String strBase64 = ParametersHandle.compressParamBase64(content);

            // this saves the file with a 0 signature
            externalSigning.setSignature(new byte[0]);

            // remember the offset (add 1 because of "<")
            int offset = signature.getByteRange()[1] + 1;

            this.log("[SIGNATURE] Sending calculated hash to APP");
            return new String[] { strBase64, processID, String.valueOf(offset) };
        } else {
            this.log("[SIGNATURE] Signature received from APP");
            String signature64 = postparams.get("sign_disgest");
            byte[] cmsSignature = ParametersHandle.decompressParamFromBase64(signature64);

            this.log("[SIGNATURE] Setting signature to document");
            externalSigning.setSignature(cmsSignature);

            pdfdoc.close();

            IOUtils.closeQuietly(signatureOptions);

            this.log("[DOXIS] Creating new version of document on Doxis");
            createNewVersionOfDocument(doxisServer, documentServer, doxisSession, document, signedFile);

            return new String[] { "SIGNOK" };
        }
    } catch (IOException ex) {
        this.log("[SAVE FOR SIGN] " + ex);
        return null;
    }

In the "IF" statement I'm generating data to be signed.在“IF”语句中,我正在生成要签名的数据。 In the "ELSE" statement adding the signature, that comes via post request (that is what ParametersHandle.decompressParamFromBase64 does), into document.在“ELSE”语句中,通过发布请求(这是ParametersHandle.decompressParamFromBase64所做的)将签名添加到文档中。 So I have two post requests for this method in this try.所以在这次尝试中,我对这个方法有两个 post 请求。

A second approach was doing each post request in one method, so I have this second code block:第二种方法是用一种方法处理每个 post 请求,所以我有第二个代码块:

// remember the offset (add 1 because of "<") 
        int offset = Integer.valueOf(postparams.get("offset"));
        this.log("[PDF BOX] Retrieving offset of bytes range for this signature. The value is: "
                + String.valueOf(offset));

        File signedPDF = new File(workingDir.getAbsolutePath() + sep + name + "_Signed.pdf");
        this.log("[SIGNATURE] Reloading document for apply signature: " + signedPDF.getAbsolutePath());

        // invoke external signature service
        String signature64 = postparams.get("sign_disgest");
        byte[] cmsSignature = ParametersHandle.decompressParamFromBase64(signature64);

        this.log("[SIGNATURE] Got signature byte array from APP.");
        // set signature bytes received from the service

        // now write the signature at the correct offset without any PDFBox methods
        this.log("[SIGNATURE] Writing signed document...");
        RandomAccessFile raf = new RandomAccessFile(signedPDF, "rw");
        raf.seek(offset);
        raf.write(Hex.getBytes(cmsSignature));
        raf.close();
        this.log("[SIGNATURE] New signed document has been saved!");

The problem is: I'm getting the error "The document has been altered or corrupted since the Signature was applied" when validating it on Adobe Reader.问题是:我在 Adob​​e Reader 上验证时收到错误“自从应用签名以来文档已被更改或损坏”。 On my understanding it should not happen since the offset of the signature byte range is being remembered on the second post call.根据我的理解,它不应该发生,因为在第二次调用后会记住签名字节范围的偏移量。

Any help or idea is appreciated,任何帮助或想法表示赞赏,

Thank you in advance.先感谢您。

[EDIT] [编辑]

For a complete list of used files: https://drive.google.com/drive/folders/1S9a88lCGaQYujlEyCrhyzqvmWB-68LR3有关已用文件的完整列表: https : //drive.google.com/drive/folders/1S9a88lCGaQYujlEyCrhyzqvmWB-68LR3

[EDIT 2] [编辑 2]

Based on @mkl comment, here is the method where the signature is made:根据@mkl 评论,这里是进行签名的方法:

public byte[] sign(byte[] hash)
        throws IOException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
    PrivateKey privKey = (PrivateKey) windowsCertRep.getPrivateKey(this.selected_alias, "");
    X509Certificate[] certificateChain = windowsCertRep.getCertificateChain(this.selected_alias);

    try
    {
        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
        X509Certificate cert = (X509Certificate) certificateChain[0];
        ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privKey);
        gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().build()).build(sha1Signer, cert));
        gen.addCertificates(new JcaCertStore(Arrays.asList(certificateChain)));
        CMSProcessableInputStream msg = new CMSProcessableInputStream(new ByteArrayInputStream(hash));
        CMSSignedData signedData = gen.generate(msg, false);
        return signedData.getEncoded();
    }
    catch (GeneralSecurityException e)
    {
        throw new IOException(e);
    }
    catch (CMSException e)
    {
        throw new IOException(e);
    }
    catch (OperatorCreationException e)
    {
        throw new IOException(e);
    }

}

I've tested the CreateVisibleSignature2 examaple, replacing the sign method for one calling this service that returns me the signature, e it works.我已经测试了CreateVisibleSignature2示例,替换了调用此服务的sign方法,该服务返回签名,e 它有效。

Thanks to Tilman Hausherr I could figure out what was going on:感谢Tilman Hausherr,我可以弄清楚发生了什么:

1 - I have a Desktop APP that communicates with SmatCards and so on, and it is the signer. 1 - 我有一个与智能卡等通信的桌面应用程序,它是签名者。 To communicate with the server (through a webpage) we use WebSocket.为了与服务器通信(通过网页),我们使用 WebSocket。 I've written my own websocket server class, and that is why it's only prepared to work with 65k bytes.我已经编写了自己的 websocket 服务器类,这就是为什么它只准备使用 65k 字节。 Than when I tried to send the data here:比当我尝试在这里发送数据时:

ExternalSigningSupport externalSigning = doc.saveIncrementalForExternalSigning(fos);           
byte[] cmsSignature = sign(externalSigning.getContent());                           

I got errors in the APP.我在 APP 中遇到错误。

2 - Tilman, suggested me to take a look on this @mkl answer where he does the same thing: create a SHA256 hash of the externalSigning.getContent() and send to be signed in another place. 2 - Tilman,建议我看一下这个@mkl 的回答,他在那里做同样的事情:创建externalSigning.getContent()的 SHA256 哈希并发送到另一个地方进行签名。 I don't know why, but the only thing that didn't work for me was:我不知道为什么,但唯一对我不起作用的是:

gen.addSignerInfoGenerator(builder.build(
                new BcRSAContentSignerBuilder(sha256withRSA,
                        new DefaultDigestAlgorithmIdentifierFinder().find(sha256withRSA))
                                .build(PrivateKeyFactory.createKey(pk.getEncoded())),
                new JcaX509CertificateHolder(cert)));

So, I've replaced this block to:因此,我已将此块替换为:

ContentSigner sha256Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privKey);

Than, my complete signature method is like:比,我完整的签名方法是这样的:

        PrivateKey privKey = (PrivateKey) windowsCertRep.getPrivateKey(this.selected_alias, "changeit");
    X509Certificate[] certificateChain = windowsCertRep.getCertificateChain(this.selected_alias);

        List<X509Certificate> certList = Arrays.asList(certificateChain);
        JcaCertStore certs = new JcaCertStore(certList);

        CMSSignedDataGenerator gen = new CMSSignedDataGenerator();

        Attribute attr = new Attribute(CMSAttributes.messageDigest,
                new DERSet(new DEROctetString(hash)));

        ASN1EncodableVector v = new ASN1EncodableVector();
        v.add(attr);

        SignerInfoGeneratorBuilder builder = new SignerInfoGeneratorBuilder(new BcDigestCalculatorProvider())
                .setSignedAttributeGenerator(new DefaultSignedAttributeTableGenerator(new AttributeTable(v)));

        AlgorithmIdentifier sha256withRSA = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withRSA");

        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
        InputStream in = new ByteArrayInputStream(certificateChain[0].getEncoded());
        X509Certificate cert = (X509Certificate) certFactory.generateCertificate(in);

        ContentSigner sha256Signer = new JcaContentSignerBuilder("SHA256WithRSA").build(privKey);
        gen.addSignerInfoGenerator(builder.build(sha256Signer, new JcaX509CertificateHolder(cert)));

        gen.addCertificates(certs);

        CMSSignedData s = gen.generate(new CMSAbsentContent(), false);
        return s.getEncoded();

So, thank you community once more!!!所以,再次感谢社区!!!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM