In Part 5 of the Intel® Software Guard Extensions (Intel® SGX) tutorial series, we’ll finish developing the enclave for the Tutorial Password Manager application. In Part 4 of the series, we created a DLL to serve as our interface layer between the enclave bridge functions and the C++/CLI program core, and defined our enclave interface. With those components in place, we can now focus our attention on the enclave itself.
You can find the list of all of the published tutorials in the article Introducing the Intel® Software Guard Extensions Tutorial Series.
There is source code provided with this installment of the series: the completed application with its enclave. This version is hardcoded to run the Intel SGX code path.
The Enclave Components
To identify which components need to be implemented within the enclave, we’ll refer to the class diagram for the application core in Figure 1, which was first introduced in Part 3. As before, the objects that will reside in the enclave are shaded in green while the untrusted components are shaded in blue.
Figure 1. Class diagram for the Tutorial Password Manager with Intel® Software Guard Extensions.
From this we can identify four classes that need to be ported:
- Vault
- AccountRecord
- Crypto
- DRNG
Before we get started, however, we do need to make a design decision. Our application must function on systems both with and without Intel SGX support, and that means we can’t simply convert our existing classes so that they function within the enclave. We must create two versions of each: one intended for use in enclaves, and one for use in untrusted memory. The question is, how should this dual-support be implemented?
Option 1: Conditional Compilation
The first option is to implement both the enclave and untrusted functionality in the same source module and use preprocessor definitions and #ifdef
statements to compile the appropriate code based on the context. The advantage of this approach is that we only need one source file for each class, and thus do not have to maintain changes in two places. The disadvantages are that the code can be more difficult to read, particularly if the changes between the two versions are numerous or significant, and the project structure will be more complex. Two of our Visual Studio* projects, Enclave
and PasswordManagerCore
, will share source files, and each will need to set a preprocessor symbol to ensure that the correct source code is compiled.
Option 2: Separate Classes
The second option is to duplicate each source file that has to go into the enclave. The advantages of this approach are that the enclave has its own copy of the source files which we can modify directly, allowing for a simpler project structure and easier code view. But, these come at a cost: if we need to make changes to the classes, those changes must be made in two places, even if those changes are common to both the enclave and untrusted versions.
Option 3: Inheritance
The third option is to use the C++ feature of class inheritance. The functions common to both versions of the class would be implemented in the base class, and the derived classes would implement the branch-specific methods. The big advantage to this approach is that it is a very natural and elegant solution to the problem, using a feature of the language that is designed to do exactly what we need. The disadvantages are the added complexity required in both the project structure and the code itself.
There is no hard and fast rule here, and the decision does not have to be a global one. A good rule of thumb is that Option 1 is best for modules where the changes are small or easily compartmentalized, and Options 2 and 3 are best when the changes are significant or result in source code that is difficult to read and maintain. However, it really comes down to style and preference, and either approach is fine.
For now, we’ll choose Option 2 because it allows for easy side-by-side comparisons of the enclave and untrusted source files. In a future installment of the tutorial series we may switch to Option 3 in order to tighten up the code.
The Enclave Classes
Each class has its own set of issues and challenges when it comes to adapting it to the enclave, but there is one universal truth that will apply to all of them: we no longer have to zero-fill our memory before freeing it. As you recall from Part 3, this was a recommended action when handling secure data in untrusted memory. Because our enclave memory is encrypted by the CPU, using an encryption key that is not available to any hardware layer, the contents of freed memory will contain what appears to be random data to other applications. This means we can remove all calls to SecureZeroMemory that are inside the enclave.
The Vault Class
The Vault class is our interface to the password vault operations. All of our bridge functions act through one or more methods in Vault. Its declaration from Vault.h
is shown below.
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; } };
The declaration for the enclave version of this class, which we’ll call E_Vault for clarity, will be identical except for one crucial change: database key handling.
In the untrusted code path, the Vault object must store the database key, decrypted, in memory. Every time we make a change to our password vault we have to encrypt the updated vault data and write it to disk, and that means the key must be at our disposal. We have four options:
- Prompt the user for their master password on every change so that the database key can be derived on demand.
- Cache the user’s master password so that the database key can be derived on demand without user intervention.
- Encrypt, encode, and/or obscure the database key in memory.
- Store the key in the clear.
None of these are good solutions and they highlight the need for technologies like Intel SGX. The first is arguably the most secure, but no user would want to run an application that behaved in this manner. The second could be achieved using the SecureString class in .NET*, but it is still vulnerable to inspection via a debugger and there is a performance cost associated with the key derivation function that a user might find unacceptable. The third option is effectively insecure as the second, only it comes without a performance penalty. The fourth option is the worst of the lot.
Our Tutorial Password Manager uses the third option: the database key is XOR’d with a 128-bit value that is randomly generated when a vault file is opened, and it is stored in memory only in this XOR’d form. This is effectively a one-time pad encryption scheme. It is open to inspection for anyone running a debugger, but it does limit the amount of time in which the database key is present in memory in the clear.
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]; }
This is obviously security through obscurity, and since we are publishing the source code, it’s not even particularly obscure. We could choose a better algorithm or go to greater lengths to hide both the database key and the pad’s secret key (including how they are stored in memory); but in the end, the method we choose would still be vulnerable to inspection via a debugger, and the algorithm would still be published for anyone to see.
Inside the enclave, however, this problem goes away. The memory is protected by hardware-backed encryption, so even when the database key is decrypted it is not open to inspection by anyone, even a process running with elevated privileges. As a result, we no longer need these class members or methods:
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]);
We can replace them with just one class member: a char array to hold the database key.
char db_key[16];
The AccountInfo Class
The account data is stored in a fixed-size array of AccountInfo objects as a member of the Vault object. The declaration for AccountInfo is also found in Vault.h
, and it is shown below:
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(); };
We actually don’t need to do anything to this class for it to work inside the enclave. Other than remove the unnecessary calls to SecureZeroFree, this class is fine as is. However, we are going to change it anyway in order to illustrate a point: within the enclave, we gain some flexibility that we did not have before.
Returning to Part 3, another of our guidelines for securing data in untrusted memory space was avoiding container classes that manage their own memory, specifically the Standard Template Library’s std::string class. Inside the enclave this problem goes away, too. For the same reason that we don’t need to zero-fill our memory before freeing it, we don’t have to worry about how the Standard Template Library (STL) containers manager their memory. The enclave memory is encrypted, so even if fragments of our secure data remain there as a result of container operations, they can’t be inspected by other processes.
There’s also a good reason to use the std::string class inside the enclave: reliability. The code behind the STL containers has been through significant peer review over the years and it can be argued that it is safer to use it than implement our own high-level string functions when given the choice. For simple code like what’s in the AccountInfo class, it’s probably not a significant issue, but in more complex programs this can be a huge benefit. However, this does come at the cost of a larger DLL due to the added STL code.
The new class declaration, which we’ll call E_AccountInfo, is shown below:
#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(); };
The tag and nonce members are still stored as char arrays. Our password encryption is done with AES in GCM mode, using a 128-bit key, a 96-bit nonce, and a 128-bit authentication tag. Since the size of the nonce and the tag are fixed there is no reason to store them as anything other than simple char arrays.
Note that this std::string-based approach has allowed us to almost completely define the class in the header file.
The Crypto Class
The Crypto class provides our cryptographic functions. The class declaration is shown below.
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); };
The public methods in this class are modeled to perform various high-level vault operations: unlock_vault, derive_master_key, validate_passphrase, encrypt_database, and so on. Each of these methods invokes one or more cryptographic algorithms in order to complete its task. For example, the unlock_vault method takes the passphrase supplied by the user, runs it through the SHA-256-based key derivation function, and uses the resulting key to decrypt the database key using AES-128 in GCM mode.
These high-level methods do not, however, directly invoke the cryptographic primitives. Instead, they call into a middle layer which implements each cryptographic algorithm as a self-contained function.
Figure 2. Cryptographic library dependancies.
The private methods that make up our middle layer are built on the cryptographic primitives and support functions provided by the underlying cryptographic library, as illustrated in Figure 2. The non-Intel SGX implementation relies on Microsoft’s Cryptography API: Next Generation (CNG) for these, but we can’t use this same library inside the enclave because an enclave cannot have dependencies on external DLLs. To build the Intel SGX version of this class, we need to replace those underlying functions with the ones in the trusted crypto library that is distributed with the Intel SGX SDK. (As you might recall from Part 2, we were careful to choose cryptographic functions that were common to both CNG and the Intel SGX trusted crypto library for this very reason.)
To create our enclave-capable Crypto class, which we’ll call E_Crypto, what we need to do is modify these private methods:
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]);
A description of each, and the primitives and support functions from CNG upon which they are built, is given in Table 1.
Method | Algorithm | CNG Primitives and Support Functions |
---|---|---|
aes_128_gcm_encrypt | AES encryption in GCM mode with:
| BCryptOpenAlgorithmProvider |
aes_128_gcm_decrypt | AES encryption in GCM mode with:
| BCryptOpenAlgorithmProvider |
sha256_multi | SHA-256 hash (incremental) | BCryptOpenAlgorithmProvider |
Table 1. Mapping Crypto class methods to Cryptography API: Next Generation functions
CNG provides very fine-grained control over its encryption algorithms, as well as several optimizations for performance. Our Crypto class is actually fairly inefficient: each time one of these algorithms is called, it initializes the underlying primitives from scratch and then completely closes them down. This is not a significant issue for a password manager, which is UI-driven and only encrypts a small amount of data at a time. A high-performance server application such as a web or database server would need a more sophisticated approach.
The API for the trusted cryptography library distributed with the Intel SGX SDK more closely resembles our middle layer than CNG. There is less granular control over the underlying primitives, but it does make developing our E_Crypto class much simpler. Table 2 shows the new mapping between our middle layer and the underlying provider.
Method | Algorithm | Intel® SGX Trusted Cryptography Library Primitives and Support Functions |
---|---|---|
aes_128_gcm_encrypt | AES encryption in GCM mode with:
| sgx_rijndael128GCM_encrypt |
aes_128_gcm_decrypt | AES encryption in GCM mode with:
| sgx_rijndael128GCM_decrypt |
sha256_multi | SHA-256 hash (incremental) | sgx_sha256_init |
Table 2. Mapping Crypto class methods to Intel® SGX Trusted Cryptography Library functions
The DRNG Class
The DRNG class is the interface to the on-chip digital random number generator, courtesy of Intel® Secure Key. To stay consistent with our previous actions we’ll name the enclave version of this class E_DRNG.
We’ll be making two changes in this class to prepare it for the enclave, but both of these changes are internal to the class methods. The class declaration will stay the same.
The CPUID Instruction
One of our application requirements is that the CPU supports Intel Secure Key. Even though Intel SGX is a newer feature than Secure Key, there is no guarantee that all future generations of all possible CPUs which support Intel SGX will also support Intel Secure Key. While it’s hard to conceive of such a situation today, best practice is to not assume a coupling between features where one does not exist. If a set of features have independent detection mechanisms, then you must assume that the features are independent of one another and check for them separately. This means that no matter how tempting it may be to assume that a CPU with support for Intel SGX will also support Intel Secure Key, we absolutely must not do so under any circumstances.
Further complicating the situation is the fact that Intel Secure Key actually consists of two independent features, both of which must also be checked separately. Our application must determine support for both the RDRAND and RDSEED instructions. For more information on Intel Secure Key, see the Intel Digital Random Number Generator (DRNG) Software Implementation Guide.
The constructor in the DRNG class is responsible for the RDRAND and RDSEED feature detection checks. It makes the necessary calls to the CPUID instruction using the compiler intrinsics __cpuid and __cpuidex, and sets a static, global variable with the results.
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 }
The problem for the E_DRNG class is that CPUID is not a legal instruction inside of an enclave. To call CPUID, one must use an OCALL to exit the enclave and then invoke CPUID in untrusted code. Fortunately, the Intel SGX SDK designers have created two convenient functions that greatly simplify this task: sgx_cpuid and sgx_cpuidex. These functions automatically perform the OCALL for us, and the OCALL itself is automatically generated. The only requirement is that the EDL file must import the sgx_tstdc.edl
header:
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 { }; };
The feature detection code in the E_DRNG constructor becomes:
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 }
Because calls to the CPUID instruction must take place in untrusted memory, the results of CPUID cannot be trusted! This warning applies whether you run CPUID yourself or rely on the SGX functions to do it for you. The Intel SGX SDK offers this advice: “Code should verify the results and perform a threat evaluation to determine the impact on trusted code if the results were spoofed.” In our tutorial password manager, there are three possible outcomes:
|
Generating Seeds from RDRAND
In the event that the underlying CPU does not support the RDSEED instruction, we need to be able to use the RDRAND instruction to generate random seeds that are functionally equivalent to what we would have received from RDSEED if it were available. The Intel Digital Random Number Generator (DRNG) Software Implementation Guide describes the process of obtaining random seeds from RDRAND in detail, but the short version is that one method for doing this is to generate 512 pairs of 128-bit values and mix the intermediate values together using the CBC-MAC mode of AES to produce a single, 128-bit seed. The process is repeated to generate as many seeds as necessary.
In the non-Intel SGX code path, the method seed_from_rdrand uses CNG to build the cryptographic algorithm. Since the Intel SGX code path can’t depend on CNG, we once again need to turn to the trusted cryptographic library that is distributed with the Intel SGX SDK. The changes are summarized in Table 3.
Algorithm | CNG Primitives and Support Functions | Intel® SGX Trusted Cryptography Library Primitives and Support Functions |
---|---|---|
aes-cmac | BCryptOpenAlgorithmProvider | sgx_cmac128_init |
Table 3. Cryptographic function changes to the E_DRNG class’s seed_from_rdrand method
Why is this algorithm embedded in the DRNG class and not implemented in the Crypto class with the other cryptographic algorithms? This is simply a design decision. The DRNG class only needs this one algorithm, so we chose not to create a co-dependency between DRNG and Crypto (currently, Crypto does depend on DRNG). The Crypto class is also structured to provide the cryptographic services for vault operations rather than function as a general-purpose cryptographic API.
Why Not Use sgx_read_rand?
The Intel SGX SDK provides the function sgx_read_rand as a means of obtaining random numbers inside of an enclave. There are three reasons why we aren’t using it:
- As documented in the Intel SGX SDK, this function is “provided to replace the C standard pseudo-random sequence generation functions inside the enclave, since these standard functions are not supported in the enclave, such as rand, srand, etc.” While sgx_read_rand does call the RDRAND instruction if it is supported by the CPU, it falls back to the trusted C library’s implementation of srand and rand if it is not. The random numbers produced by the C library are not suitable for cryptographic use. It is highly unlikely that this situation will ever occur, but as mentioned in the section on CPUID, we must not assume that it will never occur.
- There is no Intel SGX SDK function for calling the RDSEED instruction and that means we still have to use compiler intrinsics in our code. While we could replace the RDRAND intrinsics with calls to sgx_read_rand, it would not gain us anything in terms of code management or structure and it would cost us additional time.
- The intrinsics will marginally outperform sgx_read_rand since there is one less layer of function calls in the resulting code.
Wrapping Up
With these code changes, we have a fully functioning enclave! However, there are still some inefficiencies in the implementation and some gaps in functionality, and we’ll revisit the enclave design in Parts 7 and 8 in order to address them.
As mentioned in the introduction, there is sample code provided with this part for you to download. The attached archive includes the source code for the Tutorial Password Manager core, including the enclave and its wrapper functions. This source code should be functionally identical to Part 3, only we have hardcoded Intel SGX support to be on.
Coming Up Next
In Part 6 of the tutorial we’ll add dynamic feature detection to the password manager, allowing it to choose the appropriate code path based on whether or not Intel SGX is supported on the underlying platform. Stay tuned!