I've signed a PDF document with iText 8.0.2 using an ECDSA (SHA-256) certificate. Unfortunately the Adobe Reader says the signature is invalid, with the reason "Document has been altered or corrupted since it was signed". Since the digital signing topic is not my specialty, I cannot figure out how and where to look for the error. Can someone guide me?
using iText.Kernel.Pdf;
using iText.Signatures;
using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
namespace PdfSignTest
{
class Program
{
static void Main(string[] args)
{
if (args.Length < 2)
{
Console.WriteLine("Usage: PdfSignTest.exe pdfFilePath pfxFilePath");
Console.WriteLine(" - pdfFilePath: path of an unsigned pdf file which will be signed");
Console.WriteLine(" - pfxFilePath: path of a pfx file containing the full certificate chain");
Console.ReadKey();
return;
}
string srcPath = args[0];
string cerPath = args[1];
string dstPath = srcPath.Replace(".pdf", ".signed.at." + DateTime.Now.ToString("HH-mm-ss") + ".pdf");
var signed = SignPdfECC(srcPath, cerPath);
File.WriteAllBytes(dstPath, signed);
Console.WriteLine(string.Format("File {0} successfully signed and saved as {1}.", srcPath, dstPath));
Console.ReadKey();
}
private static byte[] SignPdfECC(string unsignedPdfPath, string certificatePfxPath)
{
byte[] certificatePfx = File.ReadAllBytes(certificatePfxPath);
using (PdfReader reader = new PdfReader(unsignedPdfPath))
using (MemoryStream mem = new MemoryStream())
{
PdfSigner signer = new PdfSigner(reader, mem, new StampingProperties().UseAppendMode());
X509Certificate cert = new X509Certificate(certificatePfx, "password", X509KeyStorageFlags.Exportable);
X509Certificate2 signatureCert = new X509Certificate2(cert);
ECDsa pk = signatureCert.GetECDsaPrivateKey();
IExternalSignature signature = new EcdsaSignature(pk, DigestAlgorithms.SHA256);
iText.Bouncycastle.X509.X509CertificateBC[] chain = new iText.Bouncycastle.X509.X509CertificateBC[] { new iText.Bouncycastle.X509.X509CertificateBC(new Org.BouncyCastle.X509.X509Certificate(signatureCert.GetRawCertData())) };
signer.SignDetached(signature, chain, null, null, null, 0, PdfSigner.CryptoStandard.CMS);
return mem.ToArray();
}
}
}
public class EcdsaSignature : IExternalSignature
{
private readonly string _encryptionAlgorithm;
private readonly string _hashAlgorithm;
private readonly ECDsa _pk;
public EcdsaSignature(ECDsa pk, string hashAlgorithm)
{
_pk = pk;
_hashAlgorithm = DigestAlgorithms.GetDigest(DigestAlgorithms.GetAllowedDigest(hashAlgorithm));
_encryptionAlgorithm = "ECDSA";
}
public string GetDigestAlgorithmName()
{
return "SHA-256";
}
public virtual string GetEncryptionAlgorithm()
{
return _encryptionAlgorithm;
}
public virtual string GetHashAlgorithm()
{
return _hashAlgorithm;
}
public string GetSignatureAlgorithmName()
{
return "ECDSA";
}
public ISignatureMechanismParams GetSignatureMechanismParameters()
{
return null;
}
public virtual byte[] Sign(byte[] message)
{
return _pk.SignData(message, new HashAlgorithmName(_hashAlgorithm));
}
}
}
I removed all the additional features, like TSA and LTV, expecting that maybe one of these steps caused the error. I also tried with a test certificate taken from the OpenSSL test certificates collection, and a test PDF downloaded from w3.org. All of these are here.
The issue here is that ECDsa.SignData
returns the signature value in PLAIN format while iText uses the OID for ECDSA signatures in DER format. Thus, before returning the signature value from EcdsaSignature.Sign
you have to transform it from PLAIN to DER format, e.g. like this:
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Math;
[...]
public virtual byte[] Sign(byte[] message)
{
return PlainToDer(_pk.SignData(message, new HashAlgorithmName(_hashAlgorithm)));
}
byte[] PlainToDer(byte[] plain)
{
int valueLength = plain.Length / 2;
BigInteger r = new BigInteger(1, plain, 0, valueLength);
BigInteger s = new BigInteger(1, plain, valueLength, valueLength);
return new DerSequence(new DerInteger(r), new DerInteger(s)).GetEncoded(Asn1Encodable.Der);
}
(SignWithEcdsaCert class EcdsaSignature
methods)
The PlainToDer
helper method has been borrowed from the X509Certificate2Signature
implementation of IExternalSignature
from the iText Knowledge Base article Digital Signing with iText / Part I - Overview and Simple Cases / Signing with .NET CryptoAPI/CNG Keys.