英特尔® Software Guard Extensions(英特尔® SGX)教程系列第五部分将介绍如何开发面向 Tutorial Password Manager 应用的安全区。 在本系列的第四部分,我们创建了 DLL 用作安全区桥接函数与 C++/CLI 程序内核之间的接口层,并定义了安全区界面。 准备好这些组件后,现在我们可以将重点放在安全区上。
文章英特尔® Software Guard Extensions 教程系列简介列举了所有已经发布的教程。
本系列的安装部分将提供源代码:包含安全区的已完成应用。 该版本经过硬编码,可运行英特尔 SGX 代码路径。
安全区组件
为了确定需在安全区内实施的组件,我们参考第三部分图 1 中有关应用内核的类图。 与之前一样,将驻留在安全区内的对象涂成绿色,非信任组件涂成蓝色。
图 1.基于英特尔® Software Guard Extensions 的 Tutorial Password Manager 应用的类图。
通过该图我们可确定以下四个需要移植的类:
- Vault
- AccountRecord
- Crypto
- DRNG
不过开始之前,我们需要制定设计决策。 不论系统是否支持英特尔 SGX,应用都必须能够在该系统上运行,而且这意味着如果要使现有的类在安全区内运行,不能只是简单地进行转换。 我们必须创建两个版本:一个在安全区内使用,另一个在非信任内存中使用。 问题是,如何实施这种双重支持?
选项 1: 条件编译
第一种选择是在相同的源模块中实施安全区和非信任功能,并使用预处理器定义和 #ifdef
语句根据上下文编译相应的代码。 这种方法的优点是,每个类只需一个源文件,因此不需要维护两个地方的变动。 而缺点是,代码读取难度较大,尤其是两个版本之间变动较大或较明显的情况下,而且项目结构会更加复杂。 两个 Visual Studio* 项目 — Enclave
和 PasswordManagerCore
将共享源文件,而且每个项目都需要设置预处理器符号,以确保编译相应的源代码。
选项 2: 独立类
第二种选择是复制需前往安全区的源文件。 这种方法的优点是,安全区有自己的源文件副本(我们不能直接修改),有助于简化项目结构和代码视图。 但同时伴随着一定的代价:如果需要对类进行修改,必须在两个地方进行,即使安全区和非信任版本的修改大体相同。
选项 3: 继承
第三种选择是使用类继承的 C++ 特性。 两个类版本所共有的功能在基类中实施,衍生的类将实施特定于分支的方法。 这种方法具有一个显著的优点:它使用专门设计的语言的特性,完全满足我们的需求,从而能够自然、有效地解决问题。 而缺点是增加了项目结构和代码本身的复杂性。
这里不存在任何严格、速效的准则,而且我们也不需要制定一劳永逸的决策。 实用的经验法则是:选项 1 适用于变动较少或能够简单划分的模块,选项 2 和 3 适用于变动较大或导致源代码难以读取和维护的情况。 不过最后都可归结为方式和偏好,每种方法都非常实用。
现在我们选择选项 2,因为该方法有助于轻松地并排对比安全区和非信任源文件。 在本教程后续的安装部分,我们将选择选项 3 来加强代码。
安全区类
在适应安全区方面,每个类都面临各自的问题和挑战,不过有一种普遍适用的方法:释放内存之前无需进行零填充。 大家回顾一下,第三部分推荐了一种在非信任内存中处理安全数据的方法。 由于 CPU 使用任何硬件层都不可用的加密密钥对安全区内存进行加密,因此释放的内存中包含对其他应用来说似乎是随机数据的内容。 这意味着我们可以删除安全区内所有针对 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对象必须将加密的数据库密钥保存在内存中。 只要更改密码仓库,就必须加密最新的仓库数据,并将其写入磁盘,而且这意味着我们必须自行处理密钥。 有四种方法:
- 每次更改时向用户提示主密码,以便能够按照需求衍生数据库密钥。
- 缓存用户的主密码,以便在无需用户介入的情况下按照需求衍生数据库密钥。
- 加密、编码和/或模糊内存中的数据库密钥。
- 明文保存密钥。
这些方法都不太实用,而且必须使用英特尔 SGX 等技术。 第一种方法可能最安全,但用户不想运行以这种方式运行的应用。 通过 .NET* 中的 SecureString类能够有效使用第二种方法,但仍然容易受到调试程序的检查,而且密钥衍生功能会降低性能,这是用户所不能接受的。 第三种方法的安全性比第二种方法低,但不会影响性能。 第四种方法最糟糕。
我们的 Tutorial Password Manager 采用第三种方法:数据库密钥是一个包含 128 位值的 XOR’d,该数值是打开仓库文件时随机生成的,而且仅以这种 XOR’d 的形式保存在内存中。 这是一种有效的 运行调试程序的所有用户都可以查看该密钥,但它会限制数据库密钥以明文形式存在于内存中的时间。
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 类
帐号数据以 Vault对象成员的形式保存在大小固定的 AccountInfo对象阵列中。 Vault.h
中还包含 AccountInfo声明,如下所示:
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类中的代码,该问题可能不太明显,但如果是比较复杂的程序,这可能是一个巨大的优势。 不过它伴随的代价是,由于添加了 STL 代码,所以需要较大的 DLL。
新的类声明(我们称为 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阵列的形式保存。 我们采用 GCM 模式,通过 AES 使用 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 的密钥衍生功能运行该口令,并使用生成的密钥解密以 GCM 模式使用 AES-128 的数据库密钥。
不过,这些高级方法不直接调用加密基元, 而是调用中层,以独立函数的形式实施加密算法。
图 2.加密库依赖性。
构成中层的私有方法基于加密基元构建,并支持底层加密库提供的函数,如图 2 所示。 非英特尔 SGX 实施依赖于 Microsoft 的 Cryptography API: Next Generation (CNG) 来执行这些操作,但我们不能在安全区中使用同一种库,因为安全区不能依赖外部 DLL。 为构建此类的英特尔 SGX 版本,我们需要将底层函数替换为可信加密库(与英特尔 SGX SDK 一同发布)中的函数。 (大家可能还记得第二部分所述,我们谨慎选择与 CNG 和英特尔 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]);
表 1 介绍了各种方法以及支持构建这些方法的 CNG 所提供的基元和支持函数。
方法 | 算法 | CNG 基元和支持函数 |
---|---|---|
aes_128_gcm_encrypt | 采用 GCM 模式的 AES 加密包含:
| BCryptOpenAlgorithmProvider |
aes_128_gcm_decrypt | 采用 GCM 模式的 AES 加密包含:
| BCryptOpenAlgorithmProvider |
sha256_multi | SHA-256 hash(增量) | BCryptOpenAlgorithmProvider |
表 1.将 Crypto类方法映射至 Cryptography API: Next Generation 函数
CNG 支持精细地控制其加密算法,以及多种性能优化方法。 Crypto类的效率实际上非常低:每次调用这些算法时,都需要从头初始化底层基元,然后将其完全关闭。 对于密码管理器来说该问题不算严重,因为该管理器由 UI 驱动,而且每次仅加密一小部分数据。 高性能服务器应用(比如 Web 或数据库服务器)可能需要更为复杂的方法。
相比于 CNG 来说,可信加密库(与英特尔 SGX SDK 一同发布)的 API 更加类似于中层。 它对底层基元的控制不那么精细,但能够帮助我们简化 E_Crypto类的开发过程。 表 2 显示了中层与底层基元之间全新的映射过程。
方法 | 算法 | 英特尔® SGX 可信加密库基元和支持函数 |
---|---|---|
aes_128_gcm_encrypt | 采用 GCM 模式的 AES 加密包含:
| sgx_rijndael128GCM_encrypt |
aes_128_gcm_decrypt | 采用 GCM 模式的 AES 加密包含:
| sgx_rijndael128GCM_decrypt |
sha256_multi | SHA-256 hash(增量) | sgx_sha256_init |
表 2.将 Crypto类方法映射至英特尔® SGX 可信加密库函数
DRNG 类
DRNG类是片上数字随机数生成器的接口,由英特尔® 安全密钥提供。 为了与之前的操作保持一致,我们将此类的安全区版本命名为 E_DRNG。
我们对此类进行两项修改,以使其适用于安全区,但这两项修改都需要在类方法内部进行。 类声明将保持不变。
CPUID 指令
其中一项应用要求是 CPU 支持英特尔安全密钥。 即使英特尔 SGX 比安全密钥更新,但也无法保证未来所有支持英特尔 SGX 的 CPU 也同时支持英特尔安全密钥。 虽然现在我们难以想象这种情况,但最好不要假设特性之间存在某种根本不存在的联系。 如果特性有独立的检测机制,那么必须假设这些特性相互之间是独立的,并单独检查这些特性。 这意味着,支持英特尔 SGX 的 CPU 同样支持英特尔安全密钥这种假设无论具有多大的吸引力,在任何情况下我们都绝对不能这么做。
令这种情况更为复杂的是,英特尔安全密钥实际上包含两种独立的特性,而且都必须单独检查。 应用必须确定是否同时支持 RDRAND 和 RDSEED 指令。 更多有关英特尔安全密钥的信息,请参阅英特尔® 数字随机数生成器 (DRNG) 软件实施指南。
DRNG 类中的构造函数负责查看 RDRAND 和 RDSEED 特性检测。 它要求必须使用编译器内联函数 __cpuid和 __cpuidex调用 CPUID 指令,并通过结果设置静态的全局变量。
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。 幸运的是,英特尔 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 函数代劳。 英特尔 SGX SDK 建议: “代码应该验证结果并执行线程评估,以确定结果遭到欺骗后对可信代码所造成的影响。” 在我们的 tutorial password manager 中,可能会出现三种结果:
|
通过 RDRAND 生成种子
如果底层 CPU 不支持 RDSEED 指令,那么我们必须能够使用 RDRAND 指令生成随机种子,其功能与 RDSEED 提供的种子相同(如果可用)。 英特尔数字随机数生成器 (DRNG) 软件实施指南详细介绍了如何通过 RDRAND 获取随机种子,但还有一种更加简单的方法,即生成 512 对 128 位数值,并使用 CBC-MAC 模式的 AES 将这些中间值混合在一起,以生成一个 128 位的种子。 可根据需要重复该流程,以生成多个种子。
在非英特尔 SGX 代码路径中,seed_from_rdrand方法使用 CNG 构建加密算法。 由于英特尔 SGX 代码路径不依赖 CNG,因此我们需要再次使用与英特尔 SGX SDK 一同发布的可信加密库。 表 3 对更改进行了汇总。
算法 | CNG 基元和支持函数 | 英特尔® SGX 可信加密库基元和支持函数 |
---|---|---|
aes-cmac | BCryptOpenAlgorithmProvider | sgx_cmac128_init |
表 3.加密函数对 E_DRNG类的 seed_from_rdrand方法所做的更改
该算法为何嵌入至 DRNG类,而不与其加密算法一样在 Crypto类中实施? 这只是一种设计决策。 DRNG类只需这一种算法,因此我们决定不在 DRNG和 Crypto(目前Crypto确实依赖 DRNG)之间创建相互依赖性。 Crypto同样进行了结构设计,可为向仓库操作(而非作为通用加密 API 的函数)提供加密服务。
为何不使用 sgx_read_rand?
英特尔提供函数 sgx_read_rand,作为在安全区内获取随机数的方法。 我们不使用该函数的原因有以下三点:
- 如英特尔 SGX SDK 文档中所述,该函数“用于替代安全区内的 C 标准伪随机序列生成函数,因为安全区内不支持这些标准函数,比如 rand、srand等”。尽管在 CPU 支持的情况下 sgx_read_rand确实调用 RDRAND 指令,但如果不支持,它会返回至可信 C 库的 srand和 rand实施。 C 库生成的随机数不适用于加密。 基本不可能出现这种情况,但我们在 CPUID 部分说过,绝不能假设这种情况永远不会出现。
- 这里没有用于调用 RDSEED 指令的英特尔 SGX SDK 函数,因此仍然需要在代码中使用编译器内联函数。 尽管能够用调用 sgx_read_rand替换 RDRAND 内联函数,但对代码管理或结构没有丝毫帮助,而且还会导致操作时间延长。
- 该内联函数的性能比 sgx_read_rand稍微高一些,因为最终代码中少一个函数调用层。
总结
通过更改代码,我们获得了一个功能全面的安全区。 但它的实施效率并不高,而且功能方面存在差距。我们将在第七部分和第八部分重新探讨安全区设计,使这些问题得以解决。
如简介部分所述,本部分提供示例代码供您下载。 随附档案包含面向 Tutorial Password Manager 内核的源代码,包括安全区和包装程序函数。
即将推出
本教程的第六部分将为密码管理器添加动态特性检测机制,以支持其根据底层平台是否支持英特尔 SGX 等情况选择合适的代码路径。 敬请关注!