Search code examples
c#pdfitext7aspose

Signing pdf with external signed hash and certificate in C#


i need to sign a pdf with external services. Therefore i first need a function like

public string GetHashToSign(byte[] unsignedPdf, X509Certificate cert) 
{
    // generate and return a hash of the prepared pdf document
}

This hash will then be sent to an external webservice. After the hash is sent to the webserivce, the user will be redirected to the service where he/she need to login and confirm the signage. When the signage is confirmed, the user will be redirected to another page, where the signed hash will be downloaded and the signed pdf is constructed. At this point i need a function like

public byte[] SignDocument(byte[] unsignedPdf, X509Certificate cert, string signedHash) {
    // Put all things together and return signed pdf
}

Currently i trying to get this done with iText and/or Aspose. But i can't figure out, how i can decouple this two steps and execute them on different pages.

EDIT: Here is my solution, based on the answer by @Glenner003. May it help someone.

public class PreparedResponse
{
    public byte[] PreparedFile { get; set; }
    public byte[] BytesToSign { get; set; }
}
public class SignPdfWithIText7
{
    private static IBouncyCastleFactory BcFactory = BouncyCastleFactoryCreator.GetFactory();

    public PreparedResponse PreparePdfForSignage(
        byte[] pdf,
        string certificate,
        string digestAlgorithmOid,
        SignerProperties signerProperties)
    {

        byte[] preparedBytes;
        CMSContainer cmsContainer;
        using MemoryStream unsignePdfStream = new(pdf);
        using MemoryStream preparedPdfStream = new();
        using (PdfReader reader = new PdfReader(unsignePdfStream))
        using (MemoryStream outputStream = preparedPdfStream)
        {
            var parser = BcFactory.CreateX509CertificateParser();
            IX509Certificate[] certificateChain = [.. parser.ReadAllCerts(Convert.FromBase64String(certificate))];

            string digestAlgorithm = DigestAlgorithms.GetDigest(digestAlgorithmOid);

            // 1. Preparing the container to get a size estimate
            cmsContainer = new CMSContainer();
            cmsContainer.AddCertificates(certificateChain);
            cmsContainer.GetSignerInfo()
                .SetSigningCertificateAndAddToSignedAttributes(certificateChain[0], digestAlgorithmOid);
            // In a later version the default algorithm is extracted from the certificate
            cmsContainer.GetSignerInfo().SetSignatureAlgorithm(GetAlgorithmOidFromCertificate(certificateChain[0]));
            cmsContainer.GetSignerInfo().SetDigestAlgorithm(new AlgorithmIdentifier(digestAlgorithmOid));

            // Next to these required fields, we can add validation data and other signed or unsigned attributes with
            // the following methods:
            // cmsContainer.GetSignerInfo().SetCrlResponses();
            // cmsContainer.GetSignerInfo().SetOcspResponses();
            // cmsContainer.GetSignerInfo().AddSignedAttribute();
            // cmsContainer.GetSignerInfo().AddUnSignedAttribute();

            // Get the estimated size
            long estimatedSize = cmsContainer.GetSizeEstimation();

            var digest = BcFactory.CreateIDigest(digestAlgorithm);
            // Add enough space for the digest
            estimatedSize += digest.GetDigestLength() * 2L + 2;
            // Duplicate the size for conversion to hex
            estimatedSize *= 2;

            PdfTwoPhaseSigner signer = new PdfTwoPhaseSigner(reader, outputStream);
            signer.SetStampingProperties(new StampingProperties().UseAppendMode());

            // 2. Prepare the document by adding the signature field and getting the digest in return
            byte[] documentDigest = signer.PrepareDocumentForSignature(signerProperties, digestAlgorithm,
                    PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached, (int)estimatedSize, false);

            // 3. Add the digest to the CMS container, because this will be part of the items to be signed
            cmsContainer.GetSignerInfo().SetMessageDigest(documentDigest);
            outputStream.Close();
            preparedBytes = outputStream.ToArray();
        }

        // 4. This step is completely optional. Add the CMS container to the document
        // to avoid having to build it again, or storing it separately from the document
        PreparedResponse response;
        using (PdfReader reader = new PdfReader(new MemoryStream(preparedBytes)))
        using (PdfDocument document = new PdfDocument(reader))
        using (MemoryStream outputStream = new MemoryStream())
        {
            PdfTwoPhaseSigner.AddSignatureToPreparedDocument(document, signerProperties.GetFieldName(), outputStream,
                    cmsContainer.Serialize());
            outputStream.Close();
            response = new()
            {
                PreparedFile = outputStream.ToArray(),
                BytesToSign = cmsContainer.GetSerializedSignedAttributes()
            };
        }

        // 5. The serialized signed attributes is what actually needs to be signed
        // sometimes we have to create a digest from it, sometimes this needs to be sent as is.
        return response;
    }

    private AlgorithmIdentifier GetAlgorithmOidFromCertificate(IX509Certificate x509Certificate)
    {
        ITbsCertificateStructure tbsCert = BcFactory.CreateTBSCertificate(x509Certificate.GetTbsCertificate());
        if (tbsCert.GetSubjectPublicKeyInfo().GetAlgorithm().GetParameters() != null)
        {
            if (tbsCert.GetSubjectPublicKeyInfo().GetAlgorithm().GetParameters().IsNull())
            {
                return new AlgorithmIdentifier(tbsCert.GetSubjectPublicKeyInfo().GetAlgorithm().GetAlgorithm
                    ().GetId(), BcFactory.CreateDERNull());
            }
            return new AlgorithmIdentifier(tbsCert.GetSubjectPublicKeyInfo().GetAlgorithm().GetAlgorithm
                ().GetId(), tbsCert.GetSubjectPublicKeyInfo().GetAlgorithm().GetParameters().ToASN1Primitive());
        }
        return new AlgorithmIdentifier(tbsCert.GetSubjectPublicKeyInfo().GetAlgorithm().GetAlgorithm().GetId());
    }

    public byte[] SignPreparedPdf(byte[] preparedPdf, string fieldName, byte[] signature)
    {
        using (PdfReader reader = new PdfReader((new MemoryStream(preparedPdf))))
        using (PdfDocument document = new PdfDocument(reader))
        using (MemoryStream outputStream = new MemoryStream())
        {
            // 1. Read the documents CMS container
            SignatureUtil su = new SignatureUtil(document);
            PdfSignature sig = su.GetSignature(fieldName);
            PdfString encodedCMS = sig.GetContents();
            byte[] encodedCMSdata = encodedCMS.GetValueBytes();
            CMSContainer cmsContainer = new CMSContainer(encodedCMSdata);

            // 2. Add the signatureValue to the CMS
            cmsContainer.GetSignerInfo().SetSignature(signature);

            PdfTwoPhaseSigner.AddSignatureToPreparedDocument(document, fieldName, outputStream,
                    cmsContainer.Serialize());

            outputStream.Close();
            return outputStream.ToArray();
        }
    }
}

At all it is mostly a copy of the sample @Glenner003 provided to me, there are just some tweaks to fit my needs.

As i am not very common with pdf signage, just let me say, that i had to hash the BytesToSign before sending it to the webservice. But this may defer from service to service... i dont know.


Solution

  • For iText there is a sample for this.

    It shows the usage of the PdfTwoPhaseSigner class. The method PrepareDocumentForSignature performs what you want to do in GetHashToSign and AddSignatureToPreparedDocument performs the SignDocument part.

    The class PadesTwoPhaseSigningHelper works alike for pades compliant signatures and has its own sample.