Окт 16

CryptoAPI OpenSSLНаписано, лишь, дабы систематизировать приобретенные знания и в надежде облегчить жизнь столкнувшимся с данной проблемой…

Все началось с того, что захотелось организовать защищенный обмен между windows-приложением и web-сервером. Требовалось просто передать на сервер некоторые данные и получить ответ, исключив при этом возможность подмены сервера (путем правки файла hosts) и соотвественно избежать атаки подменой данных ответа от ложного сервера.

Клиентское приложение разрабатывалось на Delphi 2009, а в качестве сервера хотелось использовать связку Apache+PHP.

Интуитивно понятно, что данная задача решается при использовании инфраструктуры открытых/закрытых ключей для шифрования трафика, и выбор естественно пал на алгоритм RSA.

Говоря о самом алгоритме RSA, хочу отметить несколько неочевидных фактов, подтвержденных экспериментами:

  1. Какие бы загадочные термины (ключ, сертификат, цифровая подпись и т.д.) не использовали при описании его работы, основан он всего лишь на 3-х числах: модуле, приватной и публичной экспоненте. Модуль и приватная экспонента, в отличии от экспоненты публичной, это очень большие числа, разрядность которых определяется в алгоритме и может варьироваться до 1024 бит (стандартное значение – 512). Публичная же экспонента, как правило, равна 65537. Этих 3-х чисел достаточно для реализации ассиметричного шифрования RSA.
  2. Считается, что шифрование должно осуществляться открытым ключем, а дешифрование – закрытым. Это не так. На самом деле даже если зашифровать данные приватным ключем – они прекрасно расшифруются публичным.

Реализация RSA на PHP лишь одна – php-OpenSSL, тут выбирать не приходится. А вот в Delphi, выбор классов и компонент для шифрования значительно шире:

  • TurboPower LockBox – Уже не поддерживаются, исходники выложены в публичный доступ, есть порт tbLockBox для Delphi 2009. Интуитивно понятная, хорошо документированная и достаточно простая библиотека.
  • SecureBlackbox – Коммерческая библиотека без исходных кодов. Довольно громоздкая и не совсем удобная. Скачать бесплатно можно здесь, работоспособность кряка не проверялась, есть вероятность появления наг-скринов в самые неожиданные моменты.
  • OpenSSL – Можно использовать и на клиенте. Для этого необходимо будет включить в проект библиотеку libeay32.dll. Заголовочный файл libeay32.pas на Delphi 2009 скорее всего не пойдет, он сделан без учета unicode.
  • Windows CryptoAPI – Родная виндовая криптосистема, соотвественно, приложение не утяжеляется, и автоматически снимаются проблемы совместимости библиотеки с будущими версиями Delphi. Заголовочный файл Wcrypt2.pas прекрасно работает на Delphi 2009.

Все библиотеки позволяют генерировать пары ключей для работы.

  • В OpenSSL для этого используется одноименная утилита openssl (необходимо установить OpenSSL for Windows).
    Генерация приватного 1024-битного ключа:
    openssl genrsa -out private.pem 1024
    Создание парного публичного ключа:
    openssl rsa -pubout -in private.pem -out public.pem
    Отображение содержимого ключа:
    openssl rsa -text -in private.pem
  • В tbLockBox для генерации пары RSA-ключей используется функция TtbRSA.GenerateKeyPair()
  • А в CryptoAPI функция CryptGenKey()

Однако при попытке использовать пару ключей в разных системах возникает проблема формата ключей:

  • openssl-функции PHP работают лишь с ключами в формате PEM, который содержит base64 кодированную ASN.1 структуру, содержащую все данные ключа.
  • tbLockBox сохраняет и читает ключи в двоичном ASN.1 формате, однако структура этих данных не соответствует структуре, используемой в PEM-файлах. Кроме того, для приватных ключей используется лишь модуль и приватная экспонента, в то время как PEM-файлы приватных ключей содержат и другие данные, необходимые для работы openssl.
  • CryptoAPI для экспорта и импорта ключей использует структуры PRIVATEKEYBLOB и PUBLICKEYBLOB

В процессе решения проблемы конвертации и изучения структуры файлов ключей, была найдена замечательная утилита ASN.1 Editor (есть исходники), которая сильно помогла понять структуру RSA-ключей.

В результате долгих изысканий был найден единственный рабочий способ (натолкнулся здесь) – сконвертировать CryptoAPI структуру PRIVATEKEYBLOB в PEM-файл пригодный для использования в OpenSSL.

Способ этот заключается в использовании утилиты openssl и указания формата исходного файла “MS PRIVATEKEYBLOB” (именно так, с пробелом):
openssl rsa -inform MS\ PRIVATEKEYBLOB -in private.dat -outform PEM -out private.pem
Обратите внимание на обратную косую черту, она экранирует символ пробела в командной строке!

И здесь же следует одна очень важная оговорка: формат “MS PRIVATEKEYBLOB” поддерживается openssl лишь начиная с версии 1.0.0 beta. На Windows данная версия на момент написания статьи еще не портирована и для конвертации ключей необходимо взять *nix или cygwin и поставить на него openssl v1.0.0beta.

Исходя из всего выше сказанного следует, что на клиенте должно использоваться CryptoAPI. В нем же необходимо сгенерировать ключи и сохранить их в формате PRIVATEKEYBLOB, а затем сконвертировать в PEM-формат, пригодный для использования в PHP.

Небольшое пояснение: PRIVATEKEYBLOB равно как и PEM-файл, содержат данные не только приватного, но и публичного ключа. И сконвертировав лишь PRIVATEKEYBLOB можно получить затем из PEM и открытый ключ (openssl rsa -pubout -in private.pem -out public.pem).

До этого момента я не имел опыта работы с CryptoAPI и в его освоении мне очень помогли статьи “Delphi и Windows API для защиты секретов” и “Использование инструментов криптографии в Delphi-приложениях“, а так же небольшая демо программа наглядно демонстрирующая использование CryptoAPI. Именно в ней и были сгенерены ключи.

Вот кусок delphi программы, демонстрирующий работу по чтению публичного ключа и шифровки текста (строки в php имеют ANSI представление, поэтому перед шифрованием преобразуем Unicode в ANSI):

var
Stream: TMemoryStream;
res: boolean;
RSA: HCRYPTPROV;
PublicKey: HCRYPTKEY;
str: ANSIString;
strlen: DWORD;
begin
// Читаем ключ из файла
Stream := TMemoryStream.Create;
Stream.LoadFromFile('public.key');

// Инициализаируем CryptoAPI
CryptAcquireContext(@RSA, nil, nil, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT);

// Импортируем ключ
CryptImportKey(RSA, PByte(Stream.Memory), Stream.Size, 0, 0, @PublicKey);

// Берем шифруемый текст, преобразовывая его из Unicode в ANSI
str := Edit1.Text;

// Вычисляем размер зашифрованных данных
strlen := Length(str);
CryptEncrypt(PublicKey, 0, true, 0, nil, @strlen, 0);

// Подготавливаем буфер нужного размера
Stream.SetSize(strlen);
strlen := Length(str);
CopyMemory(Stream.Memory, pointer(str), strlen);

// Криптуем
CryptEncrypt(PublicKey, 0, true, 0, PByte(Stream.Memory), @strlen, Stream.Size);

// Сохраняем результат в файл
Stream.SaveToFile('encrypted.dat');

// Освобождаем занятые ресурсы
Stream.Free;
CryptDestroyKey(PublicKey);
CryptReleaseContext(RSA, 0);

Для передачи зашифрованных данных серверу, удобнее использовать кодировку base64, в которой бинарные данные имеют текстовое представление. Для этой цели отлично подошел модуль DCPbase64.pas из библотеки DCPcrypt:
...
SetLength(str, ((Stream.Size + 2) div 3) * 4);
Base64Encode(pointer(Stream.Memory), pointer(str), Stream.Size);
Edit1.Text := str;
...

Переходя к рассмотрению серверной части, сразу отмечу, что есть одна неучтенная мною особенность связки CryptoAPI и OpenSSL, из-за которой сразу все не заработало. А именно – порядок следования байтов в зашифрованном сообщении. Дело в том, что в CryptoAPI используется little-endian порядок, а в OpenSSL – big-endian (наткнулся здесь, спасибо). Поэтому потребуется перестановка (проще сделать в php).

Ну и собственно серверный пример:

<?php

// Читаем приватный ключ
$privateKey = openssl_pkey_get_private(array("file://private.pem", ""));

if ( $privateKey )
print "\nPrivate Key OK";
else
print "\nPrivate key NOT OK";

// Зашифрованное сообщение
$str = 'p8DDRtK69bsIDhn6f26cYpMb2BimJdTjFerEW6Z45P+/m/nCzp55o76B07w6R/sKTX6g0jsfIH+HFZo9GaVK16oBlVKZEU9HS73XNUaeLwwdPDCABK6QvJ7nYVshhkTsJuy2mr0bAPhR9bqf826Ui7sOM3ki1XQ4PtKp3R18EUQ=';

// Декодируем
$str = base64_decode($str);

// Меняем порядок байт little-endian на big-endian
$str = strrev($str);

// Дешифруем
if (openssl_private_decrypt($str, $res, $privateKey))
print "Result = $res";
else
print "Decrypting Error";

?>

Надеюсь, кому нибудь поможет.

Отдельное спасибо Jareth-у за то,
что грохнул рабочую станцию офиса продаж,
установив на нее федору 11,
чтоб потестить openssl 1.0 beta3 :)

12 Responses to “RSA-шифрование для связки Delphi (CryptoAPI) и PHP (OpenSSL)”

  1. Дмитрий Says:

    Спасибо за статью!
    Столкнулся с аналогичной ситуацией. Изначально остановился на LockBox, но задача заключается в том, что публичный ключ мне предоставляется в открытом виде в PEM формате. LockBox, как Вы и писали, принимает только ASN формат. Делал попытки изменить классы LockBox, дабы выудить из них функции преобразования к ASN формату, но сами понимаете это не есть панацея. Статья помогла, так же остановился на CryptoAPI.

  2. Andrei Says:

    Отличная статья! Спасибо, очень помогла!

    Целую неделю возился с этими ключами, искал причину.
    Одного не могу понять, почему даже в openssl есть поддержка PRIVATEKEYBLOB, а у мелкософта добавить импорт-экспорт PEM такая проблема. Столько времени потратил впустую.. Надеюсь, доживу до времени, когда на силиконовую долину упадет Нибиру и виндоусы с их APIсаками исчезнут.

  3. mirt steelwater Says:

    хорошая статья, спасибо
    не знаете ли реализацию gnu pgp (или интерфейс к нему) на delphi?

  4. Микола Says:

    Спасибо. Вот этим “MS PRIVATEKEYBLOB”‘ом Вы сэкономили мне пару дней на написание и тестирование конвертера.

  5. Алексей Says:

    Спасибо вам огроменное! Я шифрую тоже между php и delphi с помощью DCPcrypt, но, 5 часов бился с кодировками, а в итоге все настолько банально просто оказалось с типом ANSIString…
    Спасибо еще раз, конкретно сдвинули с мертвой точки.

  6. Figaro Says:

    Статья хорошая… Ново утверждение что можно расшифровывать публичным ключом.. хм… не получается (версия 0.9.8):

    openssl genrsa -out key.pem
    openssl rsa -in key.pem -out pubkey.pem -pubout
    openssl rsautl -in test.txt -out test.enc -inkey key.pem -encrypt

    А на команде:

    openssl rsautl -in test.enc -out test.txt -inkey pubkey.pem -pubin -decrypt

    A private key is needed for this operation.

    Или может быть я не те параметры указываю?

    С уважением.

  7. Андрей Says:

    Спасибо вам огромное!!!

    Ребята, а как мне зашифрованный файл расшифровать средствами Crypto API (Delphi). Пожалуйста, дайте код!!!

  8. pixel Says:

    Способ этот заключается в использовании утилиты openssl и указания формата исходного файла “MS PRIVATEKEYBLOB” (именно так, с пробелом):
    openssl rsa -inform MS\ PRIVATEKEYBLOB -in private.dat -outform PEM -out private.pem

    А ОТКУДА ВЗЯЛСЯ ФАЙЛ PRIVATE.DAT???

  9. pixel Says:

    Подскажите, посредством чего вы создавали файл private.dat? я в тексте не нашел этой информации.

  10. mawr Says:

    Там в тексте ссылка есть на MSDN с демо программой которая “How to generate key pairs, encrypt and decrypt data with CryptoAPI”. Это пример, как можно сгенерить private.dat.

  11. pixel Says:

    Это я видел, но там прога на С – я не шарю в С. готовой проги случаем нет?

  12. pixel Says:

    вроде тема про связку Дельфи с опенссл. почему же тогда прога на С++? Поделитесь вариантом для дельфи – очень нужно.

Leave a Reply