Implement PAdES B-LT signature in a PDF with remote signing

Hi everyone,

I’m working on implementing a PDF signing solution where the actual signing operation happens remotely on a user’s device (App on a mobile phone), and I need to apply the resulting signature to a PDF using PAdES B-LT.

The Flow

Here’s what I’m trying to achieve:

My Server → Remote Signing Service → User's Device (signs with private key)
                                            ↓
My Server ← Signature + Certificate ← User's Device
     ↓
Apply to PDF

What I Have

After the user completes signing on their device, I receive:

  • The digital signature
  • The signer’s certificate

What I Need Help With

Can I use GemBox to apply an externally-created signature to a PDF?

Any guidance, code examples, or documentation pointers would be greatly appreciated!

Thanks in advance!

Hi,

GemBox.Pdf can be used to apply an externally created signature to a PDF, and there are two main workflows, depending on what the remote signing service and the user’s device expect as input and what they return as output:

  1. Input: hash to be signed by the user’s private key.
    Output: raw signature created by the user’s private key.
    GemBox.Pdf is then used to compose a CMS signature and embed it in the PDF file. In case of a PAdES signature, the signer’s certificate must be provided to calculate the input hash, because PAdES requires that the signing certificate digest is incorporated as part of a signed attribute to the CMS signature and because CMS signature would thus contain signed attributes, message digest is not just the digest of the PDF file being signed, but is calculated as described in the message digest calculation process.
    For your specific case, in which the signer’s certificate is returned at the end, together with the signature, and you want a PAdES signature, it means that you will first have to initiate a request for signing using a “dummy” input hash just for the purpose of retrieving the signer’s certificate. Then, with the PdfDelayOrReSignDigitalId class, you will be able to calculate the hash that will incorporate the hash of the PDF file to be signed and the hash of the signer’s certificate (PAdES requirement) and initiate a second request for signing using the proper input hash to retrieve the proper raw signature.
  2. Input: hash of the PDF file to be signed.
    Output: CMS signature that incorporates both the raw signature, the signer’s certificate, and any additional info.
    In this case, the remote signing service and the user’s device compose the CMS signature, and GemBox.Pdf is used to embed it in the PDF file.
    For your specific case, this workflow would still be effective even if the signer’s certificate is returned at the end, because the remote signing service and the user’s device are responsible for calculating the proper hash for the PAdES signature, calculating the raw signature of that hash, and composing the CMS signature with the raw signature and the signer’s certificate.

Option 1 is more common because the remote signing service and the user’s device don’t need to handle the details of PAdES signature hash calculation and CMS signature composition. They receive the input hash, sign it with the user’s private key, and return that raw signature. The code for this option is presented below:

using System;
using System.IO;
using System.Linq;
using GemBox.Pdf.Forms;
using GemBox.Pdf.Security;

namespace GemBox.Pdf.Test.Quick
{
    static partial class Program
    {
        static void Main()
        {
            // Initiate a signing request using a "dummy" hash to retrieve the signer's certificate required for calculating the hash for a PAdES signature.
            ExternalSign(new byte[32], PdfHashAlgorithm.SHA256, out byte[] signerCertificateBytes);
            var signerCertificate = new PdfCertificate(signerCertificateBytes);

            // Delay-sign the PDF file to retrieve the hash.
            var signedPdfFile = DelaySign(File.ReadAllBytes(Path.Combine(desktop, "Reading.pdf")), signerCertificate, out byte[] hash, out PdfHashAlgorithm hashAlgorithm);

            // Initiate a signing request using the proper hash to retrieve the proper signature. Signer certificate is ignored because we already retrieved it in the first call.
            var signature = ExternalSign(hash, hashAlgorithm, out _);

            // Re-sign the PDF file with the proper signature.
            ReSign(signedPdfFile, signerCertificate, signature, hash, hashAlgorithm);

            // Promote the signature from PAdES B-T to PAdES B-LT. This will also make the signature "LTV enabled".
            signedPdfFile = MakeLTVEnabled(signedPdfFile, signerCertificate);

            // Download the signed PDF file.
            File.WriteAllBytes(Path.Combine(desktop, "ReadingSigned.pdf"), signedPdfFile);
        }

        // This method simulates the call to the Remote Signing Service and the User's Device
        // using the code and files from the example https://www.gemboxsoftware.com/pdf/examples/c-sharp-vb-net-pdf-digital-signature/1102#external-signature
        private static byte[] ExternalSign(byte[] hash, PdfHashAlgorithm hashAlgorithm, out byte[] signerCertificate)
        {
            var desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);

            signerCertificate = File.ReadAllBytes(Path.Combine(desktop, "GemBoxRSA1024.crt"));

            using (var rsa = System.Security.Cryptography.RSA.Create())
            {
                rsa.FromXmlString(System.IO.File.ReadAllText(Path.Combine(desktop, "GemBoxRSA1024PrivateKey.xml")));

                return rsa.SignHash(
                    hash,
                    new System.Security.Cryptography.HashAlgorithmName(hashAlgorithm.ToString()),
                    System.Security.Cryptography.RSASignaturePadding.Pkcs1);
            }
        }

        private static byte[] DelaySign(byte[] fileToBeSigned, PdfCertificate signerCertificate, out byte[] hash, out PdfHashAlgorithm hashAlgorithm)
        {
            using (var inputStream = new MemoryStream(fileToBeSigned))
            using (var document = PdfDocument.Load(inputStream))
            {
                // Add an invisible signature field to the PDF document.
                // If you want to add a visible signature field, see the example https://www.gemboxsoftware.com/pdf/examples/c-sharp-vb-net-pdf-digital-signature/1102#visible-signature
                // The signature appearance must be defined in this step (delay-sign) and not in the next step (re-sign) because then content of the signed PDF file cannot be changed anymore
                // because that would invalidate the signature.
                var signatureField = document.Form.Fields.AddSignature();

                var digitalId = new PdfDelayOrReSignDigitalId(signerCertificate);

                var signer = new PdfSigner(digitalId)
                {
                    SignatureFormat = PdfSignatureFormat.CAdES,

                    // PAdES B-LT requires a time-stamp.
                    Timestamper = new PdfTimestamper("https://freetsa.org/tsr"),

                    SignatureLevel = PdfSignatureLevel.PAdES_B_LT
                };

                signatureField.Sign(signer);

                using (var outputStream = new MemoryStream())
                {
                    document.Save(outputStream);

                    hash = digitalId.Hash!;
                    hashAlgorithm = signer.HashAlgorithm;

                    return outputStream.ToArray();
                }
            }
        }

        private static void ReSign(byte[] signedPdfFile, PdfCertificate signerCertificate, byte[] signature, byte[] hash, PdfHashAlgorithm hashAlgorithm)
        {
            using (var stream = new MemoryStream(signedPdfFile))
            using (var document = PdfDocument.Load(stream))
            {
                var signatureField = (PdfSignatureField)document.Form.Fields.Single(f => f.FieldType == PdfFieldType.Signature);

                var digitalId = new PdfDelayOrReSignDigitalId(signerCertificate)
                {
                    Signature = signature
                };

                // Initialize the signer exactly in the same way as it is initialized in the delay-sign.
                var signer = new PdfSigner(digitalId)
                {
                    SignatureFormat = PdfSignatureFormat.CAdES,

                    // PAdES B-LT requires a time-stamp.
                    Timestamper = new PdfTimestamper("https://freetsa.org/tsr"),

                    SignatureLevel = PdfSignatureLevel.PAdES_B_LT,

                    HashAlgorithm = hashAlgorithm
                };

                signatureField.Sign(signer);

                if (!hash.AsSpan().SequenceEqual(digitalId.Hash))
                    throw new InvalidOperationException();
            }
        }

        private static byte[] MakeLTVEnabled(byte[] signedPdfFile, PdfCertificate signerCertificate)
        {
            using (var stream = new MemoryStream())
            {
                stream.Write(signedPdfFile, 0, signedPdfFile.Length);

                using (var document = PdfDocument.Load(stream))
                {
                    // Download validation-related information for the signer's certificate.
                    var signerValidationInfo = document.SecurityStore.GetValidationInfo(signerCertificate);

                    // Embed validation-related information for the signer's certificate in the PDF file.
                    // This will make the signature "LTV enabled".
                    document.SecurityStore.AddValidationInfo(signerValidationInfo);

                    // Save any changes done to the PDF file that were done since the last time Save was called.
                    document.Save();
                }

                return stream.ToArray();
            }
        }
    }
}

I hope this helps!

Regards,
Stipo

1 Like

What an awesome reply, thank you! I forgot to mention that the remote signing service offers an API to retrieve the certificate before I initiate the signing request, so this should all work perfectly. Thanks again for such a fantastic and detailed answer!