I am using iText 5 Java for external signing. First I create signature appearance, calculate the hash for signed attributes and leave empty place for the signature. Later when I get the signed hash from client, I insert it the PDF via MakeSignature.signDeferred
.
But PDF reader is showing the signature as invalid. Complaining the PDF has been modified.
Here is the code used for signing. I have removed a lot of functioning code to keep the code at bare essentials.
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.*;
import com.itextpdf.text.pdf.security.*;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import java.io.*;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.Calendar;
import java.util.GregorianCalendar;
public class MklTest {
static String thisHash;
static class MyExternalSignatureContainer implements ExternalSignatureContainer {
protected byte[] sig;
public MyExternalSignatureContainer(byte[] sig) {
this.sig = sig;
}
public byte[] sign(InputStream is) {
return sig;
}
@Override
public void modifySigningDictionary(PdfDictionary signDic) {
}
}
static class EmptyContainer implements ExternalSignatureContainer {
public EmptyContainer() {
}
public byte[] sign(InputStream is) {
ExternalDigest digest = hashAlgorithm1 -> DigestAlgorithms.getMessageDigest(hashAlgorithm1, null);
try {
byte[] hash = DigestAlgorithms.digest(is, digest.getMessageDigest("SHA256"));
thisHash = Hex.encodeHexString(hash);
return new byte[0];
} catch (IOException | GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
@Override
public void modifySigningDictionary(PdfDictionary pdfDictionary) {
pdfDictionary.put(PdfName.FILTER, PdfName.ADOBE_PPKMS);
pdfDictionary.put(PdfName.SUBFILTER, PdfName.ADBE_PKCS7_DETACHED);
}
}
public static String 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');
Calendar cal = GregorianCalendar.getInstance();
cal.add(Calendar.MINUTE, 10);
PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
appearance.setVisibleSignature(new Rectangle(36, 748, 144, 780), 1, fieldname);
appearance.setCertificate(chain[0]);
appearance.setReason("Nice");
appearance.setLocation("Delhi");
appearance.setSignDate(cal);
ExternalSignatureContainer external = new EmptyContainer();
MakeSignature.signExternalContainer(appearance, external, 8192);
os.close();
reader.close();
return thisHash;
}
public static Certificate getCert() throws CertificateException {
String cert = ""; // the cert we get from client
ByteArrayInputStream userCertificate = new ByteArrayInputStream(Base64.decodeBase64(cert));
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return cf.generateCertificate(userCertificate);
}
private static ExternalDigest getDigest() {
return new ExternalDigest() {
public MessageDigest getMessageDigest(String hashAlgorithm)
throws GeneralSecurityException {
return DigestAlgorithms.getMessageDigest(hashAlgorithm, null);
}
};
}
public static void createSignature(String src, String dest, String fieldname, byte[] signature) throws IOException, DocumentException, GeneralSecurityException {
PdfReader reader = new PdfReader(src);
FileOutputStream os = new FileOutputStream(dest);
ExternalSignatureContainer external = new MyExternalSignatureContainer(signature);
MakeSignature.signDeferred(reader, fieldname, os, external);
reader.close();
os.close();
}
public static void main(String[] args) throws Exception {
Certificate cert = getCert();
Certificate[] chain = {cert};
String src = "/home/spooderman/Downloads/sample.pdf";
String between = "/tmp/sample_out_between.pdf";
String dest = "/tmp/sample_out.pdf";
String fieldName = "sign";
String hash = emptySignature(src, between, fieldName, chain);
String signature = ""; // signed hash signature we get from client
byte[] signatureBytes = Hex.decodeHex(signature.toCharArray());
PdfPKCS7 sgn = new PdfPKCS7(null, chain, "SHA256", null, getDigest(), false);
sgn.setExternalDigest(signatureBytes, null, "RSA");
byte[] data = sgn.getEncodedPKCS7(Hex.decodeHex(hash.toCharArray()),null, null, null, MakeSignature.CryptoStandard.CMS);
createSignature(between, dest, fieldName, data);
}
}
This is the original PDF.
This is the PDF with empty signature.
This is the PDF with final signature.
Calculated hash for the PDF.
954927c9286320e904920b0bf12f7cad387c1a9afd5a92314960a1083593f7dc
This is the signed hash signature I received from the client.
6c14b965c7e90c3134653a9261b0666dce7a7e28cb605fc3152ad111fa7915a77396799357daf1d37c52163ce6d34bfd96ee743e721b45e929f6d8aced144f094d03dce00f25c6c1fc5aa63c92322780f7de675c194ef17303a643055dbbedfec9d5200994fcdfc3ad9488d568ad3f6cd2d262e360a79ad90b5ffb188723de559f3696dcb223930f842172e4838f7d5e6a44494ced54bca54ed12133ea189d616a10039a222ce61885ad98b8ba0bd83d63b887e2c188ca10bd2f53f92f08c5585b9826553280c19976a0ba29f7789ad6a80010b4a6431d3b6bb8f27999b23d3739de03db6db8ab46acaf38b33bd37a74465744c3f95a093deff26cb44b45e27e
I have tried a lot of things found on stackoverflow but the problem remains the same. Any help in the right direction would be really appreciated.
For the client part I am using Fortify which makes local USB tokens and HSM modules available to JS client.
The problem was that EmptyContainer.sign
method just gives out PDF bytes and not the authenticated attributes. The authenticated attributes are actually which needs to be signed. Thanks mkl for pointing in the right direction.
I modified EmptyContainer.sign
method to create a PdfPKCS7
object and call PdfPKCS7.getAuthenticatedAttributeBytes
with PDF hash as one of the arguments.
After signing the bytes returned by getAuthenticatedAttributeBytes()
method and creating CMS container of the signed bytes and the original hash, I was able to successfully sign the PDF.
Here is the code if someone needs it. It is cluttered all over but you will get the essence.
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.*;
import com.itextpdf.text.pdf.security.*;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import javax.annotation.Nullable;
import java.io.*;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Calendar;
import java.util.GregorianCalendar;
public class MklTest {
static Certificate[] chain;
static byte[] toSign;
static byte[] hash;
static class MyExternalSignatureContainer implements ExternalSignatureContainer {
protected byte[] sig;
public MyExternalSignatureContainer(byte[] sig) {
this.sig = sig;
}
public byte[] sign(InputStream is) {
return sig;
}
@Override
public void modifySigningDictionary(PdfDictionary signDic) {
}
}
static class EmptyContainer implements ExternalSignatureContainer {
public EmptyContainer() {
}
public byte[] sign(InputStream is) {
try {
ExternalDigest digest = getDigest();
String hashAlgorithm = getHashAlgorithm();
Certificate[] certs = {null};
hash = DigestAlgorithms.digest(is, digest.getMessageDigest(hashAlgorithm));
PdfPKCS7 sgn = getPkcs(certs);
toSign = sgn.getAuthenticatedAttributeBytes(hash, getOscp(), null,
MakeSignature.CryptoStandard.CMS);
return new byte[0];
} catch (IOException | GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
@Override
public void modifySigningDictionary(PdfDictionary pdfDictionary) {
pdfDictionary.put(PdfName.FILTER, PdfName.ADOBE_PPKMS);
pdfDictionary.put(PdfName.SUBFILTER, PdfName.ADBE_PKCS7_DETACHED);
}
}
public static String getHashAlgorithm() {
return "SHA256";
}
public static byte[] getOscp() {
byte[] ocsp = null;
OcspClient ocspClient = new OcspClientBouncyCastle(new OCSPVerifier(null, null));
if (chain.length >= 2) {
ocsp = ocspClient.getEncoded((X509Certificate)chain[0], (X509Certificate)chain[1], null);
}
return ocsp;
}
public static PdfPKCS7 getPkcs() throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException {
return new PdfPKCS7(null, chain, getHashAlgorithm(), null, getDigest(), false);
}
public static PdfPKCS7 getPkcs(@Nullable Certificate[] certChain) throws NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException {
//noinspection ConstantConditions
return new PdfPKCS7(null, certChain, getHashAlgorithm(), null, getDigest(), false);
}
public static void emptySignature(String src, String dest, String fieldname) throws IOException, DocumentException, GeneralSecurityException {
PdfReader reader = new PdfReader(src);
FileOutputStream os = new FileOutputStream(dest);
PdfStamper stamper = PdfStamper.createSignature(reader, os, '\0');
Calendar cal = GregorianCalendar.getInstance();
cal.add(Calendar.MINUTE, 10);
PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
appearance.setVisibleSignature(new Rectangle(36, 748, 144, 780), 1, fieldname);
appearance.setReason("Nice");
appearance.setLocation("Delhi");
appearance.setSignDate(cal);
ExternalSignatureContainer external = new EmptyContainer();
MakeSignature.signExternalContainer(appearance, external, 8192);
os.close();
reader.close();
}
public static void setChain() throws CertificateException {
String cert = ""; // the cert we get from client
ByteArrayInputStream userCertificate = new ByteArrayInputStream(Base64.decodeBase64(cert));
CertificateFactory cf = CertificateFactory.getInstance("X.509");
chain = new Certificate[]{cf.generateCertificate(userCertificate)};
}
private static ExternalDigest getDigest() {
return new ExternalDigest() {
public MessageDigest getMessageDigest(String hashAlgorithm)
throws GeneralSecurityException {
return DigestAlgorithms.getMessageDigest(hashAlgorithm, null);
}
};
}
public static TSAClient getTsa() {
return new TSAClientBouncyCastle("http://timestamp.digicert.com", null, null, 4096, "SHA-512");
}
public static void createSignature(String src, String dest, String fieldname, byte[] hash, byte[] signature) throws IOException, DocumentException, GeneralSecurityException {
PdfPKCS7 sgn = getPkcs();
sgn.setExternalDigest(signature, null, "RSA");
byte[] encodedSig = sgn.getEncodedPKCS7(hash, getTsa(), getOscp(), null,
MakeSignature.CryptoStandard.CMS);
PdfReader reader = new PdfReader(src);
FileOutputStream os = new FileOutputStream(dest);
ExternalSignatureContainer external = new MyExternalSignatureContainer(encodedSig);
MakeSignature.signDeferred(reader, fieldname, os, external);
reader.close();
os.close();
}
public static void main(String[] args) throws Exception {
setChain();
String src = "/home/spooderman/Downloads/sample.pdf";
String between = "/tmp/sample_out_between.pdf";
String dest = "/tmp/sample_out.pdf";
String fieldName = "sign";
emptySignature(src, between, fieldName);
System.out.println(Hex.encodeHexString(toSign));
String signature = ""; // signed hash signature we get from client
byte[] signatureBytes = Hex.decodeHex(signature.toCharArray());
createSignature(between, dest, fieldName, hash, signatureBytes);
}
}