使用iText进行外部签名PDF [英] External signing PDF with iText

查看:998
本文介绍了使用iText进行外部签名PDF的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

首先,尽管我已经关注StackOverflow已有一段时间了,但这是我第一次发布内容,因此,如果我根据规则做错了什么,请随时指出正确的地方方向.

我正在使用iText5开发PDF数字签名应用程序,在准备要签名的PDF之后,它依赖于外部服务来提供签名的哈希.

在第一阶段,如 iText文档中所述准备了PDF(在最终实现中,所有PDF都可能是多重签名的,所以我使用附加模式),如下所示:

 public static byte[] GetBytesToSign(string unsignedPdf, string tempPdf, string signatureFieldName, List<Org.BouncyCastle.X509.X509Certificate> certificateChain) {
        // we create a reader and a stamper
        using (PdfReader reader = new PdfReader(unsignedPdf)) {
            using (FileStream baos = File.OpenWrite(tempPdf)) {

                List<Org.BouncyCastle.X509.X509Certificate> chain = certificateChain;
                PdfStamper pdfStamper = PdfStamper.CreateSignature(reader, baos, '\0', null, true);
                sap                   = pdfStamper.SignatureAppearance;
                sap.Certificate       = certificateChain[0];
                sap.SetVisibleSignature(new iTextSharp.text.Rectangle(36, 720, 160, 780), 1, signatureFieldName);
                //sap.SetVisibleSignature(signatureFieldName);
                sap.SignDate          = DateTime.Now;
                PdfSignature dic      = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);  
                dic.Date              = new PdfDate(sap.SignDate);
                dic.Name              = CertificateInfo.GetSubjectFields(chain[0]).GetField("CN");
                sap.CryptoDictionary  = dic;
                sap.Certificate       = certificateChain[0];
                sap.Acro6Layers       = true;
                sap.Reason            = "test";
                sap.Location          = "test";

                IExternalSignatureContainer external = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
                MakeSignature.SignExternalContainer(sap, external, 8192);
                signatureContainer = new PdfPKCS7(null, chain, "SHA256", false);
                byte[] hash = DigestAlgorithms.Digest(sap.GetRangeStream(), "SHA256");
                //byte[] signatureHash = signatureContainer.getAuthenticatedAttributeBytes(hash, null, null, CryptoStandard.CMS);

                return hash;
            }
        }
    }
 

完成此步骤后,我将哈希发送到外部服务,该服务将返回签名的哈希.

检查我发送给服务的哈希值,这似乎是正确的,因为它涵盖了除新签名内容之外的所有PDF.

然后我使用以下方法结束签名过程:

 private byte[] Sign(PdfPKCS7 signatureContainer, List<X509Certificate2> chain2, List<Org.BouncyCastle.X509.X509Certificate> chain, byte[] hash, byte[] signedBytes, string tmpPdf, string signedPdf, string signatureFieldName) {
        System.Security.Cryptography.RSACryptoServiceProvider publicCertifiedRSACryptoServiceProvider = chain2[0].PublicKey.Key as System.Security.Cryptography.RSACryptoServiceProvider;
        bool verify = publicCertifiedRSACryptoServiceProvider.VerifyHash(hash, "SHA256", signedBytes); //verify if the computed hash is same as signed hash using the cert public key
        Console.WriteLine("PKey signed computed hash is equal to signed hash: " + verify);

        AsnEncodedData asnEncodedData = new AsnEncodedData(signedBytes);
        Console.WriteLine(asnEncodedData.Format(true));

        //ITEXT5
        try {
            //Console.WriteLine("Signed bytes: " + Encoding.UTF8.GetString(signedBytes));

            using (PdfReader reader = new PdfReader(tmpPdf)) {
                using (FileStream outputStream = File.OpenWrite(signedPdf)) {
                IExternalSignatureContainer external = new Objects.MyExternalSignatureContainer(signedBytes, chain, signatureContainer);
                MakeSignature.SignDeferred(reader, signatureFieldName, outputStream, external);
                }
            }
            return new byte[] { };
        }
        catch(Exception ex) {
            File.Delete(tmpPdf);
            Console.WriteLine("Error signing file: " + ex.Message);
            return new byte[] { };
        }
    }
 

在Sign方法的开头,我验证了发送到使用相同证书签名的外部服务的哈希值是否等于外部服务响应,这是正确的.

MyExternalSignatureContainer代码:

 public class MyExternalSignatureContainer : IExternalSignatureContainer {
        private readonly byte[] signedBytes;
        public List<Org.BouncyCastle.X509.X509Certificate> Chain;
        private PdfPKCS7 sigField;

        public MyExternalSignatureContainer(byte[] signedBytes) {
            this.signedBytes = signedBytes;
        }

        public MyExternalSignatureContainer(byte[] signedBytes, List<Org.BouncyCastle.X509.X509Certificate> chain, PdfPKCS7 pdfPKCS7) {
            this.signedBytes = signedBytes;
            this.Chain = chain;
            this.sigField = pdfPKCS7;
        }

        public byte[] Sign(Stream data) {
            try {
                sigField.SetExternalDigest(signedBytes, null, "RSA");
                return sigField.GetEncodedPKCS7(signedBytes, null, null, null, CryptoStandard.CMS);
            }
            catch (IOException ioe) {
                throw ioe;
            }
        }

        public void ModifySigningDictionary(PdfDictionary signDic) {
        }
    }
 

问题是当我在Acrobat中打开PDF时,它指出自从应用签名以来,该文档已被修改或损坏.

(如果我在PDF-XChange中打开相同的PDF,则表示该PDF未修改).

到目前为止我尝试过的没有运气的东西:

不能完全确定外部服务是否使用SHA256,我已经尝试将摘要的摘要更改为预签名的SHA1,从而导致Acrobat Reader中出现格式错误".

就像在StackOverlow的另一篇文章中提到的一样(我找不到链接的文章),潜在的问题是临时文件使用不同的流.我已经尝试过使用相同的流,但是也没有运气.

PDF的样本:

原始文件

临时文件

签名文件

已将Base64哈希发送到该服务:

XYfaS/SisA/tk5hcl035RpBjOczrH9E5rgiAMpqgkjI=

已发送Base64签名的哈希作为响应:

CnV3WL7skhMCtZG1r1Qi2oyE9WPO3KP4Ieu/Xm4lec+DAbYbhQxCvjMISsG3sTwYY7Lqi4luD60uceViDH848rS9OkTn8szzAnnX2fSYIwqDpG3qjJAb6NOXEv41hy+XYhSBJWS4ji2mM2ReruwPafxB1aM25L5Jyd0V7WecuNFUevUrvd85Y2KBkyBw9zCA8NDAQPPY0UT4GkXZi3Z35+Sf/s2o8zxCOlBDaIJyMvJ9De79nw4jC5L9NesHpFxx3mX1g1N33GHjUNdETgFMhnd8RDUlGLW6bsAyv78gvwE6aXF6COObap/VtlLvMOME68MzLr6izKte6uA35Zwj9Q==


mkl的答案之后更新:

根据答案,我只在一个阶段中更改了代码签名文档的代码,最终使用以下方法:

 using (PdfReader reader = new PdfReader(fileLocation)) {
    using (FileStream baos = File.OpenWrite(tmpFile)) {

        List<Org.BouncyCastle.X509.X509Certificate> chain = Chain;
        PdfStamper pdfStamper = PdfStamper.CreateSignature(reader, baos, '\0', null, true);
        PdfSignatureAppearance sap = pdfStamper.SignatureAppearance;
        sap.Certificate = Chain[0];
        sap.SetVisibleSignature(new iTextSharp.text.Rectangle(36, 720, 160, 780), 1, signatureFieldName);
        //sap.SetVisibleSignature(signatureFieldName);
        sap.SignDate = DateTime.Now;
        PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
        dic.Date = new PdfDate(sap.SignDate);
        dic.Name = CertificateInfo.GetSubjectFields(chain[0]).GetField("CN");
        sap.CryptoDictionary = dic;
        sap.Certificate = Chain[0];
        sap.Acro6Layers = true;
        //sap.CertificationLevel = PdfSignatureAppearance.CERTIFIED_FORM_FILLING_AND_ANNOTATIONS;
        sap.Reason = "test";
        sap.Location = "test";

        IExternalSignature signature = new Objects.RemoteSignature(client, signatureRequest);
        MakeSignature.SignDetached(sap, signature, Chain, null, null, null, 8192, CryptoStandard.CMS);

    }
}
 

以及IExternalSignature实现:

 public virtual byte[] Sign(byte[] message) {
    IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
    byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
    //
    // Request signature for hash value messageHash
    // and return signature bytes
    //
    signatureRequest.Hash = messageHash;
    SignatureService.SignatureResponse signatureResponse = client.Signature(signatureRequest);

    if (signatureResponse.Status.Code == "00") {
         return signatureResponse.DocumentSignature;
    }
    else {
        throw new Exception("Error signing file: " + signatureResponse.Status.Message);
    }
}
 

signatureResponse.DocumentSignature 表示服务返回的已签名字节.

现在在结果PDF中,我遇到了BER解码错误.

分析示例PDF时,您似乎将错误的证书声明为签署者证书

尽管我知道当前证书无效,但这是由服务提供的,并且在该服务的先前实现中,我将发送整个PDF进行签名,但已签名的PDF也已与此证书进行了签名.

一个问题:知道在两阶段签名中我能够使用此证书对PDF进行签名(除了签名错误后更改或损坏的文档),这种方法也不应该使用相同的证书?

当前,这是怎么回事:

检查签名:

同样,如果我在PDF-XChange中打开相同的PDF,则签名有效,并且文档未修改.要求PDF在Acrobat中有效,但是我对读者之间的这种差异感到困惑.

结果PDF


更新2

即您只需要在字节序列前为哈希添加前缀30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20.

在将此SHA256前缀添加到消息摘要之后,现在可以正确地对生成的PDF进行签名.

Adob​​e Reader会接受固定签名吗?

我对此表示怀疑.签署者证书的密钥用法仅包含用于签署其他证书的值.

当前证书仅用于测试.在生产环境中,我相信外部服务提供的证书将是有效的.

关于此问题,我还有两个问题:

对于您的代码,这意味着您必须先将哈希打包到DigestInfo结构中,然后再将其发送到服务.

问:您如何检查签名容器以得出不正确的结论?

问:在我的初始代码中,我进行了两阶段签名.在单符号方法中应用的同一主体仍然有效,即,使用SHA256前缀执行预签名字节,并在将摘要设置为最终的带符号字节之后吗?

解决方案

您的代码中存在许多问题.

首先,您的代码混合了不同的iText签名API代.有较旧的API世代需要您非常接近PDF内部,而有较新的API(从5.3.x版本开始),该API被实现为较旧API的一层,并且不需要您了解这些内部知识

"PDF文档的数字签名"白皮书着重于显示较新的API,仅4.3.3节使用在客户端上创建的签名在服务器上对文档进行签名"使用旧的API,因为用例不允许使用更新的API.

但是,您的用例确实允许使用较新的API,因此您应该尝试仅使用它.

(在某些情况下,可以混合使用这些API,但是您应该真正知道自己在做什么,但仍然可能会犯错……)

但是现在有一些更具体的问题:

处理封闭的物体

MakeSignature.Sign*方法隐式地关闭了底层的PdfStamperSignatureAppearance对象,因此,不应假定随后使用这些对象会产生明智的信息.

但是在GetBytesToSign中,您会这么做

MakeSignature.SignExternalContainer(sap, external, 8192);
signatureContainer = new PdfPKCS7(null, chain, "SHA256", false);
byte[] hash = DigestAlgorithms.Digest(sap.GetRangeStream(), "SHA256");

因此,sap.GetRangeStream()可能返回错误. (可能它仍然会返回正确的数据,但您不应指望它.)

签名错误的字节

GetBytesToSign返回签名的PDF文档范围的哈希值:

signatureContainer = new PdfPKCS7(null, chain, "SHA256", false);
byte[] hash = DigestAlgorithms.Digest(sap.GetRangeStream(), "SHA256");
//byte[] signatureHash = signatureContainer.getAuthenticatedAttributeBytes(hash, null, null, CryptoStandard.CMS);

return hash;

但是,

稍后,您的代码将使用该返回值,对其进行签名,然后尝试将返回的签名字节嵌入到PdfPKCS7签名容器中.这是错误的,必须为签名容器的签名者信息的经过身份验证的属性而不是文档哈希创建签名字节.

(顺便说一句,在这里您使用的是较旧的签名API而不了解它,因此使用不正确.)

将签名字节放在错误的位置

MyExternalSignatureContainer中,您在两个调用中使用了已签名的字节:

sigField.SetExternalDigest(signedBytes, null, "RSA");
return sigField.GetEncodedPKCS7(signedBytes, null, null, null, CryptoStandard.CMS);

第一个电话是正确的,它们在这里.不过,在第二个调用中,应该使用签名文档范围的原始哈希.

(在这里您再次使用较旧的签名API而不理解它,然后再次错误地使用它.)

提供了错误的证书

分析示例PDF,您似乎将错误的证书声明为签署者证书.我想是因为

  • 其公钥无法正确解密签名字节,并且
  • 该证书是CA证书,而不是最终实体证书,并且具有不适当的密钥用法来签署PDF文档.

如何改善代码

首先,如果我对您的理解正确,那么您需要从其他服务器请求签名,并且该其他服务器会迅速做出反应,因此在等待签名时无需释放所有资源.在这种情况下,不需要两步签名过程,您应该一步一步完成.您只需要一个自定义的IExternalSignature实现,例如

class RemoteSignature : IExternalSignature
{
    public virtual byte[] Sign(byte[] message) {
        IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
        byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
        //
        // Request signature for hash value messageHash
        // and return signature bytes
        //
        return CALL_YOUR_SERVICE_FOR_SIGNATURE_OF_HASH(messageHash);
    } 

    public virtual String GetHashAlgorithm() {
        return "SHA-256";
    } 

    public virtual String GetEncryptionAlgorithm() {
        return "RSA";
    } 
}

并像这样使用它进行签名:

PdfReader reader = new PdfReader(...);
PdfStamper pdfStamper = PdfStamper.CreateSignature(...);
PdfSignatureAppearance sap = pdfStamper.SignatureAppearance;
// set sap properties for signing
IExternalSignature signature = new RemoteSignature();
MakeSignature.SignDetached(sap, signature, chain, null, null, null, 0, CryptoStandard.CMS);

IExternalSignature实现的更新

在更新您的问题时,您添加了签名的PDF,并应用了以上更改.通过分析签名容器中的签名字节,可以清楚地看到您的签名服务被设计为非常笨拙,它应用了PKCS1 v1.5填充和RSA加密,但是假定其输入已经打包为DigestInfo结构.根据我的经验,这是一个不常见的假设,您应该告诉签名提供者正确记录这一点.

对于您的代码,这意味着必须将散列打包到DigestInfo结构中,然后才能将其发送到服务.

RFC 8017 9.2节注释1 :

对于附录B.1中提到的九种哈希函数,DER DigestInfo值的编码T等于以下值:

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

即您只需在哈希前面加上字节序列30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20.

因此,RemoteSignature类的变体,用于需要调用者将摘要打包到DigestInfo结构中的服务,如下所示:

class RemoteSignature : IExternalSignature
{
    public virtual byte[] Sign(byte[] message) {
        IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
        byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
        byte[] sha256Prefix = {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20};
        byte[] digestInfo = new byte[sha256Prefix.Length + messageHash.Length];
        sha256Prefix.CopyTo(digestInfo, 0);
        messageHash.CopyTo(digestInfo, sha256Prefix.Length);
        //
        // Request signature for DigestInfo value digestInfo
        // and return signature bytes
        //
        return CALL_YOUR_SERVICE_FOR_SIGNATURE_OF_DIGEST_INFO(digestInfo);
    } 

    public virtual String GetHashAlgorithm() {
        return "SHA-256";
    } 

    public virtual String GetEncryptionAlgorithm() {
        return "RSA";
    } 
}

Adob​​e Reader会接受固定签名吗?

我对此表示怀疑.签署者证书的密钥用法仅包含用于签署其他证书的值.

如果您查看适用于IT的Adobe数字签名指南,您会看到有效的密钥用法扩展名是

  • 不存在,即根本没有密钥使用扩展,或者
  • 具有以下一个或多个值:
    • nonRepudiation
    • signTransaction(仅11.0.09)
    • digitalSignature(11.0.10及更高版本)

因此,证书的signCertificate值可能是个问题.

First of all, although I've been following StackOverflow for quite some time, it is the first time I posted something, so if I do something wrong or not according to the rules, please feel free to point me in the right direction.

I'm developing a PDF digital signature application, using iText5, which depends on an external service to provide a signed hash after I prepare the PDF for signing.

As described in iText documentation, in the first phase I prepared the PDF (in the final implementation, all PDFs may be multi signed, so I use append mode), like so:

public static byte[] GetBytesToSign(string unsignedPdf, string tempPdf, string signatureFieldName, List<Org.BouncyCastle.X509.X509Certificate> certificateChain) {
        // we create a reader and a stamper
        using (PdfReader reader = new PdfReader(unsignedPdf)) {
            using (FileStream baos = File.OpenWrite(tempPdf)) {

                List<Org.BouncyCastle.X509.X509Certificate> chain = certificateChain;
                PdfStamper pdfStamper = PdfStamper.CreateSignature(reader, baos, '\0', null, true);
                sap                   = pdfStamper.SignatureAppearance;
                sap.Certificate       = certificateChain[0];
                sap.SetVisibleSignature(new iTextSharp.text.Rectangle(36, 720, 160, 780), 1, signatureFieldName);
                //sap.SetVisibleSignature(signatureFieldName);
                sap.SignDate          = DateTime.Now;
                PdfSignature dic      = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);  
                dic.Date              = new PdfDate(sap.SignDate);
                dic.Name              = CertificateInfo.GetSubjectFields(chain[0]).GetField("CN");
                sap.CryptoDictionary  = dic;
                sap.Certificate       = certificateChain[0];
                sap.Acro6Layers       = true;
                sap.Reason            = "test";
                sap.Location          = "test";

                IExternalSignatureContainer external = new ExternalBlankSignatureContainer(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
                MakeSignature.SignExternalContainer(sap, external, 8192);
                signatureContainer = new PdfPKCS7(null, chain, "SHA256", false);
                byte[] hash = DigestAlgorithms.Digest(sap.GetRangeStream(), "SHA256");
                //byte[] signatureHash = signatureContainer.getAuthenticatedAttributeBytes(hash, null, null, CryptoStandard.CMS);

                return hash;
            }
        }
    }

After this step, I send the hash to the external service, which returns a signed hash.

Inspecting the hash that I send to the service, it seems to be correct, since it covers all the PDF except for the new signature contents.

I then conclude the signature process using the method below:

private byte[] Sign(PdfPKCS7 signatureContainer, List<X509Certificate2> chain2, List<Org.BouncyCastle.X509.X509Certificate> chain, byte[] hash, byte[] signedBytes, string tmpPdf, string signedPdf, string signatureFieldName) {
        System.Security.Cryptography.RSACryptoServiceProvider publicCertifiedRSACryptoServiceProvider = chain2[0].PublicKey.Key as System.Security.Cryptography.RSACryptoServiceProvider;
        bool verify = publicCertifiedRSACryptoServiceProvider.VerifyHash(hash, "SHA256", signedBytes); //verify if the computed hash is same as signed hash using the cert public key
        Console.WriteLine("PKey signed computed hash is equal to signed hash: " + verify);

        AsnEncodedData asnEncodedData = new AsnEncodedData(signedBytes);
        Console.WriteLine(asnEncodedData.Format(true));

        //ITEXT5
        try {
            //Console.WriteLine("Signed bytes: " + Encoding.UTF8.GetString(signedBytes));

            using (PdfReader reader = new PdfReader(tmpPdf)) {
                using (FileStream outputStream = File.OpenWrite(signedPdf)) {
                IExternalSignatureContainer external = new Objects.MyExternalSignatureContainer(signedBytes, chain, signatureContainer);
                MakeSignature.SignDeferred(reader, signatureFieldName, outputStream, external);
                }
            }
            return new byte[] { };
        }
        catch(Exception ex) {
            File.Delete(tmpPdf);
            Console.WriteLine("Error signing file: " + ex.Message);
            return new byte[] { };
        }
    }

In the beggining of the Sign method, I verify if the hash sent to the external service, signed with the same certificate, is equal to the external service response, which is true.

MyExternalSignatureContainer code:

public class MyExternalSignatureContainer : IExternalSignatureContainer {
        private readonly byte[] signedBytes;
        public List<Org.BouncyCastle.X509.X509Certificate> Chain;
        private PdfPKCS7 sigField;

        public MyExternalSignatureContainer(byte[] signedBytes) {
            this.signedBytes = signedBytes;
        }

        public MyExternalSignatureContainer(byte[] signedBytes, List<Org.BouncyCastle.X509.X509Certificate> chain, PdfPKCS7 pdfPKCS7) {
            this.signedBytes = signedBytes;
            this.Chain = chain;
            this.sigField = pdfPKCS7;
        }

        public byte[] Sign(Stream data) {
            try {
                sigField.SetExternalDigest(signedBytes, null, "RSA");
                return sigField.GetEncodedPKCS7(signedBytes, null, null, null, CryptoStandard.CMS);
            }
            catch (IOException ioe) {
                throw ioe;
            }
        }

        public void ModifySigningDictionary(PdfDictionary signDic) {
        }
    }

The problem is when I open the PDF in Acrobat, it states that the document has been modified or corrupted since the signature was applied.

(If I open the same PDF in PDF-XChange, it says the PDF wasn't modified).

What I've tried so far without luck:

Not being completely sure if the external service uses SHA256, I've already tried changing the digest to SHA1 of the pre-signing, resulting in a "Formatting error" in Acrobat Reader.

Like stated in another post in StackOverlow regarding the same issue (I'm unable to find the post to link it), a potential problem would be using different streams for the temporary file. I already tried to use the same stream without luck as well.

Samples of the PDF's:

Original file

Temp File

Signed File

Base64 hash sent to the service:

XYfaS/SisA/tk5hcl035RpBjOczrH9E5rgiAMpqgkjI=

Base64 signed hash sent in response:

CnV3WL7skhMCtZG1r1Qi2oyE9WPO3KP4Ieu/Xm4lec+DAbYbhQxCvjMISsG3sTwYY7Lqi4luD60uceViDH848rS9OkTn8szzAnnX2fSYIwqDpG3qjJAb6NOXEv41hy+XYhSBJWS4ji2mM2ReruwPafxB1aM25L5Jyd0V7WecuNFUevUrvd85Y2KBkyBw9zCA8NDAQPPY0UT4GkXZi3Z35+Sf/s2o8zxCOlBDaIJyMvJ9De79nw4jC5L9NesHpFxx3mX1g1N33GHjUNdETgFMhnd8RDUlGLW6bsAyv78gvwE6aXF6COObap/VtlLvMOME68MzLr6izKte6uA35Zwj9Q==


Update after mkl's answer:

According to the answer, I changed the code sign the document in only one phase and ended up with the following methods:

using (PdfReader reader = new PdfReader(fileLocation)) {
    using (FileStream baos = File.OpenWrite(tmpFile)) {

        List<Org.BouncyCastle.X509.X509Certificate> chain = Chain;
        PdfStamper pdfStamper = PdfStamper.CreateSignature(reader, baos, '\0', null, true);
        PdfSignatureAppearance sap = pdfStamper.SignatureAppearance;
        sap.Certificate = Chain[0];
        sap.SetVisibleSignature(new iTextSharp.text.Rectangle(36, 720, 160, 780), 1, signatureFieldName);
        //sap.SetVisibleSignature(signatureFieldName);
        sap.SignDate = DateTime.Now;
        PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED);
        dic.Date = new PdfDate(sap.SignDate);
        dic.Name = CertificateInfo.GetSubjectFields(chain[0]).GetField("CN");
        sap.CryptoDictionary = dic;
        sap.Certificate = Chain[0];
        sap.Acro6Layers = true;
        //sap.CertificationLevel = PdfSignatureAppearance.CERTIFIED_FORM_FILLING_AND_ANNOTATIONS;
        sap.Reason = "test";
        sap.Location = "test";

        IExternalSignature signature = new Objects.RemoteSignature(client, signatureRequest);
        MakeSignature.SignDetached(sap, signature, Chain, null, null, null, 8192, CryptoStandard.CMS);

    }
}

And the IExternalSignature implementation:

public virtual byte[] Sign(byte[] message) {
    IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
    byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
    //
    // Request signature for hash value messageHash
    // and return signature bytes
    //
    signatureRequest.Hash = messageHash;
    SignatureService.SignatureResponse signatureResponse = client.Signature(signatureRequest);

    if (signatureResponse.Status.Code == "00") {
         return signatureResponse.DocumentSignature;
    }
    else {
        throw new Exception("Error signing file: " + signatureResponse.Status.Message);
    }
}

The signatureResponse.DocumentSignature represents the signed bytes returned by the service.

In the result PDF now I'm a getting a BER decoding error.

Analyzing your example PDF you appear to declare the wrong certificate as signer certificate

Although I know the current certificate isn't valid, it is provided by the service and in the previous implementation of the service, where I would send the entire PDF for signing, the signed PDF was also signed with this certificate.

A question: Knowing that in a two-phase signing I was able to sign the PDF with this certificate (apart from the altered or corrupted document after signing error), shouldn't this method also work with the same certificate?

Currently, what is happening is this:

Inspecting the signature:

Again, if I open the same PDF in PDF-XChange, the signature is valid and the document was not modified. The requirement is the PDF to be valid in Acrobat, but I'm puzzled by this difference between readers.

Result PDF


Update 2

I.e. you only have to prefix your hash with the byte sequence 30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20.

After adding this SHA256 prefix to the message digest, the resulting PDF is now correctly signed.

Will Adobe Reader accept the fixed signature?

I doubt it. The key usage of the signer certificate only contains the value for signing other certificates.

The current certificate is only used for testing. In the production environment I believe the certificate provided by the external service will be valid.

I have two more questions regarding this issue:

For your code this means that you have to pack the hash into a DigestInfo structure before sending it to the service.

Q: How did you inspect the signature container to conclude that it wasn't correct?

Q: In my initial code, I had a two-phased signing. Would the same principal applied in a single-sign method still be valid, i.e., applying the SHA256 prefix do the pre-signed bytes and after setting the digest with the resulting signed bytes?

解决方案

There are a number of issues in your code.

First of all your code mixes different iText signing API generations. There is the older API generation which requires you to work very near to the PDF internals, and there is the newer (since version 5.3.x) API which is implemented as a layer over the older API and does not require you to know those internals.

The "Digital Signatures for PDF documents" white paper focuses on showing the newer API, only section 4.3.3 "Signing a document on the server using a signature created on the client" uses the old API because the use case does not allow for the use of the newer API.

Your use case does allow for the use of the newer API, though, so you should try and use only it.

(In certain situations one can mix those APIs, but you then should really know what you're doing and still can get it awfully wrong...)

But now some more specific issues:

Working on closed objects

The MakeSignature.Sign* methods implicitly close the underlying PdfStamper and SignatureAppearance objects, so working with those objects thereafter should not be assumed to result in sensible information.

But in GetBytesToSign you do

MakeSignature.SignExternalContainer(sap, external, 8192);
signatureContainer = new PdfPKCS7(null, chain, "SHA256", false);
byte[] hash = DigestAlgorithms.Digest(sap.GetRangeStream(), "SHA256");

Thus, the sap.GetRangeStream() probably returns something wrong. (Probably it does still return the correct data but you shouldn't count on that.)

Signing the wrong bytes

GetBytesToSign returns the hash of the signed PDF document ranges:

signatureContainer = new PdfPKCS7(null, chain, "SHA256", false);
byte[] hash = DigestAlgorithms.Digest(sap.GetRangeStream(), "SHA256");
//byte[] signatureHash = signatureContainer.getAuthenticatedAttributeBytes(hash, null, null, CryptoStandard.CMS);

return hash;

Later, though, your code takes that return value, signs it, and tries to embed the returned signature bytes into the PdfPKCS7 signature container. This is wrong, the signature bytes have to be created for the authenticated attributes of the signer info of the signature container, not the document hash.

(By the way, here you use the older signature API without understanding it and, therefore, use it incorrectly.)

Putting the signed bytes in the wrong position

In MyExternalSignatureContainer you use the signed bytes in two calls:

sigField.SetExternalDigest(signedBytes, null, "RSA");
return sigField.GetEncodedPKCS7(signedBytes, null, null, null, CryptoStandard.CMS);

The first call is correct, here they belong. In the second call, though, the original hash of the signed document ranges should have been used.

(Here you again use the older signature API without understanding it and again use it incorrectly.)

Supplying the wrong certificate

Analyzing your example PDF you appear to declare the wrong certificate as signer certificate. I think so because

  • its public key cannot properly decrypt the signature bytes and
  • the certificate is a CA certificate, not an end entity certificate, with inappropriate key usages for signing PDF documents.

How to improve your code

First of all, if I understand you correctly you request the signature from some other server, and that other server reacts quickly, so there is no need to free all resources while waiting for the signature. In such a situation there is no need for a two-phase signing process, you should do it in one step. All you need is a custom IExternalSignature implementation, something like

class RemoteSignature : IExternalSignature
{
    public virtual byte[] Sign(byte[] message) {
        IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
        byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
        //
        // Request signature for hash value messageHash
        // and return signature bytes
        //
        return CALL_YOUR_SERVICE_FOR_SIGNATURE_OF_HASH(messageHash);
    } 

    public virtual String GetHashAlgorithm() {
        return "SHA-256";
    } 

    public virtual String GetEncryptionAlgorithm() {
        return "RSA";
    } 
}

and use it like this for signing:

PdfReader reader = new PdfReader(...);
PdfStamper pdfStamper = PdfStamper.CreateSignature(...);
PdfSignatureAppearance sap = pdfStamper.SignatureAppearance;
// set sap properties for signing
IExternalSignature signature = new RemoteSignature();
MakeSignature.SignDetached(sap, signature, chain, null, null, null, 0, CryptoStandard.CMS);

Update of the IExternalSignature implementation

In the update of your question you added a PDF signed with the changes above applied. Analyzing the signature bytes in the signature container it became clear that your signing service is designed to be extremely dumb, it applies PKCS1 v1.5 padding and RSA encryption but it assumes its input to be already packed into a DigestInfo structure. In my experience this is an uncommon assumption, you should tell your signature provider to properly document that.

For your code this means that you have to pack the hash into a DigestInfo structure before sending it to the service.

A simple way to do this is explained in RFC 8017 section 9.2 note 1:

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 prefix your hash with the byte sequence 30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20.

Thus, a variant of the RemoteSignature class for services that require the caller to pack the digest into a DigestInfo structure could look like this:

class RemoteSignature : IExternalSignature
{
    public virtual byte[] Sign(byte[] message) {
        IDigest messageDigest = DigestUtilities.GetDigest(GetHashAlgorithm());
        byte[] messageHash = DigestAlgorithms.Digest(messageDigest, message);
        byte[] sha256Prefix = {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20};
        byte[] digestInfo = new byte[sha256Prefix.Length + messageHash.Length];
        sha256Prefix.CopyTo(digestInfo, 0);
        messageHash.CopyTo(digestInfo, sha256Prefix.Length);
        //
        // Request signature for DigestInfo value digestInfo
        // and return signature bytes
        //
        return CALL_YOUR_SERVICE_FOR_SIGNATURE_OF_DIGEST_INFO(digestInfo);
    } 

    public virtual String GetHashAlgorithm() {
        return "SHA-256";
    } 

    public virtual String GetEncryptionAlgorithm() {
        return "RSA";
    } 
}

Will Adobe Reader accept the fixed signature?

I doubt it. The key usage of the signer certificate only contains the value for signing other certificates.

If you look into the Adobe Digital Signatures Guide for IT, you'll see that valid key usage extensions are

  • absent, i.e. no key usage extension at all, or
  • present with one or more of the following values:
    • nonRepudiation
    • signTransaction (11.0.09 only)
    • digitalSignature (11.0.10 and later)

Thus, the signCertificate value of your certificate might be a problem.

这篇关于使用iText进行外部签名PDF的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆