PDF签名嵌入后提示文档篡改问题排查(附实现代码)
我需要实现两个服务:第一个服务生成PDF的哈希值并发送给第二个服务,第二个服务对哈希签名后返回;第一个服务再将签名嵌入PDF。代码可正常运行,但最终生成的PDF会提示“文档自签名后已被更改或损坏”,请排查问题原因。
哈希生成代码
public string GenerateHash() { string Reason = "Ferid"; string Location = "Baku"; string Contact = "Huseynlyi"; string signatureFieldName = null; // appearance.SetVisibleSignature(new Rectangle(500, 150, 400, 200), 1, signatureFieldName); appearance.SignDate = DateTime.Now; appearance.Reason = Reason; appearance.Location = Location; appearance.Contact = Contact; StringBuilder buf = new StringBuilder(); buf.Append("Digitally signed by"); buf.Append("\n"); buf.Append(userName); buf.Append("\n"); buf.Append("Date: " + appearance.SignDate); appearance.Layer2Text = buf.ToString(); appearance.Acro6Layers = true; appearance.CertificationLevel = 0; PdfSignature dic = GeneratePdfSignature(); appearance.CryptoDictionary = dic; Dictionary<PdfName, int> exclusionSizes = new Dictionary<PdfName, int>(); exclusionSizes.Add(PdfName.CONTENTS, (RESERVED_SPACE_SIGNATURE * 2) + 2); // RESERVED_SPACE_SIGNATURE = 3072 appearance.PreClose(exclusionSizes); System.Security.Cryptography.HashAlgorithm sha = new SHA1CryptoServiceProvider(); Stream s = appearance.GetRangeStream(); int read = 0; byte[] buff = new byte[0x2000]; while ((read = s.Read(buff, 0, 0x2000)) > 0) { sha.TransformBlock(buff, 0, read, buff, 0); } sha.TransformFinalBlock(buff, 0, 0); StringBuilder hex = new StringBuilder(sha.Hash.Length * 2); foreach (byte b in sha.Hash) hex.AppendFormat("{0:x2}", b); return hex.ToString(); } private PdfSignature GeneratePdfSignature() { PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED) { Date = new PdfDate(appearance.SignDate), Name = userName }; dic.Reason = appearance.Reason; dic.Location = appearance.Location; dic.Contact = appearance.Contact; return dic; }
哈希签名代码
public byte[] SignHash_(string hexhash, string password) { byte[] hash = StringToByteArray(hexhash); X509Certificate2 signerCer = null; X509Store store_ = new X509Store(StoreName.My, StoreLocation.CurrentUser); store_.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly); X509Certificate2Collection colSign = store_.Certificates.Find(X509FindType.FindBySerialNumber, "11e618d4956fb5eb000300029bb5", false); store_.Close(); X509Certificate2 certificate = colSign[0]; AsymmetricAlgorithm privateKey = certificate.GetRSAPrivateKey(); RSA rsa = privateKey as RSA; if (rsa == null) { throw new Exception("The certificate does not have an RSA private key."); } HashAlgorithmName hashAlgorithm = HashAlgorithmName.SHA1; rsa.SignData(hash, hashAlgorithm, RSASignaturePadding.Pkcs1); ContentInfo content = new ContentInfo(hash); SignedCms signedCms = new SignedCms(content, true); CmsSigner signer = new CmsSigner(certificate); signer.DigestAlgorithm = new Oid(hashAlgorithm.Name); signer.IncludeOption = X509IncludeOption.EndCertOnly; signedCms.ComputeSignature(signer, false); return signedCms.Encode(); } private static byte[] StringToByteArray(string hex) { return Enumerable.Range(0, hex.Length) .Where(x => x % 2 == 0) .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) .ToArray(); }
签名嵌入PDF代码
public byte[] SignPDFToMemory(byte[] pk) { byte[] paddedSig = new byte[RESERVED_SPACE_SIGNATURE]; System.Array.Copy(pk, 0, paddedSig, 0, pk.Length); PdfDictionary dic2 = new PdfDictionary(); dic2.Put(PdfName.CONTENTS, new PdfString(paddedSig).SetHexWriting(true)); appearance.Close(dic2); return ms.ToArray(); } public void SignPDFToNewFile(byte[] pk, string destinyPDFSigned) { var pdfInMemory = SignPDFToMemory(pk); File.WriteAllBytes(destinyPDFSigned, pdfInMemory); }
注:RESERVED_SPACE_SIGNATURE的值为3072
问题原因排查
签名逻辑冗余且不符合PDF规范
在SignHash_方法中,先调用rsa.SignData生成的签名未被使用,后续通过SignedCms重新计算签名的逻辑存在错误:对于ADBE_PKCS7_DETACHED类型的PDF签名,应该将PDF哈希包装成带有摘要算法标识的PKCS#7 SignedData结构,而非直接对原始哈希字节做CMS签名,这种错误的签名结构会导致验证不通过。哈希计算的最终块处理错误
GenerateHash中sha.TransformFinalBlock(buff, 0, 0)的写法不规范,虽然语法合法,但结合前面的流读取循环,可能导致哈希值计算不准确。正确的做法是直接使用sha.ComputeHash(s)替代TransformBlock+TransformFinalBlock的组合,避免数据遗漏或哈希错误。签名填充与预留空间不匹配
SignPDFToMemory中将签名填充到RESERVED_SPACE_SIGNATURE长度的数组,但exclusionSizes中CONTENTS的预留空间是(RESERVED_SPACE_SIGNATURE * 2) + 2(对应十六进制编码后的长度)。如果签名数据的十六进制编码长度超过该预留值,会破坏PDF文件结构,触发验证错误。需确保签名编码后的长度不超过预留空间,或调整预留空间的计算逻辑。签名时间一致性问题
GenerateHash中设置的appearance.SignDate未同步到签名服务,PDF验证时会检查签名时间与文档记录时间的一致性,任何时间不一致或后续元数据修改都会导致“文档已更改”的提示。需确保签名过程使用的时间与PDF中记录的时间完全一致。
内容的提问来源于stack exchange,提问作者Farid Huseynli

