You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PDF签名嵌入后提示文档篡改问题排查(附实现代码)

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


问题原因排查

  1. 签名逻辑冗余且不符合PDF规范
    在SignHash_方法中,先调用rsa.SignData生成的签名未被使用,后续通过SignedCms重新计算签名的逻辑存在错误:对于ADBE_PKCS7_DETACHED类型的PDF签名,应该将PDF哈希包装成带有摘要算法标识的PKCS#7 SignedData结构,而非直接对原始哈希字节做CMS签名,这种错误的签名结构会导致验证不通过。

  2. 哈希计算的最终块处理错误
    GenerateHash中sha.TransformFinalBlock(buff, 0, 0)的写法不规范,虽然语法合法,但结合前面的流读取循环,可能导致哈希值计算不准确。正确的做法是直接使用sha.ComputeHash(s)替代TransformBlock+TransformFinalBlock的组合,避免数据遗漏或哈希错误。

  3. 签名填充与预留空间不匹配
    SignPDFToMemory中将签名填充到RESERVED_SPACE_SIGNATURE长度的数组,但exclusionSizes中CONTENTS的预留空间是(RESERVED_SPACE_SIGNATURE * 2) + 2(对应十六进制编码后的长度)。如果签名数据的十六进制编码长度超过该预留值,会破坏PDF文件结构,触发验证错误。需确保签名编码后的长度不超过预留空间,或调整预留空间的计算逻辑。

  4. 签名时间一致性问题
    GenerateHash中设置的appearance.SignDate未同步到签名服务,PDF验证时会检查签名时间与文档记录时间的一致性,任何时间不一致或后续元数据修改都会导致“文档已更改”的提示。需确保签名过程使用的时间与PDF中记录的时间完全一致。

内容的提问来源于stack exchange,提问作者Farid Huseynli

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 15:25:56