[Перевод] Intel Software Guard Extensions, учебное руководство. Часть 5, разработка анклава

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

Вместе с этой частью серии предоставляется исходный код: готовое приложение с анклавом. В этой версии жестко задана ветвь кода с использованием Intel SGX.

Компоненты анклава


Чтобы определить, какие компоненты следует реализовать внутри анклава, вернемся к схеме классов ядра приложения, которую мы впервые описали в третьей части — она показана на рис. 1. Как и прежде, объекты, находящиеся в анклаве, закрашены зеленым, а недоверенные компоненты — синим.

80c029c9a14c4fad82349c5fc75faafa.png
Рисунок 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 to be passed 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 to be passed 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.
Впрочем, эти высокоуровневые методы не вызывают примитивы шифрования напрямую. Они вызывают средний уровень, на котором каждый алгоритм шифрования реализован в виде независимой функции.

6cc8b522ba55488e873329aaaa502906.png
Рисунок 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
}

8f45c8ee6ea748aa80f1db40147346f5.png Поскольку вызовы к инструкции 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)

© Habrahabr.ru