密钥请求服务¶
密钥请求服务是密钥保留服务的一部分(请参考 内核密钥保留服务)。本文档更详细地解释了请求算法的工作原理。
该过程开始于内核通过调用 request_key*()
请求服务
struct key *request_key(const struct key_type *type,
const char *description,
const char *callout_info);
或者
struct key *request_key_tag(const struct key_type *type,
const char *description,
const struct key_tag *domain_tag,
const char *callout_info);
或者
struct key *request_key_with_auxdata(const struct key_type *type,
const char *description,
const struct key_tag *domain_tag,
const char *callout_info,
size_t callout_len,
void *aux);
或者
struct key *request_key_rcu(const struct key_type *type,
const char *description,
const struct key_tag *domain_tag);
或者通过用户空间调用 request_key 系统调用
key_serial_t request_key(const char *type,
const char *description,
const char *callout_info,
key_serial_t dest_keyring);
访问点之间的主要区别在于,内核接口不需要将密钥链接到密钥环以防止其立即被销毁。内核接口直接返回指向密钥的指针,并且由调用者负责销毁密钥。
request_key_tag() 调用类似于内核中的 request_key(),不同之处在于它还接受一个域标签,允许按命名空间分隔密钥并作为一个组杀死。
request_key_with_auxdata() 调用类似于 request_key_tag() 调用,不同之处在于它们允许将辅助数据传递给上层调用者(默认为 NULL)。这仅对那些定义了自己的上层调用机制而不是使用 /sbin/request-key 的密钥类型有用。
request_key_rcu() 调用类似于 request_key_tag() 调用,不同之处在于它不检查正在构建中的密钥,也不尝试构建丢失的密钥。
用户空间接口将密钥链接到与该进程关联的密钥环,以防止密钥消失,并将密钥的序列号返回给调用者。
以下示例假定所涉及的密钥类型没有定义它们自己的上层调用机制。如果它们定义了,则应将其替换为 fork 和执行 /sbin/request-key 的过程。
过程¶
请求按以下方式进行
进程 A 调用 request_key() [用户空间系统调用调用内核接口]。
request_key() 搜索进程的已订阅密钥环,以查看那里是否有合适的密钥。如果有,则返回该密钥。如果没有,并且未设置 callout_info,则返回错误。否则,该过程将继续下一步。
request_key() 看到 A 还没有所需的密钥,因此它创建了两个东西
一个未实例化的密钥 U,其类型和描述是请求的。
一个授权密钥 V,它引用密钥 U,并指出进程 A 是应该实例化和保护密钥 U 的上下文,并且可以从中满足相关的密钥请求。
request_key() 然后 fork 并执行 /sbin/request-key,其中包含一个包含指向授权密钥 V 的链接的新会话密钥环。
/sbin/request-key 承担与密钥 U 相关的权限。
/sbin/request-key 执行适当的程序来执行实际的实例化。
该程序可能想从 A 的上下文中访问另一个密钥(比如 Kerberos TGT 密钥)。它只需要请求适当的密钥,并且密钥环搜索会注意到会话密钥环的底层具有授权密钥 V。
这将允许它然后搜索进程 A 的密钥环,并使用进程 A 的 UID、GID、组和安全信息,就好像它是进程 A 一样,并获得密钥 W。
然后,该程序会执行必要的操作以获取用于实例化密钥 U 的数据,使用密钥 W 作为参考(可能它使用 TGT 联系 Kerberos 服务器),然后实例化密钥 U。
在实例化密钥 U 时,授权密钥 V 会自动被撤销,以便它不能再次使用。
然后,该程序退出 0,request_key() 删除密钥 V 并将密钥 U 返回给调用者。
这也会进一步扩展。如果密钥 W(上述步骤 7)不存在,则将创建未实例化的密钥 W,将创建另一个授权密钥 (X)(按照步骤 3),并生成另一个 /sbin/request-key 副本(按照步骤 4);但是授权密钥 X 指定的上下文仍然是进程 A,就像授权密钥 V 中的一样。
这是因为进程 A 的密钥环不能简单地附加到 /sbin/request-key 的适当位置,因为 (a) execve 将丢弃其中的两个,并且 (b) 它需要整个过程使用相同的 UID/GID/组。
负实例化和拒绝¶
持有授权密钥的人员可以负实例化一个正在构建中的密钥,而不是实例化密钥。这是一个短时间的占位符,它会导致在密钥存在时尝试重新请求该密钥失败,如果被否定则出现错误 ENOKEY,如果被拒绝则出现指定的错误。
这样做是为了防止为永远无法获得的密钥过度重复生成 /sbin/request-key 进程。
如果 /sbin/request-key 进程退出时不是 0 或因信号而死亡,则正在构建中的密钥会自动被负实例化一小段时间。
搜索算法¶
对任何特定密钥环的搜索都按以下方式进行
当密钥管理代码搜索密钥时 (keyring_search_rcu),它首先在它开始使用的密钥环上调用 key_permission(SEARCH),如果这拒绝权限,它将不再搜索。
它会考虑该密钥环内的所有非密钥环密钥,并且,如果任何密钥符合指定的条件,则会在其上调用 key_permission(SEARCH),以查看是否允许找到该密钥。如果是,则返回该密钥;如果不是,则继续搜索,并且如果错误代码的优先级高于当前设置的错误代码,则会保留该错误代码。
然后,它会考虑当前正在搜索的密钥环中的所有密钥环类型密钥。它在每个密钥环上调用 key_permission(SEARCH),如果这授予了权限,它将递归,在该密钥环上执行步骤 (2) 和 (3)。
一旦找到一个具有使用它的授权的有效密钥,该过程就会立即停止。丢弃之前匹配尝试中的任何错误,并返回该密钥。
当调用 request_key() 时,如果 CONFIG_KEYS_REQUEST_CACHE=y,则首先检查每个任务的单密钥缓存是否存在匹配项。
当调用 search_process_keyrings() 时,它会执行以下搜索,直到其中一个成功
如果存在,则搜索进程的线程密钥环。
如果存在,则搜索进程的进程密钥环。
搜索进程的会话密钥环。
如果进程已承担与 request_key() 授权密钥关联的权限,则
如果存在,则搜索调用进程的线程密钥环。
如果存在,则搜索调用进程的进程密钥环。
搜索调用进程的会话密钥环。
一旦其中一个成功,所有挂起的错误都会被丢弃,并返回找到的密钥。如果 CONFIG_KEYS_REQUEST_CACHE=y,则该密钥会放置在每个任务的缓存中,取代之前的密钥。缓存会在退出时或在恢复用户空间之前被清除。
只有当所有这些都失败时,整个过程才会失败,并返回优先级最高的错误。请注意,可能有几个错误来自 LSM。
错误优先级为
EKEYREVOKED > EKEYEXPIRED > ENOKEY
只有在直接搜索特定密钥环(其中基本密钥环不授予搜索权限)时才会返回 EACCES/EPERM。