借助系统芯片 (SoC) 中提供的可信执行环境,Android 设备可以为 Android 操作系统、平台服务乃至第三方应用提供由硬件支持的强大安全服务。寻求 Android 专用扩展的开发者应访问 android.security.keystore。
在 Android 6.0 之前的版本中,Android 已有一个非常简单的由硬件支持的加密服务 API(由 0.2 和 0.3 版的 Keymaster 硬件抽象层 (HAL) 提供)。该密钥库能够提供数字签名和验证操作,以及不对称签名密钥对的生成和导入操作。该 API 在许多设备上都已实现,但有许多安全目标无法只通过一个签名 API 来轻松达成。Android 6.0 中的密钥库在该密钥库 API 的基础上进行了扩展,能够提供更广泛的功能。
在 Android 6.0 中,密钥库不仅增加了对称加密基元(AES 和 HMAC),还增加了针对由硬件支持的密钥的访问权限控制系统。访问权限控制在密钥生成期间指定,并会在密钥的整个生命周期内被强制执行。可以将密钥限定为仅在用户通过身份验证后才可使用,并且只能用于指定的目的或只有在具有指定的加密参数时才可使用。如需了解详情,请参阅授权标记和函数页面。
除了扩大加密基元的范围外,Android 6.0 中的密钥库还增加了以下内容:
- 一个使用控制方案,用于限制密钥的使用,并降低因滥用密钥而损害安全性的风险
- 一个访问权限控制方案,用于限定只有指定的用户和客户端能够使用相应密钥,并且只能在规定的时间范围内使用
在 Android 7.0 中,Keymaster 2 增加了对密钥认证和版本绑定的支持。密钥认证提供公钥证书,这些证书中包含密钥及其访问权限控制的详细描述,以使密钥存在于安全硬件中并使其配置可以远程验证。
版本绑定将密钥绑定至操作系统和补丁级别版本。这样可确保在旧版系统或 TEE 软件中发现漏洞的攻击者无法将设备回滚到易受攻击的版本,也无法使用在较新版本中创建的密钥。此外,在已经升级到更新的版本或补丁级别的设备上使用指定版本和补丁级别的密钥时,需要先升级该密钥才能使用,因为该密钥的旧版本已失效。当设备升级时,密钥会随着设备一起“升级”,但是将设备恢复到任何一个旧版本都会导致密钥无法使用。
在 Android 8.0 中,Keymaster 3 从旧式 C 结构硬件抽象层 (HAL) 转换到根据新硬件接口定义语言 (HIDL) 中的定义生成的 C++ HAL 接口。在此变更过程中,很多参数类型发生了变化,但这些类型和方法与旧的类型和 HAL 结构体方法一一对应。如需了解详情,请参阅函数页面。
除了此接口修订之外,Android 8.0 还扩展了 Keymaster 2 的认证功能,以支持 ID 认证。 ID 认证提供了一种受限且可选的机制来严格认证硬件标识符,例如设备序列号、产品名称和手机 ID (IMEI/MEID)。为了实现此新增功能,Android 8.0 更改了 ASN.1 认证架构,添加了 ID 认证。Keymaster 实现需要通过某种安全方式来检索相关的数据项,还需要定义一种安全永久地停用该功能的机制。
Android 9 纳入了以下更新:
- 更新到 Keymaster 4
- 对嵌入式安全元件的支持
- 对安全密钥导入的支持
- 对 3DES 加密的支持
- 更改了版本绑定,以便 boot.img 和 system.img 分别设置版本以允许独立更新
术语库
下面简要介绍了各个 Keystore 组件及其关系。
AndroidKeystore 是供应用访问 Keystore 功能的 Android Framework API 和组件。它是作为标准 Java Cryptography Architecture API 的扩展程序实现的,包含在应用自己的进程空间中运行的 Java 代码。AndroidKeystore
通过将与密钥库行为有关的应用请求转发到密钥库守护程序执行这些请求。
密钥库守护程序是 Android 系统中的一个守护程序,该程序通过 Binder API 提供对所有密钥库功能的访问权限。密钥库守护程序负责存储“密钥 blob”。密钥 blob 中包含已加密的实际密钥材料,因此密钥库可以存储这些材料,但无法使用或显示这些材料。
keymasterd 是一个 HIDL 服务器,可提供对 Keymaster TA 的访问权限。(此名称未进行标准化,仅用于说明概念。)
Keymaster TA(可信应用)是在安全环境(大多数情况为 ARM SoC 上的 TrustZone)中运行的软件。它可提供所有安全的密钥库操作,能够访问原始密钥材料,验证密钥的所有访问权限控制条件,等等。
LockSettingsService 是负责用户身份验证(包括密码和指纹)的 Android 系统组件。它不是密钥库的一部分却与其相关,因为很多密钥库密钥操作都需要对用户进行身份验证。LockSettingsService
会与 Gatekeeper TA 和 Fingerprint TA 进行交互以获取身份验证令牌,并将其提供给密钥库守护程序,这些令牌最终将由 Keymaster TA 应用使用。
Gatekeeper TA(可信应用)是另一个在安全环境中运行的组件,负责验证用户密码并生成身份验证令牌(用于向 Keymaster TA 证明已在特定时间点完成对特定用户的身份验证)。
Fingerprint TA(可信应用)是另一个在安全环境中运行的组件,负责验证用户指纹并生成身份验证令牌(用于向 Keymaster TA 证明已在特定时间点完成对特定用户的身份验证)。
架构
Android Keystore API 和底层 Keymaster HAL 提供了一套基本的但足以满足需求的加密基元,以便使用访问受控且由硬件支持的密钥实现相关协议。
Keymaster HAL 是由原始设备制造商 (OEM) 提供的动态加载库,密钥库服务使用它来提供由硬件支持的加密服务。为了确保安全性,HAL 实现不会在用户空间乃至内核空间中执行任何敏感操作。敏感操作会被分配给通过某个内核接口连接的安全处理器。 最终的架构如下所示:
在 Android 设备中,Keymaster HAL 的“客户端”包含多个层(例如,应用、框架、密钥库守护程序),但在本文档中可以将其忽略。这意味着,所介绍的 Keymaster HAL API 为底层 API,供平台内部组件使用,不面向应用开发者提供。Android 开发者网站对更高层 API 进行了介绍。
Keymaster HAL 的目的不是实现安全敏感型算法,而只是对发送到安全域的请求进行编排和解排。传输格式是由实现定义的。
与之前版本的兼容性
Keymaster 1 HAL 与之前发布的 HAL(例如 Keymaster 0.2 和 0.3)完全不兼容。为了在搭载 Android 5.0 及更低版本(采用旧版 Keymaster HAL)的设备上实现互用性,密钥库提供了一个可通过调用现有硬件库来实现 Keymaster 1 HAL 的适配器,但最终仍不能提供 Keymaster 1 HAL 中的全部功能。特别是,它仅支持 RSA 和 ECDSA 算法,而且所有密钥授权强制执行都由该适配器在非安全域中进行。
Keymaster 2 通过移除 get_supported_*
方法并允许 finish()
方法接受输入,进一步简化了 HAL 接口。在可一次性获得所有输入的情况下,这样可以减少到 TEE 的往返次数,并简化 AEAD 解密的实现过程。
在 Android 8.0 中,Keymaster 3 从旧式 C 结构 HAL 转换到根据新硬件接口定义语言 (HIDL) 中的定义生成的 C++ HAL 接口。通过对生成的 IKeymasterDevice
类进行子类化并实现纯虚方法创建了新式 HAL 实现。在此变更过程中,很多参数类型发生了变化,但这些类型和方法与旧的类型和 HAL 结构体方法一一对应。
HIDL 概览
硬件接口定义语言 (HIDL) 提供了一种独立于实现语言的机制来指定硬件接口。HIDL 工具目前支持生成 C++ 和 Java 接口。大多数可信执行环境 (TEE) 实现人员应该都会发现 C++ 工具更加方便,因此本文档仅讨论 C++ 表示法。
HIDL 接口由一组方法组成,表示如下:
methodName(INPUT ARGUMENTS) generates (RESULT ARGUMENTS);
有很多不同的预定义类型,而 HAL 可以定义新的枚举和结构类型。如需详细了解 HIDL,请参阅“参考”部分。
下面显示了 Keymaster 3 IKeymasterDevice.hal
中的一个示例方法:
generateKey(vec<KeyParameter> keyParams) generates(ErrorCode error, vec<uint8_t> keyBlob, KeyCharacteristics keyCharacteristics);
这相当于 keymaster2 HAL 中的以下方法:
keymaster_error_t (*generate_key)( const struct keymaster2_device* dev, const keymaster_key_param_set_t* params, keymaster_key_blob_t* key_blob, keymaster_key_characteristics_t* characteristics);
在 HIDL 版本中,由于 dev
参数采用隐式形式,因此已被移除。params
参数不再是包含引用了一组 key_parameter_t
对象的指针的结构体,而是包含 KeyParameter
对象的 vec
(矢量)。返回值在“generates
”子句中列出,其中包含密钥 blob 的 uint8_t
值的矢量。
由 HIDL 编译器生成的 C++ 虚拟方法为:
Return<void> generateKey(const hidl_vec<KeyParameter>& keyParams, generateKey_cb _hidl_cb) override;
其中,generateKey_cb
是一个函数指针,定义如下:
std::function<void(ErrorCode error, const hidl_vec<uint8_t>& keyBlob, const KeyCharacteristics& keyCharacteristics)>
也就是说,generateKey_cb
是一个将接受 generate 子句中列出的返回值的函数。HAL 实现类会覆盖此 generateKey
方法并调用 generateKey_cb
函数指针,以将操作结果返回给调用方。请注意,该函数指针调用是同步的。调用方调用 generateKey
,同时 generateKey
调用所提供的函数指针,该指针在完成执行操作后将控件返回到 generateKey
实现,而后该实现又返回到调用方。
如需查看详细示例,请参阅 hardware/interfaces/keymaster/3.0/default/KeymasterDevice.cpp
中的默认实现。该默认实现可向后兼容采用旧式 keymaster0、keymaster1 或 keymaster2 HAL 的设备。
访问权限控制
密钥库访问权限控制的最基本原则是,每个应用都有自己的命名空间。但每条规则都有例外情况。密钥库有一些硬编码映射,这些映射使某些系统组件可以访问某些其他命名空间。这是一个非常直截了当的工具,因为它可以让一个组件完全控制其他命名空间。还有一个问题是,供应商组件用作密钥库客户端。我们目前无法为供应商组件(例如 WPA 客户端)建立命名空间。
为了适应供应商组件并实现无硬编码异常的通用访问权限控制,密钥库 2.0 引入了域和 SELinux 命名空间。
密钥库域
借助密钥库域,我们可以将命名空间与 UID 分离开。访问密钥库中的密钥的客户端必须指定其想要访问的域、命名空间和别名。根据此元组和调用方的身份,我们可以确定调用方想要访问哪个密钥以及是否具有相应的权限。
我们引入了五个域参数,用于控制访问密钥的方式。这些参数可以控制密钥描述符的命名空间参数的语义以及执行访问权限控制的方式。
DOMAIN_APP
:应用域涵盖旧版行为。Java 密钥库 SPI 默认使用此域。使用此域时,系统会忽略命名空间参数并改为使用调用方的 UID。对此域的访问由 SELinux 政策中的keystore_key
类的密钥库标签控制。DOMAIN_SELINUX
:此域表示命名空间在 SELinux 政策中具有标签。系统会查询命名空间参数并将其转换为目标上下文,并针对调用keystore_key
类的 SELinux 上下文执行权限检查。在为指定操作建立相应权限后,系统会使用完整的元组来执行密钥查询。DOMAIN_GRANT
:授权域表示命名空间参数是授权标识符。系统会忽略别名参数。 创建授权后,系统会执行 SELinux 检查。进一步的访问权限控制仅检查调用方 UID 是否与请求授权的接受者 UID 一致。DOMAIN_KEY_ID
:此域表示命名空间参数是唯一的密钥 ID。密钥本身可能是通过DOMAIN_APP
或DOMAIN_SELINUX
创建的。权限检查是在从密钥数据库加载domain
和namespace
后执行的,其方式与域、命名空间和别名元组加载 blob 的方式相同。密钥 ID 域的基本原理是连续性。通过别名访问密钥时,后续调用可能会对不同的密钥执行操作,因为可能已生成或导入新密钥,并将其绑定到此别名。但是,密钥 ID 不会改变。因此,在使用别名从密钥库数据库中加载一次密钥后,通过密钥 ID 使用密钥时,只要密钥 ID 仍然存在,就可以确定密钥是同一个密钥。我们不会将此功能提供给应用开发者。相反,它在 Android 密钥库 SPI 内使用,可提供更一致的体验,即使以不安全的方式同时使用时也是如此。DOMAIN_BLOB
:blob 域表示调用方自行管理 blob。这适用于需要在装载数据分区之前访问密钥库的客户端。密钥 blob 包含在密钥描述符的blob
字段中。
我们可以使用 SELinux 域向供应商组件授予对非常特定的密钥库命名空间的访问权限,这些命名空间可以由设置对话框等系统组件共享。
keystore_key 的 SELinux 政策
命名空间标签使用 keystore2_key_context
文件进行配置。
这些文件中的每一行会将数字形式的命名空间 ID 映射到 SELinux 标签。
例如,
# wifi_key is a keystore2_key namespace intended to be used by wpa supplicant and # Settings to share keystore keys. 102 u:object_r:wifi_key:s0
以这种方式设置新的密钥命名空间后,我们可以通过添加适当的政策向其授予访问权限。例如,如需允许 wpa_supplicant
获取和使用新命名空间中的密钥,我们需要将以下代码行添加到 hal_wifi_supplicant.te
中:
allow hal_wifi_supplicant wifi_key:keystore2_key { get, use };
在设置新的命名空间后,可以像往常一样使用 AndroidKeyStore。唯一的区别是,必须指定命名空间 ID。如需从密钥库加载密钥或将密钥导入密钥库,需要使用 AndroidKeyStoreLoadStoreParameter
指定命名空间 ID。例如,
import android.security.keystore2.AndroidKeyStoreLoadStoreParameter; import java.security.KeyStore; KeyStore keystore = KeyStore.getInstance("AndroidKeyStore"); keystore.load(new AndroidKeyStoreLoadStoreParameter(102));
如需在指定命名空间中生成密钥,必须使用 KeyGenParameterSpec.Builder#setNamespace():
指定命名空间 ID。
import android.security.keystore.KeyGenParameterSpec; KeyGenParameterSpec.Builder specBuilder = new KeyGenParameterSpec.Builder(); specBuilder.setNamespace(102);
下列上下文的描述文件可用于配置密钥库 2.0 SELinux 命名空间。为了避免冲突,每个分区的预留范围(10,000 个命名空间 ID)都不同。
分区 | 范围 | 配置文件 |
---|---|---|
系统 | 0 ... 9,999 | /system/etc/selinux/keystore2_key_contexts, /plat_keystore2_key_contexts |
扩展系统 | 10,000 ... 19,999 | /system_ext/etc/selinux/system_ext_keystore2_key_contexts, /system_ext_keystore2_key_contexts |
产品 | 20,000 ... 29,999 | /product/etc/selinux/product_keystore2_key_contexts, /product_keystore2_key_contexts |
供应商 | 30,000 ... 39,999 | /vendor/etc/selinux/vendor_keystore2_key_contexts, /vendor_keystore2_key_contexts |
客户端通过请求 SELinux 域和所需的虚拟命名空间(在此例中为 "wifi_key"
)的数字 ID 来请求密钥。
以下命名空间是在此基础上被定义的。如果这些命名空间替代了特殊的规则,下表中指出了以前与其对应的 UID。
命名空间 ID | SEPolicy 标签 | UID | 说明 |
---|---|---|---|
0 | su_key | N/A | 超级用户密钥。仅用于在 userdebug build 和 eng build 上进行测试。与 user build 无关。 |
1 | shell_key | N/A | 可用于 shell 的命名空间。主要用于测试,但也可从命令行用于 user build。 |
100 | vold_key | N/A | 供 vold 使用。 |
101 | odsing_key | N/A | 供设备端签名守护程序使用。 |
102 | wifi_key | AID_WIFI(1010) | 供 Android 的 Wi-Fi 子系统(包括 wpa_supplicant)使用。 |
120 | resume_on_reboot_key | AID_SYSTEM(1000) | 供 Android 的系统服务器用于支持重新启动时恢复。 |
访问矢量
SELinux 的 keystore_key
类已过时很长时间,其中一些权限(例如 verify
或 sign
)已失去意义。以下是密钥库 2.0 将强制执行的新权限集 keystore2_key
。
权限 | 含义 |
---|---|
delete
|
在从密钥库中移除密钥时检查是否具有此权限。 |
get_info
|
在请求密钥的元数据时检查是否具有此权限。 |
grant
|
调用方需要具有此权限才能在目标上下文中创建密钥授权。 |
manage_blob
|
调用方可以在指定的 SELinux 命名空间中使用 DOMAIN_BLOB ,从而自行管理 blob。这对于 vold 特别有用。 |
rebind
|
此权限用于控制别名是否可以重新绑定到新密钥。必须具有此权限才能执行插入,并且这意味着之前绑定的密钥将被删除。它是一个插入权限,但更好地体现了密钥库的语义。 |
req_forced_op
|
具有此权限的客户端可以创建不可调整的操作,并且除非所有操作槽位都被不可调整的操作占用,否则操作创建绝不会失败。 |
update
|
必须具有此权限才能更新密钥的子组件。 |
use
|
在创建使用密钥材料(例如用于签名、加密、解密)的 Keymint 操作时检查是否具有此权限。 |
use_dev_id
|
必须具有此权限才能生成设备识别信息(例如设备 ID 认证)。 |
此外,我们还在 SELinux 安全类 keystore2
中拆分出一组非密钥特定的密钥库权限:
权限 | 含义 |
---|---|
add_auth
|
必须提供身份验证提供方(例如 Gatekeeper 或 BiometricsManager)才能添加身份验证令牌。 |
clear_ns
|
此权限以前称为 clear_uid,允许命名空间的非所有者删除该命名空间中的所有密钥。 |
list
|
系统要求具有此权限才能按各种属性(例如所有权或身份验证受限性)枚举密钥。调用方在枚举自己的命名空间时不需要此权限。此操作在 get_info 权限范围内。 |
lock
|
此权限允许锁定密钥库(即取消主密钥),使身份验证绑定密钥不可用且不可创建。 |
reset
|
此权限允许将密钥库重置为出厂默认设置,并删除对 Android OS 的运行不重要的所有密钥。 |
unlock
|
必须具有此权限才能尝试解锁身份验证绑定密钥的主密钥。 |