SGX学习

一、什么是SGX?

  • intel SGX被设计用于保护应用程序的机密信息,使其抵御恶意软件的攻击 ,即使在应用程序、操作系统和BIOS都不可信的情况下也是如此。哪怕攻击者获得了整个平台的控制权,应用程序的机密信息依然可以收到保护。

  • 什么是机密信息?

    • 机密信息是不想被其他人看到的任何信息,比如医疗数据、个人识别信息、生物识别信息、密码、密钥以及知识产权等。这样的机密信息必须得到保护,以捍卫个人或企业的隐私、经济利益,甚至包括存亡。

    • 为了保护机密信息,Intel SGX在内存中划定了名为安全区的隔离区域,用来存放代码和数据。这些不可寻址的分页内存是从系统的物理内存中保留的,并且经过了加密;安全区是受保护的内存区域,应用程序可以在里面处理它的机密数据而不必担心泄露它们。

  • 包含了Intel SGX技术的应用程序分为两个部分,可信部分和不可信部分。当应用程序需要处理机密时,它会创建一个位于可信内存的安全区,然后调用可信函数(它是由开发者创建的、专门在安全区内执行的函数),一旦这个函数被调用,应用程序就会在可信区域执行并明文访问安全区内的代码和数据,除此之外,试图从安全区外访问安全区内存的行为都会被处理器拒绝,即使是系统特权用户的行为,这种机制使得安全区内的机密不会被泄露。当可信函数执行完毕后,安全区内的数据依然保留在可信内存中,而应用程序返回到不可信区域继续执行,并且失去了对可信内存的访问权限。

二、检测并开启Intel SGX的方法

  • 在应用程序可以使用Intel SGX之前,需要满足以下四个条件:

    • CPU必须支持Intel SGX的指令

    • BIOS必须支持Intel SGX

    • 必须在BIOS设置中开启Intel SGX

    • 必须安装Intel software guard extensions platform software,简称Intel SGX PSW

  • Intel SGX随着第6代酷睿处理器和志强E3 v6 服务器处理器系列的发行一同推出。在BIOS设置中,关于Intel SGX可能有三个选项:

    • 启用:意味着严格开启了Intel SGX

    • 禁用:意味着严格关闭了Intel SGX

    • 软件控制:允许包含Intel SGX技术的应用程序在系统重启后启用Intel SGX,让用户不需要进入BIOS设置屏幕(对于非技术用户而言,设置BIOS是一个艰巨且令人困惑的任务)

    并非所有的BIOS厂商都实现了以上三个选项,有些厂商甚至不提供关于Intel SGX的选项。

    注意:即便CPU支持这一特性,Intel SGX也不一定能立即供应用程序使用,应用程序及其安装程序需要在运行时检测Intel SGX的可用性,并根据系统的设置采取合适的措施。让应用程序足够健壮是及其必要的,即使这个程序在没有Intel SGX的情况下完全无法运行,也应当优雅地退出,并弹出Intel SGX不可用的提示框(而不能直接崩溃)。经过正确编写的Intel SGX 应用程序在运行时,绝不能仅仅因为缺少Intel SGX的支持而崩溃。

  • 安装程序应当按以下顺序检查:

    • 检查CPU和BIOS是否支持Intel SGX

    • 检查Intel SGX PSW是否安装,如果没有安装,则安装它

    • 检查Intel SGX是否已在BIOS中开启,如果没有,尝试用软件控制开启

  • 应用程序本身检查的顺序和安装程序的略有不同:

    • 确定是否已安装平台软件

    • 检查Intel SGX PSW是否安装,如果没有安装,则安装它

    注意:如果以上步骤中的任何一个失败了,说明系统不支持Intel SGX。如果应用程序无法在脱离Intel SGX的情况下运行,就应该向用户报告这个错误。正确检测Intel SGX的步骤在相关的应用程序中是至关重要的,它比只检查CPU是否支持相关的指令要复杂得多。

三、如何设计Intel SGX应用程序

  • 每个安全区都是独一无二的,所有的Intel SGX应用程序都具有相同的基本结构,这意味这,设计时需要遵循一些通用的原则。Intel SGX应用程序分为两个部分:

    • 可信部分:由它的安全区组成,一个应用程序可以拥有一个或多个安全区。安全区存储在加密的内存中,并受Intel SGX的保护。安全区是可信的,因为他们一旦被建立就不能被篡改,如果安全区的数据被恶意用户或恶意软件修改,这些行为会被CPU检测到,然后安全区就不会被加载。

    • 不可信部分:即不被Intel SGX保护的应用程序或内存区域。

  • 设计一个Intel SGX应用程序要求我们把代码分成不同的部分,我们需要判断哪些代码应该放到安全区之内,哪些代码放到安全区之外,以及可信部分和非可信部分的交互,划分代码的关键步骤如下所示:

    • 确定机密信息:机密信息就是不希望在处理它的应用程序之外被看到的信息

    • 确定机密数据的提供者和使用者:我们需要画出机密数据在应用程序各个组件中的流程图,也就是机密数据的来源和去向

    • 确定安全区界。划定边界时,我们的目标是包含尽可能多的机密数据,并尽可能地减少与非信任代码的交互。然而,我们不应该把不必加密的数据划分为机密数据。与小体积的安全区相比,大体积安全区调试起来更麻烦,击面更大,并且消耗更多资源。

    • 精简安全区的代码,当为安全区和它的接口编写代码时,牢记两点:

      • 单一系统中,用于所有安全区的内存容量是固定的,安全区的体积应该尽可能小,并且在不再需要机密数据之后,安全区应被销毁。

      • 进出安全区的调用会产生性能损失,就和CPU上下文切换一样。通过一次调用完成大量工作,要比分成多次调用来完成等量的工作更有效率。

四、Intel SGX使用的安全区定义语言(EDL)

  • Intel SGX提供的基本的保护措施是安全区的机密数据能且仅能被安全区被的代码访问,而执行安全区代码的唯一的方式就是通过开发人员创建的接口函数,CPU确保这个限制生效。即使是特权用户也无法规避它。

  • 每个安全区可以定义一个或多个E-CALL,它们是非可信应用程序进入安全区的入口点。安全区也可以定义O-CALL,它使得安全区函数可以调用外部的非可信应用程序再返回安全区中。总而言之,这些E-CALL和O-CALL组成了安全区的接口。但这些函数并非直接由非可信应用程序执行,因为对安全区入口点和出口点的访问由处理器严格控制,为了完成出入安全区的转换,需要依次执行特定的CPU指令。

  • Intel SGX SDK把这些底层细节做了抽象,以便让软件开发者可以用熟悉的编程环境进行开发,一个名为Edger8r的特殊工具,自动为你的E-CALL和O-CALL生成代理函数,如此,我们的应用程序可以像调用其他C语言函数一样调用它。为了创建这些代理函数,Edger8r从EDL文件中读取安全区接口的定义。

安全区定义语言(简称EDL)

  • EDL文件和声明函数原型的C语言风格的头文件非常相似,而且无法上也是如此,EDL文件由两部分组成,可信区和非可信区,其中E-CALL定义在可信区,而O-CALL定义在非可信区。

    enclave{
        from "sgx_tstdc.edl" import *;
        
        trusted{
            /*define ECALLs here.*/
            public int my_ecall(int value);
            int ecall_private(int value);
        };
        
        untrusted{
            /*define OCALLs here.*/
            void an_ocall(int p1,int p2);
        };
    };

    这些函数原型的声明非常像C语言,只不过还需要一些额外的关键字,以下挑几个说明:

    • 所有能被非可信程序调用的E-CALL都必须声明为Public。每个安全区至少应有一个声明为Public的E-CALL。Public关键字位于函数名之前,不含public关键字的E-CALL只能被另一个O-CALL执行。

    • 当进行E-CALL或O-CALL调用时,函数参数要从非可信内存和可信内存之间进行封送处理。

      • 若函数参数是值传递,对应的封送处理是单向的,在E-CALL或O-CALL内部进行的更改不会反映到调用的函数上。

        public int my_ecall(int value);
      • 若函数参数是引用传递,我们必须完整地描述数据的封送处理过程,每个指针或者参数之前必须有方括号来描述封送处理的方向以及封送处理的元素数量(某些情况下可省略)。

        public int my_ecall([in] int32_t *value);

        in关键字意味着数据需要封送到E-CALL或O-CALL之内;

        public int my_ecall([out] int32_t *value);

        而out关键字意味着数据需要从E-CALL或O-CALL封送回调用的函数,如果需要out,需要在调用E-CALL或O-CALL之前分配好数据的缓冲区,缓冲区需要足够大,以便容纳数据。

        public int my_ecall([in,out] int32_t *value);

        若同时指定了inout关键字,意味着要进行两个方向的封送处理。

        注意:缺省情况下,Edger8r假定数据的缓冲区大小等于参数的sizeof,在当前示例中,一个32位整数将会被封送处理。

      • 如果指针指向的是一个数组,则需要提供count参数来指定要复制的元素个数。如下所示

        public int my_ecall([in,count=10] int32_t *array);

        在当前示例中,10个32位整数将会被封送处理。如果类似10这个数目要等到运行时才能确定,也可以用另一个函数参数来表示它,如下所示:

        public int my_ecall([in,count=len] int32_t *array,size_t len);
      • 若使用in关键字封送一个以'\0'作为终止符的字符串,我们可以使用string或者是wstring来代替count关键字,如下:

        public int my_ecall([in,wstring] wchar_t *unicodename);

五、Intel SGX 使用的EDL语言的进阶

  • EDL语言允许通过inout关键字来对E-CALL和O-CALL进行封送处理,通过使用inoutcount关键字可以向边界例程提供封送处理的方向以及元素数目,然而,传递指针时,有时需要提供额外的信息,有时还需要直接对指针进行控制。

  • 有时我们需要把void指针传给E-CALL或O-CALL,问题在于边界例程无法知道void类型对应的大小,从而无法计算封送处理需要的数据缓冲区大小。为了解决这个问题,EDL语法提供了size关键字,通过指定单个元素的大小,边界例程就知道要拷贝多少数据了。

    public int my_ecall([in,count=32,size=sz] void *buffer,size_t sz);

    在本例中,32个8字节数据将会被封送到E-CALL,与count关键字类似,也可以用其他函数参数表示长度。

  • 有时我们不希望封送完整的数据缓冲区,而是希望仅把指针指向的地址传给E-CALL或O-CALL,可以通过user_check关键字完成

    public int my_ecall([user_check] void *buffer);

    此种行为传递的指针不进行边界例程的安全检查和数据封送处理,E-CALL或O-CALL将会直接作用于指针的地址。

  • 只有声明为public的E-CALL可以被不安全的函数调用,它们被称为Root E-CALL,由EDL文件中使用的public关键字指定。不含public关键字的E-CALL只能被另一个O-CALL调用。但是,O-CALL不能调用任何的E-CALL,必须通过ALLOW关键字声明O-CALL可以调用哪些E-CALL。

    enclave{
        from "sgx_tstdc.edl" import *;
        
        trusted{
            /*define ECALLs here.*/
            public int my_ecall(int value);
            int ecall_private(int value);
            void update_hash([in,count=len],char *block,size_t len);
        };
        
        untrusted{
            /*define OCALLs here.*/
            void an_ocall(int p1,int p2);
            size_t get_msg_block() allow(update_hash);
        };
    };

    在本例中,O-CALL get_msg_block()只能调用E-CALL update_hash(),即使声明为public的E-CALL函数也不能调用。

六.如何调试Intel SGX 应用程序

  • Intel SGX安全区可以以Debug模式或Release模式构建。

    • Debug模式下的安全区是可调试的。使用Intel SGX调试器附加到上面,查看当前的状态以及对代码进行逐句调试,就和调试其他程序一样。当我们在调试Intel SGX应用程序时,它很可能是以Debug模式构建的,CPU允许运行任何以Debug模式构建的安全区,但是由于我们可以用Intel SGX调试器附到安全区上面,这些安全区显然不是安全的永远不要在生产环境上部署以Debug模式构建的安全区

    • Release模式构建的安全区无论如何也无法进行调试,这条限制由CPU严格执行。

    然而,对Intel SGX安全区的调试,仍然需要相关的CPU指令支持,这意味着我们必须使用随Intel SGX SDK安装的Intel SGX调试器进行调试,其他的调试器只会跳过安全区的代码。

七、how to seal data in intel SGX

  • 有时,我们的Intel SGX应用程序可能需要把机密数据保存到安全区之外。然后,安全区从本质上来说是无状态的,当电脑进入睡眠、关机状态或者应用程序退出时,安全区即被销毁。当然,当应用程序主动将安全区销毁时,安全区也被销毁。安全区被销毁后,所有的内容都会丢失。为了持久保存安全区内存储的数据,我们必须显示地将其发送到安全区之外的非可信内存中,但是一个很显然的问题是,非可信内存是不可信的,它可能被恶意用户窃听甚至篡改。为了解决这个问题,Intel SGX提供了名为数据密封的功能,当数据被密封时,数据在安全区内用密钥加密,这个密钥是直接从CPU中获取的,这个加密的数据块,称之为密封的数据。它只能在创建它的那台电脑上进行解密,或者叫解封。这个加密过程本身可以保证数据的机密性,完整性和真实性。密封数据时,您需要从以下两种密钥策略中选择一种(不用的策略影响密钥的生成):

    • 安全区标识(MRENCLAVE):生成一个该安全区独有的密钥,这意味着只有用一台电脑的同一个安全区可以解封数据。

    • 密封标识(MRSIGNER):基于安全区密封授权方的密钥生成一个密钥,这使得一个安全区的数据可被另一个安全区解封,只要是同一台电脑,而且软件由相同开发商开发(密封授权方的密钥相同)即可。在该策略下,我们开发的旧版应用程序所密封的数据可被新版应用程序或者其他构建版本的应用程序所读取。我们可以应用这种方法在两个不同的应用程序的两个不同的安全区中共享数据。

    这两种策略的共同点是密封数据的电脑的指纹是密钥的一个输入,这意味着如果密封的数据被复制到了另外一台电脑上,则无法被解封。哪怕第二台电脑上运行了完全一样的应用程序也是如此。

    密钥的另一个输入是安全区的debug模式或release模式,debug的安全区无法解封release模式的安全区密封的数据,反之亦然。这项安全机制被用于防止Intel SGX调试器在调试debug的安全区时泄露来自release模式的安全区的数据。

    警告:密封数据可能具有十分严重是安全隐患,安全区并不会检验非可信应用程序的真实性,绝不能假设只有我们的应用程序能加载安全区,或者E-CALL一定按照我们心目中的顺序执行。任何人都可以加载我们的安全区,并按他们希望的顺序执行E-CALL。我们的安全区API绝不能因密封数据和解封数据而泄露了机密或授予了不该授予的权限。

    数据密封是Intel SGX提供的一项重要功能,它可以让我们安全地把机密数据导出到安全区以外。而且确信这些机密数据不会被篡改、替换、窃听,或者当解封数据时,被复制到另一台电脑上。

你可能感兴趣的:(智能硬件)