AMD机密计算解决方案分析
- 前言
- 数据结构
- KVM
- 虚机管理命令
- 平台管理命令字
- QEMU
- 虚机启动流程
- 可信认证
- 基本原理
- 具体流程
- 工具
- 接口
- 静态度量
- 基本原理
- 具体流程
- 内核启动
- 镜像启动
- 密钥注入
前言
- 基于AMD SEV基本原理,继续分析AMD的机密计算解决方案在QEMU/KVM虚拟化方案下的实现逻辑
数据结构
- AMD SEV机密计算场景下,Hypervisor的职能稍有改变,可以概括为以下三点:
- 管理主机计算资源管理
- 代理可信组件间通信
- 准备机密计算环境
- 对于第一点,QEMU/KVM复用已有的逻辑,无需修改,对于二、三点,QEMU/KVM主要基于SEV API实现。具体实现时,KVM需要封装SEV API并对QEMU暴露IOCTL命令字,QEMU按照SEV API定义的流程实现对机密虚机的生命周期管理逻辑。
KVM
- 分析KVM对QEMU提供的IOCTL命令字涉及的数据结构
虚机管理命令
- 虚机管理命令字提供与虚机相关操作,因此它打开的是/dev/kvm字符设备,通过
KVM_CREATE_DEVICE
创建虚机关联的fd,再通过KVM_MEMORY_ENCRYPT_OP
命令字下发命令,所有的虚机管理命令作为KVM_MEMORY_ENCRYPT_OP
命令字的参数,通过id字段的不同来区分,其命令格式如下:
- 命令格式
struct kvm_sev_cmd {__u32 id; /* sev cmd ID,取值为sev_cmd_id */__u64 data; /* 每个sev cmd对应的参数 */__u32 error;__u32 sev_fd; /* 字符设备/dev/sev对应的fd */
};
kvm_sev_cmd
结构体中id字段可选的命令集合
- 命令集合
/* Secure Encrypted Virtualization command */
enum sev_cmd_id {/* Guest initialization commands */KVM_SEV_INIT = 0,KVM_SEV_ES_INIT,/* Guest launch commands */KVM_SEV_LAUNCH_START, /* 启动虚机命令,通知PSP,Guest owner与PSP建立会话 */KVM_SEV_LAUNCH_UPDATE_DATA, /* 更新启动初始化时的虚机内存数据,通知PSP */KVM_SEV_LAUNCH_UPDATE_VMSA,KVM_SEV_LAUNCH_SECRET, /* 加载虚机密钥密文到虚机内存,通知PSP进行密钥注入 */KVM_SEV_LAUNCH_MEASURE, /* 请求PSP计算度量值 */KVM_SEV_LAUNCH_FINISH,/* Guest migration commands (outgoing) */KVM_SEV_SEND_START,KVM_SEV_SEND_UPDATE_DATA,KVM_SEV_SEND_UPDATE_VMSA,KVM_SEV_SEND_FINISH,/* Guest migration commands (incoming) */KVM_SEV_RECEIVE_START,KVM_SEV_RECEIVE_UPDATE_DATA,KVM_SEV_RECEIVE_UPDATE_VMSA,KVM_SEV_RECEIVE_FINISH,/* Guest status and debug commands */KVM_SEV_GUEST_STATUS,KVM_SEV_DBG_DECRYPT,KVM_SEV_DBG_ENCRYPT,/* Guest certificates commands */KVM_SEV_CERT_EXPORT,/* Attestation report */KVM_SEV_GET_ATTESTATION_REPORT,/* Guest Migration Extension */KVM_SEV_SEND_CANCEL,KVM_SEV_NR_MAX,
};
- 虚机管理命令目的不同,因此每条命令有其对应的参数,以
KVM_SEV_LAUNCH_START
为例,其参数格式如下:
- 命令参数
struct kvm_sev_launch_start {__u32 handle;__u32 policy; /* 启动机密虚机时的策略 *//* 机密环境准备过程中,涉及到Guest owner与PSP间的数据传输 */__u64 dh_uaddr;__u32 dh_len;__u64 session_uaddr;__u32 session_len;
};
平台管理命令字
- 平台管理命令主要是向PSP发送平台管理相关命令,与虚机并无关系,它打开的是/dev/sev字符设备并直接下发平台管理命令字,其格式如下:
- 命令格式
/*** struct sev_issue_cmd - SEV ioctl parameters** @cmd: SEV commands to execute* @opaque: pointer to the command structure* @error: SEV FW return code on failure*/
struct sev_issue_cmd {__u32 cmd; /* In */__u64 data; /* In */__u32 error; /* Out */
}
sev_issue_cmd
结构体中cmd字段可选的命令集合
- 命令集合
/*** SEV platform commands*/
enum {SEV_FACTORY_RESET = 0,SEV_PLATFORM_STATUS, /* 查询PSP状态 *//* 请求PSP重新生成PEK签发的所有证书,包括PEK公私钥对,PEK证书,PDH公私钥对,PDH证书,OCA公私钥对,OCA证书 */SEV_PEK_GEN, /* 请求PSP生成PEK公钥及其它用户制作PEK证书的信息 */SEV_PEK_CSR,/* 请求PSP重新生成PDH,该PDH的公钥信息被PEK私钥签名,由PEK签发 */SEV_PDH_GEN,/* 导出PDH公钥及PEK对公钥的签名,用于Gueste owner对PDH的可信验证 */SEV_PDH_CERT_EXPORT,/* 往PSP导入PEK证书,声明PSP的Platform owner */SEV_PEK_CERT_IMPORT,/* 查询CPU ID,在AMD CA是,此ID将作为CEK下载的输入 */SEV_GET_ID, /* This command is deprecated, use SEV_GET_ID2 */SEV_GET_ID2,SEV_MAX,
};
- 命令参数
- 每个平台命令都有其对应的参数,以
PLATFORM_STATUS
为例,在下发PLATFORM_STATUS
时会传入结构体sev_user_data_status
,用于接收PSP的输出。
/*** struct sev_user_data_status - PLATFORM_STATUS command parameters** @major: major API version* @minor: minor API version* @state: platform state* @flags: platform config flags* @build: firmware build id for API version* @guest_count: number of active guests*/
struct sev_user_data_status {/* PSP固件版本号 */__u8 api_major; /* Out */__u8 api_minor; /* Out *//* PSP状态 */__u8 state; /* Out */__u32 flags; /* Out */__u8 build; /* Out */__u32 guest_count; /* Out */
}
QEMU
- SevGuestState主要描述SEV机密虚机的配置参数和运行时状态
/*** SevGuestState:** The SevGuestState object is used for creating and managing a SEV* guest.** # $QEMU \* -object sev-guest,id=sev0 \* -machine ...,memory-encryption=sev0*/
struct SevGuestState {ConfidentialGuestSupport parent_obj;/* configuration parameters */char *sev_device;uint32_t policy; /* SEV API 中定义的虚机启动策略 */char *dh_cert_file; /* */char *session_file;uint32_t cbitpos;uint32_t reduced_phys_bits;bool kernel_hashes;/* runtime state */uint32_t handle;uint8_t api_major;uint8_t api_minor;uint8_t build_id;int sev_fd;SevState state;gchar *measurement;uint32_t reset_cs;uint32_t reset_ip;bool reset_data_valid;
};
虚机启动流程
可信认证
基本原理
- 机密虚机与普通虚机最大的不同是机密虚机所有者即Guest owner假定Hypervisor不可信,因此机密虚机的整个生命周期都不能被Hypervisor监听或篡改,保证机密虚机一定是Guest owner期望的虚机。
- 在启动流程中,Guest owner首先要做的就是对安全处理器PSP做可信认证,确认安全处理器一定是可信的。Guest owner对PSP的校验就是请求(通过PDH_CERT_EXPORT)其导出可以证明自己身份的证书,这些证书包括CEK证书、PEK证书、OCA证书以及计算共享密钥用的PDH。Guest owner拿到证书后,做以下处理:
- 对于CEK证书,首先获取签发CEK证书的CA即ASK,使用ASK的公钥解密CEK证书得到CEK公钥的明文,重新计算明文摘要并对比CEK证书的摘要,如果两者相等,说明CEK证书确实是ASK签发的。因为ASK签发CEK的实际动作就是使用ASK的私钥对CEK的公钥进行加密存放到证书中,而ASK的私钥只有ASK可以访问,如果CEK公钥由非ASK的私钥进行加密,那么使用ASK的公钥对CEK公钥解密后,得到的明文摘要一定与证书中的摘要不同。
- 对于PEK证书,获取PEK证书的CA即OCA,使用OCA的公钥解密PEK证书得到PEK公钥明文,重新计算名为摘要并对于PEK证书上的摘要,相等说明PEK证书是OCA签发的。检查OCA是否是自签的证书,如果OCA是自签证书,不做进一步处理,如果OCA是它签证书,找到签发OCA的CA,下载其公钥再对OCA进行验签,整个流程与其它证书验签类似。
- 对于PDH证书,获取签发PDH证书的CA即PEK,使用上述类似流程对其进行验签。
- 上述所有验签通过后,Guest owner认为PSP就是可信的,继而可以让Hypervisor开始启动虚机,整个流程示意图如下:
具体流程
工具
- 由于可信认证涉及的角色是Guest owner和PSP,Hypervisor因此不需要介入。PSP的可信认证工具通常由CPU厂商提供,也就是AMD和Hygon(基于AMD架构的国产CPU),我们简单介绍两者的工具:
- AMD - sevctl
- 服务器出厂后,第一次启动时还没有OCA,通过以下命令可以创建一个自签名的OCA:
$ sevctl generate oca.cert oca.key
$ sevctl provision oca.cert oca.key
- OCA创建完成后,通过以下命令验证当前服务器中的PSP是否可信,该工具会根据CPU ID从AMD官网查询对应的CEK证书并导入到PSP,最后做验证
$ sevctl verify
- 创建完自签OCA证书并完成CEK证书导入后,可以通过export命令导出PSP的整个证书链供Guest owner验签
$ sevctl export --full /opt/sev/cert_chain.cert
- Hygon - hag
- 服务器出厂后第一次上电,需要导入通用的安全证书,首先下载hygon机密计算工具包:
cd /opt/
git clone https://gitee.com/anolis/hygon-devkit.git
mv hygon-devkit hygon
cp /opt/hygon/bin/hag /usr/bin
- 在线导入通用的安全证书
hag general hgsc_import
- 也可以离线导入通用的安全证书
hag general get_id /* 获取PSP芯片ID */
hag general hgsc_version /* 查询证书版本号 */
- 访问Hygon证书下载官网 输入芯片ID和证书版本号下载证书,名称类似为hygon-hgsc-certchain-v1.0-H905P0005040204.bin,通过以下命令导入到Hygon服务器:
hag general hgsc_import -offline -in /path/to/hygon-hgsc-certchain-v1.0-H905P0005040204.bin
- 最后查询证书导入状态,is HGSC imported为YES
hag csv platform_status
- 完成以上步骤后,导出证书链包含的所有文件:
hag csv pdh_cert_export
- 证书链文件包括:
cert_chain.bin
cert_chain.cert
cert_chain_readable.txt
pdh.bin
pdh.cert
pdh_readable.txt
- 其中证书链被Guest owner用于校验PSP是否可信,证书链包含三个证书,分别是CEK、OCA 和PEK:
cat cert_chain_readable.txt | grep Userid
Userid: HYGON-SSD-PEK
Userid: HYGON-SSD-OCA
Userid: HYGON-SSD-CEK
- PDH被Guest owner用于计算与PSP之间通信的会话密钥:
cat pdh_readable.txt |grep Userid
Userid: HYGON-SSD-PDH
接口
- 的确,证书的导出确实与Hypervisor不相关,但QEMU实际上也提供了虚机运行时导出其证书的接口,以便支持Libvirt等虚机管理工具对机密虚机的能力查询:
1. qapi中对sev能力的定义
##
# @SevCapability:
#
# The struct describes capability for a Secure Encrypted
# Virtualization feature.
#
# @pdh: Platform Diffie-Hellman key (base64 encoded)
#
# @cert-chain: PDH certificate chain (base64 encoded)
#
# @cpu0-id: Unique ID of CPU0 (base64 encoded) (since 7.1)
#
# @cbitpos: C-bit location in page table entry
#
# @reduced-phys-bits: Number of physical Address bit reduction when
# SEV is enabled
#
# Since: 2.12
##
{ 'struct': 'SevCapability','data': { 'pdh': 'str','cert-chain': 'str','cpu0-id': 'str','cbitpos': 'int','reduced-phys-bits': 'int'},'if': 'TARGET_I386' }
2. 对应的C结构体
struct SevCapability {char *pdh; /* 用于计算会话密钥PDH证书 */char *cert_chain; /* 证书链 */char *cpu0_id;int64_t cbitpos;int64_t reduced_phys_bits;
};
3. 具体的查询流程
qmp_query_sev_capabilitiessev_get_capabilitieskvm_vm_ioctl(kvm_state, KVM_MEMORY_ENCRYPT_OP, NULL)fd = open(DEFAULT_SEV_DEVICE, O_RDWR) /* 打开/dev/sev字符设备 */sev_get_pdh_info(fd, &pdh_data, &pdh_len, &cert_chain_data, &cert_chain_len, errp)) /* 下发命令字导出证书 */sev_platform_ioctl(fd, SEV_PDH_CERT_EXPORT, &export, &err)cap = g_new0(SevCapability, 1);cap->pdh = g_base64_encode(pdh_data, pdh_len);cap->cert_chain = g_base64_encode(cert_chain_data, cert_chain_len);
静态度量
基本原理
- Guest owner完成对PSP的可信认证后,就可以通知Hypervisor启动虚机了,对于要启动的虚机,Guest owner需要确认Hypervisor启动虚机使用的所有引导文件与自己期望的相同。为达此目的,Guest owner需要将启动虚机涉及的所有引导文件和命令行参数作为输入,计算一个度量值,然后在虚机启动后,让PSP将相同的文件和命令行参数也作为输入,重新计算一个度量值,两者对比之后,如果相同,则可以确认Hypervisor在虚机启动过程中并没有对引导文件做手脚,启动的虚机确实是Guest owner期望的虚机。
- Guest owner对引导文件和命令行参数进行的度量计算,与PSP可信认证相互独立,因此也可以在最开始启动虚机的时候做,如下面流程图的第一步所示:
- AMD - sevctl
- sevctl工具提供了measurement命令计算度量值,使用方式如下:
$ sevctl measurement build \--api-major 01 --api-minor 40 --build-id 40 \--policy 0x05 \--tik /path/to/VM_tik.bin \--firmware /usr/share/edk2/ovmf/OVMF.amdsev.fd \--kernel /path/to/kernel \--initrd /path/to/initrd \--cmdline "my kernel cmdline" \--vmsa-cpu0 /path/to/vmsa0.bin \--vmsa-cpu1 /path/to/vmsa1.bin \--num-cpus 4--launch-measure-blob /o0nzDKE5XgtVnUZWPhUea/WZYrTKLExR7KCwuMdbActvpWfXTFk21KMZIAAhQny
- launch-measure-blob指定输出文件的位置
- Hygon - hag
- Hygon的hag工具提供了generate_launch_blob命令计算度量值:
hag csv generate_launch_blob -help
Usage: generate_launch_blob [options]
Valid options are:-help Display this summary-verbose Enable debug log print-dir dir [optional] Specify the file generation directory-build ulong Input the build id of the Firmware-bios infile Input bios file-kernel infile [optional] Input kernel file-cmdline infile [optional] Input cmdline file, default is '\0'-initrd infile [optional] Input initrd file
- 这里我们将OVMF固件作为度量值的输入,计算度量值,示例如下:
hag csv generate_launch_blob \-build 1881 \ /* 固件ID,通过hag csv platform_status查询得到 */-bios /opt/hygon/csv/OVMF.fd /* 度量值的输入:bios文件 */
- Guest owner完成对度量值的计算后,就可以启动虚机了,后续只要获取虚机启动时PSP计算的初始化内存度量值,对比两者是否相等,就可以确认Hypervisor是否启动了一个期望的虚机。这里需要解决的一个问题是,Hypervisor需要怎么做,才能既做到为PSP提供度量值计算的输入,又无法窃取PSP计算的度量值输出,或者无法代替PSP进行度量值计算呢?
- 我们知道PSP将VMCB中的ASID作为加密进程关联内存的密钥,对内存进行加密。为了让Hypervisor无法代替PSP做度量值计算,SEV的做法是:
- 让Hypervisor将计算度量值所需的材料加载到Guest内存地址空间,这里主要指OVMF、kernel和initrd等启动文件,此时虚机的内存地址空间内容还是明文,Hypervisor虽然可以进行度量值计算,但因为PSP与Guest owner通信是密文,要得到密文必须先获取Guest owner与PSP之前通过PDH协商出来的共享密钥,因此它无法得到度量值加密后的密文数据。
- 通过API
ACTIVATE
通知PSP将ASID与内存加密的密钥VEK(VM Encryption Key)关联,PSP的具体逻辑是以ASID为索引,在系统的所有密钥槽中找到对应的VEK,将其加载到内存 - 通过API
LAUNCH_UPDATE_DATA
通知PSP对虚机内存进行加密。此时虚机的内存地址空间变为密文。 - 通过API
LAUNCH_MEASURE
通知PSP对 ASID关联的内存地址空间内容进行度量值计算,并将加密后的明文返回给Guest owner
- 虚机启动的静态度量流程示意图如下:
具体流程
- 我们知道QEMU命令行支持
BIOS+grub
或OVMF+UEFI+grub
固件启动,但在SEV机密计算场景下,只对OVMF+UEFI+grub
启动方案做了适配。所以在具体启动SEV机密计算虚机,只能以OVMF方式引导虚机; - 另外QEMU命令行既支持以显式指定
kernel+initrd+commandline
的方式启动虚机;也支持直接以虚机镜像的方式启动虚机。当用户通过前者启动虚机时,需要指定OVMF、kernel和initrd文件,commandline可选,QEMU的传统做法是将OVMF载入虚机内存,将kernel和initrd文件添加到fw_cfg(Firmware Configuration) 设备,然后启动虚机,虚机启动过程中,OVMF逻辑会从fw_cfg设备中读取kernel和initrd文件并加载到内存,准备工作完成之后,跳转到kernel,移交控制权,这里可以看出,PSP对虚机初始化内存的度量,只会包含OVMF固件,并没有kernel和initrd文件,因为kernel和initrd是在虚机运行(即Guest态)之后OVMF从设备加载到内存的。在机密计算场景下,由于Hypervisor是不可信的,静态度量并没有包含kernel和initrd的内容,它完全有可能将用户kernel和initrd文件替换为恶意镜像而不被Guest owner发现,机密计算的方案需要考虑如何处理这种情况。对于使用虚机镜像的方式启动虚机的场景,也存在Hypervisor直接将镜像替换为恶意镜像的可能。我们下面分别讨论这两种场景下机密计算给出的解决方案以及具体的实现流程。
内核启动
- 内核启动指QEMU通过命令行参数显式指定kernel+initrd+cmdline启动虚机,kernel+initrd文件包含在虚机镜像之中,内核启动流程如下图所示:
- 内核启动存在的问题
虚机初始化内存中只包含原始OVMF信息,因此启动时安全处理器只对OVMF信息进行度量计算。如果Hypervisor保证OVMF固件不变,替换kernel+initrd+cmdline甚至虚机镜像,Guest owner计算的度量值也只包含OVMF信息,对比两者相同仍会通过度量,授权Hypervisor启动虚机,但OVMF引导了被替换的恶意kernel - 解决方案
将kernel+initrd+cmdline的内容进行hash计算,将得到的hash值添加到OVMF的GUIDed table中,这样PSP在进行度量计算时能够将OVMF+kernel+initrd+cmdline的信息都包含在内,Hypervisor对这些文件的修改都会在Guest owner在验证度量值时被发现。同时为了防止Hypervisor对kernel+initrd+cmdline进行修改甚至替换,OVMF在将kernel从fw_cfg设备读取到内存时,还会增加一个逻辑,即重新计算从fw_cfg设备加载的kernel+initrd+cmdline hash值并取出GUIDed table中的hash值,两者对比,相同则认为Hypervisor没有对kernel+initrd+cmdline进行修改。 - 具体修改点
- 扩展OVMF GUIDed table,增加用于存放hash值的字段,修改QEMU增加对扩展字段的解析逻辑。QEMU在启动虚机前加载OVMF的阶段,将hash填入GUID table,随OVMF一起载入虚机内存,参考QEMU源码中的sev_add_kernel_loader_hashes函数。
下图是kernel+initrd+cmdline的hash值在OVMF GUIDed table中的格式,QEMU启动阶段解析OVMF GUIDed table并找到填充hash值的位置,将hash值填入,以便OVMF在引导kernel镜像时取出做对比:
- 修改OVMF,在引导kernel+initrd+cmdline时增加hash校验环节,首先计算加载kernel+initrd+cmdline hash值,对比GUIDed table中的hash值,如果不同,证明hypervisor替换了引导文件。
- 修改Guest Owner校验程序或者度量值计算工具,增加对kernel+initrd+cmdline hash值的计算并将其内容作为填入OVMF GUIDed table,将扩展后的OVMF作为输入,计算度量值。
- 接下来我们对整个流程在QEMU中的实现进行分析,流程图如下:
- QEMU机密虚机启动命令行参数如下:
-object sev-guest,id=sev0,policy=0x1,cbitpos=47,reduced-phys-bits=5......
-machine pc-i440fx-6.2,memory-encryption=sev0
流程分析
kvm_arch_initsev_kvm_init /* 初始化SEV上下文 *//* 获取机密虚机对象数据结构,即 -object sev-guest定义的object */SevGuestState *sev = (SevGuestState *)object_dynamic_cast(OBJECT(cgs), TYPE_SEV_GUEST);/* 如果命令行没有定义set-guest对象,直接返回 */if (!sev) {return 0;} sev_launch_start
镜像启动
- 镜像启动指QEMU指定启动盘的方式启动虚机,kernel+initrd包含在虚机镜像之中。OVMF读取磁盘内容,通过grub指定磁盘上的kernel+initrd+cmdline。
- 存在的问题
虚机初始化内存中只包含原始OVMF信息,kernel+initrd+cmdline因为没有显式指定,因此不会被校验。如果hypervisor保证OVMF固件不变,替换虚机镜像,Guest owner计算的度量值也只包含OVMF信息,并且OVMF信息中没有包含虚机镜像的摘要,也无法对虚机镜像正确性做校验,因此也会出现用户授权虚机,但OVMF引导了被替换的虚机镜像 - 解决方案
如果参考内核启动的方式对虚机镜像提取摘要并做hash计算,就算可行,但磁盘与kernel+initrd+cmdline不同,其内容易变,Guest Owner需要不断计算磁盘更新后的度量值,因此方案无法产品化。解决这个问题AMD SEV提供的方案是将虚机磁盘在可信的环境中格式化成LUKS加密盘,使用OVMF+grub的方式引导磁盘,OVMF磁盘密码传递给grub,让grub可以解密磁盘。 - 具体修改点
- 扩展OVMF GUIDed table,增加用于存放secret的字段,修改QEMU增加对扩展字段的解析逻辑修改。扩展的secret表项格式如下:
QEMU在启动过程中,如果度量阶段Guest Owner授权通过,Guest Owner调用qmp_sev_inject_launch_secret将密钥密文传递给安全处理器。QEMU的处理逻辑是首先从OVMF GUIDed table表中找到secret的字段,取出OVMF期望secret存放的GPA,得到其对应HVA并将密钥密文拷贝到此处,之后调用安全处理器APILAUCH_SECRET
API,让安全处理器发起密钥注入。整个过程QEMU处理的是密钥密文。 - 修改OVMF支持引导grub并实现安全的密钥传递
- 修改grub,增加接受OVMF密钥传递逻辑并对使用该密钥对磁盘进行解密
- 关于密钥注入的QEMU代码逻辑,我们在下一节继续分析。
密钥注入
- TODO