5

I am testing the iText 7.1.2.0 library to sign pdf files, using a digital certificate or smart card (X509Certificate2) in a C # project. But I'm getting this error when I try to create the IExternalSignature.

enter image description here

According to the documentation found (here, here and here), the way to achieve this process is using the BouncyCastle library that allows extracting the primary keys from the digital certificate, however, it is giving me an error and I can not find another way to do it. In the documentation (here) they are created from .pfx files, but for this case, I need to take the primary keys directly from the certificate in the card reader. In previous versions of iText, it's allowed the creation with the following command:

IExternalSignature externalSignature = new X509Certificate2Signature(Certificate, "SHA-1");

But in version 7 it is no longer available and in the documentation I do not see how to do it.

Someone has used iText 7 and has been able to sign using an X509Certificate2 that knows the correct way to create the IExternalSignature?

This is the code that I am using:

public void SignPDF(string source, string target, X509Certificate2 certificate, string reason, string location, bool addVisibleSign, bool addTimeStamp, string strTSA, int qtySigns, int pageNumber)
    {
        try
        {
            Org.BouncyCastle.X509.X509Certificate vert = Org.BouncyCastle.Security.DotNetUtilities.FromX509Certificate(certificate);

            X509CertificateParser objCP = new X509CertificateParser();
            Org.BouncyCastle.X509.X509Certificate[] objChain = new Org.BouncyCastle.X509.X509Certificate[] { objCP.ReadCertificate(certificate.RawData) };

            IList<ICrlClient> crlList = new List<ICrlClient>();
            crlList.Add(new CrlClientOnline(objChain));

            PdfReader objReader = new PdfReader(source);
            PdfSigner objStamper = new PdfSigner(objReader, new FileStream(target, FileMode.Create), false);

            ITSAClient tsaClient = null;
            IOcspClient ocspClient = null;

            if (addTimeStamp)
            {
                OCSPVerifier ocspVerifier = new OCSPVerifier(null, null);
                ocspClient = new OcspClientBouncyCastle(ocspVerifier);
                tsaClient = new TSAClientBouncyCastle(strTSA);
            }

            PdfSignatureAppearance signatureAppearance = objStamper.GetSignatureAppearance();
            signatureAppearance.SetReason(reason);
            signatureAppearance.SetLocation(location);
            signatureAppearance.SetPageNumber(pageNumber);
            signatureAppearance.SetRenderingMode(PdfSignatureAppearance.RenderingMode.NAME_AND_DESCRIPTION);

            if (addVisibleSign && qtySigns == 1)
                signatureAppearance.SetPageRect(new iText.Kernel.Geom.Rectangle(36, 20, 144, 53)).SetPageNumber(pageNumber);
            else if (addVisibleSign && qtySigns == 2)
                signatureAppearance.SetPageRect(new iText.Kernel.Geom.Rectangle(160, 20, 268, 53)).SetPageNumber(pageNumber);
            else if (addVisibleSign && qtySigns == 3)
                signatureAppearance.SetPageRect(new iText.Kernel.Geom.Rectangle(284, 20, 392, 53)).SetPageNumber(pageNumber);
            else if (addVisibleSign && qtySigns == 4)
                signatureAppearance.SetPageRect(new iText.Kernel.Geom.Rectangle(408, 20, 516, 53)).SetPageNumber(pageNumber);

            var pk = Org.BouncyCastle.Security.DotNetUtilities.GetKeyPair(certificate.PrivateKey).Private;

            IExternalSignature externalSignature = new PrivateKeySignature(pk, "SHA-1");
            objStamper.SignDetached(externalSignature, objChain, crlList, ocspClient, tsaClient, 0, PdfSigner.CryptoStandard.CMS);

            if (objReader != null)
            {
                objReader.Close();
            }
        }
        catch (Exception ex)
        {
            result.error = true;
            result.errorMessage += "Error: " + ex.Message;
        }
    }

Thanks!

Amedee Van Gasse
  • 7,280
  • 5
  • 55
  • 101
Josh S.M.
  • 63
  • 1
  • 6

2 Answers2

7

I do not believe that class was ported to iText 7- it is just a wrapper class.

You can see how to create a custom IExternalSignatureContainer in this example

Note that the source for the iText 5 X509Certificate2Signature can be found here

So something like this:

public class X509Certificate2Signature: IExternalSignature {
    private String hashAlgorithm;
    private String encryptionAlgorithm;
    private X509Certificate2 certificate;

    public X509Certificate2Signature(X509Certificate2 certificate, String hashAlgorithm) {
        if (!certificate.HasPrivateKey)
            throw new ArgumentException("No private key.");
        this.certificate = certificate;
        this.hashAlgorithm = DigestAlgorithms.GetDigest(DigestAlgorithms.GetAllowedDigest(hashAlgorithm));
        if (certificate.PrivateKey is RSACryptoServiceProvider)
            encryptionAlgorithm = "RSA";
        else if (certificate.PrivateKey is DSACryptoServiceProvider)
            encryptionAlgorithm = "DSA";
        else
            throw new ArgumentException("Unknown encryption algorithm " + certificate.PrivateKey);
    }

    public virtual byte[] Sign(byte[] message) {
        if (certificate.PrivateKey is RSACryptoServiceProvider) {
            RSACryptoServiceProvider rsa = (RSACryptoServiceProvider) certificate.PrivateKey;
            return rsa.SignData(message, hashAlgorithm);
        }
        else {
            DSACryptoServiceProvider dsa = (DSACryptoServiceProvider) certificate.PrivateKey;
            return dsa.SignData(message);
        }
    }

    public virtual String GetHashAlgorithm() {
        return hashAlgorithm;
    }

    public virtual String GetEncryptionAlgorithm() {
        return encryptionAlgorithm;
    }
}

Will replicate the class' function in iText 7. How to use the class is shown here in my first link, though you most likely will be using the signDetached() method instead of the signDeffered() method.

Jon Reilly
  • 876
  • 1
  • 7
  • 10
0

The certificate.PrivateKey in my experience is not reliable when you're dealing with non-RSA key pairs. For example a certificate that has an ECDsa key-pair will throw a NotSupportedException("The certificate key algorithm is not supported") on calling .PrivateKey, don't know why (actually here's why, also probably useful). So I implemented a (little janky) workaround to retrieve the private key, and create an IExternalSignature out of it. Since the code has no knowledge of what type of key a certificate has, and I couldn't be bothered to process the other properties to find out, I wrote a loop that tries for a generic, RSA, DSA and ECDsa key extraction. The generic works for RSA too, but since X509Certificate2 has a GetRSAPrivateKey() method, I made a switch case for it just in case.

#nullable enable
private (IExternalSignature? signature, IDisposable? key) ExtractPrivateKey(X509Certificate2 certificate)
{
  IExternalSignature? signature = null;
  bool foundPk = false;
  int attempt = 0;
  IDisposable? key = null;
  
  while (foundPk is false)
  {
      try
      {
          switch (attempt)
          {
              case 0:
                  AsymmetricAlgorithm? pk = certificate.PrivateKey; //throws exception when certificate has an ECDsa key
                  signature = new PrivateKeySignature(DotNetUtilities.GetKeyPair(pk).Private, _digestAlgorithm);
                  key = pk;
                  break;
              case 1:
                  RSA rsa = certificate.GetRSAPrivateKey() ?? throw new NotSupportedException("RSA private key is null on the certificate.");
                  signature = new PrivateKeySignature(DotNetUtilities.GetRsaKeyPair(rsa).Private, _digestAlgorithm);
                  key = rsa;
                  break;
              case 2:
                  DSA dsa = certificate.GetDSAPrivateKey() ?? throw new NotSupportedException("Dsa private key is null on the certificate.");
                  signature = new PrivateKeySignature(DotNetUtilities.GetDsaKeyPair(dsa).Private, _digestAlgorithm);
                  key = dsa;
                  break;
              case 3:
                  ECDsa ecdsa = certificate.GetECDsaPrivateKey() ?? throw new NotSupportedException("ECDsa private key is null on the certificate.");
                  signature = new EcdsaSignature(ecdsa, _digestAlgorithm);
                  key = ecdsa;
                  break;
          }
          foundPk = true;
      }
      catch (Exception e)
      {
          _logger.LogDebug(e, $"Private key extraction ran into an exception. Attempt (zero-based): {attempt}");
      }

      attempt++;
  }

  if (signature == null)
  {
      throw new NotSupportedException(
          $"The private key of the certificate could not be retrieved for signing. {JsonConvert.SerializeObject(certificate.SubjectName)}");
  }

  return (signature, key);
}

As you can see iText 7 has a handy utilities class, that contains a lot of util functions ported from the Java version, but for some reason only a lot, and not all of them. It doesn't support ECDsa key pairs, so I had to implement my own version of that. For completeness that class is included below.

#nullable enable
using System;
using System.Security.Cryptography;
using iText.Signatures;
public class EcdsaSignature : IExternalSignature
{
    private readonly string _encryptionAlgorithm;
    private readonly string _hashAlgorithm;
    private readonly ECDsa _pk;
    private const string EcdsaEncryptionAlgorithm = "ECDSA";

    public EcdsaSignature(ECDsa? pk, string hashAlgorithm)
    {
        _pk = pk ?? throw new ArgumentNullException(nameof(pk), "ECDSA private key cannot be null.");
        _hashAlgorithm = DigestAlgorithms.GetDigest(DigestAlgorithms.GetAllowedDigest(hashAlgorithm));
        _encryptionAlgorithm = EcdsaEncryptionAlgorithm;
    }

    /// <summary>
    ///     <inheritDoc />
    /// </summary>
    public virtual string GetEncryptionAlgorithm()
    {
        return _encryptionAlgorithm;
    }

    /// <summary>
    ///     <inheritDoc />
    /// </summary>
    public virtual string GetHashAlgorithm()
    {
        return _hashAlgorithm;
    }

    /// <summary>
    ///     <inheritDoc />
    /// </summary>
    /// <remarks>
    ///     <see href="https://stackoverflow.com/a/67255440/6389395"> RFC 5480 format.</see>
    /// </remarks>
    public virtual byte[] Sign(byte[] message)
    {
        return _pk.SignData(message, new HashAlgorithmName(_hashAlgorithm), DSASignatureFormat.Rfc3279DerSequence);
    }
}

Also note that the ExtractPrivateKey() method returns the key as an IDisposable as well, this is so it can be disposed of after the signature is done. I could have made a public property in EcdsaSignature, but iText's own IExternalSignatures don't expose the PK either, so neither should I.