Java 带有PDFBox 2.0.15的外部签名

Java 带有PDFBox 2.0.15的外部签名,java,pdf,digital-signature,pdfbox,Java,Pdf,Digital Signature,Pdfbox,我正在实现一个应用程序,以便在服务器中对PDF文件进行签名,使用以下场景(以创建长历史,短历史): 客户端开始向服务器发送签名、日期/时间和水印 服务器将签名字典添加到文件中并发送要签名的数据 客户端签名内容 服务器完成签名 我正在使用PDFBox 2.0.15,并使用新功能saveIncrementalForExternalSigning,如下代码所示: try { String name = document.getID(); File signedFile

我正在实现一个应用程序,以便在服务器中对PDF文件进行签名,使用以下场景(以创建长历史,短历史):

  • 客户端开始向服务器发送签名、日期/时间和水印
  • 服务器将签名字典添加到文件中并发送要签名的数据
  • 客户端签名内容
  • 服务器完成签名
  • 我正在使用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;
        }
    
    问题是:在AdobeReader上验证文档时出现错误“自从应用签名以来,文档已被更改或损坏”。 据我所知,这不应该发生,因为签名字节范围的偏移量在第二次post调用中被记住

    感谢您的任何帮助或想法

    先谢谢你

    [编辑]

    有关已使用文件的完整列表:

    [编辑2]

    根据@mkl comment,以下是进行签名的方法:

    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
    examaple,将
    sign
    方法替换为调用此服务并返回签名的方法,即它可以正常工作。

    多亏了我能够弄清楚发生了什么:

    1-我有一个与SmatCards等通信的桌面应用程序,它是签名者。为了与服务器通信(通过网页),我们使用WebSocket。我已经编写了自己的websocket服务器类,这就是为什么它只准备使用65k字节。比我试图在这里发送数据时:

    ExternalSigningSupport externalSigning = doc.saveIncrementalForExternalSigning(fos);           
    byte[] cmsSignature = sign(externalSigning.getContent());                           
    
    我在应用程序中出错

    2-Tilman建议我看看这个,他做了同样的事情:创建
    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();
    
    PrivateKey privKey=(PrivateKey)windowsCertRep.getPrivateKey(此.selected_别名“changeit”);
    X509Certificate[]CertificateCain=windowsCertRep.GetCertificateCain(此.selected_别名);
    List certList=Arrays.asList(certificateChain);
    JcaCertStore certs=新的JcaCertStore(certList);
    CMSSignedDataGenerator gen=新的CMSSignedDataGenerator();
    Attribute attr=新属性(CMSAttributes.messageDigest,
    新的DERSet(新的DEROctetString(hash));
    ASN1EncodableVector v=新的ASN1EncodableVector();
    v、 添加(attr);
    SignerInfoGeneratorBuilder=new SignerInfoGeneratorBuilder(new BcDigestCalculatorProvider())
    .setSignedAttributeGenerator(新的DefaultSignedAttributeTableGenerator(新的AttributeTable(v)));
    算法标识符sha256withRSA=new DefaultSignatureAlgorithmIdentifierFinder().find(“sha256withRSA”);
    CertificateFactory=CertificateFactory.getInstance(“X.509”);
    InputStream in=new ByteArrayInputStream(certificateChain[0].getEncoded());
    X509Certificate cert=(X509Certificate)certFactory.generateCertificate(in);
    ContentSigner sha256Signer=新的JcaContentSignerBuilder(“SHA256WithRSA”).build(privKey);
    gen.addSignerInfoGenerator(builder.build(sha256Signer,新JcaX509CertificateHolder(cert));
    一般证书(证书);
    CMSSignedData s=gen.generate(新的CMSAbsentContent(),false);
    返回s.getEncoded();
    

    所以,再次感谢你们社区

    请添加日志输出和签名的PDF文件。所有这些一开始看起来有点混乱,但在我看了这个示例之后,似乎您确实阅读并应用了:-)请不仅共享最终签名的文件,还共享第一步的文件结果。此外,您是否测试了原始的PDFBox签名代码,仅用对您的服务的调用替换了签名容器的创建?这个签名容器可能有问题,而不是你上面共享的代码。更可疑的是:日志中的“哈希”长度是4998,但应该是55000左右。根据NOTEPAD++,PDF中签名的非零部分的长度为11962。除以2,大约是6000。但是日志的长度是5140。@Tilmanhausher谢谢你昨天的帮助,它帮助我过滤了这个问题,现在我想我已经找到了。正如我告诉你们的,我正在使用websocket在服务器和客户端之间传输数据。这里有一个提示:当我开始签名过程时,我有60662字节。在压缩它并编码到base64之后,我有72548个字节(我知道它更大,但如果我将60k编码到base64,它将变得更大)。问题是:我的websocket类已经准备好处理65k字节了,哈哈。所以现在我正在研究它,可以稍后发布结果。如果传输大数据有问题,那么只传输散列,并使用以下代码:
            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();