Используем средства библиотеки OpenSSL для криптографической защиты данных
Владимир Мешков
Самым надежным способом скрыть информацию от посторонних глаз является ее шифрование. В настоящее время существует большое количество стойких криптографических алгоритмов, позволяющих надежно защитить конфиденциальные данные и множество их программных реализаций, доступных для свободного использования.
Сегодня мы поговорим о библиотеке OpenSSL. Эта свободно распространяемая библиотека предоставляет в распоряжение пользователя набор утилит, реализующих различные криптографические алгоритмы, такие как Triple-DES, Blowfish, AES, RSA и другие.
Порядок использования утилит был подробно описан Всеволодом Стаховым в статье "Теория и практика OpenSSL" [1].
Кроме готовых к применению утилит, библиотека содержит набор функций, с помощью которых пользователь может разрабатывать собственные программы для криптографической защиты данных или создавать расширения, не входящие в стандартный набор. Большинство этих функций достаточно хорошо документированы, и наличие примеров программ значительно облегчает задачу изучения порядка их использования.
Не углубляясь в детали реализации алгоритмов, рассмотрим несколько практических примеров использования библиотеки OpenSSL для генерации псевдослучайных чисел, вычисления хэшей и шифрования данных с использованием симметричных и асимметричных алгоритмов. Основное внимание акцентируем на вопросах, которые, на мой взгляд, недостаточно полно освещены в официальной документации и требуют более детальной проработки.
Далее по тексту библиотека OpenSSL будет именоваться просто библиотека, кроме специально оговоренных случаев.
Генерация псевдослучайной последовательности
Сгенерировать псевдослучайную последовательность (ПСП) при помощи библиотеки очень просто. Для этого достаточно вызвать функцию RAND_bytes, передав ей в параметрах указатель на буфер для хранения сгенерированной последовательности и размер этого буфера. Следующий код демонстрирует это:
Листинг 1. Генерация ПСП
#include <openssl/rand.h>
void main()
{
int outf;
unsigned char buf[1024];
if(RAND_bytes(buf, sizeof(buf))) { /* 1 succes, 0 otherwise */
outf = open("./rnd_bytes", O_CREAT|O_TRUNC|O_RDWR, 0600);
write(outf, buf, sizeof(buf));
} else printf("-ERR: RAND_bytes\n");
}
Сохраним этот код в файле rand_test.c и получим исполняемый файл формата ELF при помощи команды:
gcc -o rand_test rand_test.c -lssl
После запуска на выполнение файла rand_test в текущем каталоге будет создан файл rnd_bytes размером 1024 байта, содержащий сгенерированную ПСП.
Вычисление хэшей
Хэш - это механизм контроля целостности данных, обладающий специальными свойствами:
n зная сообщение, легко вычислить хэш. Обратная задача нахождения сообщения по известному хэшу является вычислительно-трудоемкой;
n для заданного сообщения хэш является уникальным, т.е. не должно существовать двух разных сообщений с одинаковыми хэшами.
Из всех существующих алгоритмов хэширования наибольшее распространение получил алгоритм MD5. Аббревиатура "MD" означает Message Digest (краткое изложение сообщения, или дайджест). В соответствии с этим алгоритмом, входной текст обрабатывается 512-битовыми блоками, разбитыми на шестнадцать 32-битовых подблоков. Выходом алгоритма является набор из четырех 32-битовых блоков, которые объединяются в единое 128-битное значение. Рассмотрим, как вычисляется хэш сообщения по алгоритму MD5 с использованием средств библиотеки.
Для вычисления хэша библиотека предоставляет в наше распоряжение функцию MD5:
unsigned char * MD5(const unsigned char *d, unsigned long n, unsigned char *md)
Эта функция принимает три параметра - указатель на буфер с исходными данными d, размер этого буфера n и указатель на буфер для хранения вычисленного хэша md. Это очень простая в использовании функция, но у нее есть недостаток - она пригодна для вычисления хэша данных, которые можно полностью разместить в оперативной памяти. Для файлов большого размера вычисление хэша производится поэтапно. Для этого библиотека предоставляет следующий набор функций (см. openssl/md5.h):
void MD5_Init(MD5_CTX * ctx);
void MD5_Update(MD5_CTX * ctx, const void * data, unsigned long len);
void MD5_Final(unsigned char * md, MD5_CTX * ctx);
Функция MD5_Init() инициализирует контекст дайджеста - структуру, определенную в файле openssl/md5.h:
typedef struct MD5state_st {
MD5_LONG A,B,C,D;
MD5_LONG Nl,Nh;
MD5_LONG data[MD5_LBLOCK]; /* MD5_LBLOCK = 16 */
int num;
} MD5_CTX;
Инициализация контекста подразумевает его заполнение определенными значениями (см. файл md5_dgst.c исходных текстов библиотеки):
#define INIT_DATA_A (unsigned long)0x67452301L
#define INIT_DATA_B (unsigned long)0xefcdab89L
#define INIT_DATA_C (unsigned long)0x98badcfeL
#define INIT_DATA_D (unsigned long)0x10325476L
int MD5_Init(MD5_CTX *c)
{
c->A=INIT_DATA_A;
c->B=INIT_DATA_B;
c->C=INIT_DATA_C;
c->D=INIT_DATA_D;
c->Nl=0;
c->Nh=0;
c->num=0;
return 1;
}
Функция MD5_Update() вычисляет хэш. Входными параметрами этой функции являются указатель на контекст хэша ctx, указатель на блок входных данных data и размер этого блока len. Функция MD5_Final() помещает вычисленный хэш из контекста ctx в выходной буфер md, размер которого должен быть равен 16 байт.
Листинг 2 демонстрирует порядок использования перечисленных выше функций для вычисления хэша файла:
Листинг 2. Вычисление MD5-хэша для файла большого размера
#include <openssl/md5.h>
#define BUFSIZE (1025*16)
void main(int argc, char **argv)
{
MD5_CTX c; /* контекст хэша */
unsigned char buf[BUFSIZE];
unsigned char md_buf[MD5_DIGEST_LENGTH];
/* В командной строке передается имя файла, для которого вычисляется хэш */
int inf = open(argv[1], O_RDWR);
/* Инициализируем контекст */
MD5_Init(&c);
/* Вычисляем хэш */
for(;;) {
int i = read(inf, buf, BUFSIZE);
if(i <= 0) break;
MD5_Update(&c, buf, (unsigned long)i);
}
/* Помещаем вычисленный хэш в буфер md_buf */
MD5_Final(md_buf, &c);
/* Отображаем результат */
for(i = 0; i < MD5_DIGEST_LENGTH; i++) printf("%02x", md_buf[i]);
}
Проверить правильность вычисления хэша можно при помощи утилиты md5sum.
В рассмотренном листинге мы обращаемся к функциям библиотеки напрямую. В документации рекомендуется использовать высокоуровневые функции с префиксом EVP вместо прямого вызова функций алгоритма хэширования. Разберемся подробнее, что это за EVP-функции.
Библиотека поддерживает внутреннюю таблицу, в которой каждый элемент представляет собой структуру, содержащую адреса функций алгоритмов шифрования и хэширования, реализованных в библиотеке. Для работы с высокоуровневыми функциями необходимо извлечь структуру требуемого алгоритма из этой таблицы, получив, таким образом, адреса его функций. Но прежде эту таблицу необходимо заполнить. Адреса функций алгоритмов хэширования записываются в таблицу при помощи функции OpenSSL_add_all_digests(), адреса функций алгоритмов шифрования - при помощи функции OpenSSL_add_all_ciphers().
Извлечь структуру требуемого алгоритма хэширования из таблицы можно при помощи функции EVP_get_digestbyname(const char * name), где name является символьным обозначением алгоритма. Для алгоритма MD5 это будет "md5". Список всех имен приведен в файле openssl/object.h. Результат работы функции EVP_get_digestbyname() сохраняется в структуре типа EVP_MD:
struct env_md_st
{
int type;
int pkey_type;
int md_size;
unsigned long flags;
int (*init)(EVP_MD_CTX *ctx);
int (*update)(EVP_MD_CTX *ctx,const void *data,unsigned long count);
int (*final)(EVP_MD_CTX *ctx,unsigned char *md);
int (*copy)(EVP_MD_CTX *to,const EVP_MD_CTX *from);
int (*cleanup)(EVP_MD_CTX *ctx);
/* FIXME: prototype these some day */
int (*sign)();
int (*verify)();
int required_pkey_type[5]; /*EVP_PKEY_xxx */
int block_size;
int ctx_size; /* how big does the ctx->md_data need to be */
} /* EVP_MD */;
Эта структура определена в файле openssl/evp.h. В ее состав входят указатели на функции алгоритма хэширования. При вызове функции EVP_get_digestbyname() в эти указатели будут записаны реальные адреса функций библиотеки для работы с выбранным типом алгоритма хэширования, и в дальнейшем все вызовы функций будут выполняться косвенно через эти указатели. Получив адреса библиотечных функций, необходимо заполнить контекст для вычисления хэша - структуру типа EVP_MD_CTX (см. openssl/evp.h):
struct env_md_ctx_st
{
const EVP_MD *digest;
ENGINE *engine;
unsigned long flags;
void *md_data;
} /* EVP_MD_CTX */;
Заполнение контекста выполняется при помощи функции EVP_DigestInit(). В параметрах этой функции передаются указатели на контекст для вычисления хэша и на структуру, содержащую адреса функций алгоритма хэширования:
EVP_DigestInit(EVP_MD_CTX * ctx, EVP_MD * md)
Функция копирует структуру "EVP_MD * md" в контекст дайджеста путем приравнивания соответствующих указателей (см. файл crypto/evp/digest.c исходных текстов библиотеки):
ctx->digest = md;
Заполнив контекст, мы получаем возможность вызывать библиотечные функции для вычисления хэша, используя адреса, которые сохранены в структуре digest-контекста. Вычисление хэша выполняет функция EVP_DigestUpdate(), функция EVP_DigestFinal() копирует вычисленный хэш из контекста дайджеста в выходной буфер:
int EVP_DigestUpdate(EVP_MD_CTX *ctx, const void *d, unsigned int cnt);
int EVP_DigestFinal(EVP_MD_CTX *ctx, unsigned char *md, unsigned int *s);
Параметры функции EVP_DigestUpdate - указатель на контекст для вычисления хэша ctx, буфер d для хранения промежуточного результата вычисления и размер этого буфера cnt. Функция EVP_DigestFinal сохраняет размер вычисленного хэша в последнем параметре *s.
По завершении работы контекст для вычисления хэша очищается при помощи функции EVP_MD_CTX_cleanup().
Листинг 3 демонстрирует порядок использования высокоуровневых функций библиотеки для вычисления хэша файла по алгоритму MD5.
Листинг 3. Вычисление MD5-хэша файла с использованием высокоуровневых функций библиотеки
#include <openssl/md5.h>
#include <openssl/evp.h>
#define BUFSIZE (1025*16)
void main(int argc, char **argv)
{
EVP_MD_CTX mdctx; /* контекст для вычисления хэша */
const EVP_MD * md; /* структура с адресами функций алгоритма */
unsigned char md_value[EVP_MAX_MD_SIZE];
int md_len; /* размер вычисленного хэша */
/* В командной строке передаем имя файла, для которого вычисляется хэш */
int inf = open(argv[1], O_RDWR);
/* Добавляем алгоритмы хэширования во внутреннюю таблицу библиотеки */
OpenSSL_add_all_digests();
/* Получаем адреса функций алгоритма MD5 и инициализируем контекст для вычисления хэша */
md = EVP_get_digestbyname("md5");
EVP_DigestInit(&mdctx, md);
/* Вычисляем хэш */
for(;;) {
i = read(inf, buf, BUFSIZE);
if(i <= 0) break;
EVP_DigestUpdate(&mdctx, buf, (unsigned long)i);
}
/* Копируем вычисленный хэш в выходной буфер. Размер хэша сохраняем в переменной md_len */
EVP_DigestFinal(&mdctx, md_value, &md_len);
/* Очищаем контекст */
EVP_MD_CTX_cleanup(&mdctx);
/* Отобразим результат */
for(i = 0; i < md_len; i++) printf("%02x", md_value[i]);
}
Универсальность метода использования функций высокого уровня очевидна - для расчета хэша по новому алгоритму достаточно изменить только его название в функции EVP_get_digestbyname. Поэтому именно этот метод рекомендуется к использованию разработчиками библиотеки.
Симметричные алгоритмы шифрования
Целью шифрования информации является предотвращение угрозы нарушения ее конфиденциальности, т.е. несанкционированное ознакомление с ней. Алгоритмы шифрования можно разделить на две основные категории:
n симметричное шифрование;
n ассиметричное шифрование.
В алгоритмах симметричного шифрования используется один и тот же ключ для шифрования и расшифровки сообщения. Это означает, что любой, кто имеет доступ к ключу шифрования, может расшифровать сообщение. Алгоритмы симметричного шифрования именно поэтому и называют алгоритмами с секретным ключом - ключ шифрования должен быть доступен только тем, кому предназначено сообщение. Симметричное шифрование идеально подходит для шифрования информации "для себя", например, с целью отсечь несанкционированный доступ к ней в отсутствии владельца.
Библиотека поддерживает большое количество симметричных алгоритмов. Некоторые из них мы сейчас рассмотрим, и начнем с самого знаменитого - с DES.
Алгоритм DES
Алгоритм DES (Data Encryption Standart, стандарт шифрования данных) был разработан в 1973 году компанией IBM и долгое время являлся основным стандартом шифрования в мире. Этот алгоритм использует 56-битный ключ и шифрует данные блоками по 64 бита. Имеет несколько режимов работы, которые применимы и для других блочных шифров симметричной схемы:
n Режим электронной шифровальной книги Electronic Codebook Mode (ECB). Простейший режим. Открытый текст обрабатывается блоками по 64 бита (8 байт) и каждый блок шифруется с одним и тем же ключом (см. рис. 1). Самой важной особенностью режима ECB является то, что одинаковые блоки открытого текста в шифрованном тексте будут также представляться одинаковыми блоками. Поэтому при передаче достаточно длинных сообщений режим ECB не может обеспечить необходимый уровень защиты. Если сообщение имеет явно выраженную структуру, у криптоаналитика появляется возможность использовать регулярности текста. Например, если известно, что в начале сообщения всегда размещается определенный заголовок, криптоаналитик может получить в свое распоряжение целый набор соответствующих пар блоков открытого и шифрованного текста.
Рисунок 1. Режим электронной шифровальной книги ECB
n Режим сцепления шифрованных блоков Cipher Block Chaining Mode (CBC). Эта технология свободна от недостатков режима ECB. В режиме CBC входной блок данных для алгоритма шифрования вычисляется как результат операции XOR текущего блока открытого текста и блока шифрованного текста, полученного на предыдущем шаге (см. рис. 2).
Рисунок 2. Режим сцепления шифрованных блоков CBC
n Режим шифрованной обратной связи Cipher Feedback Mode (CFB). Полученный на предыдущем шаге шифрованный текст используется как входные данные для алгоритма шифрования с целью получения псевдослучайной последовательности (ПСП), XOR-разница которой и блока открытого текста определяет очередной блок шифрованного текста (см. рис. 3)
Рисунок 3. Режим 64-битовой шифрованной обратной связи CFB
n Режим обратной связи по выходу Output Feedback Mode (OFB). Работает подобно CFB, но в качестве входных данных для алгоритма шифрования используются ранее полученные выходные данные DES (см. рис. 4).
Рисунок 4. Режим 64-битовой обратной связи по выходу OFB
Если проводить аналогии с алгоритмом ГОСТ 2814789, то режим ECB соответствует режиму простой замены, OFB - режиму гаммирования, CFB - режиму гаммирования с обратной связью.
Малая длина ключа и постоянно растущая мощность современных вычислительных комплексов сделали алгоритм DES потенциально уязвимым перед атакой, основанной на полном переборе ключей. Это обстоятельство наложило существенные ограничения на использование DES в чистом виде и потребовало поиска альтернативы данному шифру. Один из вариантов решения проблемы предполагал создание совершенно нового алгоритма, другой подход сделал ставку на многократное шифрование с помощью DES с применением нескольких ключей.
Широкое распространение получил "тройной" DES (Triple-DES), представляющий собой последовательность операций шифрования-дешифрования-шифрования (EDE - encrypt-decrypt-encrypt) с использованием трех разных ключей. Схема "тройного" DES представлена на рис. 5.
Рисунок 5. Схема "тройного" DES
"Тройной" DES может также использовать два ключа. В этом случае операции шифрования выполняются на одном ключе, а операция дешифрования - на другом.
Использование функций библиотеки, реализующих алгоритм DES, предполагает два этапа: генерация ключей и собственно шифрование информации.
Генерацию DES-ключа выполняет функция DES_random_key(DES_cblock *ret). Входным параметром функции является указатель на блок данных типа DES_cblock, в котором будет сохранен ключ. Тип DES_cblock определен в файле openssl/des.h, и представляет собой 8-байтовую последовательность с контролем четности. Младший значащий бит каждого байта является битом четности:
typedef unsigned char DES_cblock[8];
После генерации ключ необходимо сконвертировать в платформенно-зависимый формат при помощи функции DES_set_key_checked(const_DES_cblock *key, DES_key_schedule *schedule). Функция принимает два параметра - указатель на сгенерированный ключ и указатель на структуру типа DES_key_schedule. Этот структурный тип определен в файле openssl/des.h. Функция DES_set_key_checked выполняет контроль четности всех байт ключа и проверяет, можно ли использовать его для шифрования, т.е. является ключ криптографически сильным или нет. Если четность байт не соблюдается, функция возвращает -1. Если сгенерированный ключ оказался криптографически слабым (weak), функция возвращает -2. Если ключ удовлетворяет всем требованиям, то он конвертируется в платформенно-зависимый формат и помещается в структуру schedule.
Исходя из вышеизложенного, код генератора ключевой последовательности для алгоритма Triple-DES будет выглядеть следующим образом:
Листинг 4. Генератор ключей для алгоритма Triple-DES
#include <openssl/des.h>
int main()
{
int key, i = 0, j = 0;
DES_cblock cb;
DES_key_schedule ks;
/* Создаем ключевой файл */
key = open(KEYS, O_CREAT|O_TRUNC|O_RDWR, 0600);
/* Генерируем три ключа */
for(; i < 3; i++) {
DES_random_key(&cb);
if((j = DES_set_key_checked(&cb, &ks)) != 0) return j;
if(write(key, (unsigned char *)&ks, DES_SCHEDULE_SZ) < 0) return -1;
}
}
Операцию криптопреобразования по алгоритму Triple-DES с тремя ключами и 64-битной обратной связью по выходу (режим OFB) выполняет функция DES_ede3_ofb64_encrypt:
void DES_ede3_ofb64_encrypt(const unsigned char *in, unsigned char *out, long length, DES_key_schedule *ks1, \
DES_key_schedule *ks2, DES_key_schedule *ks3, DES_cblock *ivec, int *num);
В параметрах функции передаются:
n указатели на блоки незашифрованных и зашифрованных данных in и out;
n размер данных для шифрования length;
n ключи шифрования ks1, ks2, ks3;
n указатель на вектор инициализации ivec (начальное заполнение блока ivec, см. рис. 2-4);
Разъясним назначение последнего параметра int *num. Шифрование выполняется блоками по 64 бита, но длина входного сообщения (файла) не обязательно должна быть кратна этому значению. Например, файл размером 26 байт состоит из трех целых 8-байтовых блоков, а из четвертого блока используются только 2 байта. Так вот значение num показывает, сколько байт последнего 8-байтового блока мы используем, или, как сказано в документации, "how much of the 64bit block we have used is contained in *num" (см. комментарии к функции DES_ede3_ofb64_encrypt, файл crypto/des/ofb64ede.c исходных текстов библиотеки).
Листинг 5. Фрагмент программы для криптопреобразования файла по алгоритму Triple-DES с использованием трех ключей и 64-битной обратной связи по выходу
/* Буферы для входных и выходных (зашифрованных) данных */
unsigned char inbuf[1024], outbuf[1024];
/* Структура для хранения ключевых данных */
DES_key_schedule ks1, ks2, ks3;
/* Считываем три ранее созданных ключа
(key - дескриптор ключевого файла) */
read(key,(unsigned char *)&ks1, DES_SCHEDULE_SZ);
read(key,(unsigned char *)&ks2, DES_SCHEDULE_SZ);
read(key,(unsigned char *)&ks3, DES_SCHEDULE_SZ);
/* Открываем входной и создаем выходной файлы */
. . . .
/* Шифруем файл */
for(;;) {
inlen = fread(inbuf, 1, 1024, in);
if(inlen <= 0) break;
DES_ede3_ofb64_encrypt(inbuf, outbuf, (long)inlen, &ks1, &ks2, &ks3, (DES_cblock *)ivec, &num);
fwrite(outbuf, 1, inlen, out);
}
С перечнем всех функций, реализующих различные режимы алгоритма DES, можно ознакомиться на страницах руководства man des, описание режимов приведено в man des_modes.
Алгоритм Blowfish
Blowfish - симметричный блочный шифр, разработанный Брюсом Шнайером (Bruce Schneier). Этот шифр использует ключи разной длины, обычно 128 бит, и шифрует данные блоками по 64 бита. Алгоритм поддерживает такие же режимы, как и DES (см. раздел "Алгоритм DES"), и считается одним из самых быстрых в своем классе.
Как и в случае алгоритма DES, использование Blowfish включает две стадии: генерацию ключевых данных и собственно криптопреобразование информации.
Генерация ключевых данных предполагает получение Nбитной случайной последовательности и последующее ее преобразование при помощи функции BF_set_key(), прототип которой определен в файле openssl/blowfish.h:
void BF_set_key(BF_KEY *key, int len, const unsigned char *data);
Задача этой функции - поместить ключ длиной len из буфера data в структуру key.
Криптопреобразование данных в режиме CFB c 64битной обратной связью выполняет функция BF_cfb64_encrypt:
void BF_cfb64_encrypt(const unsigned char *in, unsigned char *out, long length, const BF_KEY *schedule, unsigned char *ivec, int *num, int enc);
Думаю, что после того как мы рассмотрели DES, никому не составит труда разобраться в назначении параметров этой функции. Остановимся только на последнем параметре - int enc. Он задает режим работы алгоритма и может принимать два значения: BF_ENCRYPT для режима шифрования и BF_DECRYPT для режима дешифрования. Оба эти значение определены в файле openssl/blowfish.h:
#define BF_ENCRYPT 1
#define BF_DECRYPT 0
А теперь оформим все теоретические выкладки в виде функции криптографического преобразования информации по алгоритму Blowfish, работающему в режиме 64битной обратной связи (CFB-64).
Листинг 6. Функция криптографического преобразования информации по алгоритму Blowfish, режим 64-битной обратной связи (CFB-64)
#include <openssl/blowfish.h>
#define BUFSIZE 1024
/* Функция криптопреобразования информации. Параметры функции: дескрипторы входного и выходного
файлов и режим работы - BF_ENCRYPT для шифрования и BF_DECRYPT для дешифрования */
int do_crypt(FILE *in, FILE *out, int mode)
{
int num = 0;
unsigned char inbuf[BUFSIZE], outbuf[BUFSIZE];
/* Ключ шифрования длиной 128 бит и вектор инициализации */
const unsigned char key[16];
unsigned char iv[8];
BF_KEY bfkey;
/* Помещаем ключ в структуру bfkey */
BF_set_key(&bfkey, sizeof(key), key);
/* Шифруем блоки входного файла */
for(;;) {
int inlen = fread(inbuf, 1, BUFSIZE, in);
if(inlen <= 0) break;
BF_cfb64_encrypt(inbuf, outbuf, (long)inlen, &bfkey, iv, &num, mode);
fwrite(outbuf, 1, inlen, out);
}
return 1;
}
Следующий фрагмент функции иллюстрирует порядок криптопреобразования по алгоритму Blowfish, режим 64битной обратной связи по выходу (OFB-64).
Листинг 7. фрагмент функции криптографического преобразования информации по алгоритму Blowfish, режим 64-битной обратной связи по выходу (OFB-64)
void do_crypt(FILE *in, FILE *out)
{
unsigned char inbuf[BUFSIZE];
unsigned char outbuf[BUFSIZE];
/* Ключ и вектор инициализации */
. . .
BF_set_key(&bfkey, KEY_SIZE, key);
for(;;) {
int inlen = fread(inbuf, 1, BUFSIZE, in);
if(inlen <= 0) break;
BF_ofb64_encrypt(inbuf, outbuf, (long)inlen, &bfkey, iv, &num);
fwrite(outbuf, 1, inlen, out);
}
}
Использование высокоуровневых функций библиотеки для шифрования
Как и в случае алгоритмов хэширования (см. раздел "Вычисление хэшей"), разработчики библиотеки рекомендуют использовать функции высокого уровня вместо прямого обращения к функциям алгоритма шифрования. Порядок использования высокоуровневых функций шифрования и хэширования идентичен - в обоих случаях необходимо создать и инициализировать контекст, записав в него адреса функций необходимых алгоритмов. Адреса выбираются из внутренней таблицы, куда они попадают при помощи функции OpenSSL_add_all_ciphers().
Контекст алгоритма шифрования представляет собой структуру типа EVP_CIPHER_CTX, определенную в файле openssl/evp.h. Первым элементом этой структуры является указатель на структурный тип EVP_CIPHER следующего вида:
struct evp_cipher_st {
int nid;
int block_size;
int key_len; /* Default value for variable length ciphers */
int iv_len;
unsigned long flags; /* Various flags */
int (*init)(EVP_CIPHER_CTX *ctx, const unsigned char *key, const unsigned char *iv, int enc); /* init key */
int (*do_cipher)(EVP_CIPHER_CTX *ctx, unsigned char *out, const unsigned char *in, unsigned int inl);/* encrypt/decrypt data */
int (*cleanup)(EVP_CIPHER_CTX *); /* cleanup ctx */
int ctx_size; /* how big ctx->cipher_data needs to be */
int (*set_asn1_parameters)(EVP_CIPHER_CTX *, ASN1_TYPE *);
int (*get_asn1_parameters)(EVP_CIPHER_CTX *, ASN1_TYPE *);
int (*ctrl)(EVP_CIPHER_CTX *, int type, int arg, void *ptr);
void *app_data; /* Application data */
} /* EVP_CIPHER */;
Этот структурный тип содержит в своем составе указатели на функции, которые заполняются необходимыми нам значениями - адресами функций соответствующих алгоритмов. Получить адреса этих функций можно двумя способами.
Первый способ подразумевает поиск по символьному имени алгоритма:
OpenSSL_add_all_ciphers();
const EVP_CIPHER * cipher = EVP_get_cipherbyname("des_cbc");
Функция EVP_get_cipherbyname(const char * name) извлекает адреса функций алгоритма шифрования из внутренней таблицы библиотеки и заполняет структуру cipher. Входные параметры функции - символьное имя алгоритма, в данном случае это DES, режим CBC. Перечень всех имен содержится в файле openssl/object.h.
Второй способ - прямое обращение к нужной EVP-функции:
const EVP_CIPHER *cipher = EVP_des_cbc();
Получив адреса функций, инициализируем контекст алгоритма:
EVP_CIPHER_CTX_init(EVP_CIPHER_CTX *ctx);
EVP_EncryptInit(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher, const unsigned char *key, const unsigned char *iv);
Функция EVP_CIPHER_CTX_init обнуляет структуру, выделенную под контекст. Инициализацию контекста выполняет функция EVP_EncryptInit(). Параметры функции - указатель на контекст алгоритма ctx, структура с адресами библиотечных функций алгоритма cipher, блок с ключевыми данными key и вектор инициализации iv.
После инициализации контекста можно приступать к операции шифрования. Делает это функция EVP_EncryptUpdate():
int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl, const unsigned char *in, int inl);
Функция шифрует inl байтов из буфера in и записывает зашифрованные данные в буфер out. В переменной outl сохраняется количество зашифрованных байтов.
Если размер сообщения не кратен размеру блока шифрования, то выполняется вызов функции EVP_EncryptFinal, которая "дошифровывает" оставшиеся данные:
int EVP_EncryptFinal(EVP_CIPHER_CTX *ctx, unsigned char *out, int *outl);
Следующий код демонстрирует использование функций высокого уровня для шифрования файла по алгоритму AES с ключом длиной 256 бит, режим 64-битовой шифрованной обратной связи CFB.
Листинг 8. Шифрование файла по алгоритму AES, длина ключа 256 бит, режим 64-битовой шифрованной обратной связи
#include <openssl/evp.h>
#define BUFSIZE 1024
int do_crypt(char *infile)
{
int outlen, inlen;
FILE *in, *out;
unsigned char key[32]; /* 256- битный ключ */
unsigned char iv[8]; /* вектор инициализации */
unsigned char inbuf[BUFSIZE], outbuf[BUFSIZE];
EVP_CIPHER_CTX ctx;
const EVP_CIPHER * cipher;
/* Обнуляем структуру контекста */
EVP_CIPHER_CTX_init(&ctx);
/* Выбираем алгоритм шифрования */
cipher = EVP_aes_256_cfb();
/* Инициализируем контекст алгоритма */
EVP_EncryptInit(&ctx, cipher, key, iv);
/* Шифруем данные */
for(;;) {
inlen = fread(inbuf, 1, BUFSIZE, in);
if(inlen <= 0) break;
if(!EVP_EncryptUpdate(&ctx, outbuf, &outlen, inbuf, inlen)) return 0;
fwrite(outbuf, 1, outlen, out);
}
if(!EVP_EncryptFinal(&ctx, outbuf, &outlen)) return 0;
fwrite(outbuf, 1, outlen, out);
EVP_CIPHER_CTX_cleanup(&ctx);
return 1;
}
Если мы захотим использовать другой алгоритм, нам достаточно будет заменить одну строку в исходном тексте. Например, для использования алгоритма Blowfish в режиме шифрованной обратной связи по выходу (OFB) необходимо заменить строку:
cipher = EVP_aes_256_cfb();
строкой:
cipher = EVP_bf_ofb();
а также задать правильную длину ключевых данных.
Список всех EVP-функций находится в файле openssl/evp.h.
Обратный процесс дешифрования информации отличается только названиями функций: вместо "EVP_EnryptInit" пишем "EVP_DecryptInit", вместо "EVP_EncryptUpdate" - "EVP_DecryptUpdate" и т. д. Фрагмент функции дешифрования файла, зашифрованного по алгоритму AES с 256-битным ключом в режиме 64-битовой обратной связи, представлен в листинге 9.
Листинг 9. Дешифрование файла, зашифрованного по алгоритму AES, длина ключа 256 бит, режим 64-битовой шифрованной ОС
int do_decrypt(char *infile)
{
/* Объявляем переменные */
. . . .
/* Обнуляем контекст и выбираем алгоритм дешифрования */
EVP_CIPHER_CTX_init(&ctx);
EVP_DecryptInit(&ctx, EVP_aes_256_cfb(), key, iv);
/* Открываем входной и создаем выходной файлы */
. . . .
/* Дешифруем данные */
for(;;) {
inlen = fread(inbuf, 1, BUFSIZE, in);
if(inlen <= 0) break;
if(!EVP_DecryptUpdate(&ctx, outbuf, &outlen,
inbuf, inlen)) return 0;
fwrite(outbuf, 1, outlen, out);
}
/* Завершаем процесс дешифрования */
if(!EVP_DecryptFinal(&ctx, outbuf, &outlen)) return 0;
. . . .
}
Работоспособность всех программ была проверена для ОС Linux Slackware 10.2, компилятор gcc-3.3.6, библиотека OpenSSL 0.9.7c. Исходные тексты всех программ, рассмотренных в данной статье, вы можете скачать с сайта http://bob.netport.com.ua/ssl.tar.gz.
Во второй части статьи мы рассмотрим примеры использования функций библиотеки для криптографической защиты данных с помощью ассиметричных алгоритмов.
Литература:
1. Стахов В. Теория и практика OpenSSL. - Журнал "Системный администратор", N1(2), январь 2003 г. - 17-26 с.
2. Шнайер Б. Прикладная криптография. Протоколы, алгоритмы, исходные тексты на языке Си. - М.: Издательство ТРИУМФ, 2003 - 816 с.; ил.
Используем средства библиотеки OpenSSL
для криптографической защиты данных
Часть 2
Владимир Мешков
Благодаря своим уникальным свойствам ассиметричные алгоритмы шифрования применяются для защиты информации в тех областях, где использование симметричных алгоритмов затруднительно. Это защита сообщений электронной почты, HTTP-трафика, организация безопасного доступа для удаленного администрирования (Security Shell). Самый популярный представитель семейства ассиметричных алгоритмов - алгоритм RSA.
Ассиметричные алгоритмы
Симметричные алгоритмы наряду со всеми своими достоинствами имеют большой недостаток: потеря секретного ключа грозит потерей всех данных, которые с помощью этого ключа зашифрованы (имеется в виду не физическая потеря, а потеря конфиденциальности).
Использование симметричной схемы шифрования требует передачи одного и того же ключа двум и более сторонам по независимому и надежному каналу. Этот канал должен исключать возможность утечки информации, обеспечивать полный контроль над процессом передачи ключа и гарантировать его доставку получателю. Когда количество абонентов невелико и расположены они недалеко друг от друга (например, в пределах одного города), гарантированно доставить ключ абоненту достаточно просто. А если абоненты находятся в разных городах или их количество очень велико? В таком случае организация надежного канала доставки ключевых данных - задача весьма непростая, требующая значительных затрат.
Для решения в первую очередь задачи распределения ключей была выдвинута концепция двухключевой (или ассиметричной) криптографии.
Для шифрования и дешифрования применяются различные ключи. Для шифрования информации, предназначенной конкретному получателю, используют уникальный открытый ключ получателя-адресата. Соответственно для дешифрования получатель использует парный, создаваемый одновременно с открытым, секретный ключ. Для передачи открытого ключа от получателя к отправителю секретный канал не нужен.
Алгоритм RSA. Теория
Криптосистема RSA, предложенная в 1977 году Ривестом (R. Rivest), Шамиром (A. Shamir) и Адлеманом (L. Adleman), предназначена для шифрования и цифровой подписи. В настоящее время RSA является наиболее распространенной криптосистемой - стандартом де-факто для многих криптографических приложений. Криптосистема RSA широко применяется в составе различных стандартов и протоколов Интернета, включая PEM, S/MIME, PEM-MIME, S-HTTP и SSL.
Криптографическая стойкость алгоритма RSA основана на трудоемкости разложения на множители (факторизации) больших чисел. Термин "большие" означает, что число содержит 100~200 и более двоичных разрядов. Открытый и секретный ключи являются функциями двух больших простых чисел. Рассмотрим на примере, как выполняется генерация ключей алгоритма RSA, но вместо больших чисел для простоты изложения будем использовать маленькие десятичные.
Для генерации парных ключей используются два случайных простых числа, p и q. Вычисляется произведение этих чисел n и значение функции Эйлера от числа n по формуле:
φ(n)=(p-1)(q-1) [1]
Далее выбирается ключ шифрования e такой, что e и значение функции Эйлера φ(n) являются взаимно простыми числами, т.е. числами, не имеющими общих делителей, кроме единицы (единицу еще называют тривиальным делителем). Теперь необходимо найти значение ключа дешифрования d такое, чтобы выполнялось равенство:
ed = 1(mod φ(n)) [2]
или
d = e-1(mod φ(n)) [3]
Уравнение [2] означает, что остаток от деления произведения чисел e и d на значение функции Эйлера φ(n) должен быть равен 1.
Условие [2] выполняется только в том случае, если e и φ(n) являются взаимно простыми числами. Число d называется взаимно обратным к e по модулю φ(n). Уравнение [2] эквивалентно обнаружению таких d и v, что:
ed + φ(n)v = 1 [4]
Поиск обратного значения числа по модулю выполняется при помощи алгоритма Эвклида. Этот алгоритм позволяет найти наибольший общий делитель (НОД) двух чисел.
Рассмотрим пример. Пусть у нас имеются два простых числа: p=13 и q=17. Найдем произведение этих чисел:
n = 13 * 17 = 221
и значение функции Эйлера от числа n=221:
φ(n)=(p-1)(q-1)=(13-1)(17-1)=192
Теперь выберем такое число e, чтобы оно было взаимно простым с φ(n). Таким числом является, например, e=7. Далее надо найти обратное значение числа e, чтобы выполнялось уравнение [2]. Для этого с помощью алгоритма Эвклида ищем значения d и v, удовлетворяющие соотношению [4]. Суть алгоритма сводится к проведению последовательности операций деления с остатком. В соответствии с алгоритмом находим частное и остаток от деления φ(n) на e:
192 = 7 * 27 + 3
Частное равно 27, остаток - 3. Теперь последовательно делим делитель на остаток (т. е. 7 на 3 в данном случае) до тех пор, пока в остатке не получим единицу:
7 = 3 * 2 + 1
А теперь распишем процесс получения остатка в обратном порядке:
1 = 7 - 3 * 2 = 7 - (192 - 7 * 27) * 2 = 7 - (192 * 2 - 7 * 2 * 27)
Раскроем скобки:
1 = 7 + 7 * 54 - 192 * 2 = 7 * 55 - 192 * 2
В итоге получаем искомые числа d=55 и v=2. Числа d=55 и e=7 являются взаимно обратными по модулю 192, что подтверждает равенство:
7 * 55 = 1(mod 192)
Учитывая, что e=7 - это наш ключ шифрования, то число d=55 будет ключом дешифрования.
Теперь разберемся, как выполняются операции шифрования и дешифрования информации по алгоритму RSA.
Для шифрования исходное сообщение необходимо представить в виде последовательности чисел, содержащихся в интервале от 0 до n. Для примера, разобьем аббревиатуру ABC на числа в интервале (0,221). Для этого достаточно каждый символ записать в десятичном представлении:
A=41h=65, B=42h=66, C=43h=67
Шифрование сводится к вычислению:
Ci = Mi e(mod n)
Здесь Mi - это i-й блок сообщения, Ci - результат криптопреобразования. Выражаясь простым языком, мы должны возвести значение Mi в степень e и найти остаток от деления на n.
Зашифруем нашу последовательность (65,66,67), зная, что e=7 и n=221:
C1 = 657(mod 221) = 91
C2 = 667(mod 221) = 144
C3 = 677(mod 221) = 50
В зашифрованном виде наша последовательность будет выглядеть как (91,144,50).
Для дешифрования необходимо выполнить следующую операцию:
Mi = Ci d(mod n)
Дешифруем последовательность (91,144,50) при d=55 и n=221:
M1 = 9155(mod 221) = 65
M2 = 14455(mod 221) = 66
M3 = 5055(mod 221) = 67
Таким образом, исходная последовательность восстановлена.
Шифрование RSA выполняется намного эффективнее, если правильно выбрать значение e. Чаще всего используются 3, 17 и 65537. Стандарт X.509 рекомендует 65537, PEM - 3, PKCS#1 - 3 или 65537.
Ознакомившись с теорией, приступим к рассмотрению средств, предоставляемых библиотекой для защиты информации по RSA алгоритму.
Функции библиотеки для защиты информации по RSA-алгоритму
Прежде чем изучить вышеозначенные функции, приостановимся на минуту и подумаем - если мы оперируем с числами, разрядность которых составляет ~200 битов, то мы должны их где-то хранить. А ведь их надо не только хранить, но и проводить над ними различные математические операции, такие как умножение, деление, возведение в степень и т. п. Очевидно, что стандартные типы языка программирования Си, например, long или double long, и прямое использование функций стандартной библиотеки этого языка, таких как "+", "*" и т. п. для этих целей совершенно непригодны. Поэтому библиотека OpenSSL содержит ряд специальных функций для работы с большими числами, разрядность которых превышает разрядность адресной шины и регистров процессора. Для хранения этих чисел используется динамическая память. Базовым объектом библиотеки для работы с такими числами является объект типа BIGNUM. Этот тип определен в файле openssl/bn.h:
#define BN_ULONG unsigned char
typedef struct bignum_st
{
/* Pointer to an array of 'BN_BITS2' bit chunks. */
BN_ULONG *d;
int top; /* Index of last used d +1. */
/* The next are internal book keeping for bn_expand. */
int dmax; /* Size of the d array. */
int neg; /* one if the number is negative */
int flags;
} BIGNUM;
Приведем краткий перечень функций библиотеки для работы с большими числами:
n BIGNUM * BN_new(void) - создает объект типа BIGNUM и возвращает указатель на него;
n int BN_add(BIGNUM *r, const BIGNUM *a, const BIGNUM *b) - суммирует числа a и b, результат помещает в r (r=a+b);
n int BN_sub(BIGNUM *r, const BIGNUM *a, const BIGNUM *b) - выполняет операцию вычитания числа b из числа а, результат сохраняется в r (r=a-b);
n int BN_mul(BIGNUM *r, BIGNUM *a, BIGNUM *b, BN_CTX *ctx) - умножает число a на число b и сохраняет результат в r (r=a*b). Последний параметр BN_CTX *ctx используется для хранения промежуточных результатов вычисления (а BN_CTX is a structure that holds BIGNUM temporary variables used by library functions);
n int BN_div(BIGNUM *dv, BIGNUM *rem, const BIGNUM *a, const BIGNUM *d, BN_CTX *ctx) - делит число a на b, частное сохраняется в dv, остаток - в rem (dv=a/b, rem=a%b).
Подробную информацию по функциям типа BN_* смотрите на странице руководства man bn.
Все ассимметричные алгоритмы, реализованные в библиотеке, такие как RSA, DSA, Diffie-Hellman, используют эти функции.
Генерация ключей алгоритма RSA
Как в и случае симметричных алгоритмов, для криптографической защиты информации по алгоритму RSA необходимо вначале сгенерировать открытый и секретный ключи. Генерацию ключей алгоритма RSA выполняет функция RSA_generate_key следующего вида:
RSA *RSA_generate_key(int num, unsigned long e, void (*callback)(int,int,void *), void *cb_arg);
Параметры функции:
n int num - размер ключа в битах;
n unsigned long e - это то самое число e, с которым мы познакомились в пункте 2, когда рассматривали теоретические основы алгоритма RSA. Этот параметр обычно принимает значения 3, 17 или 65537.
Два последних параметра, указатели на функцию (*callback) и void *cb_arg, служат для предоставления обратной связи (feedback) с процессом генерации ключевой информации. Это, как правило, индикация хода выполнения операции генерирования ключей. Например, если мы выполним команду:
openssl genrsa -out outfile 2048
то увидим, как на экране начнут появляться символы "." и "+", отображающие процесс формирования ключей. Найдем в файле apps/genrsa.c исходных текстов библиотеки вызов функции RSA_generate_key:
rsa=RSA_generate_key(num,f4,genrsa_cb,bio_err);
Третий параметр - функция genrsa_cb - находится в этом же файле и имеет следующий вид:
static void MS_CALLBACK genrsa_cb(int p, int n, void *arg)
{
char c='*';
if (p == 0) c='.';
if (p == 1) c='+';
if (p == 2) c='*';
if (p == 3) c='\n';
BIO_write((BIO *)arg,&c,1);
(void)BIO_flush((BIO *)arg);
#ifdef LINT
p=n;
#endif
}
Как видно из текста этой функции, именно она выводит на экран символы "." и "+", отображающие ход операции генерирования ключей.
Результаты работы функции RSA_generate_key в виде открытого и закрытого ключа сохраняются в структуре типа RSA (см. include/openssl/rsa.h). Эти ключи необходимо извлечь и записать в файлы для дальнейшей работы с ними. Делается это при помощи следующих двух функций:
int PEM_write_RSAPublicKey(FILE *fp, RSA *x);
int PEM_write_RSAPrivateKey(FILE *fp, RSA *x, const EVP_CIPHER *enc, unsigned char *kstr, int klen, pem_password_cb *cb, void *u);
Первая функция записывает в файл fp открытый ключ, который находится в структуре типа RSA, на которую указывает параметр x. Вторая функция изымает секретный ключ из структуры x и записывает его в файл fp. Как правило, секретный ключ зашифровывают, что способствует повышению его защищенности. Выбор алгоритма шифрования выполняется с помощью параметра const EVP_CIPHER *enc (контекст алгоритма шифрования, см. первую часть статьи). Но для шифрования необходимо задать ключевую фразу (пароль), и сделать это можно несколькими способами.
Можно указать в параметре cb (сокращение от callback) адрес функции, которая будет запрашивать пароль. Если указатель kstr не будет равен NULL, то в качестве пароля будут использованы первые klen символов из массива, на который указывает kstr, при этом параметр cb игнорируется. Если cb == NULL, а параметр u не равен NULL, то u интерпретируется как строка, заканчивающаяся нулем, и эта строка используется как пароль. Также можно все параметры (kstr, cb, u) установить в NULL, и библиотека запросит у нас парольную фразу самостоятельно, используя свои внутренние механизмы.
Обобщим все вышесказанное в виде фрагмента программы, генерирующей ключи по алгоритму RSA, при этом секретный ключ шифруется по алгоритму Blowfish с обратной связью по выходу. Длина ключа - 2048 бит. В целях экономии места код, выполняющий обработку ошибок, пропущен.
Листинг 1. Генерация ключей алгоритма RSA
#include <stdio.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
/* Имена ключевых файлов */
#define PRIVAT "./privat.key"
#define PUBLIC "./public.key"
void main()
{
/* указатель на структуру для хранения ключей */
RSA * rsa = NULL;
unsigned long bits = 2048; /* длина ключа в битах */
FILE *priv_key_file = NULL, *pub_key_file = NULL;
/* контекст алгоритма шифрования */
const EVP_CIPHER *cipher = NULL;
priv_key_file = fopen(PRIVAT, "wb");
pub_key_file = fopen(PUBLIC, "wb");
/* Генерируем ключи */
rsa = RSA_generate_key(bits, RSA_F4, NULL, NULL);
/* Формируем контекст алгоритма шифрования */
OpenSSL_add_all_ciphers();
cipher = EVP_get_cipherbyname("bf-ofb");
/* Получаем из структуры rsa открытый и секретный ключи и сохраняем в файлах.
* Секретный ключ шифруем с помощью парольной фразы "hello"
*/
PEM_write_RSAPrivateKey(priv_key_file, rsa, cipher,
NULL, 0, NULL, "hello");
PEM_write_RSAPublicKey(pub_key_file, rsa);
/* Освобождаем память, выделенную под структуру rsa */
RSA_free(rsa);
}
Если в вызове функции PEM_write_RSAPrivateKey мы вместо "hello" оставим NULL, то библиотека самостоятельно попросит наc ввести парольную фразу для шифрования секретного ключа.
Шифрование и дешифрование по алгоритму RSA
Сформировав ключи, можно приступать к шифрованию данных. Для этого используется функция RSA_public_encrypt, которая имеет следующий прототип:
int RSA_public_encrypt(int flen, unsigned char *from, unsigned char *to, RSA *rsa, int padding);
Эта функция шифрует flen байт из буфера, на который указывает параметр from, используя ключ из структуры RSA * rsa, и помещает результат в буфер to. Размер этого буфера должен быть равен размеру ключа, который определяется при помощи функции RSA_size(RSA *).
Параметр padding используется для выбора режима выравнивания данных. В большинстве случаев используется значение RSA_PKCS1_PADDING, что соответствует стандарту PKCS#1. При использовании этого режима размер буфера from должен быть не меньше (RSA_size(rsa) - 11), т.е. на 11 байт меньше размера ключа. Размер выходных данных всегда будет кратен длине ключа.
Следующая программа демонстрирует использование функции RSA_public_encrypt для шифрования данных.
Листинг 2. Шифрование данных по алгоритму RSA
#include <openssl/rsa.h>
#include <openssl/pem.h>
void main(int argc, char **argv)
{
/* структура для хранения открытого ключа */
RSA * pubKey = NULL;
unsigned char *ptext, *ctext;
FILE * pub_key_file = NULL;
/* Открываем входной и создаем выходной файлы */
int inf = open(argv[1], O_RDWR);
int outf = open("./rsa.file",
O_CREAT|O_TRUNC|O_RDWR, 0600);
/* Считываем открытый ключ */
pub_key_file = fopen(PUBLIC, "rb");
pubKey = PEM_read_RSAPublicKey(pub_key_file, NULL, NULL, NULL);
/* Определяем длину ключа */
int key_size = RSA_size(pubKey);
ptext = malloc(key_size);
ctext = malloc(key_size);
/* Шифруем содержимое входного файла */
while(1) {
inlen = read(inf, ptext, key_size - 11);
if(inlen <= 0) break;
outlen = RSA_public_encrypt(inlen, ptext, ctext, pubKey, RSA_PKCS1_PADDING);
if(outlen != RSA_size(pubKey)) exit(-1);
write(outf, ctext, outlen);
}
}
Дешифрование данных выполняет функция RSA_private_decrypt:
int RSA_private_decrypt(int flen, unsigned char *from, unsigned char *to, RSA *rsa, int padding);
Эта функция расшифровывает flen байт из буфера from, используя ключ rsa, и записывает результаты в буфер to.
Следующий фрагмент программы демонстрирует использование функции RSA_private_decrypt для дешифрования данных.
Листинг 3. Дешифрование файла, зашифрованного по RSA-алгоритму
#include <openssl/rsa.h>
#include <openssl/pem.h>
void main(int argc, char **argv)
{
RSA *privKey = NULL;
FILE *priv_key_file;
unsigned char *ptext, *ctext;
/* Открываем входной и создаем выходной файл */
inf = open(argv[1], O_RDWR);
outf = open("./test.rsa", O_CREAT|O_TRUNC|O_RDWR, 0600);
/* Открываем ключевой файл и считываем секретный ключ */
priv_key_file = fopen(PRIVAT, "rb");
privKey = PEM_read_RSAPrivateKey(priv_key_file, NULL, NULL, NULL);
/* Определяем размер ключа */
key_size = RSA_size(privKey);
ctext = malloc(key_size);
ptext = malloc(key_size);
/* Дешифруем файл */
while(1) {
inlen = read(inf, ctext, key_size);
if(inlen <= 0) break;
outlen = RSA_private_decrypt(inlen, ctext, ptext, privKey, RSA_PKCS1_PADDING);
if(outlen < 0) exit(0);
write(outf, ptext, outlen);
}
}
Работоспособность программ была проверена для ОС Linux Slackware 10.2, библиотека OpenSSL версии 0.9.7c
Итак, вы разобрались с теоретическими основами алгоритма RSA и научились использовать некоторые функции библиотеки для защиты информации по этому алгоритму. Я говорю "некоторые", потому что этих функций в библиотеке очень много, и описать их в одной статье практически невозможно. Поэтому основным источником изучения средств библиотеки были и остаются официальная документация от разработчиков, а также любые исходные тексты программ, использующих эту библиотеку.