編寫 PAM 應用程序和服務

第 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() 通常在驗證用戶之後進行調用。調用是在驗證帳戶後和打開會話前進行的。將 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);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章