【问题标题】:External signing PDF with iTextSharp - altered/corrupted document使用 iTextSharp 进行外部签名 PDF - 更改/损坏的文档
【发布时间】:2020-03-24 16:52:18
【问题描述】:

目标是实现一个 PDF 签名过程,其中服务器(.NET Core 服务)根据请求(电子)向客户端提供要签名的哈希。然后,客户端使用通过 PKCS#11 接口从智能卡获得的私钥对给定的散列进行签名。然后将签名发送回服务器,以便使用 iTextSharp 附加到 PDF 文件中。

目前,使用 node-webcrypto-p11 使用智能卡令牌对哈希进行签名的过程非常简单(需要大量的试验和错误才能到达这里)。使用的算法是 RSASSA-PKCS1-v1_5。我可以成功签署哈希并在之后验证它。

我最近在 External signing PDF with iTextsharp (3) 的帮助下构建了我之前的实现,我使用 getAuthenticatedAttributeBytes 来获取要签名的哈希(如 mkl 所建议的那样)。

在 Acrobat Reader 中查看签名时,我看到可怕的文档已更改/损坏,与 OP pgkdev 相同。如上所述,客户端的签名过程很简单,我不怀疑那里会出现任何问题(不过我愿意对此进行审查)。

pgkdev 提到了Priyanka's question,在那里我发现我可能在签署文档的两步过程中遇到了问题,其中哈希值不再相同。

如果您检查Grazina's question,我们可以看到这样的实现是成功的,当您一步完成该过程时。

mkl 进一步提到了一种通过 2 个步骤成功完成的方法,但我错过了有关如何实现该目标的更多解释。

注意:我(据我所知)无法在第一步中做我想做的事,因为签名是由客户在 Electron 应用程序中发起的。

单击证书详细信息会显示我的完整证书详细信息。

private const string SIG_FIELD_NAME = "sigField1";

    private byte[] GetPDFHash(string pdfFilePath, byte[] certificateValue)
    {
        var preparedSigPdfFilePath = $"{pdfFilePath}.tempsig.pdf";

        //Get certificates chain from certificate value
        ICollection<X509Certificate> certificatesChain = GetCertificatesChain(certificateValue);

        byte[] hash = CreatePDFEmptySignature(pdfFilePath, preparedSigPdfFilePath, certificatesChain);
        return hash;
    }

    private void SignPDFHash(string pdfFilePath, byte[] hash, byte[] signedHash, byte[] certificateValue)
    {
        var preparedSigPdfFilePath = $"{pdfFilePath}.tempsig.pdf";
        var signedPdfFilePath = $"{pdfFilePath}.signed.pdf";

        //Get certificates chain from certificate value
        ICollection<X509Certificate> certificatesChain = GetCertificatesChain(certificateValue);

        CreateFinalSignature(preparedSigPdfFilePath, signedPdfFilePath, hash, signedHash, certificatesChain);
    }

    private byte[] CreatePDFEmptySignature(string pdfFilePath, string preparedSigPdfFilePath, ICollection<X509Certificate> certificatesChain)
    {
        byte[] hash;

        using (PdfReader reader = new PdfReader(pdfFilePath))
        {
            using (FileStream baos = System.IO.File.OpenWrite(preparedSigPdfFilePath))
            {
                PdfStamper pdfStamper = PdfStamper.CreateSignature(reader, baos, '\0', null, true);
                PdfSignatureAppearance sap = pdfStamper.SignatureAppearance;
                sap.SetVisibleSignature(new Rectangle(36, 720, 160, 780), 1, SIG_FIELD_NAME);

                sap.Certificate = certificatesChain.First();

                var externalEmptySigContainer = new MyExternalEmptySignatureContainer(PdfName.ADOBE_PPKMS, PdfName.ADBE_PKCS7_DETACHED, preparedSigPdfFilePath, certificatesChain);

                MakeSignature.SignExternalContainer(sap, externalEmptySigContainer, 8192);

                hash = externalEmptySigContainer.PdfHash;
            }
        }
        return hash;
    }

    private void CreateFinalSignature(string preparedSigPdfFilePath, string signedPdfFilePath, 
        byte[] hash, byte[] signedHash, ICollection<X509Certificate> certificatesChain)
    {
        using (PdfReader reader = new PdfReader(preparedSigPdfFilePath))
        {
            using (FileStream baos = System.IO.File.OpenWrite(signedPdfFilePath))
            {
                IExternalSignatureContainer externalSigContainer = new MyExternalSignatureContainer(hash, signedHash, certificatesChain);
                MakeSignature.SignDeferred(reader, SIG_FIELD_NAME, baos, externalSigContainer);
            }
        }
    }

    public class MyExternalEmptySignatureContainer : ExternalBlankSignatureContainer
    {
        public string PdfTempFilePath { get; set; }
        public byte[] PdfHash { get; private set; }

        public ICollection<X509Certificate> CertificatesList { get; set; }

        public MyExternalEmptySignatureContainer(PdfName filter, PdfName subFilter, string pdfTempFilePath,
            ICollection<X509Certificate> certificatesChain) : base(filter, subFilter)
        {
            PdfTempFilePath = pdfTempFilePath;
            CertificatesList = certificatesChain;
        }

        override public byte[] Sign(Stream data)
        {
            byte[] sigContainer = base.Sign(data);

            //Get the hash
            IDigest messageDigest = DigestUtilities.GetDigest("SHA-256");
            byte[] messageHash = DigestAlgorithms.Digest(data, messageDigest);

            #region Log
            var messageHashFilePath = $"{PdfTempFilePath}.messageHash-b64.txt";
            System.IO.File.WriteAllText(messageHashFilePath, Convert.ToBase64String(messageHash));
            #endregion Log

            //Add hash prefix
            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);

            #region Log
            var messageHashWithPrefixFilePath = $"{PdfTempFilePath}.messageHash-with-prefix-b64.txt";
            System.IO.File.WriteAllText(messageHashWithPrefixFilePath, Convert.ToBase64String(digestInfo));
            #endregion Log

            var sgn = new PdfPKCS7(null, this.CertificatesList, "SHA256", false);
            var authenticatedAttributeBytes =
                sgn.getAuthenticatedAttributeBytes(messageHash, null, null, CryptoStandard.CMS);

            PdfHash = authenticatedAttributeBytes;

            return sigContainer;
        }
    }

    public class MyExternalSignatureContainer : IExternalSignatureContainer
    {
        public byte[] Hash { get; set; }
        public byte[] SignedHash { get; set; }
        public ICollection<X509Certificate> CertificatesList { get; set; }

        public MyExternalSignatureContainer(byte[] hash, byte[] signedHash, ICollection<X509Certificate> certificatesList)
        {
            Hash = hash;
            SignedHash = signedHash;
            CertificatesList = certificatesList;
        }

        public byte[] Sign(Stream data)
        {
            PdfPKCS7 sgn = new PdfPKCS7(null, this.CertificatesList, "SHA256", false);
            sgn.SetExternalDigest(this.SignedHash, null, "RSA");    
            return sgn.GetEncodedPKCS7(this.Hash, null, null, null, CryptoStandard.CMS);         
        }

        public void ModifySigningDictionary(PdfDictionary signDic)  {   }
    }

    private ICollection<X509Certificate> GetCertificatesChain(byte[] certByteArray)
    {
        ICollection<X509Certificate> certChain = new Collection<X509Certificate>();

        X509Certificate2 cert = new X509Certificate2(certByteArray);

        X509Certificate regularCert = new X509CertificateParser()
            .ReadCertificate(cert.GetRawCertData());

        certChain.Add(regularCert);

        return certChain;
    }

编辑:Signed PDF

编辑: 调整 CreateFinalSignature 以使用保存到 .txt 文件中的 messageHash。结果是一样的。 Signed PDF

private void CreateFinalSignature(string preparedSigPdfFilePath, string signedPdfFilePath, byte[] signedHash, ICollection<X509Certificate> certificatesChain)
    {
        var messageHashFilePath = $"{preparedSigPdfFilePath}.messageHash-b64.txt";
        string hashString = System.IO.File.ReadAllText(messageHashFilePath);
        byte[] hash = Convert.FromBase64String(hashString);

        using (PdfReader reader = new PdfReader(preparedSigPdfFilePath))
        {
            using (FileStream baos = System.IO.File.OpenWrite(signedPdfFilePath))
            {
                IExternalSignatureContainer externalSigContainer = new MyExternalSignatureContainer(hash, signedHash, certificatesChain);
                MakeSignature.SignDeferred(reader, SIG_FIELD_NAME, baos, externalSigContainer);
            }
        }
    }

哈希是相同的,如下所示。我设置了一些断点来尝试在保存之前和从文件读取之后捕获值。

保存前的字节数组:

[0] = {byte} 133
[1] = {byte} 170
[2] = {byte} 124
[3] = {byte} 73
[4] = {byte} 225
[5] = {byte} 104
[6] = {byte} 242
[7] = {byte} 79
[8] = {byte} 44
[9] = {byte} 52
[10] = {byte} 173
[11] = {byte} 6
[12] = {byte} 7
[13] = {byte} 250
[14] = {byte} 171
[15] = {byte} 50
[16] = {byte} 226
[17] = {byte} 132
[18] = {byte} 113
[19] = {byte} 31
[20] = {byte} 125
[21] = {byte} 174
[22] = {byte} 53
[23] = {byte} 98
[24] = {byte} 68
[25] = {byte} 117
[26] = {byte} 102
[27] = {byte} 191
[28] = {byte} 109
[29] = {byte} 180
[30] = {byte} 88
[31] = {byte} 133

从 CreateFinalSignature 中的 .txt 文件读取的字节数组:

[0] = {byte} 133
[1] = {byte} 170
[2] = {byte} 124
[3] = {byte} 73
[4] = {byte} 225
[5] = {byte} 104
[6] = {byte} 242
[7] = {byte} 79
[8] = {byte} 44
[9] = {byte} 52
[10] = {byte} 173
[11] = {byte} 6
[12] = {byte} 7
[13] = {byte} 250
[14] = {byte} 171
[15] = {byte} 50
[16] = {byte} 226
[17] = {byte} 132
[18] = {byte} 113
[19] = {byte} 31
[20] = {byte} 125
[21] = {byte} 174
[22] = {byte} 53
[23] = {byte} 98
[24] = {byte} 68
[25] = {byte} 117
[26] = {byte} 102
[27] = {byte} 191
[28] = {byte} 109
[29] = {byte} 180
[30] = {byte} 88
[31] = {byte} 133

编辑:散列 authenticatedAttributeBytes,然后返回该散列以供客户端签名。

尝试了 3 种不同的散列方式,结果相同:

PdfHash = DigestAlgorithms.Digest(new MemoryStream(authenticatedAttributeBytes), messageDigest)

PdfHash = SHA256.Create().ComputeHash(authenticatedAttributeBytes)

PdfHash = SHA256Managed.Create().ComputeHash(authenticatedAttributeBytes)

GetPDFHash

的使用
byte[] bytesToSign = GetPDFHash(Path.Combine(_configuration["documentFileSystemStore:DocumentFolder"], "SignMeMightySigner.pdf"), Convert.FromBase64String(dto.base64certificateValue));

SignPDFHash

的使用
SignPDFHash(Path.Combine(_configuration["documentFileSystemStore:DocumentFolder"], "SignMeMightySigner.pdf"),Convert.FromBase64String(dto.base64signature), Convert.FromBase64String(dto.base64certificateValue));

编辑(2020 年 3 月 29 日): 我检查了我的客户端,找不到任何问题。我选择 RSASSA-PKCS1-v1_5 alg 来获取签名并在之后成功验证。在其他一些问题中,我发现在服务器和客户端之间传输字节数组可能是一个问题,但我已经检查并且值是相同的,base64 和字节数组。

决定在文本编辑器中打开 PDF,并将其与定期签名的 PDF 进行比较(相同的文本内容,只是直接通过 Adob​​e Reader 签名)。

让我困扰和担心的是,使用 iText 签名的 PDF 缺少直接签名的 PDF 内部的一大块“文本”。

还有什么我可以提供的可能的进一步分析吗?我已经看到 Stack Overflow 上的人们无法克服这个问题,有些人甚至完全放弃了它。我不想也不能这样做,我想追根究底。

Directly signed through Adobe Reader Deferred signing with iText

编辑 2020 年 3 月 30 日: 如上所述,我对 AuthenticatedAttributeBytes 进行哈希处理

PdfHash = SHA256Managed.Create().ComputeHash(authenticatedAttributeBytes);

AuthenticatedAttributeBytes

49 75 48 24 6 9 42 134 72 134 247 13 1 9 3 49 11 6 9 42 134 72 134 247 13 1 7 1 48 47 6 9 42 134 72 134 247 13 1 9 4 49 34 4 32 122 115 111 54 139 240 60 168 176 67 64 158 55 107 233 48 77 220 19 208 139 187 42 1 141 149 20 241 151 80 31 79 

AuthenticatedAttributeBytes - 散列

33 25 105 92 244 51 72 93 179 135 158 84 249 178 103 91 236 247 253 35 232 124 169 112 108 214 63 206 206 2 88 107 

(返回给客户端)AuthenticatedAttributeBytes - 散列 & base64 编码

IRlpXPQzSF2zh55U+bJnW+z3/SPofKlwbNY/zs4CWGs=

哈希签名(签名)

76 13 184 229 123 212 2 8 140 24 34 88 95 31 255 142 105 220 204 186 172 110 61 75 156 44 185 62 81 209 238 226 67 133 115 247 76 24 182 144 38 164 71 92 124 140 77 16 212 43 52 156 173 90 163 116 0 124 119 119 103 8 12 74 147 1 207 51 156 104 52 231 112 125 115 140 28 105 160 117 235 199 224 166 30 220 111 35 165 49 18 85 253 194 112 254 142 117 46 58 87 13 110 161 151 228 95 238 115 171 70 117 203 103 204 222 233 42 163 37 105 91 177 117 190 238 135 137 162 6 54 125 108 64 148 219 7 198 93 117 12 164 130 123 213 197 233 173 145 77 209 11 166 91 29 137 142 25 20 96 90 130 251 169 234 9 44 245 230 20 46 243 254 98 179 98 148 87 104 151 228 246 231 23 94 134 144 84 177 219 235 90 11 130 33 139 94 155 73 112 60 88 53 150 59 49 184 100 210 82 32 71 66 168 21 167 91 141 94 239 221 156 96 23 132 147 237 15 237 232 112 214 224 61 117 46 143 208 41 64 13 128 44 69 135 172 113 58 8 85 5 176 192 254 107 92 

(从客户端收到)哈希签名(签名)-base64

TA245XvUAgiMGCJYXx//jmnczLqsbj1LnCy5PlHR7uJDhXP3TBi2kCakR1x8jE0Q1Cs0nK1ao3QAfHd3ZwgMSpMBzzOcaDTncH1zjBxpoHXrx+CmHtxvI6UxElX9wnD+jnUuOlcNbqGX5F/uc6tGdctnzN7pKqMlaVuxdb7uh4miBjZ9bECU2wfGXXUMpIJ71cXprZFN0QumWx2JjhkUYFqC+6nqCSz15hQu8/5is2KUV2iX5PbnF16GkFSx2+taC4Ihi16bSXA8WDWWOzG4ZNJSIEdCqBWnW41e792cYBeEk+0P7ehw1uA9dS6P0ClADYAsRYescToIVQWwwP5rXA==

签名字节(从 base64 解码)与客户端记录的 uint8array 匹配。

【问题讨论】:

标签: c# pdf itext digital-signature


【解决方案1】:

您的原始代码

MyExternalEmptySignatureContainer.Sign 中,您使用 PDF 范围流的裸散列正确确定了经过身份验证的属性:

var authenticatedAttributeBytes = sgn.getAuthenticatedAttributeBytes(messageHash, null, null, CryptoStandard.CMS);

不过,在检查您的示例文件时,我发现签名的消息摘要属性包含嵌入在 DigestInfo 对象中的哈希,在其他与 sha256Prefix 一起使用时,您将应用于 MyExternalEmptySignatureContainer.Sign 中的消息摘要副本.

因此,显然,当您在 MyExternalSignatureContainer.Sign 中重新创建经过身份验证的属性时

return sgn.GetEncodedPKCS7(this.Hash, null, null, null, CryptoStandard.CMS);

您在 this.Hash 中使用了错误的值。这有两个影响,一方面显然 Message Digest 属性的值现在不正确,另一方面,为原始的、正确的经过身份验证的属性创建的签名值与您不正确的属性不匹配。因此,生成的 PDF 签名是双重错误的。

要解决此问题,请在此处使用正确的哈希值,即 PDF 范围流的哈希没有该前缀。

由于您没有展示您如何准确地使用您的方法 GetPDFHashSignPDFHash,我无法更准确地指出错误。

您的更新代码

确实,现在正确的散列在 Message Digest 属性中,但签名仍然签署了错误的散列,以防您的新示例:

Signed Attributes Hash: 54B2F135A542EEAA55270AB19210E363D00A7684405403E89B170591A7BCAB5F
Decrypted signature digest: 22D906E686A83FA1A490895A21CD6F9A9272C13FB9B16D8A6E862168458F3640

原因可能是您的MyExternalEmptySignatureContainer 属性PdfHash 的内容不是哈希,而是完整的经过身份验证的属性字节,参见。 MyExternalEmptySignatureContainer.Sign:

var authenticatedAttributeBytes = sgn.getAuthenticatedAttributeBytes(messageHash, null, null, CryptoStandard.CMS);

PdfHash = authenticatedAttributeBytes;

您可能需要计算authenticatedAttributeBytes 的哈希值并将其放入PdfHash

不过,由于您没有展示您如何准确地使用您的方法 GetPDFHashSignPDFHash,因此只能猜测。

您记录的哈希值

3 月 30 日,您共享了一次运行中传输的相关数据的日志。特别是:

AuthenticatedAttributeBytes - 散列

33 25 105 92 244 51 72 93 179 135 158 84 249 178 103 91 236 247 253 35 232 124 169 112 108 214 63 206 206 2 88 107 

哈希签名(签名)

76 13 184 229 123 212 2 8 140 24 34 88 95 31 255 142 105 220 204 186 172 110 61 75 156 44 185 62 81 209 238 226 67 133 115 247 76 24 182 144 38 164 71 92 124 140 77 16 212 43 52 156 173 90 163 116 0 124 119 119 103 8 12 74 147 1 207 51 156 104 52 231 112 125 115 140 28 105 160 117 235 199 224 166 30 220 111 35 165 49 18 85 253 194 112 254 142 117 46 58 87 13 110 161 151 228 95 238 115 171 70 117 203 103 204 222 233 42 163 37 105 91 177 117 190 238 135 137 162 6 54 125 108 64 148 219 7 198 93 117 12 164 130 123 213 197 233 173 145 77 209 11 166 91 29 137 142 25 20 96 90 130 251 169 234 9 44 245 230 20 46 243 254 98 179 98 148 87 104 151 228 246 231 23 94 134 144 84 177 219 235 90 11 130 33 139 94 155 73 112 60 88 53 150 59 49 184 100 210 82 32 71 66 168 21 167 91 141 94 239 221 156 96 23 132 147 237 15 237 232 112 214 224 61 117 46 143 208 41 64 13 128 44 69 135 172 113 58 8 85 5 176 192 254 107 92 

不过,使用证书中的公钥解密后一个签名的哈希值,会得到一个包含此哈希的 DigestInfo 对象:

136 138 205 115 82 228 115 151 231 220 177 93 171 239 123 224 245 180 234 166 132 201 244 54 69 22 18 16 115 223 70 193

因此,无论您的客户端代码做什么,它都不会为您的预散列 AuthenticatedAttributeBytes 创建签名。可能它再次对哈希字节进行哈希处理,可能它对它们的 base64 表示进行哈希处理,可能它使用了一些随机数,但它并不符合您的预期。

您应该找出您的客户端代码实际做了什么并修复它或为其提供所需的数据。

例如如果您的客户端代码无法避免重新散列数据,请将未散列的经过身份验证的属性字节提供给它。

【讨论】:

  • (更新问题)我已经相应地调整了 CreateFinalSignature 方法。我使用了在第一步中保存在 .txt 文件中的 messageHash(不带前缀)。结果是一样的。
  • @MSpan 我更新了我的答案。您的消息摘要属性现在是正确的,但签名哈希不正确。这可能是因为您在名为 PdfHash 的属性中存在非哈希值。
  • 谢谢。我明白你的意思,但我似乎找不到散列属性的正确方法。使用代码和生成的 PDF 更新问题。
  • 我只能假设您的 bytesToSign = GetPDFHash(...) 和您的 SignPDFHash(...) 通话之间出现问题
  • 啊,我很害怕。因此,由于您已经排除了后端所有可能的错误,我想我将不得不深入客户端以找出可能的错误。我会及时更新调查结果。旁注:这是否有可能是因为签名分两步完成(创建空签名和将签名的签名嵌入 PDF 之间的时间)?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-07-07
  • 2022-01-01
  • 2021-11-06
  • 1970-01-01
  • 1970-01-01
  • 2021-05-11
  • 2011-01-12
相关资源
最近更新 更多