密钥请求服务¶
密钥请求服务是密钥保留服务的一部分(参阅内核密钥保留服务)。本文档更全面地解释了请求算法的工作原理。
该过程要么始于内核通过调用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 和执行。
过程¶
请求按以下方式进行
进程 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。
然后程序执行必要的操作,以密钥 W 为参考(或许它使用 TGT 联系 Kerberos 服务器)获取用于实例化密钥 U 的数据,然后实例化密钥 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 仅在直接搜索特定密钥环且基础密钥环不授予搜索权限时返回。