Skip to content

Keystore2 Key Contexts

追踪 Keystore2 的 SELinux 命名空间从 KeyDescriptor、上下文查找到权限检查、密钥存储和失败清理。

基于android-17.0.0_r1
AndroidSELinuxKeystore2key_contexts源码阅读

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调用进程的 UIDBinder 调用者 UID否,target 是当前 keystore context
Domain.SELINUXpolicy 配置的整数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 backendbackend 打不开时服务不能正常工作
target contextkeystore2 权限模块selinux_check_access返回 PermissionDenied 或系统错误
alias/keyentryKeystore2 SQLite 数据库service.rs、security_level.rs事务回滚,旧记录保持原状
KeyMint blobKeyMint 实例或数据库 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

java
@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

java
@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

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

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

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

ini
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 规则,而不是由数字本身推断:

namespacetype典型 owner关键用途
0su_keysunative 测试
1shell_keyshellnative 测试与 shell 测试
100vold_keyvold需要 manage_blob 的存储密钥
101odsign_keyodsignART on-device signing
102wifi_keyWi-Fi 组件Wi-Fi 共享密钥
103locksettings_keysystem_serverLockSettings/RoR 密钥
104keychain_keyKeyChainSystemServiceKeyChain 密钥
120resume_on_reboot_keysystem_serverresume-on-reboot

keychain_key 是 Android 17 当前文件中的条目;只读取旧版本文件会漏掉它。反过来,映射文件中存在条目也不代表任意 domain 可以访问它,仍需检查 keystore2_key 的 allow/neverallow 规则。

3.2 类型声明 ​

源码文件:system/sepolicy/private/keystore_keys.te

text
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

text
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

make
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

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

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

ini
/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

text
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

rust
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

rust
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

rust
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

rust
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

rust
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

text
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

text
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

text
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

rust
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

rust
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

rust
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 结论,因此用注释标出省略位置。

rust
// 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

rust
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 参数解析分支与命名空间无关,未展开。

rust
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、锁和权限顺序。

rust
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

rust
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

rust
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

rust
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 分支。

rust
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_DENIEDtarget type 的具体权限未放行selinux_check_access不能把所有操作改成 *
KEY_NOT_FOUNDalias、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

rust
#[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

rust
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

rust
// 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

rust
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:

sh
# 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. 源码导航 ​

按下面顺序阅读可以从配置一路走到消费者:

  1. system/sepolicy/private/keystore2_key_contexts:整数 namespace 与 object context 的当前映射。
  2. system/sepolicy/private/keystore_keys.te、system/sepolicy/private/system_server.te、system/sepolicy/private/vold.te:type attribute 与具体权限。
  3. system/sepolicy/contexts/Android.bp、system/sepolicy/build/soong/selinux_contexts.go:分区输入、M4、安装输出。
  4. system/hardware/interfaces/keystore2/aidl/android/system/keystore2/KeyDescriptor.aidl:五种域的描述符契约。
  5. system/security/keystore2/selinux/src/lib.rs:backend、字符串生命周期和 libselinux 锁。
  6. system/security/keystore2/src/permission.rs:域到 target context 的选择和 KeyPerm 检查。
  7. system/security/keystore2/src/database.rs:access tuple、事务、key-id 锁、alias 与 grant。
  8. 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、状态和失败码发生变化。