c# - 使用iText进行外部签名PDF

标签 c# pdf itext digital-signature

首先,尽管我已经关注StackOverflow已有一段时间了,但这是我第一次发布内容,因此,如果我根据规则做错了什么,请随时指出正确的方向。
我正在使用iText5开发PDF数字签名应用程序,在准备要签名的PDF之后,它依赖于外部服务来提供签名的哈希。
iText documentation中所述,在第一阶段中,我准备了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的样本:
Original file
Temp File
Signed File
已发送到服务的Base64哈希:XYfaS/SisA/tk5hcl035RpBjOczrH9E5rgiAMpqgkjI= 作为响应发送的Base64签名的哈希值:CnV3WL7skhMCtZG1r1Qi2oyE9WPO3KP4Ieu/Xm4lec+DAbYbhQxCvjMISsG3sTwYY7Lqi4luD60uceViDH848rS9OkTn8szzAnnX2fSYIwqDpG3qjJAb6NOXEv41hy+XYhSBJWS4ji2mM2ReruwPafxB1aM25L5Jyd0V7WecuNFUevUrvd85Y2KBkyBw9zCA8NDAQPPY0UT4GkXZi3Z35+Sf/s2o8zxCOlBDaIJyMvJ9De79nw4jC5L9NesHpFxx3mX1g1N33GHjUNdETgFMhnd8RDUlGLW6bsAyv78gvwE6aXF6COObap/VtlLvMOME68MzLr6izKte6uA35Zwj9Q==
mkl's answer之后更新:
根据答案,我仅在一个阶段中更改了代码签名文档,并最终采用以下方法:
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解码错误。

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


尽管我知道当前证书无效,但是它是由服务提供的,并且在该服务的先前实现中,我将发送整个PDF进行签名,签名的PDF也已与此证书进行了签名。
一个问题:知道在两阶段签名中,我能够使用此证书对PDF进行签名(除了签名错误后更改或损坏的文档之外),该方法是否也不能使用同一证书?
当前,正在发生的事情是这样的:
Signature in Acrobat Reader
检查签名:
Signature properties
同样,如果我在PDF-XChange中打开相同的PDF,则签名有效,并且文档没有被修改。要求PDF在Acrobat中有效,但是我对读者之间的这种差异感到困惑。
Result PDF

更新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.


在将此SHA256前缀添加到消息摘要后,现在已正确签名了生成的PDF。

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.



当前证书仅用于测试。在生产环境中,我相信外部服务提供的证书将是有效的。
关于此问题,我还有两个问题:

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


问:您如何检查签名容器以得出不正确的结论?
问:在我的初始代码中,我有两个阶段的签名。在单符号方法中应用的相同主体仍然有效,即,使用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文档时使用了不合适的 key 。

  • 如何改善您的代码

    首先,如果我对您的理解正确,则可以从其他服务器请求签名,并且该其他服务器会迅速使用react,因此在等待签名时无需释放所有资源。在这种情况下,不需要两步签名过程,您应该一步一步完成。您只需要一个自定义的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 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.
        ...
    


    IE。您只需要在哈希表前面加上字节序列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";
        } 
    }
    

    Adobe Reader会接受固定签名吗?

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

    如果查看Adobe Digital Signatures Guide for IT,您会看到有效的 key 用法扩展名是

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

  • 因此,您的证书的signCertificate值可能是一个问题。

    关于c# - 使用iText进行外部签名PDF,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54559547/

    相关文章:

    c# - 如何向程序集添加上下文帮助

    c# - 在单声道下运行时,System.Windows.Forms.Timer 事件根本不会触发

    pdf - CTM矩阵乘法与先前状态与PDF位置解析中的单位矩阵?

    java.lang.IllegalArgumentException : Element not allowed

    c# - 在 Ubuntu VPS (14.04.1 LTS) 上使用单声道,为什么我的 HTTPS HttpListener 从未收到上下文?

    c# - 将 log4net 与 asp.net web 表单一起使用

    javascript - 有没有办法在新窗口中从 blob 打开带有名称的 PDF

    xcode - 如何在 Xcode 6.4 中使用矢量图

    C# - iTextSharp 该文档没有页面

    java - 数字签名外观.setContact 未显示