Keystore2 Key Contexts
本文面向已经读过 安全上下文、File Contexts 和 Service Contexts 的读者。这里的“key context”不是给某个文件路径贴标签,而是把一个整数 SELinux 命名空间 映射成一个 SELinux target context,供 keystore2 在每次密钥操作前做 keystore2_key 类访问检查。
文章解决一个具体问题:给定 KeyDescriptor { domain, nspace, alias, blob },读者如何从调用入口追到最终的 target context、数据库记录、KeyMint blob 和权限结果。它不展开 KeyMint 算法参数、认证令牌协议或 Java 密码学 SPI 的全部实现;这些对象只在它们改变命名空间权限或生命周期的位置出现。读完后,你应能判断一个“权限被拒绝”“命名空间不存在”“别名重绑定后旧 key-id 失效”或“BLOB 删除失败”分别应该查哪一层源码。
1. 问题边界
1.1 两种命名空间
Keystore2 同时支持两套看起来相似、实际上所有权不同的 namespace:
| 域 | namespace 的含义 | owner 如何确定 | 是否查 keystore2_key_contexts |
|---|---|---|---|
Domain.APP | 调用进程的 UID | Binder 调用者 UID | 否,target 是当前 keystore context |
Domain.SELINUX | policy 配置的整数 | KeyDescriptor.nspace | 是,整数转换为 target context |
Domain.APP 的 nspace 会被忽略;Domain.SELINUX 的 nspace 是稳定的 policy 接口。不要把 App UID(例如 10000)当成 SELinux namespace,也不要把 u:r:wifi_mainline_supplicant:s0 这样的进程域直接写入上下文文件:文件中的 label 是 u:object_r:wifi_key:s0 形式的对象 context,随后由 selinux_check_access 把调用者进程 context 与该对象 context 配对。
图中两条路径在权限检查处汇合,但只有 SELINUX 路径读取映射文件。这个差异解释了为什么普通应用密钥不需要新增 keystore2_key_contexts 条目,而 Wi-Fi、LockSettings 或 KeyChain 的共享密钥必须同时拥有 namespace 条目和对应 allow 规则。
1.2 对象与消费者
命名空间映射不是密钥材料本身。它由几个不同 owner 分层消费:
| 对象 | owner | 主要消费者 | 失败后的状态 |
|---|---|---|---|
| 文本映射 | system/sepolicy 构建系统 | libkeystore2_selinux 的 label backend | backend 打不开时服务不能正常工作 |
| target context | keystore2 权限模块 | selinux_check_access | 返回 PermissionDenied 或系统错误 |
| alias/keyentry | Keystore2 SQLite 数据库 | service.rs、security_level.rs | 事务回滚,旧记录保持原状 |
| KeyMint blob | KeyMint 实例或数据库 blob 表 | IKeyMintDevice、操作对象 | 由 GC 或 KeyMint deleteKey 清理 |
| grant 行 | per-boot persistent.grant 表 | Domain::GRANT 查找 | ungrant、用户清理或 key 删除时移除 |
命名空间只决定访问控制的 target,不负责创建 SQLite 行,也不负责把 blob 写入 TEE。后面每条流程都要把这几个 owner 分开看。
2. 描述符入口
2.1 AIDL域语义
源码文件:system/hardware/interfaces/keystore2/aidl/android/system/keystore2/Domain.aidl
@VintfStability
@Backing(type="int")
enum Domain {
APP = 0,
GRANT = 1,
SELINUX = 2,
BLOB = 3,
KEY_ID = 4,
}源码文件:system/hardware/interfaces/keystore2/aidl/android/system/keystore2/KeyDescriptor.aidl
@VintfStability
@RustDerive(Clone=true, Eq=true, PartialEq=true, Ord=true, PartialOrd=true)
parcelable KeyDescriptor {
Domain domain = Domain.APP;
long nspace; /* namespace is a keyword in C++, so we use another field name. */
/** A free form client-selected key name. */
@nullable String alias;
/** Opaque encrypted KeyMint blob, absent for database-backed aliases. */
@nullable byte[] blob;
}源码注释给出了五种域的查找契约:APP 使用调用 UID,SELINUX 使用 nspace,GRANT 使用 grant id,KEY_ID 使用数据库中的 key id,BLOB 直接携带 blob。BLOB 不进入数据库,因此后续删除必须走 IKeystoreSecurityLevel.deleteKey;KEY_ID 不能用于生成新 key,它只是数据库返回的稳定句柄。
2.2 Java调用方
源码文件:frameworks/base/keystore/java/android/security/keystore2/AndroidKeyStoreSpi.java
private KeyDescriptor makeKeyDescriptor(@NonNull String alias) {
KeyDescriptor descriptor = new KeyDescriptor();
descriptor.domain = getTargetDomain();
descriptor.nspace = mNamespace; // Ignored for Domain.APP.
descriptor.alias = alias;
descriptor.blob = null;
return descriptor;
}
private @Domain int getTargetDomain() {
return mNamespace == KeyProperties.NAMESPACE_APPLICATION
? Domain.APP
: Domain.SELINUX;
}engineLoad() 默认把 mNamespace 设为 NAMESPACE_APPLICATION;只有 AndroidKeyStoreLoadStoreParameter 才能选显式 namespace。这样 Java 的普通 KeyStore 操作自然落到 APP 域,系统组件通过 setNamespace() 才会发出 SELINUX 域描述符。
源码文件:frameworks/base/keystore/java/android/security/keystore/KeyGenParameterSpec.java
@SystemApi
@NonNull
public Builder setNamespace(@KeyProperties.Namespace int namespace) {
// The numeric value must have a matching keystore2_key_contexts entry.
mNamespace = namespace;
return this;
}这里没有在 Java 层验证 namespace 是否存在;验证发生在 native selabel_lookup。因此“整数合法”与“设备 policy 配置了该整数”是两个不同条件。
2.3 生成调用
源码文件:frameworks/base/keystore/java/android/security/keystore2/AndroidKeyStoreKeyPairGeneratorSpi.java
KeyDescriptor descriptor = new KeyDescriptor();
descriptor.alias = mEntryAlias;
descriptor.domain = mEntryNamespace == KeyProperties.NAMESPACE_APPLICATION
? Domain.APP
: Domain.SELINUX;
descriptor.nspace = mEntryNamespace;
descriptor.blob = null;
KeyMetadata metadata = iSecurityLevel.generateKey(
descriptor, mAttestKeyDescriptor,
constructKeyGenerationArguments(), flags, additionalEntropy);生成请求从 framework 进入 IKeystoreSecurityLevel.generateKey,而不是直接进入 IKeystoreService。这决定了 rebind 检查发生在真正调用 KeyMint 之前;数据库 owner 只在 KeyMint 成功后接管结果。
时序图中 DB 位于 KeyMint 成功之后。若权限检查失败或 KeyMint 返回错误,store_new_key 不会执行;若数据库事务失败,代码必须进入后续 GC/错误处理,不能把“KeyMint 已生成”误认为“alias 已可用”。
3. 映射文件
3.1 Android17条目
源码文件:system/sepolicy/private/keystore2_key_contexts
0 u:object_r:su_key:s0
1 u:object_r:shell_key:s0
100 u:object_r:vold_key:s0
101 u:object_r:odsign_key:s0
102 u:object_r:wifi_key:s0
103 u:object_r:locksettings_key:s0
104 u:object_r:keychain_key:s0
120 u:object_r:resume_on_reboot_key:s0这些条目只声明“整数到对象 context”的映射。用途和权限来自相邻的 type 与 allow 规则,而不是由数字本身推断:
| namespace | type | 典型 owner | 关键用途 |
|---|---|---|---|
| 0 | su_key | su | native 测试 |
| 1 | shell_key | shell | native 测试与 shell 测试 |
| 100 | vold_key | vold | 需要 manage_blob 的存储密钥 |
| 101 | odsign_key | odsign | ART on-device signing |
| 102 | wifi_key | Wi-Fi 组件 | Wi-Fi 共享密钥 |
| 103 | locksettings_key | system_server | LockSettings/RoR 密钥 |
| 104 | keychain_key | KeyChainSystemService | KeyChain 密钥 |
| 120 | resume_on_reboot_key | system_server | resume-on-reboot |
keychain_key 是 Android 17 当前文件中的条目;只读取旧版本文件会漏掉它。反过来,映射文件中存在条目也不代表任意 domain 可以访问它,仍需检查 keystore2_key 的 allow/neverallow 规则。
3.2 类型声明
源码文件:system/sepolicy/private/keystore_keys.te
type shell_key, keystore2_key_type;
type su_key, keystore2_key_type;
type vold_key, keystore2_key_type;
type odsign_key, keystore2_key_type;
type locksettings_key, keystore2_key_type;
type keychain_key, keystore2_key_type;
type resume_on_reboot_key, keystore2_key_type;源码文件:system/sepolicy/public/keystore.te、system/sepolicy/public/keystore_keys.te
type keystore, domain, keystore2_key_type;
type wifi_key, keystore2_key_type;keystore2_key_type 是 policy 的聚合属性,不是一个可直接写入 contexts 文件的 label。keystore2_key_contexts 中写的是具体 type;编译后的权限规则再通过属性把多个 type 分组。
4. 构建产物
4.1 Soong模块
源码文件:system/sepolicy/contexts/Android.bp
se_build_files {
name: "keystore2_key_contexts_files",
srcs: ["keystore2_key_contexts"], // One source is split by policy partition.
}
keystore2_key_contexts {
name: "plat_keystore2_key_contexts",
defaults: ["contexts_flags_defaults"],
srcs: [":keystore2_key_contexts_files{.plat_private}"],
}
keystore2_key_contexts {
name: "system_ext_keystore2_key_contexts",
defaults: ["contexts_flags_defaults"],
srcs: [":keystore2_key_contexts_files{.system_ext_private}"],
system_ext_specific: true,
}
keystore2_key_contexts {
name: "product_keystore2_key_contexts",
defaults: ["contexts_flags_defaults"],
srcs: [":keystore2_key_contexts_files{.product_private}"],
product_specific: true,
}
keystore2_key_contexts {
name: "vendor_keystore2_key_contexts",
defaults: ["contexts_flags_defaults"],
srcs: [
":keystore2_key_contexts_files{.plat_vendor}",
":keystore2_key_contexts_files{.vendor}",
":keystore2_key_contexts_files{.reqd_mask}",
],
soc_specific: true,
}同一输入文件经过 Soong 的 partition variant 选择,形成四个安装模块。vendor_keystore2_key_contexts 不是把 platform 文件简单复制到 vendor;它接收 platform-vendor、vendor 和 required-mask 输入,最终由 vendor policy 的安装路径消费。
4.2 M4与删注释
源码文件:system/sepolicy/build/soong/selinux_contexts.go
func keystoreKeyFactory() android.Module {
m := newModule()
// Keystore key files use the common M4 pipeline; no fc_sort backend is added.
m.build = m.buildGeneralContexts
return m
}源码文件:system/sepolicy/build/soong/selinux_contexts.go
func (m *selinuxContextsModule) buildGeneralContexts(
ctx android.ModuleContext, inputs android.Paths) android.Path {
builtContext := pathForModuleOut(ctx, ctx.ModuleName()+"_m4out")
// Add an empty line between concatenated input files before running M4.
newlineFile := pathForModuleOut(ctx, "newline")
rule.Command().Text("echo").FlagWithOutput("> ", newlineFile)
rule.Command().
Tool(ctx.Config().PrebuiltBuildTool(ctx, "m4")).
Text("--fatal-warnings -s").
Inputs(inputsWithNewline).
FlagWithOutput("> ", builtContext)
if proptools.Bool(m.properties.Remove_comment) {
// Comments are removed only after M4 has consumed them.
rule.Command().
Text("sed -e 's/#.*$//' -e '/^$/d'").
Input(builtContext).
FlagWithOutput("> ", remove_comment_output)
}
return builtContext
}这里的关键顺序是“拼接输入 → M4 → 可选删注释”。命名空间文本没有 fc_sort:它不是路径正则数据库,不能把 file contexts 的排序规则套过来。若 M4 或后续复制失败,安装文件不会更新;构建输出的消费者是分区 etc/selinux 目录,而不是运行时数据库目录。
4.3 安装标签
源码文件:system/sepolicy/private/file_contexts
/system/etc/selinux/plat_keystore2_key_contexts \
u:object_r:keystore2_key_contexts_file:s0
/(system_ext|system/system_ext)/etc/selinux/system_ext_keystore2_key_contexts \
u:object_r:keystore2_key_contexts_file:s0
/(product|system/product)/etc/selinux/product_keystore2_key_contexts \
u:object_r:keystore2_key_contexts_file:s0
/(odm|vendor/odm)/etc/selinux/odm_keystore2_key_contexts \
u:object_r:keystore2_key_contexts_file:s0源码文件:system/sepolicy/private/keystore.te
allow keystore keystore2_key_contexts_file:file r_file_perms;这是一条容易漏掉的边界:keystore2_key_contexts_file 是“映射文件”的 file type,wifi_key 等是“密钥 namespace”的 target type。前者控制 daemon 能否读配置,后者参与 keystore2_key 权限判断,两者不能互换。
5. 查找实现
5.1 Rust后端
源码文件:system/security/keystore2/selinux/src/lib.rs
pub struct KeystoreKeyBackend {
handle: *mut selinux::selabel_handle,
}
impl KeystoreKeyBackend {
const BACKEND_TYPE: i32 = SELABEL_CTX_ANDROID_KEYSTORE2_KEY as i32;
pub fn new() -> Result<Self> {
init_logger_once();
// libselinux's access-vector cache is serialized by this process-wide lock.
let _lock = LIB_SELINUX_LOCK.lock().unwrap();
let handle = unsafe {
selinux::selinux_android_keystore2_key_context_handle()
};
if handle.is_null() {
return Err(anyhow!(Error::sys("Failed to open KeystoreKeyBackend")));
}
Ok(KeystoreKeyBackend { handle })
}
}
impl Backend for KeystoreKeyBackend {
fn lookup(&self, key: &str) -> Result<Context> {
let mut con: *mut c_char = ptr::null_mut();
let c_key = CString::new(key).context("namespace is not a valid C string")?;
let _lock = LIB_SELINUX_LOCK.lock().unwrap();
match unsafe {
selinux::selabel_lookup(
self.handle, &mut con, c_key.as_ptr(), Self::BACKEND_TYPE)
} {
0 if !con.is_null() => Ok(Context::Raw(con)),
0 => Err(anyhow!(Error::sys("selabel_lookup returned NULL context"))),
_ => Err(anyhow!(io::Error::last_os_error())),
}
}
}new() 打开 Android 专用 backend;lookup() 把 namespace 整数转成字符串后调用 selabel_lookup。Context::Raw 在 Drop 时调用 freecon,backend 在 Drop 时调用 selabel_close,所以成功查找得到的 C 字符串不会泄漏。进程级互斥锁不是装饰:源码注释说明 Android 导出的 libselinux 没有可用的 avc lock callback,Keystore2 因而串行保护相关调用。
5.2 权限映射
源码文件:system/security/keystore2/src/permission.rs
static KEYSTORE2_KEY_LABEL_BACKEND: LazyLock<selinux::KeystoreKeyBackend> =
LazyLock::new(|| selinux::KeystoreKeyBackend::new().unwrap());
fn lookup_keystore2_key_context(namespace: i64) -> anyhow::Result<selinux::Context> {
// The backend key is the decimal namespace from KeyDescriptor.nspace.
KEYSTORE2_KEY_LABEL_BACKEND.lookup(&namespace.to_string())
}源码文件:system/security/keystore2/src/permission.rs
pub fn check_key_permission(
caller_uid: AppUid,
caller_ctx: &CStr,
perm: KeyPerm,
key: &KeyDescriptor,
access_vector: &Option<KeyPermSet>,
) -> anyhow::Result<()> {
// A grant vector can satisfy the requested bit before SELinux lookup.
if let Some(access_vector) = access_vector {
if access_vector.includes(perm) {
return Ok(());
}
}
let target_context = match key.domain {
Domain::APP => {
if caller_uid.0 != key.nspace {
return Err(selinux::Error::perm())
.context("Trying to access key without ownership.");
}
getcon().context("getcon failed.")?
}
Domain::SELINUX => lookup_keystore2_key_context(key.nspace)?,
Domain::GRANT => {
return Err(selinux::Error::perm())
.context(format!("permission {} was not granted", perm.name()));
}
Domain::KEY_ID => {
// The database must resolve KEY_ID before this function is called.
return Err(KsError::sys())
.context("Cannot check permission for Domain::KEY_ID.");
}
Domain::BLOB => {
let tctx = lookup_keystore2_key_context(key.nspace)?;
// Raw blobs require an extra capability in addition to the operation bit.
selinux::check_permission(caller_ctx, &tctx, KeyPerm::ManageBlob)?;
tctx
}
_ => return Err(KsError::Rc(ResponseCode::INVALID_ARGUMENT)).into(),
};
selinux::check_permission(caller_ctx, &target_context, perm)
}这段代码包含三个安全不变量:APP 必须由 UID owner 访问;GRANT 不能绕过数据库提供的 access vector;BLOB 即使请求 use,也必须额外拥有 manage_blob。Domain::KEY_ID 在这里报系统错误不是遗漏,而是提醒调用方先把 key-id 解析回真实的 APP/SELINUX access tuple。
5.3 系统调用包装
源码文件:system/security/keystore2/selinux/src/lib.rs
pub fn check_access(
source: &CStr, target: &CStr, tclass: &str, perm: &str
) -> Result<()> {
init_logger_once();
let c_tclass = CString::new(tclass)?;
let c_perm = CString::new(perm)?;
let _lock = LIB_SELINUX_LOCK.lock().unwrap();
match unsafe {
selinux::selinux_check_access(
source.as_ptr(), target.as_ptr(),
c_tclass.as_ptr(), c_perm.as_ptr(), ptr::null_mut())
} {
0 => Ok(()),
_ if io::Error::last_os_error().kind() == io::ErrorKind::PermissionDenied =>
Err(anyhow!(Error::PermissionDenied)),
_ => Err(anyhow!(io::Error::last_os_error())),
}
}PermissionDenied 与系统错误被分开包装。上层可以把前者映射成 ResponseCode::PERMISSION_DENIED,而 backend 打不开、上下文字符串非法或 libselinux 调用失败则应保留为系统错误,排障时不能把两者都归为“policy 没放行”。
6. 权限模型
6.1 Key权限
源码文件:system/security/keystore2/src/permission.rs
pub enum KeyPerm {
ConvertStorageKeyToEphemeral = KeyPermission::CONVERT_STORAGE_KEY_TO_EPHEMERAL.0,
Delete = KeyPermission::DELETE.0,
GenUniqueId = KeyPermission::GEN_UNIQUE_ID.0,
GetInfo = KeyPermission::GET_INFO.0,
Grant = KeyPermission::GRANT.0,
ManageBlob = KeyPermission::MANAGE_BLOB.0,
Rebind = KeyPermission::REBIND.0,
ReqForcedOp = KeyPermission::REQ_FORCED_OP.0,
Update = KeyPermission::UPDATE.0,
Use = KeyPermission::USE.0,
UseDevId = KeyPermission::USE_DEV_ID.0,
}权限名称来自 AIDL KeyPermission 位图,并通过宏映射为 policy class 的字符串。KeyPermSet::includes() 只做位运算;真正的 SELinux 判断仍逐个调用 selinux_check_access。因此 grant 的 access vector 只能表达“被授予哪些位”,不能凭空创造 policy 中不存在的权限。
6.2 平台规则
源码文件:system/sepolicy/private/system_server.te
allow system_server keystore:keystore2_key {
delete use_dev_id grant get_info rebind update use
};
allow system_server wifi_key:keystore2_key {
delete get_info rebind update use
};
allow system_server locksettings_key:keystore2_key {
delete get_info rebind update use
};
allow system_server keychain_key:keystore2_key {
delete get_info grant rebind update use
};源码文件:system/sepolicy/private/vold.te、system/sepolicy/private/odsign.te
allow vold vold_key:keystore2_key {
convert_storage_key_to_ephemeral delete get_info manage_blob
rebind req_forced_op update use
};
allow odsign odsign_key:keystore2_key {
delete get_info rebind use
};对比 vold_key 和 odsign_key 可以看出 namespace 的安全价值:两者都能通过 SELINUX 域定位 target,但只有 vold 被授予 manage_blob 和 convert_storage_key_to_ephemeral。映射表本身没有赋权,allow 规则才是消费者可执行的权限边界。
6.3 默认拒绝
源码文件:system/sepolicy/private/domain.te
neverallow { domain -priv_app_all -gmscore_app } *:keystore2_key gen_unique_id;
neverallow { domain -system_server } *:keystore2_key use_dev_id;新增 namespace 时,先确认目标 type 属于 keystore2_key_type,再为最小调用者增加 allow;不能用一个泛化的 attribute allow 取代所有专用类型,否则会同时扩大 use、grant 或 manage_blob 的范围。
7. 数据库状态
7.1 access tuple
源码文件:system/security/keystore2/src/database.rs
fn load_access_tuple(
tx: &Transaction,
key: &KeyDescriptor,
key_type: KeyType,
caller_uid: AppUid,
) -> Result<KeyAccessInfo> {
match key.domain {
Domain::APP | Domain::SELINUX => {
let mut access_key = key.clone();
if access_key.domain == Domain::APP {
// APP descriptors are always completed with the Binder caller UID.
access_key.nspace = caller_uid.0;
}
let key_id = Self::load_key_entry_id(tx, &access_key, key_type)?;
Ok(KeyAccessInfo { key_id, descriptor: access_key, vector: None })
}
Domain::GRANT => {
let (key_id, access_vector) = tx.query_row(
"SELECT keyentryid, access_vector FROM persistent.grant
WHERE grantee = ? AND id = ? AND
(SELECT state FROM persistent.keyentry WHERE id = keyentryid) = ?;",
params![caller_uid.0, key.nspace, KeyLifeCycle::Live],
|row| Ok((row.get(0)?, row.get(1)?),)
)?;
Ok(KeyAccessInfo {
key_id,
descriptor: key.clone(),
vector: Some(access_vector.into()),
})
}
Domain::KEY_ID => {
// Resolve the owner domain and namespace before checking SELinux.
let (domain, namespace) = tx.query_row(
"SELECT domain, namespace FROM persistent.keyentry
WHERE id = ? AND state = ?;",
params![key.nspace, KeyLifeCycle::Live],
|row| Ok((Domain(row.get(0)?), row.get(1)?)),
)?;
let access_key = KeyDescriptor { domain, nspace: namespace, ..key.clone() };
Ok(KeyAccessInfo { key_id: key.nspace, descriptor: access_key, vector: None })
}
_ => Err(anyhow!(KsError::Rc(ResponseCode::INVALID_ARGUMENT))),
}
}这段实现把“用户提供的描述符”转换成“可执行的 access tuple”。APP 的 namespace 在这里被强制改成 calling UID;SELINUX 保留调用者提供的固定 namespace;KEY_ID 从 keyentry 反查 owner。只有转换完成,权限函数才知道应该查哪个 target context。
7.2 加载与锁
源码文件:system/security/keystore2/src/database.rs
let tx = self.conn.unchecked_transaction()?;
let access = Self::load_access_tuple(&tx, key, key_type, caller_uid)?;
// Security critical: do not continue when the callback denies access.
check_permission(&access.descriptor, access.vector)?;
let (key_id_guard, tx) = match key_id_guard {
None => match KEY_ID_LOCK.try_get(access.key_id) {
None => {
// Alias may be rebound while waiting, so roll back and reload by key id.
tx.rollback()?;
let key_id_guard = KEY_ID_LOCK.get(access.key_id);
let tx = self.conn.unchecked_transaction()?;
Self::load_access_tuple(
&tx,
&KeyDescriptor { domain: Domain::KEY_ID,
nspace: access.key_id, ..Default::default() },
key_type,
caller_uid,
)?;
(key_id_guard, tx)
}
Some(guard) => (guard, tx),
},
Some(guard) => (guard, tx),
};别名查找和 key-id 锁之间存在竞态:等待锁时别名可能已经指向新 key。代码通过回滚旧事务、取得锁、按 key-id 重新加载来避免把新 alias 的 blob 错当成旧 metadata。这个锁属于数据库 owner,不是 SELinux 锁;前者保护 key 生命周期,后者保护 libselinux 调用。
状态图的 Garbage 不是“已经物理删除”的同义词。数据库会先标记未引用记录,GC 再根据 blob metadata 找到所属 KeyMint 实例并调用删除;因此排查残留密钥时要同时看事务状态、grant 行和 GC 日志。
8. 创建与导入
8.1 生成
源码文件:system/security/keystore2/src/security_level.rs
fn generate_key(
&self,
key: &KeyDescriptor,
attest_key_descriptor: Option<&KeyDescriptor>,
params: &[KeyParameter],
flags: i32,
_entropy: &[u8],
) -> Result<KeyMetadata> {
if key.domain != Domain::BLOB && key.alias.is_none() {
return Err(error::Error::Km(ErrorCode::INVALID_ARGUMENT))
.context(ks_err!("Alias must be specified"));
}
let caller_uid = AppUid::calling();
let key = match key.domain {
Domain::APP => KeyDescriptor {
domain: key.domain,
nspace: caller_uid.0,
alias: key.alias.clone(),
blob: None,
},
_ => key.clone(),
};
// Generate/import rebinding is authorization, not a database side effect.
check_key_permission(KeyPerm::Rebind, &key, &None)?;
let creation_result = self.generate_key_and_retry_on_att_id_mismatch(
params, attest_key_descriptor)?;
let user = caller_uid.owning_user();
self.store_new_key(key, creation_result, user, Some(flags))
}Domain::APP 在生成入口先被规范化为 calling UID;Domain::SELINUX 则保留固定 namespace。Rebind 在 KeyMint 调用前检查,原因是同 alias 生成新 key 会让旧记录失去 alias 引用;没有该权限不能触发替换。
8.2 入库与重绑定
源码文件:system/security/keystore2/src/security_level.rs、system/security/keystore2/src/database.rs
下面是 store_new_key 调用点的控制流摘录;证书链整理和 super-key 加密参数不影响 namespace 结论,因此用注释标出省略位置。
// security_level.rs: only database-backed domains call store_new_key.
let key = match key.domain {
Domain::BLOB => KeyDescriptor {
domain: Domain::BLOB,
blob: Some(key_blob.to_vec()),
..Default::default()
},
_ => DB.with(|db| {
// ... super-key handling and certificate assembly are omitted here.
let key_id = db.borrow_mut().store_new_key(
&key, KeyType::Client, &key_parameters, &blob_info,
&cert_info, &key_metadata, &self.km_uuid)?;
Ok(KeyDescriptor { domain: Domain::KEY_ID, nspace: key_id.id(), ..Default::default() })
})?,
};源码文件:system/security/keystore2/src/database.rs
pub fn store_new_key(
&mut self,
key: &KeyDescriptor,
key_type: KeyType,
params: &[KeyParameter],
blob_info: &BlobInfo,
cert_info: &CertificateInfo,
metadata: &KeyMetaData,
km_uuid: &Uuid,
) -> Result<KeyIdGuard> {
let (alias, domain, namespace) = match key {
KeyDescriptor { alias: Some(alias), domain: Domain::APP, nspace, blob: None }
| KeyDescriptor { alias: Some(alias), domain: Domain::SELINUX, nspace, blob: None } =>
(alias, key.domain, nspace),
_ => return Err(KsError::Rc(ResponseCode::INVALID_ARGUMENT)),
};
self.with_transaction(Immediate("TX_store_new_key"), |tx| {
let key_id = Self::create_key_entry_internal(tx, &domain, namespace, key_type, km_uuid)?;
Self::set_blob_internal(tx, key_id.id(), SubComponentType::KEY_BLOB,
Some(blob_info.blob), Some(blob_info.metadata))?;
Self::insert_keyparameter_internal(tx, &key_id, params)?;
metadata.store_in_db(key_id.id(), tx)?;
// Rebinding may orphan the previous key; the return value schedules GC.
let need_gc = Self::rebind_alias(tx, &key_id, alias, &domain, namespace, key_type)?;
Ok(key_id).do_gc(need_gc)
})
}对于 APP/SELINUX,KeyMint 返回的 blob、参数、证书和 metadata 在一个 Immediate SQLite 事务中写入,最后才执行 alias rebind。返回的 KeyMetadata.key 是 Domain::KEY_ID,后续操作可绕过 alias 重绑定带来的歧义。BLOB 分支完全不同:blob 直接返回给调用者,Keystore2 没有可恢复副本,调用者必须负责持久化。
8.3 导入
源码文件:system/security/keystore2/src/security_level.rs
下面保留 import_key 中决定域、namespace 和授权时机的真实分支;KeyMint 参数解析分支与命名空间无关,未展开。
fn import_key(
&self,
key: &KeyDescriptor,
_attestation_key: Option<&KeyDescriptor>,
params: &[KeyParameter],
flags: i32,
key_data: &[u8],
) -> Result<KeyMetadata> {
if key.domain != Domain::BLOB && key.alias.is_none() {
return Err(error::Error::Km(ErrorCode::INVALID_ARGUMENT))?;
}
let caller_uid = AppUid::calling();
let key = if key.domain == Domain::APP {
KeyDescriptor { nspace: caller_uid.0, ..key.clone() }
} else {
key.clone()
};
// ... KeyMint format validation and attestation handling are omitted.
// Import has the same alias-replacement boundary as generation.
check_key_permission(KeyPerm::Rebind, &key, &None)?;
let creation_result = self.keymint.importKey(params, key_data)?;
self.store_new_key(key, creation_result, caller_uid.owning_user(), Some(flags))
}生成和导入都用 Rebind,但 KeyMint backend 错误与数据库错误的恢复动作不同:前者没有新数据库行;后者可能已经得到一个需要 GC 的 superseded blob。看到 KEY_NOT_FOUND 时不要直接归因于 SELinux,先区分“没有 alias 行”“grant 指向的 key 已非 Live”和“KeyMint blob 操作失败”。
9. 使用、删除与BLOB
9.1 读取与操作
源码文件:system/security/keystore2/src/service.rs
下面保留 get_key_entry 的真实调用顺序;返回 KeyMetadata 的字段装配只压缩了错误上下文,不改变 owner、锁和权限顺序。
fn get_key_entry(&self, key: &KeyDescriptor) -> Result<KeyEntryResponse> {
let caller_uid = AppUid::calling();
let super_key = SUPER_KEY
.read()
.unwrap()
.get_credential_encrypted_key_by_user_id(caller_uid.owning_user());
let (key_id_guard, mut key_entry) = DB
.with(|db| {
LEGACY_IMPORTER.with_try_import(key, caller_uid, super_key, || {
db.borrow_mut().load_key_entry(
key,
KeyType::Client,
KeyEntryLoadBits::PUBLIC,
caller_uid,
|k, av| check_key_permission(KeyPerm::GetInfo, k, &av),
)
})
})
.context(ks_err!("while trying to load key info."))?;
let i_sec_level = if !key_entry.pure_cert() {
Some(
self.get_i_sec_level_by_uuid(key_entry.km_uuid())
.context(ks_err!("Trying to get security level proxy."))?,
)
} else {
None
};
Ok(KeyEntryResponse {
iSecurityLevel: i_sec_level,
metadata: KeyMetadata {
key: KeyDescriptor {
domain: Domain::KEY_ID,
nspace: key_id_guard.id(),
..Default::default()
},
keySecurityLevel: self.uuid_to_sec_level(key_entry.km_uuid()),
certificate: key_entry.take_cert(),
certificateChain: key_entry.take_cert_chain(),
// The remaining fields convert metadata and authorizations for the AIDL reply.
modificationTimeMs: key_entry.metadata().creation_date()
.map(|d| d.to_millis_epoch())
.ok_or(Error::Rc(ResponseCode::VALUE_CORRUPTED))?,
authorizations: key_parameters_to_authorizations(key_entry.into_key_parameters()),
},
})
}读取路径先由数据库解析 access tuple,再执行 GetInfo,通过后才加载 public component。真正的私钥使用会把返回的 Domain::KEY_ID 交给 createOperation,在 security level 层再次执行 Use;“能够读取证书”不等于“能够启动私钥操作”。
源码文件:system/security/keystore2/src/security_level.rs
fn delete_key(&self, key: &KeyDescriptor) -> Result<()> {
if key.domain != Domain::BLOB {
return Err(error::Error::Km(ErrorCode::INVALID_ARGUMENT));
}
let key_blob = key.blob.as_ref()
.ok_or(error::Error::Km(ErrorCode::INVALID_ARGUMENT))?;
// BLOB deletion checks both the namespace permission and manage_blob in the helper.
check_key_permission(KeyPerm::Delete, key, &None)?;
self.keymint.deleteKey(key_blob)
}数据库域的 deleteKey 由 KeystoreService::delete_key 调用 unbind_key,它会在事务中删除 alias 引用和相关 grants;BLOB 域则没有 alias、keyentry 或 grant,直接把 opaque blob 交给 KeyMint。两条路径的日志、错误码和恢复方式不能混写。
9.2 生命周期关系
图中 KeyIdGuard 只保护数据库访问期间的稳定性;它不是 SELinux capability,也不会让调用者获得新的 Use 权限。grant 反而把 access vector 作为数据库状态带入 check_key_permission,所以 GRANT 的能力上限由 grantor 当时拥有的权限决定。
10. Grant与重绑定
10.1 授权规则
源码文件:system/security/keystore2/src/permission.rs
pub fn check_grant_permission(
caller_uid: AppUid,
caller_ctx: &CStr,
access_vec: KeyPermSet,
key: &KeyDescriptor,
) -> anyhow::Result<()> {
let target_context = match key.domain {
Domain::APP => {
if caller_uid.0 != key.nspace {
return Err(selinux::Error::perm())
.context("Trying to access key without ownership.");
}
getcon()?
}
Domain::SELINUX => lookup_keystore2_key_context(key.nspace)?,
_ => return Err(KsError::sys()).context("Cannot grant this domain."),
};
selinux::check_permission(caller_ctx, &target_context, KeyPerm::Grant)?;
if access_vec.includes(KeyPerm::Grant) {
// A grant must never be able to create another grant.
return Err(selinux::Error::perm());
}
for p in access_vec {
selinux::check_permission(caller_ctx, &target_context, p)?;
}
Ok(())
}grant 需要两层证明:调用者对目标 namespace 有 grant,并且 access vector 中每一位都已经被调用者自身拥有;grant 位本身被明确拒绝,防止 grant 链无限扩散。SELINUX namespace 的 owner 由 target context 和 policy 决定,APP namespace 还额外要求 UID owner。
10.2 Grant行
源码文件:system/security/keystore2/src/database.rs
pub fn grant(
&mut self,
key: &KeyDescriptor,
caller_uid: AppUid,
grantee_uid: AppUid,
access_vector: KeyPermSet,
check_permission: impl Fn(&KeyDescriptor, &KeyPermSet) -> Result<()>,
) -> Result<KeyDescriptor> {
self.with_transaction(Immediate("TX_grant"), |tx| {
let access = Self::load_access_tuple(tx, key, KeyType::Client, caller_uid)
.context(ks_err!())?;
// Security critical: return before inserting or updating the grant row.
check_permission(&access.descriptor, &access_vector)
.context(ks_err!("check_permission failed"))?;
let grant_id = if let Some(grant_id) = tx
.query_row(
"SELECT id FROM persistent.grant
WHERE keyentryid = ? AND grantee = ?;",
params![access.key_id, grantee_uid.0],
|row| row.get(0),
)
.optional()? {
tx.execute(
"UPDATE persistent.grant SET access_vector = ? WHERE id = ?;",
params![i32::from(access_vector), grant_id],
)?;
grant_id
} else {
Self::insert_with_retry(|id| {
tx.execute(
"INSERT INTO persistent.grant (id, grantee, keyentryid, access_vector)
VALUES (?, ?, ?, ?);",
params![id, grantee_uid.0, access.key_id, i32::from(access_vector)],
)
})?
};
Ok(KeyDescriptor { domain: Domain::GRANT, nspace: grant_id, alias: None, blob: None })
.no_gc()
})
}grant id 是 per-boot 数据库生成的随机 id,返回给 grantee 的描述符只有 Domain::GRANT + nspace=id。grantee 再调用 getKeyEntry 时,数据库查询同时约束 grantee = calling UID 和 key state 为 Live;复制 grant descriptor 给别的 UID 不会生效。
10.3 删除与用户清理
源码文件:system/security/keystore2/src/database.rs
下面的删除语句对应源码中的四个 SQL 操作;为聚焦 namespace 条件,省略了每个调用后的错误上下文拼接,但保留实际表、筛选字段和 APP received-grant 分支。
pub fn unbind_key(
&mut self,
key: &KeyDescriptor,
key_type: KeyType,
caller_uid: AppUid,
check_permission: impl Fn(&KeyDescriptor, Option<KeyPermSet>) -> Result<()>,
) -> Result<()> {
let _wp = wd::watch("KeystoreDB::unbind_key");
self.with_transaction(Immediate("TX_unbind_key"), |tx| {
let access = Self::load_access_tuple(tx, key, key_type, caller_uid)
.context("Trying to get access tuple.")?;
// Security critical: do not remove rows when the callback denies access.
check_permission(&access.descriptor, access.vector)
.context("While checking permission.")?;
Self::remove_key_rows(tx, access.key_id)
.map(|need_gc| (need_gc, ()))
.context("Trying to remove key DB rows")
})
}
pub fn unbind_keys_for_namespace(&mut self, domain: Domain, namespace: i64) -> Result<()> {
let _wp = wd::watch("KeystoreDB::unbind_keys_for_namespace");
if !(domain == Domain::APP || domain == Domain::SELINUX) {
return Err(KsError::Rc(ResponseCode::INVALID_ARGUMENT)).context(ks_err!());
}
self.with_transaction(Immediate("TX_unbind_keys_for_namespace"), |tx| {
// Namespace cleanup deletes metadata, parameters and grants before key rows.
tx.execute(
"DELETE FROM persistent.keymetadata
WHERE keyentryid IN (SELECT id FROM persistent.keyentry
WHERE domain = ? AND namespace = ? AND key_type = ?);",
params![domain.0, namespace, KeyType::Client],
)?;
tx.execute(
"DELETE FROM persistent.keyparameter
WHERE keyentryid IN (SELECT id FROM persistent.keyentry
WHERE domain = ? AND namespace = ? AND key_type = ?);",
params![domain.0, namespace, KeyType::Client],
)?;
tx.execute(
"DELETE FROM persistent.grant
WHERE keyentryid IN (SELECT id FROM persistent.keyentry
WHERE domain = ? AND namespace = ? AND key_type = ?);",
params![domain.0, namespace, KeyType::Client],
)?;
if domain == Domain::APP {
// APP cleanup also removes grants received by this UID.
tx.execute("DELETE FROM persistent.grant WHERE grantee = ?;",
params![namespace])?;
}
tx.execute(
"DELETE FROM persistent.keyentry
WHERE domain = ? AND namespace = ? AND key_type = ?;",
params![domain.0, namespace, KeyType::Client],
)?;
Ok(()).need_gc()
})
}删除权限成功只意味着事务可以提交;KeyMint blob 的物理删除可能由 GC 延后执行。用户卸载或 namespace 清理还要移除 received grants,否则 grant 表会保留指向已不存在 key 的授权记录。
11. 失败边界
11.1 输入与查找错误
| 现象 | 直接原因 | 代码边界 | 不应采取的修复 |
|---|---|---|---|
INVALID_ARGUMENT | 非 BLOB 域缺 alias,或用 KEY_ID 生成 | AIDL 入口与 generate_key | 不能只把 alias 填成任意字符串掩盖域错误 |
| backend 打不开 | selinux_android_keystore2_key_context_handle() 返回空 | KeystoreKeyBackend::new | 不能改成默认 keystore context |
| namespace lookup 失败 | 无对应文本条目或 label 无效 | lookup_keystore2_key_context | 不能仅新增 allow 而不新增映射 |
PERMISSION_DENIED | target type 的具体权限未放行 | selinux_check_access | 不能把所有操作改成 * |
KEY_NOT_FOUND | alias、key-id 或 grant 不对应 Live 行 | load_access_tuple/数据库查询 | 不能把它当成 SELinux denial |
11.2 事务与恢复
生成/导入的顺序是:权限检查 → KeyMint → SQLite 事务。KeyMint 失败时没有 alias 行;SQLite 失败时事务回滚,但可能留下一个待 GC 的 superseded blob。读取时等待 KeyIdGuard 会先回滚,再按 key-id 重新加载,避免 alias 在等待期间改变。
11.3 BLOB边界
Domain::BLOB 的 namespace 仍要通过 keystore2_key_contexts 找到 target context,但 blob 本身不写入 SQLite。丢失返回给调用者的 blob 后,Keystore2 没有 alias 可以恢复它;因此 BLOB 适合 vold 等明确拥有持久化责任的组件,不适合普通 Java KeyStore alias。
12. 测试证据
12.1 SELinux包装测试
源码文件:system/security/keystore2/selinux/src/permission/tests.rs
#[test]
fn check_key_permission_domain_selinux() -> Result<()> {
let (sctx, namespace, is_su) = check_context()?;
let key = KeyDescriptor {
domain: Domain::SELINUX,
nspace: namespace as i64,
alias: None,
blob: None,
};
assert!(check_key_permission(AppUid(0), &sctx, KeyPerm::Use, &key, &None).is_ok());
assert!(check_key_permission(AppUid(0), &sctx, KeyPerm::Delete, &key, &None).is_ok());
if is_su {
assert!(check_key_permission(AppUid(0), &sctx, KeyPerm::Grant, &key, &None).is_ok());
} else {
assert_perm_failed!(check_key_permission(
AppUid(0), &sctx, KeyPerm::Grant, &key, &None));
}
Ok(())
}输入是实际进程 context(su 或 shell)和 namespace 0/1;断言覆盖 Use、Delete 以及特权相关的 Grant 差异。它证明 SELINUX namespace 会进入真实 policy 检查,但不证明设备上所有 100、102、104 等业务 namespace 都有同样权限。
源码文件:system/security/keystore2/selinux/src/concurrency_test.rs
for _ in 0..250 {
let (tctx, sctx, perm, class) = (
Context::new("u:object_r:keystore:s0").unwrap(),
Context::new(&format!("u:r:untrusted_app:s0:{}", cats)).unwrap(),
"use",
"keystore2_key",
);
// Many concurrent cache misses expose unsafe libselinux access.
check_access(&sctx, &tctx, class, perm).unwrap();
}这个测试让多个线程在不同 MLS category 下重复 selinux_check_access,并等待所有线程完成;断言是没有线程卡在 access-vector cache 的死循环。它验证进程级锁对并发调用的必要性,不验证某个 namespace 的业务 allow 规则。
12.2 集成测试
源码文件:system/security/keystore2/tests/grant_key.rs
// The grantor creates an SELINUX namespace key with an empty access vector.
let grant_key = generate_and_grant_selinux_key(GRANTEE_UID, KeyPermission::NONE.0).unwrap();
assert_eq!(grant_key.domain, Domain::GRANT);
// The grantee resolves the returned id and must be denied at getKeyEntry.
let result = get_granted_key(&keystore2, grant_key_nspace);
assert_eq!(Error::Rc(ResponseCode::PERMISSION_DENIED), result.unwrap_err());输入是 shell namespace 的 SELINUX key、目标 UID 和空 access vector;断言先确认返回 descriptor 是 GRANT,再确认 grantee 读取失败。它证明 grant access vector 会限制 GetInfo,不证明 grantor 能授予 Grant 位,也不覆盖 BLOB。
源码文件:system/security/keystore2/tests/key_id_domain.rs
let key_metadata = generate_ec_key(&sl, Domain::APP, -1, Some(alias.clone()), ...)?;
let key_entry_response = sl.keystore2.getKeyEntry(&KeyDescriptor {
domain: Domain::KEY_ID,
nspace: key_metadata.key.nspace,
alias: Some(alias),
blob: None,
})?;
assert_eq!(key_metadata.key, key_entry_response.metadata.key);
// KEY_ID is a lookup handle, not a generation domain.
let result = generate_ec_key(&sl, Domain::KEY_ID, 1, Some("bad".into()), ...);
assert_eq!(Error::Rc(ResponseCode::SYSTEM_ERROR), result.unwrap_err());第一组输入验证 alias 生成后可通过 key-id 取回同一 metadata;第二组把 KEY_ID 用在生成入口并断言系统错误。它证明数据库会把 key-id 解析成 owner tuple,也证明 KEY_ID 不能取代 APP/SELINUX 生成域;不证明 alias 重绑定本身的 GC 完成时机。
13. 排障路径
遇到失败时按调用边界逐层缩小,不要先改 policy:
# 1. Confirm the installed mapping has the decimal namespace.
adb shell 'cat /system/etc/selinux/plat_keystore2_key_contexts'
# 2. Confirm the mapping file and daemon have their expected file contexts.
adb shell 'ls -Z /system/etc/selinux/*keystore2_key_contexts'
# 3. Locate the policy rule for the caller, target type and permission.
rg -n 'allow .*_(key|keystore):keystore2_key|neverallow .*keystore2_key' system/sepolicy
# 4. Read the denial fields; tcontext should identify the namespace type.
adb shell 'dmesg | grep "avc:.*keystore2_key"'如果第一步没有 namespace,问题在分区输入或构建产物;有 namespace 但第二步 label 错误,问题在 file contexts 安装;两步都正确仍被拒绝,则把 denial 的 scontext、tcontext、tclass 和 permission 对照 allow。若 denial 不出现而返回 KEY_NOT_FOUND,转查 alias/key-id/grant 的数据库状态;若 BLOB 失败,确认调用的是 security-level 的 deleteKey,而不是 service 层的 alias 删除。
14. 源码导航
按下面顺序阅读可以从配置一路走到消费者:
system/sepolicy/private/keystore2_key_contexts:整数 namespace 与 object context 的当前映射。system/sepolicy/private/keystore_keys.te、system/sepolicy/private/system_server.te、system/sepolicy/private/vold.te:type attribute 与具体权限。system/sepolicy/contexts/Android.bp、system/sepolicy/build/soong/selinux_contexts.go:分区输入、M4、安装输出。system/hardware/interfaces/keystore2/aidl/android/system/keystore2/KeyDescriptor.aidl:五种域的描述符契约。system/security/keystore2/selinux/src/lib.rs:backend、字符串生命周期和 libselinux 锁。system/security/keystore2/src/permission.rs:域到 target context 的选择和KeyPerm检查。system/security/keystore2/src/database.rs:access tuple、事务、key-id 锁、alias 与 grant。system/security/keystore2/src/security_level.rs、service.rs:KeyMint 调用、入库、读取、删除和 BLOB 边界。
读者可以用一个 namespace 1 的 shell key 复述完整链路:Domain::SELINUX + nspace=1 → selabel_lookup("1") → shell_key → shell:s0 对 shell_key:keystore2_key 的具体 permission → SQLite alias/key-id 或 KeyMint blob。再把 permission 改成 grant、把 descriptor 改成 GRANT、或把 alias 重绑定,分别判断哪一个 owner、状态和失败码发生变化。
