I checked with other similar issues on Stackoverflow, but it doesn’t work on my case.
Situation: I am developing an application which needs to sign the pdf document. The signing key is held by another company, let’s say it’s CompanyA.
I did the following steps:
- Got the pdf document to sign ready.
- Created a Temp pdf file which added an Empty Signature in the original pdf.
- Read the Temp pdf to get the message digest. (Encode it to base64)
- Send the message digest (Base64 encoded) to the CompanyA to get signed.
- Get the signed digest (base64 encoded) from CompanyA.
- Do the base64 decoding. And embedded the result into the Temp pdf to get the final signed pdf.
Everything goes well and I can get the final signed pdf. But When I open it in Adobe reader, it says “the document has been altered or corrupted since the signature was applied”.
I used this getHashBase64Str2Sign
to get the message digest (in base64). This method calls the emptySignature()
method to create the Temp file with empty signature, and then calls the getSignatureHash()
method to read the Temp file to get the message digest.
public static String getHashBase64Str2Sign() {
try {
// Add BC provider
BouncyCastleProvider providerBC = new BouncyCastleProvider();
Security.addProvider(providerBC);
// Create parent path of dest pdf file, if not exist
File file = new File(DEST).getParentFile();
if (!file.exists()) {
file.mkdirs();
}
CertificateFactory factory = CertificateFactory.getInstance("X.509");
Certificate[] chain = new Certificate[1];
try (InputStream certIs = new FileInputStream(CERT)) {
chain[0] = factory.generateCertificate(certIs);
}
// Get byte[] hash
DeferredSigning app = new DeferredSigning();
app.emptySignature(SRC, TEMP, "sig", chain);
byte[] sh = app.getSignatureHash(TEMP, "SHA256", chain);
// Encode byte[] hash to base64 String and return
return Base64.getEncoder().encodeToString(sh);
} catch (IOException | GeneralSecurityException e) {
e.printStackTrace();
return null;
}
}
private void emptySignature(String src, String dest, String fieldname, Certificate[] chain)
throws IOException, GeneralSecurityException {
PdfReader reader = new PdfReader(src);
PdfSigner signer = new PdfSigner(reader, new FileOutputStream(dest), new StampingProperties());
PdfSignatureAppearance appearance = signer.getSignatureAppearance();
appearance.setPageRect(new Rectangle(100, 500, 200, 100));
appearance.setPageNumber(1);
appearance.setCertificate(chain[0]);
appearance.setReason("For test");
appearance.setLocation("HKSAR");
signer.setFieldName(fieldname);
/*
* ExternalBlankSignatureContainer constructor will create the PdfDictionary for
* the signature information and will insert the /Filter and /SubFilter values
* into this dictionary. It will leave just a blank placeholder for the
* signature that is to be inserted later.
*/
IExternalSignatureContainer external = new ExternalBlankSignatureContainer(PdfName.Adobe_PPKLite,
PdfName.Adbe_pkcs7_detached);
// Sign the document using an external container.
// 8192 is the size of the empty signature placeholder.
signer.signExternalContainer(external, 100000);
}
private byte[] getSignatureHash(String src, String hashAlgorithm, Certificate[] chain)
throws IOException, GeneralSecurityException {
InputStream is = new FileInputStream(src);
// Get the hash
BouncyCastleDigest digest = new BouncyCastleDigest();
PdfPKCS7 sgn = new PdfPKCS7(null, chain, hashAlgorithm, null, digest, false);
byte hash[] = DigestAlgorithms.digest(is, digest.getMessageDigest(sgn.getHashAlgorithm()));
return sgn.getAuthenticatedAttributeBytes(hash, PdfSigner.CryptoStandard.CMS, null, null);
}
private void createSignature(String src, String dest, String fieldName, byte[] sig)
throws IOException, GeneralSecurityException {
PdfReader reader = new PdfReader(src);
try (FileOutputStream os = new FileOutputStream(dest)) {
PdfSigner signer = new PdfSigner(reader, os, new StampingProperties());
IExternalSignatureContainer external = new MyExternalSignatureContainer(sig);
// Signs a PDF where space was already reserved. The field must cover the whole
// document.
PdfSigner.signDeferred(signer.getDocument(), fieldName, os, external);
}
}
Then, the message digest is sent to CompanyA for signing. After I got the signed digest from CompanyA (which is base64 encoded), I call the embedSignedHashToPdf()
method to get the signed pdf document.
public static void embedSignedHashToPdf(String signedHash) {
try {
byte[] sig = Base64.getDecoder().decode(signedHash);
// Get byte[] hash
DeferredSigning app = new DeferredSigning();
app.createSignature(TEMP, DEST, "sig", sig);
} catch (IOException | GeneralSecurityException e) {
e.printStackTrace();
}
}
class MyExternalSignatureContainer implements IExternalSignatureContainer {
protected byte[] sig;
public MyExternalSignatureContainer(byte[] sig) {
this.sig = sig;
}
@Override
public void modifySigningDictionary(PdfDictionary signDic) {
}
@Override
public byte[] sign(InputStream arg0) throws GeneralSecurityException {
return sig;
}
}
At last, I can get the signed pdf document, but it shows error in Adobe Reader, like this:
Please check the original pdf, temp pdf, and the final signed pdf file as follows: