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

C#中利用外部签名哈希与证书实现PDF签名的分步方案

基于iText7的PDF分阶段签名实现(外部服务签名场景)

我需要通过外部服务完成PDF签名,因此需要以下两个核心函数:

public string GetHashToSign(byte[] unsignedPdf, X509Certificate cert) 
{
    // 生成并返回预处理PDF文档的哈希值
}

这个哈希值会发送给外部Web服务,之后用户会被重定向到该服务完成登录和签名确认。确认完成后用户跳转回指定页面,下载已签名的哈希值并构建最终的已签名PDF,此时需要第二个函数:

public byte[] SignDocument(byte[] unsignedPdf, X509Certificate cert, string signedHash) {
    // 整合所有内容并返回已签名PDF
}

我尝试用iText和/或Aspose实现,但搞不清楚怎么把这两个步骤解耦,在不同页面分别执行。


已实现的解决方案(基于@Glenner003的回答调整)

以下是适配需求的实现代码,或许能帮到其他遇到相同问题的人:

预处理响应实体类

public class PreparedResponse
{
    public byte[] PreparedFile { get; set; }
    public byte[] BytesToSign { get; set; }
}

核心签名处理类

public class SignPdfWithIText7
{
    private static IBouncyCastleFactory BcFactory = BouncyCastleFactoryCreator.GetFactory();

    public PreparedResponse PreparePdfForSignage(
        byte[] pdf,
        string certificate,
        string digestAlgorithmOid,
        SignerProperties signerProperties)
    {

        byte[] preparedBytes;
        CMSContainer cmsContainer;
        using MemoryStream unsignePdfStream = new(pdf);
        using MemoryStream preparedPdfStream = new();
        using (PdfReader reader = new PdfReader(unsignePdfStream))
        using (MemoryStream outputStream = preparedPdfStream)
        {
            var parser = BcFactory.CreateX509CertificateParser();
            IX509Certificate[] certificateChain = [.. parser.ReadAllCerts(Convert.FromBase64String(certificate))];

            string digestAlgorithm = DigestAlgorithms.GetDigest(digestAlgorithmOid);

            // 1. 准备CMS容器以估算签名所需空间
            cmsContainer = new CMSContainer();
            cmsContainer.AddCertificates(certificateChain);
            cmsContainer.GetSignerInfo()
                .SetSigningCertificateAndAddToSignedAttributes(certificateChain[0], digestAlgorithmOid);
            // 后续版本会从证书中提取默认算法
            cmsContainer.GetSignerInfo().SetSignatureAlgorithm(GetAlgorithmOidFromCertificate(certificateChain[0]));
            cmsContainer.GetSignerInfo().SetDigestAlgorithm(new AlgorithmIdentifier(digestAlgorithmOid));

            // 除了必填字段,还可以通过以下方法添加验证数据或其他签名/未签名属性:
            // cmsContainer.GetSignerInfo().SetCrlResponses();
            // cmsContainer.GetSignerInfo().SetOcspResponses();
            // cmsContainer.GetSignerInfo().AddSignedAttribute();
            // cmsContainer.GetSignerInfo().AddUnSignedAttribute();

            // 获取估算的签名大小
            long estimatedSize = cmsContainer.GetSizeEstimation();

            var digest = BcFactory.CreateIDigest(digestAlgorithm);
            // 为摘要预留足够空间
            estimatedSize += digest.GetDigestLength() * 2L + 2;
            // 预留十六进制转换所需的双倍空间
            estimatedSize *= 2;

            PdfTwoPhaseSigner signer = new PdfTwoPhaseSigner(reader, outputStream);
            signer.SetStampingProperties(new StampingProperties().UseAppendMode());

            // 2. 预处理文档:添加签名域并获取文档摘要
            byte[] documentDigest = signer.PrepareDocumentForSignature(signerProperties, digestAlgorithm,
                    PdfName.Adobe_PPKLite, PdfName.Adbe_pkcs7_detached, (int)estimatedSize, false);

            // 3. 将文档摘要添加到CMS容器,这是待签名内容的一部分
            cmsContainer.GetSignerInfo().SetMessageDigest(documentDigest);
            outputStream.Close();
            preparedBytes = outputStream.ToArray();
        }

        // 4. 此步骤可选:将CMS容器添加到文档中,避免重复构建或单独存储
        PreparedResponse response;
        using (PdfReader reader = new PdfReader(new MemoryStream(preparedBytes)))
        using (PdfDocument document = new PdfDocument(reader))
        using (MemoryStream outputStream = new MemoryStream())
        {
            PdfTwoPhaseSigner.AddSignatureToPreparedDocument(document, signerProperties.GetFieldName(), outputStream,
                    cmsContainer.Serialize());
            outputStream.Close();
            response = new()
            {
                PreparedFile = outputStream.ToArray(),
                BytesToSign = cmsContainer.GetSerializedSignedAttributes()
            };
        }

        // 5. 序列化后的签名属性才是真正需要签名的内容
        // 根据外部服务要求,有时需要对其生成哈希再发送,有时直接发送原始字节即可
        return response;
    }

    private AlgorithmIdentifier GetAlgorithmOidFromCertificate(IX509Certificate x509Certificate)
    {
        ITbsCertificateStructure tbsCert = BcFactory.CreateTBSCertificate(x509Certificate.GetTbsCertificate());
        if (tbsCert.GetSubjectPublicKeyInfo().GetAlgorithm().GetParameters() != null)
        {
            if (tbsCert.GetSubjectPublicKeyInfo().GetAlgorithm().GetParameters().IsNull())
            {
                return new AlgorithmIdentifier(tbsCert.GetSubjectPublicKeyInfo().GetAlgorithm().GetAlgorithm
                    ().GetId(), BcFactory.CreateDERNull());
            }
            return new AlgorithmIdentifier(tbsCert.GetSubjectPublicKeyInfo().GetAlgorithm().GetAlgorithm
                ().GetId(), tbsCert.GetSubjectPublicKeyInfo().GetAlgorithm().GetParameters().ToASN1Primitive());
        }
        return new AlgorithmIdentifier(tbsCert.GetSubjectPublicKeyInfo().GetAlgorithm().GetAlgorithm().GetId());
    }

    public byte[] SignPreparedPdf(byte[] preparedPdf, string fieldName, byte[] signature)
    {
        using (PdfReader reader = new PdfReader((new MemoryStream(preparedPdf))))
        using (PdfDocument document = new PdfDocument(reader))
        using (MemoryStream outputStream = new MemoryStream())
        {
            // 1. 读取文档中的CMS容器
            SignatureUtil su = new SignatureUtil(document);
            PdfSignature sig = su.GetSignature(fieldName);
            PdfString encodedCMS = sig.GetContents();
            byte[] encodedCMSdata = encodedCMS.GetValueBytes();
            CMSContainer cmsContainer = new CMSContainer(encodedCMSdata);

            // 2. 将签名值添加到CMS容器
            cmsContainer.GetSignerInfo().SetSignature(signature);

            PdfTwoPhaseSigner.AddSignatureToPreparedDocument(document, fieldName, outputStream,
                    cmsContainer.Serialize());

            outputStream.Close();
            return outputStream.ToArray();
        }
    }
}

补充说明

整体代码基于@Glenner003的示例修改,仅做了少量适配调整。由于我对PDF签名不太熟悉,这里特别说明:根据外部服务的要求,我需要先对BytesToSign生成哈希值再发送,但这一步可能因服务不同而存在差异。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 14:54:52