内核密钥保留服务

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

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

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

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

本文档包含以下部分

密钥概述

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

每个密钥都有许多属性

  • 一个序列号。

  • 一个类型。

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

  • 访问控制信息。

  • 一个过期时间。

  • 一个有效负载。

  • 状态。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    • 已撤销。密钥由用户空间操作置于此状态。无法找到或对其进行操作(除了取消链接之外)。

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

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

密钥服务概述

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

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

    (+)“密钥环”

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

    (+)“用户”

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

    (+)“登录”

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

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

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

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

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

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

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

  • 系统中存在的每个用户 ID 都拥有两个特殊的密钥环:一个用户特定的密钥环和一个默认用户会话密钥环。默认会话密钥环使用指向用户特定密钥环的链接进行初始化。

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

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

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

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

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

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

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

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

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

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

密钥访问权限

密钥具有所有者用户 ID、组访问 ID 和权限掩码。该掩码对于所有者、用户、组和其他访问权限分别具有最多 8 位。仅定义了每组 8 位中的 6 位。授予的这些权限是

  • 查看

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

  • 读取

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

  • 写入

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

  • 搜索

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

  • 链接

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

  • 设置属性

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

对于更改所有权、组 ID 或权限掩码,拥有密钥的所有者身份或具有 sysadmin 功能就足够了。

SELinux 支持

安全类“key”已添加到 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 用户可以通过将每个新的限制作为十进制数字字符串写入相应的文件来更改这些限制。

用户空间系统调用接口

用户空间可以通过三个新的 syscall 直接操作密钥: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

主要的 syscall 是

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

    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 syscall 函数是

  • 将特殊密钥 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 作为错误代码拒绝密钥相同。

  • 设置默认的请求密钥目标密钥环

    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)
    

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

    如果参数 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 指示 out 缓冲区的大小,如上述操作适用。

    对于给定的操作,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 的编码。此

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

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

    函数,并且编码包括一些指示使用了哪个哈希函数的指示,则可以使用此方法指定哈希函数,例如“hash=sha256”。

    参数块中的 __spare[] 空间必须设置为 0。这旨在允许传递解锁密钥所需的密码短语等。

    如果成功,encrypt、decrypt 和 sign 都将返回写入输出缓冲区的数据量。验证成功时返回 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 以删除监视,也可以是筛选器规范以指示来自密钥的所需事件。

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

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

    通知记录如下所示

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

    其中,watch::type 将为“WATCH_TYPE_KEY_NOTIFY”,子类型将为以下之一

    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。监视点 ID 将设置在“info”字段中。

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

    “提供密钥/密钥环更改通知”(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);
    

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

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

当访问密钥的负载内容时,必须采取某些预防措施以防止访问与修改竞争。有关更多信息,请参阅“有关访问负载内容的注意事项”部分。

  • 要搜索密钥,请调用

    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() op 存在,则辅助数据将传递给它,并且 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 以允许链接,或返回错误以拒绝它。

    一个方便的函数 restrict_link_reject 存在,在这种情况下总是返回 -EPERM。

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

    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 参数填写 data 和 datalen;将使用密钥类型的默认配额大小填写 quotalen;expiry 将设置为 TIME_T_MAX,其余将被清除。

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

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

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

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

    只有在提供了 preparse() 方法时才需要此方法,否则不使用它。它清除 key_preparsed_payload 结构的 description 和 payload 字段中附加的任何内容,如 preparse() 方法所填充。它总是在 preparse() 成功返回后调用,即使 instantiate() 或 update() 成功。

  • 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,并且可以将某些东西附加到预解析的指针以供 (*cmp)() 使用。如果密钥匹配,(*cmp)() 应返回 true,否则返回 false。

    如果设置了预解析,则可能需要使用 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”)。

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

    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);
    

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

    在所有情况下,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() 和 co. 相同,但 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 字段将包含一个位掩码,指示密钥支持哪些操作,包括加密数据块、解密数据块、签名数据块和验证数据块上的签名。为此定义了以下常量

    KEYCTL_SUPPORTS_{ENCRYPT,DECRYPT,SIGN,VERIFY}
    

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

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

Request-Key 回调服务

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

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

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

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

获取密钥所需的,例如:Kerberos Ticket-Granting Ticket。

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

此程序应将其 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