第 3 章 编写 PAM 应用程序和服务

可插拔验证模块 (Pluggable authentication module, PAM) 为系统登录应用程序提供了验证和相关的安全服务。本章适用于希望通过 PAM 模块提供验证、帐户管理、会话管理和口令管理的系统登录应用程序开发者。此外,还为 PAM 服务模块的设计者提供了相关的信息。本章将讨论以下主题:
PAM 最初是由 Sun 开发的。自此以后 PAM 规范提交给了 X/Open,即现在的 "Open Group"。PAM 规范可在《 X/Open Single Sign-On Service (XSSO) - Pluggable Authentication》(Open Group 在英国出版,ISBN:1-85912-144-6,1997 年 6 月)中找到。 pam(3PAM)libpam(3LIB) 和 pam_sm(3PAM) 手册页中介绍了 Solaris 实现的 PAM。

PAM 框架介绍

PAM 框架由四个部分组成:
  • PAM 消费方
  • PAM 库
  • pam.conf(4) 配置文件
  • PAM 服务模块,也称为提供者
该框架可为与验证相关的活动提供统一的执行方式。采用该方式,应用程序开发者可使用 PAM 服务,而不必了解策略的语义。算法是集中提供的。可以独立于各个应用程序对算法进行修改。借助 PAM,管理员可以根据特定系统的需要调整验证过程,而不必更改任何应用程序。调整是通过 PAM 配置文件  pam.conf 来执行的。
下图说明了 PAM 体系结构。应用程序通过 PAM 应用编程接口 (application programming interface, API) 与 PAM 库进行通信。PAM 模块通过 PAM 服务提供者接口 (service provider interface, SPI) 与 PAM 库进行通信。通过这种方式,PAM 库可使应用程序和模块相互进行通信。
图 3–1 PAM 体系结构

图中显示了通过应用程序和

PAM 服务模块

PAM 服务模块是一个共享库,用于为系统登录应用程序(如  loginrlogin 和  telnet)提供验证和其他安全服务。四种类型的 PAM 服务是:
  • 验证服务模块-用于授予用户访问帐户或服务的权限。提供此服务的模块可以验证用户并设置用户凭证。
  • 帐户管理模块-用于确定当前用户的帐户是否有效。提供此服务的模块可以检查口令或帐户的失效期以及限时访问。
  • 会话管理模块-用于设置和终止登录会话。
  • 口令管理模块-用于强制实施口令强度规则并执行验证令牌更新。
一个 PAM 模块可以实现其中的一项或多项服务。将简单模块用于明确定义的任务中可以增加配置灵活性。因此,应该在不同的模块中实现 PAM 服务。然后,可以按照  pam.conf(4) 文件中定义的方式根据需要使用这些服务。
例如,Solaris OS 为系统管理员提供了用于配置站点口令策略的  pam_authtok_check(5) 模块。 pam_authtok_check(5) 模块可以检查符合各种强度条件的建议口令。
有关 Solaris PAM 模块的完整列表,请参见手册页第 5 节: Standards, Environments, and Macros。PAM 模块的前缀为  pam_

PAM 库

PAM 库  libpam(3LIB) 是 PAM 体系结构中的中心元素:
  • libpam 可以导出 API  pam(3PAM)。应用程序可以调用此 API 以执行验证、帐户管理、凭证建立、会话管理以及口令更改。
  • libpam 可以导入主配置文件  pam.conf(4)。PAM 配置文件可指定每种可用服务的 PAM 模块要求。 pam.conf 由系统管理员进行管理。
  • libpam 可以导入 SPI  pam_sm(3PAM),而导出则由服务模块完成。

PAM 验证过程

以消费方如何使用 PAM 库进行用户验证为例,请考虑  login 如何验证用户:
  1. login 应用程序通过调用  pam_start(3PAM) 并指定  login 服务来启动 PAM 会话。
  2. 该应用程序将调用  pam_authenticate(3PAM),后者是 PAM 库  libpam(3LIB) 导出的 PAM API 的一部分。
  3. 该库将在  pam.conf 文件中搜索  login 项。
  4. 对于  pam.conf 中为  login 服务配置的每个模块,PAM 库将调用  pam_sm_authenticate(3PAM)pam_sm_authenticate() 函数是 PAM SPI 的一部分。 pam.conf 控制标志和每个调用的结果将确定是否允许用户访问系统。 PAM 堆栈工作原理对此过程进行了更详细的介绍。
通过此方式,PAM 库可以将 PAM 应用程序与系统管理员已配置的 PAM 模块连接起来。

PAM 消费方的要求

PAM 消费方必须与 PAM 库  libpam 链接。应用程序使用模块提供的任何服务之前,必须通过调用  pam_start(3PAM) 初始化其 PAM 库的实例。调用  pam_start() 可初始化必须传递给所有后续 PAM 调用的句柄。应用程序完成使用 PAM 服务后,系统将调用  pam_end() 以清除 PAM 库已使用的任何数据。
PAM 应用程序与 PAM 模块之间的通信是通过 进行的。例如,以下各项有助于进行初始化:
  • PAM_USER-当前验证的用户
  • PAM_AUTHTOK-口令
  • PAM_USER_NAME-用户名提示
  • PAM_TTY-用户借此进行通信的终端
  • PAM_RHOST-用户借此进入系统的远程主机
  • PAM_REPOSITORY-对用户帐户系统信息库的任何限制
  • PAM_RESOURCE-对资源的任何控制
有关可用项的完整列表,请参见  pam_set_item(3PAM)。应用程序可以通过 pam_set_item(3PAM) 对项进行设置。应用程序可以通过 pam_get_item(3PAM) 检索模块已设置的值。但是,应用程序不能检索 PAM_AUTHTOK 和 PAM_OLDAUTHTOK。无法设置 PAM_SERVICE 项。

PAM 配置

PAM 配置文件  pam.conf(4) 用于为系统服务(如  loginrloginsu 和  cron)配置 PAM 服务模块。系统管理员可以管理此文件。如果 pam.conf 中的项顺序有误,则会导致无法预料的负面影响。例如,如果  pam.conf 配置错误,则会将多个用户锁定在外,这样必须采用单用户模式才能进行修复。有关设置顺序的说明,请参见 PAM 堆栈工作原理。有关系统管理员所管理的 PAM 配置的信息,请参见 《系统管理指南:安全性服务》中的“PAM 配置文件(参考)”

PAM 配置文件语法

该配置文件中的项采用以下格式:
service-name module-type control-flag module-path module-options
service-name
服务的名称,例如  ftplogin 或  passwd。应用程序可以针对其提供的服务使用不同的服务名。例如,Solaris 安全 shell 守护进程使用以下服务名: sshd-nonesshd-passwordsshd-kbdintsshd-pubkey 以及  sshd-hostbased。服务名  other 是用作通配符服务名的预定义名称。如果在配置文件中未找到特定的服务名,则会使用  other 的配置。
module-type
服务类型,即  authaccountsession 或  password
control-flag
控制标志。用于指明在确定服务的集成成败值的过程中模块所起的作用。有效的控制标志包括 bindingoptionalrequiredrequisite 和  sufficient。有关这些标志用法的信息,请参见 PAM 堆栈工作原理
module-path
用于实现服务的库对象的路径。如果路径名不是绝对路径名,则假设路径名相对于  /usr/lib/security/$ISA/。 通过使用与体系结构有关的宏 $ISA, libpam 可查看应用程序特定体系结构的目录。
module-options
传递给服务模块的选项。模块的手册页介绍了相应模块可接受的选项。典型的模块选项包括  nowarn 和  debug

PAM 堆栈工作原理

应用程序调用以下函数时, libpam 将读取配置文件  /etc/pam.conf 以确定针对该服务参与操作的模块:
如果对于该服务的操作(如验证或帐户管理) /etc/pam.conf 仅包含一个模块,则该模块的结果将确定操作的结果。例如, passwd 应用程序的缺省验证操作包含一个模块  pam_passwd_auth.so.1

passwd  auth required           pam_passwd_auth.so.1
另一方面,如果为服务操作定义了多个模块,那么这些模块就 堆叠起来,即,对于该服务存在一个  PAM 堆栈。例如,请考虑  pam.conf 包含以下项的情况:

login   auth requisite          pam_authtok_get.so.1
login   auth required           pam_dhkeys.so.1
login   auth required           pam_unix_cred.so.1
login   auth required           pam_unix_auth.so.1
login   auth required           pam_dial_auth.so.1
这些项表示  login 服务的  auth 栈样例。要确定此栈的结果,各个模块的结果代码需要进行 集成处理。在集成处理中,模块按照 /etc/pam.conf 中指定的顺序执行。每个成功/失败代码都会根据模块的控制标志集成到整体结果中。控制标志会导致栈很早就终止。例如, requisite 模块可能会失败,而  sufficient 或  binding 模块则可能会成功。处理栈之后,各个结果会组合成一个整体结果并随后传送给应用程序。
控制标志用于指明 PAM 模块在确定对服务的访问方面所起的作用。五个控制标志及其作用如下所示:
  • Binding-如果前面所需的模块都没有失败,则成功满足绑定模块的要求后,会向应用程序立即返回成功信息。如果满足这些条件,则不会进一步执行模块。如果失败,则会记录所需的失败信息并继续处理模块。
  • Optional-不必成功满足可选模块的要求即可使用服务。如果失败,则会记录可选的失败信息。
  • Required-必须成功满足必需模块的要求才能使用服务。如果失败,则执行该服务的其余模块后会返回错误信息。仅当绑定模块或所需的模块没有报告失败的情况下,才会返回该服务最终成功的信息。
  • Requisite-必须成功满足必备模块的要求才能使用服务。如果失败,则会立即返回错误,而不会进一步执行模块。 只有一个服务的所有必备模块都返回成功,函数才能向应用程序返回成功。
  • Sufficient-如果前面所需模块的要求都成功满足,则成功满足控制标志为 sufficient 的模块的要求后将向应用程序立即返回成功,而不会进一步执行模块。如果失败,则会记录可选的失败信息。
下面的两个图说明了如何在集成处理中确定访问权限。第一个图说明如何为每种类型的控制标志记录成败信息。第二个图说明如何确定集成值。
图 3–2 PAM 堆栈:控制标志的影响

该流程图说明控制标志如何影响
图 3–3 PAM 堆栈:如何确定集成值

该流程图说明如何在

PAM 堆栈示例

请考虑以下请求验证的  rlogin 服务示例。

示例 3–1 典型 PAM 配置文件的部分内容

该示例中的  pam.conf 文件包含以下有关  rlogin 服务的内容:

     # Authentication management
     ...     
     # rlogin service 
     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_auth.so.1
     ...
rlogin 服务请求验证时, libpam 将首先执行  pam_rhosts_auth(5) 模块。对于  pam_rhosts_auth 模块,控制标志将设置为 sufficient。如果  pam_rhosts_auth 模块可以验证用户,则处理将停止并且会向应用程序返回成功信息。
如果  pam_rhosts_auth 模块无法验证用户,则将执行下一个 PAM 模块  pam_authtok_get(5)。此模块的控制标志将设置为  requisite。如果 pam_authtok_get 失败,则验证过程将结束并向  rlogin 返回失败信息。
如果  pam_authtok_get 成功,则将执行接下来的两个模块  pam_dhkeys(5) 和  pam_unix_auth(5)。这两个模块的关联控制标志都会设置为 required,以便该过程可继续进行,无论是否会返回单独的失败信息。执行  pam_unix_auth 后,不会再保留任何用于验证  rlogin 的模块。此时,如果  pam_dhkeys 或  pam_unix_auth 返回失败信息,则会拒绝用户通过  rlogin 进行访问。

编写使用 PAM 服务的应用程序

本节提供了使用多个 PAM 函数的应用程序样例。

简单 PAM 消费方示例

以下将以 PAM 消费方应用程序作为示例。该示例是一个基本的终端锁定应用程序,用于验证尝试访问终端的用户。该示例执行以下步骤:
  1. 初始化 PAM 会话。
    PAM 会话通过调用  pam_start(3PAM) 函数来启动。调用任何其他 PAM 函数之前,PAM 消费方应用程序必须首先建立 PAM 会话。pam_start(3PAM) 函数采用以下参数:
    • plock-服务名,即应用程序的名称。PAM 框架使用服务名来确定配置文件  /etc/pam.conf 中适用的规则。服务名通常用于日志记录和错误报告。
    • pw->pw_name-用户名,即 PAM 框架所作用的用户的名称。
    • &conv-对话函数  conv,用于提供 PAM 与用户或应用程序进程通信的通用方法。对话函数是必需的,因为 PAM 模块无法了解如何进行通信。通信可以采用 GUI、命令行、智能读卡器或其他设备等方式进行。有关更多信息,请参见 编写对话函数
    • &pamh-PAM 句柄  pamh,即 PAM 框架用于存储有关当前操作信息的不透明句柄。成功调用  pam_start() 后将返回此句柄。

    注 –
    调用 PAM 接口的应用程序必须具有足够的权限才能执行任何所需的操作,如验证、口令更改、进程凭证处理或审计状态初始化。在该示例中,应用程序必须可以读取  /etc/shadow 才能验证本地用户的口令。

  2. 验证用户。
    应用程序将调用  pam_authenticate(3PAM) 来验证当前用户。通常,系统会要求用户输入口令或其他验证令牌,具体取决于验证服务的类型。PAM 框架会调用  /etc/pam.conf 中为验证服务  auth 列出的模块。服务名  plock 用于确定要使用的  pam.conf 项。如果不存在与 plock 对应的项,则缺省情况下会使用  other 中的项。 如果应用程序配置文件中明确禁止使用 NULL 口令,则应该传递 PAM_DISALLOW_NULL_AUTHTOK 标志。Solaris 应用程序将检查  /etc/default/login 中的  PA×××EQ=YES 设置。
  3. 检查帐户有效性。
    该示例使用  pam_acct_mgmt(3PAM) 函数检查已验证的用户帐户的有效性。在该示例中, pam_acct_mgmt() 用于检查口令的失效期。
    pam_acct_mgmt() 函数还会使用 PAM_DISALLOW_NULL_AUTHTOK 标志。如果  pam_acct_mgmt() 返回 PAM_NEW_AUTHTOK_REQD,则应调用  pam_chauthtok(3PAM) 以允许已验证的用户更改口令。
  4. 如果系统发现口令已过期,则会强制用户更改口令。
    该示例使用循环调用  pam_chauthtok(),直到返回成功信息为止。如果用户成功更改其验证信息(通常为口令),则  pam_chauthtok()函数将返回成功信息。在该示例中,循环将继续直到返回成功信息为止。通常,应用程序会设置终止前应尝试的最多次数。
  5. 调用  pam_setcred(3PAM)
    pam_setcred(3PAM) 函数用于建立、修改或删除用户凭证。 pam_setcred() 通常在验证用户之后进行调用。调用是在验证帐户后和打开会话前进行的。将  pam_setcred() 函数与 PAM_ESTABLISH_CRED 标志结合使用可建立新的用户会话。如果该会话是对现有会话(如对于 lockscreen)的更新,则应调用带有 PAM_REFRESH_CRED 标志的  pam_setcred()。如果会话要更改凭证(如使用  su 或承担角色),则应调用带有 PAM_REINITIALIZE_CRED 标志的  pam_setcred()
  6. 关闭 PAM 会话。
    PAM 会话通过调用  pam_end(3PAM) 函数进行关闭。 pam_end() 还将释放所有的 PAM 资源。
以下示例给出了 PAM 消费方应用程序样例的源代码。

注 –
此示例的源代码也可以通过 Sun 下载中心获取。请访问 http://www.sun.com/download/products.xml?id=41912db5


示例 3–2 PAM 消费方应用程序样例

/*
 * Copyright 2005 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <strings.h>
#include <signal.h>
#include <pwd.h>
#include <errno.h>
#include <security/pam_appl.h>
extern int pam_tty_conv(int num_msg, struct pam_message **msg,
	     struct pam_response **response, void *appdata_ptr);
/* Disable keyboard interrupts (Ctrl-C, Ctrl-Z, Ctrl-\) */
static void
disable_kbd_signals(void)
{
	(void) signal(SIGINT, SIG_IGN);
	(void) signal(SIGTSTP, SIG_IGN);
	(void) signal(SIGQUIT, SIG_IGN);
}
/* Terminate current user session, i.e., logout */
static void
logout()
{
	pid_t pgroup = getpgrp();
	(void) signal(SIGTERM, SIG_IGN);
	(void) fprintf(stderr, "Sorry, your session can't be restored.\n");
	(void) fprintf(stderr, "Press return to terminate this session.\n");
	(void) getchar();
	(void) kill(-pgroup, SIGTERM);
	(void) sleep(2);
	(void) kill(-pgroup, SIGKILL);
	exit(-1);
}
int
/*ARGSUSED*/
main(int argc, char *argv)
{
	struct pam_conv conv = { pam_tty_conv, NULL };
	pam_handle_t *pamh;
	struct passwd *pw;
	int err;
	disable_kbd_signals();
	if ((pw = getpwuid(getuid())) == NULL) {
		(void) fprintf(stderr, "plock: Can't get username: %s\n",
		    strerror(errno));
		exit(1);
	}
	
	/* Initialize PAM framework */
	err = pam_start("plock", pw->pw_name, &conv, &pamh);
	if (err != PAM_SUCCESS) {
		(void) fprintf(stderr, "plock: pam_start failed: %s\n",
		    pam_strerror(pamh, err));
		exit(1);
	}
	/* Authenticate user in order to unlock screen */
	do {
		(void) fprintf(stderr, "Terminal locked for %s. ", pw->pw_name);
		err = pam_authenticate(pamh, 0);
		if (err == PAM_USER_UNKNOWN)
			logout();
		else if (err != PAM_SUCCESS) {
			(void) fprintf(stderr, "Invalid password.\n");
		}
	} while (err != PAM_SUCCESS);
	/* Make sure account and password are still valid */
	switch (err = pam_acct_mgmt(pamh, 0)) {
	case PAM_USER_UNKNOWN:
	case PAM_ACCT_EXPIRED:
		/* User not allowed in anymore */
		logout();
		break;
	case PAM_NEW_AUTHTOK_REQD:
		/* The user's password has expired. Get a new one */
		do {
			err = pam_chauthtok(pamh, 0);
		} while (err == PAM_AUTHTOK_ERR);
		if (err != PAM_SUCCESS)
			logout();
		break;
	}
if (pam_setcred(pamh, PAM_REFRESH_CRED) != pam_success){
    logout();
}
	(void) pam_end(pamh, 0);
	exit(0);
	/*NOTREACHED*/
}

其他有用的 PAM 函数

前面的 示例 3–2 是一个简单的应用程序,它仅说明了几个主要的 PAM 函数。本节将介绍其他一些有用的 PAM 函数。
成功验证用户后,将会调用  pam_open_session(3PAM) 函数打开新会话。
调用  pam_getenvlist(3PAM) 函数可以建立新环境。 pam_getenvlist() 将返回要与现有环境合并的新环境。

编写对话函数

PAM 模块或应用程序可以采用多种方式与用户进行通信:命令行、对话框等。因此,与用户通信的 PAM 消费方的设计者需要编写 对话函数。对话函数用于在用户与模块之间传递消息,而与通信方式无关。对话函数可从对话函数回调  pam_message 参数中的  msg_style 参数派生消息类型。请参见  pam_start(3PAM)
开发者不应对 PAM 如何与用户进行通信做出假设。而应用程序应该与用户交换消息,直到操作完成为止。应用程序会显示与对话函数对应的消息字符串,但不进行解释或修改。一条单独的消息中可以包含多行、控制字符或额外的空格。请注意,服务模块负责本地化发送给对话函数的任何字符串。
下面提供了对话函数样例  pam_tty_conv()pam_tty_conv() 采用以下参数:
  • num_msg-传递给函数的消息数。
  • **mess-指向缓冲区的指针,该缓冲区包含来自用户的消息。
  • **resp-指向缓冲区的指针,该缓冲区包含对用户所做的响应。
  • *my_data-指向应用程序数据的指针。
函数样例从  stdin 获取用户输入。例程需要为响应缓冲区分配内存。可以设置最大值 PAM_MAX_NUM_MSG 以限制消息的数量。如果对话函数返回错误,则对话函数会负责清除并释放为响应分配的任何内存。此外,对话函数必须将响应指针设置为 NULL。请注意,应使用零填充方法来完成内存清除。对话函数的调用方负责释放返回给它的所有响应。要进行对话,该函数将循环处理来自用户应用程序的消息。有效的消息将写入 stdout,所有错误将写入  stderr

注 –
该示例的源代码也可以通过 Sun 下载中心获取。请访问 http://www.sun.com/download/products.xml?id=41912db5


示例 3–3 PAM 对话函数

/* 
 * Copyright 2005 Sun Microsystems, Inc.  All rights reserved. 
 * Use is subject to license terms. 
 */
 
#pragma ident	"@(#)pam_tty_conv.c	1.4	05/02/12 SMI"  
#define	__EXTENSIONS__	/* to expose flockfile and friends in stdio.h */ 
#include <errno.h>
#include <libgen.h>
#include <malloc.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <stropts.h>
#include <unistd.h>
#include <termio.h>
#include <security/pam_appl.h>
static int ctl_c;	/* was the conversation interrupted? */
/* ARGSUSED 1 */
static void
interrupt(int x)
{
	ctl_c = 1;
}
/* getinput -- read user input from stdin abort on ^C
 *	Entry	noecho == TRUE, don't echo input.
 *	Exit	User's input.
 *		If interrupted, send SIGINT to caller for processing.
 */
static char *
getinput(int noecho)
{
	struct termio tty;
	unsigned short tty_flags;
	char input[PAM_MAX_RESP_SIZE];
	int c;
	int i = 0;
	void (*sig)(int);
	ctl_c = 0;
	sig = signal(SIGINT, interrupt);
	if (noecho) {
		(void) ioctl(fileno(stdin), TCGETA, &tty);
		tty_flags = tty.c_lflag;
		tty.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL);
		(void) ioctl(fileno(stdin), TCSETAF, &tty);
	}
	/* go to end, but don't overflow PAM_MAX_RESP_SIZE */
	flockfile(stdin);
	while (ctl_c == 0 &&
	    (c = getchar_unlocked()) != '\n' &&
	    c != '\r' &&
	    c != EOF) {
		if (i < PAM_MAX_RESP_SIZE) {
			input[i++] = (char)c;
		}
	}
	funlockfile(stdin);
	input[i] = '\0';
	if (noecho) {
		tty.c_lflag = tty_flags;
		(void) ioctl(fileno(stdin), TCSETAW, &tty);
		(void) fputc('\n', stdout);
	}
	(void) signal(SIGINT, sig);
	if (ctl_c == 1)
		(void) kill(getpid(), SIGINT);
	return (strdup(input));
}
/* Service modules do not clean up responses if an error is returned.
 * Free responses here.
 */
static void
free_resp(int num_msg, struct pam_response *pr)
{
	int i;
	struct pam_response *r = pr;
	if (pr == NULL)
		return;
	for (i = 0; i < num_msg; i++, r++) {
		if (r->resp) {
			/* clear before freeing -- may be a password */
			bzero(r->resp, strlen(r->resp));
			free(r->resp);
			r->resp = NULL;
		}
	}
	free(pr);
}
/* ARGSUSED */
int
pam_tty_conv(int num_msg, struct pam_message **mess,
    struct pam_response **resp, void *my_data)
{
	struct pam_message *m = *mess;
	struct pam_response *r;
	int i;
	if (num_msg <= 0 || num_msg >= PAM_MAX_NUM_MSG) {
		(void) fprintf(stderr, "bad number of messages %d "
		    "<= 0 || >= %d\n",
		    num_msg, PAM_MAX_NUM_MSG);
		*resp = NULL;
		return (PAM_CONV_ERR);
	}
	if ((*resp = r = calloc(num_msg,
	    sizeof (struct pam_response))) == NULL)
		return (PAM_BUF_ERR);
	/* Loop through messages */
	for (i = 0; i < num_msg; i++) {
		int echo_off;
		/* bad message from service module */
		if (m->msg == NULL) {
			(void) fprintf(stderr, "message[%d]: %d/NULL\n",
			    i, m->msg_style);
			goto err;
		}
		/*
		 * fix up final newline:
		 * 	removed for prompts
		 * 	added back for messages
		 */
		if (m->msg[strlen(m->msg)] == '\n')
			m->msg[strlen(m->msg)] = '\0';
		r->resp = NULL;
		r->resp_retcode = 0;
		echo_off = 0;
		switch (m->msg_style) {
		case PAM_PROMPT_ECHO_OFF:
			echo_off = 1;
			/*FALLTHROUGH*/
		case PAM_PROMPT_ECHO_ON:
			(void) fputs(m->msg, stdout);
			r->resp = getinput(echo_off);
			break;
		case PAM_ERROR_MSG:
			(void) fputs(m->msg, stderr);
			(void) fputc('\n', stderr);
			break;
		case PAM_TEXT_INFO:
			(void) fputs(m->msg, stdout);
			(void) fputc('\n', stdout);
			break;
		default:
			(void) fprintf(stderr, "message[%d]: unknown type "
			    "%d/val=\"%s\"\n",
			    i, m->msg_style, m->msg);
			/* error, service module won't clean up */
			goto err;
		}
		if (errno == EINTR)
			goto err;
		/* next message/response */
		m++;
		r++;
	}
	return (PAM_SUCCESS);
err:
	free_resp(i, r);
	*resp = NULL;
	return (PAM_CONV_ERR);
}

编写提供 PAM 服务的模块

本节介绍如何编写 PAM 服务模块。

PAM 服务提供者要求

PAM 服务模块使用  pam_get_item(3PAM) 和  pam_set_item(3PAM) 与应用程序进行通信。要相互进行通信,服务模块需要使用 pam_get_data(3PAM) 和  pam_set_data(3PAM)。如果同一项目的服务模块需要交换数据,则应建立该项目的唯一数据名称。然后,服务模块即可通过  pam_get_data() 和  pam_set_data() 函数共享此数据。
服务模块必须返回以下三类 PAM 返回码之一:
  • 如果模块在所请求的策略中做出了明确决定,则返回 PAM_SUCCESS。
  • 如果模块未做出策略决定,则返回 PAM_IGNORE。
  • 如果模块参与的决定导致失败,则返回 PAM_ errorerror 可以是常规错误代码或特定于服务模块类型的代码。错误不能是其他服务模块类型的错误代码。有关错误代码,请参见特定的  pam_sm_ module-type 手册页。
如果服务模块执行多个函数,则应将这些函数分成单独的模块。使用此方法,系统管理员可对策略配置进行更为精细的控制。
应该为任何新的服务模块提供手册页。手册页应该包括以下各项:
  • 模块接受的参数。
  • 模块实现的所有函数。
  • 标志对算法的影响。
  • 任何所需的 PAM 项。
  • 特定于此模块的错误返回信息。
服务模块必须支持 PAM_SILENT 标志,以防止显示消息。建议使用  debug 参数将调试信息记录到  syslog 中。请将  syslog(3C) 与 LOG_AUTH 和 LOG_DEBUG 结合使用来记录调试。其他消息应发送到具有 LOG_AUTH 和相应优先级的  syslog()。决不能使用 openlog(3C)closelog(3C) 和  setlogmask(3C),因为这些函数会干扰应用程序设置。

PAM 提供者服务模块样例

以下是一个 PAM 服务模块样例。此示例将查看用户是否是允许访问此服务的组的成员。如果成功,则提供者随后将授予访问权限,如果失败则记录错误消息。该示例执行以下步骤:
  1. 解析从  /etc/pam.conf 中的配置行传递到此模块的标志。
    此模块可接受  nowarn 和  debug 标志以及特定的标志  group。使用  group 标志可以配置模块,允许其访问除缺省使用的组  root 以外的特定组。有关示例,请参见源代码中 DEFAULT_GROUP 的定义。例如,要允许属于组  staff 的用户访问  telnet(1),用户可以使用以下行(位于  /etc/pam.conf 中的  telnet 栈中):
    telnet  account  required  pam_members_only.so.1 group=staff
  2. 获取用户名、服务名和主机名。
    用户名通过调用  pam_get_user(3PAM)(用于从 PAM 句柄中检索当前用户名)获取。如果未设置用户名,则会拒绝访问。服务名和主机名通过调用  pam_get_item(3PAM) 获取。
  3. 验证要使用的信息。
    如果未设置用户名,则拒绝访问。如果未定义要使用的组,则拒绝访问。
  4. 验证当前用户是否是允许访问此主机并且授予访问权限的特殊组的成员。
    如果该特殊组已定义但根本不包含任何成员,则将返回 PAM_IGNORE,指明此模块不参与任何帐户验证过程。该决策将留给栈中的其他模块。
  5. 如果用户不是特殊组的成员,则会显示一条消息,通知用户访问被拒绝。
    记录消息以记录此事件。
以下示例给出了 PAM 提供者样例的源代码。

注 –
此示例的源代码也可以通过 Sun 下载中心获得。请访问 http://www.sun.com/download/products.xml?id=41912db5


示例 3–4 PAM 服务模块样例

/*
 * Copyright 2005 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */
#include <stdio.h>
#include <stdlib.h>
#include <grp.h>
#include <string.h>
#include <syslog.h>
#include <libintl.h>
#include <security/pam_appl.h>
/*
 * by default, only users who are a member of group "root" are allowed access
 */
#define	DEFAULT_GROUP "root"
static char *NOMSG =
	"Sorry, you are not on the access list for this host - access denied.";
int
pam_sm_acct_mgmt(pam_handle_t * pamh, int flags, int argc, const char **argv)
{
	char *user = NULL;
	char *host = NULL;
	char *service = NULL;
	const char *allowed_grp = DEFAULT_GROUP;
	char grp_buf[4096];
	struct group grp;
	struct pam_conv *conversation;
	struct pam_message message;
	struct pam_message *pmessage = &message;
	struct pam_response *res = NULL;
	int i;
	int nowarn = 0;
	int debug = 0;
	/* Set flags to display warnings if in debug mode. */
	for (i = 0; i < argc; i++) {
		if (strcasecmp(argv[i], "nowarn") == 0)
			nowarn = 1;
		else if (strcasecmp(argv[i], "debug") == 0)
			debug = 1;
		else if (strncmp(argv[i], "group=", 6) == 0)
			allowed_grp = &argv[i][6];
	}
	if (flags & PAM_SILENT)
		nowarn = 1;
	/* Get user name,service name, and host name. */
	(void) pam_get_user(pamh, &user, NULL);
	(void) pam_get_item(pamh, PAM_SERVICE, (void **) &service);
	(void) pam_get_item(pamh, PAM_RHOST, (void **) &host);
	/* Deny access if user is NULL. */
	if (user == NULL) {
		syslog(LOG_AUTH|LOG_DEBUG,
		    "%s: members_only: user not set", service);
		return (PAM_USER_UNKNOWN);
	}
	if (host == NULL)
		host = "unknown";
	/*
	 * Deny access if vuser group is required and user is not in vuser
	 * group
	 */
	if (getgrnam_r(allowed_grp, &grp, grp_buf, sizeof (grp_buf)) == NULL) {
		syslog(LOG_NOTICE|LOG_AUTH,
		    "%s: members_only: group \"%s\" not defined",
		    service, allowed_grp);
		return (PAM_SYSTEM_ERR);
	}
	/* Ignore this module if group contains no members. */
	if (grp.gr_mem[0] == 0) {
		if (debug)
			syslog(LOG_AUTH|LOG_DEBUG,
			    "%s: members_only: group %s empty: "
			    "all users allowed.", service, grp.gr_name);
		return (PAM_IGNORE);
	}
	/* Check to see if user is in group. If so, return SUCCESS. */
	for (; grp.gr_mem[0]; grp.gr_mem++) {
		if (strcmp(grp.gr_mem[0], user) == 0) {
			if (debug)
				syslog(LOG_AUTH|LOG_DEBUG,
				    "%s: user %s is member of group %s. "
				    "Access allowed.",
				    service, user, grp.gr_name);
			return (PAM_SUCCESS);
		}
	}
	/*
	 * User is not a member of the group.
	 * Set message style to error and specify denial message.
	 */
	message.msg_style = PAM_ERROR_MSG;
	message.msg = gettext(NOMSG);
	/* Use conversation function to display denial message to user. */
	(void) pam_get_item(pamh, PAM_CONV, (void **) &conversation);
	if (nowarn == 0 && conversation != NULL) {
		int err;
		err = conversation->conv(1, &pmessage, &res,
		    conversation->appdata_ptr);
		if (debug && err != PAM_SUCCESS)
			syslog(LOG_AUTH|LOG_DEBUG,
			    "%s: members_only: conversation returned "
			    "error %d (%s).", service, err,
			    pam_strerror(pamh, err));
		/* free response (if any) */
		if (res != NULL) {
			if (res->resp)
				free(res->resp);
			free(res);
		}
	}
	/* Report denial to system log and return error to caller. */
	syslog(LOG_NOTICE | LOG_AUTH, "%s: members_only: "
	    "Connection for %s not allowed from %s", service, user, host);
	return (PAM_PERM_DENIED);
}