【问题标题】:Sign multiple location with same response xml signature in PKCS7 (CMS)在 PKCS7(CMS)中使用相同的响应 xml 签名签署多个位置
【发布时间】:2019-03-19 09:22:51
【问题描述】:

PDF 文件需要用国家数字身份签名。
国家数字身份 WebService 提供签名文件的工具,在我的项目中我已经集成了相同的。

请求 Esign 服务以PKCS7(CMS) 格式给出响应。 我想在多个位置附加相同的响应,所以我创建了多个空签名容器发布我从服务接收响应。

我参考了这篇文章:Sign Pdf Using ITextSharp and XML Signature

但是在给定的文章中,我们只有一个签名位置,但我有多个签名位置。

我正在使用itextsharp 库。 使用MakeSignature.SignDeferred方法在多个位置附加签名但显示PDF无效。

请在下面找到我从 Webservice 收到的响应 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+TJ8oecpjCbmhPnDK2VgaJ2h00sfsfdsflIe/toKwAmV4PTBA1a5wkz77hj+HTkWXMkPEIsBUnBirVpHxe2bYaa7jcIIpWtJmqvcSurKTOeyFRa+AFWfwWHB/EzHJlDmgiMXzrNauxJ4HpphNaRU+bO5JdyzJs/8Zx4i6qwSEybkuprL3GdO9C7zMPiC98CTfO2dfUrbZWy1pSvwEqlVXQIfrkp+m2JRbFgT8EEIGfXUS+AJBPRwhY1Xsww==</SignatureValue>
        <KeyInfo>
            <KeyValue>
                <RSAKeyValue>
                    <Modulus>0o9vohWZ3ztI9ea8D/zUEUBRq6c82BE7sFmr1hNMeuGSJQFf39ceesRtGUzlUYVWXcU23P8sVZ5419CHh7ApFzUXaLD72i/2d5FFI0n3iRlTQec9PEUHyrvOCVDpqBhbnrO/EHBqRluUQJTQUtMu5mhPNFV7IIJMTEAsUhCL9adZXXQK9NeK0foRr29Oq7VdEGfSeLzHIibpQmhNPh89oJXqu0cmbNSW4J4i2GmwHQpmsmHaSQcgh4mgVrykO64pAKXPreAPipDHQM1l/e5hilYlWfLHxhC5OdfdfdsbTCTcydQ218IVulFOFhdQt7xVV61TOmoTC2elhWbDqoLJBVU5mBfQ==</Modulus>
                    <Exponent>AQAB</Exponent>
                </RSAKeyValue>
            </KeyValue>
            <X509Data>
                <X509SubjectName>CN=D-Random detail</X509SubjectName>
                <X509Certificate>--public certificate of provider--- </X509Certificate>
            </X509Data>
        </KeyInfo>
    </Signature>
</EsignResp>

编辑:根据最新的通信,Web 服务为我提供的任何散列提供响应。他们不验证它。哈希是任何 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 中只能看到空签名:/Fileremoved/

如果我在没有 appendmode 的情况下尝试相同 PdfStamper stamper = PdfStamper.CreateSignature(reader, os, '\0');PdfLiteral PRefLiteral = new PdfLiteral((PRef.Number + 1 + 2 * (reader.NumberOfPages - 1)) + " 0 R"); 然后它工作正常:/Fileremoved/,但它只能用于单一签名者。 Nd 如果我们再次尝试使用相同的 pdf 来重新签名,那么旧签名将变得无效。 (显然因为没有使用附加模式。)

我想要在附加模式下进行签名,需要在 PdfLiteral 行中进行更改 - 我不太了解它的实际工作原理。

签名文件:/Fileremoved/ 输入文件:/Fileremoved/

【问题讨论】:

  • PDF 格式仅支持位置。您可以多次出现,但 Acrobat 将无法通过验证。所以,一个位置和一个外观。
  • 同一位置可以在多个页面上重复吗?
  • This answer 分享了如何创建具有多个签名字段的 PDF 的想法,这些字段具有单独的可视化效果,它们都引用相同的签名值。注意:虽然此过程创建的内容不违反 PDF 规范的字母(仅禁止从多个页面引用相同字段对象的情况,无论是通过相同的小部件还是通过不同的小部件),但它显然确实违反了它的意图,它的精神。因此,此过程也可能被禁止作为规范勘误文档的一部分。
  • 感谢@mkl,但在显示的示例中,它在向 Web 服务发送请求之前使用证书链并设置签名容器,正如我的问题中所问的,我在同一个 XMl 符号中获取用户证书和 PKCS7 签名回应。
  • 当您使用返回完整 P7 签名容器的服务时,这并不是绝对必要的。您的public byte[] Sign(Stream data) 只需要对流数据进行哈希处理,从您的服务请求该哈希的 P7 签名,然后返回 P7 容器。仅当您希望签名小部件显示证书中的信息时,您需要事先获得证书。

标签: c# pdf itext signature pkcs#7


【解决方案1】:

第一次快速浏览您的代码发现了两个主要错误。

哈希两次

您对文档数据进行两次哈希处理(为此使用不同的 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 中的 cmets

    <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.OpenWritedocumented 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 个字节,结果看起来会好很多!

要解决这个问题,你应该使用文件模式Create,即documented

相当于请求如果文件不存在,使用CreateNew;否则,使用Truncate

using (FileStream os = new FileStream(signedPdf, FileMode.Create, FileAccess.Write, FileShare.None))

您的签名服务存在问题 - 身份尚未生效

如上所述,如果您将共享的“signedPdf.pdf”文件减少到前 175982 个字节,结果看起来会好很多!不幸的是,只是更好,还不是很好:

查看详细信息,您“身份已过期或尚未生效”的原因变得更加清晰:

即PDF 声明的签名时间为 09:47:59 UTC+1。

但是看证书:

即您的证书在 09:48:40 UTC+1 之前有效。

因此,声称的签名时间比您的用户证书生效前的半分钟多!这显然不能被验证者接受......

显然,您的签名服务会按需为您创建一个短期证书,从那时起有效期为半小时。而您开始创建 PDF 签名的时间不在该时间间隔内。

我怀疑他们会根据您的要求更改签名服务的设计。因此,您将不得不稍微作弊并在将来稍微使用签名时间。

默认情况下,PdfSignatureAppearance 构造函数将签名时间设置为当前时间,即当此行执行时:

PdfSignatureAppearance appearance = stamper.SignatureAppearance;

幸运的是,如果您立即使用,您可以更改此声明的签名时间

appearance.SignDate = [some other date time];

您应该在此处使用的日期时间必须在您致电签名服务的时间之后不久(我建议不超过 5 分钟)。

这当然意味着 您不能随意等到执行该服务调用。一旦您分配了上面声明的签名时间,您就承诺已成功调用您的在声称的时间之前不久签署服务!

此外,如果该签名服务的反应很慢或仅在重试后才反应,您的软件应明确检查您从中检索的签名容器中的证书,并将其有效期间隔与您声称的签名时间进行比较。如果声称的签名时间不在该时间间隔内,请重新开始签名!


现在很明显,您使用的 AllPagesSignatureContainer 是为一个非常特殊的用例而设计的,并且仍然必须适应您的用例。

AllPagesSignatureContainer 适配为追加模式

AllPagesSignatureContainer 实现基本上是从this answer 复制的,在未以附加模式登录时运行良好,但在以附加模式登录时却失败了。

起初这似乎是合理的,因为该类必须预测将用于签名值的对象编号。此预测取决于确切的用例,并且打开附加模式会显着改变此用例。因此,我在评论中的建议是

如果需要追加模式,请尝试替换

PdfLiteral PRefLiteral = ...

AllPagesSignatureContainer 中的一行

PdfLiteral PRefLiteral = new PdfLiteral((PRef.Number + reader.NumberOfPages) + " 0 R");

在我的测试中有效,但在您的测试中仍然没有。对您签名文件的分析发现了原因:我的测试文件使用了交叉引用表,而您的测试文件使用了交叉引用流。

为追加模式和对象流调整AllPagesSignatureContainer

附加模式下的 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 的实现基本上是从中复制而来的,已相应更新。

【讨论】:

  • 谢谢@mkl 我应用了您建议的更改,对于第一个错误“两次哈希”,现在我现在只使用一种哈希方法并创建临时 PDF,但创建的临时 PDF 显示“签名验证期间出错. BER 解码时遇到错误”。我尝试改变散列方式但得到同样的错误。我还根据您的 cmets 使用最新代码编辑了问题
  • “但创建的临时 pdf 显示” - 临时 pdf 文件“tempPdf.pdf”未签名。它正在等待嵌入签名容器。签名的 pdf 文件是“signedPdf.pdf”。如果您指的是后一个文件,请分享以供分析。
  • @urmivijayvargiya 查看我的答案的新部分,使用错误的文件模式您的签名服务存在问题 - 身份尚未生效
  • 感谢您的输入,@mkl 对于单个占位符它有效,现在我正在尝试多个页面复制相同的图像。
  • stackoverflow.com/questions/47526151/… - 引用了这个建议的链接,但我没有在 AllPagesSignatureContainer 中引用链,因为我在实际发送哈希签名之前设置注释。链仅在签名后可用。 @mkl,请为此问题提供意见。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-15
  • 2015-05-30
  • 2020-09-11
  • 2016-12-14
相关资源
最近更新 更多