📜 ⬆️ ⬇️

Intel Software Guard Extensions tutorial. Part 4, the enclave

In the fourth installment of the Intel Software Guard Extensions (Intel SGX) tutorial series, we will create an enclave and its interface. We will look at the boundaries of the enclave, defined in Part 3 , and define the necessary functions of the bridge, consider the influence of the bridge’s functions on the object model, and create the project infrastructure necessary to integrate the enclave into our application. Instead of an ECALL enclave, we still use stubs; We will move on to the full integration of the enclave in the fifth part of this series.



Along with this part of the series, source code is provided: an enclave stub and interface functions; This code is available for download.

Application architecture


Before designing the enclave interface, you need to think about the overall architecture of the application. As we discussed in the first part , the enclaves are implemented as dynamic-link libraries (DLLs in Windows * and shared libraries in Linux *) and must be linked only with 100% native C code.
')
At the same time, the graphical user interface of the Tutorial Password Manager program is written in C #. A mixed assembly written in C ++ / CLI is used to switch from managed to unmanaged code, but although this assembly contains its own code, it does not consist of 100% of its own code and therefore cannot directly interact with the Intel SGX enclave. Attempts to embed untrusted enclave bridge functions in C ++ / CLI assemblies will result in unrecoverable errors:

Command line error D8045: cannot compile C file 'Enclave_u.c'; with the /clr option 

This means that it is necessary to place the functions of an untrusted bridge in a separate DLL library consisting entirely of its own code. As a result, there will be at least three DLL libraries in our application: the C ++ / CLI core, the enclave bridge, and the enclave itself. This structure is shown in Fig. one.


Figure 1. Components of a mixed enclave application.

Further improvements


Since the functions of the bridge of the enclave must be in a separate DLL library, we will take the next step: we will place all functions directly interacting with the enclave into this library. This separation of application levels will simplify the management of the program and its debugging, as well as increase the ease of integration by reducing the impact on other modules. If a class or module performs a specific task with a clearly defined boundary, changes to other modules are less likely to affect it.

In our case, the PasswordManagerCoreNative class should not be burdened with the additional task of creating enclave instances. This class only needs to know if the platform supports the Intel SGX extensions to perform the corresponding function.

As an example, the following code snippet shows the unlock () method:

 int PasswordManagerCoreNative::vault_unlock(const LPWSTR wpassphrase) { int rv; UINT16 size; char *mbpassphrase = tombs(wpassphrase, -1, &size); if (mbpassphrase == NULL) return NL_STATUS_ALLOC; rv= vault.unlock(mbpassphrase); SecureZeroMemory(mbpassphrase, size); delete[] mbpassphrase; return rv; } 

This is a very simple method: it takes a user passphrase in the form of wchar_t, converts it into a variable-length encoding (UTF-8), then calls the unlock () method in the storage object. Instead of cluttering up this class and this method with enclave functions, it is better to add enclave support to this method by adding one line:

 int PasswordManagerCoreNative::vault_unlock(const LPWSTR wpassphrase) { int rv; UINT16 size; char *mbpassphrase = tombs(wpassphrase, -1, &size); if (mbpassphrase == NULL) return NL_STATUS_ALLOC; // Call the enclave bridge function if we support Intel SGX if (supports_sgx()) rv = ew_unlock(mbpassphrase); else rv= vault.unlock(mbpassphrase); SecureZeroMemory(mbpassphrase, size); delete[] mbpassphrase; return rv; } 

Our goal is to free this class from working with the enclave to the greatest extent. Other necessary additions for the PasswordManagerCoreNative class: support for the Intel SGX flag and methods for setting and getting this flag.

 class PASSWORDMANAGERCORE_API PasswordManagerCoreNative { int _supports_sgx; // Other class members ommitted for clarity protected: void set_sgx_support(void) { _supports_sgx = 1; } int supports_sgx(void) { return _supports_sgx; } 

Design of the enclave


The general plan of the application is ready, so you can do the design of the enclave and its interface. To do this, let us return to the class diagram of the application kernel, which we first described in the third part — it is shown in Fig. 2. Objects in the enclave are shaded green, and untrusted components are blue.


Figure 2. Class diagram in Tutorial Password Manager with Intel Software Guard Extensions.

Only one connection crosses the enclave border: the connection between the PasswordManagerCoreNative object and the Vault . This means that most of our ECALLs are just shells of class methods in Vault . You also need to add additional ECALL to manage the infrastructure of the enclave. One of the difficulties in the development of an enclave is that ECALL, OCALL and the bridge functions must be native C code, and we widely use C ++ components. After starting the enclave, we also need functions that fill the gap between C and C ++ (objects, constructors, overloads, and others).

The shells and functions of the bridge will be in its own DLL library, which we will call EnclaveBridge.dll. For clarity, we will supply the wrapper function with the prefix “ew_” (enclave wrapper), and the bridge functions that form ECALL with the prefix “ve_” (vault enclave).

Calls from PasswordManagerCoreNative to the corresponding method in Vault will follow the path shown in Figure. 3


Figure 3. The way to perform bridge and ECALL functions.

The method in PasswordManagerCoreNative calls the wrapper function in EnclaveBridge.dll. This shell, in turn, calls one or more ECALLs that go into the enclave and call the corresponding class method in the Vault object. After all of the ECALL is completed, the wrapper function returns to the caller in PasswordManagerCoreNative and gives it the return value.

Logistics enclave


When creating an enclave, you first need to decide on a system to manage the enclave itself. The enclave must be running, and the resulting enclave ID must be provided to ECALL functions. Ideally, all this should be transparent to the top levels of the application.

The easiest solution for Tutorial Password Manager is to use global variables in the EnclaveBridge DLL to post information about the enclave. This solution is constrained: in the enclave there can only be one active stream at a time. This is a reasonable solution, since the performance of the Password Manager will still not increase when using multiple threads to work with the repository. Most actions are controlled by the user interface, they do not constitute a significant load on the CPU.

To solve the transparency problem, each wrapper function must first call the function to check if the enclave is running and start it if it is not already running. The logic is pretty simple:

 #define ENCLAVE_FILE _T("Enclave.signed.dll") static sgx_enclave_id_t enclaveId = 0; static sgx_launch_token_t launch_token = { 0 }; static int updated= 0; static int launched = 0; static sgx_status_t sgx_status= SGX_SUCCESS; // Ensure the enclave has been created/launched. static int get_enclave(sgx_enclave_id_t *eid) { if (launched) return 1; else return create_enclave(eid); } static int create_enclave(sgx_enclave_id_t *eid) { sgx_status = sgx_create_enclave(ENCLAVE_FILE, SGX_DEBUG_FLAG, &launch_token, &updated, &enclaveId, NULL); if (sgx_status == SGX_SUCCESS) { if ( eid != NULL ) *eid = enclaveId; launched = 1; return 1; } return 0; } 

First, each wrapper function calls the get_enclave () function, which checks whether the enclave is running, using a static variable. If so, then this function (if necessary) places the enclave ID in the pointer eid . This step is optional because the enclave identifier is also stored in the global variable enclaveID , and you can use it directly.

What happens if the enclave is lost due to a power failure or due to an error that causes a crash? To do this, we check the return value of ECALL: it indicates the success or failure of the operation itself ECALL, and not the function called in the enclave.

 sgx_status = ve_initialize(enclaveId, &vault_rv); 

The return value of the function called in the enclave, if any, is passed through a pointer provided as the second argument to ECALL (these function prototypes are automatically created by the Edger8r program). You should always check the return value of an ECALL. Any result other than SGX_SUCCESS indicates that the program could not successfully enter the enclave, and the requested function was not started. (Note that we also defined sgx_status as a global variable. This is another simplification due to the single-threaded architecture of our application).

We will add a function that analyzes the error returned by the ECALL function and checks the state of the enclave (lost, crash):

 static int lost_enclave() { if (sgx_status == SGX_ERROR_ENCLAVE_LOST || sgx_status == SGX_ERROR_ENCLAVE_CRASHED) { launched = 0; return 1; } return 0; } 

These are correctable errors. In the upper levels, there is no logic to cope with these conditions, but we provide it in the EnclaveBridge DLL to support further program development.

Also note the lack of a function to destroy the enclave. While the password manager application is open on the user's system, there is an enclave in memory, even if the user has locked his vault. This is not the right way to work with enclaves. Enclaves consume resources from a pool that is far from unlimited, even when idle. We will deal with this problem in a subsequent release of this series when we talk about sealing data.

Enclave Definition Language


Before turning to the device of the enclave, let's talk a little about the syntax of the Enclave Definition Language (EDL). The enclave bridge functions, both ECALL and OCALLs, have prototypes in the EDL file with the following general structure:

 enclave { // Include files // Import other edl files // Data structure declarations to be used as parameters of the function prototypes in edl trusted { // Include file if any. It will be inserted in the trusted header file (enclave_t.h) // Trusted function prototypes (ECALLs) }; untrusted { // Include file if any. It will be inserted in the untrusted header file (enclave_u.h) // Untrusted function prototypes (OCALLs) }; }; 

ECALL prototypes are in the trusted part, and OCALL are in the untrusted part. The EDL syntax is similar to the C syntax, and the EDL function prototypes are very similar to, but not identical to, the C function prototypes. In particular, the parameters of the bridge function and return values ​​are limited to some fundamental data types, and the EDL includes additional keywords and syntax for defining enclave behavior. The Intel Software Guard Extensions (Intel SGX) SDK User Guide describes the EDL syntax and tutorial on how to create an example enclave exclusively and exclusively. We will not repeat everything that is written there, but simply discuss the elements of this language related to our application.

When parameters are passed to the enclave functions, they are placed in the protected memory space of the enclave. For parameters passed as values, no additional action is required, since the values ​​are placed in the protected enclave stack, as for calling any other functions. For pointers, the situation is completely different.

For parameters passed as pointers, the data referenced by the pointer must be passed into and out of the enclave. Boundary procedures that perform this data transfer must “know” two things:

  1. In which direction should the data be copied: to the bridge function, from the bridge function or in both directions?
  2. What is the size of the data buffer referenced by the pointer?

Pointer direction


When providing the function of an index parameter, you must specify the direction using keywords in square brackets: [in], [out] or [in, out], respectively. The meaning of these keywords is shown in table 1.
DirectionECALLOCALL
inThe buffer is copied from the application to the enclave. Changes will only affect the buffer inside the enclave.The buffer is copied from the enclave to the application. Changes will only affect the buffer outside the enclave.
outThe buffer will be allocated inside the enclave and initialized with zero values. It will be copied to the original buffer when the ECALL exits.The buffer will be allocated outside the enclave and initialized with zero values. This untrusted buffer will be copied to the original buffer when the OCALL exits.
in, outData is copied back and forth.Just like in ECALL.
Table 1. Direction parameters of pointers and their values ​​in ECALL and OCALL.

Note that the direction is relative to the called function of the bridge. For the function ECALL, [in] means “copy the buffer to the enclave”, but for OCALL the same parameter means “copy the buffer to an untrusted function”. (There is also a user_check parameter that can be used in place of them, but it does not belong to the subject of our discussion. For information on its purpose and use, see the SDK documentation.)

Buffer size


Boundary procedures calculate the total buffer size in bytes as follows:

   = element_size * element_count 

By default, for boundary procedures, the value of element_count is 1, and element_size is calculated based on the element referenced by the pointer parameter, for example, for an integer pointer, element_size will be:

 sizeof(int) 

For a single element of a fixed data type, such as int or float, no additional information is required in the prototype of the EDL function. For the void pointer, you must specify the size of the element, otherwise an error will occur during compilation. For arrays, char and wchar_t strings, and other types where the data buffer exceeds one element, you must specify the number of elements in the buffer, otherwise only one element will be copied.

Add count or size (or both) to the keywords in square brackets. They can be given a constant value or one of the parameters of the function. In most cases, the count and size functionality is the same, but it is recommended to use them in the right context. Strictly speaking, size should be specified only when passing a void pointer. In other cases, you should use count.

When passing a string C and wstring (char array or wchar_t array with terminating NULL), you can use the string or wstring parameter instead of count or size . In this case, the boundary procedures will determine the size of the buffer, getting the length of the string directly.

 function([in, size=12] void *param); function([in, count=len] char *buffer, uint32_t len); function([in, string] char *cstr); 

Please note that you can use string or wstring only if the direction is [in] or [in, out]. If only the [out] direction is specified, the string has not yet been created, so the boundary procedure cannot get the size of the buffer. If you specify [out, string], an error will occur during compilation.

Bridge Shell and Functions


You can now define the shells and functions of the bridge. As mentioned above, most of our ECALLs are just shells of class methods in Vault . The class definition for public member functions is shown below:

 class PASSWORDMANAGERCORE_API Vault { // Non-public methods and members ommitted for brevity public: Vault(); ~Vault(); int initialize(); int initialize(const char *header, UINT16 size); int load_vault(const char *edata); int get_header(unsigned char *header, UINT16 *size); int get_vault(unsigned char *edate, 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(UINT32 idx, char *mbname, UINT16 *mbname_len, char *mblogin, UINT16 *mblogin_len, char *mburl, UINT16 *mburl_len); int accounts_get_password(UINT32 idx, char **mbpass, UINT16 *mbpass_len); 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; } }; 

There are several problem functions in this class. Some of them are obvious: for example, this is the constructor, destructor and overload for initialize () . These are the C ++ components that we need to call using the C functions. Some problems are not as obvious as they are inherent in the device functions. Some of these problematic methods were incorrectly created on purpose so that we could look at certain problems in this tutorial, but other methods were incorrectly created without any far-reaching goals, it just happened. We will solve these problems sequentially by submitting both prototypes for shell functions and EDL prototypes for proxy / bridge procedures.

Constructor and destructor


In the non-Intel SGX code branch, the Vault class is a member of PasswordManagerCoreNative . This is not possible in the Intel SGX code branch. However, an enclave may include C ++ code if the bridge functions themselves are functions of C.

Since we have limited the enclave to one thread, the Vault class can be made a static global object in the enclave. This greatly simplifies the code and eliminates the need to create bridge functions and logic to create instances.

Overloading the initialize () method


There are two prototypes of the initialize () method:

  1. The no argument method initializes the Vault object for a new password store with no content. This is the first-time password repository.
  2. The two-argument method initializes the Vault object from the vault file header. This is the existing password repository that the user opens (and then tries to unlock).

This method is divided into two shell functions:

 ENCLAVEBRIDGE_API int ew_initialize(); ENCLAVEBRIDGE_API int ew_initialize_from_header(const char *header, uint16_t hsize); 

The corresponding ECALL functions are defined as:

 public int ve_initialize (); public int ve_initialize_from_header ([in, count=len] unsigned char *header, uint16_t len); 

get_header ()


This method has a fundamental problem. Here is the prototype:

 int get_header(unsigned char *header, uint16_t *size); 

This function performs the following tasks:

  1. It receives a header block for the storage file and places it in the buffer pointed to by the header. The caller must allocate enough memory to store this data.
  2. If you pass the NULL pointer in the header parameter, then for uint16_t, to which the pointer is pointing, the header block size is set, so the calling method knows how much memory needs to be allocated.

This is a fairly common compression technique in some programmer communities, but for enclaves, a problem arises: when passing a pointer to ECALL or OCALL, the boundary functions copy the data referenced by the pointer into or out of the enclave (or in both directions). These boundary functions need the size of the data buffer to know how many bytes to copy. In the first case, a valid pointer with a variable size is used, which is not difficult, but in the second case, we have a NULL pointer and a size equal to zero.

One could come up with an EDL prototype for the ECALL function, in which it would all work, but usually clarity is more important than brevity. Therefore, it is better to divide the code into two ECALL functions:

 public int ve_get_header_size ([out] uint16_t *sz); public int ve_get_header ([out, count=len] unsigned char *header, uint16_t len); 

The wrapper function of the enclave will provide the necessary logic so that we do not need to change other classes:

 ENCLAVEBRIDGE_API int ew_get_header(unsigned char *header, uint16_t *size) { int vault_rv; if (!get_enclave(NULL)) return NL_STATUS_SGXERROR; if ( header == NULL ) sgx_status = ve_get_header_size(enclaveId, &vault_rv, size); else sgx_status = ve_get_header(enclaveId, &vault_rv, header, *size); RETURN_SGXERROR_OR(vault_rv); } 

accounts_get_info ()


This method works like get_header () : passes a NULL pointer and returns the size of the object in the corresponding parameter. However, this method is not distinguished by grace and convenience due to the set of arguments parameters. It is better to divide it into two shell functions:

 ENCLAVEBRIDGE_API int ew_accounts_get_info_sizes(uint32_t idx, uint16_t *mbname_sz, uint16_t *mblogin_sz, uint16_t *mburl_sz); ENCLAVEBRIDGE_API int ew_accounts_get_info(uint32_t idx, char *mbname, uint16_t mbname_sz, char *mblogin, uint16_t mblogin_sz, char *mburl, uint16_t mburl_sz); 

And two related ECALL functions:

 public int ve_accounts_get_info_sizes (uint32_t idx, [out] uint16_t *mbname_sz, [out] uint16_t *mblogin_sz, [out] uint16_t *mburl_sz); public int ve_accounts_get_info (uint32_t idx, [out, count=mbname_sz] char *mbname, uint16_t mbname_sz, [out, count=mblogin_sz] char *mblogin, uint16_t mblogin_sz, [out, count=mburl_sz] char *mburl, uint16_t mburl_sz ); 

accounts_get_password ()


This is the most problematic code in the entire application. Here is the prototype:

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

The first thing that catches your eye: it passes a pointer to a pointer to mbpass. This method allocates memory.

Clearly this is not the best idea. No other method in the Vault class allocates memory, so it is internally inconsistent, and the API violates the convention because it does not provide a method to free memory on behalf of the caller. At the same time, there is also a problem peculiar only to enclaves: the enclave cannot allocate memory in untrusted space.

This can be handled in a wrapper function. , ECALL, , Vault , PasswordManagerCoreNative . : , — , . PasswordManagerCoreNative , - ( Intel SGX).

 ENCLAVEBRIDGE_API int ew_accounts_get_password_size(uint32_t idx, uint16_t *len); ENCLAVEBRIDGE_API int ew_accounts_get_password(uint32_t idx, char *mbpass, uint16_t len); 

EDL :

 public int ve_accounts_get_password_size (uint32_t idx, [out] uint16_t *mbpass_sz); public int ve_accounts_get_password (uint32_t idx, [out, count=mbpass_sz] char *mbpass, uint16_t mbpass_sz); 

load_vault()


load_vault() . :

 int load_vault(const char *edata); 

Vault . Vault , , .

, . ECALL, , , . .

- :

 ENCLAVEBRIDGE_API int ew_load_vault(const unsigned char *edata); 

ECALL , EDL:

 public int ve_load_vault ([in, count=len] unsigned char *edata, uint32_t len) 

, - . ECALL.

 ENCLAVEBRIDGE_API int ew_load_vault(const unsigned char *edata) { int vault_rv; uint32_t dbsize; if (!get_enclave(NULL)) return NL_STATUS_SGXERROR; // We need to get the size of the password database before entering the enclave // to send the encrypted blob. sgx_status = ve_get_db_size(enclaveId, &dbsize); if (sgx_status == SGX_SUCCESS) { // Now we can send the encrypted vault data across. sgx_status = ve_load_vault(enclaveId, &vault_rv, (unsigned char *) edata, dbsize); } RETURN_SGXERROR_OR(vault_rv); } 


, PasswordManagerCoreNative wchar_t char. , wchar_t?

. Windows wchar_t API- Win32, UTF-16 . UTF-16 16 : , ASCII, , , . UTF-16 , 16 , ASCII.

, ASCII. , . Tutorial Password Manager , .NET, UTF-8 . UTF-8 — , 8 . ASCII UTF-16 ASCII. , UTF-8 , UTF-16, , , .

( , , ).

Code example


As mentioned above, this section provides sample code for download . The attached archive includes the source code for the Tutorial Password Manager Bridge DLL and the enclave DLLs. The functions of the enclave are just stubs for now, they will be filled in the fifth part.

In further releases


In the fifth part of this tutorial, we will complete the creation of the enclave, for which we will transfer the Crypto, DRNG and Vault classes to the enclave and combine them with ECALL functions. Follow the news!

Source: https://habr.com/ru/post/318628/


All Articles