iTextSharp and Windows SSL Certificates
Well, it took a bit of work, but I figured out how to add certificates and intermediate certificates into a PDF document while signing it using iTextSharp. The examples they had were close, but not quite there. They had hard coded the buffer size to 4000. Seemed a bit small, it got real tight when I had 2 intermediate certificates, so I made a change to that to use a more dynamic approach. I did a 4000 + RawData.Length
for each certificate. Definitely opened up some more room to fit everything in there. After doing that in their little SignMsg
method, I changed the IncludeOptions
to be ExcludeRoot
. This way, it would import the entire chain, skipping the root certificate (it should already be on the client's computer) You can always include it, but it will still not be trusted. So that's all up to you. If doing a windows SSL cert, you will need to check the correct options in Adobe Reader or Acrobat. Its in the Preferences->Security->Advanced->Windows Integration->Check all 3.
Here's my final proof of concept code:
using System;
using System.Configuration;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.Pkcs;
using System.Security.Cryptography.X509Certificates;
using iTextSharp.text;
using iTextSharp.text.pdf;
using System.Linq;
namespace PDFStuff
{
class PDFPlayground
{
public static X509Store GetStore()
{
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.MaxAllowed);
return store;
}
public static X509Certificate2 GetCertificateByThumbprint(string thumbprint, bool requirePrivate)
{
if (string.IsNullOrEmpty(thumbprint))
{
return null;
}
X509Store store = GetStore();
X509Certificate2 result = null;
try
{
foreach (X509Certificate2 c in store.Certificates)
{
if (c.Thumbprint == thumbprint &&
(c.HasPrivateKey || !requirePrivate))
{
result = c;
}
}
}
catch (Exception exception)
{
}
finally
{
store.Close();
}
return result;
}
public static byte[] SignDocument(byte[] pdfDocument)
{
//get the certificate
X509Certificate2 cert = GetCertificateByThumbprint(ConfigurationManager.AppSettings["PDFCertificateThumbprint"], true);
if (cert == null)
{
//no cert, dont do anything
return pdfDocument;
}
using (MemoryStream stream = new MemoryStream())
{
//set initial variables
X509Chain chain = X509Chain.Create();
PdfReader reader = new PdfReader(pdfDocument);
PdfStamper stamper = PdfStamper.CreateSignature(reader, stream, '\0', null, false);
PdfSignatureAppearance sap = stamper.SignatureAppearance;
PdfSignature signatureDictionary = new PdfSignature(PdfName.ADOBE_PPKMS, PdfName.ADBE_PKCS7_SHA1);
Dictionary<PdfName, int> reservedSpace = new Dictionary<PdfName, int>();
HashAlgorithm sha = new SHA1CryptoServiceProvider();
int read = 0;
byte[] buff = new byte[8192];
int csize = 0;
byte[] outc = null;
Stream sapStream = null;
byte[] pk = null;
PdfDictionary certificateDictionary = new PdfDictionary();
//build my certificate chain
chain.Build(cert);
//guess the size of the certificates and signature.
//we don't know for sure how big it really is going to be until after we hash it, so, we give it 4k for the signature
//and add the size of all the raw data of the certificates.
csize = 4096 + //4K for the signature
(from c in chain.ChainElements.OfType<X509ChainElement>()
select c.Certificate.RawData.Length).Sum(); //add up the size of each of the certificates and add it up
//certificate buffer
outc = new byte[csize];
//protect certain features of the document
stamper.SetEncryption(null,
Guid.NewGuid().ToByteArray(), //random password
PdfWriter.ALLOW_PRINTING |
PdfWriter.ALLOW_COPY |
PdfWriter.ALLOW_SCREENREADERS,
PdfWriter.ENCRYPTION_AES_256);
//////sign the document
sap.SetVisibleSignature(new Rectangle(1, 1, 1, 1), 1, "Signature field"); //we use a rectangle of 1,1,1,1 to hide it.
sap.SignDate = DateTime.Parse(cert.GetEffectiveDateString());
sap.Reason = "reason, i am"; //reason for being signed
sap.Location = "location, i am not"; //location it was signed
//set to use the wincer_signed, its a windows cert, not sure if this is needed.
sap.SetCrypto(null, null, null, PdfSignatureAppearance.WINCER_SIGNED);
//set some stuff in the signature dictionary. we dont need to set the appearance stuff because the signature is hidden.
signatureDictionary.Reason = sap.Reason;
signatureDictionary.Location = sap.Location;
signatureDictionary.Date = new PdfDate(sap.SignDate);
signatureDictionary.Name = cert.Subject; //certificate name
//set the appearance only stuff.
sap.Acro6Layers = true; //set to true to make adobe 6.x and higher happier with the layers.
sap.Render = PdfSignatureAppearance.SignatureRender.NameAndDescription; //what to show in the signature display. since its hidden, we dont care
// but if we do allow an image, then we have to put in an image.
//set the crypto dictionary
sap.CryptoDictionary = signatureDictionary;
//reserve some space for certs and signatures
reservedSpace[PdfName.CONTENTS] = csize * 2 + 2; //*2 because binary data is stored as hex strings (i think). +2 for end of field
sap.PreClose(reservedSpace); //actually reserve it
//build the signature
sapStream = sap.RangeStream;
while ((read = sapStream.Read(buff, 0, 8192)) > 0)
{
sha.TransformBlock(buff, 0, read, buff, 0);
}
sha.TransformFinalBlock(buff, 0, 0);
pk = SignMsg(sha.Hash, chain, false);
//put the certs and signature into the reserved buffer
Array.Copy(pk, 0, outc, 0, pk.Length);
//put the reserved buffer into the reserved space
certificateDictionary.Put(PdfName.CONTENTS, new PdfString(outc).SetHexWriting(true));
//write the signature
sap.Close(certificateDictionary);
//close the stamper and save it
stamper.Close();
//close the reader
reader.Close();
//return the saved pdf
return stream.GetBuffer();
}
}
public static byte[] SignMsg(Byte[] msg, X509Chain chain, bool detached)
{
// Place message in a ContentInfo object.
// This is required to build a SignedCms object.
ContentInfo contentInfo = new ContentInfo(msg);
// Instantiate SignedCms object with the ContentInfo above.
// Has default SubjectIdentifierType IssuerAndSerialNumber.
SignedCms signedCms = new SignedCms(contentInfo, detached);
// Formulate a CmsSigner object for the signer.
CmsSigner cmsSigner = new CmsSigner(chain.ChainElements[0].Certificate); //first cert in the chain is the signer cert
// Do the whole certificate chain. This way intermediate certificates get sent across as well.
cmsSigner.IncludeOption = X509IncludeOption.ExcludeRoot;
// Sign the CMS/PKCS #7 message. The second argument is
// needed to ask for the pin.
signedCms.ComputeSignature(cmsSigner, false);
// Encode the CMS/PKCS #7 message.
return signedCms.Encode();
}
}
}