使用 iText 对 PDF 进行外部签名

Gra*_*ina 6 c# pdf itext digital-signature

首先,虽然我关注 StackOverflow 已经有一段时间了,但这是我第一次发帖,所以如果我做错了或不符合规则,请随时指出正确的方向。

我正在开发一个 PDF 数字签名应用程序,使用 iText5,在我准备好签名的 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;
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

在这一步之后,我将哈希发送到外部服务,它返回一个签名的哈希。

检查我发送给服务的哈希,它似乎是正确的,因为它涵盖了除新签名内容之外的所有 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[] { };
        }
    }
Run Code Online (Sandbox Code Playgroud)

在 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) {
        }
    }
Run Code Online (Sandbox Code Playgroud)

问题是当我在 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);

    }
}
Run Code Online (Sandbox Code Playgroud)

和 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

signatureResponse.DocumentSignature表示由服务返回的符号字节。

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

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

虽然我知道当前的证书无效,但它是由服务提供的,在服务的先前实现中,我将发送整个 PDF 进行签名,签名的 PDF 也使用此证书进行签名。

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

目前,正在发生的事情是这样的:

在 Acrobat Reader 中签名

检查签名:

签名属性

同样,如果我在 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 现在已正确签名。

Adobe Reader 会接受固定签名吗?

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

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

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

对于您的代码,这意味着您必须在将散列发送到服务之前将其打包到 DigestInfo 结构中。

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

问:在我的初始代码中,我进行了两阶段签名。在单签名方法中应用的相同主体是否仍然有效,即,应用 SHA256 前缀来执行预签名字节并在使用生成的签名字节设置摘要之后?

mkl*_*mkl 6

您的代码中有许多问题。

首先,您的代码混合了不同的 iText 签名 API 代。有较旧的 API 代,它要求您非常接近 PDF 内部结构,还有较新的(自 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");
Run Code Online (Sandbox Code Playgroud)

因此,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;
Run Code Online (Sandbox Code Playgroud)

但是,稍后您的代码会获取该返回值,对其进行签名,并尝试将返回的签名字节嵌入到PdfPKCS7签名容器中。这是错误的,必须为签名容器的签名者信息的认证属性创建签名字节,而不是文档哈希。

(顺便说一句,这里您在不理解它的情况下使用了较旧的签名 API,因此,使用它是错误的。)

将带符号的字节放在错误的位置

MyExternalSignatureContainer您在两个调用中使用带符号的字节:

sigField.SetExternalDigest(signedBytes, null, "RSA");
return sigField.GetEncodedPKCS7(signedBytes, null, null, null, CryptoStandard.CMS);
Run Code Online (Sandbox Code Playgroud)

第一个调用是正确的,它们属于这里。但是,在第二次调用中,应该使用签名文档范围的原始散列。

(这里你再次使用旧的签名 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";
    } 
}
Run Code Online (Sandbox Code Playgroud)

并像这样使用它来签名:

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);
Run Code Online (Sandbox Code Playgroud)

更新IExternalSignature实施

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

对于您的代码,这意味着必须在将散列DigestInfo发送到服务之前将其打包成一个结构。

RFC 8017 第 9.2 节注释 1 中解释了一种简单的方法:

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

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

即你只需要在你的哈希前面加上字节序列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";
    } 
}
Run Code Online (Sandbox Code Playgroud)

Adobe Reader 会接受固定签名吗?

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

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

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

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