Здравствуйте!
Моя задача - реализовать шифрование 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)
 | Причина: Не указана