PDFBox 2.0.15的外部签名

时间:2019-07-03 09:55:28

标签: java pdf digital-signature pdfbox

我正在实现一个应用程序,以在服务器上签署PDF文件,并具有以下情况(简短地说,历史悠久):

  1. 客户端开始向服务器发送签名,日期/时间和水印
  2. 服务器将签名字典添加到文件中并发送要签名的数据
  3. 客户签名内容
  4. 服务器完成签名

我正在使用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;
    }

在“ IF”语句中,我正在生成要签名的数据。在“ ELSE”语句中,将通过邮寄请求(即ParametersHandle.decompressParamFromBase64所做的签名)添加到文档中。因此,在此尝试中,我对此方法有两个发布请求。

第二种方法是在一种方法中执行每个发布请求,因此我有第二个代码块:

// 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!");

问题是:在Adobe Reader上进行验证时,出现错误“自应用签名以来,文档已被更改或损坏”。 根据我的理解,应该不会发生这种情况,因为在第二个post调用中会记住签名字节范围的偏移量。

感谢任何帮助或想法,

谢谢。

[编辑]

有关已使用文件的完整列表:https://drive.google.com/drive/folders/1S9a88lCGaQYujlEyCrhyzqvmWB-68LR3

[编辑2]

基于@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);
    }

}

我已经测试过CreateVisibleSignature2的例子,将sign方法替换为一个调用此服务的方法,该服务会向我返回签名,它可以正常工作。

1 个答案:

答案 0 :(得分:1)

感谢Tilman Hausherr,我能弄清楚发生了什么事:

1-我有一个与SmatCards进行通信的桌面APP,它是签名者。为了与服务器通信(通过网页),我们使用WebSocket。我已经编写了自己的websocket服务器类,这就是为什么它仅准备使用65k字节的原因。比我尝试在此处发送数据的时间要短:

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

我在APP中出现错误。

2-蒂尔曼(Tilman),建议我看一下这个@mkl answer,他做同样的事情:创建externalSigning.getContent()的SHA256哈希并发送到另一个地方进行签名。我不知道为什么,但是唯一对我不起作用的是:

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

因此,我已将此块替换为:

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

然后,我完整的签名方法如下:

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

所以,再次感谢社区!!!