Здравствуйте!
Моя задача - реализовать шифрование XML, отправляемых на сервис ФСС, в нативном Windows приложении на Delphi.
Стандарт XML Encryption Syntax and Processing (W3C), шифрование сессионного ключа и данных происходит по алгоритму ГОСТ 28147-89.
Аналогичное шифрование на .NET сделали быстро и без проблем, а вот c нативной реализацией через Crypto API возникли трудности.
Схема реализации, которая была выбрана:
1. Сформировать исходный XML
2. Зашифровать буфер, содержащий XML, через функцию CryptEncryptMessage открытым ключом сертификата. На входе сертификат в формате DER (ACertificate) и буфер, содержащий XML (ASourceData). На выходе PKCS#7 Enveloped Data, содержащая параметры шифрования, ключи и зашифрованные данные.
Код:
function TCryptoApiController.EncryptData(const ACertificate,
ASourceData: AnsiString): AnsiString;
var
FCryptProv: HCRYPTPROV;
FCertContext: PCCERT_CONTEXT;
FCertArray: PCCERT_CONTEXT_ARRAY;
FEncryptParams: CRYPT_ENCRYPT_MESSAGE_PARA;
FEncryptSizeNeed: DWORD;
begin
AcquireContext(@FCryptProv, nil, CRYPT_VERIFYCONTEXT or CRYPT_SILENT, '');
try
// получение сертификата
FCertContext := FCertCreateCertificateContext(PKCS_7_ASN_ENCODING or X509_ASN_ENCODING, @ACertificate[1], Length(ACertificate));
if FCertContext = nil then
RaiseLastCryptoApiError;
try
FillChar(FEncryptParams, SizeOf(FEncryptParams), 0);
FEncryptParams.cbSize := SizeOf(CRYPT_ENCRYPT_MESSAGE_PARA);
FEncryptParams.dwMsgEncodingType := PKCS_7_ASN_ENCODING or X509_ASN_ENCODING;
FEncryptParams.hCryptProv := FCryptProv;
FEncryptParams.ContentEncryptionAlgorithm.pszObjId := PAnsiChar(szOID_CP_GOST_28147);
SetLength(FCertArray, 1);
FCertArray[0] := FCertContext;
// шифрование исходных данных
if FCryptEncryptMessage(@FEncryptParams, 1, FCertArray, @ASourceData[1], Length(ASourceData), nil, @FEncryptSizeNeed) then
begin
SetLength(Result, FEncryptSizeNeed);
FillChar(Result[1], FEncryptSizeNeed, 0);
if not FCryptEncryptMessage(@FEncryptParams, 1, FCertArray, @ASourceData[1], Length(ASourceData), @Result[1], @FEncryptSizeNeed) then
RaiseLastCryptoApiError;
// When processing the data returned in the buffer of the pbEncryptedBlob, applications need to use the actual size of the data returned.
// The actual size can be slightly smaller than the size of the buffer specified on input.
SetLength(Result, FEncryptSizeNeed);
end else
RaiseLastCryptoApiError;
finally
if FCertContext <> nil then
FCertFreeCertificateContext(FCertContext);
end;
finally
ReleaseContext(FCryptProv);
end
end
3. Из PKCS# Enveloped Data через ASN.1 парсер получить готовую структуру GostR3410-KeyTransport и записать в зашифрованный XML в параметр EncryptedData -> KeyInfo -> EncryptedKey -> CipherData в соответствии со стандартом
https://tools.ietf.org/h...v-cryptopro-cpxmldsig-094. Из PKCS# Enveloped Data через ASN.1 парсер получить зашифрованные данные и вектор инициализации и записать их в зашифрованный XML в параметр EncryptedData -> CipherData в соответствии со тем же стандартом
https://tools.ietf.org/h...v-cryptopro-cpxmldsig-09 (The resulting cipher text is prefixed by the IV. If included in XML output, it is then base64 encoded.)
В процессе реализации и чтения стандартов выяснил, что стандарт XML Encryption Syntax and Processing (W3C) предполагает использование "padding" перед шифрованием. Опытным путем выяснил, что функция CryptEncryptMessage "padding" не применяет (зашифровал 1 байт и на выходе в PKCS# Enveloped Data получил зашифрованные данные размером этот же 1 байт, хотя в случае "padding" они должны были быть выровнены до 8 байт).
Реализовал "padding" на своей стороне:
Код:
function MakePadding(const Data: AnsiString): AnsiString;
var
FPaddingCount: Integer;
FPaddingSuffix: AnsiString;
I: Integer;
begin
FPaddingCount := 8 - (Length(Data) mod 8);
FPaddingSuffix := '';
for I := 1 to FPaddingCount do
FPaddingSuffix := FPaddingSuffix + AnsiChar(FPaddingCount);
Result := Data + FPaddingSuffix;
end;
В итоге получил:
1. Зашифрованные данные PKCS# Enveloped Data
https://lapo.it/asn1js/#...BFB8EB76A5731C17B77D20522. Зашифрованный XML, сформированный описанным выше методом, request.xml (во вложении)
Зашифрованные данные PKCS#7 Enveloped Data (п.1) без проблем расшифровываются через Crypto API функцию CryptDecryptMessage (в примерах указан сертификат ФСС, но я пробовал шифровать своим сертификатом, для которого у меня есть закрытый ключ, чтобы проверить расшифровку).
Но сформированный request.xml принимающий сервис ФСС расшифровать не может!
Для проверки я реализовал шифрование этого же исходного XML на .NET через библиотеку GostCryptography:
Код:
// CryptoPro CSP
GostCryptoConfig.ProviderType = ProviderTypes.CryptoPro;
// Создание объекта для шифрации XML
var encryptedXml = new GostEncryptedXml();
Console.Write(encryptedXml.Mode);
var certificate = new X509Certificate2(fileName: "C:\\Work\\test.cer");
var xmlDocument = new XmlDocument();
xmlDocument.Load("C:\\Work\\FSS\\request_sample.xml");
// Шифрование всего документа
var elementEncryptedData = encryptedXml.Encrypt(xmlDocument.DocumentElement, certificate);
var soapDocument = new XmlDocument();
var soapElement = soapDocument.CreateElement("soapenv:Envelope", "http://schemas.xmlsoap.org/soap/envelope/");
soapDocument.AppendChild(soapElement);
var bodyElement = soapDocument.CreateElement("soapenv:Body", "http://schemas.xmlsoap.org/soap/envelope/");
soapElement.AppendChild(bodyElement);
// Замена элемента его зашифрованным представлением
GostEncryptedXml.ReplaceElement(bodyElement, elementEncryptedData, true);
soapDocument.Save("C:\\Work\\FSS\\request_sample_encrypted.xml");
Полученный request_sample_encrypted.xml (во вложении) без проблем принимается сервисом ФСС!
Я проверил:
- Размер и структура XML request.xml и request_sample_encrypted.xml полностью совпадают
- Проверил EncryptedData -> KeyInfo -> EncryptedKey -> CipherData через ASN.1 Viewer: структура, размеры параметров и OID полностью совпадают (понятно, что сами значения отличаются)
- Размер зашифрованных данных тоже совпадает
Попробовал зашифровать XML на своем сертификате через CryptEncryptMessage по описанной выше схеме, а расшифровать через .NET. Действительно, расшифровать не получается, возвращается ошибка "Плохие данные".
Подскажите, пожалуйста, в чем может быть проблема.
Вопросы:1. Функция CryptEncryptMessage в приведенном мной примере шифрует данные по алгоритму urn:ietf:params:xml:ns:cpxmlsec:algorithms:gost28147 с упаковкой ключей urn:ietf:params:xml:ns:cpxmlsec:algorithms:transport-gost2001? или она реализует какой-то другой алгоритм? какими параметрами алгоритма шифрования можно управлять при использовании функции CryptEncryptMessage? Какие параметры используются по умолчанию? Странно, что моя структура GostR3410-KeyTransport полностью совпадает с рабочим примером.
2. Правильно я понимаю, что в моем примере PKCS#7 Enveloped Data (см. выше) вектор инициализации - это x"40A6B01F5F877B74" ? При записи EncryptedData -> CipherData в XML вектор инициализации нужно просто добавить перед зашифрованными данными x"93E4F5...", чтобы получилось x"40A6B01F5F877B7493E4F5..." и потом запаковать в Base64?
3. Почему функция CryptEncryptMessage не использует "padding"? Можно ли каким-то параметром включить для нее "padding"? (про CryptSetKeyParam и KP_PADDING я знаю, но здесь нет прямой работы с ключами) Правильно ли я реализовал "padding" на своей стороне?
Отредактировано пользователем 11 апреля 2018 г. 2:47:28(UTC)
| Причина: Не указана