Я внедряю приложение для подписи файлов 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;
}
В выражении «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 это работает.