使用iTextSharp进行外部签名PDF-文档已更改/损坏

时间:2020-03-24 16:52:18

标签: c# pdf itext digital-signature

目标是实施PDF签名过程,在该过程中,服务器(.NET Core服务)将根据请求(Electron)向客户端提供要签名的哈希。然后,客户端使用通过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 进一步提到了一种成功完成此操作的方法,该方法分两步进行,但我缺少有关如何准确实现此目标的更多解释。

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

enter image description here enter image description here enter image description here

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

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以使用messageHash(已保存到.txt文件中)。结果是一样的。 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(相同的文本内容,只需通过Adobe 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匹配。 enter image description here

1 个答案:

答案 0 :(得分:0)

您的原始代码

MyExternalEmptySignatureContainer.Sign中,您可以使用PDF范围流的裸哈希来正确确定经过身份验证的属性:

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

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

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

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

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

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

由于您没有说明如何正确使用方法GetPDFHashSignPDFHash,因此我无法更精确地查明错误。

您更新的代码

实际上,现在正确的哈希值位于“消息摘要”属性中,但是在新示例的情况下,签名仍然签名错误的哈希值:

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表示形式的哈希处理,可能使用了一些随机数,但是它不是您期望的那样。

您应该找出客户端代码的实际作用,并对其进行修复或将其提供所需的数据。

例如如果无法阻止您的客户端代码重新散列数据,请向其提供未哈希的经过身份验证的属性字节。