Search code examples
c#bouncycastleitext7

How to insert .p7s byte array into PDF with iText7?


I am trying to insert a .p7s byte array info to signature field, I follow this image below:

PDF Document Structure Image

My steps:

Prepare Signature Container

the original PDF is "tmp/example.pdf" and the output of this part is "results/prepared.pdf"

PdfSigner signer = new PdfSigner(new PdfReader("tmp/example.pdf"), new FileStream("results/prepared.pdf", FileMode.Create), new StampingProperties().UseAppendMode());
signer.SetFieldName("Signature1");

PdfDocument _pdfDocument = new PdfDocument(new PdfReader("tmp/example.pdf"));

PdfSignatureAppearance sigAppearance = signer.GetSignatureAppearance();
sigAppearance
    .SetPageRect(new Rectangle(144, 144, 200, 100))
    .SetPageNumber(1)
    .SetContact("This is contact")
    .SetReason("This is reason")
    .SetLocation("This is location")
    .SetSignatureCreator("This is signature creator");

MyExternalSignatureContainer _container = new MyExternalSignatureContainer(PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached, _chain);

IExternalSignatureContainer container = _container; 
signer.SignExternalContainer(container, 8192);
byte[] _sb = _container.Signed_Bytes;

My External Signature Container Class

public byte[] Sign(Stream data)
{
    iText.Signatures.PdfPKCS7 _sgn = new iText.Signatures.PdfPKCS7(null, chain, "SHA256", false);
    byte[] _hash = iText.Signatures.DigestAlgorithms.Digest(data, "SHA256");
    byte[] _sh = _sgn.GetAuthenticatedAttributeBytes(_hash, PdfSigner.CryptoStandard.CMS,
        null, null);

    Signed_Bytes = _sh;

    return new byte[0]; ;
}

until this part, everything goes fine, I got "results/prepared.pdf" hash and sent to external signing service and got the .p7s

now I want to insert the .p7s byte[] into the signature value part of a PDF structure based on the image above.

I try to get the PdfDictionay's ByteRange info of "results/prepared.pdf" using code below, I expect to get injected .p7s to signature container of "results/prepared.pdf" at "results/injected.pdf"

PdfDocument pdfDocument = new PdfDocument(new PdfReader("results/prepared.pdf"), new PdfWriter("results/injected.pdf").SetSmartMode(true));

 SignatureUtil signatureUtil = new SignatureUtil(pdfDocument);
PdfSignature signature = signatureUtil.GetSignature("Signature1");

PdfDictionary _pd = signatureUtil.GetSignatureDictionary("Signature1");

Now I got the result of "_pd" as follow:

{<</ByteRange [0 107457 123843 2688 ] /ContactInfo This is contact /Contents 

my understanding (please correct me if I am wrong) is I should put .p7s byte array to 107457 as start position.

Try to inject .p7s into existing signature container

I try to make paddedSig array below and copy .p7s to it:

byte[] _p7s = System.IO.File.ReadAllBytes("tmp/example.p7s");
byte[] paddedSig = new byte[8192];
System.Array.Copy(_p7s, 0, paddedSig, 0, _p7s.Length);

and then try to put paddedSig to PdfDictionary as below:

PdfDictionary _pd = signatureUtil.GetSignatureDictionary("Signature1");
pd.Put(PdfName.Contents, new PdfString(paddedSig).SetHexWriting(true));

pdfDocument.Close();

a new PDF named "results/injected.pdf" generated, but:

Signature contains incorrect, unrecognized, corrupted or suspicious data. Support Information: SigDict /Contents illegal data

What did I miss? .. How to inject .p7s into prepared signature container?

To response Mkl's post:

What I don't understand is how to embed returning PKCS#7 byte into the pdf.. assume byte[] _p7s is the result from API_CALL

byte[] _p7s = API_CALL;
byte[] paddedSig = new byte[8192];
System.Array.Copy(_p7s, 0, paddedSig, 0, _p7s.Length);

and then try to put paddedSig to PdfDictionary as below:

PdfDictionary _pd = signatureUtil.GetSignatureDictionary("Signature1");
pd.Put(PdfName.Contents, new PdfString(paddedSig).SetHexWriting(true));

pdfDocument.Close();

the result is:

Signature contains incorrect, unrecognized, corrupted or suspicious data. Support Information: SigDict /Contents illegal data

Trying with .p7s file

I have an example.p7s file, I try to embed using the code provided as below:

 byte[] _p7s = System.IO.File.ReadAllBytes("tmp/example.p7s");

 private static void Embed_P7S(Org.BouncyCastle.X509.X509Certificate[] _chain, byte[] _p7s)
    {
        PdfDocument document = new PdfDocument(new PdfReader("results/example-prepared.pdf"));
        Stream output = new FileStream("results/example-prepared-signed.pdf", FileMode.Create);

        ExternalInjectingSignatureContainer container2 = new ExternalInjectingSignatureContainer(_p7s);

        PdfSigner.SignDeferred(document, "Signature1", output, container2);
    }
}


internal class ExternalInjectingSignatureContainer :IExternalSignatureContainer
{
    public ExternalInjectingSignatureContainer(byte[] signature)
    {
        Signature = signature;
    }

    public void ModifySigningDictionary(PdfDictionary signDic)
    {
    }

    public byte[] Sign(Stream data)
    {
        return Signature;
    }

    public byte[] Signature;
}

result:

example-prepared-signed.pdf no certificate shown (this is like the original pdf) but the size bigger than the original pdf

original.pdf is 105KB example-prepared is 124KB example-prepared-signed is 121KB

this is the .p7s


Solution

  • First of all, you do not have to split the signing process like you do. I've seen a lot of questions in which the developers want to do this, but strictly speaking it is not necessary (well, under the hood iText still will first create a prepared PDF and later inject the signature container, but it can be kept under the hood).

    Splitting the process only is necessary if the external signing service takes very long to create the signature and you cannot keep the PDF in memory for that time.

    I'll look into both variants here.

    Single pass signing

    If your external signing service returns the result (a full PKCS#7 signature container) fast enough, you should use this approach. The base code starts similar to yours:

    PdfSigner signer = new PdfSigner(new PdfReader("example.pdf"), new FileStream("example-signed.pdf", FileMode.Create), new StampingProperties().UseAppendMode());
    signer.SetFieldName("Signature1");
    
    PdfSignatureAppearance sigAppearance = signer.GetSignatureAppearance();
    sigAppearance
        .SetPageRect(new Rectangle(144, 144, 200, 100))
        .SetPageNumber(1)
        .SetContact("This is contact")
        .SetReason("This is reason")
        .SetLocation("This is location")
        .SetSignatureCreator("This is signature creator");
    
    ExternalServiceSignatureContainer container = new ExternalServiceSignatureContainer();
    
    signer.SignExternalContainer(container, 8192);
    

    The difference to your code is in the IExternalSignatureContainer implementation:

    public class ExternalServiceSignatureContainer : IExternalSignatureContainer
    {
        public void ModifySigningDictionary(PdfDictionary signDic)
        {
            signDic.Put(PdfName.Filter, PdfName.Adobe_PPKLite);
            signDic.Put(PdfName.SubFilter, PdfName.Adbe_pkcs7_detached);
        }
    
        public byte[] Sign(Stream data)
        {
            // Call your external signing service to create a CMS signature container
            // for the data in the InputStream and return that signature container
    
            [... see below ...]
        }
    }
    

    Depending on your API to access that external signing service the implementation of Sign differs. In each case I assume the API_CALL to return the result PKCS#7 signature container as byte array:

    • You may either be able to directly call it with the stream

      return YOUR_SIGNING_API_CALL_FOR_STREAM(data);
      
    • or with a byte[] generated from the stream contents

      return YOUR_SIGNING_API_CALL_FOR_ARRAY(StreamUtil.InputStreamToArray(data));
      

      as parameter,

    • or you may first have to hash the data yourself (e.g. as follows) and send your hash to the service.

      byte[] hash = DigestAlgorithms.Digest(data, DigestAlgorithms.SHA256);
      return YOUR_SIGNING_API_CALL_FOR_HASH(hash)
      

    The output of the signer already is the finalized, signed PDF.

    This essentially is the case already discussed in this answer.

    Two pass signing

    If your external signing service does not return the result (a full PKCS#7 signature container) fast enough (e.g. in case of batch signature APIs or in case of services waiting for some confirmation for a long time if necessary) or if on your side you implement the part before calling the signing service and the part thereafter in separate programs (some people indeed do so), you can use this approach.

    Again the base code starts similar to yours:

    PdfSigner signer = new PdfSigner(new PdfReader("example.pdf"), new FileStream("example-prepared.pdf", FileMode.Create), new StampingProperties().UseAppendMode());
    signer.SetFieldName("Signature1");
    
    PdfSignatureAppearance sigAppearance = signer.GetSignatureAppearance();
    sigAppearance
        .SetPageRect(new Rectangle(144, 144, 200, 100))
        .SetPageNumber(1)
        .SetContact("This is contact")
        .SetReason("This is reason")
        .SetLocation("This is location")
        .SetSignatureCreator("This is signature creator");
    
    ExternalEmptySignatureContainer container = new ExternalEmptySignatureContainer();
    
    signer.SignExternalContainer(container, 8192);
    
    byte[] dataToSign = container.Data;
    

    The ExternalEmptySignatureContainer now only provides the data to sign by the signing service later, it does not inject a signature container yet

    public class ExternalEmptySignatureContainer : IExternalSignatureContainer
    {
        public void ModifySigningDictionary(PdfDictionary signDic)
        {
            signDic.Put(PdfName.Filter, PdfName.Adobe_PPKLite);
            signDic.Put(PdfName.SubFilter, PdfName.Adbe_pkcs7_detached);
        }
    
        public byte[] Sign(Stream data)
        {
            // Store the data to sign and return an empty array
    
            [... see below ...]
    
            return new byte[0];
        }
    
        public byte[] Data;
    }
    

    Depending on your API to access that external signing service the implementation of Sign differs.

    • If your signing API expects the original data for signing, use a byte[] generated from the stream contents

      Data = StreamUtil.InputStreamToArray(data);
      
    • If your signing API expects the a hash of the original data for signing, calculate it like this from the stream contents

      Data = DigestAlgorithms.Digest(data, DigestAlgorithms.SHA256);
      

    The output of the signer is the intermediary, prepared PDF.

    The next step is to call the signing service and to retrieve the PKCS#7 signature container:

    byte[] signature = YOUR_SIGNING_API_CALL(dataToSign);
    

    Finally you inject that signature container into the prepared PDF:

    PdfDocument document = new PdfDocument(new PdfReader("example-prepared.pdf"));
    using (Stream output = new FileStream("example-prepared-signed.pdf", FileMode.Create))
    {
        ExternalInjectingSignatureContainer container2 = new ExternalInjectingSignatureContainer(signature);
    
        PdfSigner.SignDeferred(document, "Signature1", output, container2);
    }
    

    The IExternalSignatureContainer implementation merely injects the signature bytes:

    public class ExternalInjectingSignatureContainer : IExternalSignatureContainer
    {
        public ExternalInjectingSignatureContainer(byte[] signature)
        {
            Signature = signature;
        }
    
        public void ModifySigningDictionary(PdfDictionary signDic)
        {
        }
    
        public byte[] Sign(Stream data)
        {
            return Signature;
        }
    
        public byte[] Signature;
    }
    

    The output is the finalized, signed PDF.