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
相关产品推荐
相关产品推荐

