密钥请求服务

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

该过程要么始于内核通过调用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()调用类似,但它不检查正在构建的密钥,也不尝试构建缺失的密钥。

用户空间接口将密钥链接到与进程关联的密钥环,以防止密钥消失,并向调用者返回密钥的序列号。

以下示例假定所涉及的密钥类型没有定义自己的上层调用机制。如果它们有,则应将这些机制替换为 /sbin/request-key 的 fork 和执行。

过程

请求按以下方式进行

  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. 然后程序执行必要的操作,以密钥 W 为参考(或许它使用 TGT 联系 Kerberos 服务器)获取用于实例化密钥 U 的数据,然后实例化密钥 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 仅在直接搜索特定密钥环且基础密钥环不授予搜索权限时返回。