0. 参考资料
本文参考了英飞凌开源的远程证明代码的实现,并在此基础上进行了修改,这份开源的代码有三个分支, master 分支是远程证明实现相关的文档; device 分支是远程证明的客户端实现; server 分支是远程证明的服务端实现。除此之外还参考内核完整性度量模块相关的代码。
1. 概述
最近做了一些可信计算相关的工作,阶段性进展是搞清楚了远程证明的原理以及本地使用 tpm 模拟器实现了一个简单的远程证明方案。
可信计算是一个提出很多年的课题,这些年一直存在于理论研究中,网上能找到的资料除了 TCG (可信计算组织) 撰写的官方文档外,就剩下那些纯理论的论文,很少涉及工程实践。在 github 上也很少有相关的开源项目。本文将介绍可信计算中的一个小分支——远程证明。
2. 远程证明理论
2.1. 什么是远程证明
远程证明是指一个节点将自己平台的某些信息使用约定的格式和协议向另一个节点报告,使得另一节点能够获得这些信息,并判定该平台的可信状态。其目的是保证两个节点的身份和安全属性符合对一方的要求,其平台状态是可靠的。而在可信计算平台上的远程证明是指在平台上使用身份认证密钥 AIK 对当前存储的 PCR 值进行签名,然后报告给远程挑战者其平台的状态,是建立在可信度量、可信报告基础之上的技术。
2.2. 远程证明的原理
简单远程证明过程的完成要经历两个阶段:可信度量与可信报告。可信度量是平台组件完整性度量值的计算与存储,可信报告是将平台组件的度量值报告给外来用户,然后用户通过验证度量值来判断平台和应用程序是否被篡改。
可信度量是指通过一定的方法按步骤度量并报告平台的状态。从系统加电启动,一直到最后应用程序每一步都需要度量,整个启动序列都遵循先度量再执行的原则。当前阶段的代码负责度量下一阶段即将要执行的代码,然后再将度量值扩展到 PCR 中,这样一级信任一级,以此保证平台的可信,保证环境的安全。当然任何信任关系中总是存在某种基础性的假设,必然存在默认环节的信任关系的基石。在一个信任关系依次传递的链条中,源头在启动的过程中是被假设为安全可信的,不会受到度量。
在系统进行可信度量时,除了度量结果要在 PCR 中进行扩展之外,还将具体每一步的度量操作、中间状态和度量结果保存下来,可以作为系统可信度量的详细步骤进行参考,存储度量日志(Storage Measurement Log,SML)用于存储这些信息。由于 PCR 被认为是不可篡改的,并且它所保存的值可以通过 SML 重新计算出来,因此 SML 一般不需要安全保护。
3. 内核完整性度量
内核完整性度量模块 (IMA) 提供了远程证明所需的度量并扩展 PCR 和度量报告。
内核完整性度量的实现在 security/integrity/ima 目录下。要想使用 IMA ,需要首先配置内核的 config 文件,使能 IMA 。 还要在 config 文件中指定完整性度量扩展的 PCR 寄存器的索引值。
- CONFIG_IMA=y
- CONFIG_IMA_MEASURE_PCR_IDX=11
3.1. IMA 策略配置
策略配置在 ima_policy.c 文件中。内核默认不进行完整性度量,需要进行配置,可以在内核启动项中增加 ima_tcb 选项即可开启,这是内核的默认策略,该策略度量的范围比较大,我增加了一个只度量二进制程序的策略,实现如下:
在 ima_policy.c 文件中增加如下代码,配置度量二进制程序的策略。
static struct ima_rule_entry bprm_measurement_rules[] __ro_after_init = {
{.action = MEASURE, .func = BPRM_CHECK, .mask = MAY_EXEC,
.flags = IMA_FUNC | IMA_MASK},
};
static int __init bprm_measure_policy_setup(char *str)
{
if (bprm_policy)
return 1;
bprm_policy = 1;
return 1;
}
__setup("bprm_tcb", bprm_measure_policy_setup);
在 ima_init_policy 函数中增加将策略添加到策略列表的代码。
void __init ima_init_policy(void)
{
if (bprm_policy) {
for (i = 0; i < ARRAY_SIZE(bprm_measurement_rules); i++)
list_add_tail(&bprm_measurement_rules[i].list,
&ima_default_rules);
}
}
这样在内核启动项中增加 bprm_tcb 启动项即可开启动量二进制程序的策略了。
3.2. IMA 模版
IMA 模版主要用来配置度量报告,配置度量报告要存哪些值,以及导出度量报告。
内核默认支持的模板如下,其中 name 在 Kconfig 中默认选择的是 ima-ng ,因此 fmt 选择的是 d-ng|n-ng 。
static struct ima_template_desc builtin_templates[] = {
{.name = IMA_TEMPLATE_IMA_NAME, .fmt = IMA_TEMPLATE_IMA_FMT},
{.name = "ima-ng", .fmt = "d-ng|n-ng"},
{.name = "ima-sig", .fmt = "d-ng|n-ng|sig"},
{.name = "", .fmt = ""}, /* placeholder for a custom format */
};
根据模版的 fmt ,从 supported_fields 中选出度量报告中存储文件 hash 值和文件名称。 其中 field_init 函数用来生成度量报告, field_show 函数用来导出度量报告。
- ima_eventdigest_ng_init: 存储数据格式为 [
] + ':' + '\0' + digest ,以及数据长度 - ima_eventname_ng_init: 存储数据为文件路径,以及路径长度
- ima_show_template_digest_ng: 将该 field_id 对应的报告以长度、内容的格式导出,根据不同的 show 字段显示不同的格式
- ima_show_template_string: 将该 field_id 对应的报告以长度、内容的格式导出,根据不同的 show 字段显示不同的格式
static struct ima_template_field supported_fields[] = { {.field_id = "d", .field_init = ima_eventdigest_init, .field_show = ima_show_template_digest}, {.field_id = "n", .field_init = ima_eventname_init, .field_show = ima_show_template_string}, {.field_id = "d-ng", .field_init = ima_eventdigest_ng_init, .field_show = ima_show_template_digest_ng}, {.field_id = "n-ng", .field_init = ima_eventname_ng_init, .field_show = ima_show_template_string}, {.field_id = "sig", .field_init = ima_eventsig_init, .field_show = ima_show_template_sig}, };
3.3. IMA 生成度量报告
IMA 根据策略来生成度量报告,比如我配置的策略是度量二进制,那么在有二进制运行时就进行度量并生成度量报告。
除了配置策略之外, IMA 有一个默认策略,那就是 boot_aggregate ,这是度量报告的第一条数据。我们以它为例介绍 IMA 生成度量报告的流程。
- 度量日志的第一条记录是 boot_aggregate ,如果存在 tpm 芯片的话它的 hash 值是索引为 0~7 的 PCR 的值进行 hash ,在 ima_calc_boot_aggregate 函数中实现;如果没有 tpm 芯片的话则设为 0 。
- ima_alloc_init_template: 该函数根据模版进行初始化 entry ,主要填充 struct ima_field_data 字段。
- ima_store_template: 该函数将初始化后的 entry 中 struct ima_field_data 字段的数据计算 hash 值,保存到 digest 字段中,然后将 entry 添加到全局链表中,如果 tpm 芯片存在的话,将 digest 字段的值扩展到 PCR 中。
static int __init ima_add_boot_aggregate(void) { static const char op[] = "add_boot_aggregate"; const char *audit_cause = "ENOMEM"; struct ima_template_entry *entry; struct integrity_iint_cache tmp_iint, *iint = &tmp_iint; struct ima_event_data event_data = {iint, NULL, boot_aggregate_name, NULL, 0, NULL}; int result = -ENOMEM; int violation = 0; struct { struct ima_digest_data hdr; char digest[TPM_DIGEST_SIZE]; } hash; memset(iint, 0, sizeof(*iint)); memset(&hash, 0, sizeof(hash)); iint->ima_hash = &hash.hdr; iint->ima_hash->algo = HASH_ALGO_SHA1; iint->ima_hash->length = SHA1_DIGEST_SIZE; if (ima_tpm_chip) { result = ima_calc_boot_aggregate(&hash.hdr); if (result < 0) { audit_cause = "hashing_error"; goto err_out; } } result = ima_alloc_init_template(&event_data, &entry); if (result < 0) { audit_cause = "alloc_entry"; goto err_out; } result = ima_store_template(entry, violation, NULL, boot_aggregate_name, CONFIG_IMA_MEASURE_PCR_IDX); if (result < 0) { ima_free_template_entry(entry); audit_cause = "store_entry"; goto err_out; } return 0; err_out: integrity_audit_msg(AUDIT_INTEGRITY_PCR, NULL, boot_aggregate_name, op, audit_cause, result, 0); return result; }
- 创建 entry ,并调用 field_init 函数对其初始化
int ima_alloc_init_template(struct ima_event_data *event_data, struct ima_template_entry **entry) { struct ima_template_desc *template_desc = ima_template_desc_current(); int i, result = 0; *entry = kzalloc(sizeof(**entry) + template_desc->num_fields * sizeof(struct ima_field_data), GFP_NOFS); if (!*entry) return -ENOMEM; (*entry)->template_desc = template_desc; for (i = 0; i < template_desc->num_fields; i++) { struct ima_template_field *field = template_desc->fields[i]; u32 len; result = field->field_init(event_data, &((*entry)->template_data[i])); if (result != 0) goto out; len = (*entry)->template_data[i].len; (*entry)->template_data_len += sizeof(len); (*entry)->template_data_len += len; } return 0; out: ima_free_template_entry(*entry); *entry = NULL; return result; }
- ima_calc_field_array_hash: 对所有的 field 数据进行 hash 计算,包括长度,将 hash 值保存在 digest 字段
- ima_add_template_entry: 将 entry 添加到全局链表,导出度量报告时需要使用。最后将 hash 值扩展到 PCR 中。
int ima_store_template(struct ima_template_entry *entry, int violation, struct inode *inode, const unsigned char *filename, int pcr) { static const char op[] = "add_template_measure"; static const char audit_cause[] = "hashing_error"; char *template_name = entry->template_desc->name; int result; struct { struct ima_digest_data hdr; char digest[TPM_DIGEST_SIZE]; } hash; if (!violation) { int num_fields = entry->template_desc->num_fields; /* this function uses default algo */ hash.hdr.algo = HASH_ALGO_SHA1; result = ima_calc_field_array_hash(&entry->template_data[0], entry->template_desc, num_fields, &hash.hdr); if (result < 0) { integrity_audit_msg(AUDIT_INTEGRITY_PCR, inode, template_name, op, audit_cause, result, 0); return result; } memcpy(entry->digest, hash.hdr.digest, hash.hdr.length); } entry->pcr = pcr; result = ima_add_template_entry(entry, violation, op, inode, filename); return result; }
总结一下, boot_aggregate 的度量报告生成流程:
- 计算索引 0~7 的 PCR 值的hash值,如果没有 tpm 芯片 hash 值为 0
- 将 [
] + ':' + '\0' + digest 和数据长度写入 struct ima_field_data 结构中 - 将 ‘boot_aggregate’ + ‘\0’ 和数据长度写入struct ima_field_data 结构中
- 将上述两步进行 hash 处理,保存到 digest 字段中
- 将 entry 添加到全局链表,如果有 tpm 芯片将 digest 值扩展到 PCR 11 中
3.4. IMA 导出度量报告
在进行远程证明时需要将度量报告导出发给远端服务器,内核帮我们提供了导出度量报告的接口,在 ima_fs.c 文件中。
- 导出度量报告有两个接口,一个是导出 ascii 格式的,一个是导出二进制格式的,其中 ascii 码格式是可视化的,这两种格式都可以作为远程证明的度量报告,在英飞凌的源码中使用的是二进制格式的度量报告。
- 导出度量报告就是遍历全局的 entry 链表,将数据一条一条读取出来,内核在 /sys/kernel/security/ima 目录下提供了 ascii_runtime_measurements 和 binary_runtime_measurements 两个文件,直接读取即可。
- 最终是调用 field_show 函数,输出报告内容
int __init ima_fs_init(void) { ascii_runtime_measurements = securityfs_create_file("ascii_runtime_measurements", S_IRUSR | S_IRGRP, ima_dir, NULL, &ima_ascii_measurements_ops); if (IS_ERR(ascii_runtime_measurements)) goto out; runtime_measurements_count = securityfs_create_file("runtime_measurements_count", S_IRUSR | S_IRGRP, ima_dir, NULL, &ima_measurements_count_ops); if (IS_ERR(runtime_measurements_count)) goto out; }
4. tpm 模拟器环境搭建
首先安装 tpm 相关的工具包,包括 tpm2-tss-2.0 、 tpm2-abrmd-2.0 、 tpm2-tools-2.0 三个包,这里注意,我用的这几个包的版本比较老了,大部分命令与英飞凌的远程证明实现中不兼容,但命令的原理是相同的。
首先,我们要添加 /usr/lib/systemd/system/tpm2-abrmd.service 文件,这里要注意,使用 tpm 模拟器的话,要添加 ‘–tcti=libtss2-tcti-mssim.so.0:host=127.0.0.1,port=2321’ 选项。
[Unit]
Description=TPM2 Access Broker and Resource Management Daemon
[Service]
Type=dbus
BusName=com.intel.tss2.Tabrmd
StandardOutput=syslog
ExecStart=/usr/sbin/tpm2-abrmd --tcti=libtss2-tcti-mssim.so.0:host=127.0.0.1,port=2321
User=tss
[Install]
WantedBy=multi-user.target
还要添加 /etc/dbus-1/system.d/tpm2-abrmd.conf 文件,内容如下:
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<!-- ../system.conf have denied everything, so we just punch some holes -->
<policy user="tss">
<allow own="com.intel.tss2.Tabrmd"/>
</policy>
<policy user="root">
<allow own="com.intel.tss2.Tabrmd"/>
</policy>
<policy context="default">
<allow send_destination="com.intel.tss2.Tabrmd"/>
<allow receive_sender="com.intel.tss2.Tabrmd"/>
</policy>
</busconfig>
去 tpm官网 下载 TPM 模拟器,编译安装,运行 tpm 模拟器 tpm_server -rm & ,这里的 -rm 作用是清空 tpm 模拟器的数据。
编译安装 tpm2-tss-2.0 和 tpm2-tools-2.0 。
然后启动 tpm2-abrmd ,执行 systemctl start tpm2-abrmd.service 。
最后执行 tpm2_pcrlist 检测 tpm模拟器是否能够正常运行。
5. 远程证明服务端
远程证明服务端是使用 SpringBoot 开发,项目由 maven 构建,所以需要安装 mvn 工具,并且代码中使用了 java-9 的特性,还需要安装 OpenJDK9 才能编译通过。具体流程如下:
yum install maven
tar -zxf OpenJDK9-OPENJ9_x64_Linux_jdk-9.0.4.12_openj9-0.9.0.tar.gz -C /usr/lib/jvm
alternatives --install /usr/bin/java java /usr/lib/jvm/jdk-9.0.4+12/bin/java 2
alternatives --config java
export JAVA_HOME=/usr/lib/jvm/jdk-9.0.4+12
mvn install
sudo java -jar server-0.0.1-SNAPSHOT.jar
由于我使用的 tpm 模拟器,tpm 命令无法操作 nv 。而在英飞凌的实现中,通过下面命令获取 ek 的证书。
# ek 证书默认存储在 0x1c00002 地址。
tpm2_nvread 0x1c00002 -o ek.crt
在我的实验中没有使用 ek 证书,直接让客户端将 ek 的公钥发送到服务段。所以证明的过程中没有实现 ek 证书有效性的认证。
另外一个修改点是 ek 公钥的长度大于了 600 个字节,所以在 User.java 中将长度改为 1000 。还有就是我内核生成度量日志使用的模版是 ima-ng ,而英飞凌的实现是按照 ima-sig 格式解析的,两者差异是 ima-sig 多了一个签名值,所以要在代码里将签名值的解析屏蔽掉。
6. 远程证明客户端
英飞凌的实现中有一个 bug , fByteAry2HexStr 函数的第一个参数定义成 char * 类型, char 类型最高位为符号位,如果符号位为 1 时,转成 hexstr 时为 0xff ,这里导致了传输公钥时出错,而公钥中存在着大量超过 128 的数据。
另外,就是英飞凌实现的 tpm 指令与我的环境不兼容。
初始化 tpm 模拟器指令:
tpm2_takeownership -c
tpm2_takeownership -o ownerpasswd -e endorsepasswd -l lockpasswd
tpm2_getpubek -o ownerpasswd -e endorsepasswd -g rsa -f ek.pub -H 0x81010001
tpm2_getpubak -o ownerpasswd -e endorsepasswd -E 0x81010001 -g rsa -D sha256 -s rsassa -k 0x81010002 -f ak.pub -n ak.name
伪造 pcr 值,因为时 tpm 模拟器,所以要根据度量日志伪造 pcr 值:
#! /usr/bin/python
import os
with open("ascii_runtime_measurements", "r") as f:
lines = f.readlines()
for line in lines:
pcr = line.split(" ")[1]
os.system("tpm2_pcrextend 11:sha1=%s" % pcr)
执行远程证明指令:
tpm2_readpublic -H 0x81010002 -o ak.pub
tpm2_readpublic -H 0x81010001 -o ek.pub
tpm2_pcrlist -L sha1:0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23 -o pcr
./bin/attune
./bin/atelic
sh activecredential.sh
sh quote.sh
./bin/attest
基于 tpm 模拟器的远程证明实现就介绍到这里,详细代码实现可以查看 remote-attestation-optiga-tpm 代码。