密钥请求服务

密钥请求服务是密钥保留服务的一部分(请参考 内核密钥保留服务)。本文档更详细地解释了请求算法的工作原理。

该过程开始于内核通过调用 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 的过程。

过程

请求按以下方式进行

  1. 进程 A 调用 request_key() [用户空间系统调用调用内核接口]。

  2. request_key() 搜索进程的已订阅密钥环,以查看那里是否有合适的密钥。如果有,则返回该密钥。如果没有,并且未设置 callout_info,则返回错误。否则,该过程将继续下一步。

  3. request_key() 看到 A 还没有所需的密钥,因此它创建了两个东西

    1. 一个未实例化的密钥 U,其类型和描述是请求的。

    2. 一个授权密钥 V,它引用密钥 U,并指出进程 A 是应该实例化和保护密钥 U 的上下文,并且可以从中满足相关的密钥请求。

  4. request_key() 然后 fork 并执行 /sbin/request-key,其中包含一个包含指向授权密钥 V 的链接的新会话密钥环。

  5. /sbin/request-key 承担与密钥 U 相关的权限。

  6. /sbin/request-key 执行适当的程序来执行实际的实例化。

  7. 该程序可能想从 A 的上下文中访问另一个密钥(比如 Kerberos TGT 密钥)。它只需要请求适当的密钥,并且密钥环搜索会注意到会话密钥环的底层具有授权密钥 V。

    这将允许它然后搜索进程 A 的密钥环,并使用进程 A 的 UID、GID、组和安全信息,就好像它是进程 A 一样,并获得密钥 W。

  8. 然后,该程序会执行必要的操作以获取用于实例化密钥 U 的数据,使用密钥 W 作为参考(可能它使用 TGT 联系 Kerberos 服务器),然后实例化密钥 U。

  9. 在实例化密钥 U 时,授权密钥 V 会自动被撤销,以便它不能再次使用。

  10. 然后,该程序退出 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 或因信号而死亡,则正在构建中的密钥会自动被负实例化一小段时间。

搜索算法

对任何特定密钥环的搜索都按以下方式进行

  1. 当密钥管理代码搜索密钥时 (keyring_search_rcu),它首先在它开始使用的密钥环上调用 key_permission(SEARCH),如果这拒绝权限,它将不再搜索。

  2. 它会考虑该密钥环内的所有非密钥环密钥,并且,如果任何密钥符合指定的条件,则会在其上调用 key_permission(SEARCH),以查看是否允许找到该密钥。如果是,则返回该密钥;如果不是,则继续搜索,并且如果错误代码的优先级高于当前设置的错误代码,则会保留该错误代码。

  3. 然后,它会考虑当前正在搜索的密钥环中的所有密钥环类型密钥。它在每个密钥环上调用 key_permission(SEARCH),如果这授予了权限,它将递归,在该密钥环上执行步骤 (2) 和 (3)。

一旦找到一个具有使用它的授权的有效密钥,该过程就会立即停止。丢弃之前匹配尝试中的任何错误,并返回该密钥。

当调用 request_key() 时,如果 CONFIG_KEYS_REQUEST_CACHE=y,则首先检查每个任务的单密钥缓存是否存在匹配项。

当调用 search_process_keyrings() 时,它会执行以下搜索,直到其中一个成功

  1. 如果存在,则搜索进程的线程密钥环。

  2. 如果存在,则搜索进程的进程密钥环。

  3. 搜索进程的会话密钥环。

  4. 如果进程已承担与 request_key() 授权密钥关联的权限,则

    1. 如果存在,则搜索调用进程的线程密钥环。

    2. 如果存在,则搜索调用进程的进程密钥环。

    3. 搜索调用进程的会话密钥环。

一旦其中一个成功,所有挂起的错误都会被丢弃,并返回找到的密钥。如果 CONFIG_KEYS_REQUEST_CACHE=y,则该密钥会放置在每个任务的缓存中,取代之前的密钥。缓存会在退出时或在恢复用户空间之前被清除。

只有当所有这些都失败时,整个过程才会失败,并返回优先级最高的错误。请注意,可能有几个错误来自 LSM。

错误优先级为

EKEYREVOKED > EKEYEXPIRED > ENOKEY

只有在直接搜索特定密钥环(其中基本密钥环不授予搜索权限)时才会返回 EACCES/EPERM。