I'm using iText 7 for applying signatures to pdf documents. I also use my own implementation of IExternalSignatureContainer in order to integrate the certificates into the PKCS7 CMS since the signing service only returns a PKCS1 signature.
The signature process is asynchrony (user has to authenticate) i'd like to do the following:
Reason for this is that i don't have too keep the prepared document in memory and also for batch signing.
My problem is that the hash value created is always different. (Even if i set the date/time via pdfSigner.SetSignDate to the same value) or every PdfReader/PdfSigner instance.
//Create the hash of of the pdf document
//Part of my IExternalSignatureContainer Sign method
//Called from iText pdfSigner.SignExternalContainer
//The produced hash is always different
byte[] hash = DigestAlgorithms.Digest(pdfStream, DigestAlgorithms.GetMessageDigest(hashAlgorithm));
Question: Is there a way to
Attached is a complete example of the process (Including the signature creation which actually needs to be done by a different service)
using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using iText.Kernel.Pdf;
using iText.Signatures;
using Org.BouncyCastle.X509;
using X509Certificate = Org.BouncyCastle.X509.X509Certificate;
namespace SignExternalTestManuel
{
class Program
{
const string filePath = @"c:\temp\pdfsign\";
public static string pdfToSign = Path.Combine(filePath, @"test.pdf");
public static string destinationFile = Path.Combine(filePath, "test_signed.pdf");
public static string LocalUserCertificatePublicKey = Path.Combine(filePath, "BITSignTestManuel5Base64.cer");
public static string LocalCaCertificatePublicKey = Path.Combine(filePath, "BITRoot5Base64.cer");
public static string privateKeyFile = Path.Combine(filePath, "BITSignTestManuel5.pfx");
public static string privateKeyPassword = "test";
public static void Main(String[] args)
{
PdfReader reader = new PdfReader(pdfToSign);
using (FileStream os = new FileStream(destinationFile, FileMode.OpenOrCreate))
{
StampingProperties stampingProperties = new StampingProperties();
stampingProperties.UseAppendMode();
PdfSigner pdfSigner = new PdfSigner(reader, os, stampingProperties);
pdfSigner.SetCertificationLevel(PdfSigner.NOT_CERTIFIED);
IExternalSignatureContainer external = new GsSignatureContainer(
PdfName.Adobe_PPKLite,
PdfName.Adbe_pkcs7_detached);
pdfSigner.SetSignDate(new DateTime(2021, 2, 22, 10, 0, 0));
pdfSigner.SetFieldName("MySignatureField");
pdfSigner.SignExternalContainer(external, 32000);
}
}
}
public class GsSignatureContainer : IExternalSignatureContainer
{
private PdfDictionary sigDic;
public GsSignatureContainer(PdfName filter, PdfName subFilter)
{
sigDic = new PdfDictionary();
sigDic.Put(PdfName.Filter, filter);
sigDic.Put(PdfName.SubFilter, subFilter);
}
/// <summary>
/// Implementation based on https://kb.itextpdf.com/home/it7kb/examples/how-to-use-a-digital-signing-service-dss-such-as-globalsign-with-itext-7#HowtouseaDigitalSigningService(DSS)suchasGlobalSign,withiText7-Examplecode
/// </summary>
/// <param name="pdfStream"></param>
/// <returns></returns>
public byte[] Sign(Stream pdfStream)
{
//Create the certificate chaing since the signature is just a PKCS1, the certificates must be added to the signature
X509Certificate[] chain = null;
string cert = System.IO.File.ReadAllText(Program.LocalUserCertificatePublicKey);
string ca = System.IO.File.ReadAllText(Program.LocalCaCertificatePublicKey);
chain = CreateChain(cert, ca);
X509CrlParser p = new X509CrlParser();
String hashAlgorithm = DigestAlgorithms.SHA256;
PdfPKCS7 pkcs7Signature = new PdfPKCS7(null, chain, hashAlgorithm, false);
//Create the hash of of the pdf document
//Part of my IExternalSignatureContainer Sign method
//Called from iText pdfSigner.SignExternalContainer
//The produced hash is always different
byte[] hash = DigestAlgorithms.Digest(pdfStream, DigestAlgorithms.GetMessageDigest(hashAlgorithm));
byte[] signature = null;
//Create the hash based on the document hash which is suitable for pdf siging with SHA256 and a X509Certificate
byte[] sh = pkcs7Signature.GetAuthenticatedAttributeBytes(hash, null, null, PdfSigner.CryptoStandard.CMS);
//Create the signature via own certificate
signature = CreateSignature(sh, Program.privateKeyFile, Program.privateKeyPassword);
pkcs7Signature.SetExternalDigest(signature, null, "RSA");
return pkcs7Signature.GetEncodedPKCS7(hash, null, null, null, PdfSigner.CryptoStandard.CMS);
}
public void ModifySigningDictionary(PdfDictionary signDic)
{
signDic.PutAll(sigDic);
}
private static X509Certificate[] CreateChain(String cert, String ca)
{
//Note: The root certificate could be omitted and it would still work
X509Certificate[] chainy = new X509Certificate[2];
X509CertificateParser parser = new X509CertificateParser();
chainy[0] = new X509Certificate(parser.ReadCertificate(Encoding.UTF8.GetBytes(cert))
.CertificateStructure);
chainy[1] = new X509Certificate(parser.ReadCertificate(Encoding.UTF8.GetBytes(ca))
.CertificateStructure);
return chainy;
}
#region "Create signature, will be done by an actual service"
private byte[] CreateSignature(byte[] hash, string privateKeyFile, string privateKeyPassword)
{
//Sign data directly with a X509Certificate
X509Certificate2 rootCertificateWithPrivateKey = new X509Certificate2();
byte[] rawData = System.IO.File.ReadAllBytes(privateKeyFile);
rootCertificateWithPrivateKey.Import(rawData, privateKeyPassword, X509KeyStorageFlags.Exportable);
using (var key = rootCertificateWithPrivateKey.GetRSAPrivateKey())
{
return key.SignData(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
}
#endregion
}
}
Question: Is there a way to
- Produce the hash of a pdf document "ahead of time" on one instance of the PdfReader
- Create the signature
- Apply the signature on a different instance of the PdfReader
This use case currently is not supported by iText, in each pass in particular
It is possible to patch iText to use the same values in each pass, but before patching a library you should consider whether you can adapt your architecture to make a patch unnecessary.
In your case, for example, if you cannot keep the original PdfSigner
instance, an alternative approach could be to have the original PdfSigner
after hashing store its result file with dummy signature bytes (e.g. new byte[0]
). Then, after retrieving the signature container, you can inject it into the stored file in a different service using PdfSigner.signDeferred
as long as both service can access shared storage (or the first service can at least forward the file the the storage of the second service).