在 PKCS7 (CMS) 中使用相同响应 xml 签名对多个位置进行签名

Sign multiple location with same response xml signature in PKCS7 (CMS)

PDF 文档需要使用国家数字身份进行签名。
国家数字身份 WebService 提供签署文件的便利,在我的项目中我集成了同样的功能。

请求电子签名服务以PKCS7(CMS)格式给出响应。 我想在多个位置附加相同的响应,所以我正在创建多个空签名容器 post 我收到来自服务的响应。

我参考了这篇文章:

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

我正在使用 itext sharp 库。 使用 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, '[=12=]',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/

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

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

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

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

散列两次

您对文档数据进行了两次哈希处理(为此使用了不同的 API……很奇怪!):

        Stream data = appearance.GetRangeStream();

        byte[] hash = DigestAlgorithms.Digest(data, "SHA256");

        [...]

        _signatureHash = hash;// signatureHash;
    }
}

[...]
using (SHA256.Create())
{
    _signatureHash = SHA256.Create().ComputeHash(_signatureHash);
}

这是错误的,没有意义。

注入错误的签名容器

你说

Requesting Esign services give response in PKCS7(CMS) format.

但是您并没有使用结果中的 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.OpenWritedocumented on docs.microsoft.com

equivalent to the FileStream(String, FileMode, FileAccess, FileShare) constructor overload with file mode set to OpenOrCreate, the access set to Write, and the share mode set to None.

文件模式OpenOrCreate依次为documented指定

that the operating system should open a file if it exists; otherwise, a new file should be created.

因此,如果给定位置已经有一个文件,该文件将保留下来,您可以开始写入它。

如果您创建的新文件比旧文件长,这没问题,您最终会覆盖所有旧文件内容,然后文件会增长以容纳额外的新内容。

但是如果您创建的新文件比旧文件短,您就会遇到一个问题:在新文件结束后,仍然有来自旧的、更长的文件的数据。因此,您的结果是两个文件的大杂烩。

这发生在您共享的示例文件的情况下,"signedPdf.pdf" 的新内容只有 175982 字节长,但似乎有一些具有该名称的旧文件长 811986 字节。因此,您共享的 "signedPdf.pdf" 文件长 811986 字节,前 175982 字节包含您的操作结果,其余数据来自其他文件。

如果您将共享的 "signedPdf.pdf" 文件减少到它的前 175982 个字节,结果看起来会好得多!

要解决此问题,您应该使用文件模式 Create,即 documented

equivalent to requesting that if the file does not exist, use CreateNew; otherwise, use Truncate.

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

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

如上所述,如果您将共享 "signedPdf.pdf" 文件缩减到其前 175982 字节,结果看起来会好得多!不幸的是更好,还不够好:

您 "identity has expired or is not yet valid" 的原因通过查看详细信息变得更加清晰:

即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

基本上从 this answer 复制的 AllPagesSignatureContainer 实现在不以追加模式登录时工作正常,但在以追加模式登录时失败。

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

If you need append mode, try to replace the

PdfLiteral PRefLiteral = ...

line in the AllPagesSignatureContainer by

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 实现基本上是从中复制的,已相应更新。