“请问我怎么才能保证 Java 程序内存中密码的安全呢?”如果你也过有类似的问题,并且在网上搜到一些并不十分完善却又无可奈何的答案,说明你就是 Java 程序安全性问题的 stakeholder。
这个问题的标准答案是 Java 机密计算技术,它将机密计算技术引入 Java 的世界,为 Java 程序的安全性带来了重大的提升。基于此,龙蜥社区云原生机密计算 SIG 推出了 Java 机密计算的具体实现技术——Teaclave Java TEE SDK, 以下简称 Teaclave Java。该技术具有以下显著优点:
全场景安全性。当用户有机密计算硬件支持时,Teaclave Java 可以实现最高安全等级的 Java 可信计算;当用户没有相关硬件时,退化为安全沙箱隔离级别的可信计算,亦可有效保护用户的敏感数据和计算过程的安全性。
开发和构建简单。基于 SPI 的纯 Java 编程模型,一键式构建,将 Java 机密计算开发构建门槛一降到底。
Teaclave Java 已经过企业级内部场景的验证,在 Apache 社区开源。描述本技术的论文由龙蜥社区云原生机密计算 SIG 与上海交通大学、大连理工大学合作发表在软件工程顶会 ICSE 2023(https://conf.researchr.org/home/icse-2023)上,并且获得了本届会议的 ACM SIGSOFT 杰出论文奖。这是 2020 年以来,龙蜥社区云原生机密计算 SIG、上海交通大学、大连理工大学首次获此殊荣。
问题的本质
这个问题的本质是如何在具有风险的运行时环境中安全地使用敏感数据。当我们在运行时将密码解密后,密码就会以明文的形式存在于内存中,也就是 Java 的堆上,如图 1 的左半部分所示。如果系统遭受攻击,比如 2021 年声名大噪的 log4j 漏洞攻击,Java 堆上的内容就会被窃取。即便没有被攻击,在为了性能诊断而做 heap dump 时也有可能主动将敏感信息泄漏出去。所以将诸如密码这样的安全敏感信息暴露在普通运行环境具有高度的风险。
一种保护思路是尽可能地缩短明文密码在内存中的存放时间,以缩短敏感信息暴露的时间窗口。
如图 1 右半部分所示,在使用完密码后,及时将其从内存中销毁,这样会比先前更加安全一些。因为密码是文本信息,会用字符串类 java.lang.String 保存。Java 的 String 是一种 immutable 类型,创建后不能更改内容,所以没有可以重置内容的 API。要销毁密码只能通过反射将 String 类内部保存字符内容的数组内容置空,从而将密码内容从 Java 堆上抹去。直接将密码字符串设置为 null 是没有用的,这样只是把 String 变量的指针设为空,对于 Java 堆上的密码数据没有任何影响,只有等到下次垃圾回收时才有可能将密码数据从堆中清除。
另一种方法是用 char 数组保存密码,而不是 String 类,这样就不必调用反射,让销毁更加便利。还有一种方法是用 byte 数组保存密码,因为其明文是字符编码而非人可读的字符,所以会更难被人看懂。
这些就是目前可以从网络上搜到的解决方法,本文将它们称为“朴素”的 Java 密码保护方案。因为这些方案只是缩短了明文密码在 Java 堆上的生存时间,并没有真正将明文密码保护起来。而且“及时”一词有很大的弹性,开发人员未必能准确地判断出何时才是及时。
更具有典型性的案例就是著名的 log4j 漏洞问题(https://nvd.nist.gov/vuln/detail/CVE-2021-44228)。攻击者可以利用 log4j 2.14 的漏洞将恶意 class 文件上传到服务器并通过 Java 的动态类加载机制运行,从而窃取 Java 堆中保存的服务器私钥。有了私钥,服务器与客户端之间的所有通信内容对于攻击者都如同明文了。
在以上两个例子中,需要在运行时保护的密码和密钥都是安全敏感的数据。而在实际场景中,保护范围并不仅限于敏感数据,还有可能扩大到运算过程。比如鉴权认证场景中,需要保证认证的过程可信,不能被攻击者篡改。再比如云服务的用户将自己的算法部署上云时,虽然部署的制品可以加密,以保护传输和存储时的安全,云厂商提供了固若金汤的安全防护以免受外部攻击,但是用户依然会担心云厂商有没有在运行时窥探用户的计算过程,是否存在监守自盗的可能。
由此可见,保护 Java 应用中的安全敏感数据和运算并不是一件遥远的需求,而是具有迫切的现实意义的需求。对于云计算的供应商,让用户相信其敏感数据和运算对于云厂商自己也是不可见的黑盒亦具有重大的商业价值。
Java 机密计算现状
保护运行时的敏感数据并不是一个新鲜话题,而属于迄今已发展了 20 多年的技术——机密计算的一部分。机密计算是一种提供硬件级的系统隔离,以保障数据安全和程序运行安全的技术。机密计算将执行环境划分为富执行环境(Rich Execution Environment,REE)和可信执行环境(Trusted Execution Environment, TEE),认为 REE 和 TEE 应该相互隔离,TEE 需要通过硬件加密以保证外界无法知晓其中的内容。安全敏感的内容应该放在 TEE 中运行,其他内容则在 REE 中执行。
这套机制早在 1999 年就已提出,不过早期的硬件加密技术能力有限,仅有支持执行加解密程序的 TPM(可信任的平台模块,Trusted Platform Module)硬件。2008 年 Arm 发布 TrustZone 技术白皮书,以支持 Arm 平台的通用型机密计算任务。2015 年 Intel 也推出了带有支持通用应用加密的 SGX(软件保护扩展,Software Guard Extension)芯片的硬件设备,2021 年 SGX 升级为可支持 1T 内存、具有更高性能的 SGX2。
机密计算的核心理念是在具有被攻击风险的运行时环境中提供一块安全区域供安全敏感程序运行,实现了安全敏感数据和程序在传输、存储和计算全流程的安全可信。目前机密计算在隐私安全、区块链、多方计算、IoT 和边缘设备,以及个人计算设备上均有广泛的应用和广阔的前景。
看起来机密计算技术正是解决 Java 程序安全性问题的标准答案,那么我们是否能够在 Java 应用中应用机密计算技术呢?
Occlum – 在 TEE 中放入 JVM 和应用整体
SGX、TrustZone 等为通用型机密计算提供了硬件基础,Intel、微软等开源的驱动和 SDK 则为通用型机密计算提供了软件基础。基于这些软硬件基础,开发者已经可以在软件应用中使用机密计算。但是机密计算对于 Java 应用并不友好,因为 TEE 中只能运行 native 程序,所以 Java 程序并不能直接运行于 TEE 中。要在 TEE 中运行 Java 程序,就必须先在 TEE 中启动一个 JVM,然后在 JVM 上执行 Java 程序。那么是否能在 TEE 中运行 JVM 呢?答案是肯定的,那就是 Occlum,其原理如图 2 所示。
Occlum 是介于 TEE 底层 SDK 与 JVM 之间的一层 LibOS,作为操作系统支持普通 JVM 在 TEE 中的运行。用户将包含了机密代码在内的整个 Java 程序部署在 TEE 中,由 Occlum 支持 JVM 执行。图 2 右半部分给出了部署的结构,其中黄色的 APP 代表整个 Java 应用及其所需三方库,红色圆圈代表可信代码。应用通过 REE 中的启动器——通常只是一个很小的命令行工具,启动执行。这种方案的兼容性好,用户基本不需要修改原有代码即可获得机密计算支持。但是缺点也很明显——放入 TEE 的代码太多,会导致两个问题:
安全性下降。原本需要在 TEE 中执行的可信程序可能并不多,但是此方案需要将所有的 Java 程序、三方库、JVM 和 LibOS 全部放入 TEE,导致 TCB(可信计算基,Trusted Computing Base)太大,安全性并不理想。TCB 是安全领域衡量安全性的重要指标,指信任的代码量。TCB 越大,其中可能存在安全隐患的代码就越多,程序的安全性就越差,所以 TCB 越小越好。以 log4j 攻击为例,Occlum 仍然无法对其免疫。因为 log4j 库与机密代码并没有被分隔在不同的执行环境中,而都部署在 TEE 中,所以攻击者上传的恶意类文件也会位于 TEE 中,仍然可以从内存中访问到私钥。
性能下降。TEE 的硬件不是通用硬件,与 REE 相比存在性能退化,所以将应用整体放入 TEE 中会导致整个应用的性能下降。但用户原本的需求只是局部加密,为了局部加密而导致整体性能下降会增大应用机密计算的成本。虽然一般用户可以接受为了安全而产生的部分性能退化,但是对于过度加密产生的额外性能退化会感到难以接受。
综上可见,Occlum 方案虽然具有简单易行的优势,但是其在安全性和性能方面的缺点却是其投入实际应用的主要障碍。
Teaclave Java TEE SDK – 在 TEE 中仅放入可信代码
因为在 TEE 中整体支持 JVM 和全部应用程序的方案会在 TEE 中执行过多的代码,导致安全性和性能下降而难以投入实用,能不能换种思路,仅将可信代码放入 TEE 呢?考虑到 TEE 中只能执行 native code,那么是不是可以将可信代码从 Java 代码直接编译为 native code 放入 TEE 运行呢?答案是肯定的,这就是本文的主角 Teaclave Java TEE SDK,以下简称 Teaclave Java。
Teaclave Java 是由 JVM 团队开发的 Java 机密计算开发框架和构建工具链,可以一站式快速实现 Java 机密计算应用的开发和构建。退一步考虑,即使用户没有支持机密计算的硬件环境,Teaclave Java 也可以实现安全沙箱隔离,有效保障敏感数据和程序的运行时安全。
Teaclave Java 的关键技术特性有:
模块分隔、机密计算服务化,如图 3 所示。
简洁完善的机密计算服务生命周期管理 API。
Java 静态编译机密内容。
隐藏实现细节、自动生成所有辅助代码。
在这些技术的支持下,Teaclave Java 能够将从普通模块到机密模块的 Java 模块间服务调用转为从普通模块到机密 native 库的函数调用,如图 4 所示。
模块分隔、机密计算服务化
Teaclave Java 将应用代码分为三个模块,Host、Enclave 和 Common。Host 中是普通的安全非敏感程序,Enclave 中是安全敏感程序,Common 中则是前两者都会用到的公共代码。这种模块划分方式一是为了让开发者感知到代码的安全性区分,二是为了构建时针对不同模块使用不同工具链的便利性。
Host 和 Enclave 是解耦合的,它们之间只能通过 Java 的 SPI(Service Provider Interface)机制交互,而不能直接调用。机密计算的实现在 Enclave 模块中被封装成为了服务,其接口声明定义在 Common 模块中,并用 @EnclaveService 注解标识。当 Host 中的程序需要用到某一机密计算任务时,就可以先加载服务实例,再调用相应的函数。这一结构组织关系如图 3 所示。
例如我们可以在 Common 中声明一个如代码块 1 所示的机密计算服务接口,其中提供了用于认证加密的密码是否有效的 API,authenticate 函数。该函数接受一个用户传入的加密的密码,返回该密码的认证结果。
代码块 1:在 Common 模块定义机密计算服务接口声明示例
AuthenticationService 接口的具体实现则在 Enclave 模块的 AuthenticationServiceImpl 类中定义,如代码块 2 所示。该类的 authenticate 函数先使用私钥对输入的加密字符串解密,获得明文结果,然后将其和内存中保存的正确的密码比对,再返回是否一致的检查结果。该类中保存的正确密码值和私钥都是安全敏感数据,authenticate 函数的实现也是安全敏感运算。它们都将在 TEE 中运行,以黑盒的形式提供给外部使用。从外部只能看到加密的输入数据和返回的判定结果,而无法窥探到实际的运行过程和数据。
代码块 2 在 Enclave 模块定义机密计算服务接口实现示例
Host 模块使用机密计算服务的代码示例如代码块 3 所示,从中可以看到对机密计算服务 AuthenticationService 接口的使用和普通的 SPI 接口别无二致,依然是加载服务、调用函数、根据结果执行不同的动作等过程。稍有区别的地方在于先要创建出机密计算环境 Enclave 的实例,然后从中加载机密计算服务实例,由此将机密计算的服务实例和环境实例绑定,最后再销毁环境。这些机密计算环境生命周期管理的 API 由 Teaclave Java 提供。从代码块 3 中可见,在 Host 模块中无需感知密码和私钥究竟是什么,也不用了解认证的过程,只是将认证函数当作黑盒服务调用。
代码块 3 从 Host 模块使用机密计算服务示例
以上三部分代码就构成了一个完整的 Java 机密计算应用。从开发的角度看起来与编写一个普通的 SPI 服务调用的应用基本一样,只需要专注于业务逻辑的开发即可,并不需要学习机密计算底层的内容。因此 Teaclave Java 将 Java 机密计算的开发门槛降低到了 0。
构建机密计算应用
Teaclave Java 提供了一套完整的构建工具链以支持上文所述的编程模型,用户只需输入几个简单的 maven 命令即可完成全部构建任务。构建工具链将非机密代码和机密代码分别编译为 Java bytecode 产物和可部署于 SGX 中的 native 库,以及自动生成完成机密计算服务调用所需的所有的辅助代码。
图 4 展示了 Teaclave Java 的构建部署视图,主要包括三方面内容:
1)Host 和 Common 模块被编译为普通的 Java bytecode,部署在普通环境中执行。
2)Enclave 和它所使用到的 Common 模块中的内容被编译为 native 机密库文件,部署在 SGX 硬件中执行。
3)从 Java bytecode 到 native code 之间并不能直接调用,而需要一些适配转换工作,包括:
服务代理:通过 J ava 的动态代理机制将 Host 模块中的机密计算服务调用代理到实际的 native 函数上,并完成上下文环境的同步、服务参数和返回值的序列化反序列化等工作。
JNI 层:Java 侧的 native 函数声明、native 侧的 JNI 函数声明和到机密库函数的调用等辅助代码。
这些适配转换调用的代码在构建中被自动生成,分别部署在普通环境和 SGX 中,在图 4 中它们被用蓝色标出。
构建过程中的重要一步是将机密部分的 Java 代码编译为 native 代码的 Java 静态编译。
Java 静态编译
Java 程序原本需要在 JVM 上才能运行,但是 Java 静态编译技术可以将 Java 程序(包括 JDK 库依赖和三方库依赖)加上必要的运行时支持代码一起编译为 native 代码,然后直接运行。以此实现了 Java 程序无需 JVM 的轻量级运行。
Teaclave Java 采用了目前最成熟的 Java 静态编译技术——Oracle 主导的开源项目 GraalVM 进行 Java 静态编译。GraalVM 首先对 Java 程序做可达性分析,找到从程序入口开始的所有可能执行到的代码范围,然后仅编译这些可达的代码,得到一个 native 制品(被称为 native image)。程序入口对于可执行程序来说是 main 函数,对于库文件来说是暴露的公共 API。具体到 Teaclave Java 场景,入口就是开发者定义的机密计算服务函数,也就是 Enclave 模块中定义的机密计算服务接口的实现函数。这些接口实现会用到三种依赖,Common 模块中的代码、某些 JDK 库以及其他 Java 三方库,但是只会用到这些依赖的部分代码,而非全部代码。
GraalVM 就会将实际用到的代码分析出来,与机密计算服务的实现代码和 GraalVM 提供的运行时支持(被称为 Substrate VM)一起编译为 native image。但是 GraalVM 是面向通用场景和硬件平台的,所以 Teaclave Java 为其额外提供了针对 SGX 硬件平台的适配和机密计算需求的优化。当我们编译出 native image 后,会发现其具有了一些特别的性质:
TCB 下降。GraalVM 仅编译从机密计算服务入口可达的代码,因此与 Occlum 将 LibOS、JVM 和 Java 应用全部放入 TEE 的方案相比,TCB 大幅降低了。
安全性提升。Native image 在运行时有自己的 native 内存堆,它与 Java 堆是相互隔离的,从 Java 应用中很难被访问到(Java 通过 Unsafe 接口依然可以访问 native 内存,但是难度提升很多)。而且 Java 静态编译去掉了 Java 的动态特性,只有在编译时经过显式配置的反射和动态类加载才会生效,其他运行时的动态行为是无效的。Log4j 漏洞攻击在 native image 上本身就是无效的。因此 native image 可以被视作一个安全沙箱,即使没有 SGX 硬件环境,native image 相比 Java 程序也提升了安全性。部署在 SGX 里之后,TEE 的安全性会更高,因为消除了 Java 动态特性对 TEE 安全性的威胁。
性能提升。GraalVM 的 Java 静态编译对代码有相当程度的编译优化,其运行时性能大致可以达到 JVM 的 C1 优化水平,再加上无需启动 JVM、没有类加载过程、没有解释执行、没有 JIT 消耗资源等等,在执行短小的任务时与 Java 程序相比能有 1 个数量级的性能提升,内存也有大幅削减。
这些性质可以有效地提升机密程序的安全性,提升了 Teaclave Java 的实用性。
Teaclave Java 技术评估
以上介绍了 Teaclave Java 提供的 Java 机密计算编程模型和采用的构建方式等技术问题,那么最终实现的效果如何呢?本文以 log4j 漏洞攻击为例分析 Teaclave Java 的功能有效性。
在 TCB 改进和运行时性能分析方面,我们准备了如表格 1 所示的 10 个测试。前 4 个“app-”前缀的是我们自己写的简单应用,将它们当作机密程序,以各自的 main 入口当作普通程序。后 6 个“ct-”前缀的用例则采用了著名开源加密框架 BouncyCastle (https://www.bouncycastle.org/java.html)的单元测试,我们提供了单一入口调用这些测试,将测试入口当作普通程序,单元测试当作机密程序。
(表 1 / 测试用例描述)
Java 机密计算框架则采用了 OcclumJ 和 Teaclave Java 进行对比。OcclumJ 是我们实现的一种介于 Occlum 和 Teaclave Java 之间的机密计算模型,采用 Teaclave Java 的模块化和机密计算服务化的模型,但是不做 Java 静态编译,而是在 TEE 中以 Occlum 方式运行机密计算服务。
功能有效性评估
图 5 给出了 log4j 漏洞攻击的原理示意(a 子图)和 Teaclave Java 防范 log4j 漏洞攻击的原理(b 子图)。对于一个普通的 Java 应用服务,它和客户端通过三个步骤交互。
1)客户端从服务端获取公钥。
2)客户端用公钥对消息加密,然后将密文发送给服务端。
3)服务端从运行时内存中拿出私钥解密消息,然后再处理消息内容。假设服务端使用了 log4j-2.14.x 版本做日志记录,其中的漏洞允许攻击者诱导 log4j 从远程服务器下载指定的恶意 class 文件(图 5-a 中的步骤 4、5、6),然后动态加载恶意类,从 Java 堆内存上获取到私钥(步骤 7)传给攻击者。
有了服务器的私钥,服务器和客户端之间的所有通信对于攻击者而言都如同明文了。
(图 5 / Java 机密计算保护应用免 受 Log4j 漏洞攻击示意图)
图 5-b 展示了 Teaclave Java 如何保护应用服务端免受 log4j 漏洞攻击的威胁。Teaclave Java 将应用的普通代码放在 REE 中,安全敏感的解密和私钥放在 TEE 中,客户端送来加密消息会被 REE 中的代理服务转到 TEE 中进行解密。此时当攻击者发起 log4j 攻击时,因为 Log4j 部署在 REE 中,恶意代码也只能在 REE 中运行,而无法拿到 TEE 内存上的私钥,攻击失效。假如机密代码也使用了 log4j 记日志,导致 log4j 运行在 TEE 中运行会发生什么呢?
此时 log4j 将攻击者恶意代码下载到了 TEE 中,但是因为 Teaclave Java 采用了 Java 静态编译技术,恶意代码在编译时是未知的,不会被编译到 native image 中。而 Java 静态编译技术并不支持对 native image 中不存在的代码进行动态加载执行,所以即便恶意类被下载到了 TEE,也不会被执行。因此在这种场景下 Teaclave Java 支持的机密计算依然是安全的。但是如果采用了 Occlum 方案,因为 TEE 中有了 JVM,就可以动态加载恶意代码并运行,攻击就会成功。
再退一步,当在没有 SGX 硬件为 TEE 加密时,native image 依然是一个 native 沙箱,恶意 Java 代码无法轻易从 native 内存中拿到安全敏感内容。
TCB 评估
因为 Teaclave Java 不再需要 LibOS 和 JVM,机密代码部分也是按需编译部署。OcclumJ 方案虽然采用了分模块模型,但是并没有做静态分析,因此只是模块级别的代码划分,虽然较 Occlum 完全不划分有所改进,但是与 Teaclave Java 函数级的划分相比仍有相当大的差距。图 6 展示了 OcclumJ 和 Teaclave Java 放入 TEE 的二进制编译产物的大小对比。蓝条是 OcclumJ 的结果,橙条是 Teaclave Java 的结果,图中的 Lejacon 是 Teaclave Java 在论文中的代号。
由图 6 中的对比数据可见,Teaclave Java 的编译后 TCB 大小仅为 Occlum 的大约 1/20 到 1/10。考虑到编译时 native 代码的膨胀问题,两者实际的函数数量差距更大,所以 Teaclave Java 的 TCB 低于 Occlum 一个数量级,从而具有了更高的安全性。
运行时性能评估
因为 native image 会直接以 native 代码的形式运行,省去了包括 JVM 启动、类加载、解释执行等步骤的 Java 程序的冷启动过程,所以启动速度会非常快。如果要执行的机密代码较少,会很快执行完毕。但是 native image 的代码编译质量不如 JVM 的 C2,所以当程序执行的时间足够长,Java 代码被 JIT 充分编译后,native image 的运行时性能就会随着时间的增长而与 Java 程序越来越接近,然后被超越。所以 Teaclave Java 在小型应用的性能远优于 OcclumJ,但是在长时间执行的应用方面该优势就会缩小。
图 7 就展现出了这一特点。图中的蓝线是机密代码部分采用 OcclumJ 模型的执行时间,黄线是采用 Teaclave Java 模型的执行时间,绿线是在普通环境中在普通 JVM 上直接运行的时间。程序在 TEE 中运行的时间要大于普通环境,主要因为增加了机密环境的创建、机密内存的分配等开销,我们将其统称为机密环境开销。黄线在执行时间较短的场景中保持了与绿线接近的性能,说明 Java 程序冷启动的开销与 native image 的机密环境开销差不多可以相抵。当程序执行时间较长时,冷启动开销被摊薄,但是机密环境开销与 TEE 内存使用量成正比,所以黄线较绿线在最后三个测试用例上的上升线条更陡峭。
图 8 给出了 OcclumJ 和 Teaclave Java 的运行时内存使用量对比。OcclumJ 的内存消耗包括 LibOS、JVM 和应用三部分,而 Teaclave Java 模型的内存消耗只有应用和 native image 中的轻量级运行时。更简化的结构为 Teaclave Java 模式的机密计算带来了更少的内存消耗。
总结
Teaclave Java 是一个使用简单、效果显著、性能良好的 Java 机密计算解决方案,能够帮助用户彻底解决保护 Java 应用中的安全敏感内容和运算的问题。Teaclave Java 具有硬件宽容性,当具备 SGX 硬件环境时,可以使 Java 用户也能像其他 native 语言用户一样享受到机密计算带来的最高等级运行时安全保护;在缺少机密计算的硬件环境时,仍然可以提供安全沙箱对机密代码实施内存隔离,以避免安全敏感内容直接暴露。可以说,Teaclave Java 就是保护 Java 应用中敏感数据和运算安全的标准答案。
Oracle 已经把 GraalVM 的 Java 静态编译技术贡献给了 OpenJDK,预计在 JDK 21 会合入 OpenJDK 主干。因此在未来 Teaclave Java 方案就可以获得 JDK 的原生支持。我们也计划向 Java 社区提交关于增加机密计算规范的文件,希望可以将 Teaclave Java 的机密计算模型上升为 Java 原生的机密计算方案。
本技术发表的论文为:Xinyuan Miao, Ziyi Lin, Shaojun Wang, Lei Yu, Sanhong Li, Zihan Wang, Pengbo Nie, Yuting Chen, Beijun Shen, He Jiang. Lejacon: A Lightweight and Efficient Approach to Java Confidential Computing on SGX. ICSE 2023.
论文链接:
https://ddst.sjtu.edu.cn/Management/Upload/[News]a845acae286b470bb55013c1b5e425e2/20232101456536725sSV.pdf
Teaclave Java 项目的源代码已经贡献到了 Apache 社区,加入机密计算框架 Teaclave 项目,目前正在开源孵化中。
项目链接:https://github.com/apache/incubator-teaclave-java-tee-sdk
龙蜥社区云原生机密计算 SIG 主页:
https://openanolis.cn/sig/coco
作者介绍
林子熠,云原生机密计算 SIG Maintainer。
评论 2 条评论