Написано, лишь, дабы систематизировать приобретенные знания и в надежде облегчить жизнь столкнувшимся с данной проблемой…
Все началось с того, что захотелось организовать защищенный обмен между windows-приложением и web-сервером. Требовалось просто передать на сервер некоторые данные и получить ответ, исключив при этом возможность подмены сервера (путем правки файла hosts) и соотвественно избежать атаки подменой данных ответа от ложного сервера.
Клиентское приложение разрабатывалось на Delphi 2009, а в качестве сервера хотелось использовать связку Apache+PHP.
Интуитивно понятно, что данная задача решается при использовании инфраструктуры открытых/закрытых ключей для шифрования трафика, и выбор естественно пал на алгоритм RSA.
Говоря о самом алгоритме RSA, хочу отметить несколько неочевидных фактов, подтвержденных экспериментами:
- Какие бы загадочные термины (ключ, сертификат, цифровая подпись и т.д.) не использовали при описании его работы, основан он всего лишь на 3-х числах: модуле, приватной и публичной экспоненте. Модуль и приватная экспонента, в отличии от экспоненты публичной, это очень большие числа, разрядность которых определяется в алгоритме и может варьироваться до 1024 бит (стандартное значение – 512). Публичная же экспонента, как правило, равна 65537. Этих 3-х чисел достаточно для реализации ассиметричного шифрования RSA.
- Считается, что шифрование должно осуществляться открытым ключем, а дешифрование – закрытым. Это не так. На самом деле даже если зашифровать данные приватным ключем – они прекрасно расшифруются публичным.
Реализация 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
Ноябрь 13th, 2009 at 3:33
Спасибо за статью!
Столкнулся с аналогичной ситуацией. Изначально остановился на LockBox, но задача заключается в том, что публичный ключ мне предоставляется в открытом виде в PEM формате. LockBox, как Вы и писали, принимает только ASN формат. Делал попытки изменить классы LockBox, дабы выудить из них функции преобразования к ASN формату, но сами понимаете это не есть панацея. Статья помогла, так же остановился на CryptoAPI.
Ноябрь 1st, 2010 at 5:26
Отличная статья! Спасибо, очень помогла!
Целую неделю возился с этими ключами, искал причину.
Одного не могу понять, почему даже в openssl есть поддержка PRIVATEKEYBLOB, а у мелкософта добавить импорт-экспорт PEM такая проблема. Столько времени потратил впустую.. Надеюсь, доживу до времени, когда на силиконовую долину упадет Нибиру и виндоусы с их APIсаками исчезнут.
Декабрь 9th, 2010 at 18:33
хорошая статья, спасибо
не знаете ли реализацию gnu pgp (или интерфейс к нему) на delphi?
Декабрь 19th, 2010 at 2:27
Спасибо. Вот этим “MS PRIVATEKEYBLOB”‘ом Вы сэкономили мне пару дней на написание и тестирование конвертера.
Январь 10th, 2011 at 7:23
Спасибо вам огроменное! Я шифрую тоже между php и delphi с помощью DCPcrypt, но, 5 часов бился с кодировками, а в итоге все настолько банально просто оказалось с типом ANSIString…
Спасибо еще раз, конкретно сдвинули с мертвой точки.
Май 5th, 2011 at 15:17
Статья хорошая… Ново утверждение что можно расшифровывать публичным ключом.. хм… не получается (версия 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.
Или может быть я не те параметры указываю?
С уважением.
Май 20th, 2011 at 7:21
Спасибо вам огромное!!!
Ребята, а как мне зашифрованный файл расшифровать средствами Crypto API (Delphi). Пожалуйста, дайте код!!!
Октябрь 31st, 2011 at 22:43
Способ этот заключается в использовании утилиты openssl и указания формата исходного файла “MS PRIVATEKEYBLOB” (именно так, с пробелом):
openssl rsa -inform MS\ PRIVATEKEYBLOB -in private.dat -outform PEM -out private.pem
А ОТКУДА ВЗЯЛСЯ ФАЙЛ PRIVATE.DAT???
Ноябрь 1st, 2011 at 19:21
Подскажите, посредством чего вы создавали файл private.dat? я в тексте не нашел этой информации.
Ноябрь 1st, 2011 at 20:01
Там в тексте ссылка есть на MSDN с демо программой которая “How to generate key pairs, encrypt and decrypt data with CryptoAPI”. Это пример, как можно сгенерить private.dat.
Ноябрь 1st, 2011 at 20:13
Это я видел, но там прога на С – я не шарю в С. готовой проги случаем нет?
Ноябрь 2nd, 2011 at 22:21
вроде тема про связку Дельфи с опенссл. почему же тогда прога на С++? Поделитесь вариантом для дельфи – очень нужно.