Статус: Новичок
Группы: Участники
Зарегистрирован: 17.01.2020(UTC) Сообщений: 5 Сказал(а) «Спасибо»: 3 раз
|
Добрый день. В конце прошлого года перешли на использование сертификата 2012 госта, но для зашифрования всё ещё использовался сертификат ФСС 2001 госта. Процесс был отлажен и работал. Сейчас нужно и шифровать сертификатом 2012 госта, так как 2001 больше не используется. С этим возникла проблема. Попытка просто использовать новые параметры шифрования (указанные в спецификации ФСС) ничего не дала. Попробовали использовать библиотеку скачанную по ссылке с сайта ФСС (http://www.api-fss.ru/) для зашифрования, с использованием тех же сертификатов. На сайте ФСС указано что автор библиотеки "Сернов И.Е., Санкт-Петербург, АО «Адмиралтейские верфи»". Так как библиотека от стороннего разработчика и похоже сама содержит ошибки (не полностью соответствует документации и падает в неожиданных местах) использовать её в "боевых" целях нельзя. Но с её помощью получилось сформировать зашифрованный запрос, который ФСС успешно расшифровал и прислал ожидаемый зашифрованный ответ. Ответ тоже удалось расшифровать без проблем. У нас зашифрование производится в Delphi с использованием CryptoPro CSP 4.0.9971. Ниже приводится код зашифрования. Код разбит регионами с комментариями, созданными на основе спецификации ФСС. Внутри регионов имеются комментарии непосредственно к коду, но вначале каждого региона вплоть до фрагмента "//---------" - копипаста из спецификации, без каких-либо изменений.
Код:
procedure EncryptData(ACryptoProvider: HCRYPTPROV; ARemotePublicKeyBlob: TBytes;
AData: TBytes; out AEncryptedResult, ATransportBlob: TBytes);
var
hEphemeralKey: HCRYPTKEY;
hAgreeKey: HCRYPTKEY;
hSessionKey: HCRYPTKEY;
size: DWORD;
keyParam: DWORD;
sourceDataLen: DWORD;
encryptDataLen: DWORD;
providerPublicKeyBlob: TBytes;
ephemPublicKeyBlob: ^CRYPT_PUBLICKEYBLOB absolute providerPublicKeyBlob;
sessionKeyBlob: TBytes;
sessionKeySimpleBlob: ^CRYPT_SIMPLEBLOB_GOST28147 absolute sessionKeyBlob;
initVector: TBytes;
encryptedData: TBytes;
provType: DWORD;
transport: TGostR3410KeyTransport;
begin
try
size := SizeOf(provType);
CheckCryptoCall(CryptGetProvParam(ACryptoProvider, PP_PROVTYPE, @provType, @size, 0));
Assert(provType = PROV_GOST_2012_256);
{$region '1. Создание случайного сессионного ключа.'}
// При работе с ключами на алгоритме ГОСТ 2012 следует инициализировать генератор
// параметрами шифрования TK26Z (предоставляется провайдером).
// Алгоритм шифрования GOST28147.
// ---------
CheckCryptoCall(CryptGenKey(ACryptoProvider, CALG_G28147, CRYPT_EXPORTABLE, @hSessionKey));
// Параметры шифрования TK26Z, на криптопровайдере по ГОСТ 2012, установлены поумолчанию
// на всякий случай перепроверяем.
CheckCryptoCall(CryptGetKeyParam(hSessionKey, KP_CIPHEROID, nil, @size, 0));
SetLength(keyParamAsBytes, size);
CheckCryptoCall(CryptGetKeyParam(hSessionKey, KP_CIPHEROID, @keyParamAsBytes[0], @size, 0));
Assert(TEncoding.ANSI.GetString(keyParamAsBytes) = '1.2.643.7.1.2.5.1.1'#0); // szOID_Gost28147_89_TC26_Z_ParamSet + #0
// Устанавливать эти параметры несмотря на то что они уже установлены, пробовал эффект тот-же,
// но проверки должно быть достаточно чтобы не переживать на этот счёт.
{$endregion}
{$region '2. Зашифрование сессионного ключа.'}
// ---------
{$region '2.1. Создание шифратора для зашифрования ключа.'}
// Применяется алгоритм трансформации "urn:ietf:params:xml:ns:cpxmlsec:algorithms:transport-gost2001".
// Cоздается эфемерный ключ, который согласуется с открытым ключом получателя,
// и формируется ключ согласования (на котором будет зашифрован сессионный ключ);
// ---------
// Формирование эфемерного ключа в качестве локального закрытого ключа
// отправителя зашифрованного сообщения.
// В принципе, работать будет и на обычном локальном закрытом ключе (CryptGetUserKey).
CheckCryptoCall(CryptGenKey(ACryptoProvider, CALG_DH_GR3410_12_256_EPHEM, CRYPT_EXPORTABLE, @hEphemeralKey));
// Проверяем что параметры эфемерного ключа также установлены в TK26Z
CheckCryptoCall(CryptGetKeyParam(hEphemeralKey, KP_CIPHEROID, nil, @size, 0));
SetLength(keyParamAsBytes, size);
CheckCryptoCall(CryptGetKeyParam(hEphemeralKey, KP_CIPHEROID, @keyParamAsBytes[0], @size, 0));
Assert(TEncoding.ANSI.GetString(keyParamAsBytes) = '1.2.643.7.1.2.5.1.1'#0); // szOID_Gost28147_89_TC26_Z_ParamSet + #0
// Экспорт открытого ключа в BLOB из локального закрытого.
CheckCryptoCall(CryptExportKey(hEphemeralKey, 0, PUBLICKEYBLOB, 0, nil, @size));
SetLength(providerPublicKeyBlob, size);
CheckCryptoCall(CryptExportKey(hEphemeralKey, 0, PUBLICKEYBLOB, 0, @providerPublicKeyBlob[0], @size));
// Получение ключа согласования импортом открытого ключа получателя зашифрованного сообщения
// на локальном закрытом ключе отправителя.
CheckCryptoCall(CryptImportKey(
ACryptoProvider, @ARemotePublicKeyBlob[0], Length(ARemotePublicKeyBlob), hEphemeralKey, 0, @hAgreeKey));
// Установка алгоритма ключа согласования
// Пробовал и CALG_PRO12_EXPORT и CALG_PRO_EXPORT, всё-равно наше сообщение расшифровать не могут
// а мы их расшифровываем только с CALG_PRO_EXPORT;
keyParam := CALG_PRO12_EXPORT;
CheckCryptoCall(CryptSetKeyParam(hAgreeKey, KP_ALGID, @keyParam, 0));
{$endregion}
{$region '2.2. Создание блока KeyInfo с сертификатом;'}
// ---------
// Этот пункт опускаем, так как он делается уровнем выше в xml.
{$endregion}
{$region '2.3. Шифрование сессионного ключа происходит с помощью указанного асимметричного ключа (ГОСТ Р 34.10).'}
// Cессионный ключ используется для шифрования данных и в свою очередь так же шифруется.
// CALG_DH_EL_EPHEM - идентификатор алгоритма обмена ключей по Диффи-Хеллману на базе закрытого ключа
// эфемерной пары. Открытый ключ получается по ГОСТ Р 34.10 2001.
// CALG_DH_GR3410_12_256_EPHEM - идентификатор алгоритма обмена ключей по Диффи-Хеллману на базе
// закрытого ключа эфемерной пары. Открытый ключ получается по ГОСТ Р 34.10 2012 (256 бит).
// CALG_DH_GR3410_12_512_EPHEM - идентификатор алгоритма обмена ключей по Диффи-Хеллману на базе
// закрытого ключа эфемерной пары. Открытый ключ получается по ГОСТ Р 34.10 2012 (512 бит).
// ---------
// Экспорт сессионного ключа в BLOB
// Используем полученный ранее ключ согласования для зашифрования и экспорта сессионного ключа
CheckCryptoCall(CryptExportKey(hSessionKey, hAgreeKey, SIMPLEBLOB, 0, nil, @size));
SetLength(sessionKeyBlob, size);
CheckCryptoCall(CryptExportKey(hSessionKey, hAgreeKey, SIMPLEBLOB, 0, @sessionKeyBlob[0], @size));
{$endregion}
{$endregion}
{$region '3. Зашифрование документа.'}
// ---------
{$region '3.1. Создание шифратора в режиме зашифрования.'}
// Применяется алгоритм
// "urn:ietf:params:xml:ns:cpxmlsec:algorithms:gost28147".
// Возможные параметры шифратора GostJCE/CBC/ISO10126Padding;
// ---------
// Установка режима шифрования CBC, он же CBCSTRICT
keyParam := CRYPT_MODE_CBCSTRICT;
CheckCryptoCall(CryptSetKeyParam(hSessionKey, KP_MODE, @keyParam, 0));
// С выравниванием ISO10126_Padding не удаётся ни зашифровать запрос (так чтобы его расшифровал получатель)
// ни расшифровать ответ.
// С выравниванием ANSIX923_PADDING расшифровать удаётся, но зашифрованое нами сообщение получатель
// всё равно не может расшифровать.
keyParam := ISO10126_Padding; // ANSIX923_PADDING;
CheckCryptoCall(CryptSetKeyParam(hSessionKey, KP_PADDING, @keyParam, 0));
{$endregion}
{$region '3.2. Добавление зашифрованного сессионного ключа, полученного ранее (добавление блока KeyInfo;'}
// ---------
// Этот пункт частично опускаем, так как работа с KeyInfo производится на уровнем выше с xml.
// Подготавливаем ATransportBlob который снаружи переводится в BASE64 и добавляется в xml
transport.SessionEncryptedKey.EncryptedKey := sessionKeySimpleBlob.GetEncryptedKey;
transport.SessionEncryptedKey.MaskKey := nil;
transport.SessionEncryptedKey.MacKey := sessionKeySimpleBlob.GetMacKey;
transport.TransportParameters.EncryptionParamSet := sessionKeySimpleBlob.GetEncryptionParamSet;
transport.TransportParameters.EphemeralPublicKey.Algorithm.Algorithm := ephemPublicKeyBlob.GetPublicKeyAlgorithmOid;
transport.TransportParameters.EphemeralPublicKey.Algorithm.Parameters := ephemPublicKeyBlob.GetPublicKeyParameters;
transport.TransportParameters.EphemeralPublicKey.SubjectPublicKey := ephemPublicKeyBlob.GetPublicKey;
transport.TransportParameters.Ukm := sessionKeySimpleBlob.GetSV;
ATransportBlob := EncodeTransport(transport);
{$endregion}
{$region '3.3. Зашифрование документа на сессионном ключе.'}
// ---------
// Получение из сессионного ключа параметра вектора инициализации.
CheckCryptoCall(CryptGetKeyParam(hSessionKey, KP_IV, nil, @size, 0));
SetLength(initVector, size);
CheckCryptoCall(CryptGetKeyParam(hSessionKey, KP_IV, @initVector[0], @size, 0));
// Определение размера незашифрованных данных.
sourceDataLen := Length(AData);
encryptDataLen := sourceDataLen;
// Шифрование
CryptEncrypt(hSessionKey, 0, true, 0, nil, @encryptDataLen, 0);
encryptedData := AData;
SetLength(encryptedData, encryptDataLen);
CryptEncrypt(hSessionKey, 0, true, 0, @encryptedData[0], @sourceDataLen, encryptDataLen);
// Формирование структуры зашифрованных данных.
AEncryptedResult := TbtkArrayHelper<Byte>.Concat([initVector, encryptedData]);
{$endregion}
{$endregion}
finally
if hSessionKey <> 0 then
CheckCryptoCall(CryptDestroyKey(hSessionKey));
if hAgreeKey <> 0 then
CheckCryptoCall(CryptDestroyKey(hAgreeKey));
if hEphemeralKey <> 0 then
CheckCryptoCall(CryptDestroyKey(hEphemeralKey));
end;
end;
Вопрос: Что делаем не так? p.s.: Не могу сказать что хорошо подкован в криптографии и свободно ориентируюсь в связанных с нею стандартах, так что возможно есть очевидная проблема которой я просто не вижу. Всё что на данный момент перечитал на эту тему, позволило только несколько переработать написанный предшественником код, локализовать проблему и актуализировать несколько идентификаторов, а местами устранить хардкод. На сколько я понимаю проблема точно должна быть в зашифровании, так как снаружи приведённого выше метода меняется только та часть soap-запроса которая содержит результат работы метода зашифрования. И в случае когда используем результат работы метода, ФСС не может расшифровать сообщение, а когда используем полученные упоминавшейся ранее библиотекой данные, ФСС успешно расшифровывает запрос. p.p.s.: Спецификация на которую опираемся актуальная и используемые сертификаты тоже. Проверено-перепроверено, так как были подозрения что проблема не в коде. p.p.p.s: Боевой код который используется в приложении не привожу из-за того что нет на это прав. Приводимый код - фрагмент тестового приложения, алгоритмически он соответствует оригиналу и проблемы возникающие в боевом коде в нём полностью повторяются. На всякий случай привожу и шаблон XML-запроса который используется в тестировании. Именно в нём текст "EncrypttedKeyValue" и "EncryptedDataValue" замещаются результатом работы метода шифрования.
Код:
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope
xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:fil="http://ru/ibs/fss/ln/ws/FileOperationsLn.wsdl"
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<soapenv:Header>
</soapenv:Header>
<soapenv:Body>
<EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Content" xmlns="http://www.w3.org/2001/04/xmlenc#">
<EncryptionMethod Algorithm="urn:ietf:params:xml:ns:cpxmlsec:algorithms:gost28147" />
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">
<EncryptionMethod Algorithm="urn:ietf:params:xml:ns:cpxmlsec:algorithms:transport-gost2001" />
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>ВырезаноЧтобыНеСветить</X509Certificate>
</X509Data>
</KeyInfo>
<CipherData>
<CipherValue>EncryptedKeyValue</CipherValue> <!-- заменяется на ATransportBlob возвращаемый из EncryptData -->
</CipherData>
</EncryptedKey>
</KeyInfo>
<CipherData>
<CipherValue>EncryptedDataValue</CipherValue> <!-- заменяется на AEncryptedResult возвращаемый из EncryptData -->
</CipherData>
</EncryptedData>
</soapenv:Body>
</soapenv:Envelope>
Буду признателен за помощь и дельные советы. Отредактировано пользователем 11 февраля 2020 г. 12:35:25(UTC)
| Причина: Не указана
|