I'm trying to build an application where the following happens:
- A client requests a PDF-hash from the server.
- The server generates the hash of a PDF-file and sends this to the client.
- The client signs this hash with his private key and sends the signed hash along with the public part of his own certificate.
- The server generates a new, signed PDF file.
The problem I'm having with this: It seems impossible for the server to generate a to-be-signed hash without having the client's certificate available beforehand. I'd really prefer to create a solution where the server does not need to know the client's certificate in order to create the document digest.
All the examples I have found so far use the PdfPKCS7.getAuthenticatedAttributeBytes function to get the to-be-signed hash, but this requires a client certificate to be known. I have looked at the "Digital Signatures for PDF documents" white paper by Bruno Lowagie, but I failed to see exactly what information is digested.
Here's a code snippet of my current attempt:
public byte[] simplePresign(String src, String digestAlgorithm) throws IOException, DocumentException, GeneralSecurityException {
this.digestAlgorithm = digestAlgorithm;
tsaClient = new CustomTSAClient();
PdfReader reader = new PdfReader(src);
os = new ByteArrayOutputStream();
PdfAStamper stamper = PdfAStamper.createSignature(reader, os, '\0', PdfAConformanceLevel.PDF_A_1B);
appearance = stamper.getSignatureAppearance();
PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
appearance.setCryptoDictionary(dic);
HashMap<PdfName, Integer> exc = new HashMap<PdfName, Integer>();
exc.put(PdfName.CONTENTS, getEstimatedSize(null, tsaClient) * 2 + 2);
appearance.preClose(exc);
InputStream data = appearance.getRangeStream();
MessageDigest mDigest = DigestAlgorithms.getMessageDigest(digestAlgorithm, null);
return DigestAlgorithms.digest(data, mDigest);
}
Unfortunately this hash does not seem to be correct, signing this hash and generating a signed document based on the signed hash leads to an invalid signature.
I would be most grateful if someone can help me improve this code snippet, or otherwise give me some insight in the data that I need to digest for a signature.
It seems that you have overlooked the DeferredSigning example.
In this example, we first create a PDF with an empty signature:
public void emptySignature(String src, String dest, String fieldname, Certificate[] chain) throws IOException, DocumentException, GeneralSecurityException {
PdfReader reader = new PdfReader(src);
FileOutputStream os = new FileOutputStream(dest);
PdfStamper stamper = PdfStamper.createSignature(reader, os, '\0');
PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
appearance.setVisibleSignature(new Rectangle(36, 748, 144, 780), 1, fieldname);
appearance.setCertificate(chain[0]);
ExternalSignatureContainer external = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
MakeSignature.signExternalContainer(appearance, external, 8192);
}
Granted, the public certificate chain[0]
is passed to the appearance
in this example, it is used to create the visual appearance and to create the PdfPKCS7
object.
Once you have a PDF with an empty signature, you can create a PdfSignatureAppearance
on the server and get the hash that can be sent to the client for signing. This can be done using the getRangeStream()
method to get the ranges of PDF bytes that need to be hashed. This method returns an InputStream
, that can be used like this:
BouncyCastleDigest digest = new BouncyCastleDigest();
PdfPKCS7 sgn = new PdfPKCS7(null, chain, hashAlgorithm, null, digest, false);
byte[] hash = DigestAlgorithms.digest(is, digest.getMessageDigest(hashAlgorithm));
Calendar cal = Calendar.getInstance();
byte[] sh = sgn.getAuthenticatedAttributeBytes(hash, cal, null, null, CryptoStandard.CMS);
Now you can send this sh
to the client for signing. You will receive another byte[]
which is the actual signature that needs to be added to the PDF, let's say the byte[]
is called sig
.
Your external signature container can be kept very simple: it just needs to return the signature bytes:
class MyExternalSignatureContainer implements ExternalSignatureContainer {
protected byte[] sig;
public MyExternalSignatureContainer(byte[] sig) {
this.sig = sig;
}
public byte[] sign(InputStream is) throws Exception {
return sig;
}
public void modifySigningDictionary(PdfDictionary signDic) {
}
}
You can now use the createSignature()
method on the server:
public void createSignature(String src, String dest, String fieldname, PrivateKey pk, Certificate[] chain) throws IOException, DocumentException, GeneralSecurityException {
PdfReader reader = new PdfReader(src);
FileOutputStream os = new FileOutputStream(dest);
ExternalSignatureContainer external = new MyExternalSignatureContainer(sig);
MakeSignature.signDeferred(reader, fieldname, os, external);
}