Enclave Definition Language (EDL)文件用于描述在函数原型中使用的Enclave trusted和untrusted函数和类型。Edger8r Tool会使用该文件为enclave导出(由ECALLs使用)和导入(由OCALLs使用)创建C包装函数。
enclave {
//Include files
//Import other edl files
//Data structure declarations to be used as parameters of the
//function prototypes in edl
trusted {
//Include header files if any
//Will be includedd in enclave_t.h
//Trusted function prototypes
};
untrusted {
//Include header files if any
//Will be included in enclave_u.hhead
//Untrusted function prototypes
};
};
只有当被用作库EDL时,可信块是可选的,并且该EDL将被其他EDL文件导入。然而,不受信任的块总是可选的。
每个EDL文件都遵循这种通用格式:
enclave {
// 一个.edl文件可以可选的import函数从:
// 其他.edl文件
from “other/file.edl” import foo, bar; // selective importing
from “another/file.edl” import *; // import all functions
// include的c头文件,这些头文件将包含在为trusted和untrusted routines生成的文件中
include "string.h"
include "mytypes.h"
// 可选类型定义(struct, union, enum)
struct mysecret {
int key;
const char* text;
};
enum boolean { FALSE = 0, TRUE = 1 };
// ECALL声明(可选)
trusted {
//将包含在enclave_u.h的头文件(可选),将被插入到trusted的头文件中
public void set_secret([in] struct mysecret* psecret);
void some_private_func(enum boolean b); // private ECALL (non-root ECALL).
};
// OCALL声明(可选)
untrusted {
//将包含在enclave_u.h的头文件(可选),将被插入到untrusted的头文件中
“untrusted.h”
//untrusted函数原型,此OCALL不允许调用另一个ECALL。
void ocall_print();
//这个函数可以调用一个函数"some_private_func"。
int another_ocall([in] struct mysecret* psecret)
allow(some_private_func);
};
};
在edl文件中两种类型的C/ c++注释都是有效的,如:
enclave {
include “stdio.h” // include stdio header
include “../../util.h” /* this header defines some custom public types */
};
包含定义类型的C头文件(C struct, union, typedefs等);否则,如果在EDL中引用了这些类型,则自动生成的代码将无法编译。包含的头文件可以是全局的,也可以只属于受信任的函数,也可以只属于不受信任的函数。
全局包含的头文件并不意味着在enclave和不受信任的应用程序代码中包含相同的头文件。在下面的例子中,enclave将使用Intel®Software Guard Extensions SDK中的stdio.h。而应用程序代码将使用主机编译器附带的stdio.h。
当开发人员将现有代码迁移到Intel SGX技术时,使用include指令很方便,因为在这种情况下已经定义了数据类型。类似于其他IDL语言,如Microsoft接口定义语言(MIDL)和CORBA接口定义语言(OMG-IDL),用户可以在EDL文件中定义数据类型,sgx_edger8r将生成一个带有数据类型定义的C头文件。有关EDL中支持的数据类型的列表,请参见基本类型。
enclave {
include “stdio.h” // 全局头文件
include “../../util.h”
trusted {
include “foo.h” // 仅限trusted函数
};
untrusted {
include “bar.h” // 仅限untrusted函数
};
};
下表中列出的标识符保留作为Enclave定义语言的关键字使用。
数据类型 | |||||
---|---|---|---|---|---|
char | short | int | float | double | void |
int8_t | int16_t | int32_t | int64_t | size_t | wchar_t |
uint8_t | int16_t | int32_t | int64_t | unsigned | struct |
union | enum | long | |||
指针参数处理 | |||||
in | out | user_check | count | size | readonly |
isptr | string | wstring | |||
其他 | |||||
enclave | from | import | trusted | untrusted | include |
public | allow | isary | const | propagate_errno | transition_using_threads |
函数调用约定 | |||||
cdecl | stdcall | fastcall | dllimport |
EDL支持以下基本类型:
char、short、long、int、float、double、void、int8_t、int16_t、int32_t、int64_t、size_t、wchar_t、uint8_t、uint16_t、uint32_t、uint64_t、unsigned、struct、enum、union。
它还支持long long和64位long double。
基本数据类型可以使用C修饰符:const, *,[]来修饰。
可以通过包含C头文件来定义其他类型。
EDL定义了几个可以与指针一起使用的属性:
in, out, user_check, string, wstring, size, count, isptr, readonly。
下面的将解释它们中的每一个的作用。
指针应该显式地使用指针方向属性in、out或user_check属性来修饰。[in]和[out]作为方向属性。
direction属性指示可信边例程(可信桥和可信代理)复制指针所指向的缓冲区。为了复制缓冲区内容,可信边缘例程必须知道需要复制多少数据。由于这个原因,direction属性通常后面跟着一个大小或计数修饰符。如果两者都没有提供,且指针为NULL,则可信边例程假定计数为1。在复制缓冲区时,可信桥接必须避免覆盖ECALL中的enclave内存,可信代理必须避免泄漏OCALL中的秘密。为了实现这个目标,作为ECALL参数传递的指针必须指向不可信内存,作为OCALL参数传递的指针必须指向可信内存。如果这些条件不满足,受信任的桥和受信任的代理将分别在运行时报告错误,并且不会执行ECALL和OCALL函数。
您可以使用direction属性来换取性能保护。否则,您必须使用下面描述的user_check属性,并在使用之前通过指针验证从非信任内存获得的数据,因为指针所指向的内存可能会发生意外变化,因为它存储在非信任内存中。但是,direction属性对包含指针的结构没有帮助。在这种情况下,您必须亲自验证和复制缓冲区内容(如果需要的话,还需要递归地)。或者,您可以定义一个可以深度复制的结构。有关更多信息,请参见结构深度复制。
enclave {
trusted {
public void test_ecall_user_check([user_check] int * ptr);
public void test_ecall_in([in] int * ptr);
public void test_ecall_out([out] int * ptr);
public void test_ecall_in_out([in, out] int * ptr);
};
untrusted {
void test_ocall_user_check([user_check] int * ptr);
void test_ocall_in([in] int * ptr);
void test_ocall_out([out] int * ptr);
void test_ocall_in_out([in, out] int * ptr);
};
};
enclave {
trusted {
// 不允许没有direction属性或'user_check'的指针
public void test_ecall_not(int * ptr);
// 不允许有函数指针
public void test_ecall_func([in]int (*func_ptr)());
};
};
针对上面的例子做出一些解释。
对于ECALL
[user_check]:在函数test_ecall_user_check中,指针ptr不会被验证;您应该验证传递给受信任函数的指针。ptr所指向的缓冲区也不会被复制到缓冲区内部。
[in]:在函数test_ecall_in中,将在enclave中分配一个与指针ptr对应的数据类型(int)相同大小的缓冲区。ptr指向的内容(int),将被复制到内部新分配的内存中(开内存同时复制值)。在enclave内执行的任何更改对不受信任的app都是不可见的。
[out]:在函数test_ecall_out中,与ptr对应的数据类型(int)相同大小的缓冲区将被分配到enclave中,但ptr所指向的内容,一个整数值将不会被复制(只开内存不复制值)。相反,它将被初始化为零。ECALL返回后,enclave内的缓冲区将被复制到ptr所指向的外部缓冲区(将enclave中的值带出)。
[in,out]: 在函数test_ecall_in_out中,会在enclave中分配一个相同大小的缓冲区,ptr所指向的内容,一个整数值,将被复制到这个缓冲区中。返回后,encalve缓冲区将被复制到外部缓冲区。(与正常使用的指针类似)
对于OCALL:
[user_check]:在函数test_ocall_user_check中,指针ptr不会被验证;ptr所指向的缓冲区不会被复制到外部缓冲区。此外,如果ptr指向enclave内存,则应用程序不能读取/修改ptr所指向的内存。
[in]:在函数test_ocall_in中,与ptr对应的数据类型(int)相同大小的缓冲区将被分配到app端(untrusted端)。ptr指向的内容,一个整数值,将被复制到外部新分配的内存中(从enclave复制到app)。应用程序执行的任何更改在enclave内都不可见。
[out]:在函数test_ocall_out中,将在app端(untrusted端)分配一个与ptr数据类型(int)大小相同的缓冲区,其内容将初始化为零。在OCALL返回后,enclave外的缓冲区将被复制到ptr所指向的enclave缓冲区。(将app内存复制到enclave中)
[in, out]: 在函数test_ocall_in_out中,将在app端分配一个相同大小的缓冲区,ptr所指向的内容(int),将被复制到这个缓冲区。OCALL返回后,enclave外的缓冲区将被复制到enclave内的缓冲区。(与正常使用的指针类似)
下表总结了包装器函数在使用in/out属性时的行为:
ECALL | OCALL | |
---|---|---|
user_check | 指针不被检查。用户必须执行检查和/或复制。 | 指针不被检查。用户必须执行检查和/或复制。 |
in | 从app复制到enclave中的缓冲区。之后,更改只会影响enclave内的缓冲区。安全,但缓慢。 | 从enclave复制到app的缓冲区。如果指针指向enclave数据,则必须使用。 |
out | ECALL函数将分配一个缓冲区供enclave使用。返回时,该缓冲区将被复制到原始缓冲区(app中的缓冲区)。 | untrusted的缓冲区将由OCALL函数复制到enclave中。安全,但缓慢。 |
in,out | 结合内外行为,数据被来回复制。 | 同ECALL |
EDL无法分析C头文件中的C类型定义和宏。如果指针类型别名为没有星号(*)的类型/宏,则EDL解析器可能报告错误或没有正确复制指针的数据。(意思就是typedef xx* ptr可能会出问题)
在这种情况下,使用[isptr]属性声明类型,表明它是一个指针类型。有关更多信息,请参见用户定义数据类型。
// Error, PVOID is not a pointer in EDL(报错)
void foo([in, size=4] PVOID buffer);
// OK(正常)
void foo([in, size=4] void* buffer);
// OK, “isptr” indicates “PVOID” is pointer type(使用isptr声明PVOID是指针)
void foo([in, isptr, size=4] PVOID buffer);
// OK, opaque type, copy by value
// 实际地址必须在untrusted内存(app)中
void foo(HWND hWnd);
ECALL中的指针
在ECALLs中,可信桥接检查封送结构是否与enclave内存重叠,并自动在可信堆栈上分配空间以保存该结构的副本。然后检查指针参数的完整范围是否与enclave内存重叠。当一个指向带有in属性的不可信内存的指针传递给enclave时,可信桥在enclave内部分配内存,并将该指针指向的内存从外部复制到enclave内存。当一个指向带有out属性的不可信内存的指针传递给enclave时,可信桥在可信内存中分配一个缓冲区,对缓冲区内容进行零操作以清除之前的任何数据,并将指向该缓冲区的指针传递给可信函数。可信函数返回后,可信桥将可信缓冲区的内容复制到不可信内存中。在属性结合时,信任的桥梁在enclave分配内存,复制之前调用内存缓冲区的信任信任函数,一旦信任函数返回,可信桥副本的内容可信不可信的内存缓冲区。复制出的数据量与复制入的数据量相同。
在可信桥返回之前,它释放在带有方向性属性的指针参数的ECALL函数开始时分配的所有可信堆内存。在可信桥返回后,尝试使用由可信桥分配的缓冲区会导致未定义的行为。
注意:当带有带有out属性的指针参数的ECALL返回时,可信桥总是将数据从enclave内存中的缓冲区复制到外部的缓冲区。如果出现故障,必须清除该缓冲区中的所有敏感数据。
OCALL中的指针
对于OCALL,可信代理在外部堆栈上分配内存以传递封送结构,并检查指针参数的完整范围是否在enclave内。当从enclave (OCALL)传递一个具有in属性的可信内存指针时,可信代理在enclave外分配内存,并将该指针指向的内存从enclave内复制到不可信内存。当从enclave (OCALL)传递一个指向具有out属性的可信内存的指针时,可信代理将在不可信堆栈上分配一个缓冲区,并将指向该缓冲区的指针传递给不可信函数。当不可信函数返回后,可信代理将不可信缓冲区的内容复制到可信内存中。在属性结合时,信任代理分配内存外enclave制之前调用内存缓冲区的不可信不可信的功能,和之后untrus-ted函数返回可信代理不可信的缓冲区的内容复制到信任的记忆。复制出的数据量与复制入的数据量相同。
当可信代理函数返回时,它释放在OCALL函数开始处为带有方向属性的指针参数分配的所有不可信堆栈内存。尝试使用受信任代理在返回后分配的缓冲区会导致未定义的行为。
属性:user_check
在某些情况下,由方向属性施加的限制可能不支持跨enclave边界进行数据通信的应用程序需求。例如,缓冲区可能太大,无法装入enclave内存,需要将其分割成更小的块,然后在一系列ECALLs中进行处理,或者应用程序可能需要将一个指针作为ECALL参数传递到可信内存(enclave上下文)。
为了支持这些特定的场景,EDL语言提供了user_check属性。使用user_check属性声明的参数不进行描述的[in]和[out]属性的任何检查。但是,您必须理解将指针传入和传出enclave的风险,特别是user_check属性。您必须确保所有指针检查和数据复制都正确完成,否则可能会危及enclave机密。
缓冲区大小的计算
使用以下属性计算缓冲区大小的通用公式:
总
字
节
数
=
c
o
u
n
t
∗
s
i
z
e
总字节数 = count * size
总字节数=count∗size
属性:size
size属性用于指示用于复制的缓冲区大小(以字节为单位),这取决于方向属性([in]/[out])(当没有指定count属性时)。这个属性是必需的,因为信任桥梁需要知道整个缓冲区作为一个指针传递,以确保它不重叠的enclave内存,并将缓冲区的内容从不可信的内存复制到信任的记忆和/或反之亦然根据方向属性。大小可以是一个整型常量,也可以是函数的一个参数。size属性通常用于void指针。
例子:
正确语法:
enclave{
trusted {
// 拷贝 100 个字节
public void test_size1([in, size=100] void* ptr, size_t len);
// 拷贝 len 个字节
public void test_size2([in, size=len] void* ptr, size_t len);
};
};
不支持的语法:
enclave{
trusted {
// size/count属性必须与指针方向一起使用([in, out])
void test_attribute_cant([size=len] void* ptr, size_t len);
};
};
属性:count
count属性用于指示用于复制的字节数指针所指向的sizeof元素块,这取决于direction属性。count和size属性修饰符也有同样的作用。受信任桥或受信任代理复制的字节数是该参数指向的数据类型的数量和大小的乘积。该计数可以是一个整型常量,也可以是函数的一个参数。size和count属性修饰符也可以组合使用。在这种情况下,可信边缘例程将复制若干字节,这些字节是EDL文件中函数声明中指定的count和size参数(size*count)的乘积。
例子:
enclave{
trusted {
// 拷贝 cnt * sizeof(int) 字节
public void test_count([in, count=cnt] int* ptr, unsigned cnt);
// 拷贝 cnt * len 个字节
public void test_count_size([in, count=cnt, size=len] int* ptr, unsigned cnt, size_t len);
};
};
属性:string
属性string和wstring分别表示参数是一个以NULL结束的C字符串或以NULL结束的wchar_t字符串。为了防止“check first,use later”类型的攻击,可信边缘例程首先在不可信内存中操作,以确定字符串的长度。一旦字符串被复制到enclave中,受信任的桥显式地为NULL终止字符串。为第一步中确定的长度分配在可信内存中的缓冲区的大小以及字符串终止字符的大小。
注意:
string和wstring属性的使用有一些限制:
例子:
支持的语法:
enclave {
trusted {
// 不能单独将 [out] 和 "string/wstring"配合使用
// 可以配合 [in] , [in, out] 使用
public void test_string([in, out, string] char* str);
public void test_wstring([in, out, wstring] char* wstr);
public void test_const_string([in, string] const char* str);
};
};
不支持的语法:
enclave {
include "user_types.h" //内含定义 typedef void const * pBuf2;
trusted {
// string /wstring属性必须与指针方向一起使用
void test_string_cant([string] char* ptr);
void test_string_cant_usercheck([user_check, string] char* ptr);
// string/wstring属性不能单独只与[out]属性一起使用
void test_string_out([out, string] char* str);
// string /wstring属性必须用于char/wchar_t指针
void test_string_out([in, string] void* str);
};
};
在第一个示例中,当string属性用于函数test_string时,使用strlen(str)+1作为将字符串复制入和复制出enclave的大小。额外的字节用于null终止。
在函数test_wstring中,wcslen(str)+1(双字节单位)将用作从enclave中复制字符串的大小。
const关键字
EDL语言接受const关键字,其含义与C标准中的const关键字相同。然而,EDL语言中对该关键字的支持是有限的。它只能与指针一起使用,并作为最外部的限定符。这满足了Intel®SGX中最重要的用途,即检测const指针(指向const数据的指针)与out属性之间的冲突。C标准中支持的其他形式的const关键字在EDL语言中不受支持。