13.12.2007 15:10:39CryptVerifyMessageSignature не работает Ответов: 11
Евгений
Добрый день! Нужна Ваша помощь. Установлена КриптоПро 3.0. Задача: подписать сообщение в формате PKCS#7, зашифровать. На противоположной стороне получить, расшифровать, проверить подпись. УЦ сгенерены контейнеры ключей, сертификаты ОК и удостоверяющих центров. На тестовую машину скопирован контейнер с ключами (на пользователя) и сертификаты.
Длительное изучение форума позволило разобраться с шифровкой-расшифровкой и даже :) сформировать подпись. Единственный вопрос с шифрованием такой: при шифровании обычно го *.txt размером 9 байт на выходе получаем файл в 1306 байт (подпись прикреплена к сообщению + сертификат).
Если подписываю оснасткой: csptest -lowsign -in *.txt -out **.txt -my sertname -sign -add получаю на выходе файл аналогичного размера 1306 байт. Только при просмотре в нем попадаются читабельные куски (само сообщение - исходный txt, имена удостоверяющих центров). Мое же сообщение нечитабельно. Это нормально? Или просто оснастка не кодирует сообщение?

А главный вопрос в проверке подписи. На выходе получаю нулевой файл. Все перелопатил - не могу ошибку найти. Может быть подскажете...
Итак: при проверке подписи использую тот же сертификат и то же хранилище (который использовался и при формировании подписи) (в тестовом режиме это возможно?)

текст процедуры:
procedure TForm1.N40Click(Sender: TObject); {Меню "Проверить подпись"}
var
hStoreHandle : HCERTSTORE; // Хранилище
enctype: Cardinal; // формат данных
pSignerCert: PCCERT_CONTEXT; // сертификат
pSignerName, s: PWideChar; // сайнер
VerifyParams: CRYPT_VERIFY_MESSAGE_PARA;
hProv: HCRYPTPROV;

pbSignedMessageBlob: PBYTE; // Указатель на буфер проверяемого сообщения
cbSignedMessageBlob: DWORD; // Размер проверяемого сообщения
pbDecodedMessageBlob: PBYTE; // Указатель на буфер декодированного сообщения
cbDecodedMessageBlob: PDWORD; // Указатель на DWORD определяющий размер

err: string;
f1: file;

const
CERT_STORE_NAME = 'MY'; // наименование хранилища
SIGNER_NAME = 'name'; // наименование сертификата

// Функция обратного вызова для структуры CRYPT_MESSAGE_VERIFY_PARA VerifyParams
function CryptGetSignerCertificateCallback (pvGetArg: pvoid;
dwCertEncodingType: DWORD;
pSignerID: PCERT_INFO;
hMSGCertStore: HCERTSTORE
):PCCERT_CONTEXT; stdcall;
begin
Result:=PCCERT_CONTEXT (pvGetArg);
end;

begin
CryptAcquireContextA(@hProv, 'cont', GOST_34, 75,0);
GetMem(s, 2*Length(CERT_STORE_NAME)+1);
StringToWideChar(CERT_STORE_NAME,s,2*Length(CERT_STORE_NAME)+1);

enctype := X509_ASN_ENCODING or PKCS_7_ASN_ENCODING;
// Открываем хранилище сертификата
hStoreHandle := CertOpenSystemStore(hProv, s);

GetMem(pSignerName, Length(SIGNER_NAME));
pSignerName := SIGNER_NAME;

pSignerCert:= CertFindCertificateInStore
(hStoreHandle,
enctype,
0,
CERT_FIND_SUBJECT_STR_A,
pSignerName,
nil);


FillChar(VerifyParams, sizeof(CRYPT_VERIFY_MESSAGE_PARA), #0);

VerifyParams.cbSize := sizeof(CRYPT_VERIFY_MESSAGE_PARA);
VerifyParams.dwMsgAndCertEncodingType := enctype;
VerifyParams.hCryptProv := hProv;
VerifyParams.pfnGetSignerCertificate := @CryptGetSignerCertificateCallback;
VerifyParams.pvGetArg := pSignerCert;

OpenDialog1.Title := 'Укажите файл для проверки подписи';
if OpenDialog1.Execute then
begin
AssignFile(f1, OpenDialog1.FileName);
reset(f1, 1);
cbSignedMessageBlob:= FileSize(f1);
GetMem(pbSignedMessageBlob, cbSignedMessageBlob);
BlockRead(f1, pbSignedMessageBlob^, cbSignedMessageBlob);
CloseFile(f1);
end;

CryptVerifyMessageSignature(@VerifyParams,
0, // единственнный подписчик
pbSignedMessageBlob, // Указатель на подпись
cbSignedMessageBlob, // Длина подписи
0, //
cbDecodedMessageBlob,
nil);

GetMem (pbDecodedMessageBlob,cbDecodedMessageBlob^);
CryptVerifyMessageSignature(@VerifyParams,
0,
pbSignedMessageBlob,
cbSignedMessageBlob,
pbDecodedMessageBlob,
cbDecodedMessageBlob,
nil);

SaveDialog1.Title := 'Сохранить раскодированное сообщение...';
if SaveDialog1.Execute then
begin
AssignFile(f1, SaveDialog1.FileName);
rewrite(f1, 1);
BlockWrite(f1, pbDecodedMessageBlob,cbDecodedMessageBlob^);
CloseFile (f1);
MessageDlg('Подпись успешно сохранена', mtInformation, [mbOK], 0);
end;

CertCloseStore (hStoreHandle,0);

end;


Выполнял пошагово - проверял: pSignerCert находит сертификат нормально. Ошибок в процессе выполнения нет. После первого вызова CryptVerifyMessageSignature pbDecodedMessageBlob равен nil, cbDecodedMessageBlob равна 0. На выходе сообщение размером 0 байт.
Где я накосячил, подскажите, плизззз.

И вдолгонку: а на конкретных клиентах куда устанавливать сертификат открытого ключа (в какое хранилище) и как к нему обращаться из программы (вместо 'MY')?
Спасибо за помощь!
 
Ответы:
13.12.2007 15:47:58Kirill Sobolev
На 1й взгляд, ошибка в вызове CryptVerifyMessageSignature - предпоследний параметр дб не переменная, а ее адрес, т.е. @cbDecodedMessageBlob.
То что у Вас сообщение нечитабельно - это нормально, Вы же его шифруете, а опция -lowsign делает только подпись.
Личные сертификаты у клиентов с ссылкой на секретный ключ тоже надо устанавливать в My.
14.12.2007 9:25:05Евгений
Добрый день, Кирилл! Спасибо за оперативный ответ.
Описание функции смотрел на MSDN, где предпоследний параметр был описан как "_inout DWORD* pcbDecoded". На С, правда, не работаю, поэтому в Дельфях описал параметр функции как pcbDecoded :PDWORD. Тем не менее воспользовался Вашим советом, в процедуре описал переменную так (не изменив описание функции):
//cbDecodedMessageBlob: PDWORD; //так было
cbDecodedMessageBlob: DWORD; // так стало
И при вызове функции передаю ее адрес:
CryptVerifyMessageSignature(@VerifyParams,
0,
pbSignedMessageBlob,
cbSignedMessageBlob,
0,
{вот здесь изменил!} @cbDecodedMessageBlob,
nil);

GetMem (pbDecodedMessageBlob,cbDecodedMessageBlob);

Результат, к сожалению, не изменился. После первого вызова функции cbDecodedMessageBlob равен 0. Реально д.б. 9 байт. В результате так и пишется 0й файл.

Плохо представляю себе алгоритм проверки подписи, поэтому уточню еще вот что: программу гоняю на тестовой машине. Оснасткой КриптоПро (с галочкой "для пользователя") на нее скопирован контейнер и сертификаты (УЦ и личный) (все сгенерено неким уд. центром). Больше никаких сертификатов не установлено. При создании подписи инициализирую криптопровайдер CryptAcquireContextA(@hProv, 'cont', GOST_34, 75,0); и использую хранилище 'MY', имя сертификата "name".
При проверке подписи (на этой же машине) обращаюсь к тому же криптопровайдеру, и использую то же хранилище 'MY' и ссылаюсь на тот же сертификат "name". Может быть в этом косяк?

Вопрос же о сертификатах на клиентах сформулировал не совсем точно:
Для каждого клиента удост. центром переданы дискеты со сгенеренными ключами и двумя файлами сертификатов (*.p7b - содержащий, как я понимаю сертификаты УД. центров; и *.cer - сертификат открытого ключа). На клиентах все это будет установлено, и при подписи сообщения сертификат программно можно будет отловить в контейнере 'MY'.
Общая структура представляет собой "нцать" клиентов и некий центр управления и контроля (ЦУК), куда будет стекаться информация от клиентов. Так вот, как я понимаю, для того, чтобы проверить подпись, скажем "Клиента №1", на ЦУКе нужно будет иметь (и подтягивать программно)сертификат открытого ключа Клиента №1. Верно? При этом ЦУК имеет свою пару ключей и свой сертификат в хранилище "Личные", к которому программно мы можем достучаться по имени 'MY'. Куда на ЦУКе необходимо установить сертификат открытого ключа Клиента №1 (в какой контейнер), и по какому имени (вместо 'MY') к этому контейнеру можно обратиться программно?
Спасибо за помощь!
14.12.2007 11:59:24Kirill Sobolev
А что возвращает сама функция и GetLastError?
Алгорит проверки подписи для тестирования вполне подходит.
Чтобы ЦУКу не возиться с установкой сертификатов клиентов, проще всего будет включать их в сами подписанные сообщения, если для Вас некритично 1-2кб дополнительного траффика. А вообще Вы можете, например, создать свое хранилище для таких сертификатов и туда их устанавливать.
14.12.2007 13:10:32Евгений
Добрый день!
А вот это мой косяк - когда набрасывал программу вчерновую не прописал GetLastError. Оказывается, CryptVerifyMessageSignature не отрабатывает - функция возвращает 0. GetLastError выдает 2148086027.

Возник такой вопрос: если сертификаты могут быть включены (а насколько я понял они и так внутри сообщения, т.к. при подписи у меня SigParams.cMsgCert := 1) в сами сообщения и принимающей стороне не обязательно иметь сертификат открытого ключа, то зачем мы обращаемся к конкретному криптопровайдеру и вытаскиваем из хранилища сертификат? Если я Вас правильно понял, то на принимающей стороне функции CryptVerifyMessageSignature достаточно знать enctype? А чем тогда гарантируется авторство отправителя? Я наивно полагал, что для того, чтобы в ЦУКе проверить подпись Клиента № необходимо иметь в распоряжении ЦУКа сертификат открытого ключа Клиента?
14.12.2007 13:58:32Kirill Sobolev
2148086027 - встречено неверное значение тега ASN1, т.е. на вход подается что-то битое, возможно неправильно расшифровывается перед этим.
Зачем Вы вытаскиваете сертификат из хранилища если у Вас он в сообщении - я не знаю :) И если не задавать callback функцию, то по умолчанию поиск как будет поиск в своем сообщении. Enctype - всегда одинаковый, по крайней мере пока. Авторство же отправителя гарантируется его сертификатом, ГОСТ обеспечивает неотрекаемость.
14.12.2007 14:35:10Евгений
Сообщение перед проверкой подписи не шифровалось! Сразу после подписи проверяем подпись(прога тестится). Попробовал, кстати, проверить подпись оснасткой криптопро файла, который подписал своей программой. Вылазит "...неверное значение тэга...". Получается, что проблема возникла еще при подписи.
Проверил: криптопровайдер инициализируется, личный сертификат находится и подтягивается нормально (проверял - сравнивал по s/n сертификата). Получается ошибка может быть только в SigParams.
Итого суть вопрос:
у меня присвоено:
SigParams.HashAlgorithm.pszObjId := szOID_CP_GOST_R3411;
где
const
szOID_CP_GOST_R3411 = '1.2.643.2.2.9';
Высмотрел где-то на форуме - это значение верно?

Спасибо!
14.12.2007 15:20:30Kirill Sobolev
Да, верно.
Если бы ошибка была в подписи или SigParams, то у Вас бы CryptSignMessage не отрабатывала, а я так понял что она ошибок не выдает. Возможно, проблема в сохранении подписанного сообщения.
15.12.2007 12:11:15Евгений
Здравствуйте, Кирилл!
Вы оказались абсолютно правы - косяк был при сохранении подписанного сообщения. Обнаружил, исправил - на тестовой машине все заработало.

Осталась еще одна проблема. Теперь проверяю прогу следующим образом: есть две машины - Клиент №1 и ЦУК.
(На Клиенте)Сообщение -> 1 шаг: Подпись -> 2 шаг:Шифрование -> (3 шаг: передача в ЦУК) -> (На ЦУКе) 4 шаг: расшифровка -> 5 шаг: проверка подписи -> 6 шаг: сохранение сообщения.

Если я все эти действия реализую на одной машине (при шифровании и расшифровке использую один контейнер 'MY' и ссылаюсь на один сертификат "name" - все работает. Входное и выходное тестовое сообщение идентичны.

Если я реализую по этой схеме обмен Клиент - ЦУК, происходит следующий косяк:
на ЦУКе расшифровка проходит успешно! Сообщение после выполнения 4 шага(на ЦУКе) и 1 шага (на Клиенте) идентичны!
Но проверка подписи на ЦУКе не проходит. Первый вызов CryptVerifyMessageSignature проходи без ошибки, функция возвращает 1. Нормально определяется размер расшифрованного сообщения - cbDecodedMessageBlob. А вот на втором вызове функции происходит косяк: функция возвращает 1, а GetLastError 2148073478. В результате сохраняется файл размером 0.

При этом, если я беру расшифрованный на ЦУКе файл(после выполнения 4 шага) и проверку подписи этого файла делаю на клиенте - ошибки нет. На выходе получаю валидный файл.
В чем может быть проблема?
Спасибо за помощь!
17.12.2007 11:24:52Kirill Sobolev
Попробуйте сертификат подписчика не брать из хранилища а передавать с самом сообщении, соответственно callback функциями не пользоваться.
18.12.2007 10:17:02Евгений
Здравствуйте, Кирилл!
Все заработало! Косяк нашелся в VerifyParams и callback функции. Большое спасибо за помощь! Вы мне очень помогли!

Остался еще маленький вопросик, но может быть и в нем дадите подсказку! Каждый сертификат имеет следующие поля (их видно при просмотре сертификата КриптоПрошной оснасткой):
f.e. Субъект: E=a@a.ru, C=RU, S=область, L=Город, O=Имя организации, CN=name, OID.1.2.840.113549.1.9.2=INN.
На клиенте вытаскиваю сертификат из хранилища, либо в ЦУКе вытаскиваю сертификат из полученного и расшифрованного сообщения. Можно ли программно (есть ли функции) вытащить эти данные из сертификата? Подскажите, плиззз, если есть, какие функции посмотреть...
Спасибо за помощь!
18.12.2007 11:59:32Kirill Sobolev
Пожалуйста
Субъект в виде строки из CERT_CONTEXT проще всего получить функций CertGetNameString.