内核密钥保留服务

此服务允许将加密密钥、身份验证令牌、跨域用户映射以及类似内容缓存在内核中,供文件系统和其他内核服务使用。

允许使用密钥环;这些是一种特殊的密钥类型,可以保存指向其他密钥的链接。进程各自具有三个标准密钥环订阅,内核服务可以搜索相关的密钥。

可以通过启用以下选项来配置密钥服务:

“安全选项”/“启用访问密钥保留支持”(CONFIG_KEYS)

本文档包含以下部分:

密钥概述

在此上下文中,密钥表示加密数据、身份验证令牌、密钥环等的单元。这些在内核中由 struct key 表示。

每个密钥都有许多属性:

  • 一个序列号。

  • 一个类型。

  • 一个描述(用于在搜索中匹配密钥)。

  • 访问控制信息。

  • 一个过期时间。

  • 一个有效负载。

  • 状态。

  • 每个密钥都会颁发一个 key_serial_t 类型的序列号,该序列号在该密钥的生命周期内是唯一的。所有序列号都是正非零 32 位整数。

    用户空间程序可以使用密钥的序列号来访问它,但需要进行权限检查。

  • 每个密钥都属于定义的“类型”。内核服务(例如文件系统)必须先在内核内部注册类型,然后才能添加或使用该类型的密钥。用户空间程序无法直接定义新类型。

    密钥类型在内核中由 struct key_type 表示。这定义了可以对该类型的密钥执行的许多操作。

    如果从系统中删除了某个类型,则该类型的所有密钥都将失效。

  • 每个密钥都有一个描述。这应该是一个可打印的字符串。密钥类型提供一个操作,用于在密钥上的描述和条件字符串之间执行匹配。

  • 每个密钥都有一个所有者用户 ID、一个组 ID 和一个权限掩码。这些用于控制进程可以从用户空间对密钥执行哪些操作,以及内核服务是否能够找到该密钥。

  • 每个密钥都可以由密钥类型的实例化函数设置为在特定时间过期。密钥也可以是不朽的。

  • 每个密钥都可以有一个有效负载。这是表示实际“密钥”的数据量。对于密钥环,这是密钥环链接到的密钥列表;对于用户定义的密钥,这是一个任意数据块。

    不需要有有效负载;实际上,有效负载可能只是存储在 struct key 本身中的一个值。

    实例化密钥时,将使用一个数据块调用密钥类型的实例化函数,然后以某种方式创建密钥的有效负载。

    同样,当用户空间想要读取密钥的内容时,如果允许,将调用另一个密钥类型操作,将密钥附加的有效负载转换回数据块。

  • 每个密钥都可以处于以下几个基本状态之一:

    • 未实例化。密钥存在,但没有附加任何数据。从用户空间请求的密钥将处于此状态。

    • 已实例化。这是正常状态。密钥已完全形成,并附加了数据。

    • 负。这是一个相对短暂的状态。密钥充当一个注释,表示之前对用户空间的调用失败,并充当密钥查找的限制。可以将负密钥更新为正常状态。

    • 已过期。密钥可以设置生存期。如果超过其生存期,它们将转换为此状态。可以将过期的密钥更新回正常状态。

    • 已撤销。用户空间操作会将密钥置于此状态。它无法被找到或操作(除了取消链接它)。

    • 已死。密钥的类型已取消注册,因此该密钥现在无用。

最后三种状态的密钥将进行垃圾回收。请参阅“垃圾回收”部分。

密钥服务概述

密钥服务除了密钥之外还提供许多功能:

  • 密钥服务定义了三种特殊的密钥类型:

    (+)“密钥环”

    密钥环是包含其他密钥列表的特殊密钥。可以使用各种系统调用来修改密钥环列表。创建时,不应给密钥环有效负载。

    (+)“用户”

    此类型的密钥具有描述和有效负载,它们是任意数据块。这些可以由用户空间创建、更新和读取,而不是供内核服务使用。

    (+)“登录”

    与“用户”密钥类似,“登录”密钥的有效负载是任意数据块。它旨在存储内核可以访问但用户空间程序无法访问的机密。

    描述可以是任意的,但必须以描述密钥“子类”的非零长度字符串为前缀。子类通过“:”与描述的其余部分分隔。“登录”密钥可以从用户空间创建和更新,但有效负载只能从内核空间读取。

  • 每个进程都会订阅三个密钥环:线程特定的密钥环、进程特定的密钥环和会话特定的密钥环。

    当发生任何类型的克隆、fork、vfork 或 execve 时,线程特定的密钥环将从子进程中丢弃。仅在需要时才会创建新的密钥环。

    除非提供 CLONE_THREAD,否则在克隆、fork、vfork 时,子进程中的进程特定密钥环将被替换为空的密钥环,在这种情况下,它是共享的。execve 也会丢弃进程的进程密钥环并创建一个新的密钥环。

    会话特定的密钥环在克隆、fork、vfork 和 execve 之间是持久的,即使后者执行设置 UID 或设置 GID 的二进制文件也是如此。但是,进程可以使用 PR_JOIN_SESSION_KEYRING 将其当前的会话密钥环替换为新的密钥环。允许请求一个匿名的密钥环,或者尝试创建或加入一个具有特定名称的密钥环。

    当线程的实际 UID 和 GID 发生更改时,线程密钥环的所有权会发生更改。

  • 系统中每个驻留的用户 ID 都持有两个特殊的密钥环:用户特定的密钥环和默认的用户会话密钥环。默认会话密钥环初始化为指向用户特定密钥环的链接。

    当进程更改其实际 UID 时,如果它以前没有会话密钥,它将订阅新 UID 的默认会话密钥。

    如果进程尝试访问其会话密钥,但它没有会话密钥,则它将订阅其当前 UID 的默认密钥。

  • 每个用户都有两个配额,用于跟踪他们拥有的密钥。一个限制密钥和密钥环的总数,另一个限制可以消耗的描述和有效负载空间的总量。

    用户可以通过 procfs 文件查看有关此内容和其他统计信息的信息。root 用户还可以通过 sysctl 文件更改配额限制(请参阅“新的 procfs 文件”部分)。

    进程特定的密钥环和线程特定的密钥环不计入用户的配额。

    如果以某种方式修改密钥或密钥环的系统调用会使用户超过配额,则操作将被拒绝并返回错误 EDQUOT。

  • 有一个系统调用接口,用户空间程序可以通过该接口创建和操作密钥和密钥环。

  • 有一个内核接口,服务可以通过该接口注册类型和搜索密钥。

  • 有一种方法可以让从内核完成的搜索回调到用户空间,以请求在进程的密钥环中找不到的密钥。

  • 有一个可选的文件系统,可以通过该文件系统查看和操作密钥数据库。

密钥访问权限

密钥具有所有者用户 ID、组访问 ID 和权限掩码。该掩码为拥有者、用户、组和其他访问权限分别具有最多八位。每个八位集中仅定义了六位。授予的这些权限是:

  • 查看

    这允许查看密钥或密钥环的属性 - 包括密钥类型和描述。

  • 读取

    这允许查看密钥的有效负载或密钥环的链接密钥列表。

  • 写入

    这允许实例化或更新密钥的有效负载,或者允许将链接添加到密钥环或从中删除链接。

  • 搜索

    这允许搜索密钥环并查找密钥。搜索只能递归进入设置了搜索权限的嵌套密钥环。

  • 链接

    这允许将密钥或密钥环链接到另一个密钥或密钥环。要创建从密钥环到密钥的链接,进程必须具有密钥环的“写入”权限和密钥的“链接”权限。

  • 设置属性

    这允许更改密钥的 UID、GID 和权限掩码。

对于更改所有权、组 ID 或权限掩码,成为密钥的所有者或拥有 sysadmin 能力就足够了。

SELinux 支持

“密钥”安全类已添加到 SELinux 中,以便可以将强制访问控制应用于在各种上下文中创建的密钥。此支持是初步的,并且在不久的将来可能会发生重大变化。目前,上面解释的所有基本权限也在 SELinux 中提供;SELinux 只是在执行所有基本权限检查后被调用。

文件 /proc/self/attr/keycreate 的值会影响新创建的密钥的标签。如果该文件的内容与 SELinux 安全上下文相对应,则该密钥将被分配该上下文。否则,该密钥将被分配调用密钥创建请求的任务的当前上下文。必须授予任务明确的权限,才能使用密钥安全类中的“创建”权限将特定上下文分配给新创建的密钥。

如果登录程序已进行检测以在登录过程中正确初始化 keycreate,则与用户关联的默认密钥环将仅使用用户的默认上下文进行标记。否则,它们将使用登录程序本身的上下文进行标记。

但是,请注意,与 root 用户关联的默认密钥环会使用默认的内核上下文进行标记,因为它们是在启动过程的早期创建的,此时 root 用户尚未有机会登录。

与新线程关联的密钥环都使用其关联线程的上下文进行标记,会话和进程密钥环的处理方式类似。

新的 ProcFS 文件

procfs 中添加了两个文件,管理员可以通过它们了解密钥服务的状态

  • /proc/keys

    这列出了当前读取文件的任务可见的密钥,提供了有关其类型、描述和权限的信息。无法通过这种方式查看密钥的有效负载,但可能会提供有关它的一些信息。

    列表中仅包含那些授予读取进程“查看”权限的密钥,无论它是否拥有它们。请注意,LSM 安全检查仍然会执行,并且可能会进一步过滤掉当前进程未授权查看的密钥。

    该文件的内容如下所示

    SERIAL   FLAGS  USAGE EXPY PERM     UID   GID   TYPE      DESCRIPTION: SUMMARY
    00000001 I-----    39 perm 1f3f0000     0     0 keyring   _uid_ses.0: 1/4
    00000002 I-----     2 perm 1f3f0000     0     0 keyring   _uid.0: empty
    00000007 I-----     1 perm 1f3f0000     0     0 keyring   _pid.1: empty
    0000018d I-----     1 perm 1f3f0000     0     0 keyring   _pid.412: empty
    000004d2 I--Q--     1 perm 1f3f0000    32    -1 keyring   _uid.32: 1/4
    000004d3 I--Q--     3 perm 1f3f0000    32    -1 keyring   _uid_ses.32: empty
    00000892 I--QU-     1 perm 1f000000     0     0 user      metal:copper: 0
    00000893 I--Q-N     1  35s 1f3f0000     0     0 user      metal:silver: 0
    00000894 I--Q--     1  10h 003f0000     0     0 user      metal:gold: 0
    

    标志是

    I       Instantiated
    R       Revoked
    D       Dead
    Q       Contributes to user's quota
    U       Under construction by callback to userspace
    N       Negative key
    
  • /proc/key-users

    此文件列出系统上至少有一个密钥的每个用户的跟踪数据。此类数据包括配额信息和统计信息

    [root@andromeda root]# cat /proc/key-users
    0:     46 45/45 1/100 13/10000
    29:     2 2/2 2/100 40/10000
    32:     2 2/2 2/100 40/10000
    38:     2 2/2 2/100 40/10000
    

    每行的格式为

    <UID>:                  User ID to which this applies
    <usage>                 Structure refcount
    <inst>/<keys>           Total number of keys and number instantiated
    <keys>/<max>            Key count quota
    <bytes>/<max>           Key size quota
    

还添加了四个新的 sysctl 文件,用于控制密钥的配额限制

  • /proc/sys/kernel/keys/root_maxkeys /proc/sys/kernel/keys/root_maxbytes

    这些文件保存 root 用户可能拥有的最大密钥数以及 root 用户可能在这些密钥中存储的最大总字节数。

  • /proc/sys/kernel/keys/maxkeys /proc/sys/kernel/keys/maxbytes

    这些文件保存每个非 root 用户可能拥有的最大密钥数以及每个用户可能在其密钥中存储的最大总字节数。

root 用户可以通过将每个新限制作为十进制数字字符串写入相应的 文件来更改这些限制。

用户空间系统调用接口

用户空间可以通过三个新的系统调用直接操作密钥:add_key、request_key 和 keyctl。后者提供了许多用于操作密钥的函数。

当直接引用密钥时,用户空间程序应使用密钥的序列号(一个正 32 位整数)。但是,有一些特殊值可用于引用与进行调用的进程相关的特殊密钥和密钥环

CONSTANT                        VALUE   KEY REFERENCED
==============================  ======  ===========================
KEY_SPEC_THREAD_KEYRING         -1      thread-specific keyring
KEY_SPEC_PROCESS_KEYRING        -2      process-specific keyring
KEY_SPEC_SESSION_KEYRING        -3      session-specific keyring
KEY_SPEC_USER_KEYRING           -4      UID-specific keyring
KEY_SPEC_USER_SESSION_KEYRING   -5      UID-session keyring
KEY_SPEC_GROUP_KEYRING          -6      GID-specific keyring
KEY_SPEC_REQKEY_AUTH_KEY        -7      assumed request_key()
                                          authorisation key

主要的系统调用是

  • 创建给定类型、描述和有效负载的新密钥,并将其添加到指定的密钥环中

    key_serial_t add_key(const char *type, const char *desc,
                         const void *payload, size_t plen,
                         key_serial_t keyring);
    

    如果密钥环中已存在与建议的密钥类型和描述相同的密钥,则将尝试使用给定的有效负载更新该密钥,如果密钥类型不支持该功能,则将返回错误 EEXIST。进程还必须具有写入密钥的权限才能更新它。新密钥将授予所有用户权限,而没有组或第三方权限。

    否则,这将尝试创建指定类型和描述的新密钥,并使用提供的有效负载实例化它并将其附加到密钥环。在这种情况下,如果进程没有写入密钥环的权限,则会生成错误。

    如果密钥类型支持,则如果描述为 NULL 或空字符串,则密钥类型将尝试从有效负载的内容生成描述。

    有效负载是可选的,如果类型不需要,则指针可以为 NULL。有效负载的大小为 plen,对于空有效负载,plen 可以为零。

    可以通过将类型设置为“keyring”,将密钥环名称设置为描述(或 NULL),并将有效负载设置为 NULL 来生成新的密钥环。

    用户定义的密钥可以通过指定类型“user”来创建。建议用户定义的密钥的描述以类型 ID 和冒号为前缀,例如用于 Kerberos 5 票证授予票证的“krb5tgt:”。

    任何其他类型都必须预先由内核服务(如文件系统)向内核注册。

    如果成功,则返回新密钥或更新密钥的 ID。

  • 在进程的密钥环中搜索密钥,可能会调用用户空间来创建它

    key_serial_t request_key(const char *type, const char *description,
                             const char *callout_info,
                             key_serial_t dest_keyring);
    

    此函数按照线程、进程、会话的顺序搜索进程的所有密钥环,以查找匹配的密钥。这与 KEYCTL_SEARCH 非常相似,包括将发现的密钥可选地附加到密钥环。

    如果找不到密钥,并且 callout_info 不为 NULL,则将调用 /sbin/request-key 以尝试获取密钥。callout_info 字符串将作为参数传递给该程序。

    要将密钥链接到目标密钥环,密钥必须授予调用者对该密钥的“链接”权限,并且密钥环必须授予“写入”权限。

    另请参阅密钥请求服务

keyctl 系统调用函数是

  • 将特殊密钥 ID 映射到此进程的真实密钥 ID

    key_serial_t keyctl(KEYCTL_GET_KEYRING_ID, key_serial_t id,
                        int create);
    

    查找由 “id” 指定的特殊密钥(如有必要,将创建密钥),如果存在,则返回找到的密钥或密钥环的 ID。

    如果密钥尚不存在,则如果 “create” 不为零,将创建密钥;如果 “create” 为零,则将返回错误 ENOKEY。

  • 将此进程订阅的会话密钥环替换为新的密钥环

    key_serial_t keyctl(KEYCTL_JOIN_SESSION_KEYRING, const char *name);
    

    如果 name 为 NULL,则会创建一个匿名密钥环,该密钥环作为其会话密钥环附加到进程,从而取代旧的会话密钥环。

    如果 name 不为 NULL,则如果存在该名称的密钥环,则进程将尝试将其作为会话密钥环附加,如果未获得允许,则返回错误;否则,将创建该名称的新密钥环并将其作为会话密钥环附加。

    要附加到命名密钥环,密钥环必须具有进程所有权的“搜索”权限。

    如果成功,则返回新的会话密钥环的 ID。

  • 更新指定的密钥

    long keyctl(KEYCTL_UPDATE, key_serial_t key, const void *payload,
                size_t plen);
    

    这将尝试使用给定的有效负载更新指定的密钥,如果密钥类型不支持该功能,则将返回错误 EOPNOTSUPP。进程还必须具有写入密钥的权限才能更新它。

    有效负载的长度为 plen,并且可能像 add_key() 一样缺失或为空。

  • 撤销密钥

    long keyctl(KEYCTL_REVOKE, key_serial_t key);
    

    这使密钥无法用于进一步操作。进一步尝试使用该密钥将遇到错误 EKEYREVOKED,并且将不再能够找到该密钥。

  • 更改密钥的所有权

    long keyctl(KEYCTL_CHOWN, key_serial_t key, uid_t uid, gid_t gid);
    

    此函数允许更改密钥的所有者和组 ID。可以将 uid 或 gid 之一设置为 -1 以禁止该更改。

    只有超级用户才能将密钥的所有者更改为密钥当前所有者之外的其他用户。同样,只有超级用户才能将密钥的组 ID 更改为调用进程的组 ID 或其组列表的成员之外的其他组 ID。

  • 更改密钥上的权限掩码

    long keyctl(KEYCTL_SETPERM, key_serial_t key, key_perm_t perm);
    

    此函数允许密钥的所有者或超级用户更改密钥上的权限掩码。

    仅允许使用可用的位;如果设置了任何其他位,将返回错误 EINVAL。

  • 描述密钥

    long keyctl(KEYCTL_DESCRIBE, key_serial_t key, char *buffer,
                size_t buflen);
    

    此函数以字符串形式返回密钥属性(但不包括其有效负载数据)的摘要(在提供的缓冲区中)。

    除非发生错误,否则它始终返回它可以生成的数据量,即使该数据量对于缓冲区来说太大,但它不会复制超过请求量的数据到用户空间。如果缓冲区指针为 NULL,则不会发生复制。

    进程必须具有密钥的“查看”权限,此函数才能成功。

    如果成功,则会将字符串放置在缓冲区中,格式如下

    <type>;<uid>;<gid>;<perm>;<description>
    

    其中 type 和 description 是字符串,uid 和 gid 是十进制,perm 是十六进制。如果缓冲区足够大,则字符串的末尾将包含一个 NUL 字符。

    可以使用以下方法进行解析

    sscanf(buffer, "%[^;];%d;%d;%o;%s", type, &uid, &gid, &mode, desc);
    
  • 清除密钥环

    long keyctl(KEYCTL_CLEAR, key_serial_t keyring);
    

    此函数清除附加到密钥环的密钥列表。调用进程必须具有密钥环的“写入”权限,并且它必须是密钥环(否则将导致错误 ENOTDIR)。

    如果用户具有 CAP_SYS_ADMIN 能力,则此函数还可以用于清除特殊内核密钥环(如果它们已适当标记)。DNS 解析器缓存密钥环就是这种情况的一个示例。

  • 将密钥链接到密钥环

    long keyctl(KEYCTL_LINK, key_serial_t keyring, key_serial_t key);
    

    此函数创建从密钥环到密钥的链接。进程必须具有密钥环的“写入”权限,并且必须具有密钥的“链接”权限。

    如果密钥环不是密钥环,则将导致错误 ENOTDIR;如果密钥环已满,则将导致错误 ENFILE。

    链接过程会检查密钥环的嵌套,如果看起来太深,则返回 ELOOP,如果链接会引入循环,则返回 EDEADLK。

    密钥环中与新密钥在类型和描述方面匹配的任何密钥的链接都将从密钥环中丢弃,因为添加了新密钥。

  • 将密钥从一个密钥环移动到另一个密钥环

    long keyctl(KEYCTL_MOVE,
                key_serial_t id,
                key_serial_t from_ring_id,
                key_serial_t to_ring_id,
                unsigned int flags);
    

    将 “id” 指定的密钥从 “from_ring_id” 指定的密钥环移动到 “to_ring_id” 指定的密钥环。如果两个密钥环相同,则不执行任何操作。

    可以在 “flags” 中设置 KEYCTL_MOVE_EXCL,以使操作在目标密钥环中存在匹配的密钥时失败并返回 EEXIST,否则将替换此类密钥。

    进程必须具有密钥的 “链接” 权限才能使此函数成功,并且必须具有两个密钥环的“写入”权限。KEYCTL_LINK 可能发生的任何错误也适用于此处的目的地密钥环。

  • 取消链接密钥或密钥环与另一个密钥环的链接

    long keyctl(KEYCTL_UNLINK, key_serial_t keyring, key_serial_t key);
    

    此函数在密钥环中查找指向指定密钥的第一个链接,如果找到则删除它。该密钥的后续链接将被忽略。该进程必须对密钥环具有写入权限。

    如果密钥环不是一个密钥环,将会产生 ENOTDIR 错误;如果密钥不存在,将会产生 ENOENT 错误。

  • 在密钥环树中搜索密钥

    key_serial_t keyctl(KEYCTL_SEARCH, key_serial_t keyring,
                        const char *type, const char *description,
                        key_serial_t dest_keyring);
    

    此函数搜索由指定的密钥环为根的密钥环树,直到找到一个符合类型和描述标准的密钥。在递归进入其子密钥环之前,会检查每个密钥环中的密钥。

    该进程必须对顶层密钥环具有搜索权限,否则将产生 EACCES 错误。只有该进程具有搜索权限的密钥环才会被递归进入,并且只有该进程具有搜索权限的密钥和密钥环才能被匹配。如果指定的密钥环不是一个密钥环,将产生 ENOTDIR 错误。

    如果搜索成功,该函数将尝试将找到的密钥链接到目标密钥环(如果提供了非零 ID)。在这种情况下,所有适用于 KEYCTL_LINK 的约束也适用。

    如果搜索失败,将返回 ENOKEY、EKEYREVOKED 或 EKEYEXPIRED 错误。如果成功,将返回结果密钥 ID。

  • 从密钥读取有效负载数据

    long keyctl(KEYCTL_READ, key_serial_t keyring, char *buffer,
                size_t buflen);
    

    此函数尝试从指定的密钥中读取有效负载数据到缓冲区。进程必须对密钥具有读取权限才能成功。

    返回的数据将由密钥类型进行处理以进行展示。例如,一个密钥环将返回一个 key_serial_t 条目的数组,表示它订阅的所有密钥的 ID。用户定义的密钥类型将按原样返回其数据。如果一个密钥类型没有实现此功能,将产生 EOPNOTSUPP 错误。

    如果指定的缓冲区太小,则将返回所需的缓冲区大小。请注意,在这种情况下,缓冲区的内容可能已被以某种未定义的方式覆盖。

    否则,如果成功,该函数将返回复制到缓冲区的数据量。

  • 实例化一个部分构造的密钥

    long keyctl(KEYCTL_INSTANTIATE, key_serial_t key,
                const void *payload, size_t plen,
                key_serial_t keyring);
    long keyctl(KEYCTL_INSTANTIATE_IOV, key_serial_t key,
                const struct iovec *payload_iov, unsigned ioc,
                key_serial_t keyring);
    

    如果内核回调到用户空间以完成密钥的实例化,用户空间应在调用的进程返回之前使用此调用为密钥提供数据,否则该密钥将被自动标记为负值。

    进程必须对密钥具有写入权限才能实例化它,并且该密钥必须是未实例化的。

    如果指定了密钥环(非零),该密钥也将被链接到该密钥环,但是所有适用于 KEYCTL_LINK 的约束也适用于此情况。

    payload 和 plen 参数描述了有效负载数据,与 add_key() 相同。

    payload_iov 和 ioc 参数在 iovec 数组中描述了有效负载数据,而不是单个缓冲区。

  • 负实例化一个部分构造的密钥

    long keyctl(KEYCTL_NEGATE, key_serial_t key,
                unsigned timeout, key_serial_t keyring);
    long keyctl(KEYCTL_REJECT, key_serial_t key,
                unsigned timeout, unsigned error, key_serial_t keyring);
    

    如果内核回调到用户空间以完成密钥的实例化,如果用户空间无法满足请求,则应在调用的进程返回之前使用此调用将密钥标记为负值。

    进程必须对密钥具有写入权限才能实例化它,并且该密钥必须是未实例化的。

    如果指定了密钥环(非零),该密钥也将被链接到该密钥环,但是所有适用于 KEYCTL_LINK 的约束也适用于此情况。

    如果密钥被拒绝,对它的未来搜索将返回指定的错误代码,直到被拒绝的密钥过期。否定密钥与返回 ENOKEY 错误码拒绝密钥相同。

  • 设置默认的 request-key 目标密钥环

    long keyctl(KEYCTL_SET_REQKEY_KEYRING, int reqkey_defl);
    

    这将设置此线程的隐式请求密钥将被附加到的默认密钥环。reqkey_defl 应该是以下常量之一

    CONSTANT                                VALUE   NEW DEFAULT KEYRING
    ======================================  ======  =======================
    KEY_REQKEY_DEFL_NO_CHANGE               -1      No change
    KEY_REQKEY_DEFL_DEFAULT                 0       Default[1]
    KEY_REQKEY_DEFL_THREAD_KEYRING          1       Thread keyring
    KEY_REQKEY_DEFL_PROCESS_KEYRING         2       Process keyring
    KEY_REQKEY_DEFL_SESSION_KEYRING         3       Session keyring
    KEY_REQKEY_DEFL_USER_KEYRING            4       User keyring
    KEY_REQKEY_DEFL_USER_SESSION_KEYRING    5       User session keyring
    KEY_REQKEY_DEFL_GROUP_KEYRING           6       Group keyring
    

    如果成功,将返回旧的默认值;如果 reqkey_defl 不是上述值之一,将返回 EINVAL 错误。

    默认密钥环可以被指示给 request_key() 系统调用的密钥环覆盖。

    请注意,此设置会在 fork/exec 中继承。

    [1] 默认值是:如果存在线程密钥环,则为线程密钥环;否则,如果存在进程密钥环,则为进程密钥环;否则,如果存在会话密钥环,则为会话密钥环;否则,为用户默认会话密钥环。

  • 设置密钥的超时时间

    long keyctl(KEYCTL_SET_TIMEOUT, key_serial_t key, unsigned timeout);
    

    这将设置或清除密钥的超时时间。超时时间可以为 0 以清除超时时间,或者为秒数以设置到未来的过期时间。

    进程必须对密钥具有属性修改权限才能设置其超时时间。无法使用此函数在负值、已撤销或已过期的密钥上设置超时。

  • 假设授予实例化密钥的权限

    long keyctl(KEYCTL_ASSUME_AUTHORITY, key_serial_t key);
    

    这将假设或解除实例化指定密钥所需的权限。只有当线程在其密钥环中的某个位置具有与指定密钥关联的授权密钥时,才能假设权限。

    一旦假设权限,搜索密钥时也会使用请求者的安全标签、UID、GID 和组搜索请求者的密钥环。

    如果请求的权限不可用,将返回 EPERM 错误,如果目标密钥已经实例化,权限已被撤销,也会返回 EPERM 错误。

    如果指定的密钥为 0,则将解除所有假设的权限。

    假设的授权密钥在 fork 和 exec 中继承。

  • 获取附加到密钥的 LSM 安全上下文

    long keyctl(KEYCTL_GET_SECURITY, key_serial_t key, char *buffer,
                size_t buflen)
    

    此函数返回一个字符串,表示在提供的缓冲区中附加到密钥的 LSM 安全上下文。

    除非发生错误,否则它始终返回它可以生成的数据量,即使该数据量对于缓冲区来说太大,但它不会复制超过请求量的数据到用户空间。如果缓冲区指针为 NULL,则不会发生复制。

    如果缓冲区足够大,则字符串末尾会包含一个 NUL 字符。这包含在返回的计数中。如果没有强制执行 LSM,则将返回一个空字符串。

    进程必须具有密钥的“查看”权限,此函数才能成功。

  • 在其父进程上安装调用进程的会话密钥环

    long keyctl(KEYCTL_SESSION_TO_PARENT);
    

    此函数尝试将调用进程的会话密钥环安装到调用进程的父进程上,替换父进程当前的会话密钥环。

    调用进程必须与其父进程具有相同的拥有权,密钥环必须与调用进程具有相同的拥有权,调用进程必须对密钥环具有 LINK 权限,并且活动的 LSM 模块不能拒绝权限,否则将返回 EPERM 错误。

    如果没有足够的内存来完成操作,将返回 ENOMEM 错误,否则将返回 0 表示成功。

    密钥环将在父进程下次离开内核并恢复执行用户空间时被替换。

  • 使密钥失效

    long keyctl(KEYCTL_INVALIDATE, key_serial_t key);
    

    此函数将密钥标记为失效,然后唤醒垃圾回收器。当密钥的引用计数达到零时,垃圾回收器会立即从所有密钥环中删除失效的密钥并删除该密钥。

    标记为失效的密钥会立即对正常的密钥操作不可见,尽管它们在删除之前在 /proc/keys 中仍然可见(它们用 ‘i’ 标志标记)。

    进程必须对密钥具有搜索权限才能成功执行此函数。

  • 计算 Diffie-Hellman 共享密钥或公钥

    long keyctl(KEYCTL_DH_COMPUTE, struct keyctl_dh_params *params,
                char *buffer, size_t buflen, struct keyctl_kdf_params *kdf);
    

    params 结构包含三个密钥的序列号

    - The prime, p, known to both parties
    - The local private key
    - The base integer, which is either a shared generator or the
      remote public key
    

    计算的值是

    result = base ^ private (mod prime)
    

    如果基数是共享生成器,则结果是本地公钥。如果基数是远程公钥,则结果是共享密钥。

    如果参数 kdf 为 NULL,则适用以下内容

    • 缓冲区长度必须至少为素数的长度,或为零。

    • 如果缓冲区长度为非零,则在成功计算并复制到缓冲区时,将返回结果的长度。当缓冲区长度为零时,将返回最小所需缓冲区长度。

    kdf 参数允许调用者对 Diffie-Hellman 计算应用密钥派生函数 (KDF),其中仅将 KDF 的结果返回给调用者。KDF 的特点是 struct keyctl_kdf_params,如下所示

    • char *hashname 指定一个以 NUL 结尾的字符串,用于标识从内核加密 API 中使用的散列,并将其应用于 KDF 操作。KDF 实现符合 SP800-56A 以及 SP800-108(计数器 KDF)。

    • char *otherinfo 指定 SP800-56A 第 5.8.1.2 节中记录的 OtherInfo 数据。缓冲区的长度由 otherinfolen 给出。OtherInfo 的格式由调用者定义。如果不需要使用 OtherInfo,则 otherinfo 指针可以为 NULL。

    如果密钥类型不受支持,此函数将返回 EOPNOTSUPP 错误;如果找不到密钥,将返回 ENOKEY 错误;如果调用者无法读取密钥,将返回 EACCES 错误。此外,当参数 kdf 不为 NULL 且缓冲区长度或 OtherInfo 长度超过允许的长度时,该函数将返回 EMSGSIZE。

  • 限制密钥环链接

    long keyctl(KEYCTL_RESTRICT_KEYRING, key_serial_t keyring,
                const char *type, const char *restriction);
    

    现有的密钥环可以通过根据限制方案评估密钥的内容来限制其他密钥的链接。

    “keyring” 是一个现有密钥环的密钥 ID,用于应用限制。它可以为空,也可以已经链接了密钥。即使新限制会拒绝它们,现有的链接密钥仍将保留在密钥环中。

    “type” 是一个注册的密钥类型。

    “restriction” 是一个字符串,描述如何限制密钥链接。该格式因密钥类型而异,并且该字符串被传递给请求类型的 lookup_restriction() 函数。它可以指定一种方法和用于限制的相关数据,例如签名验证或密钥有效负载的约束。如果请求的密钥类型稍后被取消注册,则在该密钥类型被删除后,无法向密钥环添加任何密钥。

    要应用密钥环限制,进程必须具有设置属性权限,并且密钥环必须之前未被限制。

    受限制密钥环的一个应用是使用非对称密钥类型验证 X.509 证书链或单个证书签名。 有关适用于非对称密钥类型的具体限制,请参阅 非对称/公钥加密密钥类型

  • 查询非对称密钥

    long keyctl(KEYCTL_PKEY_QUERY,
                key_serial_t key_id, unsigned long reserved,
                const char *params,
                struct keyctl_pkey_query *info);
    

    获取有关非对称密钥的信息。可以通过使用 params 参数来查询特定的算法和编码。这是一个包含以空格或制表符分隔的键值对的字符串。当前支持的键包括 enchash。信息在 keyctl_pkey_query 结构中返回。

    __u32   supported_ops;
    __u32   key_size;
    __u16   max_data_size;
    __u16   max_sig_size;
    __u16   max_enc_size;
    __u16   max_dec_size;
    __u32   __spare[10];
    

    supported_ops 包含一个标志位掩码,指示支持哪些操作。 这是通过按位或运算以下内容构建的

    KEYCTL_SUPPORTS_{ENCRYPT,DECRYPT,SIGN,VERIFY}
    

    key_size 表示密钥的大小(以位为单位)。

    max_*_size 表示要签名的 blob 数据、签名 blob、要加密的 blob 和要解密的 blob 的最大字节大小。

    __spare[] 必须设置为 0。此项旨在未来用于传递解锁密钥所需的一个或多个密码。

    如果成功,则返回 0。如果密钥不是非对称密钥,则返回 EOPNOTSUPP。

  • 使用非对称密钥加密、解密、签名或验证 blob。

    long keyctl(KEYCTL_PKEY_ENCRYPT,
                const struct keyctl_pkey_params *params,
                const char *info,
                const void *in,
                void *out);
    
    long keyctl(KEYCTL_PKEY_DECRYPT,
                const struct keyctl_pkey_params *params,
                const char *info,
                const void *in,
                void *out);
    
    long keyctl(KEYCTL_PKEY_SIGN,
                const struct keyctl_pkey_params *params,
                const char *info,
                const void *in,
                void *out);
    
    long keyctl(KEYCTL_PKEY_VERIFY,
                const struct keyctl_pkey_params *params,
                const char *info,
                const void *in,
                const void *in2);
    

    使用非对称密钥对 blob 数据执行公钥加密操作。对于加密和验证,非对称密钥可能只需要公钥部分可用,但对于解密和签名,还需要私钥部分。

    params 指向的参数块包含若干整数值。

    __s32           key_id;
    __u32           in_len;
    __u32           out_len;
    __u32           in2_len;
    

    key_id 是要使用的非对称密钥的 ID。 in_lenin2_len 指示 in 和 in2 缓冲区中的数据量,out_len 指示输出缓冲区的大小,这对于上述操作是适当的。

    对于给定的操作,in 和 out 缓冲区的使用方式如下:

    Operation ID            in,in_len       out,out_len     in2,in2_len
    ======================= =============== =============== ===============
    KEYCTL_PKEY_ENCRYPT     Raw data        Encrypted data  -
    KEYCTL_PKEY_DECRYPT     Encrypted data  Raw data        -
    KEYCTL_PKEY_SIGN        Raw data        Signature       -
    KEYCTL_PKEY_VERIFY      Raw data        -               Signature
    

    info 是一个由键值对组成的字符串,用于提供补充信息。其中包括:

    enc=<encoding> 加密/签名 blob 的编码。此

    可以是 "pkcs1" 表示 RSASSA-PKCS1-v1.5 或 RSAES-PKCS1-v1.5;"pss" 表示 "RSASSA-PSS";"oaep" 表示 "RSAES-OAEP"。如果省略或为 "raw",则指定加密函数的原始输出。

    hash=<algo> 如果数据缓冲区包含哈希的输出

    函数,并且编码中包含一些关于使用了哪个哈希函数的指示,则可以使用此项指定哈希函数,例如 "hash=sha256"。

    参数块中的 __spare[] 空间必须设置为 0。此项的意图包括允许传递解锁密钥所需的密码。

    如果成功,加密、解密和签名都会返回写入输出缓冲区的数据量。验证成功时返回 0。

  • 监视密钥或密钥环的更改。

    long keyctl(KEYCTL_WATCH_KEY, key_serial_t key, int queue_fd,
                const struct watch_notification_filter *filter);
    

    这将设置或删除对指定密钥或密钥环的更改监视。

    "key" 是要监视的密钥的 ID。

    "queue_fd" 是指代开放管道的文件描述符,该管道管理将向其中传递通知的缓冲区。

    "filter" 为 NULL 以删除监视,或为过滤器规范以指示密钥所需的事件。

    有关更多信息,请参阅通用通知机制

    请注意,对于任何特定的 { 密钥, queue_fd } 组合,只能放置一个监视。

    通知记录如下所示:

    struct key_notification {
            struct watch_notification watch;
            __u32   key_id;
            __u32   aux;
    };
    

    其中,watch::type 将为 "WATCH_TYPE_KEY_NOTIFY",subtype 将为以下之一:

    NOTIFY_KEY_INSTANTIATED
    NOTIFY_KEY_UPDATED
    NOTIFY_KEY_LINKED
    NOTIFY_KEY_UNLINKED
    NOTIFY_KEY_CLEARED
    NOTIFY_KEY_REVOKED
    NOTIFY_KEY_INVALIDATED
    NOTIFY_KEY_SETATTR
    

    这些指示密钥正在实例化/拒绝、更新、在密钥环中创建链接、从密钥环中删除链接、密钥环被清除、密钥被撤销、密钥失效或密钥的某个属性已更改(用户、组、权限、超时、限制)。

    如果监视的密钥被删除,则将发出一个基本 watch_notification,其中 "type" 设置为 WATCH_TYPE_META,"subtype" 设置为 watch_meta_removal_notification。"info" 字段中将设置监视点 ID。

    这需要通过启用以下选项进行配置:

    "提供密钥/密钥环更改通知" (KEY_NOTIFICATIONS)

内核服务

用于密钥管理的内核服务相当简单易用。它们可以分为两个领域:密钥和密钥类型。

处理密钥相当简单。首先,内核服务注册其类型,然后搜索该类型的密钥。它应保留密钥,只要它有需要,然后应将其释放。对于文件系统或设备文件,搜索可能会在打开调用期间执行,并在关闭时释放密钥。如何解决两个不同用户打开同一文件导致的冲突密钥留给文件系统作者解决。

要访问密钥管理器,必须 #include 以下头文件:

<linux/key.h>

特定的密钥类型应在 include/keys/ 下有一个头文件,该文件应用于访问该类型。例如,对于类型为“user”的密钥,该头文件为:

<keys/user-type.h>

请注意,可能会遇到两种不同类型的密钥指针:

  • struct key *

    这只是指向密钥结构本身。密钥结构将至少是四字节对齐的。

  • key_ref_t

    这等效于 struct key *,但如果调用者“拥有”密钥,则设置最低有效位。“拥有”是指调用进程从其密钥环之一具有指向密钥的可搜索链接。有三个函数用于处理这些:

    key_ref_t make_key_ref(const struct key *key, bool possession);
    
    struct key *key_ref_to_ptr(const key_ref_t key_ref);
    
    bool is_key_possessed(const key_ref_t key_ref);
    

    第一个函数从密钥指针和所有权信息(必须为 true 或 false)构造密钥引用。

    第二个函数从引用中检索密钥指针,第三个函数检索所有权标志。

访问密钥的有效负载内容时,必须采取某些预防措施来防止访问与修改竞争。有关更多信息,请参阅“关于访问有效负载内容的说明”部分。

  • 要搜索密钥,请调用:

    struct key *request_key(const struct key_type *type,
                            const char *description,
                            const char *callout_info);
    

    这用于请求描述与根据密钥类型的 match_preparse() 方法指定的描述匹配的密钥或密钥环。这允许进行近似匹配。如果 callout_string 不为 NULL,则将调用 /sbin/request-key 以尝试从用户空间获取密钥。在这种情况下,callout_string 将作为参数传递给程序。

    如果该函数失败,将返回错误 ENOKEY、EKEYEXPIRED 或 EKEYREVOKED。

    如果成功,密钥将已附加到隐式获取的请求密钥密钥的默认密钥环,如 KEYCTL_SET_REQKEY_KEYRING 设置的那样。

    另请参阅密钥请求服务

  • 要在特定域中搜索密钥,请调用:

    struct key *request_key_tag(const struct key_type *type,
                                const char *description,
                                struct key_tag *domain_tag,
                                const char *callout_info);
    

    这与 request_key() 相同,只不过可以指定一个域标签,该标签会导致搜索算法仅匹配与该标签匹配的密钥。domain_tag 可以为 NULL,指定与任何指定的域分离的全局域。

  • 要搜索密钥,并将辅助数据传递给 upcaller,请调用:

    struct key *request_key_with_auxdata(const struct key_type *type,
                                         const char *description,
                                         struct key_tag *domain_tag,
                                         const void *callout_info,
                                         size_t callout_len,
                                         void *aux);
    

    这与 request_key_tag() 相同,只不过如果存在辅助数据,则会将其传递给 key_type->request_key() 操作,并且 callout_info 是长度为 callout_len 的 blob(如果给定),(长度可以为 0)。

  • 要在 RCU 条件下搜索密钥,请调用:

    struct key *request_key_rcu(const struct key_type *type,
                                const char *description,
                                struct key_tag *domain_tag);
    

    这类似于 request_key_tag(),只不过它不检查正在构造的密钥,如果找不到匹配项,它也不会调用用户空间来构造密钥。

  • 不再需要密钥时,应使用以下命令释放密钥:

    void key_put(struct key *key);
    

    void key_ref_put(key_ref_t key_ref);
    

    这些可以从中断上下文中调用。如果未设置 CONFIG_KEYS,则不会解析参数。

  • 可以通过调用以下函数之一来创建对密钥的额外引用:

    struct key *__key_get(struct key *key);
    struct key *key_get(struct key *key);
    

    因此,引用需要在使用完毕后通过调用 key_put() 来处理。传入的密钥指针将返回。

    在 key_get() 的情况下,如果指针为 NULL 或未设置 CONFIG_KEYS,则密钥将不会被取消引用,也不会发生增量。

  • 可以通过调用以下命令获取密钥的序列号:

    key_serial_t key_serial(struct key *key);
    

    如果 key 为 NULL 或未设置 CONFIG_KEYS,则将返回 0(在后一种情况下,不解析参数)。

  • 如果在搜索中找到密钥环,则可以通过以下命令进一步搜索:

    key_ref_t keyring_search(key_ref_t keyring_ref,
                             const struct key_type *type,
                             const char *description,
                             bool recurse)
    

    这仅搜索指定的密钥环(recurse == false)或指定的密钥环树(recurse == true)以查找匹配的密钥。失败时返回错误 ENOKEY(使用 IS_ERR/PTR_ERR 确定)。如果成功,则需要释放返回的密钥。

    密钥环引用的所有权属性用于通过权限掩码控制访问,如果成功,则会传播到返回的密钥引用指针。

  • 可以通过以下命令创建密钥环:

    struct key *keyring_alloc(const char *description, uid_t uid, gid_t gid,
                              const struct cred *cred,
                              key_perm_t perm,
                              struct key_restriction *restrict_link,
                              unsigned long flags,
                              struct key *dest);
    

    这将创建具有给定属性的密钥环并返回它。如果 dest 不为 NULL,则新的密钥环将链接到它指向的密钥环。不会对目标密钥环进行权限检查。

    如果密钥环会使配额超载,则会返回错误 EDQUOT(如果密钥环不应计入用户的配额,则在标志中传递 KEY_ALLOC_NOT_IN_QUOTA)。也可能会返回错误 ENOMEM。

    如果 restrict_link 不为 NULL,则它应指向一个结构,其中包含每次尝试将密钥链接到新密钥环时都会调用的函数。该结构还可以包含密钥指针和关联的密钥类型。调用该函数是为了检查是否可以将密钥添加到密钥环中。密钥类型由垃圾回收器使用,以清理此结构中的函数或数据指针(如果给定的密钥类型未注册)。内核中 key_create_or_update() 的调用者可以传递 KEY_ALLOC_BYPASS_RESTRICTION 以禁止检查。使用此方法的一个示例是管理内核启动时设置的加密密钥环,其中还允许用户空间添加密钥 - 前提是它们可以由内核已有的密钥验证。

    调用时,将向限制函数传递正在添加到的密钥环、密钥类型、正在添加的密钥的有效负载以及用于限制检查的数据。请注意,当正在创建新密钥时,这在有效负载预解析和实际密钥创建之间调用。该函数应返回 0 以允许链接,或返回错误以拒绝链接。

    为了在这种情况下始终返回 -EPERM,存在一个方便的函数 restrict_link_reject。

  • 要检查密钥的有效性,可以调用此函数:

    int validate_key(struct key *key);
    

    此项检查确保相关密钥尚未过期且未被吊销。如果密钥无效,将返回错误 EKEYEXPIRED 或 EKEYREVOKED。如果密钥为 NULL 或未设置 CONFIG_KEYS,则将返回 0(在后一种情况下,不解析参数)。

  • 要注册密钥类型,应调用以下函数

    int register_key_type(struct key_type *type);
    

    如果已存在同名的类型,则会返回错误 EEXIST。

  • 要注销密钥类型,请调用

    void unregister_key_type(struct key_type *type);
    

在某些情况下,可能需要处理一组密钥。该工具提供对密钥环类型的访问权限,以管理此类捆绑包。

struct key_type key_type_keyring;

这可以与诸如 request_key() 之类的函数一起使用,以查找进程密钥环中的特定密钥环。然后可以使用 keyring_search() 搜索找到的密钥环。请注意,无法使用 request_key() 搜索特定的密钥环,因此以这种方式使用密钥环的效用有限。

访问有效负载内容的注意事项

最简单的有效负载只是直接存储在 key->payload 中的数据。在这种情况下,访问有效负载时无需使用 RCU 或锁定。

更复杂的有效负载内容必须分配,并且指向它们的指针必须设置在 key->payload.data[] 数组中。必须选择以下方法之一来访问数据

  1. 不可修改的密钥类型。

    如果密钥类型没有修改方法,那么可以无需任何形式的锁定即可访问密钥的有效负载,前提是已知该密钥已实例化(未实例化的密钥无法“找到”)。

  2. 密钥的信号量。

    可以使用信号量来控制对有效负载的访问并控制有效负载指针。它必须被写锁定以进行修改,并且必须被读锁定以进行一般访问。这样做的不利之处在于,访问器可能需要休眠。

  3. RCU。

    当未持有信号量时,必须使用 RCU;如果持有信号量,则内容不会意外更改,因为仍然必须使用信号量来序列化对密钥的修改。密钥管理代码会为密钥类型处理此问题。

    但是,这意味着使用

    rcu_read_lock() ... rcu_dereference() ... rcu_read_unlock()
    

    读取指针,以及

    rcu_dereference() ... rcu_assign_pointer() ... call_rcu()
    

    设置指针并在宽限期后处理旧内容。请注意,只有密钥类型应该修改密钥的有效负载。

    此外,RCU 控制的有效负载必须包含一个 struct rcu_head,供 call_rcu() 使用,如果有效负载的大小可变,则必须包含有效负载的长度。如果未持有密钥的信号量,则不能保证 key->datalen 与刚刚取消引用的有效负载一致。

    请注意,key->payload.data[0] 有一个标记为 __rcu 使用的影子。这称为 key->payload.rcu_data0。以下访问器包装对此元素的 RCU 调用

    1. 设置或更改第一个有效负载指针

      rcu_assign_keypointer(struct key *key, void *data);
      
    2. 在持有密钥信号量的情况下读取第一个有效负载指针

             [const] void *dereference_key_locked([const] struct key *key);
      
      Note that the return value will inherit its constness from the key
      parameter.  Static analysis will give an error if it things the lock
      isn't held.
      
    3. 在持有 RCU 读锁的情况下读取第一个有效负载指针

      const void *dereference_key_rcu(const struct key *key);
      

定义密钥类型

内核服务可能需要定义自己的密钥类型。例如,AFS 文件系统可能需要定义 Kerberos 5 票证密钥类型。为此,作者填写一个 key_type 结构并将其注册到系统。

实现密钥类型的源文件应包含以下头文件

<linux/key-type.h>

该结构有许多字段,其中一些是强制性的

  • const char *name

    密钥类型的名称。这用于将用户空间提供的密钥类型名称转换为指向该结构的指针。

  • size_t def_datalen

    这是可选的 - 它提供作为配额贡献的默认有效负载数据长度。如果密钥类型的有效负载始终或几乎始终大小相同,那么这是一种更有效的方法。

    可以通过调用以下函数在实例化或更新期间随时更改特定密钥的数据长度(和配额)

    int key_payload_reserve(struct key *key, size_t datalen);
    

    使用修改后的数据长度。如果不可行,将返回错误 EDQUOT。

  • int (*vet_description)(const char *description);

    此可选方法用于检查密钥描述。如果密钥类型不批准密钥描述,则可能返回错误,否则应返回 0。

  • int (*preparse)(struct key_preparsed_payload *prep);

    此可选方法允许密钥类型在创建密钥(添加密钥)或获取密钥信号量(更新或实例化密钥)之前尝试解析有效负载。prep 指向的结构如下所示

    struct key_preparsed_payload {
            char            *description;
            union key_payload payload;
            const void      *data;
            size_t          datalen;
            size_t          quotalen;
            time_t          expiry;
    };
    

    在调用该方法之前,调用方将使用有效负载 blob 参数填充数据和 datalen;quotalen 将使用密钥类型的默认配额大小填充;expiry 将设置为 TIME_T_MAX,其余部分将被清除。

    如果可以从有效负载内容中提出描述,则应将其作为字符串附加到描述字段。如果 add_key() 的调用方传递 NULL 或 “”,则这将用作密钥描述。

    该方法可以将任何它喜欢的东西附加到有效负载。这只是传递给 instantiate() 或 update() 操作。如果设置了过期时间,则如果从此数据实例化密钥,则该过期时间将应用于该密钥。

    如果成功,该方法应返回 0,否则返回负错误代码。

  • void (*free_preparse)(struct key_preparsed_payload *prep);

    只有在提供了 preparse() 方法时才需要此方法,否则它将未使用。它会清除 key_preparsed_payload 结构的 description 和 payload 字段上附加的任何内容,这些内容由 preparse() 方法填充。即使 instantiate() 或 update() 成功,也始终会在 preparse() 成功返回后调用它。

  • int (*instantiate)(struct key *key, struct key_preparsed_payload *prep);

    此方法在构造期间用于将有效负载附加到密钥。附加的有效负载不必与传递给此函数的数据有任何关系。

    prep->data 和 prep->datalen 字段将定义原始有效负载 blob。如果提供了 preparse(),则也可以填充其他字段。

    如果附加到密钥的数据量与 keytype->def_datalen 中的大小不同,则应调用 key_payload_reserve()。

    此方法不必锁定密钥即可附加有效负载。key->flags 中未设置 KEY_FLAG_INSTANTIATED 的事实阻止了其他任何对象获得对密钥的访问权限。

    在此方法中休眠是安全的。

    generic_key_instantiate() 用于将数据从 prep->payload.data[] 简单地复制到 key->payload.data[],并在第一个元素上进行 RCU 安全分配。然后,它将清除 prep->payload.data[],以便 free_preparse 方法不会释放数据。

  • int (*update)(struct key *key, const void *data, size_t datalen);

    如果可以更新此类型的密钥,则应提供此方法。它用于从提供的 blob 数据更新密钥的有效负载。

    prep->data 和 prep->datalen 字段将定义原始有效负载 blob。如果提供了 preparse(),则也可以填充其他字段。

    如果在实际进行任何更改之前数据长度可能会更改,则应调用 key_payload_reserve()。请注意,如果此操作成功,则类型将承诺更改密钥,因为它已经被更改,因此必须首先完成所有内存分配。

    在调用此方法之前,密钥将具有其信号量写锁定,但这仅阻止其他写入器;必须在 RCU 条件下对密钥的有效负载进行任何更改,并且必须使用 call_rcu() 来处理旧的有效负载。

    应在进行更改之前调用 key_payload_reserve(),但在进行所有分配和其他可能失败的函数调用之后。

    在此方法中休眠是安全的。

  • int (*match_preparse)(struct key_match_data *match_data);

    此方法是可选的。当即将执行密钥搜索时调用它。它被赋予以下结构

    struct key_match_data {
            bool (*cmp)(const struct key *key,
                        const struct key_match_data *match_data);
            const void      *raw_data;
            void            *preparsed;
            unsigned        lookup_type;
    };
    

    在进入时,raw_data 将指向调用方用于匹配密钥的标准,并且不应被修改。(*cmp)() 将指向默认匹配器函数(该函数针对 raw_data 执行精确的描述匹配),并且 lookup_type 将设置为指示直接查找。

    以下 lookup_type 值可用

    • KEYRING_SEARCH_LOOKUP_DIRECT - 直接查找对类型和描述进行哈希处理,以将搜索范围缩小到少量密钥。

    • KEYRING_SEARCH_LOOKUP_ITERATE - 迭代查找遍历密钥环中的所有密钥,直到找到匹配的密钥。这必须用于任何不在密钥描述上进行简单直接匹配的搜索。

    该方法可以将 cmp 设置为指向其选择的执行其他形式匹配的函数,可以将 lookup_type 设置为 KEYRING_SEARCH_LOOKUP_ITERATE,并且可以将某些内容附加到 preparsed 指针,供 (*cmp)() 使用。(*cmp)() 如果密钥匹配,则应返回 true,否则返回 false。

    如果设置了 preparsed,则可能需要使用 match_free() 方法来清理它。

    如果成功,该方法应返回 0,否则返回负错误代码。

    允许在此方法中休眠,但 (*cmp)() 可能不会休眠,因为锁将保持在其上。

    如果未提供 match_preparse(),则此类型的密钥将通过其描述完全匹配。

  • void (*match_free)(struct key_match_data *match_data);

    此方法是可选的。如果给定,则会在成功调用 match_preparse() 后调用它来清理 match_data->preparsed。

  • void (*revoke)(struct key *key);

    此方法是可选的。在吊销密钥后,调用它来丢弃部分有效负载数据。调用方将具有密钥信号量写锁定。

    在此方法中休眠是安全的,尽管应注意避免针对密钥信号量发生死锁。

  • void (*destroy)(struct key *key);

    此方法是可选的。当密钥被销毁时,调用它来丢弃密钥上的有效负载数据。

    此方法不需要锁定密钥即可访问有效负载;此时可以认为密钥不可访问。请注意,密钥的类型可能在此函数被调用之前已更改。

    在此方法中休眠是不安全的;调用者可能持有自旋锁。

  • void (*describe)(const struct key *key, struct seq_file *p);

    此方法是可选的。它在读取 /proc/keys 期间被调用,以文本形式总结密钥的描述和有效负载。

    此方法将在持有 RCU 读取锁的情况下被调用。如果要访问有效负载,应使用 rcu_dereference() 来读取有效负载指针。不能信任 key->datalen 与有效负载的内容保持一致。

    描述不会改变,但密钥的状态可能会改变。

    在此方法中休眠是不安全的;RCU 读取锁被调用者持有。

  • long (*read)(const struct key *key, char __user *buffer, size_t buflen);

    此方法是可选的。它由 KEYCTL_READ 调用,以将密钥的有效负载转换为用户空间可以处理的数据 blob。理想情况下,该 blob 的格式应与传递给实例化和更新方法的格式相同。

    如果成功,应返回可以产生的 blob 大小,而不是复制的大小。

    此方法将在密钥的信号量读取锁定的情况下被调用。这将阻止密钥的有效负载发生更改。访问密钥的有效负载时,无需使用 RCU 锁定。在此方法中休眠是安全的,例如在访问用户空间缓冲区时可能发生的情况。

  • int (*request_key)(struct key_construction *cons, const char *op, void *aux);

    此方法是可选的。如果提供,request_key() 及其友元将调用此函数,而不是向上调用 /sbin/request-key 来操作此类型的密钥。

    aux 参数与传递给 request_key_async_with_auxdata() 和类似函数的参数相同,否则为 NULL。同时传递的还有要操作的密钥的构造记录和操作类型(目前仅为“create”)。

    此方法允许在向上调用完成之前返回,但在任何情况下都必须调用以下函数来完成实例化过程,无论是否成功,无论是否有错误

    void complete_request_key(struct key_construction *cons, int error);
    

    成功时,error 参数应为 0,错误时为 -ve。构造记录会被此操作销毁,并且授权密钥将被撤销。如果指示了错误,则如果未实例化,则正在构造的密钥将被负实例化。

    如果此方法返回错误,则该错误将返回给 request_key*() 的调用者。必须在返回之前调用 complete_request_key()。

    正在构造的密钥和授权密钥可以在 cons 指向的 key_construction 结构中找到

    • struct key *key;

      正在构造的密钥。

    • struct key *authkey;

      授权密钥。

  • struct key_restriction *(*lookup_restriction)(const char *params);

    此可选方法用于使用户空间可以配置密钥环限制。传入限制参数字符串(不包括密钥类型名称),此方法返回指向 key_restriction 结构的指针,该结构包含相关函数和数据以评估每次尝试的密钥链接操作。如果没有匹配项,则返回 -EINVAL。

  • asym_eds_opasym_verify_signature

    int (*asym_eds_op)(struct kernel_pkey_params *params,
                       const void *in, void *out);
    int (*asym_verify_signature)(struct kernel_pkey_params *params,
                                 const void *in, const void *in2);
    

    这些方法是可选的。如果提供了第一个,则允许使用密钥来加密、解密或签名数据 blob,第二个允许使用密钥来验证签名。

    在所有情况下,params 块中都提供了以下信息

    struct kernel_pkey_params {
            struct key      *key;
            const char      *encoding;
            const char      *hash_algo;
            char            *info;
            __u32           in_len;
            union {
                    __u32   out_len;
                    __u32   in2_len;
            };
            enum kernel_pkey_operation op : 8;
    };
    

    这包括要使用的密钥;指示要使用的编码的字符串(例如,“pkcs1” 可以与 RSA 密钥一起使用,以指示 RSASSA-PKCS1-v1.5 或 RSAES-PKCS1-v1.5 编码,或者如果没有编码,则使用 “raw”);用于生成签名数据的哈希算法的名称(如果适用);输入和输出(或第二个输入)缓冲区的大小;以及要执行的操作的 ID。

    对于给定的操作 ID,输入和输出缓冲区的使用方式如下

    Operation ID            in,in_len       out,out_len     in2,in2_len
    ======================= =============== =============== ===============
    kernel_pkey_encrypt     Raw data        Encrypted data  -
    kernel_pkey_decrypt     Encrypted data  Raw data        -
    kernel_pkey_sign        Raw data        Signature       -
    kernel_pkey_verify      Raw data        -               Signature
    

    asym_eds_op() 处理由 params->op 指定的加密、解密和签名创建。请注意,params->op 也为 asym_verify_signature() 设置。

    加密和签名创建都将原始数据放在输入缓冲区中,并在输出缓冲区中返回加密结果。如果设置了编码,则可能已添加填充。在签名创建的情况下,根据编码,创建的填充可能需要指示摘要算法 - 其名称应在 hash_algo 中提供。

    解密将加密数据放在输入缓冲区中,并在输出缓冲区中返回原始数据。如果设置了编码,则将检查并剥离填充。

    验证将原始数据放在输入缓冲区中,将签名放在第二个输入缓冲区中,并检查两者是否匹配。将验证填充。根据编码,用于生成原始数据的摘要算法可能需要在 hash_algo 中指示。

    如果成功,asym_eds_op() 应返回写入输出缓冲区的字节数。asym_verify_signature() 应返回 0。

    可能会返回各种错误,包括如果不支持该操作则返回 EOPNOTSUPP;如果验证失败则返回 EKEYREJECTED;如果所需的加密不可用,则返回 ENOPKG。

  • asym_query:

    int (*asym_query)(const struct kernel_pkey_params *params,
                      struct kernel_pkey_query *info);
    

    此方法是可选的。如果提供,则允许确定密钥中保存的公钥或非对称密钥的信息。

    参数块与 asym_eds_op() 等相同,但 in_len 和 out_len 未使用。应使用编码和 hash_algo 字段来适当减少返回的缓冲区/数据大小。

    如果成功,将填写以下信息

    struct kernel_pkey_query {
            __u32           supported_ops;
            __u32           key_size;
            __u16           max_data_size;
            __u16           max_sig_size;
            __u16           max_enc_size;
            __u16           max_dec_size;
    };
    

    supported_ops 字段将包含一个位掩码,指示密钥支持的操作,包括加密 blob、解密 blob、签名 blob 和验证 blob 上的签名。为此定义了以下常量

    KEYCTL_SUPPORTS_{ENCRYPT,DECRYPT,SIGN,VERIFY}
    

    key_size 字段是以位为单位的密钥大小。max_data_size 和 max_sig_size 是用于创建和验证签名的最大原始数据和签名大小;max_enc_size 和 max_dec_size 是用于加密和解密的最大原始数据和签名大小。max_*_size 字段以字节为单位度量。

    如果成功,将返回 0。如果密钥不支持此操作,则将返回 EOPNOTSUPP。

请求密钥回调服务

要创建新密钥,内核将尝试执行以下命令行

/sbin/request-key create <key> <uid> <gid> \
        <threadring> <processring> <sessionring> <callout_info>

<key> 是正在构造的密钥,三个密钥环是发出搜索的进程中的进程密钥环。包含这些密钥环的原因有两个

1 其中一个密钥环中可能有一个身份验证令牌

获取密钥是必需的,例如:Kerberos 票证授予票证。

2 新密钥可能应缓存在这些环中的一个中。

此程序应在尝试访问任何更多密钥之前将其 UID 和 GID 设置为指定的 UID 和 GID。然后,它可能会寻找一个用户特定的进程,将请求传递给该进程(可能是一个路径,例如,KDE 桌面管理器放置在另一个密钥中)。

该程序(或它调用的任何程序)应通过调用 KEYCTL_INSTANTIATE 或 KEYCTL_INSTANTIATE_IOV 来完成密钥的构造,这也允许它在返回之前将密钥缓存在其中一个密钥环中(可能是会话环)。或者,可以使用 KEYCTL_NEGATE 或 KEYCTL_REJECT 将密钥标记为负;这也允许将密钥缓存在其中一个密钥环中。

如果返回时密钥仍处于未构造状态,则该密钥将被标记为负数,它将被添加到会话密钥环中,并且将向密钥请求者返回错误。

可以从调用此服务的任何人或任何事物提供补充信息。这将作为 <callout_info> 参数传递。如果没有提供此类信息,则将 “-” 作为此参数传递。

同样,内核可能会尝试通过执行以下命令来更新已过期或即将过期的密钥

/sbin/request-key update <key> <uid> <gid> \
        <threadring> <processring> <sessionring>

在这种情况下,程序实际上不需要将密钥附加到环上;提供环以供参考。

垃圾回收

死密钥(已删除其类型的密钥)将自动从指向它们的那些密钥环中取消链接,并由后台垃圾收集器尽快删除。

同样,撤销的和过期的密钥将被垃圾回收,但仅在经过一定时间后才会进行。此时间设置为

/proc/sys/kernel/keys/gc_delay