随着IP安全体系结构(IPsec,Internet Protocol Security)的引入,密钥加密和认证密钥的管理越来越需要一套标准机制。RFC 2367介绍了一个通用密钥管理API,可用于IPsec和其他网络安全服务,该API创建了一个新协议族,即PF_KEY域,这这个密钥管理域中,只支持原始套接字。
在大多系统上,常值AF_KEY被定义成与PF_KEY有相同的值,但RFC 2367明确密钥管理套接字必须用PF_KEY常值。
打开原始密钥管理套接字需要权限,在权限按需分割的系统上,对于打开密钥管理套接字这样的操作会有一个单独的权限,在普通Unix系统上,密钥管理套接字只有超级用户能打开。
IPsec基于安全关联(SA,Security Association)为分组提供安全服务。SA描述了源地址与目的地址(加上可选的传输协议和端口)、安全机制(如身份验证方式)、密钥素材的组合。一个流上可以有多个SA(如一个用于认证、一个用于加密)。存放在系统中的所有SA构成的集合称为安全关联数据库(SADB,Security Association DataBase)。
一个系统的SADB可能用于IPsec以外的场合,如OSPFv2、RIPv2(Routing Information Protoco v2,路由信息协议第二版)、RSVP(Resource ReserVation Protocol,资源预留协议)、Mobile-IP(或MIP,Mobile Internet Protocol,移动互联网协议)等在SADB中也可能有各自的表项。因此PF_KEY套接字不仅限IPsec使用。
IPsec还需要一个安全策略数据库(SPDB,Security Policy DataBase),SPDB描述分组流通的需求,例如主机A和主机B之间的分组流通必须通过IPsec AH(Authentication Header,IPsec认证头是一种用于确保数据包完整性和身份验证的协议)认证,未经认证的一律丢弃。SADB描述如何执行所需的安全步骤,如主机A和主机B之间的分组流通按照策略在使用IPsec AH,SADB就含有所用的算法和密钥。但SPDB没有标准的维护机制,尽管PF_KEY可以维护SADB,但对SPDB无能为力。KAME(一个网络协议栈的开源实现,KAME是日本词汇中代表"互连"或"相互"的一个词)的IPsec实现使用PF_KEY的扩展来维护SPDB,但它没有标准可循。
密钥管理套接字上支持3种操作:
1.通过写到密钥管理套接字,进程可以往内核和打开着密钥管理套接字的其他进程发消息。SADB表项的增加和删除也采用这种操作实现,如OSPFv2等自行进行安全检查的进程也采用这种方式从某个密钥管理守护进程请求密钥。
2.通过读密钥管理套接字,进程可以从内核或其他进程接收消息。内核可以采用这种操作请求某个密钥管理守护进程为依照策略来说需要受到保护的新TCP会话安装一个SA。
3.进程可以往内核发送一个倾泻(dumping)请求,内核作为应答倾泻出当前SADB,这是一个调试功能,并非所有系统都可用。
所有密钥管理套接字的消息都有同样的首部,每个消息后跟各种扩展,取决于可提供的或所请求的信息。所有相关结构都定义在头文件net/pfkeyv2.h中。每个消息、扩展都是64位对齐的,消息长度是8字节的整数倍。所有长度字段都是以64位为单位的,即长度字段的值为1代表8字节。如果某个扩展长度不足64位的整数倍,则必须填充到下一个64位边界,填充字节的具体值没有定义。以下是密钥管理消息首部:
密钥管理信息首部结构sadb_msg中的sadb_msg_type成员的值确定密钥管理消息类型,可选值如下:
每个sadb_msg首部结构后可跟零个或多个扩展,大多消息类型都有必需和可选的扩展,以下是所有扩展类型:
进程使用SADB_DUMP消息倾泻当前SADB,它是最简单的密钥管理消息,不需要任何扩展,单纯是一个16字节的sadb_msg首部。一个进程通过某个密钥管理套接字发送一个SADB_DUMP消息到内核后,内核通过同一个套接字响应一系列SADB_DUMP消息,每个消息都是一个16字节的sadb_msg首部,每个消息对应一个SADB表项,这个列表的末尾由sadb_msg结构中的sadb_msg_seq成员值为0来指示。
通过把请求SADB_DUMP消息的sadb_msg.sadb_msg_satype成员设为下图中的某个值,进程可限制返回的SA的类型,若该成员的值为SADB_SATYPE_UNSPEC常值,则SADB中所有SA都会返回:
并非所有系统都支持所有SA类型,KAME实现仅支持IPsec的两类SA(SADB_SATYPE_AH和SADB_SATYPE_ESP),因此如果试图倾泻SADB_SATYPE_RIPV2类型的SA,将返回EINVAL错误。如果SADB中没有所请求类型的SA,将返回ENOENT错误。
以下是用于倾泻SADB的程序:
void sadb_dump(int type) {
int s;
char buf[4096];
struct sadb_msg msg;
int goteof;
// 需要特定系统权限,因为它允许访问敏感的密钥素材
s = Socket(PF_KEY, SOCK_RAW, PF_KEY_V2);
/* Build and write SADB_DUMP request */
bzero(&msg, sizeof(msg));
// 调用socket时第三个参数为PF_KEY_V2,因此此处的版本字段sadb_msg_version也设为该值
msg.sadb_msg_version = PF_KEY_V2;
msg.sadb_msg_type = SADB_DUMP;
msg.sadb_msg_satype = type;
// sadb_msg_len的单位是8字节块
msg.sadb_msg_len = sizeof(msg) / 8;
// 将sadb_msg_pid成员设置为自己的进程id
// 从进程到内核的所有消息都要以发送者的PID来标识
msg.sadb_msg_pid = getpid();
printf("Sending dump message:\n");
// 使用我们的print_sadb_msg函数显示本消息
// 我们不给出该冗长无味的函数的源码,它以直观可读方式
// 显示正写到或已读自密钥管理套接字的某个消息
print_sadb_msg(&msg, sizeof(msg));
Write(s, &msg, sizeof(msg));
printf("\nMessages returned:\n");
/* Read and print SADB_DUMP replies until done */
goteof = 0;
while (goteof == 0) {
int msglen;
struct sadb_msg *msgp;
// 每次会读出一个内核响应的SADB_DUMP消息
msglen = Read(s, &buf, sizeof(buf));
msgp = (struct sadb_msg *)&buf;
print_sadb_msg(msgp, msglen);
if (msgp->sadb_msg_seq == 0) {
goteof = 1;
}
}
close(s);
}
int main(int argc, char **argv) {
int satype = SADB_SATYPE_UNSPEC;
int c;
opterr = 0; /* don't want getopt() writing to stderr */
// POSIX的getopt函数的第三个参数指定允许出现的命令行选项字符
// 本例中t字符后跟一个冒号,表示选项t需要一个参数
// 如果该参数为Oi:l:v,则表明程序接受4个选项
// i和l选项需要参数,而O和v选项不需要参数
// getopt函数与在unistd.h头文件中定义的以下4个全局变量协同工作
// extern char *optarg;
// extern int optind, opterr, optopt;
// 在调用getopt前把opterr设为0,表示发生命令行参数与第3个参数不匹配等错误时
// getopt函数不把出错消息写到标准错误,因为我们想自行处理错误
// POSIX声称第3个参数以“:”打头也能阻止函数写出到标准错误,但有些实现不支持
while ((c = getopt(argc, argv, "t:")) != -1) {
switch (c) {
case 't':
// 使用我们的getsatypebyname函数从文本串得到类型值
// 用于指定倾泻时指定的SA类型
if ((satype = getsatypebyname(optarg)) == -1) {
err_quit("invalid -t option %s", optarg);
}
break;
default:
err_quit("unrecognized option: %c", c);
}
}
sadb_dump(satype);
}
在一个具有2个静态SA(预配置的、静态定义的安全关联)的系统上运行本倾泻程序的输出:
向SADB增加一个SA最直接的方法是手动填写所有参数并发送一个SADB_ADD消息。虽然手动指定密钥素材会导致不易更改密钥(易于更改密钥对避免密码分析攻击很重要),但配置起来很容易(不涉及实际的密钥协商过程)。
SADB_ADD消息必需的扩展有3种:SA、地址、密钥,可选的扩展也有3种:生命期、身份、敏感性。
SA扩展由sadb_sa结构描述:
sadb_sa_spi成员含有安全参数索引(SPI,Security Parameters Index),SPI、目的地址、所用协议(如IPsec AH)三者唯一标识一个SA。接收分组时,SPI用于查找与该数据包相关的SA;发送分组时,SPI插入到分组中供对端使用。SPI没有别的含义,因此其值可以顺序地或随机地分配,也可使用目的系统想使用的方法进行分配。sadb_sa_replay成员指定反重放窗口的大小。sadb_sa_state成员值在动态创建的SA的生命周期内会发生变化,可取值如下:
手动创建的SA总是处于SADB_SASTATE_MATURE状态。
sadb_sa_auth成员和sadb_sa_encrypt成员本别指定本SA的认证算法(用于验证通信双方的身份)和加密算法(用于保护数据的机密性),可取值如下:
sadb_sa_flags成员目前只定义了一个标志,即SADB_SAFLAGS_PFS,该标志要求完备前向安全(PFS,Perfect Forward Security),即密钥的值不依赖于先前的密钥或某个主密钥,PFS确保即使长期的私钥(在非对称加密系统中使用)泄露,先前的通信内容也无法被解密,在传统的加密协议中,使用的是长期有效的密钥(更通用的值,不仅限于私钥),如果这些密钥被泄露,攻击者可以将其用于解密先前截获的通信数据,而PFS使用的是临时生成的一次性密钥,这些密钥仅用于加密和解密单个会话或通信。该标志值用于从密钥管理守护进程请求密钥的场合,增加静态SA时不用(因为静态创建SA时,密钥在其创建时就已确定)。
除了以上SA扩展外,SADB_ADD消息的另一种必需的扩展是地址扩展。由常值SADB_EXT_ADDRESS_SRC和SADB_EXT_ADDRESS_DST指定的是源地址和目的地址,这两个地址是必需的;而常值SADB_EXT_ADDRESS_PROXY指定的是代理地址,代理地址是可选的。代理地址详细信息可见RFC 2367。地址扩展使用以下sadb_address结构:
sadb_address结构中的sadb_address_exttype成员确定本地址是源地址、目的地址还是代理地址。sadb_address_proto成员指定本SA匹配的协议,若为0则匹配所有协议。sadb_address_prefixlen成员给出sadb_address结构表示的地址的有效位数(如IPv4是32位),这样单个SA可以匹配多个地址。sadb_address结构后跟匹配地址族的sockaddr结构(如sockaddr_in或sockaddr_in6)。sockaddr中的端口仅在sadb_address_proto指定的协议支持端口号的前提下(如IPPROTO_TCP)才有效。sadb_address_reserved成员是保留字段,通常设为0。
SADB_ADD消息的最后一种必需的扩展是认证和加密密钥,分别由SADB_EXT_KEY_AUTH和SADB_EXT_KEY_ENCRYPT指定,由sadb_key结构描述:
sadb_key_exttype成员定义本密钥是认证密钥还是加密密钥。sadb_key_bits成员指定本密钥的位数。密钥本身紧跟在sadb_key结构后。
增加一个静态SADB表项的代码:
void sadb_add(struct sockaddr *src, struct sockaddr *dst, int type, int alg,
int spi, int keybits, unsigned char *keydata) {
int s;
char buf[4096], *p; /* XXX */
struct sadb_msg *msg;
struct sadb_sa *saext;
struct sadb_address *addrext;
struct sadb_key *keyext;
int len;
int mypid;
s = Socket(PF_KEY, SOCK_RAW, PF_KEY_V2);
mypid = getpid();
/* Build and write SADB_ADD request */
// 构造SADB_ADD消息首部
bzero(&buf, sizeof(buf));
p = buf;
msg = (struct sadb_msg *)p;
msg->sadb_msg_version = PF_KEY_V2;
msg->sadb_msg_type = SADB_ADD;
msg->sadb_msg_satype = type;
msg->sadb_msg_pid = getpid();
len = sizeof(*msg);
p += sizeof(*msg);
// 添加必需的SA扩展
saext = (struct sadb_sa *)p;
saext->sadb_sa_len = sizeof(*saext) / 8;
saext->sadb_sa_exttype = SADB_EXT_SA;
saext->sadb_sa_spi = htonl(spi); // 必须以网络字节序存放
// 关闭重放保护
saext->sadb_sa_replay = 0; /* no replay protection with static keys */
saext->sadb_sa_state = SADB_SASTATE_MATURE;
// 设置认证算法
saext->sadb_sa_auth = alg;
// 设置加密算法
saext->sadb_sa_encrypt = SADB_EALG_NONE;
saext->sadb_sa_flags = 0;
len += saext->sadb_sa_len * 8;
p += saext->sadb_sa_len * 8;
// 将源地址以SADB_EXT_ADDRESS_SRC扩展形式添加到本消息
addrext = (struct sadb_address *)p;
// 长度字段先加7再除8,是按64位边界填充后的长度
addrext->sadb_address_len = (sizeof(*addrext) + salen(src) + 7) / 8;
addrext->sadb_address_exttype = SADB_EXT_ADDRESS_SRC;
// 本SA适用于所有协议
addrest->sadb_address_proty = 0; /* any protocol */
// 设置匹配地址时的前缀长度,此处将IPv4设为32位,IPv6设为128位,表示完全匹配整个IP地址
addrext->sadb_address_prefixlen = prefix_all(src);
addrext->sadb_address_reserved = 0;
// 将源地址存放在地址扩展后面
memcpy(addrext + 1, src, salen(src));
len += addrext->sadb_address_len * 8;
p += addrext->sadb_address_len * 8;
// 与源地址一样的方式将目的地址加入本消息
// 但sadb_address_exttype为SADB_EXT_ADDRESS_DST
addrext = (struct sadb_address *)p;
addrext->sadb_address_len = (sizeof(*addrext) + salen(dst) + 7) / 8;
addrext->sadb_address_exttype = SADB_EXT_ADDRESS_DST;
addrext->sadb_address_proto = 0; /* any protocol */
addrext->sadb_address_prefixlen = prefix_all(dst);
addrext->sadb_address_reserved = 0;
memcpy(addrext + 1, dst, salen(dst));
len += addrext->sadb_address_len * 8;
p += addrext->sadb_address_len * 8;
// 添加认证密钥
keyext = (struct sadb_key *)p;
/* "+7" handles alignment requirements */
// 此处的keybits是以位为单位的,除8将其转换为字节
// 假如keybits/8是4.5,而sizeof(*keyext)为4,则结果会是8,而非16,keybits为8位的倍数才不会出现此问题
keyext->sadb_key_len = (sizeof(*keyext) + (keybits / 8) + 7) / 8;
keyext->sadb_key_exttype = SADB_EXT_KEY_AUTH;
keyext->sadb_key_bits = keybits;
keyext->sadb_key_reserved = 0;
// 把密钥数据复制到本扩展首部后面
memcpy(keyext + 1, keydata, keybits / 8);
len += keyext->sadb_key_len * 8;
p += keyext->sadb_ley_len * 8;
msg->sadb_msg_len = len / 8;
printf("Sending add message:\n");
print_sadb_msg(buf, len);
Write(s, buf, len);
printf("\nReply returned:\n");
/* Read and print SADB_ADD reply, discarding any others */
for (; ; ) {
int msglen;
struct sadb_msg *msgp;
msglen = Read(s, &buf, sizeof(buf));
msgp = (struct sadb_msg *)&buf;
// 寻找pid与本进程一致的消息
if (msgp->sadb_msg_pid == mypid && msgp->sadb_msg_type == SADB_ADD) {
print_sadb_msg(msgp, msglen);
break;
}
}
close(s);
}
运行以上程序,发送SADB_ADD消息为127.0.0.1和127.0.0.1之间的分组流通增设一个SA:
上图中,应答消息中没有给出密钥内容,这是因为应答消息被发送到所有PF_KEY套接字,但不同的套接字可能属于不同的保护域(保护域是一种安全机制,用于隔离和控制不同进程或实体之间的访问权限,每个保护域都有自己的访问规则和权限设置),密钥数据不应该跨越保护域(这是为了防止密钥数据被未经授权的实体或进程访问、修改或泄露)。把这个SA添加到SADB后,我们对127.0.0.1执行ping命令使该SA真正被使用,然后倾泻出SADB以检查所添加的SA:
从倾泻的结果可见,内核把我们的IP协议从0改为了255,这是本实现的一个特性,而非PF_KEY套接字的普遍特性。此外内核把前缀长度由32改为了128(另一个缺陷),它看起来是由内核混淆IPv4和IPv6地址所引起。内核还返回了另一个我们的倾泻程序不认识的扩展(编号19),不认识的扩展我们的倾泻程序会根据它的长度字段跳过。内核还返回了生命期扩展,含有本SA的当前生命期信息,生命期扩展相关的结构如下:
生命期扩展有3种,SADB_LIFETIME_SOFT和SADB_LIFETIME_HARD这两个扩展分别指定一个SA的软生命期和硬生命期。当软生命期结束时,内核发送一个SADB_EXPIRE消息;当硬生命期结束后,该SA不能再用。最后一种生命期扩展是SADB_LIFETIME_CURRENT扩展,它用于指出相应SA的当前生命期,它会在SADB_DUMP、SADB_EXPIRE、SADB_GET消息中返回。
周期性地重新产生密钥(动态维护安全关联)有助于进一步提高安全性,这种操作通常由诸如IKE(Internet Key Exchange Protocol,互联网密钥交换协议)之类的协议执行。
为了获悉何时需要为一对主机提供新的SA,密钥管理守护进程应预先用SADB_REGISTER请求消息向内核注册自身,其中的sadb_msg_satype成员指出守护进程能处理的SA类型。如果守护进程能处理多种SA类型,它就为其中每个类型发送一个SADB_REGISTER请求消息。在SADB_REGISTER应答消息中,内核提供一系列受支持算法扩展,用来指出哪些加密和认证机制、哪些密钥长度得到支持。受支持算法扩展由sadb_supported结构描述:
紧跟在每个sadb_supported结构(相当于该扩展的首部)后的是一系列以sadb_alg结构给出的加密或认证算法的描述。
sadb_supported扩展首部后的每个sadb_alg结构代表系统支持的一个算法,下图是为处理SA类型SADB_SATYPE_ESP而发出的SADB_REGISTER请求的一个可能应答:
以下程序使用SADB_REGISTER请求向内核注册自身进程,然后显示内核在应答中返回的受支持算法列表:
void sadb_register(int type) {
int s;
char buf[4096]; /* XXX */
struct sadb_msg msg;
int goteof;
int mypid;
s = Socket(PF_KEY, SOCK_RAW, PF_KEY_V2);
mypid = getpid();
/* Build and write SADB_REGISTER request */
bzero(&msg, sizeof(msg));
msg.sadb_msg_version = PF_KEY_V2;
msg.sadb_msg_type = SADB_REGISTER;
msg.sadb_msg_satype = type;
msg.sadb_msg_len = sizeof(msg) / 8;
msg.sadb_msg_pid = mypid;
printf("Sending register message:\n");
print_sadb_msg(&msg, sizeof(msg));
Write(s, &msg, sizeof(msg));
// SADB_REGISTER请求消息不需要任何扩展
printf("\nReply returned:\n");
/* Read and print SADB_REGISTER reply, discarding any others */
for (; ; ) {
int msglen;
struct sadb_msg *msgp;
msglen = Read(s, &buf, sizeof(buf));
msgp = (struct sadb_msg *)&buf;
// 应答消息中的pid是本进程的pid,且类型为SADB_REGISTER
if (msgp->sadb_msg_pid == mypid && msgp->sadb_msg_type == SADB_REGISTER) {
print_sadb_msg(msgp, msglen);
break;
}
}
close(s);
}
在一个不仅仅支持RFC 2367中规定协议的系统上运行以上register程序:
当内核需要与某个目的地址通信时,如果根据策略该单向分组流必须经由一个SA而内核没有可用SA时,内核就向注册了所需SA类型的密钥管理套接字发送一个SADB_ACQUIRE消息,其中含有一个描述内核所提议算法及密钥长度的提议扩展,该提议可能综合了系统支持的配置与限制该单向分组流的预配置策略。提议内容是一个由算法、密钥长度、生命期所构成的按照优先顺序排列的列表。当一个密钥管理守护进程收到一个SADB_ACQUIRE消息后,它执行必要的操作以选择一个内核提议的密钥,再把该密钥安装到内核中。密钥管理守护进程使用SADB_GETSPI消息请求内核从一个期望的范围内选择一个SPI,内核对于该SADB_GETSPI消息的响应还包括建立一个处于larval(幼虫)状态的SA,然后守护进程使用由内核提供的SPI与远端协商安全参数,接着使用SADB_UPDAE更新该SA,使它进入mature(成熟)状态。动态创建的SA通常含有关联的软生命期和硬生命期,任何一个生命期结束时,内核将发送一个SADB_EXPIRE消息,其中指出期满的是软生命期还是硬生命期,如果软生命期结束,SA就进入dying(垂死)状态,期间它仍可使用,但内核应该为它获取一个新SA;如果硬生命期结束,此SA就进入dead(死亡)状态,这种状态的SA不能继续使用,必须从SADB中删除。
密钥管理套接字用于在内核、密钥管理守护进程、路由守护进程等安全相关的消费进程间交换SA。SA既可以手工静态安装,也可以使用密钥协商协议自动动态安装。动态密钥有关联的生命期,当软生命期结束时,密钥守护进程得到通知,这样的SA如果在硬生命期结束前未被新SA替换,那就不能再使用。
进程和内核通过密钥管理套接字交换的信息共有10种类型,每种消息类型都有关联的扩展,有的扩展是必需的,有的是可选的。每个由进程发送的消息的应答被内核发送到所有打开着的密钥管理套接字,但其中含有敏感数据的扩展都会被抹除。