Linux内核安全模块学习-内核密钥管理子系统

Posted mb62de8abf75c00

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux内核安全模块学习-内核密钥管理子系统相关的知识,希望对你有一定的参考价值。


本篇介绍密钥管理子系统,只涉及内核如何管理密钥,不涉及内核加密算法的实现。密钥本质上是一段数据,内核对它的管理有些类似对文件的管理。但是因为Linux内核不愿意让密钥像文件那样“静态”存储在磁盘或者其他永久性存储介质上,所以内核对密钥的管理又有些像对进程的管理,有创建、实例化、删除等操作。

先从系统中可见的伪文件系统proc看起,内核密钥管理在proc文件系统中创建了两个只读文件:/proc/keys和/proc/key-users。它们没有被创建在/proc/pid目录下,而是被直接创建在了proc文件系统的根目录下。这就造成了进程根本无法查看到别的进程的密钥。keys文件列出当前进程可查看的密钥,所以不同的进程读出的内容会不同。列出的内容包括序列号、过期时间、访问允许位、uid、gid、类型、描述等。key-users列出密钥的统计信息,包括uid、使用计数、密钥总数量和实例化数量、密钥数量的配额信息、密钥占用内存的配额信息。

密钥

密钥在内核代码中称为key,因为key是由用户态进程创建,由内核管理,其实体存储在内核申请的内存中,所以密钥管理需要实施配额管理。密钥有对称密钥和非对称密钥两大类,每类密钥又有很多种。密钥种类不同,payload中数据的格式和长度也不同。所以key数据结构中包含了数据成员type,其类型为key_type,其中包含若干函数指针,用于处理payload。主要内容可以参见include/linux/key-type.h。

include/linnux/key.h
struct key
...
key_serial_t serial; //内核通过此序列号来唯一地标识一个key
struct key_user *user; //包含一些和用户配额相关的数据,像用户拥有key的数量,用户拥有key占用总内存之类
kuid_t uid; //标识key的属主
kgid_t gid; //标识key的属组
key_perm_t perm; //key的访问权限
void *security; //被LSM使用,指向的内容由具体的LSM模块定义

unsigned short datalen; //标识payload的长度
union
union
unsigned long value;
void __rcu *rcudata;
void *data;
void *data2[2];
payload;
struct assoc_array keys;
;

union
struct keyring_index_key index_key;
struct
struct key_type *type;
char *description; //字符串,用于用户态进程查询密钥。用户态先用该字符串从内核查询到key对应的serial,后面直接用serial对key进行操作
;
;
unsigned long flags; //包含密钥的状态信息和密钥的生命周期有关
...
;

include/linux/key_type.h
struct key_type
const char* name;
...
int (*instantiate)(struct key *key, struct key_preparsed_payload *prep);
int (*update)(struct key *key, struct key_preparsed_payload *prep);
int (*match)(const struct key *key, const void *desc);
void (*revoke)(struct key *key);
...
;

flags用于存放密钥的状态信息。密钥是动态创建的,它是有生命周期。用户态进程首先会创建密钥,内核响应用户态进程的请求会生成一个密钥,分配内存。密钥的第二个状态是instantiated,内核将用户态进程提供的输入填入密钥的负载(payload)之中。这时的密钥就可以被用来做加解密使用。密钥的死亡状态有三种。revoke和invalidate都是由用户态进程发起的请求,内核对invalidate的响应是立即让密钥失效,收回密钥上的资源;内核对revoke的响应也是让密钥失效并收回资源,只是在此之前先调用密钥所属的类型(struct key_type)中定义的一个函数(revoke)。dead状态不是由用户态进程删除密钥导致的,而是由于一种类型(key_type)的密钥失效导致的。

    -------------
| allocated | -----> user_construct
------------- / |
| / |
| / |
\\/ \\ / |
---------------- \\/
| instantiated | <----- negatice
----------------
/ | \\
/ | \\
\\/ \\/ \\/
revoked dead mvalidated

密钥的构造由用户态进程发起,密钥的payload数据由用户态进程提供,或者由用户态进程指令内核生成。当内核子系统要用到某个密钥,而这个密钥还不存在怎么办?一种简单的做法是由内核启动一个用户态进程,再由这个进程来填充密钥的payload。在发起新进程之前,内核首先分配一个密钥,将密钥的状态设置为user_construct。发起的新进程负责填充密钥的payload。这时,进程有两个选择:一个是立刻提供payload并通知内核将密钥的状态置为instantiated;另一个是不能马上提供payload数据,它就通知内核将密钥的状态置为negative,以后再提供数据并修改状态。

security/keys/request_key.c
struct key *request_key(struct key_type *type, const char *description, const char *callout_info)
struct key *key; // 定义密钥结构体变量
size_t callout_len = 0;
int ret;

if (callout_info)
callout_len = strlen(callout_info);
// request_key_and_link会尝试查找密钥,如果没有找到,request_key_and_link会调用construct_key_and_link
key = request_key_and_link(type, description, callout_info, callout_len, NULL, NULL, KEY_ALLOC_IN_QUOTA);
if (!IS_ERR(key))
ret = wait_for_key_construction(key, false);
if (ret < 0)
key_put(key);
return ERR_PTR(ret);



return key;

密钥类型

钥匙环keyring
钥匙环可以包含若干密钥,当然这些密钥可以是另一个钥匙环。寻找一个密钥时,需要配合参数type,也就是说,不同类型的密钥在不同的名字空间中。比如一个类型为trusted的密钥和一个类型为user的密钥可以同名(严格地说,不是名字,是描述description),不会引起冲突。而是,一个密钥可以链接到多个钥匙环,密钥在不同的钥匙环中,其名字(描述)总是一样的。keyring使用key结构体类型的assoc_array成员,非keyring使用payload。

struct key 
...
union
union
unsigned long value;
void __rcu *rcudata;
void *data;
void *data2[2];
payload;
struct assoc_array keys;
;

密钥挂在钥匙环上,钥匙环可以再挂在另一个钥匙环上。当用户态进程要找一个钥匙,该从哪个钥匙环开始?文件系统有一个根目录,根目录是文件查找的起点。钥匙环也类似,钥匙环有若干个特殊的ID,供用户态进程查找。

ID

含义

-1

线程keyring

-2

进程keyring

-3

会话keyring

-4

用户ID对应的keyring

-5

用户会话keyring

-6

组ID对应的keyring

-7

request_key操作中认证密钥(auth_key)的keyring

-8

request_key目的keyring

每个线程有一个自己的钥匙环,每个进程有一个自己的钥匙环。会话概念的引入和登录(login)过程有关,用户登录系统,就是启动了一次会话,这次登录的进程,及后续子孙进程共享同一个会话id。现在通过字符终端tty登录Linux还是这样的情况。简而言之,一个会话就是一组进程,它们共享一些资源,比如会话钥匙环。
用户ID对应的钥匙环和组ID对应的钥匙环,脱离进程而存在。用户会话钥匙环主要用在登录程序,用户登录系统,输入用户名和口令,登录程序启动新进程(一般是一个shell),同时启动一个新会话,这个新进程的会话钥匙环就先设置为此次登录的用户的用户会话钥匙环

user类型的密钥由用户态进程创建,并且一般是用户态进程使用此种类型密钥。logon类型和user类型很相似,主要区别在于进程可以写入logon类型密钥的负载,但是不能读出logon类型密钥的负载。logon类型密钥的负载存储的是用户名和口令。内核中的一些子系统,比如cifs,会使用这些信息。

asymmetric对应非对称密钥,非对称密钥有两个密钥:公钥和私钥,公钥存储在payload成员中,私钥存储在type_data中。

encrypted这种类型的密钥之所以命名为encrypted,原因是用户态进程只能读到加密后的密钥数据,因此用户态进程是无法使用这种密钥的。这种密钥是由内核中的程序使用的,如ecryptfs和IMA。用来加密encrypted密钥数据的密钥有两种,一种是前面提到的user类型的密钥,另一种是后面要提到的trusted类型的密钥。内核中的密钥,是由用户态进程动态创建的,这里,encryted类型的密钥的设计的初衷就是不允许用户态接触到明文存储的密钥数据,那么,用户态进程又该怎么创建这种密钥呢?答案是,创建这种密钥时的payload是一个字符串,其中包含一个指令,内核根据该指令来创建密钥。指令语法:

  1. “new [format] key-type:master-key-name keylen” -创建密钥,密钥长度为keylen,使用类型为key-type,名字为master-key-name的密钥作为此次创建的密钥的加密密钥。format有两种形式:deflaut和ecryptfs。加密密钥的类型可以是trusted或者user。
  2. “load hex_blob”-根据hex_blob的值来创建密钥,hex_blob是一个hex字符串,字符串本身是有格式的,其中包含用于加密的密钥的类型和名字、哈希校验值及加密后的密钥数据。一般用法是创建一个encrypted密钥,将其内容读出导入一个文件,在每次系统启动时根据文件内容创建密钥。比如:
    cat /etc/keys/kmk | keyctl padd user kmk @u
    keyctl add encrypted evm-key "load `cat /etc/keys/evm-key` " @u
  3. “update key-type:master-key-name”-改变用于加密密钥的密钥。用户修改密钥的负载(payload),负载字符串是上面这个格式时,encrypted类型的密钥的加密密钥就会被更改。

trusted这种类型的密钥和TPM相关。由TPM硬件生成一个密钥,并存储在TPM硬件中。同encrypted类型,创建密钥时的payload是一个指令字符串。语法是:
“new keylen [options]”
“load hex_blob [pcrlock=pcrnum]”

系统调用

密钥管理子系统添加了三个系统调用:

  1. key_serial_t add_key(const char *type, const char *description, const void *payload, size_t plen, key_serial_t keyring)
    创建成功后,新密钥会被链接入参数keyring表示的钥匙环中
  2. long keyctl(int cmd, …) …后续参数由cmd取值决定
    cmd的取值有:KEYCTL_GET_KEYRING_ID、KEYCTL_JOIN_SESSION_KEYRING、KEYCTL_UPDATE、KEYCTL_REVOKER、KEYCTL_DESCRIBE、KEYCTL_CLEAR、KEYCTL_LINK、KEYCTL_UNLINK、KEYCTL_SEARCH、KEYCTL_READ、KEYCTL_CHOWN、KEYCTL_SETPERM、KEYCTL_INSTANTIATE、KEYCTL_INSTANTIATE_IOV、KEYCTL_NEGATE、KEYCTL_REJECCT、KEYCTL_SET_TIMEOUT、KEYCTL_INVALIDATE、KEYCTL_GET_SECURITY、KEYCTL_GET_PERSISTENT、KEYCTL_SET_REQKEY_KEYRING、KEYCTL_ASSUME_AUTHORITY
  3. key_serial_t request_key(const char *type, const char *description, const char *callout_info, key_serial_t keyring)
    用户态进程通过这个系统调用让内核查询一个密钥,并将其链接入参数指定的钥匙环。如果这个密钥已经存在,则这个系统调用的功能和keyctl(KEYCTL_SEARCH, …)几乎没有区别。如果这个密钥不存在,在这个系统调用中内核还要负责创建密钥。那么,密钥的pauload在哪里?内核的做法是根据参数callout_info启动一个用户态进程,由这个用户态进程来具体创建并实例化这个密钥。实际中的问题是内核启动的这个进程本身可能还是无法创建密钥,它又要启动别的进程来做这个工作。在这种重复委托的情况下,有两个东西必须以某种方式传递:一个是密钥,这个密钥已经在内核中创建了,需要用户态进程来实例化;另一个是钥匙环,就是这个密钥在成功实例化后,需要被链接到哪一个钥匙环中。还有一个额外的数据,就是进程的凭证,helper 1或者helper 2或者helper n进程要为process 1进程实例化密钥,这些helper可能要查找process 1的一些和密钥相关的数据。内核引入了一种新的密钥类型:key_type_request_key_auth。这种类型的密钥不是用来加密数据的,而是用来在进程间传递实例化一个密钥所需的信息。在内核创建进程helper 1的时候,先创建一个key_type_request_key_auth类型的密钥,链接入helper 1进程的会话钥匙环。key_type_request_key_auth类型的密钥的payload的子成员data本是void指针,其所指的实例的类型为request_key_auth。
struct request_key_auth
struct key *target_key;
struct key *dest_keyring;
const struct cred *cred;
void *callout_info;
size_t callout_len;
pid_t pid;

helper 1进程启动后就可以调用keyctl(KEYCTL_ASSUME_AUTHORITY, …),将这个key_type_request_key_auth类型的密钥置入自己的进程数据结构的request_key_auth。内核在做密钥相关的查找时,也会查找与request_key_auth中cred相关的密钥。

密钥访问类型

详情

Read

读出一个密钥的payload;读出一个钥匙环下所有链接的信息

Write

写入一个密钥的payload;在一个钥匙环中添加链接或删除链接

View

查看一个密钥的类型、描述等属性

Search

查找一个钥匙环,在搜索一个钥匙环时只会递归搜索其下有搜索访问权限的子钥匙环

Link

允许一个密钥或钥匙环被链接入一个钥匙环

Set Attribute

允许修改密钥的uid、gid、访问许可信息


以上是关于Linux内核安全模块学习-内核密钥管理子系统的主要内容,如果未能解决你的问题,请参考以下文章

Linux系统启动流程内核及模块管理

Linux 内核及系统启动流程

Linux内核系统体系概述

八Linux精简系统和内核管理裁剪

Linux 内核Linux 内核特性 ( 组织形式 | 进程调度 | 内核线程 | 多平台虚拟内存管理 | 虚拟文件系统 | 内核模块机制 | 定制系统调用 | 网络模块架构 )

Linux 系统启动流程