Прочитайте следующие ссылки:
- Технический документ по цифровой подписи iText и примеры C#. (в частности, глава 4). другой отличный и краткое описание процесса подписания PDF.
- документация CAPICOM.
- Онлайн-примеры/вопросы здесь и в архивах списка рассылки 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 для верхнего/нижнего колонтитула страницы документа.
PdfPKCS7
создает контейнер подписи CMS/PKCS#7. С другой стороны, вы используете API, для которого подписанный ответ имеет формат PKCS#7. Таким образом, вам не нужен экземплярPdfPKCS7
. - person mkl   schedule 10.03.2015hash
уже является хеш-значением, т. е. интерфейс не должен его хэшировать. Поскольку я не знаю API CAPICOM, я не совсем уверен, что делает внешний код. Если он снова хэширует, вам придется его настроить. - person mkl   schedule 10.03.2015SignedData
Content
, которое задокументировано для хранения подписываемых данных, а не хэша данных, подлежащих подписи. Таким образом, вы действительно дважды хэшируете. К сожалению, я не вижу, как использовать CAPICOM для данных, которые вы уже хешировали. Таким образом, похоже, что вам нужно передавать весь диапазонный поток, что нецелесообразно. - person mkl   schedule 11.03.2015sign_IE
явно добавляет кAuthenticatedAttributes
. Возможно, вы также можете добавить атрибут MessageDigest таким же образом и не устанавливатьContent
(или установить для него значение null). Вероятно, можно так обмануть CAPICOM, чтобы он подписал желаемый дайджест. Но на самом деле это всего лишь дикая идея... - person mkl   schedule 13.03.2015PdfStamper
в директивеusing
. Таким образом, в ходеreturn View(f)
он частично деинициализируется; это происходит перед вызовомIndex
. Таким образом, ваше заполнение возвращенного контейнера подписи может быть выполнено неправильно. - person mkl   schedule 13.03.2015