Как подписать PDF-файл с помощью смарт-карты в веб-контексте с помощью iText?

Прочитайте следующие ссылки:

Код хеширования:

BouncyCastle.X509Certificate[] chain = Utils.GetSignerCertChain();
reader = Utils.GetReader();
MemoryStream stream = new MemoryStream();
using (var stamper = PdfStamper.CreateSignature(reader, stream, '\0'))
{
    PdfSignatureAppearance sap = stamper.SignatureAppearance;
    sap.SetVisibleSignature(
        new Rectangle(36, 740, 144, 770),
        reader.NumberOfPages,
        "SignatureField"
    );
    sap.Certificate = chain[0];
    sap.SignDate = DateTime.Now;
    sap.Reason = "testing web context signatures";

    PdfSignature pdfSignature = new PdfSignature(
        PdfName.ADOBE_PPKLITE, PdfName.ADBE_PKCS7_DETACHED
    );
    pdfSignature.Date = new PdfDate(sap.SignDate);
    pdfSignature.Reason = sap.Reason;
    sap.CryptoDictionary = pdfSignature;

    Dictionary<PdfName, int> exclusionSizes = new Dictionary<PdfName, int>();
    exclusionSizes.Add(PdfName.CONTENTS, SIG_BUFFER * 2 + 2);
    sap.PreClose(exclusionSizes);

    Stream sapStream = sap.GetRangeStream();
    byte[] hash = DigestAlgorithms.Digest(
        sapStream,
        DigestAlgorithms.SHA256
    );

// is this needed?
    PdfPKCS7 sgn = new PdfPKCS7(
        null, chain, DigestAlgorithms.SHA256, true
    );
    byte[] preSigned = sgn.getAuthenticatedAttributeBytes(
        hash, sap.SignDate, null, null, CryptoStandard.CMS
    );

    var hashedValue = Convert.ToBase64String(preSigned);
}

Простой тест - фиктивный документ Pdf создается при запросе начальной страницы, вычисляется хэш и помещается в скрытое поле ввода в кодировке Base64. (hashedValue выше)

Затем используйте CAPICOM на стороне клиента, чтобы отправить форму и получить подписанный пользователем ответ:

PdfSignatureAppearance sap = (PdfSignatureAppearance)TempData[TEMPDATA_SAP];
PdfPKCS7 sgn = (PdfPKCS7)TempData[TEMPDATA_PKCS7];
stream = (MemoryStream)TempData[TEMPDATA_STREAM];
byte[] hash = (byte[])TempData[TEMPDATA_HASH];

byte[] originalText = (Encoding.Unicode.GetBytes(hashValue));
// Oid algorithm verified on client side
ContentInfo content = new ContentInfo(new Oid("RSA"), originalText);

SignedCms cms = new SignedCms(content, true);
cms.Decode(Convert.FromBase64String(signedValue));
// CheckSignature does not throw exception
cms.CheckSignature(true);
var encodedSignature = cms.Encode();

/* tried this too, but no effect on result
sgn.SetExternalDigest(
    Convert.FromBase64String(signedValue),
    null,
    "RSA"
);
byte[] encodedSignature = sgn.GetEncodedPKCS7(
    hash, sap.SignDate, null, null, null, CryptoStandard.CMS
);
*/
byte[] paddedSignature = new byte[SIG_BUFFER];
Array.Copy(encodedSignature, 0, paddedSignature, 0, encodedSignature.Length);
var pdfDictionary = new PdfDictionary();
pdfDictionary.Put(
    PdfName.CONTENTS,
    new PdfString(paddedSignature).SetHexWriting(true)
);
sap.Close(pdfDictionary);

Так что прямо сейчас я не уверен, что я испортил часть хеширования, часть подписи или и то, и другое. Во фрагменте кода подписи выше и в клиентском коде (не показан) я вызываю то, что, как мне кажется, является кодом проверки подписи, но это тоже может быть неправильно, так как это впервые для меня. Получите печально известное сообщение «Документ был изменен или поврежден с момента его подписания» при открытии PDF-файла.

Клиентский код (не автором) можно найти здесь. Исходник имеет ошибку именования переменных, которая была исправлена. Для справки, в документации CAPICOM говорится, что подписанный ответ в формате PKCS#7.

РЕДАКТИРОВАНИЕ 2015-03-12:

После некоторых хороших указаний от @mkl и дополнительных исследований кажется, что CAPICOM практически непригоден для использования в этом сценарии. Хотя это не задокументировано четко (что еще нового?) в соответствии с здесь и здесь CAPICOM ожидает строку utf16 (Encoding.Unicode в .NET) в качестве входных данных для создания цифровой подписи. Оттуда он либо дополняет, либо усекает (в зависимости от того, какой источник в предыдущем предложении является правильным) любые данные, которые он получает, если длина является нечетным числом. т.е. создание подписи будет ВСЕГДА ОШИБАТЬСЯ, если Stream будет возвращено PdfSignatureAppearance.GetRangeStream() имеет нечетное число. Возможно, мне следует создать параметр Мне повезло: подписать, если длина ранжированного потока четная, и выдать InvalidOperationException, если нечетная. (грустная попытка пошутить)

Для справки, вот тестовый проект.

EDIT 25-03-2015:

Чтобы закрыть этот цикл, вот ссылка на VS 2013 ASP Проект .NET MVC. Возможно, это не лучший способ, но он действительно обеспечивает полностью работающее решение проблемы. Из-за странной и негибкой реализации подписывания CAPICOM, как описано выше, известно, что возможное решение потенциально потребует второго прохода и способа вставки дополнительного байта, если возвращаемое значение PdfSignatureAppearance.GetRangeStream() (опять же, Stream.Length) — нечетное число. Я собирался попробовать долгий и трудный путь, дополнив содержимое PDF, но, к счастью, коллега обнаружил, что дополнить PdfSignatureAppearance.Reason намного проще. Требование второго прохода, чтобы сделать что-то с iText [Sharp], не является беспрецедентным - например. добавление страницы x из y для верхнего/нижнего колонтитула страницы документа.


person kuujinbo    schedule 09.03.2015    source источник
comment
// это необходимо? - Это необходимо, только если используемый вами внешний API подписи просто возвращает подписанный дайджест; в этом случае экземпляр PdfPKCS7 создает контейнер подписи CMS/PKCS#7. С другой стороны, вы используете API, для которого подписанный ответ имеет формат PKCS#7. Таким образом, вам не нужен экземпляр PdfPKCS7.   -  person mkl    schedule 10.03.2015
comment
Однако имейте в виду, что hash уже является хеш-значением, т. е. интерфейс не должен его хэшировать. Поскольку я не знаю API CAPICOM, я не совсем уверен, что делает внешний код. Если он снова хэширует, вам придется его настроить.   -  person mkl    schedule 10.03.2015
comment
@mkl - извините за поздний ответ и спасибо за оба комментария. (в режиме только для чтения на работе, не спрашивайте) Глупо с моей стороны, но я никогда не думал о потенциальной проблеме двойного хеширования. Проведу еще немного тестирования и обновлю свой вопрос.   -  person kuujinbo    schedule 11.03.2015
comment
Я просмотрел документацию msdn. Действительно, внешний код помещает вычисленный хэш в свойство CAPICOM SignedData Content, которое задокументировано для хранения подписываемых данных, а не хэша данных, подлежащих подписи. Таким образом, вы действительно дважды хэшируете. К сожалению, я не вижу, как использовать CAPICOM для данных, которые вы уже хешировали. Таким образом, похоже, что вам нужно передавать весь диапазонный поток, что нецелесообразно.   -  person mkl    schedule 11.03.2015
comment
Я также просмотрел старый пример подписи iTextSharp (версия 4.x) с использованием CAPICOM. Этот код работал только потому, что он создавал подписи типа подписи PDF adbe.pkcs7.sha1, для которых хэш SHA1 ранжированного потока действительно является данными, встроенными в подпись PKCS#7 и подписанной ею. Это больше не реальный вариант, потому что A он требует использования SHA1, который в серьезных контекстах недействителен, и B его использование не рекомендуется, по крайней мере, начиная с ISO 32000-1 и будет официально признан устаревшим в ISO 32000-2.   -  person mkl    schedule 11.03.2015
comment
@mkl - еще раз СПАСИБО. То же самое наблюдалось в отношении того, как CAPICOM создает подписи: Цифровая подпись состоит из хэша содержимого, которое нужно подписать, и я не был уверен, правильно ли интерпретирую, но вы это подтвердили. Провел быстрый тест, пытаясь хэшировать весь диапазонный поток (закодированный в base64 и понимаю, что в большинстве случаев это нецелесообразно), но это тоже не сработало. Посмотрю еще немного...   -  person kuujinbo    schedule 11.03.2015
comment
@mkl - обновил вопрос. Заняло больше времени, чем должно было, но, наконец, определило проблему благодаря вам. Обидно, потому что для размера PDF-файлов, которые мы планируем подписать (максимум 1-2 МБ), производительность подписи всего ранжированного потока была не так уж и плоха. Если у вас есть другие идеи, это было бы очень признательно. Пожалуйста, суммируйте свои комментарии выше в ответ, и я приму.   -  person kuujinbo    schedule 13.03.2015
comment
Если у вас есть другие идеи - я видел, что функция sign.js sign_IE явно добавляет к AuthenticatedAttributes. Возможно, вы также можете добавить атрибут MessageDigest таким же образом и не устанавливать Content (или установить для него значение null). Вероятно, можно так обмануть CAPICOM, чтобы он подписал желаемый дайджест. Но на самом деле это всего лишь дикая идея...   -  person mkl    schedule 13.03.2015
comment
Кстати, я только что скачал ваш тестовый проект и посмотрел код: вы создаете PdfStamper в директиве using. Таким образом, в ходе return View(f) он частично деинициализируется; это происходит перед вызовом Index. Таким образом, ваше заполнение возвращенного контейнера подписи может быть выполнено неправильно.   -  person mkl    schedule 13.03.2015


Ответы (1)


Использование PdfPkcs7

Код на стороне сервера содержит этот блок после расчета дайджеста потока диапазона и перед отправкой данных на веб-страницу:

PdfPKCS7 sgn = new PdfPKCS7(
    null, chain, DigestAlgorithms.SHA256, true
);
byte[] preSigned = sgn.getAuthenticatedAttributeBytes(
    hash, sap.SignDate, null, null, CryptoStandard.CMS
);

var hashedValue = Convert.ToBase64String(preSigned);

В рассматриваемом случае в этом нет необходимости. Это необходимо только в том случае, если используемый вами внешний API подписи просто возвращает подписанный дайджест; в этом случае экземпляр PdfPKCS7 создает контейнер подписи CMS/PKCS#7. Вы, с другой стороны, используете API, для которого вы знаете

В документации CAPICOM указано, что подписанный ответ находится в формате PKCS#7.

Таким образом, вам не нужен и (что более важно) не следует использовать экземпляр PdfPKCS7.

Что подписывает sign.js

Содержимое переменной hash на стороне сервера уже является значением хеш-дайджеста данных для подписи. Таким образом, интерфейс, то есть используемый там sign.js, не должен хешировать его снова, чтобы получить значение атрибута дайджеста сообщения для помещения в подпись.

Но методы подписи sign.js для IE в конечном итоге выполняются

var signedData = new ActiveXObject("CAPICOM.SignedData");

// Set the data that we want to sign
signedData.Content = src;

SignedData.Content, с другой стороны, задокументировано как

Содержимое Чтение/запись Подписываемые данные.

(msdn: SignedData объект)

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

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

Но раньше были образцы подписи с помощью CAPICOM...

Действительно, в некоторых старых примерах подписи iTextSharp (версия 4.x) использовался CAPICOM. Но этот код работал только потому, что он создавал подписи типа подписи PDF adbe.pkcs7.sha1, для которых хэш SHA1 ранжированного потока действительно является данными, встроенными и подписанными PKCS#7. подпись.

Это больше не реальный вариант, потому что

  • это требует использования SHA1, который в серьезных контекстах недействителен, и
  • его использование не рекомендуется, по крайней мере, начиная с ISO 32000-1 (2008 г.) и будет официально объявлено устаревшим в ISO 32000-2 (в стадии разработки).
person mkl    schedule 13.03.2015
comment
Спасибо еще раз. И извините, проголосовали сегодня утром, но только сейчас отметили ответ. На следующей неделе также попробую ваши два других предложения. - person kuujinbo; 14.03.2015