Внешняя подпись с PDFBox 2.0.15 - PullRequest
0 голосов
/ 03 июля 2019

Я внедряю приложение для подписи файлов 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. Насколько я понимаю, этого не должно произойти, поскольку смещение диапазона байтов сигнатуры запоминается при втором пост-вызове.

Любая помощь или идея приветствуется,

Заранее спасибо.

[EDIT]

Полный список используемых файлов: 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 для вызова этого сервиса, который возвращает мне сигнатуру, e это работает.

1 Ответ

1 голос
/ 04 июля 2019

Благодаря Тилману Хаушерру Я мог понять, что происходит:

1 - У меня есть приложение для настольного компьютера, которое связывается с SmatCards и т. Д., И это подписавшая сторона. Для связи с сервером (через веб-страницу) мы используем WebSocket. Я написал свой собственный класс сервера веб-сокетов, и поэтому он готов работать только с 65 КБ. Чем когда я пытался отправить данные сюда:

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

Я получил ошибки в приложении.

2 - Тилман предложил мне взглянуть на этот @ mkl ответ , где он делает то же самое: создайте хэш SHA256 для externalSigning.getContent() и отправьте его для подписи в другом месте. Я не знаю почему, но единственное, что не сработало для меня, было:

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

Итак, еще раз спасибо сообществу !!!

...