Intel Software Guard Extensions, навчальний посібник. Частина 5, розробка анклаву

В п'ятій частині серії навчальних матеріалів, присвячених розширень Intel Software Guard Extensions (Intel SGX), ми завершимо розробку анклаву для програми Tutorial Password Manager. четвертої частини цієї серії ми створили DLL-бібліотеку, яка використовується в якості рівня інтерфейсу між функціями мосту анклаву і ядром програми C++/CLI, а також визначили інтерфейс анклаву. Ці компоненти готові, тому тепер можна перейти до самого анклаву.

Разом з цією частиною серії надається вихідний код: готове додаток з анклавом. У цій версії жорстко задана гілка коду з використанням Intel SGX.

Компоненти анклаву
Щоб визначити, які компоненти слід реалізувати всередині анклаву, повернемося до схеми класів ядра програми, яку ми вперше описали третій частині — вона показана на рис. 1. Як і раніше, об'єкти, що знаходяться в анклаві, зафарбовані зеленим, а недоверенные компоненти — синім.


Малюнок 1. Схема класів у Tutorial Password Manager з Intel Software Guard Extensions.

За цією схемою можна визначити чотири класи, які слід перенести в анклав:
  • Vault
  • AccountRecord
  • Crypto
  • DRNG
Втім, перед початком роботи потрібно прийняти рішення про влаштування програми. Наше додаток має працювати в системах як з підтримкою Intel SGX, так і без Intel SGX. Це означає, що неможливо просто перетворити існуючі класи так, щоб вони працювали всередині анклаву. Потрібно створити за дві версії кожного класу: одну для використання в анклавах, іншу для використання в ненадійна пам'яті. Питання в тому, як реалізувати цю подвійну підтримку?

Варіант 1. Умовна компіляція
Перший варіант — реалізувати функціональність для анклаву і для ненадійна пам'яті в одному і тому ж модулі коду та використовувати визначення попередньої обробки та інструкції #ifdef для компіляції потрібного коду в залежності від контексту. Перевага такого підходу полягає в тому, що для кожного класу потрібен всього один файл вихідного коду, немає необхідності застосовувати кожну зміну в двох місцях. Недолік полягає в тому, що такий код менш зрозумілий, особливо при наявності кількох або значних змін між версіями. Крім того, ускладнюється структура проекту. У двох проектах Visual Studio*, Enclave PasswordManagerCore, будуть загальні файли вихідного коду, і кожному потрібно поставити символ попередньої обробки, щоб забезпечити компіляцію правильної версії вихідного коду.

Варіант 2. Окремі класи
Другий варіант полягає в дублюванні кожного файлу вихідного коду, який повинен бути розміщений в анклаві. Переваги цього підходу полягають у тому, що в анклаві буде власна копія файлів вихідного коду, які можна буде змінювати безпосередньо, завдяки чому спрощується структура проекту і перегляд коду. Але є і недолік: якщо потрібно внести зміни в класи, ці зміни доведеться здійснювати в двох місцях, навіть якщо зміни однакові і для версії в анклаві, і для версії в ненадійна пам'яті.

Варіант 3. Спадкування
Третій варіант передбачає використання успадкування класів, доступного в C++. Функції, загальні для обох версій класу, реалізуються в базовому класі, а похідні класи будуть реалізовувати методи, що відносяться до кожної з гілок коду. Важлива перевага цього підходу полягає в тому, що це дуже природне і елегантне рішення проблеми: ми використовуємо можливість мови, призначену саме для таких ситуацій. Недоліком є підвищена складність структури проекту і самого коду.

Тут немає ніяких жорстких правил, будь-яке прийняте рішення не обов'язково використовувати завжди і скрізь. Загальна рекомендація така: варіант 1 найкраще підходить для модулів, де змін небагато або де їх можна легко виокремити; варіанти 2 і 3 краще підходять для випадків, коли зміни досить істотні або отриманий вихідний код дуже складний з точки зору читання і обслуговування. Якщо ж звести вибір до рівня стилю і уподобань, то кожний з перерахованих підходів цілком працездатний.

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

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

Клас Vault
Клас Vault — наш інтерфейс до операцій з сховищем паролів. Всі функції моста діють за допомогою одного або декількох методів у класі Vault. Його оголошення з Vault.h показано нижче.

class PASSWORDMANAGERCORE_API Vault
{
Crypto crypto;
char m_pw_salt[8];
char db_key_nonce[12];
char db_key_tag[16];
char db_key_enc[16];
char db_key_obs[16];
char db_key_xor[16];
UINT16 db_version;
UINT32 db_size; // Use get_db_size() to fetch this value so it gets updated as needed
char db_data_nonce[12];
char db_data_tag[16];
char *db_data;
UINT32 state;
// Cache the number of defined accounts so that the GUI doesn't have to fetch
// "empty" account info unnecessarily.
UINT32 naccounts;

AccountRecord accounts[MAX_ACCOUNTS];
void clear();
void clear_account_info();
void update_db_size();

void get_db_key(char key[16]);
void set_db_key(const char key[16]);

public:
Vault();
~Vault();

int initialize();
int initialize(const unsigned char *header, UINT16 size);
int load_vault(const unsigned char *edata);

int get_header(unsigned char *header, UINT16 *size);
int get_vault(unsigned char *edata, UINT32 *size);

UINT32 get_db_size();

void lock();
int unlock(const char *password);

int set_master_password(const char *password);
int change_master_password(const char *oldpass, const char *newpass);

int accounts_get_count(UINT32 *count);
int accounts_get_info_sizes(UINT32 idx, UINT16 *mbname_sz, UINT16 *mblogin_sz, UINT16 *mburl_sz);
int accounts_get_info(UINT32 idx, char *mbname, UINT16 mbname_sz, char *mblogin, UINT16 mblogin_sz,
char *mburl, UINT16 mburl_sz);

int accounts_get_password_size(UINT32 idx, UINT16 *mbpass_sz);
int accounts_get_password(UINT32 idx, char *mbpass, UINT16 mbpass_sz);

int accounts_set_info(UINT32 idx, const char *mbname, UINT16 mbname_len, const char *mblogin, UINT16 mblogin_len,
const char *mburl, UINT16 mburl_len);
int accounts_set_password(UINT32 idx, const char *mbpass, UINT16 mbpass_len);

int accounts_generate_password(UINT16 length, UINT16 pwflags, char *cpass);

int is_valid() { return _VST_IS_VALID(state); }
int is_locked() { return ((state&_VST_LOCKED) == _VST_LOCKED) ? 1 : 0; }
};

Оголошення версії цього класу для анклаву, яку ми для ясності назвемо E_Vault, буде ідентичною, за винятком одного важливого зміни.

У ненадійна гілки коду об'єкт Vault повинен зберегти розшифрований ключ бази даних в пам'яті. Кожен раз, коли ми виробляємо будь-яка зміна сховища паролів, потрібно зашифровувати оновлені дані сховища і записувати їх на диск. Це означає, що ключ повинен бути у нашому розпорядженні. Перед нами чотири шляхи:
  1. Пропонувати користувачеві вводити головний пароль при кожному зміні, щоб формувати ключ бази даних на вимогу.
  2. Кешувати головний пароль користувача, щоб формувати ключ бази даних на вимогу без втручання користувача.
  3. Зашифрувати, закодувати або приховати ключ бази даних в пам'яті.
  4. Зберігати ключ в незашифрованому вигляді.
Жодне з цих рішень не є задовільним. Відсутність більш зручних рішень знову підкреслює затребуваність таких технологій як Intel SGX. Перше рішення можна — із застереженнями — вважати більш безпечним, але ні один користувач не захоче користуватися додатком, яке буде вести себе докладним чином. Друге рішення здійсненно за допомогою класу SecureString.NET*, але воно як і раніше буде вразливим до отримання ключа за допомогою відладчика. Крім того, для формування ключа потрібні певні обчислювальні ресурси, з-за чого продуктивність може знизитися до неприйнятного для користувачів рівня. Третій варіант, по суті, настільки ж небезпечний, як і другий, але без зниження продуктивності. Четвертий варіант — найгірший з усіх.

У нашому додатку Tutorial Password Manager використовується третій варіант: ключ бази даних кодується з допомогою XOR з 128-розрядним значенням, яке формується довільним чином при відкритті файлу сховища і зберігається в пам'яті тільки в такій формі після обробки з допомогою XOR. Це, по суті, схема з шифруванням одноразовим ключем. Ключ доступний для всіх, кому вдасться запустити відладчик, але час, протягом якого ключ бази даних в пам'яті в незашифрованому вигляді, обмежена.

void Vault::set_db_key(const char db_key[16])
{
UINT i, j;
for (i = 0; i < 4; i++)
for (j = 0; j < 4; j++) db_key_obs[4 * i + j] = db_key[4 * i + j] ^ db_key_xor[4 * i + j];
}

void Vault::get_db_key(char db_key[16])
{
UINT i, j;
for (i = 0; i < 4; i++)
for (j = 0; j < 4; j++) db_key[4 * i + j] = db_key_obs[4 * i + j] ^ db_key_xor[4 * i + j];
}

Цей підхід, очевидно, відноситься до моделі «безпека через невідомості», але оскільки ми публікуємо вихідний код, ні про яку особливу невідомості говорити не доводиться. Можна було б вибрати більш вдалий алгоритм або зробити більше зусиль, щоб приховати ключ бази даних і одноразовий ключ (в тому числі і способи їх зберігання в пам'яті), але обраний нами метод все одно залишиться вразливим для перегляду за допомогою відладчика, а алгоритм приховування все одно буде опубліковано і є доступним для всіх.

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

char db_key_obs[16];
char db_key_xor[16];

void get_db_key(char key[16]);
void set_db_key(const char key[16]);

Можна замінити їх все одним членом класу: масив char для зберігання ключа бази даних.

char db_key[16];

Клас AccountInfo
Дані облікового запису зберігаються в масиві фіксованого розміру об'єктів AccountInfo в якості члена об'єкта Vault. Оголошення AccountInfo також знаходиться в Vault.h, воно показано нижче:

class PASSWORDMANAGERCORE_API AccountRecord
{
char nonce[12];
char tag[16];
// Store these in their multibyte form. There's no sense in translating
// them back to wchar_t since they have passed to be in and out as
// char * anyway.
char *name;
char *login;
char *url;
char *epass;
UINT16 epass_len; // can't rely on NULL termination! It's an encrypted string.

int set_field(char **field, const char *value, UINT16 len);
void zero_free_field(char *field, UINT16 len);

public:
AccountRecord();
~AccountRecord();

void set_nonce(const char *in) { memcpy(nonce, in, 12); }
void set_tag(const char *in) { memcpy(tag, in, 16); }

int set_enc_pass(const char *in, UINT16 len);
int set_name(const char *in, UINT16 len) { return set_field(&name, in, len); }
int set_login(const char *in, UINT16 len) { return set_field(&login, in, len); }
int set_url(const char *in, UINT16 len) { return set_field(&url, in, len); }

const char *get_epass() { return (epass == NULL)? "" : (const char *)epass; }
const char *get_name() { return (name == NULL) ? "" : (const char *)name; }
const char *get_login() { return (login == NULL) ? "" : (const char *)login; }
const char *get_url() { return (url == NULL) ? "" : (const char *)url; }
const char *get_nonce() { return (const char *)nonce; }
const char *get_tag() { return (const char *)tag; }

UINT16 get_name_len() { return (name == NULL) ? 0 : (UINT16)strlen(name); }
UINT16 get_login_len() { return (login == NULL) ? 0 : (UINT16)strlen(login); }
UINT16 get_url_len() { return (url == NULL) ? 0 : (UINT16)strlen(url); }
UINT16 get_epass_len() { return (epass == NULL) ? 0 : epass_len; }

void clear();
};


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

Повернемося до третьої частини цієї серії: одне з правил захисту даних в недоверенном просторі пам'яті стосувалася відмови від використання класів-контейнерів, які керують своєю власною пам'яттю, зокрема, класу std::string з бібліотеки стандартних шаблонів. Всередині анклаву ця проблема зникає. По тій же самій причині, по якій нам не потрібно заповнювати пам'ять нулями перед її вивільненням, нам не потрібно і турбуватися про те, як контейнери бібліотеки стандартних шаблонів (STL) управляють своєю пам'яттю. Пам'ять анклаву зашифрована, тому навіть якщо фрагменти захищених даних залишаться там після операцій з контейнерами, ці дані не будуть доступні для інших процесів.

Крім того, є вагомий аргумент на користь застосування класу std::string анклаву: код контейнерів STL був досконально вивчений розробниками за кілька років, тому можна стверджувати, що цей код безпечніше, ніж наші власні високорівневі рядкові функції (при наявності вибору). Для простого коду, такого як у класі AccountInfo, це не дуже важливо, але в більш складних програмах це може виявитися досить корисним перевагою. Втім, при цьому зростає розмір DLL-бібліотеки за рахунок додаткового коду STL.

Оголошення нового класу, який ми назвемо E_AccountInfo, показано нижче:

#define TRY_ASSIGN(x) try{x.assign(in,len);} catch(...){return 0;} return 1

class E_AccountRecord
{
char nonce[12];
char tag[16];
// Store these in their multibyte form. There's no sense in translating
// them back to wchar_t since they have passed to be in and out as
// char * anyway.
string name, login, url, epass;

public:
E_AccountRecord();
~E_AccountRecord();

void set_nonce(const char *in) { memcpy(nonce, in, 12); }
void set_tag(const char *in) { memcpy(tag, in, 16); }

int set_enc_pass(const char *in, uint16_t len) { TRY_ASSIGN(epass); }
int set_name(const char *in, uint16_t len) { TRY_ASSIGN(name); }
int set_login(const char *in, uint16_t len) { TRY_ASSIGN(login); }
int set_url(const char *in, uint16_t len) { TRY_ASSIGN(url); }

const char *get_epass() { return epass.c_str(); }
const char *get_name() { return name.c_str(); }
const char *get_login() { return login.c_str(); }
const char *get_url() { return url.c_str(); }

const char *get_nonce() { return (const char *)nonce; }
const char *get_tag() { return (const char *)tag; }

uint16_t get_name_len() { return (uint16_t) name.length(); }
uint16_t get_login_len() { return (uint16_t) login.length(); }
uint16_t get_url_len() { return (uint16_t) url.length(); }
uint16_t get_epass_len() { return (uint16_t) epass.length(); }

void clear();
};

Члени tag nonce і раніше зберігаються у вигляді масивів char. Для шифрування пароля використовується алгоритм AES в режимі GCM з 128-розрядним ключем, 96-розрядним випадковим числом і 128-розрядним тегом автентифікації. Оскільки використовується фіксований розмір випадкового числа і тега, немає необхідності зберігати їх в будь-яких структурах, більш складних, ніж прості масиви char.

Зверніть увагу, що такий підхід на базі std::string дає нам можливість майже повністю визначити клас у файлі заголовка.

Клас Crypto
Клас Crypto надає функції шифрування. Оголошення цього класу наведено нижче.

class PASSWORDMANAGERCORE_API Crypto
{
DRNG drng;

crypto_status_t aes_init (BCRYPT_ALG_HANDLE *halgo, LPCWSTR algo_id, PBYTE chaining_mode, DWORD chaining_mode_len, BCRYPT_KEY_HANDLE *hkey, PBYTE key, ULONG key_len);
void aes_close (BCRYPT_ALG_HANDLE *halgo, BCRYPT_KEY_HANDLE *hkey);

crypto_status_t aes_128_gcm_encrypt(PBYTE key, PBYTE nonce, ULONG nonce_len, PBYTE pt, DWORD pt_len, PBYTE ct, DWORD ct_sz, PBYTE tag, DWORD tag_len);
crypto_status_t aes_128_gcm_decrypt(PBYTE key, PBYTE nonce, ULONG nonce_len, PBYTE ct, DWORD ct_len, PBYTE pt, DWORD pt_sz, PBYTE tag, DWORD tag_len);
crypto_status_t sha256_multi (PBYTE *messages, ULONG *lengths, BYTE hash[32]);

public:
Crypto(void);
~Crypto(void);

crypto_status_t generate_database_key (BYTE key_out[16], GenerateDatabaseKeyCallback callback);
crypto_status_t generate_salt (BYTE salt[8]);
crypto_status_t generate_salt_ex (PBYTE salt, ULONG salt_len);
crypto_status_t generate_nonce_gcm (BYTE nonce[12]);

crypto_status_t unlock_vault(PBYTE passphrase, ULONG passphrase_len, BYTE salt[8], BYTE db_key_ct[16], BYTE db_key_iv[12], BYTE db_key_tag[16], BYTE db_key_pt[16]);

crypto_status_t derive_master_key (PBYTE passphrase, ULONG passphrase_len, BYTE salt[8], BYTE mkey[16]);
crypto_status_t derive_master_key_ex (PBYTE passphrase, ULONG passphrase_len, PBYTE salt, ULONG salt_len, ULONG iterations, BYTE mkey[16]);

crypto_status_t validate_passphrase(PBYTE passphrase, ULONG passphrase_len, BYTE salt[8], BYTE db_key[16], BYTE db_iv[12], BYTE db_tag[16]);
crypto_status_t validate_passphrase_ex(PBYTE passphrase, ULONG passphrase_len, PBYTE salt, ULONG salt_len, ULONG iterations, BYTE db_key[16], BYTE db_iv[12], BYTE db_tag[16]);

crypto_status_t encrypt_database_key (BYTE master_key[16], BYTE db_key_pt[16], BYTE db_key_ct[16], BYTE iv[12], BYTE tag[16], DWORD flags= 0);
crypto_status_t decrypt_database_key (BYTE master_key[16], BYTE db_key_ct[16], BYTE iv[12], BYTE tag[16], BYTE db_key_pt[16]);

crypto_status_t encrypt_account_password (BYTE db_key[16], PBYTE password_pt, ULONG password_len, PBYTE password_ct, BYTE iv[12], BYTE tag[16], DWORD flags= 0);
crypto_status_t decrypt_account_password (BYTE db_key[16], PBYTE password_ct, ULONG password_len, BYTE iv[12], BYTE tag[16], PBYTE password);

crypto_status_t encrypt_database (BYTE db_key[16], PBYTE db_serialized, ULONG db_size, PBYTE db_ct, BYTE iv[12], BYTE tag[16], DWORD flags= 0);
crypto_status_t decrypt_database (BYTE db_key[16], PBYTE db_ct, ULONG db_size, BYTE iv[12], BYTE tag[16], PBYTE db_serialized);

crypto_status_t generate_password(PBYTE buffer, USHORT buffer_len, USHORT flags);
};

Публічні методи в цьому класі змінені для виконання високорівневих операцій з сховищем: unlock_vault, derive_master_key, validate_passphrase, encrypt_database і т. д. Кожен з цих методів викликає один або кілька алгоритмів шифрування для виконання свого завдання. Наприклад, метод unlock_vault отримує парольну фразу, надану користувачем, пропускає її через функцію формування ключа на основі алгоритму SHA-256, потім використовує отриманий ключ для розшифровки ключа бази даних за допомогою алгоритму AES-128 в режимі GCM.
Втім, ці високорівневі методи не викликають примітиви шифрування напряму. Вони викликають середній рівень, на якому кожен алгоритм шифрування реалізований у вигляді незалежної функції.


Малюнок 2. Залежно бібліотеки шифрування.

Приватні методи, що утворюють наш середній шар, побудовані на основі примітивів шифрування і підтримують функції, надані базовою бібліотекою шифрування, як показано на рис. 2. Реалізація, не використовує Intel SGX, спирається на API Microsoft Cryptography: Next Generation (CNG), але цю ж бібліотеку не можна використовувати всередині анклаву, оскільки анклав не може залежати від зовнішніх DLL-бібліотек. Щоб створити версію цього класу для Intel SGX, потрібно замінити ці базові функції з функціями довіреної бібліотеки шифрування, яка поширюється разом з Intel SGX SDK. (Як ви, ймовірно, пам'ятаєте з другої частини, ми дуже прискіпливо відбирали функції шифрування, спільні для CNG і для довіреної бібліотеки шифрування Intel SGX, саме з цієї причини.)

Щоб створити клас Crypto з підтримкою анклаву, який ми назвемо E_Crypto, потрібно змінити наступні приватні методи:

crypto_status_t aes_128_gcm_encrypt(PBYTE key, PBYTE nonce, ULONG nonce_len, PBYTE pt, DWORD pt_len, PBYTE ct, DWORD ct_sz, PBYTE tag, DWORD tag_len);
crypto_status_t aes_128_gcm_decrypt(PBYTE key, PBYTE nonce, ULONG nonce_len, PBYTE ct, DWORD ct_len, PBYTE pt, DWORD pt_sz, PBYTE tag, DWORD tag_len);
crypto_status_t sha256_multi (PBYTE *messages, ULONG *lengths, BYTE hash[32]);

Опис кожного методу, а також примітивів і підтримуючих функцій з CNG, на основі яких вони побудовані, наводиться в таблиці 1.
Метод Алгоритм Примітиви CNG і підтримуючі функції
aes_128_gcm_encrypt Шифрування AES в режимі GCM:
• 128-бітний ключ
• 128-розрядний тег автентифікації
• Відсутність додаткових перевірених даних (AAD)
BCryptOpenAlgorithmProvider
BCryptSetProperty
BCryptGenerateSymmetricKey
BCryptEncrypt
BCryptCloseAlgorithmProvider
BCryptDestroyKey
aes_128_gcm_decrypt Шифрування AES в режимі GCM:
• 128-бітний ключ
• 128-розрядний тег автентифікації
• Відсутність AAD
BCryptOpenAlgorithmProvider
BCryptSetProperty
BCryptGenerateSymmetricKey
BCryptDecrypt
BCryptCloseAlgorithmProvider
BCryptDestroyKey
sha256_multi Хеш SHA-256 (додатковий) BCryptOpenAlgorithmProvider
BCryptGetProperty
BCryptCreateHash
BCryptHashData
BCryptFinishHash
BCryptDestroyHash
BCryptCloseAlgorithmProvider
Таблиця 1. Зіставлення методів класу Crypto з функціями API Cryptography: Next Generation (CNG).

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

Інтерфейс API для довіреної бібліотеки шифрування, поширюваної разом з Intel SGX SDK, більше схожий на наш середній рівень, ніж CNG. При цьому підтримується менше можливостей точного управління базовими примітивами, але зате створення класу E_Crypto стає набагато простіше. У таблиці 2 показано нове зіставлення між середнім рівнем і базовим постачальником.
Метод Алгоритм Примітиви і підтримуючі функції Intel SGX Trusted Cryptography Library
aes_128_gcm_encrypt Шифрування AES в режимі GCM:
• 128-бітний ключ
• 128-розрядний тег автентифікації
• Відсутність додаткових перевірених даних (AAD)
sgx_rijndael128GCM_encrypt
aes_128_gcm_decrypt Шифрування AES в режимі GCM:
• 128-бітний ключ
• 128-розрядний тег автентифікації
• Відсутність AAD
sgx_rijndael128GCM_decrypt
sha256_multi Хеш SHA-256 (додатковий) sgx_sha256_init
sgx_sha256_update
sgx_sha256_get_hash
sgx_sha256_close
Таблиця 2. Зіставлення методів класу Crypto з функціями Intel SGX Trusted Cryptography Library.

Клас DRNG
Клас DRNG є інтерфейсом до апаратного цифрового генератора випадкових чисел, який доступний завдяки підтримці технології Intel Secure Key. Для однорідності з попередніми діями версія цього класу, призначена для анклаву, буде називатися E_DRNG.

Ми зробимо дві зміни в цьому класі, щоб пристосувати його до анклаву. Обидва зміни є внутрішніми для методів цього класу. Оголошення класу залишається таким же.

Інструкція CPUID

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

Ситуація ускладнюється ще й тим, що Intel Secure Key складається з двох окремих компонентів, наявність кожного їх них також потрібно окремо перевірити. Наше додаток має визначити підтримку інструкцій RDRAND і RDSEED. Додаткові відомості про технології Intel Secure Key див. керівництво по впровадженню програмного забезпечення генерації випадкових чисел (DRNG) Intel.

Конструктор в класі DRNG відповідає за перевірки, необхідні для виявлення компонентів RDRAND RDSEED. Він здійснює необхідні виклики до інструкції CPUID з допомогою вбудованих функцій компілятора __cpuid __cpuidex і задає статичну глобальну змінну з результатами.

static int _drng_support= DRNG_SUPPORT_UNKNOWN;
static int _drng_support= DRNG_SUPPORT_UNKNOWN;

DRNG::DRNG(void)
{
int info[4];

if (_drng_support != DRNG_SUPPORT_UNKNOWN) return;

_drng_support= DRNG_SUPPORT_NONE;

// Check our feature support

__cpuid(info, 0);

if ( memcmp(&(info[1]), "Genu", 4) ||
memcmp(&(info[3]), "ineI", 4) ||
memcmp(&(info[2]), "ntel", 4) ) return;

__cpuidex(info, 1, 0);

if ( ((UINT) info[2]) & (1<<30) ) _drng_support|= DRNG_SUPPORT_RDRAND;

#ifdef COMPILER_HAS_RDSEED_SUPPORT
__cpuidex(info, 7, 0);

if ( ((UINT) info[1]) & (1<<18) ) _drng_support|= DRNG_SUPPORT_RDSEED;
#endif
}

Проблема для класу E_DRNG полягає в тому, що CPUID не є припустимою інструкцією всередині анклаву. Щоб викликати CPUID, потрібно використовувати OCALL для виходу з анклаву, а потім викликати CPUID в недоверенном коді. На щастя, розробники Intel SGX SDK створили дві зручні функції, що істотно спрощують цю задачу: sgx_cpuid sgx_cpuidex. Ці функції автоматично виконують OCALL, причому створення OCALL відбувається автоматично. Єдина вимога полягає в тому, що файл EDL повинен імпортувати заголовок sgx_tstdc.edl:

enclave {

/* Needed for the call to sgx_cpuidex */
from "sgx_tstdc.edl" import *;

trusted {
/* define ECALLs here. */

public int ve_initialize ();
public int ve_initialize_from_header ([in, count=len] unsigned char *header, uint16_t len);
/* Our other ECALLs have been omitted for brevity */
};

untrusted {
};
};

Код виявлення системних компонентів в конструкторі E_DRNG стає таким:

static int _drng_support= DRNG_SUPPORT_UNKNOWN;

E_DRNG::E_DRNG(void)
{
int info[4];
sgx_status_t status;

if (_drng_support != DRNG_SUPPORT_UNKNOWN) return;

_drng_support = DRNG_SUPPORT_NONE;

// Check our feature support

status= sgx_cpuid(info, 0);
if (status != SGX_SUCCESS) return;

if (memcmp(&(info[1]), "Genu", 4) ||
memcmp(&(info[3]), "ineI", 4) ||
memcmp(&(info[2]), "ntel", 4)) return;

status= sgx_cpuidex(info, 1, 0);
if (status != SGX_SUCCESS) return;

if (info[2]) & (1 << 30)) _drng_support |= DRNG_SUPPORT_RDRAND;

#ifdef COMPILER_HAS_RDSEED_SUPPORT
status= __cpuidex(info, 7, 0);
if (status != SGX_SUCCESS) return;

if (info[1]) & (1 << 18)) _drng_support |= DRNG_SUPPORT_RDSEED;
#endif
}

Оскільки виклики до інструкції CPUID здійснюються в ненадійна пам'яті, результатами CPUID не можна довіряти! Це застереження діє у всіх випадках, коли ви запускаєте CPUID самостійно чи використовуєте для цього функції SGX. У пакеті Intel SGX SDK пропонується наступна порада: «код повинен перевіряти результати й оцінювати загрозу, щоб визначити вплив на довірений код у разі підробки результатів».
У нашому навчальному менеджері паролів можливо три варіанти:
  1. Інструкції RDRAND та/або RDSEED не виявлені, але підроблений позитивний результат для однієї з них. Це призведе до помилки через неправильні інструкцій під час виконання, відбудеться аварійний збій програми.
  2. Інструкція RDRAND виявлена, але підроблений негативний результат. Це призведе до помилки під час виконання; програма завершить роботу штатним чином, оскільки необхідний компонент не виявлено.

  3. Інструкція RDSEED виявлена, але підроблений негативний результат. В цьому випадку програма повернеться до використання інструкції RDRAND для отримання початкових випадкових значень, що незначно вплине на продуктивність. У всіх інших аспектах програма буде працювати штатним чином.
Оскільки найгіршим сценарієм є відмова в обслуговуванні, але секрети програми при цьому не розкриваються, і захист не порушується, ми не будемо виявляти атаки з використанням підробок (підміна).

Формування початкових значень з допомогою RDRAND

Якщо ЦП системи не підтримує інструкцію RDSEED, нам потрібно мати можливість використовувати інструкцію RDRAND для формування випадкових початкових значень, функціонально рівноцінні тим, які ми отримали при використанні інструкції RDSEED (якби вона була доступна). керівництво по впровадженню програмного забезпечення генератора випадкових чисел (DRNG) Intel докладно описується процес отримання випадкових початкових значень з допомогою RDRAND, а короткий опис алгоритму така: потрібно сформувати 512 пар 128-розрядних значень і перемішати проміжні значення між собою за допомогою режиму CBC-MAC алгоритму AES для отримання одного 128-розрядного початкового значення. Процес повторюється, щоб сформувати стільки початкових значень, скільки потрібно.

В гілці коду без використання Intel SGX метод seed_from_rdrand використовує CNG для складання алгоритму шифрування. Оскільки гілка коду Intel SGX не може залежати від CNG, знову потрібно використовувати довірену бібліотеку шифрування, яка поширюється разом з Intel SGX SDK. Зведення змін наведено в таблиці 3.

Алгоритм Примітиви CNG і підтримуючі функції Примітиви і підтримуючі функції Intel SGX Trusted Cryptography Library
aes-cmac BCryptOpenAlgorithmProvider
BCryptGenerateSymmetricKey
BCryptSetProperty
BCryptEncrypt
BCryptDestroyKey
BCryptCloseAlgorithmProvider
sgx_cmac128_init
sgx_cmac128_update
sgx_cmac128_final
sgx_cmac128_close
Таблиця 3. Зміни функції шифрування в методі seed_from_rdrand класу E_DRNG.

Чому цей алгоритм вбудовано у клас DRNG, а не реалізований в класі Crypto разом з іншими алгоритмами шифрування? Просто тому, що було прийнято таке рішення при проектуванні структури програми. Класу DRNG потрібно тільки один цей алгоритм, тому ми вирішили не створювати взаємозалежність між класами DRNG Crypto (зараз клас Crypto залежить від DRNG). Крім того, клас Crypto структурований таким чином, щоб надавати послуги шифрування для операцій з сховищем, а не для роботи в якості API шифрування загального призначення.

Чому ми не використовуємо sgx_read_rand?

У складі Intel SGX SDK є функція sgx_read_rand, що дозволяє отримувати випадкові числа в межах анклаву. Ми не використовуємо її з трьох причин:
  1. Згідно документації до Intel SGX SDK, ця функція «надається в якості заміни стандартних функцій генераторів псевдовипадкових послідовностей C всередині анклаву, оскільки ці стандартні функції, такі як rand, srand та ін., не підтримуються всередині анклаву». Функція sgx_read_rand дійсно викликає інструкцію RDRAND, якщо її підтримує ЦП, але якщо ця інструкція не підтримується, то функція викликає стандартні реалізації функцій srand rand, реалізовані у довіреної бібліотеці C. Випадкові числа, створювані бібліотекою C, непридатні для використання в шифруванні. Ймовірність того, що така ситуація коли-небудь виникне, дуже мала, але, як було сказано в розділі, присвяченому CPUID, не слід покладатися на те, що така ситуація не станеться ніколи.
  2. Не існує функції Intel SGX SDK для виклику інструкції RDSEED. Це означає, що нам доведеться писати в коді вбудовані функції компілятора. Можна було б замінити вбудовані функції RDRAND на виклики sgx_read_rand, але це не дасть нам ніяких переваг з точки зору управління кодом або структури коду, але зажадає додаткового часу.
  3. Вбудовані функції будуть працювати трохи швидше, ніж sgx_read_rand, оскільки в коді буде на один рівень менше викликів функцій.
Підбиття підсумків
Після всіх цих змін коду ми отримали повністю діючий анклав. Тим не менш, його реалізація поки не надто ефективна, є прогалини і у функціональності, тому ми повернемося до роботи над анклавом в сьомий і восьмий частинах цієї серії навчальних курсів.
Як вже було сказано вище, в цій частині надається приклад коду для завантаження.

що Додається архів включає вихідний код ядра програми Tutorial Password Manager, включаючи анклав і його функції-оболонки. Цей вихідний код функціонально ідентичний кодом, який додавався до третьої частини; різниця лише в тому, що тут ми жорстко закодували підтримку Intel SGX.

В подальших випусках
У шостій частині цього навчального посібника ми додамо в диспетчер паролів автоматичне виявлення системних компонентів, щоб програма могла вибирати потрібну гілку коду в залежності від того, чи підтримує платформа розширення Intel SGX. Стежте за новинами!

Завантажувані файли доступні на умовах ліцензійної угоди Intel Software Guard Extensions Tutorial Series.
Джерело: Хабрахабр

0 коментарів

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