Продолжая разговор на тему электронных подписей (далее ЭП), надо сказать о проверке. В предыдущей стать я разбирал более сложную часть задачи — создание подписи. В этой статье всё несколько проще. Большая часть кода это адаптация примеров из КРИПТО ПРО .NET SDK. Проверять будем в первую очередь подписи по ГОСТ Р 34.10-2001 и ГОСТ Р 34.10-2012, для этого нам и нужен КРИПТО ПРО.
Задача для нас разбивается на 3 части: отделённая подпись, подпись в PDF и подпись в MS Word.
Проверка отделённой подписи:
Комментарии все в коде, обращу только ваше внимание на получение сертификата, он нам понадобиться далее, т.к. сертификат мы будем проверять отдельно.
Ну и не забываем оборачивать всё в try-catch и прочие using. В примере я этого намеренно не делаю, что бы сократить объём
Валидация подписи в PDF. Тут нам понадобиться iTextSharp (актуальная версия на момент написания 5.5.13):
Комментировать опять же особенно нечего. Разве что надо сказать о Oid «1.2.840.113549.1.9.5» — это Oid даты подписания.
И последний в нашем списке это docx, пожалуй самый простой вариант:
Теперь будем разбирать сертификат и валидировать всю цепочку сертификатов. Поэтому сборка должна работать из под пользователя, у которого есть доступ в сеть.
И тут начинается ад, т.к. я не знаю как получить информацию о владельце сертификата через Oid, поэтому буду парсить строку. Смейтесь громче: цирк начинается.
А если серьёзно, то милости прошу в комменты тех, кто знает как сделать это через Oid-ы:
Код максимально сокращён, для лучшего понимания сути.
В общем это всё, жду комментариев по получению Oid-ов из сертификата и любой аргументированной критики.
Задача для нас разбивается на 3 части: отделённая подпись, подпись в PDF и подпись в MS Word.
Проверка отделённой подписи:
//dataFileRawBytes - массив байт подписанного файла
ContentInfo contentInfo = new ContentInfo(dataFileRawBytes);
SignedCms signedCms = new SignedCms(contentInfo, true);
//signatureFileRawBytes - массив байт подписи
signedCms.Decode(signatureFileRawBytes);
if (signedCms.SignerInfos.Count == 0)
{
//обработка в случае отсутствия подписей
}
foreach (SignerInfo signerInfo in signedCms.SignerInfos)
{
//получаем дату подписания
DateTime? signDate =
(signerInfo.SignedAttributes
.Cast<CryptographicAttributeObject>()
.FirstOrDefault(x => x.Oid.Value == "1.2.840.113549.1.9.5")
?.Values[0]
as Pkcs9SigningTime)?.SigningTime;
bool valid;
try
{
signerInfo.CheckSignature(true);
valid = true;
}
catch (CryptographicException exc)
{
valid = false;
}
//получаем сертификат для проверки. Пригодится при проверке сертификата
X509Certificate2 certificate = signerInfo.Certificate;
Комментарии все в коде, обращу только ваше внимание на получение сертификата, он нам понадобиться далее, т.к. сертификат мы будем проверять отдельно.
Ну и не забываем оборачивать всё в try-catch и прочие using. В примере я этого намеренно не делаю, что бы сократить объём
Валидация подписи в PDF. Тут нам понадобиться iTextSharp (актуальная версия на момент написания 5.5.13):
using (MemoryStream fileStream = new MemoryStream(dataFileRawBytes))
using (PdfReader pdfReader = new PdfReader(fileStream))
{
AcroFields acroFields = pdfReader.AcroFields;
//получаем названия контейнеров подписей
List<string> signatureNames = acroFields.GetSignatureNames();
if (!signatureNames.Any())
{
//обработка отсутствия ЭП
}
foreach (string signatureName in signatureNames)
{
//далее следует магия получения подписи из контейнера
PdfDictionary singleSignature = acroFields.GetSignatureDictionary(signatureName);
PdfString asString1 = singleSignature.GetAsString(PdfName.CONTENTS);
byte[] signatureBytes = asString1.GetOriginalBytes();
RandomAccessFileOrArray safeFile = pdfReader.SafeFile;
PdfArray asArray = singleSignature.GetAsArray(PdfName.BYTERANGE);
using (
Stream stream =
new RASInputStream(
new RandomAccessSourceFactory().CreateRanged(
safeFile.CreateSourceView(),
asArray.AsLongArray())))
{
using (MemoryStream ms = new MemoryStream((int)stream.Length))
{
stream.CopyTo(ms);
byte[] data = ms.GetBuffer();
ContentInfo contentInfo = new ContentInfo(data);
SignedCms signedCms = new SignedCms(contentInfo, true);
signedCms.Decode(signatureBytes);
bool checkResult;
//получили подпись и проверяем её, без проверки сертификата
try
{
signedCms.CheckSignature(true);
checkResult = true;
}
catch (Exception)
{
checkResult = false;
}
foreach (SignerInfo signerInfo in signedCms.SignerInfos)
{
//получаем дату подписания
DateTime? signDate = (signerInfo.SignedAttributes
.Cast<CryptographicAttributeObject>()
.FirstOrDefault(x =>
x.Oid.Value == "1.2.840.113549.1.9.5")
?.Values[0]
as Pkcs9SigningTime)?.SigningTime;
//получаем сертификат
X509Certificate2 certificate = signerInfo.Certificate;
}
}
}
}
}
Комментировать опять же особенно нечего. Разве что надо сказать о Oid «1.2.840.113549.1.9.5» — это Oid даты подписания.
И последний в нашем списке это docx, пожалуй самый простой вариант:
using (MemoryStream fileStream = new MemoryStream(dataFileRawBytes))
using (Package filePackage = Package.Open(fileStream))
{
PackageDigitalSignatureManager digitalSignatureManager =
new PackageDigitalSignatureManager(filePackage);
if (!digitalSignatureManager.IsSigned)
{
//обрабатываем ситуацию отсутствия подписей
}
foreach (PackageDigitalSignature signature in
digitalSignatureManager.Signatures)
{
DateTime? signDate = signature.SigningTime;
bool checkResult = signature.Verify() == VerifyResult.Success;
//обратите внимание на способ получения сертификата
X509Certificate2 certificate =
new X509Certificate2(signature.Signer);
}
}
Теперь будем разбирать сертификат и валидировать всю цепочку сертификатов. Поэтому сборка должна работать из под пользователя, у которого есть доступ в сеть.
И тут начинается ад, т.к. я не знаю как получить информацию о владельце сертификата через Oid, поэтому буду парсить строку. Смейтесь громче: цирк начинается.
А если серьёзно, то милости прошу в комменты тех, кто знает как сделать это через Oid-ы:
private static void FillElectronicSignature(X509Certificate2 certificate)
{
foreach (KeyValuePair<string, string> item in ParseCertificatesSubject(certificate.Subject))
{
switch (item.Key)
{
case "C":
string certificatesCountryName =
item.Value;
break;
case "S":
string certificatesState =
item.Value;
break;
case "L":
string certificatesLocality =
item.Value;
break;
case "O":
string certificatesOrganizationName =
item.Value;
break;
case "OU":
string certificatesOrganizationalUnitName =
item.Value;
break;
case "CN":
string certificatesCommonName =
item.Value;
break;
case "E":
string certificatesEmail =
item.Value;
break;
case "STREET":
string certificatesStreet =
item.Value;
break;
//тут интересный момент, если Window русскоязычный, то КРИПТО ПРО вернёт ИНН, а если англоязычный, то INN
//именно тут начиналась не пойми что после deploy на тестовый стенд
//локально работает, на тестовом - нет
case "ИНН":
case "INN":
case "1.2.643.3.131.1.1":
string certificatesInn =
item.Value;
break;
//аналогично предыдущему
case "ОГРН":
case "OGRN":
case "1.2.643.100.1":
string certificatesOgrn =
item.Value;
break;
//аналогично предыдущему
case "СНИЛС":
case "SNILS":
case "1.2.643.100.3":
string certificatesSnils =
item.Value;
break;
case "SN":
string certificatesOwnerLastName =
item.Value;
break;
case "G":
string certificatesOwnerFirstName =
item.Value;
break;
//тут рекомендую добавить блок default и всё что не удалось определить ранее писать в лог
}
}
DateTime certificateNotBefore =
certificate.NotBefore;
DateTime certificateNotAfter =
certificate.NotAfter;
string certificatesSerialNumber =
certificate.SerialNumber;
if (!certificate.Verify())
{
//строим цепочку сертификатов
using (X509Chain x509Chain = new X509Chain())
{
x509Chain.Build(certificate);
//получаем все ошибки цепочки
X509ChainStatus[] statuses = x509Chain.ChainStatus;
//собираем все флаги ошибок в один int, так проще хранить
int certificatesErrorCode =
statuses.Aggregate(X509ChainStatusFlags.NoError,
(acc, chainStatus) => acc | chainStatus.Status, result => (int)result);
}
}
}
/// <summary>
/// Разобрать строку с данными о владельце сертификата
/// </summary>
private static Dictionary<string, string> ParseCertificatesSubject(string subject)
{
Dictionary<string, string> result = new Dictionary<string, string>();
//количество двойных кавычек, для определения конца значения
int quotationMarksCount = 0;
//признак что сейчас обрабатывается "ключ или значение"
bool isKey = true;
//переменная для сбора ключа
string key = string.Empty;
//Переменная для сбора значения
string value = string.Empty;
for (int i = 0; i < subject.Length; i++)
{
char c = subject[i];
if (isKey && c == '=')
{
isKey = false;
continue;
}
if (isKey)
key += c;
else
{
if (c == '"')
quotationMarksCount++;
bool isItemEnd = (c == ',' && subject.Length >= i + 1 && subject[i + 1] == ' ');
bool isLastChar = subject.Length == i + 1;
if ((isItemEnd && quotationMarksCount % 2 == 0) || isLastChar)
{
if (isItemEnd)
i++;
if (isLastChar)
value += c;
isKey = true;
if (value.StartsWith("\"") && value.EndsWith("\""))
value = value.Substring(1, value.Length - 2);
value = value.Replace("\"\"", "\"");
result.Add(key, value);
key = string.Empty;
value = string.Empty;
quotationMarksCount = 0;
continue;
}
value += c;
}
}
return result;
}
Код максимально сокращён, для лучшего понимания сути.
В общем это всё, жду комментариев по получению Oid-ов из сертификата и любой аргументированной критики.