Search code examples
c#pdfdigital-signatureitext7

Digitally sign a pdf file in 2 steps with iText 7, result is corrupted file


I'm trying to digitally sign a PDF file in two steps: compute hash, sign hash, and then merge the signed hash to the output file. The library that I'm using is iText 7.

However, the result is a corrupted file and cannot be opened.

What is wrong?

First I'm computing the hash and sign the input file with an empty signature container:

public static byte[] ComputeHash(string source, string temp, X509Certificate[] chains, string reason, string location, int qtySigns, int pageNumber)
{
    IList<ICrlClient> crlList = new List<ICrlClient>();
    crlList.Add(new CrlClientOnline(chains));

    using (var reader = new PdfReader(source))
    {
        using (var os = new FileStream(temp, FileMode.OpenOrCreate, FileAccess.Write))
        {
            var signer = new PdfSigner(reader, os, new StampingProperties().UseAppendMode());

            var signatureAppearance = signer.GetSignatureAppearance();
            signatureAppearance
                .SetReason(reason)
                .SetLocation(location)
                .SetPageNumber(pageNumber)
                .SetRenderingMode(PdfSignatureAppearance.RenderingMode.NAME_AND_DESCRIPTION)
                .SetCertificate(chains[0])
                .SetPageRect(new iText.Kernel.Geom.Rectangle(36, 20, 144, 53));

            var container = new EmptySignatureContainer(PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached);
            signer.SignExternalContainer(container, 8192);

            byte[] hash = container.Hash;

            return hash;
        }
    }
}

Here is the container:

public class EmptySignatureContainer : IExternalSignatureContainer
{
    private PdfDictionary _sigDic;
    public byte[] Hash;

    public byte[] Sign(Stream data)
    {
        string hashAlgorithm = "SHA256";

        try
        {
            this.Hash = DigestAlgorithms.Digest(data, hashAlgorithm);
        }
        catch (IOException e)
        {
            throw new GeneralSecurityException("EmptySignatureContainer signing exception", e);
        }

        return new byte[0];
    }

    public void ModifySigningDictionary(PdfDictionary signDic)
    {
        signDic.PutAll(_sigDic);
    }

    public EmptySignatureContainer(PdfName filter, PdfName subFilter)
    {
        _sigDic = new PdfDictionary();
        _sigDic.Put(PdfName.Filter, filter);
        _sigDic.Put(PdfName.SubFilter, subFilter);
    }
}

After that, I get the authenticated attribute bytes and compute its hash:

var hash = ComputeHash(inputFile, tempFile, chain, "sign-reason", "",  1, 1);
PdfPKCS7 sgn = new PdfPKCS7(null, chain, "SHA256", false);
var authenAttrBytes = sgn.GetAuthenticatedAttributeBytes(hash, PdfSigner.CryptoStandard.CMS, null, null);

var computedHash = SHA256Managed.Create().ComputeHash(authenAttrBytes);`

The computedHash then will be sent to the hsm server to sign. The signed hash then will be added back to the temporary file as following code:

Sign(tempFile, outputFile, chain, authenAttrBytes, signedHash);

And here is the Sign method:

public static void Sign(string tempFile, string targetFile, X509Certificate[] chains, byte[] hash, byte[] signedHash)
{
    using (PdfReader reader = new PdfReader(tempFile))
    {
        using (FileStream outStream = System.IO.File.OpenWrite(targetFile))
        {
            var signedContainer = new SignedSignatureContainer(hash, signedHash, chains);
            var signer = new PdfSigner(reader, outStream, new StampingProperties());
            signer.SignExternalContainer(signedContainer, 8192);
        }
    }
}

The Signed container as following:

public  class SignedSignatureContainer : IExternalSignatureContainer
{
    public byte[] Hash { get; set; }
    public byte[] SignedHash { get; set; }
    public X509Certificate[] CertChains { get; set; }

    public SignedSignatureContainer(byte[] hash, byte[] signedHash, X509Certificate[] certCertChains)
    {
        this.Hash = hash;
        this.SignedHash = signedHash;
        this.CertChains = certCertChains;
    }

    public byte[] Sign(Stream data)
    {
        var sgn = new PdfPKCS7(null,  CertChains, "SHA256", false);
        sgn.SetExternalDigest(this.SignedHash, null, "RSA");
        return sgn.GetEncodedPKCS7(this.Hash, CryptoStandard.CMS, null, null, null );
    }

    public void ModifySigningDictionary(PdfDictionary signDic)
    {
        signDic.Put(PdfName.Filter, PdfName.Adobe_PPKLite);
        signDic.Put(PdfName.SubFilter, PdfName.Adbe_pkcs7_detached);
    }
}

Solution

  • In your Sign method you use the PdfSigner method SignExternalContainer:

    var signedContainer = new SignedSignatureContainer(hash, signedHash, chains);
    var signer = new PdfSigner(reader, outStream, new StampingProperties());
    signer.SignExternalContainer(signedContainer, 8192);
    

    But that does not fill in the originally prepared signature! Instead this creates another signature field and fills it with a signature for the original one.

    Use the static PdfSigner method SignDeferred instead:

    /// <summary>Signs a PDF where space was already reserved.</summary>
    /// <param name="document">the original PDF</param>
    /// <param name="fieldName">the field to sign. It must be the last field</param>
    /// <param name="outs">the output PDF</param>
    /// <param name="externalSignatureContainer">
    /// the signature container doing the actual signing. Only the
    /// method ExternalSignatureContainer.sign is used
    /// </param>
    public static void SignDeferred(PdfDocument document, String fieldName, Stream outs,
         IExternalSignatureContainer externalSignatureContainer)