可插拔认证模块

商标

FreeBSD 是 FreeBSD 基金会的注册商标。

本文由 ThinkSec AS 和 Network Associates Laboratories(Network Associates, Inc. 的安全研究部门)为 FreeBSD 项目编写,根据 DARPA/SPAWAR 合同 N66001-01-C-8035(“CBOSS”)完成,作为 DARPA CHATS 研究项目的一部分。

Linux 是 Linus Torvalds 的注册商标。

Motif、OSF/1 和 UNIX 是注册商标,IT DialTone 和 The Open Group 是 The Open Group 在美国和其他国家的商标。

Sun、Sun Microsystems、Java、Java 虚拟机、JDK、JRE、JSP、JVM、Netra、OpenJDK、Solaris、StarOffice、SunOS 和 VirtualBox 是 Sun Microsystems, Inc. 在美国和其他国家的商标或注册商标。

许多制造商和销售商用来区分其产品的名称都被声称为商标。当这些名称出现在本文档中时,如果 FreeBSD 项目知悉商标声明,则这些名称后将加上“™”或“®”符号。

摘要

本文介绍了可插拔认证模块 (PAM) 库的基本原理和机制,并解释了如何配置 PAM、如何将 PAM 集成到应用程序以及如何编写 PAM 模块。


1. 概述

可插拔认证模块 (PAM) 库是一个通用的身份验证相关服务的 API,它允许系统管理员通过安装新的 PAM 模块来添加新的身份验证方法,并通过编辑配置文件来修改身份验证策略。

PAM 由 Sun Microsystems 的 Vipin Samar 和 Charlie Lai 于 1995 年定义和开发,自那时起变化不大。1997 年,The Open Group 发布了 X/Open 单点登录 (XSSO) 预备规范,该规范对 PAM API 进行了标准化,并添加了单点登录 (或更确切地说,集成登录) 的扩展。在撰写本文时,此规范尚未被采用为标准。

虽然本文主要关注使用 OpenPAM 的 FreeBSD 5.x,但它也适用于使用 Linux-PAM 的 FreeBSD 4.x 以及其他操作系统,如 Linux 和 Solaris™。

2. 术语和约定

2.1. 定义

围绕 PAM 的术语比较混乱。Samar 和 Lai 的原始论文以及 XSSO 规范都没有尝试正式定义参与 PAM 的各种参与者和实体的术语,他们使用的术语 (但没有定义) 有时会造成误解和歧义。第一次尝试建立一致且明确的术语是在 1999 年由 Andrew G. Morgan (Linux-PAM 的作者) 撰写的白皮书中。虽然 Morgan 对术语的选择是一个巨大的进步,但在我看来,它绝非完美。以下是根据 Morgan 的灵感,尝试为参与 PAM 的所有参与者和实体定义精确且明确的术语。

账户

申请人向仲裁人请求的凭据集合。

申请人

请求身份验证的用户或实体。

仲裁人

拥有验证申请人凭据的必要权限和批准或拒绝请求的权限的用户或实体。

响应 PAM 请求将被调用的模块序列。该链包括有关模块调用顺序、传递给模块的参数以及如何解释结果的信息。

客户端

代表申请人启动身份验证请求并从申请人那里获取必要身份验证信息的应用程序。

设施

PAM 提供的四种基本功能组之一:身份验证、账户管理、会话管理和身份验证令牌更新。

模块

一个或多个相关函数的集合,这些函数实现特定身份验证设施,这些函数被收集到一个单独的 (通常是动态可加载的) 二进制文件中,并由单个名称标识。

策略

描述如何处理特定服务的 PAM 请求的完整配置语句集。策略通常包含四个链,每个设施一个,尽管一些服务不使用所有四个设施。

服务器

代表仲裁人与客户端进行对话、检索身份验证信息、验证申请人凭据并批准或拒绝请求的应用程序。

服务

提供类似或相关功能并需要类似身份验证的服务器类别。PAM 策略是在每个服务的基础上定义的,因此声明相同服务名称的所有服务器都将受到相同策略的约束。

会话

服务器向申请人提供服务的上下文。PAM 的四个设施之一,会话管理,专门处理此上下文的建立和拆除。

令牌

与账户关联的信息块,例如密码或口令,申请人必须提供此信息以证明其身份。

事务

同一申请人对同一服务器的同一实例的一系列请求,从身份验证和会话设置开始,到会话拆除结束。

2.2. 使用示例

本节旨在通过一些简单的示例来说明上面定义的一些术语的含义。

2.2.1. 客户端和服务器合一

这个简单的示例展示了 alice su(1)root

% whoami
alice

% ls -l `which su`
-r-sr-xr-x  1 root  wheel  10744 Dec  6 19:06 /usr/bin/su

% su -
Password: xi3kiune
# whoami
root
  • 申请人是 alice

  • 账户是 root

  • su(1) 进程既是客户端又是服务器。

  • 身份验证令牌是 xi3kiune

  • 仲裁人是 root,这就是为什么 su(1) 被设置为 root

2.2.2. 客户端和服务器分离

以下示例展示了 eve 尝试启动与 login.example.comssh(1) 连接,请求以 bob 登录,并成功。Bob 应该选择一个更好的密码!

% whoami
eve

% ssh bob@login.example.com
bob@login.example.com's password:
% god
Last login: Thu Oct 11 09:52:57 2001 from 192.168.0.1
Copyright (c) 1980, 1983, 1986, 1988, 1990, 1991, 1993, 1994
	The Regents of the University of California.  All rights reserved.
FreeBSD 4.4-STABLE (LOGIN) 4: Tue Nov 27 18:10:34 PST 2001

Welcome to FreeBSD!
%
  • 申请人是 eve

  • 客户端是 Eve 的 ssh(1) 进程。

  • 服务器是 login.example.com 上的 sshd(8) 进程

  • 账户是 bob

  • 身份验证令牌是 god

  • 虽然本示例中没有显示,但仲裁人是 root

2.2.3. 策略示例

以下是 FreeBSD 的 sshd 默认策略

sshd	auth		required	pam_nologin.so	no_warn
sshd	auth		required	pam_unix.so	no_warn try_first_pass
sshd	account		required	pam_login_access.so
sshd	account		required	pam_unix.so
sshd	session		required	pam_lastlog.so	no_fail
sshd	password	required	pam_permit.so
  • 此策略适用于 sshd 服务 (不一定是 sshd(8) 服务器)

  • authaccountsessionpassword 是设施。

  • pam_nologin.sopam_unix.sopam_login_access.sopam_lastlog.sopam_permit.so 是模块。从本示例中可以清楚地看出 pam_unix.so 提供至少两种设施 (身份验证和账户管理)。

3. PAM 基础

3.1. 设施和原语

PAM API 提供六种不同的身份验证原语,这些原语被分为四种设施,如下所述。

auth

身份验证。 此设施负责验证申请人并建立账户凭据。它提供两个原语

  • pam_authenticate(3) 验证申请人,通常通过请求身份验证令牌并将其与存储在数据库中或从身份验证服务器获取的值进行比较。

  • pam_setcred(3) 建立账户凭据,如用户 ID、组成员资格和资源限制。

账户

账户管理。 此设施处理与身份验证无关的账户可用性问题,例如基于时间或服务器工作负载的访问限制。它提供一个原语

会话

会话管理。 此设施处理与会话设置和拆除相关的任务,例如登录记录。它提供两个原语

  • pam_open_session(3) 执行与会话设置相关的任务:在 utmpwtmp 数据库中添加条目、启动 SSH 代理等。

  • pam_close_session(3) 执行与会话拆除相关的任务:在 utmpwtmp 数据库中添加条目、停止 SSH 代理等。

password

密码管理。 此设施用于更改与账户关联的身份验证令牌,无论是由于它已过期还是由于用户希望更改它。它提供一个原语

  • pam_chauthtok(3) 更改身份验证令牌,可以选择验证它是否足够难以猜测、之前从未使用过等。

3.2. 模块

模块是 PAM 中一个非常核心的概念;毕竟,它们是“PAM”中的“M”。PAM 模块是独立的程序代码片段,它为一种特定机制实现一种或多种设施中的原语;例如,身份验证设施的可能机制包括 UNIX® 密码数据库、NIS、LDAP 和 Radius。

3.2.1. 模块命名

FreeBSD 在单个模块中实现每种机制,该模块名为 pam_mechanism.so(例如,pam_unix.so 代表 UNIX® 机制)。其他实现有时会为不同的功能使用单独的模块,并在模块名称中包含功能名称和机制名称。举个例子,Solaris™ 有一个 pam_dial_auth.so.1 模块,通常用于对拨号用户进行身份验证。

3.2.2. 模块版本控制

FreeBSD 最初的 PAM 实现基于 Linux-PAM,没有为 PAM 模块使用版本号。这通常会导致与旧版应用程序出现问题,这些应用程序可能与旧版本的系统库链接,因为无法加载匹配版本的所需模块。

另一方面,OpenPAM 会查找与 PAM 库具有相同版本号的模块(目前为 2),只有在无法加载版本化的模块时才会回退到非版本化的模块。因此,可以为旧版应用程序提供旧版模块,同时允许新的(或新构建的)应用程序利用最新的模块。

虽然 Solaris™ PAM 模块通常具有版本号,但它们并没有真正进行版本控制,因为该编号是模块名称的一部分,必须包含在配置中。

3.3. 链和策略

当服务器启动 PAM 事务时,PAM 库会尝试加载在 pam_start(3) 调用中指定的服务的策略。该策略指定如何处理身份验证请求,并在配置文件中定义。这是 PAM 中的另一个核心概念:管理员可以通过简单地编辑一个文本文件来调整系统安全策略(从广义上讲)。

策略包含四个链,分别对应四种 PAM 功能。每个链都是一个配置语句序列,每个语句指定一个要调用的模块,一些(可选的)传递给模块的参数,以及一个控制标志,用于描述如何解释模块的返回值。

理解控制标志对于理解 PAM 配置文件至关重要。有四种不同的控制标志。

绑定

如果模块成功并且链中较早的模块没有失败,则立即终止链并授予请求。如果模块失败,则执行链的其余部分,但最终拒绝请求。

此控制标志由 Sun 在 Solaris™ 9(SunOS™ 5.9)中引入,也受 OpenPAM 支持。

必需

如果模块成功,则执行链的其余部分,并授予请求,除非其他模块失败。如果模块失败,则也会执行链的其余部分,但最终拒绝请求。

必要

如果模块成功,则执行链的其余部分,并授予请求,除非其他模块失败。如果模块失败,则立即终止链并拒绝请求。

足够

如果模块成功并且链中较早的模块没有失败,则立即终止链并授予请求。如果模块失败,则忽略该模块并执行链的其余部分。

由于此标志的语义可能有些混乱,尤其是在将其用于链中的最后一个模块时,建议如果实现支持,则改用 绑定 控制标志。

可选

执行模块,但忽略其结果。如果链中所有模块都标记为 可选,则始终会授予所有请求。

当服务器调用六个 PAM 原语之一时,PAM 会检索与原语所属功能相对应的链,并按列表顺序调用链中列出的每个模块,直到到达末尾或确定不再需要进一步处理(要么是因为一个 绑定足够 模块成功,要么是因为一个 必要 模块失败)。当且仅当调用了至少一个模块且所有非可选模块成功时才授予请求。

请注意,同一个模块可以在同一个链中多次列出,尽管这种情况并不常见。例如,一个在目录服务器中查找用户名和密码的模块可以多次调用,不同的参数指定要联系的不同目录服务器。PAM 将同一个链中同一个模块的不同出现视为不同的、无关的模块。

3.4. 事务

下面描述了典型 PAM 事务的生命周期。请注意,如果这些步骤中的任何一个失败,服务器应向客户端报告合适的错误消息并中止事务。

  1. 如果有必要,服务器会通过独立于 PAM 的机制获得仲裁者的凭证——最常见的是通过由 root 启动或设置为 root 来获得。

  2. 服务器调用 pam_start(3) 来初始化 PAM 库并指定其服务名称和目标帐户,并注册合适的对话函数。

  3. 服务器获取与事务相关的各种信息(例如申请者的用户名和客户端运行的主机名),并使用 pam_set_item(3) 将其提交给 PAM。

  4. 服务器调用 pam_authenticate(3) 来验证申请者。

  5. 服务器调用 pam_acct_mgmt(3) 来验证请求的帐户是否可用且有效。如果密码正确但已过期,则 pam_acct_mgmt(3) 会返回 PAM_NEW_AUTHTOK_REQD 而不是 PAM_SUCCESS

  6. 如果上一步返回 PAM_NEW_AUTHTOK_REQD,则服务器现在调用 pam_chauthtok(3) 来强制客户端更改请求帐户的身份验证令牌。

  7. 现在申请者已成功验证,服务器调用 pam_setcred(3) 来建立请求帐户的凭证。它能够做到这一点,因为它代表仲裁者行事,并持有仲裁者的凭证。

  8. 建立正确的凭证后,服务器调用 pam_open_session(3) 来设置会话。

  9. 服务器现在执行客户端请求的任何服务——例如,为申请者提供一个 shell。

  10. 服务器完成为客户端服务后,调用 pam_close_session(3) 来拆除会话。

  11. 最后,服务器调用 pam_end(3) 来通知 PAM 库它已完成,可以释放其在事务过程中分配的任何资源。

4. PAM 配置

4.1. PAM 策略文件

4.1.1. /etc/pam.conf

传统的 PAM 策略文件是 /etc/pam.conf。此文件包含系统的所有 PAM 策略。该文件中的每一行描述了链中的一个步骤,如下所示

login   auth    required        pam_nologin.so  no_warn

这些字段按顺序依次为:服务名称、功能名称、控制标志、模块名称和模块参数。任何其他字段都被解释为附加的模块参数。

为每个服务/功能对构建一个单独的链,因此,虽然相同服务和功能的行的出现顺序很重要,但各个服务和功能的列出顺序并不重要。原始 PAM 论文中的示例按功能对配置行进行分组,而 Solaris™ 标准 pam.conf 仍然这样做,但 FreeBSD 的标准配置按服务对配置行进行分组。两种方式都可以;两种方式都有意义。

4.1.2. /etc/pam.d

OpenPAM 和 Linux-PAM 支持一种替代配置机制,这是 FreeBSD 中的首选机制。在这种方案中,每个策略都包含在一个与它应用的服务同名的单独文件中。这些文件存储在 /etc/pam.d/ 中。

这些按服务划分的策略文件只有四个字段,而不是 pam.conf 的五个字段:服务名称字段被省略。因此,与上一节中的示例 pam.conf 行相比,在 /etc/pam.d/login 中将有以下一行

auth    required        pam_nologin.so  no_warn

由于这种简化的语法,可以通过将每个服务名称链接到同一个策略文件来将相同的策略用于多个服务。例如,要对 susudo 服务使用相同的策略,可以执行以下操作

# cd /etc/pam.d
# ln -s su sudo

之所以有效,是因为服务名称是从文件名而不是策略文件中的指定内容确定的,因此同一个文件可以用于多个不同名称的服务。

由于每个服务的策略都存储在单独的文件中,因此 pam.d 机制也使为第三方软件包安装额外的策略变得非常容易。

4.1.3. 策略搜索顺序

正如我们上面所见,PAM 策略可以在多个地方找到。如果同一个服务的策略存在于多个地方,会发生什么情况?

必须理解,PAM 的配置系统以链为中心。

4.2. 配置行的细分

正如 PAM 策略文件 中所述,/etc/pam.conf 中的每一行都包含四个或更多字段:服务名称、功能名称、控制标志、模块名称和零个或多个模块参数。

服务名称通常(但并不总是)是语句应用到的应用程序的名称。如果您不确定,请参阅各个应用程序的文档以确定它使用什么服务名称。

请注意,如果您使用 /etc/pam.d/ 而不是 /etc/pam.conf,则服务名称由策略文件的名称指定,并在实际配置行中省略,配置行从功能名称开始。

功能是 功能和原语 中描述的四个功能关键字之一。

同样,控制标志是 链和策略 中描述的四个关键字之一,描述如何解释模块的返回值。Linux-PAM 支持一种替代语法,允许您指定与每个可能返回值关联的操作,但应避免这种语法,因为它是非标准的,并且与 Linux-PAM 分派服务调用的方式密切相关(这与 Solaris™ 和 OpenPAM 的方式有很大区别)。不出所料,OpenPAM 不支持这种语法。

4.3. 策略

要正确配置 PAM,必须了解策略是如何解释的。

当应用程序调用 pam_start(3) 时,PAM 库会加载指定服务的策略并构建四个模块链(每个设施一个)。如果其中一个或多个链为空,则会替换来自“other”服务策略的相应链。

当应用程序稍后调用六个 PAM 原语中的一个时,PAM 库会检索对应设施的链,并按链中列出的顺序依次调用链中每个模块的相应服务函数。在每次调用服务函数后,会使用模块类型和服务函数返回的错误代码来确定下一步的操作。除了下面将讨论的几个例外情况,以下表格适用。

表 1. PAM 链执行摘要
PAM_SUCCESSPAM_IGNOREother

绑定

if (!fail) break;

-

fail = true;

必需

-

-

fail = true;

必要

-

-

fail = true; break;

足够

if (!fail) break;

-

-

可选

-

-

-

如果在链的末尾或到达“break”时 fail 为 true,则调度程序将返回第一个失败模块返回的错误代码。否则,它将返回 PAM_SUCCESS

第一个值得注意的例外情况是,错误代码 PAM_NEW_AUTHTOK_REQD 被视为成功,但如果没有任何模块失败,并且至少有一个模块返回了 PAM_NEW_AUTHTOK_REQD,则调度程序将返回 PAM_NEW_AUTHTOK_REQD

第二个例外情况是,pam_setcred(3)bindingsufficient 模块视为 required 模块。

第三个也是最后一个例外情况是,pam_chauthtok(3) 会运行整个链两次(一次用于初步检查,一次用于实际设置密码),并且在初步阶段,它会将 bindingsufficient 模块视为 required 模块。

5. FreeBSD PAM 模块

5.1. pam_deny(8)

The pam_deny(8) 模块是最简单的模块之一;它对任何请求都以 PAM_AUTH_ERR 响应。它在快速禁用服务(将其添加到每个链的顶部)或终止 sufficient 模块链时非常有用。

5.2. pam_echo(8)

The pam_echo(8) 模块只是将它的参数作为 PAM_TEXT_INFO 消息传递给对话函数。它主要用于调试,但也可以用来在启动身份验证过程之前显示消息,例如“未经授权的访问将被起诉”。

5.3. pam_exec(8)

The pam_exec(8) 模块将它的第一个参数视为要执行的程序的名称,其余参数作为命令行参数传递给该程序。一种可能的应用是使用它在登录时运行一个程序,该程序会挂载用户的家目录。

5.5. pam_group(8)

The pam_group(8) 模块根据申请人是否属于特定文件组(通常对于 su(1) 而言是 wheel)来接受或拒绝申请。它主要用于维护 BSD su(1) 的传统行为,但也具有许多其他用途,例如将某些用户组排除在特定服务之外。

5.6. pam_guest(8)

The pam_guest(8) 模块允许使用固定登录名进行访客登录。可以对密码施加各种要求,但默认行为是只要登录名是访客帐户,就允许任何密码。The pam_guest(8) 模块可以轻松地用于实现匿名 FTP 登录。

5.7. pam_krb5(8)

The pam_krb5(8) 模块

5.8. pam_ksu(8)

The pam_ksu(8) 模块

5.10. pam_login_access(8)

The pam_login_access(8) 模块提供了帐户管理原语的实现,该原语强制执行在 login.access(5) 表中指定的登录限制。

5.11. pam_nologin(8)

The pam_nologin(8) 模块在 /var/run/nologin 存在时拒绝非 root 登录。该文件通常由 shutdown(8) 在计划的关机时间剩余不到五分钟时创建。

5.13. pam_permit(8)

The pam_permit(8) 模块是最简单的模块之一;它对任何请求都以 PAM_SUCCESS 响应。它在作为占位符时非常有用,因为如果没有它,一个或多个链将为空。

5.16. pam_rootok(8)

The pam_rootok(8) 模块仅当调用它的进程(假设由申请人运行)的真实用户 ID 为 0 时报告成功。这对于非联网服务(如 su(1)passwd(1))非常有用,因为 root 应该能够自动访问这些服务。

5.18. pam_self(8)

The pam_self(8) 模块仅当申请人的名称与目标帐户的名称匹配时报告成功。它对于非联网服务(如 su(1))最有用,因为在这种服务中可以轻松验证申请人的身份。

5.19. pam_ssh(8)

The pam_ssh(8) 模块提供身份验证和会话服务。身份验证服务允许用户在 ~/.ssh 目录中具有密码保护的 SSH 私钥的用户通过输入其密码进行身份验证。会话服务启动 ssh-agent(1) 并使用在身份验证阶段解密的密钥预加载它。此功能对于本地登录特别有用,无论是在 X 中(使用 xdm(8) 或其他支持 PAM 的 X 登录管理器)还是在控制台中。

5.21. pam_unix(8)

The pam_unix(8) 模块使用传统的 UNIX® 密码身份验证,使用 getpwnam(3) 获取目标帐户的密码并将其与申请人提供的密码进行比较。它还提供帐户管理服务(强制执行帐户和密码过期时间)和密码更改服务。这可能是最重要的单个模块,因为绝大多数管理员希望至少为某些服务维护历史行为。

6. PAM 应用程序编程

本节尚未编写。

7. PAM 模块编程

本节尚未编写。

附录 A:PAM 应用程序示例

以下是使用 PAM 的 su(1) 的最小实现。请注意,它使用 OpenPAM 特定的 openpam_ttyconv(3) 对话函数,该函数在 security/openpam.h 中声明。如果您希望在具有不同 PAM 库的系统上构建此应用程序,则必须提供自己的对话函数。健壮的对话函数的实现令人惊讶地困难;PAM 对话函数示例 中提出的函数是一个不错的起点,但不应在现实世界的应用程序中使用。

/*-
 * Copyright (c) 2002,2003 Networks Associates Technology, Inc.
 * All rights reserved.
 *
 * This software was developed for the FreeBSD Project by ThinkSec AS and
 * Network Associates Laboratories, the Security Research Division of
 * Network Associates, Inc.  under DARPA/SPAWAR contract N66001-01-C-8035
 * ("CBOSS"), as part of the DARPA CHATS research program.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote
 *    products derived from this software without specific prior written
 *    permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * $P4: //depot/projects/openpam/bin/su/su.c#10 $
 * $FreeBSD: head/en_US.ISO8859-1/articles/pam/su.c 38826 2012-05-17 19:12:14Z hrs $
 */

#include <sys/param.h>
#include <sys/wait.h>

#include <err.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>

#include <security/pam_appl.h>
#include <security/openpam.h>	/* for openpam_ttyconv() */

extern char **environ;

static pam_handle_t *pamh;
static struct pam_conv pamc;

static void
usage(void)
{

	fprintf(stderr, "Usage: su [login [args]]\n");
	exit(1);
}

int
main(int argc, char *argv[])
{
	char hostname[MAXHOSTNAMELEN];
	const char *user, *tty;
	char **args, **pam_envlist, **pam_env;
	struct passwd *pwd;
	int o, pam_err, status;
	pid_t pid;

	while ((o = getopt(argc, argv, "h")) != -1)
		switch (o) {
		case 'h':
		default:
			usage();
		}

	argc -= optind;
	argv += optind;

	if (argc > 0) {
		user = *argv;
		--argc;
		++argv;
	} else {
		user = "root";
	}

	/* initialize PAM */
	pamc.conv = &openpam_ttyconv;
	pam_start("su", user, &pamc, &pamh);

	/* set some items */
	gethostname(hostname, sizeof(hostname));
	if ((pam_err = pam_set_item(pamh, PAM_RHOST, hostname)) != PAM_SUCCESS)
		goto pamerr;
	user = getlogin();
	if ((pam_err = pam_set_item(pamh, PAM_RUSER, user)) != PAM_SUCCESS)
		goto pamerr;
	tty = ttyname(STDERR_FILENO);
	if ((pam_err = pam_set_item(pamh, PAM_TTY, tty)) != PAM_SUCCESS)
		goto pamerr;

	/* authenticate the applicant */
	if ((pam_err = pam_authenticate(pamh, 0)) != PAM_SUCCESS)
		goto pamerr;
	if ((pam_err = pam_acct_mgmt(pamh, 0)) == PAM_NEW_AUTHTOK_REQD)
		pam_err = pam_chauthtok(pamh, PAM_CHANGE_EXPIRED_AUTHTOK);
	if (pam_err != PAM_SUCCESS)
		goto pamerr;

	/* establish the requested credentials */
	if ((pam_err = pam_setcred(pamh, PAM_ESTABLISH_CRED)) != PAM_SUCCESS)
		goto pamerr;

	/* authentication succeeded; open a session */
	if ((pam_err = pam_open_session(pamh, 0)) != PAM_SUCCESS)
		goto pamerr;

	/* get mapped user name; PAM may have changed it */
	pam_err = pam_get_item(pamh, PAM_USER, (const void **)&user);
	if (pam_err != PAM_SUCCESS || (pwd = getpwnam(user)) == NULL)
		goto pamerr;

	/* export PAM environment */
	if ((pam_envlist = pam_getenvlist(pamh)) != NULL) {
		for (pam_env = pam_envlist; *pam_env != NULL; ++pam_env) {
			putenv(*pam_env);
			free(*pam_env);
		}
		free(pam_envlist);
	}

	/* build argument list */
	if ((args = calloc(argc + 2, sizeof *args)) == NULL) {
		warn("calloc()");
		goto err;
	}
	*args = pwd->pw_shell;
	memcpy(args + 1, argv, argc * sizeof *args);

	/* fork and exec */
	switch ((pid = fork())) {
	case -1:
		warn("fork()");
		goto err;
	case 0:
		/* child: give up privs and start a shell */

		/* set uid and groups */
		if (initgroups(pwd->pw_name, pwd->pw_gid) == -1) {
			warn("initgroups()");
			_exit(1);
		}
		if (setgid(pwd->pw_gid) == -1) {
			warn("setgid()");
			_exit(1);
		}
		if (setuid(pwd->pw_uid) == -1) {
			warn("setuid()");
			_exit(1);
		}
		execve(*args, args, environ);
		warn("execve()");
		_exit(1);
	default:
		/* parent: wait for child to exit */
		waitpid(pid, &status, 0);

		/* close the session and release PAM resources */
		pam_err = pam_close_session(pamh, 0);
		pam_end(pamh, pam_err);

		exit(WEXITSTATUS(status));
	}

pamerr:
	fprintf(stderr, "Sorry\n");
err:
	pam_end(pamh, pam_err);
	exit(1);
}

附录 B:PAM 模块示例

以下是 pam_unix(8) 的最小实现,仅提供身份验证服务。它应该能够使用大多数 PAM 实现进行构建和运行,但如果可用,它会利用 OpenPAM 扩展:请注意 pam_get_authtok(3) 的使用,这极大地简化了向用户提示输入密码的过程。

/*-
 * Copyright (c) 2002 Networks Associates Technology, Inc.
 * All rights reserved.
 *
 * This software was developed for the FreeBSD Project by ThinkSec AS and
 * Network Associates Laboratories, the Security Research Division of
 * Network Associates, Inc.  under DARPA/SPAWAR contract N66001-01-C-8035
 * ("CBOSS"), as part of the DARPA CHATS research program.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote
 *    products derived from this software without specific prior written
 *    permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * $P4: //depot/projects/openpam/modules/pam_unix/pam_unix.c#3 $
 * $FreeBSD: head/en_US.ISO8859-1/articles/pam/pam_unix.c 38826 2012-05-17 19:12:14Z hrs $
 */

#include <sys/param.h>

#include <pwd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <security/pam_modules.h>
#include <security/pam_appl.h>

#ifndef _OPENPAM
static char password_prompt[] = "Password:";
#endif

#ifndef PAM_EXTERN
#define PAM_EXTERN
#endif

PAM_EXTERN int
pam_sm_authenticate(pam_handle_t *pamh, int flags,
	int argc, const char *argv[])
{
#ifndef _OPENPAM
	struct pam_conv *conv;
	struct pam_message msg;
	const struct pam_message *msgp;
	struct pam_response *resp;
#endif
	struct passwd *pwd;
	const char *user;
	char *crypt_password, *password;
	int pam_err, retry;

	/* identify user */
	if ((pam_err = pam_get_user(pamh, &user, NULL)) != PAM_SUCCESS)
		return (pam_err);
	if ((pwd = getpwnam(user)) == NULL)
		return (PAM_USER_UNKNOWN);

	/* get password */
#ifndef _OPENPAM
	pam_err = pam_get_item(pamh, PAM_CONV, (const void **)&conv);
	if (pam_err != PAM_SUCCESS)
		return (PAM_SYSTEM_ERR);
	msg.msg_style = PAM_PROMPT_ECHO_OFF;
	msg.msg = password_prompt;
	msgp = &msg;
#endif
	for (retry = 0; retry < 3; ++retry) {
#ifdef _OPENPAM
		pam_err = pam_get_authtok(pamh, PAM_AUTHTOK,
		    (const char **)&password, NULL);
#else
		resp = NULL;
		pam_err = (*conv->conv)(1, &msgp, &resp, conv->appdata_ptr);
		if (resp != NULL) {
			if (pam_err == PAM_SUCCESS)
				password = resp->resp;
			else
				free(resp->resp);
			free(resp);
		}
#endif
		if (pam_err == PAM_SUCCESS)
			break;
	}
	if (pam_err == PAM_CONV_ERR)
		return (pam_err);
	if (pam_err != PAM_SUCCESS)
		return (PAM_AUTH_ERR);

	/* compare passwords */
	if ((!pwd->pw_passwd[0] && (flags & PAM_DISALLOW_NULL_AUTHTOK)) ||
	    (crypt_password = crypt(password, pwd->pw_passwd)) == NULL ||
	    strcmp(crypt_password, pwd->pw_passwd) != 0)
		pam_err = PAM_AUTH_ERR;
	else
		pam_err = PAM_SUCCESS;
#ifndef _OPENPAM
	free(password);
#endif
	return (pam_err);
}

PAM_EXTERN int
pam_sm_setcred(pam_handle_t *pamh, int flags,
	int argc, const char *argv[])
{

	return (PAM_SUCCESS);
}

PAM_EXTERN int
pam_sm_acct_mgmt(pam_handle_t *pamh, int flags,
	int argc, const char *argv[])
{

	return (PAM_SUCCESS);
}

PAM_EXTERN int
pam_sm_open_session(pam_handle_t *pamh, int flags,
	int argc, const char *argv[])
{

	return (PAM_SUCCESS);
}

PAM_EXTERN int
pam_sm_close_session(pam_handle_t *pamh, int flags,
	int argc, const char *argv[])
{

	return (PAM_SUCCESS);
}

PAM_EXTERN int
pam_sm_chauthtok(pam_handle_t *pamh, int flags,
	int argc, const char *argv[])
{

	return (PAM_SERVICE_ERR);
}

#ifdef PAM_MODULE_ENTRY
PAM_MODULE_ENTRY("pam_unix");
#endif

附录 C:PAM 对话函数示例

下面介绍的对话函数是 OpenPAM 的 openpam_ttyconv(3) 的一个简化版本。它功能齐全,应该让读者了解对话函数应该如何运行,但它对于现实世界的使用来说过于简单。即使您没有使用 OpenPAM,也可以随意下载源代码并根据您的需要调整 openpam_ttyconv(3);我们认为它尽可能地健壮,因为一个面向 tty 的对话函数能够实现的健壮性。

/*-
 * Copyright (c) 2002 Networks Associates Technology, Inc.
 * All rights reserved.
 *
 * This software was developed for the FreeBSD Project by ThinkSec AS and
 * Network Associates Laboratories, the Security Research Division of
 * Network Associates, Inc.  under DARPA/SPAWAR contract N66001-01-C-8035
 * ("CBOSS"), as part of the DARPA CHATS research program.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote
 *    products derived from this software without specific prior written
 *    permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * $FreeBSD: head/en_US.ISO8859-1/articles/pam/converse.c 38826 2012-05-17 19:12:14Z hrs $
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <security/pam_appl.h>

int
converse(int n, const struct pam_message **msg,
	struct pam_response **resp, void *data)
{
	struct pam_response *aresp;
	char buf[PAM_MAX_RESP_SIZE];
	int i;

	data = data;
	if (n <= 0 || n > PAM_MAX_NUM_MSG)
		return (PAM_CONV_ERR);
	if ((aresp = calloc(n, sizeof *aresp)) == NULL)
		return (PAM_BUF_ERR);
	for (i = 0; i < n; ++i) {
		aresp[i].resp_retcode = 0;
		aresp[i].resp = NULL;
		switch (msg[i]->msg_style) {
		case PAM_PROMPT_ECHO_OFF:
			aresp[i].resp = strdup(getpass(msg[i]->msg));
			if (aresp[i].resp == NULL)
				goto fail;
			break;
		case PAM_PROMPT_ECHO_ON:
			fputs(msg[i]->msg, stderr);
			if (fgets(buf, sizeof buf, stdin) == NULL)
				goto fail;
			aresp[i].resp = strdup(buf);
			if (aresp[i].resp == NULL)
				goto fail;
			break;
		case PAM_ERROR_MSG:
			fputs(msg[i]->msg, stderr);
			if (strlen(msg[i]->msg) > 0 &&
			    msg[i]->msg[strlen(msg[i]->msg) - 1] != '\n')
				fputc('\n', stderr);
			break;
		case PAM_TEXT_INFO:
			fputs(msg[i]->msg, stdout);
			if (strlen(msg[i]->msg) > 0 &&
			    msg[i]->msg[strlen(msg[i]->msg) - 1] != '\n')
				fputc('\n', stdout);
			break;
		default:
			goto fail;
		}
	}
	*resp = aresp;
	return (PAM_SUCCESS);
 fail:
        for (i = 0; i < n; ++i) {
                if (aresp[i].resp != NULL) {
                        memset(aresp[i].resp, 0, strlen(aresp[i].resp));
                        free(aresp[i].resp);
                }
        }
        memset(aresp, 0, n * sizeof *aresp);
	*resp = NULL;
	return (PAM_CONV_ERR);
}

进一步阅读

论文

使登录服务独立于身份验证技术 Vipin Samar。Charlie Lai。Sun Microsystems。

X/Open 单点登录初步规范。The Open Group。1-85912-144-6。1997 年 6 月。

可插拔身份验证模块。Andrew G. Morgan。1999-10-06。

用户手册

PAM 管理。Sun Microsystems。

OpenPAM 主页 Dag-Erling Smørgrav。ThinkSec AS。

Linux-PAM 主页 Andrew Morgan。

Solaris PAM 主页。Sun Microsystems。


最后修改日期:2024 年 9 月 23 日,由 Fernando Apesteguía 修改