Search code examples
c#.netpdfitextpades

Itext8 .net pdf signature with a trusted third party


I'm asking for your help today for a PaDES Baseline-LTA Profile PDF signing project with iText8 in C#.

I use a trusted third party API for sign a hash, i want to follow this workflow :

  • compute Data To Be Signed
  • compute Data To Be Signed Digest (sha256)
  • Sign Hash (trusted third-party API)
  • Create the signed document

The trusted third party advises me to generate the DTBS with DSS (https://github.com/esig/dss), but I'm working in C# and don't want to use Java.

I've tried several approaches, including this one: https://github.com/itext/itext-publications-signatures-java/blob/develop/src/test/java/com/itextpdf/samples/signatures/chapter04/C4_07_ClientServerSigning.java

Honestly, I feel like I'm totally off the mark ...

Is it possible to create a DTBS with BouncyCastle without the help of DSS?

What method should I use with iText to achieve this type of signature?

Thanks in advance for your help.

EDIT :

pdf generated : https://drive.google.com/file/d/1rnY7tuXPJkzSgLFasQ6R7I8BDQ1RWwM_/view?usp=sharing

i work with itext 8.0.3

Test code :


    using Flurl;
    using Flurl.Http;
    using iText.Bouncycastle.X509;
    using iText.Commons.Bouncycastle.Cert;
    using iText.Kernel.Geom;
    using iText.Kernel.Pdf;
    using iText.Signatures;
    using Org.BouncyCastle.X509;
    using SignatureCerteurope.Certeurope;
    using SignatureCerteurope.ComponentC.Json;
    using System.Security.Cryptography.X509Certificates;
    
    string DEST = "SIGNED.PDF";
    string SRC = "TEST.PDF";
    
    var sign = new SignatureAPI();
    var cert = await sign.GetCertificate();
    
    string base64Certificate = cert.pem
                    .Replace("-----BEGIN CERTIFICATE-----\n", "")
                    .Replace("\n-----END CERTIFICATE-----\n", "");
    
    byte[] certificateBytes = Convert.FromBase64String(base64Certificate);
    
    Org.BouncyCastle.X509.X509CertificateParser parser = new Org.BouncyCastle.X509.X509CertificateParser();
    
    Org.BouncyCastle.X509.X509Certificate x509cert = parser.ReadCertificate(certificateBytes);
    
    iText.Commons.Bouncycastle.Cert.IX509Certificate[] certificateWrappers = new IX509Certificate[1];
    
    certificateWrappers[0] = new X509CertificateBC(x509cert);
    
    new C4_07_ClientServerSigning().Sign(SRC, DEST, certificateWrappers, PdfSigner.CryptoStandard.CMS, "test reason", "test location");
    
    public class C4_07_ClientServerSigning
    {
        public void Sign(string src, string dest, IX509Certificate[] chain, PdfSigner.CryptoStandard subfilter, string reason, string location)
        {
            PdfReader reader = new PdfReader(src);
            PdfSigner signer = new PdfSigner(reader, new FileStream(dest, FileMode.Create), new StampingProperties());
    
            // Create the signature appearance
            Rectangle rect = new Rectangle(36, 648, 200, 100);
            signer.SetReason(reason)
                    .SetLocation(location)
                    .SetPageRect(rect)
                    .SetPageNumber(1)
                    .SetFieldName("sig");
    
            IExternalSignature signature = new ServerSignature();
    
            signer.SignDetached(signature, chain, null, null, null, 0, subfilter);
        }
    
        public class ServerSignature : IExternalSignature
        {
            public string GetDigestAlgorithmName()
            {
                return DigestAlgorithms.SHA256;
            }
    
            public string GetSignatureAlgorithmName()
            {
                return "RSA";
            }
    
            public ISignatureMechanismParams GetSignatureMechanismParameters()
            {
                return null;
            }
    
            public byte[] Sign(byte[] message)
            {
                HashPDF json = new HashPDF
                {
                    orderRequestId = 444928,
                    hash = new List<HashItem>()
                };
    
                HashItem unHash = new HashItem
                {
                    hash = Convert.ToBase64String(message)
                };
                json.hash.Add(unHash);
                var sign = new SignatureAPI();
                HashSignatureRequestCollectDTO jsonres = sign.SignHash("444928", json).GetAwaiter().GetResult();
                return Convert.FromBase64String(jsonres.Signatures[0].Signature);
            }
        }
    }
    public class SignatureAPI
    {
        private const int _ORDER_REQUEST_ID = 444928;
        private const string _API_BASE_URL = "SIGNURL";
        private const string _SERIAL_NUMBER_CERTIFICATE = "17E1BC";
    
        public SignatureAPI()
        {
            X509Store store = new X509Store(StoreName.My);
            store.Open(OpenFlags.ReadOnly);
    
            X509Certificate2Collection col = store.Certificates.Find(X509FindType.FindBySerialNumber, _SERIAL_NUMBER_CERTIFICATE, false);
        
            X509Certificate2 certificate = col[0];
    
            FlurlHttp.Clients.WithDefaults(builder => builder.ConfigureInnerHandler(h => { h.ClientCertificates.Add(certificate); }));
        }
    
        public async Task<HashSignatureRequestCollectDTO> SignHash(string id, HashPDF json)
        {
            string apiUrl = "SIGNHASH";
            var response = await apiUrl.SetQueryParam("orderRequestId", id).PostJsonAsync(json);
    
            var result = await response.GetStringAsync();
            HashSignatureRequestCollectDTO jsonRes = result.ConvertJsonStringWithTypeToObject<HashSignatureRequestCollectDTO>();
    
            return jsonRes;
        }
    
        public async Task<CertificateJson> GetCertificate()
        {
           string apiUrl = "GETCERT";
           return await apiUrl.SetQueryParam("orderRequestId", _ORDER_REQUEST_ID).GetJsonAsync<CertificateJson>();
        }
    }
    
    public class SignatureResponse
    {
        public int SignatureRequestId { get; set; }
        public string Status { get; set; }
        public string Hash { get; set; }
        public string Signature { get; set; }
        public string ErrorMessage { get; set; }
    }
    
    public class HashSignatureRequestCollectDTO
    {
        public int OrderRequestId { get; set; }
        public object ExternalOrderRequestId { get; set; }
        public List<SignatureResponse> Signatures { get; set; }
    }
    
    public class HashItem
    {
        public string hash { get; set; }
    }
    
    public class HashPDF
    {
        public int orderRequestId { get; set; }
        public List<HashItem> hash { get; set; }
    }

EDIT 2:

I tried another approach as recommended by mkl with PdfPadesSigner . Here is my code:

public class C4_07_ClientServerSigningPdfPades
{
    public void Sign(string src, string dest, IX509Certificate[] chain, PdfSigner.CryptoStandard subfilter, string reason, string location)
    {
        PdfReader reader = new PdfReader(src);
        PdfPadesSigner padesSigner = new PdfPadesSigner(reader, new FileStream(dest, FileMode.Create));
        
        SignerProperties properties = new SignerProperties();
        properties.SetFieldName("signatureField");

        IExternalSignature signature = new ServerSignature();
        TSAClientBouncyCastle tsa = new TSAClientBouncyCastle("https://rfc3161.ai.moda");
        padesSigner.SignWithBaselineLTAProfile(new SignerProperties(), chain, signature, tsa);
    }

    public class ServerSignature : IExternalSignature
    {
        public string GetDigestAlgorithmName()
        {
            return DigestAlgorithms.SHA256;
        }

        public string GetSignatureAlgorithmName()
        {
            return "RSA";
        }

        public ISignatureMechanismParams GetSignatureMechanismParameters()
        {
            return null;
        }

        public byte[] Sign(byte[] message)
        {
            HashPDF json = new HashPDF
            {
                orderRequestId = 444928,
                hash = new List<HashItem>()
            };

            byte[] digestValue;
            using (SHA256 sha256 = SHA256.Create())
            {
                digestValue = sha256.ComputeHash(message);
            }

            // Série d'octets à ajouter
            byte[] additionalByte = { 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20 };

            // Concaténation des deux tableaux de bytes
            byte[] DigestInfo = new byte[digestValue.Length + additionalByte.Length];

            Buffer.BlockCopy(additionalByte, 0, DigestInfo, 0, additionalByte.Length);
            Buffer.BlockCopy(digestValue, 0, DigestInfo, additionalByte.Length, digestValue.Length);

            HashItem digestInfoHashItem = new HashItem
            {
                hash = Convert.ToBase64String(DigestInfo)
            };
            json.hash.Add(digestInfoHashItem);

            var sign = new SignatureAPI();
            HashSignatureRequestCollectDTO jsonres = sign.SignHash("444928", json).GetAwaiter().GetResult();
            return Convert.FromBase64String(jsonres.Signatures[0].Signature);
        }
    }
}

here is the generated document: https://drive.google.com/file/d/1V7FHZZGA2bKDkG0zusJSL7CIvSZwauDj/view?usp=sharing

I tried to validate the consistency of the signature with the tool: https://signatures-conformance-checker.etsi.org/

It systematically reports: Error while trying to get the concrete factory suitable for the suffix. No factory was found for the suffix: PDF

Can you explain why?

Edit 3: the signature doesn't get through and I get a NullReferenceException

   à iText.Bouncycastle.Asn1.Ocsp.BasicOcspResponseBC.GetCerts()
   à iText.Signatures.SignUtils.GetCertsFromOcspResponse(IBasicOcspResponse ocspResp)
   à iText.Signatures.LtvVerification.AddRevocationDataForOcspCert(Byte[] ocspEnc, IX509Certificate signingCert, IOcspClient ocsp, ICrlClient crl, Level level, CertificateInclusion certInclude, CertificateOption certOption, ValidationData validationData, ICollection`1 processedCerts)
   à iText.Signatures.LtvVerification.AddRevocationDataForCertificate(IX509Certificate signingCert, IX509Certificate[] certificateChain, IX509Certificate cert, IOcspClient ocsp, ICrlClient crl, Level level, CertificateInclusion certInclude, CertificateOption certOption, ValidationData validationData, ICollection`1 processedCerts)
   à iText.Signatures.LtvVerification.AddRevocationDataForChain(IX509Certificate signingCert, IX509Certificate[] certChain, IOcspClient ocsp, ICrlClient crl, Level level, CertificateInclusion certInclude, CertificateOption certOption, ValidationData validationData, ICollection`1 processedCerts)
   à iText.Signatures.LtvVerification.AddVerification(String signatureName, IOcspClient ocsp, ICrlClient crl, CertificateOption certOption, Level level, CertificateInclusion certInclude)
   à iText.Signatures.PdfPadesSigner.PerformLtvVerification(PdfDocument pdfDocument, IList`1 signatureNames, RevocationDataNecessity revocationDataNecessity)
   à iText.Signatures.PdfPadesSigner.SignWithBaselineLTAProfile(SignerProperties signerProperties, IX509Certificate[] chain, IExternalSignature externalSignature, ITSAClient tsaClient)

Edit 4 the 2 exceptions below rise, randomly

System.IO.IOException
  HResult=0x80131620
  Message=Timestamp token estimation size is not large enough to accommodate the entire timestamp token. Timestamp token estimation size is: 5441 bytes, however real timestamp token size is: 5943 bytes.
  Source=itext.sign
  Arborescence des appels de procédure :
   à iText.Signatures.PdfSigner.Timestamp(ITSAClient tsa, String signatureName)
   à iText.Signatures.PdfPadesSigner.PerformTimestamping(PdfDocument document, Stream outputStream, ITSAClient tsaClient)
   à iText.Signatures.PdfPadesSigner.SignWithBaselineLTAProfile(SignerProperties signerProperties, IX509Certificate[] chain, IExternalSignature externalSignature, ITSAClient tsaClient)



System.NullReferenceException
  HResult=0x80004003
  Message=Object reference not set to an instance of an object.
  Source=itext.bouncy-castle-adapter
  Arborescence des appels de procédure :
   à iText.Bouncycastle.Asn1.Ocsp.BasicOcspResponseBC.GetCerts()
   à iText.Signatures.SignUtils.GetCertsFromOcspResponse(IBasicOcspResponse ocspResp)
   à iText.Signatures.LtvVerification.AddRevocationDataForOcspCert(Byte[] ocspEnc, IX509Certificate signingCert, IOcspClient ocsp, ICrlClient crl, Level level, CertificateInclusion certInclude, CertificateOption certOption, ValidationData validationData, ICollection`1 processedCerts)
   à iText.Signatures.LtvVerification.AddRevocationDataForCertificate(IX509Certificate signingCert, IX509Certificate[] certificateChain, IX509Certificate cert, IOcspClient ocsp, ICrlClient crl, Level level, CertificateInclusion certInclude, CertificateOption certOption, ValidationData validationData, ICollection`1 processedCerts)
   à iText.Signatures.LtvVerification.AddRevocationDataForChain(IX509Certificate signingCert, IX509Certificate[] certChain, IOcspClient ocsp, ICrlClient crl, Level level, CertificateInclusion certInclude, CertificateOption certOption, ValidationData validationData, ICollection`1 processedCerts)
   à iText.Signatures.LtvVerification.AddVerification(String signatureName, IOcspClient ocsp, ICrlClient crl, CertificateOption certOption, Level level, CertificateInclusion certInclude)
   à iText.Signatures.PdfPadesSigner.PerformLtvVerification(PdfDocument pdfDocument, IList`1 signatureNames, RevocationDataNecessity revocationDataNecessity)
   à iText.Signatures.PdfPadesSigner.SignWithBaselineLTAProfile(SignerProperties signerProperties, IX509Certificate[] chain, IExternalSignature externalSignature, ITSAClient tsaClient)

Edit 5 :

The certificates present in processedCerts when the AddRevocationDataForOcspCert function is called varies from test to test, but the tests fail when the certificates below are present in the list :


SerialNumber: 7002784885422699301467740558332354838
IssuerDN: C=US,O=DigiCert\, Inc.,CN=DigiCert Trusted G4 RSA4096 SHA256 TimeStamping CA
Start Date: 14/07/2023 00:00:00
Final Date: 13/10/2034 23:59:59
SubjectDN: C=US,O=DigiCert\, Inc.,CN=DigiCert Timestamp 2023
Public Key: Org.BouncyCastle.Crypto.Parameters.RsaKeyParameters
Signature Algorithm: SHA-256withRSA

----

SerialNumber: 76161378635050837295056099126206603988
IssuerDN: C=GB,ST=Greater Manchester,L=Salford,O=Sectigo Limited,CN=Sectigo RSA Time Stamping CA
Start Date: 03/05/2023 00:00:00
Final Date: 02/08/2034 23:59:59
SubjectDN: C=GB,ST=Manchester,O=Sectigo Limited,CN=Sectigo RSA Time Stamping Signer #4
Public Key: Org.BouncyCastle.Crypto.Parameters.RsaKeyParameters
Signature Algorithm: SHA-384withRSA

Solution

  • Decrypting the signature bytes in the signature container in your example PDF reveals that they do not contain a DigestInfo structure wrapping the hash of the signed attributes (as required by RFC 8017 - PKCS #1: RSA Cryptography Specifications Version 2.2 - and its precursor RFCs) but the signed attributes themselves.

    So while you in your code assume that your remote signature service will hash and wrap the bytes you send to it, that service actually does not but expects you to do so.

    This discrepancy actually is obvious in this line in your ServerSignature.Sign implementation:

    json.hash.Add(unHash);
    

    Here you add something un-hashed to a collection that by its name should contain some hashed items.

    To fix this, you should replace

    HashItem unHash = new HashItem
    {
        hash = Convert.ToBase64String(message)
    };
    json.hash.Add(unHash);
    

    by something like

    byte[] digestValue = <<Calculate SHA-256 hash of message>>;
    byte[] digestInfo = <<Wrap digestValue in a DigestInfo object>>;
    HashItem digestInfoHashItem = new HashItem
    {
        hash = Convert.ToBase64String(digestInfo)
    };
    json.hash.Add(digestInfoHashItem);
    

    To Wrap digestValue in a DigestInfo object, RFC 8017 offers a short cut:

    For the nine hash functions mentioned in Appendix B.1, the DER encoding T of the DigestInfo value is equal to the following:

    ...

    SHA-256: (0x)30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 || H.

    I.e. you only have to prepend the bytes 30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 to your digestValue bytes.


    As an aside, you mention this is part of a PaDES Baseline-LTA Profile PDF signing project. In that case you might want to check whether you can use the new PdfPadesSigner iText class.