PDF文档需要使用国家数字身份签名。
国家数字身份WebService提供了签名文件的设施,在我的项目中我已将其集成。
请求Esign服务以PKCS7(CMS)
格式给出响应。
我想在多个位置附加相同的响应,所以我要在收到服务响应后创建多个空签名容器。
我提到了这篇文章:Sign Pdf Using ITextSharp and XML Signature
但是在给定的文章中,我们仅存在一个签名位置,但是我有多个签名位置。
我正在使用itext
犀利的Library。
使用MakeSignature.SignDeferred
方法在多个位置附加签名,但显示PDF无效。
请在下面找到我从Web服务收到的响应XML:
<?xml version="1.0" encoding="UTF-8"?>
<EsignResp errCode="NA" errMsg="NA" resCode="259A52453BE95D3A1071193995E062E3EAD796AD" status="1" ts="2019-03-18T14:26:59" txn="UKC:eSign:2998:20190318142602814">
<UserX509Certificate>--Usercerti in base64--</UserX509Certificate>
<Signatures>
<DocSignature error="" id="1" sigHashAlgorithm="SHA256">--Signature in base 64 in PKCS7(CMS)---</DocSignature>
</Signatures>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"></SignatureMethod>
<Reference URI="">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></Transform>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></DigestMethod>
<DigestValue>MrOfovytOIp/8qlEkgamrcyhGTSGTN5aS1P+08Fbwfk=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>BBexJyk47YaTdoDgXaFRCtJq1Gc3KsZNt48/I8X4TgNJ6gh2NI9Y5Y9Tc7bozrK/QRy1VYPOWYq5r/YdunjMQLmJJicyeqeqe2eD+TJ8oecpjCbmhPnDK2VgaJ2h00lIe/toKwAmV4PTBA1a5wkz77hj+HTkWXMkPEIsBUnBirVpHxe2bYaa7jcIIpWtJmqvcSurKTOeyFRa+AFWfwWHB/EzHJlDmgiMXzrNauxJ4HpphNaRU+bO5JdyzJs/8Zx4i6qwSEybkuprL3GdO9C7zMPiC98CTfO2UrbZWy1pSvwEqlVXQIfrkp+m2JRbFgT8EEIGfXUS+AJBPRwhY1Xsww==</SignatureValue>
<KeyInfo>
<KeyValue>
<RSAKeyValue>
<Modulus>0o9vohWZ3ztI9ea8D/zUEUBRq6c82BE7sFmr1hNMeuGSJQFf39ceesRtGUzlUYVWXcU23P8sVZ5419CHh7ApFzUXaLD72i/2d5FFI0n3iRlTQec9PEUHyrvOCVDpqBhbnrO/EHBqRluUQJTQUtMu5mhPNFV7IIJMTEAsUhCL9adZXXQK9NeK0foRr29Oq7VdEGfSeLzHIibpQmhNPh89oJXqu0cmbNSW4J4i2GmwHQpmsmHaSQcgh4mgVrykO64pAKXPreAPipDHQM1l/e5hilYlWfLHxhC5ObTCTcydQ218IVulFOFhdQt7xVV61TOmoTC2elhWbDqoLJBVU5mBfQ==</Modulus>
<Exponent>AQAB</Exponent>
</RSAKeyValue>
</KeyValue>
<X509Data>
<X509SubjectName>CN=DS NSDL E GOVERNANCE INFRASTRUCTURE LIMITED 3,ST=MAHARASHTRA,PostalCode=400013,O=NSDL E GOVERNANCE INFRASTRUCTURE LIMITED,C=IN</X509SubjectName>
<X509Certificate>--public certificate of provider--- </X509Certificate>
</X509Data>
</KeyInfo>
</Signature>
</EsignResp>
编辑:根据最新的通信,Web Service会为从我头开始提供的任何哈希提供响应。他们不验证它。哈希是任何64个字符串。请让我知道我可以使用哪种方式在PDF文档上附加PKCS7签名。
以下用于生成请求的代码:
if (System.IO.File.Exists(tempPdf))
System.IO.File.Delete(tempPdf);
using (PdfReader reader = new PdfReader(pdfReadServerPath))
{
using (FileStream os = System.IO.File.OpenWrite(tempPdf))
{
PdfStamper stamper = PdfStamper.CreateSignature(reader, os, '\0',null,true);
PdfSignatureAppearance appearance = stamper.SignatureAppearance;
appearance.SetVisibleSignature(new Rectangle(15, 15, 100, 100), 1, "sign1");
appearance.CertificationLevel = PdfSignatureAppearance.NOT_CERTIFIED;
AllPagesSignatureContainer external = new AllPagesSignatureContainer(appearance);
MakeSignature.SignExternalContainer(appearance, external, 8192);
Stream data = appearance.GetRangeStream();
Stream data = appearance.GetRangeStream();
byte[] hash = ReadFully(data); //Convert stream to byte
_signatureHash = hash;
}
}
//create sha256 message digest
using (SHA256.Create())
{
_signatureHash = SHA256.Create().ComputeHash(_signatureHash);
}
bool check = false;
string hexencodedDigest = null;
//create hex encoded sha256 message digest
hexencodedDigest = new BigInteger(1, _signatureHash).ToString(16);
hexencodedDigest = hexencodedDigest.ToUpper();
if (hexencodedDigest.Length == 64)
{
**Send this hexencoded hash to webservice**
}
以下用于添加签名的代码:
//DLL Call
eSign2_1_Request_Response req_resp = new eSign2_1_Request_Response();
//// Response XML Digest process
string resp_xml = Request.Form["msg"].ToString();//signature response XML;
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.LoadXml(resp_xml);
XmlElement EsignResp = xmlDoc.DocumentElement;
if (EsignResp.Attributes != null && EsignResp.Attributes["status"].Value != "1")
{
req_resp.WriteTextFileLog("errCode: " + EsignResp.Attributes["errCode"].Value + " & Error Message: " + EsignResp.Attributes["errMsg"].Value, "log", base_folder_path);
}
else
{
req_resp.WriteTextFileLog(resp_xml, "xml", base_folder_path + "\\" + file_withoutExtn + "_responseXML.txt");
//-------Continue to generate signed PDF by passing parameter to DLL
XmlNodeList nodeList = xmlDoc.GetElementsByTagName("Signatures");
string signature = nodeList[0].FirstChild.InnerText;
string signedPdf = @"D:\POC Hosted\TryNSDL\TryNSDL\wwwroot\TempPath\signedPdf.pdf";
string tempPdf = @"D:\POC Hosted\TryNSDL\TryNSDL\wwwroot\TempPath\tempPdf.pdf";
using (PdfReader reader = new PdfReader(tempPdf))
{
using (FileStream os = System.IO.File.OpenWrite(signedPdf))
{
byte[] encodedSignature = Convert.FromBase64String(signature);
IExternalSignatureContainer external = new MyExternalSignatureContainer(encodedSignature);
MakeSignature.SignDeferred(reader, "sign1", os, external);
}
}
}
Allsignature容器的代码:
public class AllPagesSignatureContainer : IExternalSignatureContainer
{
public AllPagesSignatureContainer(PdfSignatureAppearance appearance)
{
this.appearance = appearance;
}
public void ModifySigningDictionary(PdfDictionary signDic)
{
signDic.Put(PdfName.FILTER, PdfName.ADOBE_PPKMS);
signDic.Put(PdfName.SUBFILTER, PdfName.ADBE_PKCS7_DETACHED);
PdfStamper stamper = appearance.Stamper;
PdfReader reader = stamper.Reader;
PdfDictionary xobject1 = new PdfDictionary();
PdfDictionary xobject2 = new PdfDictionary();
xobject1.Put(PdfName.N, appearance.GetAppearance().IndirectReference);
xobject2.Put(PdfName.AP, xobject1);
PdfIndirectReference PRef = stamper.Writer.PdfIndirectReference;
PdfLiteral PRefLiteral = new PdfLiteral((PRef.Number + reader.NumberOfPages) + " 0 R");
for (int i = 2; i < reader.NumberOfPages+1; i++)
{
var signatureField = PdfFormField.CreateSignature(stamper.Writer);
signatureField.Put(PdfName.T, new PdfString("ClientSignature_" + i.ToString()));
signatureField.Put(PdfName.V, PRefLiteral);
signatureField.Put(PdfName.F, new PdfNumber("132"));
signatureField.SetWidget(new Rectangle(15, 15, 100, 100), null);
signatureField.Put(PdfName.SUBTYPE, PdfName.WIDGET);
signatureField.Put(PdfName.AP, xobject1);
signatureField.SetPage();
Console.WriteLine(signatureField);
stamper.AddAnnotation(signatureField, i);
}
}
public byte[] Sign(Stream data)
{
return new byte[0];
}
PdfSignatureAppearance appearance;
}
我在创建签名时使用了追加模式,所以没有签名。在Adobe Reader中,只有空白的签名可见:https://www.sendspace.com/file/5d1z0t
如果在没有appendmode PdfStamper stamper = PdfStamper.CreateSignature(reader, os, '\0');
和PdfLiteral PRefLiteral = new PdfLiteral((PRef.Number + 1 + 2 * (reader.NumberOfPages - 1)) + " 0 R");
的情况下尝试相同的操作
那么它可以正常运行:https://www.sendspace.com/file/agat9a,但是它只能用于单个签名者。 Nd如果我们再次尝试使用相同的pdf进行签名,则旧签名将无效。 (显然是因为未使用附加模式。)
我想为了使签名能够在附加模式下工作,需要在PdfLiteral
行中进行更改-我对相同的实际操作方式知之甚少。
签名文件:https://www.sendspace.com/file/5d1z0t 输入文件:https://www.sendspace.com/file/wh2h2y
答案 0 :(得分:3)
快速浏览代码会发现两个主要错误。
您对文档数据进行了两次哈希处理(为此使用了不同的API ...很奇怪!):
Stream data = appearance.GetRangeStream();
byte[] hash = DigestAlgorithms.Digest(data, "SHA256");
[...]
_signatureHash = hash;// signatureHash;
}
}
[...]
using (SHA256.Create())
{
_signatureHash = SHA256.Create().ComputeHash(_signatureHash);
}
这是错误的,这没有道理。
你说
请求Esign服务以PKCS7(CMS)格式给出响应。
但是,您可以尝试构建一个自己的CMS容器,而不是从结果中使用CMS签名容器,而是将Esign响应CMS容器注入,就像它只是一个签名哈希一样:
XmlNodeList UserX509Certificate = xmlDoc.GetElementsByTagName("UserX509Certificate");
byte[] rawdat = Convert.FromBase64String(UserX509Certificate[0].InnerText);
var chain = new List<Org.BouncyCastle.X509.X509Certificate>
{
Org.BouncyCastle.Security.DotNetUtilities.FromX509Certificate(new X509Certificate2(rawdat))
};
var signaturee = new PdfPKCS7(null, chain, "SHA256", false);
_signature = signaturee;
_signature.SetExternalDigest(Convert.FromBase64String(signature), null, "RSA");
byte[] encodedSignature = _signature.GetEncodedPKCS7(_hash, null, null, null, CryptoStandard.CMS);
根据您在XML中的注释
<DocSignature error="" id="1" sigHashAlgorithm="SHA256">--Signature in base 64 in PKCS7(CMS)---</DocSignature>
此DocSignature
元素包含CMS签名容器。
因此,删除上面的代码段,而是将DocSignature
元素的内容(不要忘记对base64进行解码)放入byte[] encodedSignature
中。现在,您可以像以前一样将其注入准备好的签名中:
IExternalSignatureContainer external = new MyExternalSignatureContainer(encodedSignature);
MakeSignature.SignDeferred(reader, "sign1", os, external);
在解决了上述问题之后,又出现了两个问题:
您打开流以这样写:
using (FileStream os = System.IO.File.OpenWrite(signedPdf))
File.OpenWrite
是documented on docs.microsoft.com
等同于
FileStream(String, FileMode, FileAccess, FileShare)
构造函数重载,文件模式设置为OpenOrCreate
,访问权限设置为Write
,共享模式设置为None
。
文件模式OpenOrCreate
依次为documented指定
操作系统应打开一个文件(如果存在);否则,应创建一个新文件。
因此,如果在给定位置已经有文件,则该文件将保留,您便开始写入该文件。
如果您创建的新文件比旧文件长,那么这没问题,您最终将覆盖所有旧文件内容,然后该文件将容纳更多新内容。
但是,如果您创建的新文件比旧文件短,那么您就会遇到问题:新文件结束后,仍然有旧的较长文件中的数据。因此,您的结果是两个文件的大杂烩。
在您共享示例文件的情况下发生了这种情况,“ signedPdf.pdf”的新内容只有175982字节长,但是似乎有一些旧文件的名称为811986字节长。因此,您共享的“ signedPdf.pdf”文件长811986字节,前175982字节包含操作结果,其余数据来自其他文件。
如果将共享的“ signedPdf.pdf”文件缩减为前175982个字节,结果看起来会好得多!
要解决此问题,您应该使用documented的文件模式Create
等效于请求如果文件不存在,请使用
CreateNew
;否则,请使用Truncate
。
using (FileStream os = new FileStream(signedPdf, FileMode.Create, FileAccess.Write, FileShare.None))
如上所述,如果将共享的“ signedPdf.pdf”文件缩减为前175982个字节,结果看起来会好得多!不幸的是,仅仅是更好而还不是很好:
通过查看详细信息,您“身份已过期或尚未生效”的原因变得更加清楚:
即PDF声明的签名时间为UTC + 1:09:47:59。
但要查看证书:
即您的证书有效时间不早于UTC + 1的09:48:40。
因此,您声明的签署时间超过了您的用户证书生效之前的半分钟!显然验证者无法接受这一点...
很明显,您的签名服务会根据需要为您创建一个短期证书,此证书从那时起有效半小时。在这段时间内开始创建PDF签名的时间是 not 。
我怀疑他们会根据您的要求更改签名服务的设计。因此,您将不得不作弊并在以后使用签名时间。
默认情况下,PdfSignatureAppearance
构造函数将签名时间设置为当前时间,即执行此行时:
PdfSignatureAppearance appearance = stamper.SignatureAppearance;
幸运的是,如果您立即使用,可以更改此声明的签名时间
appearance.SignDate = [some other date time];
您应该在此处使用的日期时间应在致电签名服务的时间之后不久(我建议不超过5分钟)。
这当然意味着您不能随意等待执行该服务调用。一旦您在上面指定了声明的签名时间,您便会成功地调用在声明的时间之前签署服务!
此外,如果签名服务仅反应缓慢或仅在重试后才响应,则您的软件应确定检查从其检索的签名容器中的证书,并将其有效期与声明的签名时间进行比较。如果声明的签名时间不在该间隔内,请再次开始签名!
现在很明显,您使用的AllPagesSignatureContainer
是为非常特殊的用例而设计的,仍然必须适合您的用例。
AllPagesSignatureContainer
适应追加模式本质上是从this answer复制的AllPagesSignatureContainer
实现在没有以附加模式签名时可以正常工作,但是在以附加模式签名时失败了。
起初这是合理的,因为该类必须预测将用于签名值的对象编号。此预测取决于确切的用例,并且打开附加模式会大大改变此用例。因此,我在评论中的建议是
如果需要追加模式,请尝试替换
在PdfLiteral PRefLiteral = ...
AllPagesSignatureContainer
中的行
PdfLiteral PRefLiteral = new PdfLiteral((PRef.Number + reader.NumberOfPages) + " 0 R");
在我的测试中有效,但在您的测试中却无效。对您签名文件的分析发现了原因:我的测试文件使用交叉引用表,而您的测试文件使用交叉引用流。
AllPagesSignatureContainer
适应追加模式和对象流附加模式下的iText使用原始文件的压缩功能,即,在您的文件中,iText在存储允许存储在对象流中的间接对象后立即创建对象流。
如果您的文件是iText,则为对象流保留一个对象号,并且在AllPagesSignatureContainer
预测签名值对象号与实际生成签名值的时间之间保留了该对象号。因此,文件中的实际签名值对象数比预测数高1。
因此,对于具有交叉引用流的PDF,要解决此问题,只需将
替换为PdfLiteral PRefLiteral = ...
行即可
PdfLiteral PRefLiteral = new PdfLiteral((PRef.Number + reader.NumberOfPages + 1) + " 0 R");
即通过在原始预测值上加1。不幸的是,现在对于具有交叉引用表的PDF来说,预测是错误的...
解决此问题的更好方法是强制iText在预测签名值对象编号之前使用对象编号为交叉参考流PDF保留对象编号,然后使用原始预测代码。一种实现方法是在预测之前立即创建和写入间接对象,例如像这样:
stamper.Writer.AddToBody(new PdfNull(), stamper.Writer.PdfIndirectReference, true);
PdfIndirectReference PRef = stamper.Writer.PdfIndirectReference;
PdfLiteral PRefLiteral = new PdfLiteral((PRef.Number + reader.NumberOfPages) + " 0 R");
The answer的AllPagesSignatureContainer
实现本质上是从其复制而来的。