Подписать несколько местоположений с помощью одной и той же XML-подписи ответа в PKCS7 (CMS) - PullRequest
2 голосов
/ 19 марта 2019

PDF-документ должен быть подписан под национальной цифровой идентификацией.
Национальная цифровая идентификация WebService предоставляет возможность подписывать документы, в мой проект я интегрировал то же самое.

Запрос услуг Esign дает ответ в формате PKCS7(CMS). Я хочу добавить один и тот же ответ в нескольких местах, поэтому я создаю несколько пустых контейнеров подписи и получаю ответ от службы.

Я отослал эту статью: Подписать PDF, используя ITextSharp и XML Signature

Но в данной статье у нас есть только одно местоположение подписи, но у меня есть несколько мест подписи.

Я использую библиотеку itext sharp. Использование MakeSignature.SignDeferred Метод для добавления подписи в нескольких местах, но он показывает PDF недействительным.

Ниже приведен XML-код ответа, который я получил от Webservice:

<?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>

РЕДАКТИРОВАТЬ: В соответствии с последним сообщением, веб-службы обеспечивают ответ на любой хэш предоставляется с моей стороны. Они не подтверждают это. Хеш - любая строка из 64 символов. Пожалуйста, дайте мне знать, каковы возможные способы, которыми я могу использовать это, чтобы добавить подпись PKCS7 в документ PDF.

Ниже код для генерации запроса:

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);
        }
    }
}

Код для подписи контейнера:

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,, но его можно использовать только для одного подписавшего. Если мы снова попытаемся использовать тот же pdf для отставки, тогда старые подписи станут недействительными. (очевидно, поскольку режим добавления не используется.)

Я полагаю, что для подписи для работы в режиме добавления требуется изменение в строке PdfLiteral - у меня меньше идей о том, как это работает на самом деле.

Подписанный файл: https://www.sendspace.com/file/5d1z0t Входной файл: https://www.sendspace.com/file/wh2h2y

1 Ответ

3 голосов
/ 19 марта 2019

Первый быстрый просмотр вашего кода выявил две основные ошибки.

Хеширование дважды

Вы дважды хешируете данные документа (для этого используются разные 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, внедряя контейнер CMS ответа Esign response.как если бы это был просто подписанный хеш:

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 - , задокументировано на docs.microsoft.com , чтобы быть

эквивалентно перегрузке конструктора FileStream(String, FileMode, FileAccess, FileShare) с режимом файла, установленным на OpenOrCreate, доступом к Write и режимом совместного использования, установленным на None.

Режим файла OpenOrCreate, в свою очередь, задокументирован для указания

, что операционная система должна открывать файл, если он существует;в противном случае должен быть создан новый файл.

Таким образом, , если в указанном месте уже есть файл, этот файл остается, и вы начинаете запись в него.

Если новый файл, который вы создаете, длиннее старого, это не проблема, вы в конечном итоге перезаписываете все содержимое старого файла, а затем файл увеличивается для размещения дополнительного нового содержимого.

Но если новый файл, который вы создаете, короче старого, у вас есть проблема: после окончания нового файла все еще остаются данные из старого, более длинного файла.Таким образом, ваш результат - сборка из двух файлов.

Это произошло в случае файлов с примерами, которыми вы поделились, ваш новый контент "signaturePdf.pdf" имеет длину всего 175982 байта, но, похоже, некоторые старыефайл с тем именем длиной 811986 байт.Таким образом, файл "signaturePdf.pdf", которым вы поделились, имеет длину 811986 байт, первые 175982 байта содержат результат вашей операции, остальные данные из какого-то другого файла.

Если вы урезаете свой общий "signaturePdf.pdf "файла до его первых 175982 байт, результат выглядит намного лучше!

Чтобы решить эту проблему, вы должны использовать режим файла Create, который задокументирован , чтобы быть

эквивалентно запросу, что если файл не существует, используйте CreateNew;в противном случае используйте Truncate.

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

Проблема с вашей службой подписи - идентификационная информация еще не действительна

Как упоминалось выше, если вы урезаете свою общую подписанную подписьPign.pdfmsgstr "файл на первые 175982 байта, результат выглядит намного лучше!К сожалению, просто лучше, но пока не очень хорошо:

Signature Panel

Причина, по которой ваша «личность истекла или еще не действительна», становится яснее, если взглянуть на детали.:

Signature Properties

Т.е. время подписания, заявленное в PDF, составляет 09:47:59 UTC + 1.

Но, глядя насертификат:

Certificate Viewer

Т.е. ваш сертификат действителен не ранее 09:48:40 UTC + 1.

Таким образом, заявленная подписьпрошло более полминуты, прежде чем ваш пользовательский сертификат вступил в силу!Это, очевидно, не может быть одобрено валидатором ...

Очевидно, ваша служба подписи создает для вас краткосрочный сертификат по требованию, действительный всего за полчаса.И время, когда вы начали создавать подпись PDF, в этом интервале составляет , а не .

Я сомневаюсь, что они изменят дизайн службы подписи под ваши требования. Таким образом, вам придется немного обмануть и немного использовать время подписания в будущем.

По умолчанию время подписи устанавливается текущим конструктором PdfSignatureAppearance, т. Е. Когда эта строка выполняется:

PdfSignatureAppearance appearance = stamper.SignatureAppearance;

К счастью, вы можете изменить заявленное время подписи, если вы сразу же используете

appearance.SignDate = [some other date time];

Дата, которую вы должны использовать здесь, должна быть вскоре (я бы предложил не более 5 минут) после того времени, когда вы позвоните в службу подписи.

Это, конечно, означает, что вы не можете произвольно ждать, пока не будет выполнен этот сервисный вызов. Как только вы назначили заявленное время подписания выше, вы обязуетесь успешно вызвать вашу подпись сервис незадолго до этого потребовал времени!

Кроме того, если служба подписи реагирует медленно или только после нескольких попыток, ваше программное обеспечение должно окончательно проверить сертификат в контейнере подписи, который вы извлекаете из него, и сравнить его срок действия с указанным вами временем подписания. Если заявленное время подписания не находится в этом интервале, начните подписывать снова!


Теперь стало очевидно, что AllPagesSignatureContainer, который вы использовали, был разработан для очень особого варианта использования и все же должен был быть адаптирован к вашему варианту использования.

Адаптация AllPagesSignatureContainer для режима добавления

Реализация AllPagesSignatureContainer по существу скопирована с этого ответа работала нормально, когда не выполняла подпись в режиме добавления, но при подписывании в режиме добавления она не удалась.

Сначала это было правдоподобно, потому что этот класс должен предсказать номер объекта, который будет использоваться для значения подписи. Этот прогноз зависит от конкретного варианта использования, и включение режима добавления значительно меняет этот вариант использования. Таким образом, мой совет в комментарии был

Если вам нужен режим добавления, попробуйте заменить

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");

Ответ Реализация AllPagesSignatureContainer, по существу скопированная с, была соответствующим образом обновлена.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...