First of all, although I've been following StackOverflow for quite some time, it is the first time I posted something, so if I do something wrong or not according to the rules, please feel free to point me in the right direction.
I'm developing a PDF digital signature application, using iText5, which depends on an external service to provide a signed hash after I prepare the PDF for signing.
As described in iText documentation, in the first phase I prepared the PDF (in the final implementation, all PDFs may be multi signed, so I use append mode), like so:
public static byte[] GetBytesToSign(string unsignedPdf, string tempPdf, string signatureFieldName, List<Org.BouncyCastle.X509.X509Certificate> certificateChain) {
// we create a reader and a stamper
using (PdfReader reader = new PdfReader(unsignedPdf)) {
using (FileStream baos = File.OpenWrite(tempPdf)) {
List<Org.BouncyCastle.X509.X509Certificate> chain = certificateChain;
PdfStamper pdfStamper = PdfStamper.CreateSignature(reader, baos, '\0', null, true);
sap = pdfStamper.SignatureAppearance;
sap.Certificate = certificateChain[0];
sap.SetVisibleSignature(new iTextSharp.text.Rectangle(36, 720, 160, 780), 1, signatureFieldName);
//sap.SetVisibleSignature(signatureFieldName);
sap.SignDate = DateTime.Now;
PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
dic.Date = new PdfDate(sap.SignDate);
dic.Name = CertificateInfo.GetSubjectFields(chain[0]).GetField("CN");
sap.CryptoDictionary = dic;
sap.Certificate = certificateChain[0];
sap.Acro6Layers = true;
sap.Reason = "test";
sap.Location = "test";
IExternalSignatureContainer external = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
MakeSignature.SignExternalContainer(sap, external, 8192);
signatureContainer = new PdfPKCS7(null, chain, "SHA256", false);
byte[] hash = DigestAlgorithms.Digest(sap.GetRangeStream(), "SHA256");
//byte[] signatureHash = signatureContainer.getAuthenticatedAttributeBytes(hash, null, null, CryptoStandard.CMS);
return hash;
}
}
}
After this step, I send the hash to the external service, which returns a signed hash.
Inspecting the hash that I send to the service, it seems to be correct, since it covers all the PDF except for the new signature contents.
I then conclude the signature process using the method below:
private byte[] Sign(PdfPKCS7 signatureContainer, List<X509Certificate2> chain2, List<Org.BouncyCastle.X509.X509Certificate> chain, byte[] hash, byte[] signedBytes, string tmpPdf, string signedPdf, string signatureFieldName) {
System.Security.Cryptography.RSACryptoServiceProvider publicCertifiedRSACryptoServiceProvider = chain2[0].PublicKey.Key as System.Security.Cryptography.RSACryptoServiceProvider;
bool verify = publicCertifiedRSACryptoServiceProvider.VerifyHash(hash, "SHA256", signedBytes); //verify if the computed hash is same as signed hash using the cert public key
Console.WriteLine("PKey signed computed hash is equal to signed hash: " + verify);
AsnEncodedData asnEncodedData = new AsnEncodedData(signedBytes);
Console.WriteLine(asnEncodedData.Format(true));
//ITEXT5
try {
//Console.WriteLine("Signed bytes: " + Encoding.UTF8.GetString(signedBytes));
using (PdfReader reader = new PdfReader(tmpPdf)) {
using (FileStream outputStream = File.OpenWrite(signedPdf)) {
IExternalSignatureContainer external = new Objects.MyExternalSignatureContainer(signedBytes, chain, signatureContainer);
MakeSignature.SignDeferred(reader, signatureFieldName, outputStream, external);
}
}
return new byte[] { };
}
catch(Exception ex) {
File.Delete(tmpPdf);
Console.WriteLine("Error signing file: " + ex.Message);
return new byte[] { };
}
}
In the beggining of the Sign method, I verify if the hash sent to the external service, signed with the same certificate, is equal to the external service response, which is true.
MyExternalSignatureContainer code:
public class MyExternalSignatureContainer : IExternalSignatureContainer {
private readonly byte[] signedBytes;
public List<Org.BouncyCastle.X509.X509Certificate> Chain;
private PdfPKCS7 sigField;
public MyExternalSignatureContainer(byte[] signedBytes) {
this.signedBytes = signedBytes;
}
public MyExternalSignatureContainer(byte[] signedBytes, List<Org.BouncyCastle.X509.X509Certificate> chain, PdfPKCS7 pdfPKCS7) {
this.signedBytes = signedBytes;
this.Chain = chain;
this.sigField = pdfPKCS7;
}
public byte[] Sign(Stream data) {
try {
sigField.SetExternalDigest(signedBytes, null, "RSA");
return sigField.GetEncodedPKCS7(signedBytes, null, null, null, CryptoStandard.CMS);
}
catch (IOException ioe) {
throw ioe;
}
}
public void ModifySigningDictionary(PdfDictionary signDic) {
}
}
The problem is when I open the PDF in Acrobat, it states that the document has been modified or corrupted since the signature was applied.
(If I open the same PDF in PDF-XChange, it says the PDF wasn't modified).
What I've tried so far without luck:
Not being completely sure if the external service uses SHA256, I've already tried changing the digest to SHA1 of the pre-signing, resulting in a "Formatting error" in Acrobat Reader.
Like stated in another post in StackOverlow regarding the same issue (I'm unable to find the post to link it), a potential problem would be using different streams for the temporary file. I already tried to use the same stream without luck as well.
Samples of the PDF's:
Original file
Temp File
Signed File
Base64 hash sent to the service:
XYfaS/SisA/tk5hcl035RpBjOczrH9E5rgiAMpqgkjI=
Base64 signed hash sent in response:
CnV3WL7skhMCtZG1r1Qi2oyE9WPO3KP4Ieu/Xm4lec+DAbYbhQxCvjMISsG3sTwYY7Lqi4luD60uceViDH848rS9OkTn8szzAnnX2fSYIwqDpG3qjJAb6NOXEv41hy+XYhSBJWS4ji2mM2ReruwPafxB1aM25L5Jyd0V7WecuNFUevUrvd85Y2KBkyBw9zCA8NDAQPPY0UT4GkXZi3Z35+Sf/s2o8zxCOlBDaIJyMvJ9De79nw4jC5L9NesHpFxx3mX1g1N33GHjUNdETgFMhnd8RDUlGLW6bsAyv78gvwE6aXF6COObap/VtlLvMOME68MzLr6izKte6uA35Zwj9Q==
Update after mkl's answer:
According to the answer, I changed the code sign the document in only one phase and ended up with the following methods:
using (PdfReader reader = new PdfReader(fileLocation)) {
using (FileStream baos = File.OpenWrite(tmpFile)) {
List<Org.BouncyCastle.X509.X509Certificate> chain = Chain;
PdfStamper pdfStamper = PdfStamper.CreateSignature(reader, baos, '\0', null, true);
PdfSignatureAppearance sap = pdfStamper.SignatureAppearance;
sap.Certificate = Chain[0];
sap.SetVisibleSignature(new iTextSharp.text.Rectangle(36, 720, 160, 780), 1, signatureFieldName);
//sap.SetVisibleSignature(signatureFieldName);
sap.SignDate = DateTime.Now;
PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
dic.Date = new PdfDate(sap.SignDate);
dic.Name = CertificateInfo.GetSubjectFields(chain[0]).GetField("CN");
sap.CryptoDictionary = dic;
sap.Certificate = Chain[0];
sap.Acro6Layers = true;
//sap.CertificationLevel = PdfSignatureAppearance.CERTIFIED_FORM_FILLING_AND_ANNOTATIONS;
sap.Reason = "test";
sap.Location = "test";
IExternalSignature signature = new Objects.RemoteSignature(client, signatureRequest);
MakeSignature.SignDetached(sap, signature, Chain, null, null, null, 8192, CryptoStandard.CMS);
}
}
And the IExternalSignature implementation:
public virtual byte[] Sign(byte[] message) {
IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
//
// Request signature for hash value messageHash
// and return signature bytes
//
signatureRequest.Hash = messageHash;
SignatureService.SignatureResponse signatureResponse = client.Signature(signatureRequest);
if (signatureResponse.Status.Code == "00") {
return signatureResponse.DocumentSignature;
}
else {
throw new Exception("Error signing file: " + signatureResponse.Status.Message);
}
}
The signatureResponse.DocumentSignature represents the signed bytes returned by the service.
In the result PDF now I'm a getting a BER decoding error.
Analyzing your example PDF you appear to declare the wrong certificate as signer certificate
Although I know the current certificate isn't valid, it is provided by the service and in the previous implementation of the service, where I would send the entire PDF for signing, the signed PDF was also signed with this certificate.
A question: Knowing that in a two-phase signing I was able to sign the PDF with this certificate (apart from the altered or corrupted document after signing error), shouldn't this method also work with the same certificate?
Currently, what is happening is this:
Inspecting the signature:
Again, if I open the same PDF in PDF-XChange, the signature is valid and the document was not modified. The requirement is the PDF to be valid in Acrobat, but I'm puzzled by this difference between readers.
Result PDF
Update 2
I.e. you only have to prefix your hash with the byte sequence 30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20.
After adding this SHA256 prefix to the message digest, the resulting PDF is now correctly signed.
Will Adobe Reader accept the fixed signature?
I doubt it. The key usage of the signer certificate only contains the value for signing other certificates.
The current certificate is only used for testing. In the production environment I believe the certificate provided by the external service will be valid.
I have two more questions regarding this issue:
For your code this means that you have to pack the hash into a DigestInfo structure before sending it to the service.
Q: How did you inspect the signature container to conclude that it wasn't correct?
Q: In my initial code, I had a two-phased signing. Would the same principal applied in a single-sign method still be valid, i.e., applying the SHA256 prefix do the pre-signed bytes and after setting the digest with the resulting signed bytes?