Chinaunix首页 | 论坛 | 博客
  • 博客访问: 149677
  • 博文数量: 51
  • 博客积分: 1545
  • 博客等级: 上尉
  • 技术积分: 600
  • 用 户 组: 普通用户
  • 注册时间: 2006-08-31 10:20
文章分类

全部博文(51)

文章存档

2011年(3)

2010年(48)

我的朋友

分类:

2010-01-20 10:51:01

学习使用可插拔验证模块(Pluggable Authentication Modules,PAM)进行用户身份验证,以实现身份验证机制与应用程序的分离。

内容
简介

在 ,我们讨论了基于口令的用户验证的基础知识。我们给出了身份验证授权 这两个术语的定义, 讨论了基于本地文件的口令储存和加密算法,并介绍了 Solaris 操作系统提供的读取和加密口令的 API 函数。最后,我们通过一个示例程序展示了要求用户输入口令和比较口令的过程。

Solaris 操作系统为用户验证提供了一个可扩展的机制,这就是可插拔验证模块框架。在本文中,我们给出了 PAM 的概述并开发一个基于 PAM 的程序,该程序在本系列的第1篇文章中有详细的描述。尽管在应用程序中使用 PAM 进行用户验证会增加复杂性,但是 PAM 能有效的实现验证机制和应用程序的分离。这也就意味着,无需修改应用程序就能加入新的验证机制(例如,生物特征扫描)或修改现有的验证机制。

PAM概述

PAM 的结构主要由两部分组成:PAM 库,它提供了 API 和 SPI( 服务提供者接口 ),和各种可堆叠的服务模块。配置文件 /etc/pam.conf 是用于配置各种系统服务的服务模块,例如 logincron。应用程序通过 API 与 PAM 进行通信,而服务模块则使用的是 SPI。

PAM服务模块

PAM 服务模块 是一个共享库,它提供了一个或多个以下的验证和安全服务:

  • 验证服务模块。用于验证用户访问帐户或服务的权限,并建立用户凭证。
  • 帐户管理模块。用于确定当前用户帐号的有效性。例如,该模块能够检查口令或帐户的有效期以及规定的访问时间限制。
  • 会话管理模块。用于设置和终止登录会话。
  • 口令管理模块。用于强制实施口令强度规则,并执行验证令牌的更新。

PAM 模块可以实现其中的一项或多项服务。将简单模块用于明确定义的任务中可以增加配置灵活性。因此,应该在不同的模块中实现 PAM 服务。然后,可以按照 /etc/pam.conf文件中定义的方式根据需要使用这些服务。

PAM 配置文件

我们上面说的 PAM 结构是通过配置文件 /etc/pam.conf来配置的,系统管理员可以管理该文件。在 pam.conf 中每个服务系统可以有一个或多个条目。在 pam.conf 中这些条目的顺序是十分重要的,因为错误的配置 pam.conf 文件会导致在多用户的模式下所有系统用户的死锁。系统服务不能用自己的集合项来使用 "其他" 服务。

该配置文件中的条目将采用以下格式,其中前面四个是必须的。

  • Service name。服务名称,例如 cronloginpasswd。应用程序可以针对其提供的服务使用不同的服务名。服务名 other 是用作通配符服务名的预定义名称。如果在配置文件中未找到特定的服务名,则会使用 other 配置。 例如,Solaris 安全 shell 守护进程 sshd 并用 sshd 做为预验证方法的关键字。(更详细的内容,请参考 sshd 手册的 SECURITY 一节)
  • Module type。 服务模块类型,即 accountauthpasswordsessionaccount 服务模块用于验证账户的有效性(例如,口令,账户的有效期,访问时间限制),auth 服务模块用于验证账户的访问和建立用户凭证,password服务模块用于管理用户口令的修改,session 服务模块用于建立和取消登录会话。
  • Control flag。 控制标志。用于指明在确定服务的集成成败值的过程中模块所起的作用。有效的控制标志包括 bindingoptionalrequiredrequisitesufficient。我们将在下一个部分中将详细的讨论这些内容。
  • Module path。 用于实现服务的库对象的路径。如果路径名不是绝对路径名,则假设路径名相对于 /usr/lib/security/$ISA,通过使用与体系结构有关的宏 $ISA ,可查看应用程序特定体系结构的目录。
  • Module options。传递给服务模块的选项。模块的手册页介绍了相应模块可接受的选项。
PAM 堆栈模块

上面提到过应用程序能够调用多个服务模块,并且每个模块的条目都在配置文件 /etc/pam.conf 中。应用程序调用以下函数时,PAM 库将读取配置文件 pam.conf 以确定针对该服务参与操作的模块:

pam_authenticate
pam_acct_mgmt
pam_setcred
pam_open_session
pam_close_session
pam_chauthtok

如果服务在配置文件 pam.conf 中只有一个条目,那么模块的结果决定了用了该种服务的操作的结果。然而,如果服务在配置文件pam.conf 中定义了多个项,那么模块将采用堆栈的形式,操作的结果取决于集成的处理,该处理考虑服务 PAM 栈中所有模块的结果(除非模块被过早的被终止了)。

对于一个给定的服务,每个出现在配置文件 pam.conf 中的模块都将顺序的执行。因为控制标识决定了模块的值,所以无论成功或失败模块都被集成到了整个的操作结果中:

  • binding。如果前面所需的模块都没有失败,则成功满足绑定模块的要求后,会向应用程序立即返回成功信息。如果满足这些条件,则不会进一步执行模块。如果失败,则会记录 "所需的失败" 信息并继续处理模块。
  • optional。不必成功满足可选模块"的要求即可使用服务。如果失败,则会记录可选的失败信息。
  • required。必须成功满足必需模块的要求才能使用服务。如果失败,,则执行该服务的其余模块后会返回a "错误"信息.仅当绑定模块或所需的模块没有报告失败的情况下,才会返回该服务最终成功的信息。
  • requisite。必须成功满足必备模块的要求才能使用服务。如果失败,则会立即返回 "错误",而不会进一步执行模块。只有一个服务的所有必备模块都返回成功,函数才能向应用程序返回成功。
  • sufficient。如果该模块失败,将标记为 "选择失败"。如果前面所需模块的要求都成功满足,则成功满足控制标志为 suffcient 的模块的要求后将向应用程序立即返回成功,而不会进一步执行模块。如果失败,则会记录可选的失败信息。

前面的表述让人费解,因此让我们来看配置文件pam.conf, 的一个示例,该示例讲述的是rlogin服务。下面是它在文件 pam.conf中的条目:

rlogin  auth sufficient         pam_rhosts_auth.so.1	
rlogin auth requisite pam_authtok_get.so.1
rlogin auth required pam_dhkeys.so.1
rlogin auth required pam_unix_cred.so.1
rlogin auth required pam_unix_auth.so.1

rlogin 服务请求验证的时候,将首先执行(pam_rhosts_auth)模块。对于该模块,控制标志将设置为sufficient。如果pam_rhosts_auth 模块可以验证用户,则处理停止并且会向应用程序返回成功信息。

如果 pam_rhosts_auth 模块无法验证用户,则执行下一个模块(pam_authtok_get) 。此模块的控制标志将设置为 requisite。如果 pam_authtok_get失败,则验证过程将结束并向应用程序返回失败信息。如果pam_authtok_get成功,则将执行接下来的三个模块(pam_dhkeyspam_unix_credpam_unix_auth) 。这三个模块的关联控制标志都会设置为 required,这也意味着,如果应用程序返回了成功信息这三个模块都必须顺序的返回验证成功信息。然而,只要有其中的一个模块失败,失败就会被记录并且只有三个模块执行完毕之后该记录才会返回给应用程序。

此时,最后一个模块 pam_unix_auth 已经执行完毕,成功或失败的信息会返回给应用程序,如果失败则会拒绝用户通过rlogin进行访问。

一些 API 函数

既然我们已经描述了 PAM 框架和它的配置,接着让我们来关注下一些 PAM 的 API函数。对于一个非常简单的 PAM 应用程序,有3个需要考虑的函数: pam_startpam_endpam_authenticate

pam_start 函数

pam_start 函数用于初始化一个 PAM 验证事务。

#include 
int pam_start (const char service, const char user,
const struct pam_conv pam_conv, pam_handle_t pamh);

这个函数初始化一个服务和用户验证事务,服务和用户分别由 serviceuser 表示。 参数 pam_conv 指定了将使用的会话函数。一旦成功 pamh 就指向一个句柄,该句柄必须由随后的 PAM 函数使用,与文件描述符用于文件的方式类似。我们在本系列的下一个部分讨论会话函数,但是顾名思义,会话函数是用于处理与用户的会话(例如,提示用户输入口令)。

pam_end 函数

The pam_end 函数用于终止一个 PAM 验证事务

#include 

int pam_end (pam_handle_t *pamh, int status);

pamh 表示的 PAM 验证事务被终止,并且参数 status 被传递给存储在PAM句柄的清理函数(如果有的话)。

pam_authenticate 函数

pam_authenticate 函数用于验证当前的用户

#include 

int pam_authenticate (pam_handle_t *pamh, int flags);

该函数用于验证当前的用户。用户通过 pam_start 调用创建句柄 pamh 时指定了该函数。用户验证的过程是与模块相关的,但是验证通常涉及询问用户的口令,提示用户输入以前的口令或令牌,或者验证用户身份的其他一些方法。其他的验证机制包括生物特征扫描和智能卡。参数 flags 可用于设置各种验证服务的标志。

一个 PAM 应用程序的例子

我们已经描述了足够的资料,现在来看一个使用 PAM 验证方式的样例应用程序。和以前的文章一样,该示例将不停地询问当前用户(由这个过程中的真实用户 ID 决定)的口令直到输入了正确的口令,如何正确口令则结束程序。下面是程序的源代码。

 1 #include 
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8 extern int check_conv (int num_msg, struct pam_message **msg,
9 struct pam_response *resp, void *app_data);
10 int main (void)
11 {
12 struct passwd *pwd_info;
13 struct pam_conv conv = {check_conv, NULL};
14 pam_handle_t *ph;
15 int error;
16 if ((pwd_info = getpwuid (getuid ())) == NULL) {
17 fprintf (stderr, "Call to getpwuid failed\n");
18 exit (1);
19 }
20 if ((error = pam_start ("check_pass", pwd_info -> pw_name,
21 &conv, &ph)) != PAM_SUCCESS) {
22 fprintf (stderr, "Call to pam_start failed: %s\n",
23 pam_strerror (ph, error));
24 exit (1);
25 }
26 for (;;) {
27 error = pam_authenticate (ph, 0);
28 if (error == PAM_SUCCESS) {
29 printf ("Passwords match.\n");
30 break;
31 } else {
32 printf ("Passwords don't match.\n");
33 }
34 }
35 pam_end (ph, 0);
36 return (0);
37 }

我们来仔细察看这段共有 37 行的代码。

1-9: 包括头文件和声明会话函数。尽管在本文中没有描述会话函数,为了那些想现在就进行体验的读者,我们给出了这个例子中使用的函数。

16-19: 调用 getpwuid 以获得由这个过程中的真实用户 ID 标识的当前用户的口令文件信息。我们可以转而使用 getlogincuserid。但是,有安全意识的程序应该避免使用这些函数,因为它们依赖并且信任 /var/adm/utmpx 的内容。 在一些UNIX平台上,尽管不是在 Solaris 平台上,这些文件是所有人可写的,因此一个不怀好意的用户可能改变文件中针对某个授权用户的项, 而我们的程序并不知晓。我们这样做是因为在下一步我们必须将用户的用户名传递给 pam_start

20-25: 调用 pam_start 初始化 PAM 会话,使用 "check_pass" 作为服务名和确定的用户名。 注意,由于正在使用的服务名在默认的路径 /etc/pam.conf, 下不存在,因此将使用other 服务的验证条目。如果有某个错误发生,通知用户并且退出。

26-34: 在无限循环中,使用调用 pam_start 时建立的会话函数调用 pam_authenticate 来验证用户的身份。简单地说,正是这个会话函数提示用户输入口令和从用户那里读取口令。 如果 pam_authenticate返回 PAM_SUCCESS,则打印一条消息通知口令匹配并且退出该循环。 否则,打印一条失败消息然后重试。

35: 调用 pam_end 终止 PAM 事务。

下面是会话函数的源代码。(我们将在本系列的下一篇文章中详细的讨论它们。)

 1 #include 
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8 int check_conv (int num_msg, struct pam_message **msg,
9 struct pam_response **resp, void *app_data)
10 {
11 struct pam_message *m = *msg;
12 struct pam_response *r;
13 int i;
14 char *ct_passwd;
15 if ((num_msg <= 0) || (num_msg >= PAM_MAX_NUM_MSG)) {
16 fprintf (stderr, "Invalid number of messages\n");
17 *resp = NULL;
18 return (PAM_CONV_ERR);
19 }
20 if ((*resp = r = calloc (num_msg, sizeof (struct pam_response))) == NULL)
21 return (PAM_BUF_ERR);
22 for (i = 0; i < num_msg; i++) {
23 switch (m->msg_style) {
24 case PAM_PROMPT_ECHO_OFF:
25 ct_passwd = getpassphrase ("Enter password: ");
26 r->resp = strdup (ct_passwd);
27 m++;
28 r++;
29 break
30 case PAM_PROMPT_ECHO_ON:
31 if (m->msg)
32 fputs (m->msg, stdout);
33 r->resp = NULL;
34 m++;
35 r++;
36 break;
37 case PAM_ERROR_MSG:
38 if (m->msg)
39 fprintf (stderr, "%s\n", m->msg);
40 m++;
41 r++;
42 break;
43 case PAM_TEXT_INFO:
44 if (m->msg)
45 printf ("%s\n", m->msg);
46 m++;
47 r++;
48 break;
49 }
50 return (PAM_SUCCESS);
51 }
52 }

在编译这个程序之后,运行并看看会发生些什么。

rich@marrakesh4112# make pam_check_pass
cc -o pam_check_pass -lpam pam_check_pass.c pam_check_pass_conv.c
pam_check_pass.c:
pam_check_pass_conv.c:
rich@marrakesh4113# ./pam_check_pass
Enter password:
Passwords don't match.
Enter password:
Passwords don't match.

注意,不管我们输入什么口令,应用程序都提示口令不匹配。这是因为用户 "rich" 没有适当的权限。在后台,我们需要读取影子文件,以比较输入口令是不正确。这是因为 "rich" 是一个本地用户,并且只有根用户可以读取影子文件 (/etc/shadow)。我们切换为根用户并重试。

rich@marrakesh4114# su
Password:
# ./pam_check_pass
Enter password:
Passwords don't match.
Enter password:
Passwords match.

这次,在第一次输入一个错误口令后,我们成功了。注意,使用 PAM 时并不能使用最低权限这个选项。因为要求使用全域权限。

和以前一样,可以很容易将该示例修改为一个简单的终端锁定程序。同样的,这也是留给读者的一个练习

结束语

在本文开头,我们简要介绍 PAM 的概况,然后讨论了 PAM 框架的各个部分。我们围绕 PAM 服务模块、PAM 配置文件 /etc/pam.conf,以及如何开发服务模块栈展开了探讨。

然后,我们描述了PAM API 中的一些重要函数:pam_start, pam_end, 和 pam_authenticate。最后,我们编写了一个示例程序将这些都联系起来:支持 PAM 的口令比对程序,该程序在本系列中的第一篇文章中有详细的描述。

在本系列的下一篇文章中,我们将介绍如何编写一个 PAM 会话程序,并了解一些其它的 API 函数。

阅读(1047) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~