Windows10下使用Intel SGX功能(三):其他示例分析

参考资料

其他示例详细说明

链接:

  • File-Encryptor
    使用AES Mbed API 对enclave内的数据进行加解密。目前暂时不支持OpenSSL
  • Data-Sealing
    介绍OE中sealing和unsealing特性。
  • Attestation
    介绍 OE证明,即两个enclave之间如何证明。支持SHA和PCKS1。只在SGX-FLC系统上工作
  • Attested TLS
    介绍OE中 认证 TLS channel的机制。只在SGX-FLC系统上工作
  • Switchless Calls
    介绍OE中无开关调用机制。
  • Host-side Enclave Verification
    Host端的enclave证明,即在enclave外如何对SGX enclave进行远程证明。只在SGX-FLC系统上工作
  • Pluggable Allocators
    如何自定义分配器,使得在多线程enclave下获得更好的性能。比如插入一个自定义的分配器(本例中为snmalloc)来替换默认的内存分配器(dlmalloc),以提高多线程enclave的性能。

统一编译方式

mkdir build && cd build
cmake .. -G Ninja -DNUGET_PACKAGE_PATH=C:\oe_prereqs
ninja
ninja run

如果直接 cmake ..,compiler 会选择 x86 的,从而报错。目前还不会调~

File-Encryptor

  • OE SDK的加密库:subset of the open sources mbedTLS
  • 在该示例中使用到的OE API有:
    • mbedtls_aes_setkey_*
    • mbedtls_aes_crypt_cbc
    • mbedtls_pkcs5_pbkdf2_hmac
    • mbedtls_ctr_drbg_random
    • mbedtls_entropy_*
    • mbedtls_ctr_drbg_*
    • mbedtls_sha256_*

该示例中,文件加密过程如下:

加密过程:将一个输入文件分成16字节的块。然后将每个块逐一发送到enclave进行加密,直到遇到最后一个块。并确保最后一个块被填充,使其成为16字节的块,这是enclave使用的AES-CBC加密算法的要求。
解密过程:解密过程与加密过程相反,只是在其initialize_encryptor调用中向enclave中的加密器提供一个加密头,其中包含一个encryption_header_t,该加密头有加密元数据,以便加密器验证其密码并从中检索出加密密钥。

encryption_header_t定义如下:

typedef struct _encryption_header
{
    size_t fileDataSize;
    unsigned char digest[HASH_VALUE_SIZE_IN_BYTES];
    unsigned char encrypted_key[ENCRYPTION_KEY_SIZE_IN_BYTES];
} encryption_header_t;

Data-Sealing

由于enclave一旦被释放,其状态是不会被保存的。可以使用Data Sealing技术保存enclave的状态,以便下次重启时加载。

在enclave状态信息离开 "enclave"之前,它们被加密以保护其不受不信任的主机的影响。数据密封是对enclave的状态进行加密的过程,以便在enclave之外进行持久性存储。

这种加密是使用私人密封密钥进行的,该密钥来自enclave所运行的TEE系统。另一方面,数据解封是一个相反的过程,它使用相同的密封密钥对enclave的密封数据进行解密。这使得enclave的状态可以在后来同一enclave被恢复时得到恢复。

OE没有发布方便的API,例如Intel SGX SDK的sgx_seal_datasgx_unseal_data,它使用预先确定的固定加密算法(AES-GCM)进行加密,而是决定只提供获取密封密钥的通用例程,并将加密算法留给enclave开发者选择他们认为合适的算法。

OE 支持的两种密封密钥类型

  • OE_SEAL_POLICY_UNIQUE
    这种类型的密封密钥是由enclave的测量结果得出的。在这种政策下,被密封的秘密只能由封印它的确切enclave代码的实例来解封。这种政策对应于使用SGX MRENCLAVE身份来推导密封密钥。
  • OE_SEAL_POLICY_PRODUCT
    这种类型的密封密钥来自于enclave的签名者。在这种政策下,被密封的秘密可以由与封印enclave的签署者相同的任何enclave来解封。政策名称中的 "PRODUCT "假定所有由同一签名人签名的enclave都属于同一产品。这个策略对应于使用SGX MRSIGNER和ISVPRODID值来推导密封密钥。

OE 获取密封密钥的API

  • oe_get_seal_key_by_policy
    获得基于当前enclave属性(如其安全版本和调试状态)和已选择的由密封策略指定的身份属性的对称密钥。

  • oe_get_seal_key
    调用oe_get_seal_key来获得一个密封密钥,其属性与之前调用oe_get_seal_key_by_policy返回的keyInfo中的属性相同。这个方法被用来获取密封密钥,以解开之前密封的数据。官方文档建议这样做,因为诸如服务器打补丁等事件会改变用于在oe_get_seal_key_by_policy中导出密封密钥的属性(例如CPUSVN)。所以应该将keyInfo与加密数据一起持久化

测试代码测试流程

  1. 分别使用策略OE_SEAL_POLICY_UNIQUEOE_SEAL_POLICY_PRODUCT对测试数据进行密封到enclave_a_v1,得到sealed_data_t类型的密封数据。
typedef struct _sealed_data_t
{
    size_t total_size;
    unsigned char signature[SIGNATURE_LEN];
    unsigned char opt_msg[MAX_OPT_MESSAGE_LEN];
    unsigned char iv[IV_SIZE]; // 
    size_t key_info_size;
    size_t encrypted_data_len; 
    unsigned char encrypted_data[];
} sealed_data_t;

密封过程:(此部分代码未看到具体的实现 ???

  • seal_data调用oe_get_seal_key_by_policy,使用OE_SEAL_POLICY_UNIQUEOE_SEAL_POLICY_PRODUCT来获得一个唯一的密封密钥及其密封密钥信息。
  • 生成一个初始化向量。
  • 加密输入数据。
  • 分配 sealed_data_t 结构 从 sealed_data_t 结构中生成一个带有密封密钥的签名。
  • 在返回主机之前,用上述信息填充 sealed_data_t 结构。
  1. enclave_a_v1中分别以OE_SEAL_POLICY_UNIQUEOE_SEAL_POLICY_PRODUCT策略解封sealed_data_t数据,得到成功结果。
  2. enclave_a_v2中分别以OE_SEAL_POLICY_UNIQUEOE_SEAL_POLICY_PRODUCT策略解封sealed_data_t数据,得到失败结果。
  3. enclave_b中分别以OE_SEAL_POLICY_UNIQUEOE_SEAL_POLICY_PRODUCT策略解封sealed_data_t数据,得到失败结果。

关于密封过程问题
密封关键代码如下:

// openenclave\enclave\core\seal.c: on_seal(...)

    plugin = _find_plugin(plugin_id);
    if (plugin == NULL)
        OE_RAISE(OE_NOT_FOUND);

    result = plugin->seal(
        settings,
        settings_count,
        plaintext,
        plaintext_size,
        additional_data,
        additional_data_size,
        blob,
        blob_size);

其中 plugin->seal()调用的是Seal plug-in definition中的回调函数 seal。而该回调函数是什么时候被赋值没有弄明白?

// openenclave\enclave\core\seal.h: _oe_seal_plugin_definition
/**
 * Seal plug-in definition
 */
typedef struct _oe_seal_plugin_definition
{
    const oe_uuid_t id;

    oe_result_t (*seal)(
        const oe_seal_setting_t* settings,
        size_t settings_count,
        const uint8_t* plaintext,
        size_t plaintext_size,
        const uint8_t* additional_data,
        size_t additional_data_size,
        uint8_t** blob,
        size_t* blob_size);

    oe_result_t (*unseal)(
        const uint8_t* blob,
        size_t blob_size,
        const uint8_t* additional_data,
        size_t additional_data_size,
        uint8_t** plaintext,
        size_t* plaintext_size);
} oe_seal_plugin_definition_t;

使用 Intel SGX 进行 seal/unseal的更多参考文件如下:

Attestation

该示例展示的是如何在两个enclave之间进行认证,并建立通道进行交互。
注意:与本地SGX功能不同,目前远程SGX功能只在SGX-FLC系统上工作。底层SGX库对端到端远程验证的支持只在SGX-FLC系统上可用。目前还没有计划将这些库移植到SGX1系统或模拟模式。

大多数TEE使用一种特定于该TEE的证明机制。然而,SGX有两个独立的机制用于本地和远程认证

  • Local Attestation

本地认证是指同一TEE平台上的两个enclave在交换信息之前建立对彼此的信任。在开放enclave中,这是通过创建和验证一个enclave的 "英特尔SGX报告 "来实现的。

当 enclave 向平台上其他 enclave 报告身份时,先获取当前的 enclave 的身份信息和属性、平台硬件 TCB 信息,附加上用户希望交互的数据,生成报告结构;然后获取目标 enclave 的报告密钥,对报告结构生成一个 MAC 标签,形成最终的报告结构,传递给目标 enclave,由目标 enclave 验证请求报告身份的 enclave 跟自己是否运行于同一平台.

  • Remote Attestation

远程认证是一个可信计算基(trusted computing base,TCB)的过程,它是硬件和软件的组合,获得了远程enclave/供应商的信任。在Open Enclave中,这是通过创建和验证一个enclave的 "英特尔SGX quote "来完成的。

为了实现远程认证,需要引入一个特殊的引用(quoting)enclave.同一平台 enclave 之间的验证使用的是对称密钥,不适用于远程认证,因此,平台间的认证采用非对称密钥机制.由引用 enclave 创建平台认证的签名密钥EPID(enhanced privacy identification),这个密钥不仅代表平台,还代表着底层硬件的可信度,并且绑定处理器固件的版本,当 enclave 系统运行时,只有引用 enclave 才能访问到 EPID 密钥.

更多信息参考:Intel® SGX: Intel® EPID Provisioning and Attestation Services

安全通信通道
安全地传递秘密需要一个安全的通信渠道,这通常由传输层安全(TLS)保证。
在没有TLS的情况下,建立安全通信通道的几个备选方案是。

  1. 使用已建立的临时私钥进行签名的Diffie-Hellman密钥交换,并在这之后使用对称密钥加密法进行通信。
  2. 在其中一个enclave,例如enclave_a,生成一个短暂的对称密钥,用enclave_b的公钥加密,用你的私钥签名,然后把它发送到enclave_b。这将确保对称密钥只为两个enclave所知,而信任的根源在于远程证明。

示例说明
在一个典型的开放enclave应用中,经常可以看到多个enclave一起工作以实现共同的目标。一旦一个 "enclave "验证了对方是值得信赖的,他们就可以在一个受保护的通道上交换信息,该通道通常提供保密性、完整性和重放保护。
所以该示例展示如何通过使用Open Enclave API oe_verifier_get_format_settings()oe_get_evidence()oe_verify_evidence()来证明两个enclave之间的关系,而不是证明一个enclave到一个远程(主要是云)服务。

为了简化过程,同时又不失去解释证明工作的重点,该示例将host1和host2合并为一个单一的主机,以消除处理两个主机之间通信的额外套接字代码逻辑。

Local Attestation:

Remote Attestation:

Host 端实现
主机进程是驱动enclave应用程序的因素。它负责管理enclave的寿命和调用enclave ECALLs,但应被视为一个不受信任的组件,绝不允许处理用于enclave的明文秘密。

  1. 创建需要相互证明的enclave:enclave_aenclave_b
oe_create_attestation_enclave( enclaveImagePath, OE_ENCLAVE_TYPE_AUTO, OE_ENCLAVE_FLAG_DEBUG, NULL, 0, &enclave);
  1. 要求enclave_a提供证据和公钥。
get_evidence_with_public_key(enclave_a,
                             &ret,
                             format_id,
                             &format_settings,
                             &pem_key, // 持有识别enclave_a的公钥
                             &evidence); // 包含由enclave平台签署的用于远程验证的证据
  1. 要求enclave_b证明(验证)enclave_a的证据。
verify_evidence_and_set_public_key(enclave_a,
                                   &ret,
                                   &format_id,
                                   &pem_key,
                                   &evidence);

enclave_b对``verify_evidence_and_set_public_key()的实现中,它调用oe_verify_evidence(),这将在enclave部分描述,以处理所有平台特有的证据验证操作。如果成功,pem_key`中的公钥将被存储在enclave内,供将来使用。

  1. 重复步骤2和3,要求enclave_a以验证enclave_b
  2. 要求enclave_a生成一个用enclave_b的公钥加密的信息。
generate_encrypted_message(enclave_a, &ret, &encrypted_message, &encrypted_message_size);
  1. 将加密后的信息发送给enclave_b进行解密,并验证解密的信息是否正确。
process_encrypted_message(enclave_b, &ret, encrypted_message, encrypted_message_size);
  1. 释放所有资源。
oe_terminate_enclave(enclave_a);
oe_terminate_enclave(enclave_b);

Enclave 端实现

示例中将enclave2("Attester")证明给enclave1("Verifier")。

  1. 从验证人那里得到一个挑战
    为了进行证明并确保证据是最新的,验证者(enclave1)需要能够构建一个挑战,它希望在证明者(enclave2)生成其证据时使用。(在SGX本地验证中,挑战也包含验证者的身份)。这是通过从验证者enclave调用oe_verifier_get_format_settings()来完成的,其中 format_id 标识了验证机制(例如,SGX本地验证与SGX远程验证)。
oe_result_t oe_verifier_get_format_settings(
    const oe_uuid_t* format_id,
    uint8_t** settings,
    size_t* settings_size);
  1. 从Attesterenclave产生证据
    使用验证者提供的挑战,Attester enclave需要生成验证者可以评估的其可信度的密码学上的强大证据。在样本中,这是通过要求平台产生这种证据来完成的。
    oe_get_evidence()的一个重要特点是,可以将应用的具体数据作为custom_claims_buffer参数传入,以签入证据。
  • 这在SGX中被限制为64字节。正如示例中所说明的,可以通过首先对数据进行散列,然后将其传递给oe_get_evidence()方法,将任意大的数据签入证据。
  • 这对引导enclave和挑战者之间的安全通信渠道非常有用。
    • 在这个例子中,enclave将一个短暂的公钥的哈希值签入其证据中,然后挑战者可以用它来加密对它的回应。
    • custom_claims_buffer的其他使用例子可能是包括一个nonce,或启动一个Diffie-Helman密钥交换。
  1. 验证证据的完整性
    一旦证据生成并传递给验证者,验证者可以调用oe_verify_evidence()来验证证据。
    以英特尔SGX远程认证为例,英特尔SGX quote 是使用英特尔颁发的证书链进行验证的,该证书链只对SGX平台有效。注意:目前,远程证明验证只支持Azure ACC虚拟机,但英特尔将通过Open Enclave SDK更广泛地扩大对它的支持。
    在这一点上,挑战者知道证据来自于在TEE中运行的enclave,而且证据中的信息可以被信任。
  2. 验证Attester Enclave身份
    1. 核实证据的完整性
    oe_result_t oe_verify_evidence(
        const oe_uuid_t* format_id,
        const uint8_t* evidence_buffer,
        size_t evidence_buffer_size,
        const uint8_t* endorsements_buffer,
        size_t endorsements_buffer_size,
        const oe_policy_t* policies,
        size_t policies_size,
        oe_claim_t** claims,
        size_t* claims_length);
    
    1. 在Attester Enclave建立信任
      为了建立对产生证据的enclave的信任,要检查签名者ID、产品ID和安全版本的值,看它们是否是预定义的可信值。一旦建立了enclave的信任,任何附带数据的有效性将通过比较其SHA256摘要和存储在签名证据中的自定义索赔中的哈希值来确保。
      官方建议:
      • 确保enclave的身份与预期值相符。
        • 如果你想与enclave的确切位数身份相匹配,请验证OE_CLAIM_UNIQUE_ID的值。且对enclave的任何补丁都会在未来改变unique_id的要求。
        • 如果想匹配可能跨越多个二进制版本的enclave的身份,请验证OE_CLAIM_SIGNER_IDOE_CLAIM_PRODUCT_ID值。这就是证明样本的作用。
      • 确保enclave的OE_CLAIM_SECURITY_VERSION值与最低要求的安全版本相匹配。
      • 确保OE_CLAIM_CUSTOM_CLAIMS_BUFFER主张中编码的哈希值与任何伴随数据的哈希值相匹配,如样本所示。

Enclave 端的密码学
示例中的attestation/common/crypto.cpp文件说明了如何在enclave内部使用mbedTLS进行加密操作,比如 RSA密钥生成、加密和解密和SHA256散列。
一般来说,Open Enclave SDK提供了对mbedTLS的默认支持,该支持分层在Open Enclave核心运行时之上,有一个小的集成接口,开发者可以将该加密库换成自己使用的加密库。 mbedTLS支持的功能见这里

Attested TLS

注意:该示例目前只能在SGX-FLC系统上运行。

首先需要熟悉:What is an Attested TLS channel

有两种经认证的TLS通道:

  1. TLS两端都是TEE终端;
  2. TLS通道两端只有一个TEE终端。

Attested TLS 示例有以下特点:

  • 展示了两个enclave之间在enclave应用和非enclave应用之间的已认证的TLS功能
  • 在enclave内使用MbedTLS/OpenSSL加密库进行TLS
  • 通过在构建时将环境变量OE_CRYPTO_LIB设置为mbedtlsopenssl来选择MbedTLSOpenSSL加密库(注意,OE_CRYPTO_LIB是区分大小写的)。
  • 如果OE_CRYPTO_LIB没有设置,Mbed TLS将被默认使用(见MakefileCMakeLists.txt)。
  • 要使用基于SymCrypt的支持FIPSOpenSSL,将OE_CRYPTO_LIB设置为openssl_symcrypt_fips
  • 用推荐的密码套件和椭圆曲线配置服务器和客户端(更多细节请参考【Recommended TLS configurations when using OpenSSL】)。
  • 使用以下Enclave API
    • oe_get_attestation_certificate_with_evidence
    • oe_free_attestation_certificate
    • oe_verify_attestation_certificate_with_evidence

该示例有两个部分,第一部分有两个enclave应用程序,一个用于在enclave内托管TLS客户端,另一个用于TLS服务器。

在这个例子的第二部分,有一个普通的应用程序作为非enclaveTLS客户端运行,还有一个enclave应用程序实例化了一个enclave,它承载了一个TLS服务器。

服务端功能

  • Host 部分(tls_server_host)
    • 在通过ecall将控制过渡到enclave之前,先实例化一个enclave
  • Enclave(tls_server_enclave.signed)
    • 调用oe_get_attestation_certificate_with_evidence生成证书
    • 使用MbedTLS/OpenSSL API生成的证书配置TLS服务器
    • 启动TLS服务器并等待客户端连接请求
    • 读取客户端有效载荷并以服务器有效载荷回复
  • 如何启动server实例
../server/host/tls_server_host ../server/enc/tls_server_enc.signed -port:12341

客户端功能

  • Host 部分(tls_client_host)
    • 在通过ecall将控制过渡到enclave之前,先实例化一个enclave。
  • Enclave (tls_client_enclave.signed)
    • 调用oe_get_attestation_certificate_with_evidence来生成证书
    • 使用MbedTLS/OpenSSL API来配置TLS客户端
    • 在配置上述证书作为客户端的证书后
    • 启动TLS客户端并连接到服务器
    • 发送客户端的有效载荷并等待服务器的有效载荷
  • 启动client实例
../client/host/tls_client_host ../client/enc/tls_client_enclave.signed -server:localhost -port:12341

非enclave客户端功能

  • 在这种情况下使用时,这个非enclave客户端被假定为持有秘密的受信任方,只有在服务器被验证后才与服务器共享
  • 通过套接字连接到服务器端口
  • 使用OpenSSL API配置TLS客户端
  • 调用oe_verify_attestation_certificate_with_evidence来验证服务器的证书
  • 发送客户端有效载荷并等待服务器的有效载荷
../client/tls_non_enc_client -server:localhost -port:12341

运行:

  • 使用 Mbed TLS
cmake -G Ninja -DOE_CRYPTO_LIB=mbedtls .. -DNUGET_PACKAGE_PATH=C:\oe_prereqs
  • 使用 OpenSSL
cmake -G Ninja -DOE_CRYPTO_LIB=openssl .. -DNUGET_PACKAGE_PATH=C:\oe_prereqs
  • 使用 FIPS-enabled OpenSSL based on SymCrypt
cmake -G Ninja -DOE_CRYPTO_LIB=openssl_symcrypt_fips .. -DNUGET_PACKAGE_PATH=C:\oe_prereqs

编译时如果提示如下错误

  Could NOT find OpenSSL, try to set the path to OpenSSL root folder in the
  system variable OPENSSL_ROOT_DIR (missing: OPENSSL_CRYPTO_LIBRARY
  OPENSSL_INCLUDE_DIR)

解决方法:需要直接安装OpenSSL,而不是git中的openssl,不然系统找不到,即使添加到path也不行。

循环运行经过认证的TLS服务器
默认情况下,服务器在与客户完成一个TLS会话后退出。-server-in-loop运行时选项改变了这种行为,允许TLS服务器处理多个客户端请求。

.\server\host\tls_server_host .\server\enc\tls_server_enc.signed -port:12341 -server-in-loop

Switchless Calls

该示例演示了如何从enclave内部对主机应用程序进行无开关的ocalls,以及从主机应用程序内部对enclave进行无开关的ecalls。它有以下属性。

  • 解释无开关调用的概念
  • 识别适合无开关调用的情况
  • 演示如何在EDL中把一个函数标记为transition_using_threads,并使用oeedger8r工具来编译它
  • 演示如何配置一个enclave以启用源自该enclave的无开关调用
  • 推荐无开关调用在实践中需要的工作线程数量

什么是无开关调用
在一个enclave应用中,主机将ECALLs变成由其创建的enclave所暴露的函数。同样地,enclave也可以把OCALLs变成由创建enclave的主机暴露的函数。在任何一种情况下,执行都必须从一个不受信任的环境过渡到一个受信任的环境,或者反之亦然。由于转换的成本很高,因为要进行繁重的安全检查,所以让调用无上下文切换可能更有性能优势:调用者将函数调用委托给另一个环境中的工作线程,后者负责调用函数并将结果发布给调用者的真正工作。在被感知的函数调用过程中,调用线程和工作线程都不会离开他们各自的执行上下文。

在调用过程中,调用线程和工作线程需要交换两次信息。当无开关调用开始时,调用者需要将工作(将有关函数调用的信息封装在一个对象中,详见下一节)传递给工作线程。而当调用完成后,工作线程需要将结果传回给调用者。这两个交换都需要同步进行。

虽然无开关调用节省了过渡时间,但它们至少需要一个额外的线程来为调用服务。目前,为调用提供服务的工作线程忙于等待消息,因此会消耗大量的CPU。因此,更多的工作线程通常意味着对CPU核心的更多竞争和更多的线程上下文切换,损害了性能。为了决定是否让一个特定的函数无开关,我们必须权衡相关的成本和节约。一般来说,无开关调用的最佳候选函数是:时间短,因此转换在调用的整体执行时间中占比较高;调用频繁,因此转换时间的节省会增加。

简言之:无开关调用为了节省强安全下host和enclave调用时上下文的开销,创建额外的工作线程来传递信息。

Open Enclave如何支持无开关调用
Open Enclave支持SGX硬件和模拟模式下的同步无开关OCALLsECALLs。当enclave内的调用者进行 无开关OCALL 时,受信任的Open Enclave运行时从函数调用中创建一个job。job对象包括信息,如函数ID、被编入缓冲区的参数,以及用于保存返回值的缓冲区。该job被发布到一个共享内存区域,enclave和主机都可以访问。

一个主机工作线程检查并从共享内存区域检索job。它使用不受信任的Open Enclave运行时,通过解密参数来处理作业,然后分派给被调用者函数,最后将结果转发回受信任的Open Enclave运行时,并进一步转发给调用者。

无开关ECALL 的工作方式类似。job由不受信任的Open Enclave运行时(主机端)创建,并发布到一个共享内存区域。一个enclave工作线程检查并从共享区域中检索job,并通过解密参数来处理job,然后分派给被调用函数,最后将结果转发给不受信任的Open Enclave运行时,并进一步转发给调用者。

如果一个enclave支持多个同时的ECALL,那么就可以从enclave进行多个同时的无开关OCALL。在这种情况下,我们使用多线程的主机工作。Open Enclave允许用户配置为服务无开关OCALL和ECALL而创建多少个工作线程。需要注意的是,太多的工作线程可能会增加线程之间的核心竞争,并降低性能。因此,如果一个enclave启用了无开关调用,Open Enclave将它的工作线程数量限制在指定的enclave线程数量内。

在目前的实现中,官方建议使用下面两个数值中的非零最小值:
1)同时活动的调用者线程数
2)工作线程可能可用的内核数

例如,在一台4核机器上,如果同时活动的调用者线程数为2,并且除了两个进行无开关调用的线程和无开关工作线程外没有其他线程,那么1)和2)都将是2。所以我们建议将主机工作线程的数量设置为2。

如果发生2)为零或负数时,例如,如果主机又启动了两个额外的线程,这些线程预计将与两个调用者线程一起活动,那么工作线程可用的核心数实际上是0,1)和2)的最小值将是0。 在这种情况下,建议将工作线程的数量设置为1,但要确保无开关调用至少由一个线程提供服务。

上述标准可能随着官方对工作线程的行为进行修改后改变。

EDL端修改

  • 加法示例

在该实例中,enclave不知道host加法。它依靠一个主机函数将一个数字递增1,并重复调用它N次,将N加到一个给定的数字上。由于host函数很短,而且经常被调用,所以把它变成一个无开关函数是合适的。

示例中为了比较无开关调用与常规调用的性能。为此定义了两个主机函数的变体:host_increment_regularhost_increment_switchless,前者是一个普通的OCALL,后者是无开关调用。
此外还定义了两个enclave函数enclave_add_N_regularenclave_add_N_switchless,它们分别调用宿主函数host_increment_regularhost_increment_switchless。这两个enclave函数都在一个循环中反复调用其host函数。迭代的次数由参数n决定。

  • 减法示例

同样地,为了演示无开关ECALL,假装host不知道减法。它依靠一个enclave函数将一个数字减去1,并重复调用它N次来从一个给定的数字中减去N。由于enclave函数很短,而且经常被调用,所以把它变成一个无开关函数是合适的。

示例定义了两个无开关的enclave函数enclave_decrement_switchlessenclave_decrement_regular来比较无开关ecalls的性能。
主机函数和enclave函数被定义在EDL文件switchless.edl中,如下所示:

enclave {
    trusted {
        public void enclave_add_N_switchless([in, out] int* m, int n);
        public void enclave_add_N_regular([in, out] int* m, int n);

        public void enclave_decrement_switchless([in, out] int* m) transition_using_threads;
        public void enclave_decrement_regular([in, out] int* m);
    };t

    untrusted {
        void host_increment_switchless([in, out] int* m) transition_using_threads;
        void host_increment_regular([in, out] int* m);
    };
};

函数host_increment_switchless的声明以关键字transition_using_threads结束,表明它在运行时应该被无开关地调用。然而,这是一个尽力而为的指令。如果无开关调用资源不可用,例如,enclave没有被配置为无开关能力,或者主机工作线程忙于服务其他无开关OCALL,Open Enclave运行时仍可能选择退回到传统OCALL。在这个例子中,host_increment_switchless总是被无开关地调用,因为没有同时发生的无开关OCALL。同样地,函数enclave_decrement_switchless的声明以关键字transition_using_threads结束,使其成为一个无开关的ECALL。

host端修改

  1. 配置线程

在这个例子中,主线程将首先从enclave内不断发布无开关的Ocalls。为了服务这些调用,我们将通过将结构的第一个字段(max_host_workers)设置为1来创建一个主机工作线程。由于只有一个线程在进行同步的无开关调用,创建多个工作线程并没有什么好处。在一台至少有两个内核的机器上,调用者和工作线程都有可能在专用内核上不间断地运行,造成较少的争论,从而通过无开关调用实现最大的速度。在拥有两个以上内核的机器上,可以根据前面的建议增加调用者和工作线程的数量。

一旦所有的无开关调用都得到了服务,主线程将返回到主机,然后继续发布无开关生态调用。为了服务这些无开关的ecalls,通过设置结构的第二个字段(max_enclave_workers)为1创建一个enclave工作线程。这时,为无开关ocalls提供服务的主机工作线程将自动进入睡眠状态,因为没有更多的ocalls需要它来处理。就像无开关Ocalls的情况一样,在一台至少有两个核心的机器上,一个enclave工作线程和一个主机调用者线程将导致通过无开关调用实现最大速度。在拥有两个以上内核的机器上,工作线程的数量可以按照前面的建议增加。

oe_enclave_setting_context_switchless_t switchless_setting = {
        1,  // number of host worker threads
        1}; // number of enclave worker threads.
  1. 配置enclave

将host结构地址和设置类型放入要创建的enclave的设置阵列中。尽管只有一个enclave的设置(用于无开关),但我们希望将来能灵活地为enclave增加一个以上的设置(有不同的类型)。

oe_enclave_setting_t settings[] = {{
        .setting_type = OE_ENCLAVE_SETTING_CONTEXT_SWITCHLESS,
        .u.context_switchless_setting = &setting,
    }};
  1. 创建enclave
oe_create_switchless_enclave(
             argv[1],
             OE_ENCLAVE_TYPE_SGX,
             flags,
             settings,
             OE_COUNTOF(settings),
             &enclave);
  1. host 调用 enclave (ECALL)

分别进行普通调用和无开关调用,并打印耗时。

...
result = enclave_add_N_switchless(enclave, &m, n);
...
result = enclave_add_N_regular(enclave, &m, n);

enclave端修改
实现enclave_add_N_switchlessenclave_add_N_regular,进行n次递增加法运算。

Host-side Enclave Verification

该示例展示远程证明功能,参考【Attestation】示例。当应用程序进行主机端enclave验证时,这意味着主机应用程序正试图验证远程enclave的硬件和软件设置,以便应用程序能够确定是否信任远程enclave。

用到了openenclave/host_verify.h头文件。

主机侧验证,需要对以下3个文件进行验证:

  • SGX report
  • 以SGX_ECDSA格式提供的证据
  • SGX certificate
...
oe_verifier_initialize();
...
result = verify_report(report_filename, endorsement_filename);
...
result = verify_evidence(evidence_filename, endorsement_filename);
...
result = verify_cert(certificate_filename);
...
oe_verifier_shutdown();
...

Pluggable Allocators

目前OE SDK 默认的分配器为dlmalloc

自从v0.10版本以来,Open Enclave SDK将snmalloc打包成一个库oesnmalloc,可以按照下面描述的步骤插入其中。它被设计成能在enclave内很好地工作,并被像CCF这样有高吞吐量要求的项目所使用。CCF观察到使用snmalloc有以下性能改进。

CCF关于dlmallocoesnmalloc的性能对比如下:

CCF SmallBank benchmark, 1m transactions, Standard_DC8 VM:

OpenEnclave with dlmalloc:

1 worker thread: 35k Tx/s
2 worker threads: 37k Tx/s
3 worker threads: 29k Tx/s
4 worker threads: 27k Tx/s

OpenEnclave with snmalloc:

1 worker thread: 39k Tx/s
2 worker threads: 77k Tx/s
3 worker threads: 110k Tx/s
4 worker threads: 115k Tx/s
5 worker threads: 143k Tx/s
6 worker threads: 156k Tx/s

插入一个自定义分配器

  1. 为分配器配置堆大小

高性能、线程感知的分配器有最小的内存要求,这些要求可能是恒定的(例如:tcmalloc),也可能是每个enclave线程(snmalloc)。enclave的堆的大小必须被配置为满足这个最低要求。
该示例使用了oesnmalloc,它是snmalloc的一个版本,可以在enclave内工作。oesnmalloc要求每个线程至少有256KB,因此enclave在enclave/allocator_demo.conf中进行了适当配置。

# snmalloc requires at least 256 KB per enclave thread.
# Given 16 enclave threads (NumTCS), this implies
#    minimum heap size = (256 * 1024 * 8) / 4096 = 512 pages.
# The heap size (4096 pages) is well above the minimum requirement,
# and accounts for the large number of allocations performed by
# each enclave thread in the sample.
NumHeapPages=4096
NumTCS=16
  1. 链接分配器.*

分配器必须通过在 oelibcxxoelibcoecore 库之前的链接器行中指定它来插入。这将导致链接器选择可插入的分配器实现,而不是默认的分配器实现。

enclave/CMakeLists.txt中,oesnmalloc因此被指定在oelibcxx库之前。

 target_link_libraries(enclave_custom
     openenclave::oeenclave
     # Specify pluggable allocator library
     openenclave::oesnmalloc
     openenclave::oelibcxx)

使分配器可插入

  1. 让分配器在enclave内编译/工作

该步骤是确保分配器可以被编译为在enclave内使用。这涉及到消除对平台特性的使用,如在enclave内无法使用的mmap

  1. 实现可插拔分配器接口

通过实现openenclave/include/advanced/allocator.h中声明的回调函数,可以使一个分配器变得可插拔。

Pluggable Allocators Design Document描述了可插拔分配器的设计.

开源参考如下例子进行接口实现:

示例说明
示例中创建了两个enclave,一个是使用默认的分配器(enclave_default),一个是使用oesnmalloc(enclave_custom),逐渐增加线程数,直到最大线程,然后通过时间统计分析性能。

edl文件

    trusted {
        public void enclave_thread(
	        uint64_t num_allocations,      // Number of allocations to perform
	        uint64_t max_allocation_size); // Maximum size of each allocated object
    };

enclave 端
enclave 配置如下:

  • enclave/allocator_demo.conf
NumHeapPages=8192
NumTCS=16
  • enclave/CMakeLists.txt
  target_link_libraries(enclave_default
    openenclave::oeenclave
    openenclave::oelibcxx)

  target_link_libraries(
    enclave_custom openenclave::oeenclave
    # Specify pluggable allocator library
    openenclave::oesnmalloc
    openenclave::oelibcxx)

ECALL实现(enclave/enc.cpp
首先分配空间:

    std::queue<void*> allocations;

    // 一个长度为QUEUE_LENGTH (15)项的队列被创建并初始化为NULL
    for (uint32_t i = 0; i < QUEUE_LENGTH; ++i)
        allocations.push(nullptr);

每当一个对象被分配,它就被推到队列中。对象的大小是在0max_allocation_size之间随机选择的。
在一个对象被添加到队列之前,队列中的第一个项目被弹出并释放。以这种方式使用队列,可以确保在一个线程中的特定时间内有QUEUE_LENGTH对象活着。保持多个对象存活反映了现实世界的应用,在这些应用中,许多对象同时存在于内存中。

    for (uint64_t i = 0; i < num_allocations; ++i)
    {
        // Pop item from queue.
        void* ptr = allocations.front();
        allocations.pop();

        // allocate object and add to queue.
        uint64_t bytes = uint64_t(rand()) % max_allocation_size;
        allocations.push(malloc(bytes));

        // Free last popped item.
        free(ptr);
    }

host 端
主机希望将两个enclave的签名版本作为前两个命令行参数传递。它还支持命令行参数--simulate--num-allocations--max-threads--max-allocation-size,以便对基准进行配置。

static void _print_usage_and_exit(const char* argv[])
{
    printf(
        "usage:\n"
        "    %s <default-enclave-path> <custom-enclave-path> "
        "[--simulate] "
        "[--num-allocations <value>] "
        "[--max-threads <value>]"
        "[--max-allocation-size <value>]\n",
        argv[0]);
    exit(1);
}

在每个enclave上,主机调用_run_benchmark函数来执行分配基准。_run_benchmark函数首先创建了enclave,然后启动了多个调用enclave_thread ECALL的线程。它测量并打印出耗费的时间。

    // Launch enclave threads that perform lots of memory allocations and
    // deallocations. Measure and print the elapsed time.
    {
        auto start_time = high_resolution_clock::now();

        vector<thread> threads(num_threads);
        for (size_t i = 0; i < threads.size(); ++i)
            threads[i] = std::thread([enclave]() {
                enclave_thread(enclave, _num_allocations, _max_allocation_size);
            });

        for (size_t i = 0; i < threads.size(); ++i)
            threads[i].join();

        auto end_time = high_resolution_clock::now();
        auto elapsed =
            duration_cast<milliseconds>(end_time - start_time).count();

        printf("    %32s = %4lu milliseconds\n", allocator_name, elapsed);
    }

主机重复该基准,每次都增加线程的数量。这证明了像snmalloc这样的线程感知分配器在分配密集的多线程enclave中如何更好地扩展。

    for (uint32_t num_threads = 1; num_threads <= _max_threads;
         num_threads += 1)
    {
        printf("num-threads = %u:\n", num_threads);
        _run_benchmark(argv[1], "dlmalloc   (default allocator)", num_threads);
        _run_benchmark(argv[2], "oesnmalloc (pluggable allocator)", num_threads);
        printf("\n");
    }

从示例结果中可以看到,oesnmalloc在多线程情况下表现更好。但是不同的机器上表现不同,需要用户自己根据基准进行测试。

posted @ 2023-03-08 10:09  水中墨色  阅读(561)  评论(0编辑  收藏  举报