Безпечне криптопрограммирование. Частина 2, заключна

Продовжуємо переклад набору правил безпечного криптопрограммирования від Жана-Філіпа Омассона…

Запобігайте втручання компілятора в частині коду, критичним чином впливають на безпеку
Проблема
Деякі компілятори оптимізують операції, які вони вважають марними.

Наприклад, компілятор MS Visual C++ порахував зайвим оператор |memset| в наступному фрагменті коду реалізації анонімної мережі Tor:

int
crypto_pk_private_sign_digest(...)
{
char digest[DIGEST_LEN];
(...)
memset(digest, 0, sizeof(digest));
return r;
}

Однак роль цього оператора |memset полягає в очищенні буфера |digest| від конфіденційних даних, таким чином, щоб при будь-яких подальших считываниях даних з неинициализированного стека неможливо було отримати конфіденційну інформацію.

Деякі компілятори вважають, що вони можуть видаляти перевірки умов, вважаючи код помилковим де-небудь в програмі. Наприклад, виявивши наступний фрагмент коду

call_fn(ptr); // завжди разыменовывает ptr.

// багато-багато рядків

if (ptr == NULL) { error("ptr must not be NULL"); }

деякі компілятори вирішать, що умова |ptr == NULL| завжди має приймати значення БРЕХНЯ, оскільки в іншому випадку було б некоректним розв'язати його функції |call_fn()|.

Рішення
Проаналізуйте скомпільований код і переконайтеся, що всі інструкції присутні в ньому. (Це неможливо для додатків стандартного розміру, але це слід зробити для фрагмента коду, критичного з точки зору безпеки).

Розберіться які оптимізації може робити ваш компілятор і акуратно оцініть ефект кожної з них з точки зору принципів безпечного програмування. Зокрема, будьте акуратні з оптимизациями, які прибирають фрагменти коду або розгалуження, а також фрагменти коду, які запобігають помилкам, які «не можуть з'явитися», якщо частина програми коректна.

Коли це можливо, подумайте про відключення тієї оптимізації при компіляції, яка усуває або послаблює перевірку умов, що впливають на безпеку.

Щоб запобігти видалення інструкцій допомогою оптимізації, функція може бути перевизначено з використанням ключового слова volatile. Це, наприклад, використовується в libottery при перевизначенні |memset|:

void * (*volatile memset_volatile)(void *, int, size_t) = memset;

У C11 введений виклик memset_s, для якого заборонено вилучення при оптимізації.

#define __STDC_WANT_LIB_EXT1__ 1
#include < string.h>
...
memset_s(secret, sizeof(secret), 0, sizeof(secret));


Не допускайте змішування безпечних і небезпечних програмних інтерфейсів
Проблема
Багато середовища програмування надають різні реалізації одних і тих же програмних інтерфейсів – їх функціонал зовні однаковий, але при цьому властивості безпеки кардинально відрізняються.

Ця проблема характерна для датчиків випадкових чисел: OpenSSL є |RAND_bytes ()| |RAND_pseudo_bytes()|, C-бібліотеках BSD є |RAND_bytes ()| |RAND_pseudo_bytes()|, в Java – |SecureRandom| |Random|

Ще одним прикладом може бути той факт, що в системах, які надають не залежать від часу функції порівняння байтових слів, в той же час присутні варіанти, які можуть давати витік за часом.

Погані рішення
Іноді функція є безпечною на одних платформах і небезпечною на інших. У цих випадках програмісти використовують цю функцію, вважаючи, що їх код буде виконуватися на платформах, де він безпечний. Це поганий підхід, оскільки код може бути портований на інші платформи і стати небезпечним – ніхто цього і не помітить.

У системах, які дозволяють перевизначення платформозавісімих функцій, деякі програмісти зумовлюють небезпечні функції безпечними і пишуть програми, використовуючи програмний інтерфейс, який в звичайній ситуації є небезпечним. Це досить спірний підхід, оскільки призводить до того, що програміст пише код, який виглядає небезпечним. Більш того, якщо перевизначено метод не спрацює коли-небудь, програма стане небезпечною, і це не можна буде визначити. І нарешті, це призведе до того, що фрагменти коду таких програм будуть небезпечні, якщо їх скопіювати в інші проекти.

Рішення
По можливості не використовуйте небезпечні варіанти безпечних функцій. Наприклад, ПДСЧ на базі стійкого потокового шифру з випадковим початковим заповненням досить швидкий для більшості додатків. Незалежна від типу даних заміна memcmp також досить швидка, щоб бути використаною для всіх операцій порівняння областей пам'яті.

Якщо ви не можете видалити небезпечні функції, перевизначити їх таким чином, щоб видавалася помилка на етапі компіляції, або використовуйте утиліти для статичного аналізу коду, щоб виявити та попередити про використання небезпечних функцій. Якщо ви можете перевизначити небезпечну функцію її безпечним варіантом, то для більшої безпеки ніколи не викликайте небезпечний API і переконайтеся, що ви можете виявляти факт його використання.

Якщо необхідно залишити обидва варіанти (безпечний та небезпечний) упевніться, що імена функцій різні настільки, що буде важко випадково використовувати небезпечний варіант. Наприклад, якщо у вас є безпечний та небезпечний ПДСЧ, не називайте небезпечний варіант «Random», «FastRandom», «MersenneTwister» або «LCGRand» – замість цього назвіть його, наприклад, «InsecureRandom». Розробляйте свої програмні інтерфейси таким чином, щоб використання небезпечних функцій завжди трохи лякало.

Якщо ваша платформа надає небезпечний варіант функції без імені, яке говорить про її небезпечність і ви не можете видалити цю функцію, використовуйте обгортку системного виклику з безпечним ім'ям, потім шляхом статичного аналізу коду виявити всі використання небезпечного імені.

Якщо функція безпечна на одних платформах і небезпечна на інших, не використовуйте функцію безпосередньо: замість цього визначте і використовуйте безпечну обгортку.

Уникайте змішування рівнів безпеки і абстракції криптографічних примітивів на одному рівні API
Проблема
Коли не ясно, якого аналізу вимагають різні частини програмного інтерфейсу, програміст може досить легко зробити помилку в те, які функціональні можливості вони можуть безпечно використовувати.

Розглянемо наступний приклад (вигаданий, але схожий на ті, які зустрічаються в реальному житті) програмного інтерфейсу RSA:

enum rsa_padding_t { no_padding, pkcs1v15_padding, oaep_sha1_padding, pss_padding };
int do_rsa(struct rsa_key *key, int encrypt, public int, enum rsa_padding_t padding_type, uint8_t *input, uint8_t *output);

Припустимо, що параметр «key» містить компоненти реквізитів, тоді функція може бути викликана 16-ма способами, багато з яких безглузді, а деякі небезпечні.
шифрування/розшифрування симетричне/асиметричне
тип паддінга
зауваження
0 0 none Розшифрування без паддінга. Можливість підробки.
0 0 pkcs1v15 Розшифрування PKCS1 v1.5. Можливо, піддається атаці Блейнбахера.
0 0 oaep Розшифрування OAEP. Хороший варіант.
0 0 pss Розшифрування PSS. Досить дивний варіант, можливо, призводить до ненавмисних помилок
0 1 none Підпис без паддінга. Можливість підробки.
0 1 pkcs1v15 Підпис PKCS1 v1.5. Підходить для деяких додатків, але краще використовувати підпис PSS.
0 1 oaep Підпис OAEP. Підходить для деяких додатків але краще використовувати підпис PSS.
0 1 pss Підпис PSS. Дуже хороший варіант.
... ... ... залишилися варіанти (шифрування і перевірка підпису).


Зазначимо, що тільки 4 з 16-ти можливих способів виклику цієї функції безпечні, ще 6 небезпечні, а решта 6 в деяких випадках можуть викликати проблеми при застосуванні. Такий API підходить тільки для тих розробників, хто розуміє наслідки застосування різних способів доповнення в системі RSA.

Тепер уявіть, що ми додамо програмні інтерфейси для блочного шифрування в різних режимах, генерації ключів, різноманітних кодів автентифікації повідомлень і підписів. Будь-який програміст, який спробує розробити коректну функцію, що реалізує аутентифікацію і шифрування даних з використанням таких програмних інтерфейсів, буде мати величезне число варіантів вибору, при цьому число безпечних варіантів, очевидним чином, скоротиться.

Рішення
  • Надавайте високорівневі програмні інтерфейси. Наприклад, надайте функції, що реалізовують шифрування і аутентифікацію даних, які використовують тільки стійкі алгоритми і при цьому безпечним чином. Коли ви пишете функцію, яка надає різні комбінації симетричних та асиметричних алгоритмів та їх режимів роботи, переконайтеся, що ця функція не дозволяє використовувати небезпечні алгоритми та їх небезпечні комбінації.
  • Коли це можливо, уникайте низькорівневих API. Більшості користувачів немає необхідності використовувати RSA без доповнення, використовувати блочний шифр режимі ECB або використовувати підпис DSA з обраним користувачем випадковим значенням. Ці функції можуть бути використані як будівельні блоки для того, щоб реалізувати що-небудь стійке – наприклад, зробити OAEP-паддінґ до виклику RSA без доповнення, використовувати шифрування в режимі ECB для блоків 1,2,3,..., щоб реалізувати режим лічильника або використовувати випадкову або непередбачувану байтовую послідовність для випадкового значення DSA, але практика показує, що вони частіше будуть використовуватися неправильно, ніж правильно.

    Деякі інші примітиви необхідні для реалізації певних протоколів, але швидше за все не будуть придатними для реалізації нових протоколів. Наприклад, ви не можете реалізувати в даний час сумісний із браузером TLS без CBC, PKCS1 v1.5 і RC4, але будь-який з даних примітивів не є хорошим варіантом.

    Якщо ви надаєте криптографічний модуль для використання недосвідченими програмістами, буде краще уникати таких функцій повністю і вибирати (API) тільки функції, які реалізують добре описані високорівневі безпечні операції.
  • Якщо ж ви все-таки повинні надавати інтерфейс і досвідченим, і недосвідченим користувачам, чітко розділіть високорівневий і низькорівневі програмні інтерфейси. Функція «безпечного шифрування» не повинна бути тією ж самою функцією, що і «некоректне шифрування» з дещо зміненими аргументами. В мовах, які поділяють функції і типи в пакети і заголовки, безпечні й небезпечні криптофункции не повинні міститися в одних і тих же пакетах і заголовках. У мовах з підтипами, повинні бути окремі типи безпечних криптореализаций.


Використовуйте беззнакові типи для представлення двійкових даних
Проблема
У деяких C-подібних мовах знакові і беззнакові цілочисельні типи є різними. Зокрема, в C питання про те, чи є тип |char| знаковим залежить від реалізації. Це може привести до появи проблемного коду – такого, наприклад, як наведений далі:

int decrypt_data(const char *key, char *bytes, size_t len);

void fn(...) {
//...
char *name;
char buf[257];
decrypt_data(key, buf, 257);

int name_len = buf[0];
name = malloc(name_len + 1);
memcpy(name, buf+1, name_len);
name[name_len] = 0;
//...
}

Якщо |char| беззнаковий, то даний код веде себе так, як ми від нього очікуємо. Але якщо |char| знаковий, |buf[0]| може приймати негативні значення, приводячи до дуже великих значень аргументів функцій |malloc| |memcpy| і можливості пошкодження купи, якщо ми намагаємося встановити значення останнього знака в 0. Ситуація може бути навіть гірше, якщо |buf[0]| дорівнює 255, тоді name_len буде рівним -1. Таким чином, ми виділимо в пам'яті буфер розміру 0 байтів, а потім зробимо копіювання |(size_t)-1 memcpy| в даний буфер, що призведе до засмічення купи.

Рішення
В мовах, які розрізняють знакові і беззнакові байтові типи, реалізації повинні використовувати беззнакові типи для подання байтових рядків у своїх API.

Очищайте пам'ять від секретних даних
Проблема
У більшості операційних систем пам'ять, використовувана одним процесом, може бути використана іншим процесом без попереднього очищення, тому що перший процес зупинений або повернув пам'ять системі. Якщо пам'ять містить секретні ключі, вони будуть доступні іншого процесу, що збільшує шанс їх компрометації. У багатокористувацьких системах це дає можливість визначати ключі інших користувачів системи. Навіть усередині однієї системи подібна ситуація може призвести до того, що раніше порівняно безпечні уразливості можуть призводити до витоку секретних даних.

Рішення
Очищайте всі змінні, що містять секретні дані, до того моменту як ви про них забудете і станете користуватися. Використовуючи функцію |mmap()| пам'ятайте, що запуск |munmap()| моментально звільняє пам'ять і ви втрачаєте над нею контроль.

Для очищення пам'яті або знищення об'єктів, які йдуть з вашого поля зору, використовуйте платформозависимые функції очищення пам'яті, де це можливо – такі як |SecureZeroMemory()| для win32 або |OPENSSL_cleanse()| OpenSSL.

Більш-менш універсальне рішення для C може бути таким:

void burn( void *v, size_t n )
{
volatile unsigned char *p = ( volatile unsigned char * )v;
while( n-- ) *p++ = 0;
}


Використовуйте «сильну» випадковість

Проблема
Багатьом криптографічних систем потрібні джерела випадковості, при цьому такі системи можуть ставати небезпечними навіть у випадку невеликих відхилень від випадковості в таких джерелах. Наприклад, витік навіть одного випадкового числа в DSA призведе до вкрай швидкого визначення секретного ключа. Недостатню випадковість буває досить важко визначити: помилка генератора випадкових чисел Debian в OpenSSL залишалася непоміченою протягом двох років, привівши до компрометації великої кількості ключів. Вимоги до випадкових чисел для криптографічних додатків дуже жорсткі: багато генератори псевдовипадкових чисел не відповідають їм.

Погані рішення
Для криптографічних додатків

  • Не покладайтеся на передбачувані джерела випадковості, такі як мітки часу, ідентифікатори, температурні датчики і т. д.
  • не покладайтеся на функції вироблення псевдовипадкових чисел загального користування, такі як |rand()|,|srand()|,|random()| бібліотеки |stdlib| або |random| мови Python
  • Не використовуйте генератор Вихор Мерсенна (Mersenne Twister)
  • Не використовуйте ресурси на зразок www.random.org (випадкові дані можуть стати відомі третім особам або бути також використані ними).
  • Не використовуйте свій власний генератор випадкових чисел, навіть якщо він заснований на стійкому криптопримитиве (якщо тільки ви точно не знаєте, що робите).
  • Не використовуйте одні і ті самі випадкові біти в різних місцях програми, для «економного» витрачання.
  • Не робіть висновок про те, що генератор стійкий тільки по тому, що він проходить тести Diehard або NIST.
  • Не робіть висновок про те, що криптографічно стійкого генератор обов'язково захищає від читання вперед і читання назад.
  • Ніколи не використовуйте «випадковість» в чистому вигляді як випадкових даних (аналогові джерела випадковості часто мають відхилення, тому N бітів, отриманих з такого джерела, має менше N бітів випадковості).


Рішення
Мінімізуйте використання випадковості допомогою вибору примітивів та їх дизайну (наприклад, Ed25519 дозволяє одержувати криві для електронного підпису детермінованим чином). Для генерації випадкових чисел використовуйте джерела, що надаються операційними системами і гарантовано задовольняють криптографічним вимогам, такі як |/dev/random|. На платформах з обмеженими ресурсами розгляньте можливість використання аналогових джерел випадкового шуму і гарною процедури замішування.

Обов'язково перевіряйте значення, вироблені вашим датчиком, щоб бути впевненими, що отримуються байти такі, якими вони повинні бути, і що вони були записані належним чином.

Дотримуйтесь рекомендацій Наді Хенингер та ін. в розділі 7 їх статті.

На процесорах Intel з архітектурою Ivy Bridge (і наступних поколінь), вбудований генератор гарантує високу ентропію і швидкість роботи.

В Unix системах зазвичай використовуються |/dev/random| або |dev/urandom|. Проте перший з них має властивість блокування, тобто він не повертає значень у випадку, якщо вважає, що накопичено недостатньо випадковості. Це властивість обмежує зручність
його використання, і тому |/dev/urandom| використовується частіше. Використовувати |/dev/urandom| досить просто:

#include < sys/types.h>
#include < sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include < stdio.h>

int main() {
int randint;
int bytes_read;
int fd = open("/dev/urandom", O_RDONLY);
if (fd != -1) {
bytes_read = read(fd, &randint, sizeof(randint));
if (bytes_read != sizeof(randint)) {
fprintf(stderr, "read() failed (%d read bytes)\n", bytes_read);
return -1;
}
}
else {
fprintf(stderr, "open() failed\n");
return -2;
}
printf("%08x\n", randint); /* assumes sizeof(int) <= 4 */
close(fd);
return 0;
}

Однак цієї простої програми може бути недостатньо для безпечної вироблення випадковості: більш безпечним буде виконання додаткових перевірок на помилки як у функції |getentropy_urandom| LibreSSL

static int
getentropy_urandom(void *buf, size_t len)
{
struct stat st;
size_t i;
int fd, cnt, flags;
int save_errno = errno;

start:

flags = O_RDONLY;
#ifdef O_NOFOLLOW
flags |= O_NOFOLLOW;
#endif
#ifdef O_CLOEXEC
flags |= O_CLOEXEC;
#endif
fd = open("/dev/urandom", flags, 0);
if (fd == -1) {
if (errno == EINTR)
goto start;
goto nodevrandom;
}
#ifndef O_CLOEXEC
fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC);
#endif

/* Lightly verify that the node device looks sane */
if (fstat(fd, &st) == -1 || !S_ISCHR(st.st_mode)) {
close(fd);
goto nodevrandom;
}
if (ioctl(fd, RNDGETENTCNT, &cnt) == -1) {
close(fd);
goto nodevrandom;
}
for (i = 0; i < len; ) {
size_t wanted = len - i;
ssize_t ret = read(fd, (char *)buf + i wanted);

if (ret == -1) {
if (errno == EAGAIN || errno == EINTR)
continue;
close(fd);
goto nodevrandom;
}
i += ret;
}
close(fd);
if (gotdata(buf, len) == 0) {
errno = save_errno;
return 0; /* satisfied */
}
nodevrandom:
errno = EIO;
return -1;
}

У Windows-системах |CryptGenRandom| з Win32 API виробляє псевдовипадкові біти придатні для використання в криптографії. Microsoft пропонує наступний варіант використання:

#include <stddef.h>
#include <stdint.h>
#include <windows.h>

#pragma comment(lib, "advapi32.lib")

int randombytes(unsigned char *out, size_t outlen)
{
static HCRYPTPROV handle = 0; /* only freed when program ends */
if(!handle) {
if(!CryptAcquireContext(&handle, 0, 0, PROV_RSA_FULL,
CRYPT_VERIFYCONTEXT | CRYPT_SILENT)) {
return -1;
}
}
while(outlen > 0) {
const DWORD len = outlen > 1048576UL ? 1048576UL : outlen;
if(!CryptGenRandom(handle, len, out)) {
return -2;
}
out += len;
outlen -= len;
}
return 0;
}

Якщо орієнтуватися на використання у Windows XP або більш пізніх версіях, вказаний вище код на CryptoAPI може бути замінений на |RtlGenRandom|

#include <stdint.h>
#include < stdio.h>

#include <Windows.h>

#define RtlGenRandom SystemFunction036
#if defined(__cplusplus)
extern "С"
#endif
BOOLEAN NTAPI RtlGenRandom(PVOID RandomBuffer, ULONG RandomBufferLength);

#pragma comment(lib, "advapi32.lib")

int main()
{
uint8_t buffer[32] = { 0 };

if (FALSE == RtlGenRandom(buffer, sizeof buffer))
return -1;

for (size_t i = 0; i < sizeof buffer; ++i)
printf("%02X ", buffer[i]);
printf("\n");

return 0;
}


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.