Pull to refresh
1062.35
OTUS
Цифровые навыки от ведущих экспертов

Введение в OpenSSL: основы криптографиии

Reading time14 min
Views30K
Original author: Marty Kalin

Если вы искали руководство по криптографии, лежащей в основе OpenSSL, то вы оказались в правильном месте.

Эта статья является первой в серии из двух статей, посвященных основам криптографии, используемой в OpenSSL — библиотеке инструментов промышленного уровня, популярной и в среде Linux, и за ее пределами. (Чтобы установить самую последнюю версию OpenSSL, перейдите сюда.) Что касается взаимодействия с библиотекой, то вы можете вызывать ее функции из кода, а также в вашем распоряжении есть утилиты командной строки. Примеры кода для этой статьи приведены на C - на том же языке, на котором написана сама библиотека OpenSSL.

Две статьи этой серии в совокупности охватывают такие темы, как криптографические хеши, цифровые подписи, цифровые сертификаты, а также шифрование и дешифрование. Вы можете найти код и примеры для командной строки в ZIP-архиве на моем сайте.

Начнем же мы с обзора SSL, которому библиотека OpenSSL обязана своим именем (большей его части).

Краткая история

Secure Socket Layer (SSL, слой защищенных сокетов) — это криптографический протокол, выпущенный Netscape в 1995 году. Этот протокольный уровень может располагаться поверх HTTP, тем самым добавляя в конец букву S (HTTPS), которая означает secure. Протокол SSL предоставляет различные меры обеспечения безопасности, две из которых являются основополагающими в HTTPS:

  • Взаимная аутентификация (peer authentication, также известная как mutual challenge): каждая сторона соединения аутентифицирует идентификационные данные другой стороны. Если Алиса и Боб собираются обмениваться сообщениями через SSL, то сначала каждый должен аутентифицировать идентификационные данные своего собеседника.

  • Конфиденциальность: отправитель шифрует сообщения перед их отправкой по каналу связи. Затем получатель расшифровывает каждое полученное сообщение. Этот процесс защищает сетевое взаимодействие. Даже если злоумышленник Ева перехватит зашифрованное сообщение от Алисы к Бобу (атака через посредника), то она будет не в состоянии расшифровать это сообщение из-за необходимой для этого вычислительной сложности.

Эти две ключевые меры обеспечения безопасности SSL, в свою очередь, связаны с другими, которые зачастую остаются в тени. Например, SSL поддерживает целостность сообщений, что гарантирует, что полученное сообщение совпадает с отправленным. Эта фича реализована на основе хеш-функций, которые также входят в набор инструментов OpenSSL.

SSL имеет разные версии (например, SSLv2 и SSLv3), а в 1999 году на основе SSLv3 был реализован аналогичный протокол Transport Layer Security (TLS, протокол защиты транспортного уровня). TLSv1 и SSLv3 похожи, но не настолько, чтобы работать друг с другом. Тем не менее, принято называть SSL/TLS одним и тем же протоколом. Например, функции OpenSSL часто содержат SSL в имени, даже если используется TLS, а не SSL. Кроме того, вызов утилит командной строки OpenSSL начинается со строки openssl.

Документация по OpenSSL, к сожалению, достаточно неоднородна в своей полноте, особенно если смотреть за пределами справочных страниц (man pages), которые сами по себе достаточно громоздкие, учитывая, насколько велик инструментарий OpenSSL. В этой статье я  сфокусирую внимание на основных темах посредством примеров кода и командной строки. Начнем мы со знакомого всем нам примера — доступа к веб-сайту по HTTPS — и используем этот пример, чтобы разобрать основополагающие криптографические элементы.

HTTPS-клиент 

Приведенная ниже программа client подключается к Google по HTTPS:

/* компиляция: gcc -o client client.c -lssl -lcrypto */ 

#include <stdio.h>

#include <stdlib.h>

#include <openssl/bio.h> /* Базовые потоки ввода/вывода */

#include <openssl/err.h> /* ошибки */

#include <openssl/ssl.h> /* основная библиотека */

#define BuffSize 1024


void report_and_exit(const char* msg) {
  perror(msg);
  ERR_print_errors_fp(stderr);
  exit(-1);
}

 

void init_ssl() {
  SSL_load_error_strings();
  SSL_library_init();
}

 

void cleanup(SSL_CTX* ctx, BIO* bio) {
  SSL_CTX_free(ctx);
  BIO_free_all(bio);
}

 

void secure_connect(const char* hostname) {
  char name[BuffSize];
  char request[BuffSize];
  char response[BuffSize];

 

  const SSL_METHOD* method = TLSv1_2_client_method();
  if (NULL == method) report_and_exit("TLSv1_2_client_method...");

  SSL_CTX* ctx = SSL_CTX_new(method);
  if (NULL == ctx) report_and_exit("SSL_CTX_new...");

  BIO* bio = BIO_new_ssl_connect(ctx);
  if (NULL == bio) report_and_exit("BIO_new_ssl_connect...");

  SSL* ssl = NULL;

  /* связываем канал bio, сессию SSL и конечную точку сервера */

  sprintf(name, "%s:%s", hostname, "https");
  BIO_get_ssl(bio, &ssl); /* сессия */
  SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY); /* надежность */
  BIO_set_conn_hostname(bio, name); /* подготовка к подключению */

  /* пытаемся подключиться */
  if (BIO_do_connect(bio) <= 0) {
    cleanup(ctx, bio);
    report_and_exit("BIO_do_connect...");
  }


  /* проверяем хранилище доверенных сертификатов и сам сертификат */
  if (!SSL_CTX_load_verify_locations(ctx,
                                      "/etc/ssl/certs/ca-certificates.crt", /* хранилище доверенных сертификатов */


                                      "/etc/ssl/certs/")) /* больше доверенных сертификатов*/


    report_and_exit("SSL_CTX_load_verify_locations...");


  long verify_flag = SSL_get_verify_result(ssl);
  if (verify_flag != X509_V_OK)
    fprintf(stderr,
            "##### Certificate verification error (%i) but continuing...\n",
            (int) verify_flag);

  /* теперь получаем домашнюю страницу в качестве проверочных данных */
  sprintf(request,
          "GET / HTTP/1.1\x0D\x0AHost: %s\x0D\x0A\x43onnection: Close\x0D\x0A\x0D\x0A",
          hostname);
  BIO_puts(bio, request);
 

  /* читаем HTTP-ответ с сервера и выводим через stdout */
  while (1) {
    memset(response, '\0', sizeof(response));
    int n = BIO_read(bio, response, BuffSize);
    if (n <= 0) break; /* 0 — конец потока, < 0 — ошибка */


  puts(response);
  }


  cleanup(ctx, bio);
}


int main() {
  init_ssl();


  const char* hostname = "www.google.com:443";
  fprintf(stderr, "Trying an HTTPS connection to %s...\n", hostname);
  secure_connect(hostname);

return 0;
}

(Прим. переводчика: этот код оказался нерабочим, исправленную версию можно посмотреть здесь)

Эту программу можно скомпилировать и запустить из командной строки (обратите внимание на строчную букву L в -lssl и -lcrypto):

gcc -o client client.c -lssl -lcrypto

Эта программа пытается установить безопасное соединение с сайтом www.google.com. В рамках TLS-рукопожатия с сервером Google программа-клиент получает один или несколько цифровых сертификатов, которые она затем пытается проверить (в моей системе они проверку не проходят). Тем не менее, далее программа-клиент получает по защищенному каналу домашнюю страницу Google. Эта программа полагается на упомянутые ранее артефакты безопасности, хотя в коде можно выделить только цифровой сертификат. Остальные же артефакты пока остаются за кадром, подробнее о них мы поговорим чуть дальше.

Как правило, программа-клиент на C или C++, которая открывает (незащищенный) HTTP-канал, будет использовать такой объект, как файловый дескриптор сетевого сокета, который является конечной точкой в ​​соединении между двумя процессами (например, программой-клиентом и сервером Google). Файловый дескриптор, в свою очередь, представляет собой неотрицательное целочисленное значение, которое идентифицирует внутри программы любую файловую конструкцию, которую эта программа открывает. Такой программе также понадобится структура для указания сведений об адресе сервера.

Ни один из этих относительно низкоуровневых объектов не встречается в программе-клиенте, поскольку библиотека OpenSSL заключает инфраструктуру сокетов и спецификацию адресов в высокоуровневые объекты безопасности. В результате мы получаем простой API. Вот какие процессы обеспечения безопасности мы можем увидеть в этом примере программы-клиента, не заглядывая под капот:

  • Программа начинается с загрузки необходимых библиотек OpenSSL, при этом моя функция init_ssl выполняет два вызова OpenSSL:

SSL_library_init(); SSL_load_error_strings();
  • Следующий шаг инициализации пытается получить контекст, информационную структуру, необходимую для установления и поддержания безопасного канала с сервером. В этом примере используется TLS1.2, что  мы можем увидеть в этом вызове функции из библиотеки OpenSSL:

const SSL_METHOD* method = TLSv1_2_client_method(); /* TLS 1.2 */

Если вызов выполнен успешно, то указатель method передается библиотечной функции, создающей контекст типа SSL_CTX:

SSL_CTX* ctx = SSL_CTX_new(method);

Программа-клиент проверяет наличие ошибок в каждом из этих критических библиотечных вызовов, а затем завершает работу, если какой-либо вызов завершается некорректно.

  • Теперь в игру вступают два других артефакта OpenSSL: сессия безопасности (security session) типа SSL, которая от начала и до конца управляет безопасным соединением; и защищенный поток типа BIO (базовый ввод/вывод), который используется для связи с сервером. Поток BIO создается с помощью следующего вызова:

BIO* bio = BIO_new_ssl_connect(ctx);

Обратите внимание, что аргументом является наша чрезвычайно важная переменная с контекстом. Тип BIO — это обертка OpenSSL для типа FILE в C. Эта обертка защищает потоки ввода/вывода между программой-клиентом и сервером Google.

  • Располагая SSL_CTX и BIO, программа затем связывает их вместе в сессии SSL. Эту работу выполняют три следующих библиотечных вызова:

BIO_get_ssl(bio, &ssl); /* получает сессию TLS */SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY); /* для надежности */BIO_set_conn_hostname(bio, name); /* подготовка к соединению с Google */

Само безопасное соединение устанавливается с помощью этого вызова:

BIO_do_connect(bio);

Если этот последний вызов не увенчается успехом, программа-клиент завершает работу; в противном случае соединение готово для поддержки конфиденциального обмена данными между программой-клиентом и сервером Google.

Во время рукопожатия с сервером программа-клиент получает один или несколько цифровых сертификатов, удостоверяющих подлинность сервера. Однако программа-клиент не отправляет собственный сертификат, что означает одностороннюю аутентификацию. (Серверы обычно настроены не ожидать сертификата клиента.) Несмотря на неудачную проверку сертификата сервера, программа-клиент получает домашнюю страницу Google по безопасному каналу с сервером.

Почему проверка сертификата Google не заканчивается успехом? Типичная установка OpenSSL имеет каталог /etc/ssl/certs, который включает файл ca-certificates.crt. Каталог и файл содержат цифровые сертификаты, которым OpenSSL доверяет из коробки и, соответственно, составляют хранилище доверенных сертификатов (truststore). Хранилище доверенных сертификатов можно обновлять по мере необходимости, в частности, включать новые доверенные сертификаты и удалять ненадежные.

Программа-клиент получает три сертификата от сервера Google, но хранилище доверенных сертификатов OpenSSL на моей машине не содержит точных совпадений с ними. Как я в скором времени расскажу, программа-клиент не занимается, например, проверкой цифровой подписи на сертификате Google (подпись, подтверждающая сертификат). Если этой подписи доверяют, то сертификату, содержащему ее, также следует доверять. Тем не менее клиентская программа продолжает загрузку, а затем отображение домашней страницы Google. В следующем разделе мы рассмотрим это более подробно.

Скрытые механизмы безопасности в программе-клиенте

Начнем с артефакта безопасности, который мы видим в примере с клиентом — цифрового сертификата — и рассмотрим, как другие артефакты безопасности с ним связаны. Превалирующим стандартом структуры цифрового сертификата является X509, а сертификат производственного уровня выдаются центром сертификации (CA, certificate authority), таким как Verisign.

Цифровой сертификат содержит различную информацию (например, даты начала и истечения срока действия, а также доменное имя владельца), включая идентификационные данные издателя и цифровую подпись, которая представляет собой зашифрованное криптографическое хеш-значение. Сертификат также имеет незашифрованное хеш-значение, которое служит его идентифицирующим отпечаток (fingerprint).

Хеш-значение получается в результате сопоставления произвольного количества битов с хеш-суммой (digest) фиксированной длины. Что представляют собой биты (бухгалтерский отчет, роман или, может быть, фильм) не имеет значения. Например, хеш-алгоритм Message Digest версии 5 (MD5) отображает входные биты любой длины в 128-битное хеш-значение, тогда как алгоритм SHA1 (Secure Hash Algorithm версии 1) отображает входные биты в 160-битное значение. Различные входные биты приводят к разным — действительно, статистически уникальным — значениям хеш-функции. В следующей статье мы рассмотрим это более подробно и сосредоточимся на том, что именно делает хеш-функцию криптографической.

Цифровые сертификаты различаются по типу (например, корневой/root, промежуточный/intermediate и конечный/end-entity сертификаты) и образуют иерархию, отражающую эти типы. Как следует из названия, корневой сертификат находится на вершине иерархии, а сертификаты под ним наследуют любое доверие, которое имеет корневой сертификат. Библиотеки OpenSSL и большинство современных языков программирования имеют тип X509, как и функции, которые работают с такими сертификатами. Сертификат от Google имеет формат X509, и клиент проверяет, является ли этот сертификат X509_V_OK.

Сертификаты X509 основаны на инфраструктуре открытых ключей (PKI, public-key infrastructure), которая включает в себя алгоритмы (в основном доминирует RSA) для создания пар ключей: публичный ключ и его парный приватный ключ. Публичный ключ — это идентификационные данные (identity): публичный ключ Amazon идентифицирует его, а мой публичный ключ идентифицирует меня. Приватный ключ должен храниться его владельцем в секрете.

Ключи в паре имеют несколько стандартных применений. Публичный ключ можно использовать для шифрования сообщения, а приватный ключ из той же пары затем можно использовать для его расшифровки. Приватный ключ также можно использовать для подписи документа или другого электронного артефакта (например, программы или электронного письма), а затем публичный ключ из пары можно использовать для проверки этой подписи. Рассмотрим два примера.

В первом примере Алиса открывает свой публичный ключ всему миру, включая Боба. Затем Боб шифрует сообщение с помощью публичного ключа Алисы, отправляя ей зашифрованное сообщение. Сообщение, зашифрованное публичным ключом Алисы, расшифровывается ее приватным ключом, который (по идее) есть только у нее, вот так:

	           +------------------+ encrypted msg  +-------------------+
Bob's msg--->|Alice's public key|--------------->|Alice's private key|--->
Bob's msg
				     +------------------+                +-------------------+

Расшифровка сообщения без приватного ключа Алисы в принципе возможна, но на практике нереальна, учитывая надежность криптографической системы парных ключей, такой ​​как RSA.

Теперь в качестве второго примера рассмотрим подписание документа для подтверждения его подлинности. Алгоритм подписи использует приватный ключ из пары для обработки криптографического хеша подписываемого документа:

										+-------------------+
Hash of document--->|Alice's private key|--->Alice's digital signature of the document
										+-------------------+

Предположим, что Алиса подписывает цифровой подписью контракт, отправленный Бобу. Затем Боб может использовать публичный ключ Алисы для проверки подписи:

									             							 +------------------+
Alice's digital signature of the document--->|Alice's public key|--->verified or not
                                             +------------------+

Невозможно подделать подпись Алисы без ее приватного ключа: следовательно, в интересах Алисы сохранить ее приватный ключ в тайне.

Ни один из этих элементов безопасности, за исключением цифровых сертификатов, не является явным в нашей программе-клиенте. В следующей статье подробно описаны примеры, в которых используются утилиты OpenSSL и библиотечные функции.

OpenSSL из командной строки

А пока давайте взглянем на инструменты командной строки OpenSSL: в частности, утилиту для проверки сертификатов с сервера во время  TLS-рукопожатия. Вызов утилит OpenSSL начинается с команды openssl а затем добавляется комбинация аргументов и флагов для указания желаемой операции.

Рассмотрим эту команду:

openssl list-cipher-algorithms

Результатом является список связанных алгоритмов, составляющих набор шифров (cipher suite). Ниже приведено начало списка с комментариями для разъяснения аббревиатур:

AES-128-CBC ## Advanced Encryption Standard, Cipher Block Chaining
AES-128-CBC-HMAC-SHA1 ## Hash-based Message Authentication Code с хешами SHA1 
AES-128-CBC-HMAC-SHA256 ## тоже самое, но с SHA256 вместо SHA1
...

Следующая команда, используя аргумент s_client, открывает безопасное соединение с www.google.com и выводит на экран информацию об этом соединении:

openssl s_client -connect www.google.com:443 -showcerts

Номер порта 443 является стандартным, который используется серверами для приема соединений HTTPS, а не HTTP. (Для HTTP стандартный порт — 80). Сетевой адрес www.google.com:443 также встречается в программе-клиенте. Если попытка подключения успешна, отображаются три цифровых сертификата от Google вместе с информацией о безопасном сеансе, используемом наборе шифров и связанных элементах. Например, вот фрагмент начала вывода, который объявляет о предстоящей цепочке сертификатов. Кодировка сертификатов — base64:

Certificate chain
 0 s:/C=US/ST=California/L=Mountain View/O=Google LLC/CN=www.google.com
 i:/C=US/O=Google Trust Services/CN=Google Internet Authority G3
-----BEGIN CERTIFICATE-----
MIIEijCCA3KgAwIBAgIQdCea9tmy/T6rK/dDD1isujANBgkqhkiG9w0BAQsFADBU
MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMSUw
...

Такие крупные сайты как Google, как правило, отправляют несколько сертификатов для аутентификации.

Выходные данные заканчиваются сводной информацией о сессии TLS, включая сведения о наборе шифров:

SSL-Session:
    Protocol : TLSv1.2
    Cipher : ECDHE-RSA-AES128-GCM-SHA256
    Session-ID: A2BBF0E4991E6BBBC318774EEE37CFCB23095CC7640FFC752448D07C7F438573
...

В программе-клиенте используется протокол TLS1.2, а Session-ID однозначно идентифицирует соединение между утилитой openssl и сервером Google. Запись Cipher может быть проанализирована следующим образом:

  • ECDHE (протокол Диффи-Хеллмана на эллиптических кривых) — эффективный и действенный алгоритм для управления TLS-рукопожатием. В частности, ECDHE решает проблему распределения ключей, гарантируя, что обе стороны соединения (например, программа-клиент и веб-сервер Google) используют один и тот же ключ шифрования/дешифрования, известный как ключ сессии (session key). Следующая часть серии детальнее раскроет эту тему.

  • RSA (Rivest Shamir Adleman) — доминирующая криптосистема с публичным ключом, названная в честь трех ученых, впервые описавших эту систему в конце 1970-х годов. Пары ключей генерируются с помощью алгоритма RSA.

  • AES128 (Advanced Encryption Standard) — это блочный шифр, который шифрует и расшифровывает блоки битов. (Альтернативой является потоковый шифр, который шифрует и расшифровывает биты по одному.) Шифр ​​является симметричным в том смысле, что для шифрования и дешифрования используется один и тот же ключ, что в первую очередь поднимает проблему распределения ключей. AES поддерживает размеры ключей 128 (используется здесь), 192 и 256 бит: чем больше ключ, тем лучше защита.

Размеры ключей для симметричных криптосистем, таких как AES, как правило, меньше, чем для асимметричных систем (на основе пары ключей), таких как RSA. Например, 1024-битный ключ RSA относительно мал, тогда как 256-битный ключ в настоящее время является самым большим для AES.

  • GCM (режим счетчика Галуа) обрабатывает повторное применение шифра (в данном случае AES128) во время защищенного общения. Блоки AES128 имеют размер всего 128 бит, и безопасный обмен данными, скорее всего, будет состоять из нескольких блоков AES128 от одной стороны к другой. GCM эффективен и обычно сочетается с AES128.

  • SHA256 (Secure Hash Algorithm с 256 бит) — это популярный криптографический алгоритм хеширования. Создаваемые хеш-значения имеют размер 256 бит, хотя с помощью SHA возможны и большие значения.

Наборы шифров находятся в постоянном развитии. Не так давно, например, Google использовала потоковый шифр RC4 (Ron's Cipher версии 4 в честь Рона Ривеста из RSA). В RC4 теперь онаружены уязвимости, которые предположительно частично объясняют переход Google на AES128.

Подытожим

Это знакомство с OpenSSL посредством разбора безопасного веб-клиента C и различных примеров для командной строки выдвинуло на передний план несколько тем, нуждающихся в дополнительных пояснениях. Следующая статья посвящена деталям, начиная с криптографических хешей и заканчивая более полным обсуждением того, как цифровые сертификаты решают проблему распространения ключей.


Приглашаем всех желающих на открытое занятие «Встраиваем интерпретатор в приложение на C». На этом открытом уроке мы рассмотрим встраивание интерпретатора и виртуальной машины языка программирования высокого уровня в программу на C на примере скриптового языка Lua. Регистрируйтесь по ссылке.

Tags:
Hubs:
Total votes 10: ↑6 and ↓4+2
Comments5

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS