view src/module.rs @ 70:9f8381a1c09c

Implement low-level conversation primitives. This change does two primary things: 1. Introduces new Conversation traits, to be implemented both by the library and by PAM client applications. 2. Builds the memory-management infrastructure for passing messages through the conversation. ...and it adds tests for both of the above, including ASAN tests.
author Paul Fisher <paul@pfish.zone>
date Tue, 03 Jun 2025 01:21:59 -0400
parents a674799a5cd3
children 58f9d2a4df38
line wrap: on
line source

//! Functions and types useful for implementing a PAM module.

// Temporarily allowed until we get the actual conversation functions hooked up.
#![allow(dead_code)]

use crate::constants::{ErrorCode, Flags, Result};
use crate::conv::BinaryData;
use crate::conv::{Conversation, Message, Response};
use crate::handle::PamModuleHandle;
use secure_string::SecureString;
use std::ffi::CStr;

/// A trait for a PAM module to implement.
///
/// The default implementations of all these hooks tell PAM to ignore them
/// (i.e., behave as if this module does not exist) by returning [`ErrorCode::Ignore`].
/// Override any functions you wish to handle in your module.
/// After implementing this trait, use the [`pam_hooks!`](crate::pam_hooks!) macro
/// to make the functions available to PAM.
///
/// For more information, see [`pam(3)`’s root manual page][manpage]
/// and the [PAM Module Writer’s Guide][mwg].
///
/// [manpage]: https://www.man7.org/linux/man-pages/man3/pam.3.html
/// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/Linux-PAM_MWG.html
#[allow(unused_variables)]
pub trait PamModule<T: PamModuleHandle> {
    // Functions for auth modules.

    /// Authenticate the user.
    ///
    /// This is probably the first thing you want to implement.
    /// In most cases, you will want to get the user and password,
    /// using [`LibPamHandle::get_user`] and [`LibPamHandle::get_authtok`],
    /// and verify them against something.
    ///
    /// See [the Module Writer's Guide entry for `pam_sm_authenticate`][mwg]
    /// for more information.
    ///
    /// # Valid flags
    ///
    /// This function may be called with the following flags set:
    ///
    /// - [`Flags::SILENT`]
    /// - [`Flags::DISALLOW_NULL_AUTHTOK`]
    ///
    /// # Returns
    ///
    /// If the password check was successful, return `Ok(())`.
    ///
    /// Sensible error codes to return include:
    ///
    /// - [`ErrorCode::AuthenticationError`]: Generic authentication error
    ///   (like an incorrect password).
    /// - [`ErrorCode::CredentialsInsufficient`]: The application does not have
    ///   sufficient credentials to authenticate the user.
    /// - [`ErrorCode::AuthInfoUnavailable`]: The module was not able to access
    ///   the authentication information, for instance due to a network failure.
    /// - [`ErrorCode::UserUnknown`]: The supplied username is not known by this service.
    /// - [`ErrorCode::MaxTries`]: The user has tried authenticating too many times.
    ///   They should not try again.
    ///
    /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-auth.html#mwg-pam_sm_authenticate
    fn authenticate(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
        Err(ErrorCode::Ignore)
    }

    /// Perform "account management".
    ///
    /// When PAM calls this function, the user has already been authenticated
    /// by an authentication module (either this one or some other module).
    /// This hook can check for other things, for instance:
    ///
    /// - Date/time (keep your kids off the computer at night)
    /// - Remote host (only let employees log in from the office)
    ///
    /// You can also check things like, e.g., password expiration,
    /// and alert that the user change it before continuing,
    /// or really do whatever you want.
    ///
    /// See [the Module Writer's Guide entry for `pam_sm_acct_mgmt`][mwg]
    /// for more information.
    ///
    ///
    /// # Valid flags
    ///
    /// This function may be called with the following flags set:
    ///
    /// - [`Flags::SILENT`]
    /// - [`Flags::DISALLOW_NULL_AUTHTOK`]
    ///
    /// # Returns
    ///
    /// If the user should be allowed to log in, return `Ok(())`.
    ///
    /// Sensible error codes to return include:
    ///
    /// - [`ErrorCode::AccountExpired`]: The user's account has expired.
    /// - [`ErrorCode::AuthenticationError`]: Generic authentication error.
    /// - [`ErrorCode::NewAuthTokRequired`]: The user's authentication token has expired.
    ///   PAM will ask the user to set a new authentication token, which may be handled by
    ///   this module in [`Self::change_authtok`].
    /// - [`ErrorCode::PermissionDenied`]: This one is pretty self-explanatory.
    /// - [`ErrorCode::UserUnknown`]: The supplied username is not known by this service.
    ///
    /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-acct.html#mwg-pam_sm_acct_mgmt
    fn account_management(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
        Err(ErrorCode::Ignore)
    }

    /// Set credentials on this session.
    ///
    /// If an authentication module knows more about the user than just
    /// their authentication token, then it uses this function to provide
    /// that information to the application. It should only be called after
    /// authentication but before a session is established.
    ///
    /// See [the Module Writer's Guide entry for `pam_sm_setcred`][mwg]
    /// for more information.
    ///
    /// # Valid flags
    ///
    /// This function may be called with the following flags set:
    ///
    /// - [`Flags::SILENT`]
    /// - [`Flags::ESTABLISH_CREDENTIALS`]: Initialize credentials for the user.
    /// - [`Flags::DELETE_CREDENTIALS`]: Delete the credentials associated with this module.
    /// - [`Flags::REINITIALIZE_CREDENTIALS`]: Re-initialize credentials for this user.
    /// - [`Flags::REFRESH_CREDENTIALS`]: Extend the lifetime of the user's credentials.
    ///
    /// # Returns
    ///
    /// If credentials were set successfully, return `Ok(())`.
    ///
    /// Sensible error codes to return include:
    ///
    /// - [`ErrorCode::CredentialsUnavailable`]: The credentials cannot be retrieved.
    /// - [`ErrorCode::CredentialsExpired`]: The credentials have expired.
    /// - [`ErrorCode::CredentialsError`]: Some other error occurred when setting credentials.
    /// - [`ErrorCode::UserUnknown`]: The supplied username is not known by this service.
    ///
    /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-auth.html#mwg-pam_sm_setcred
    fn set_credentials(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
        Err(ErrorCode::Ignore)
    }

    // Function for chauthtok modules.

    /// Called to set or reset the user's authentication token.
    ///
    /// PAM calls this function twice in succession.
    ///  1. The first time, [`Flags::PRELIMINARY_CHECK`] will be set.
    ///     If the new token is acceptable, return success;
    ///     if not, return [`ErrorCode::TryAgain`] to re-prompt the user.
    ///  2. After the preliminary check succeeds, [`Flags::UPDATE_AUTHTOK`]
    ///     will be set. On this call, actually update the stored auth token.
    ///
    /// See [the Module Writer's Guide entry for `pam_sm_chauthtok`][mwg]
    /// for more information.
    ///
    /// # Valid flags
    ///
    /// This function may be called with the following flags set:
    ///
    /// - [`Flags::SILENT`]
    /// - [`Flags::CHANGE_EXPIRED_AUTHTOK`]: This module should only change
    ///   any expired passwords, and leave non-expired passwords alone.
    ///   If present, it _must_ be combined with one of the following.
    /// - [`Flags::PRELIMINARY_CHECK`]: Don't actually change the password,
    ///   just check if the new one is valid.
    /// - [`Flags::UPDATE_AUTHTOK`]: Do actually change the password.
    ///
    /// # Returns
    ///
    /// If the authentication token was changed successfully
    /// (or the check passed), return `Ok(())`.
    ///
    /// Sensible error codes to return include:
    ///
    /// - [`ErrorCode::AuthTokError`]: The service could not get the authentication token.
    /// - [`ErrorCode::AuthTokRecoveryError`]: The service could not get the old token.
    /// - [`ErrorCode::AuthTokLockBusy`]: The password cannot be changed because
    ///   the authentication token is currently locked.
    /// - [`ErrorCode::AuthTokDisableAging`]: Aging (expiration) is disabled.
    /// - [`ErrorCode::PermissionDenied`]: What it says on the tin.
    /// - [`ErrorCode::TryAgain`]: When the preliminary check is unsuccessful,
    ///   ask the user for a new authentication token.
    /// - [`ErrorCode::UserUnknown`]: The supplied username is not known by this service.
    ///
    /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-chauthtok.html#mwg-pam_sm_chauthtok
    fn change_authtok(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
        Err(ErrorCode::Ignore)
    }

    // Functions for session modules.

    /// Called when a session is opened.
    ///
    /// See [the Module Writer's Guide entry for `pam_sm_open_session`][mwg]
    /// for more information.
    ///
    /// # Valid flags
    ///
    /// The only valid flag is [`Flags::SILENT`].
    ///
    /// # Returns
    ///
    /// If the session was opened successfully, return `Ok(())`.
    ///
    /// A sensible error code to return is:
    ///
    /// - [`ErrorCode::SessionError`]: Cannot make an entry for this session.
    ///
    /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-session.html#mwg-pam_sm_open_session
    fn open_session(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
        Err(ErrorCode::Ignore)
    }

    /// Called when a session is being terminated.
    ///
    /// See [the Module Writer's Guide entry for `pam_sm_close_session`][mwg]
    /// for more information.
    ///
    /// # Valid flags
    ///
    /// The only valid flag is [`Flags::SILENT`].
    ///
    /// # Returns
    ///
    /// If the session was closed successfully, return `Ok(())`.
    ///
    /// A sensible error code to return is:
    ///
    /// - [`ErrorCode::SessionError`]: Cannot remove an entry for this session.
    ///
    /// [mwg]: https://www.chiark.greenend.org.uk/doc/libpam-doc/html/mwg-expected-of-module-session.html#mwg-pam_sm_close_session
    fn close_session(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> Result<()> {
        Err(ErrorCode::Ignore)
    }
}

/// Provides methods to make it easier to send exactly one message.
///
/// This is primarily used by PAM modules, so that a module that only needs
/// one piece of information at a time doesn't have a ton of boilerplate.
/// You may also find it useful for testing PAM application libraries.
///
/// ```
/// # use nonstick::Result;
/// # use nonstick::conv::Conversation;
/// # use nonstick::module::ConversationMux;
/// # fn _do_test(conv: impl Conversation) -> Result<()> {
/// let mut mux = ConversationMux(conv);
/// let token = mux.masked_prompt("enter your one-time token")?;
/// # Ok(())
/// # }
pub struct ConversationMux<C: Conversation>(pub C);

impl<C: Conversation> Conversation for ConversationMux<C> {
    fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
        self.0.send(messages)
    }
}

impl<C: Conversation> ConversationMux<C> {
    /// Prompts the user for something.
    pub fn prompt(&mut self, request: &str) -> Result<String> {
        let resp = self.send(&[Message::Prompt(request)])?.pop();
        match resp {
            Some(Response::Text(s)) => Ok(s),
            _ => Err(ErrorCode::ConversationError),
        }
    }

    /// Prompts the user for something, but hides what the user types.
    pub fn masked_prompt(&mut self, request: &str) -> Result<SecureString> {
        let resp = self.send(&[Message::MaskedPrompt(request)])?.pop();
        match resp {
            Some(Response::MaskedText(s)) => Ok(s),
            _ => Err(ErrorCode::ConversationError),
        }
    }

    /// Prompts the user for a yes/no/maybe conditional (a Linux-PAM extension).
    ///
    /// PAM documentation doesn't define the format of the response.
    pub fn radio_prompt(&mut self, request: &str) -> Result<String> {
        let resp = self.send(&[Message::RadioPrompt(request)])?.pop();
        match resp {
            Some(Response::Text(s)) => Ok(s),
            _ => Err(ErrorCode::ConversationError),
        }
    }

    /// Alerts the user to an error.
    pub fn error(&mut self, message: &str) {
        let _ = self.send(&[Message::Error(message)]);
    }

    /// Sends an informational message to the user.
    pub fn info(&mut self, message: &str) {
        let _ = self.send(&[Message::Info(message)]);
    }

    /// Requests binary data from the user (a Linux-PAM extension).
    pub fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData> {
        let resp = self
            .send(&[Message::BinaryPrompt { data, data_type }])?
            .pop();
        match resp {
            Some(Response::Binary(d)) => Ok(d),
            _ => Err(ErrorCode::ConversationError),
        }
    }
}

/// Generates the dynamic library entry points for a [PamModule] implementation.
///
/// Calling `pam_hooks!(SomeType)` on a type that implements [PamModule] will
/// generate the exported `extern "C"` functions that PAM uses to call into
/// your module.
///
/// ## Examples:
///
/// Here is full example of a PAM module that would authenticate and authorize everybody:
///
/// ```no_run
/// use nonstick::{Flags, LibPamHandle, PamModule, PamModuleHandle, Result as PamResult, pam_hooks};
/// use std::ffi::CStr;
/// # fn main() {}
///
/// struct MyPamModule;
/// pam_hooks!(MyPamModule);
///
/// impl<T: PamModuleHandle> PamModule<T> for MyPamModule {
///     fn authenticate(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
///         let password = handle.get_authtok(Some("what's your password?"))?;
///         eprintln!("If you say your password is {:?}, who am I to disagree!", password.unsecure());
///         Ok(())
///     }
///
///     fn account_management(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
///         let username = handle.get_user(None)?;
///         // You should use a Conversation to communicate with the user
///         // instead of writing to the console, but this is just an example.
///         eprintln!("Hello {username}! I trust you unconditionally!");
///         Ok(())
///     }
/// }
/// ```
#[macro_export]
macro_rules! pam_hooks {
    ($ident:ident) => {
        mod _pam_hooks_scope {
            use std::ffi::{c_char, c_int, CStr};
            use $crate::{ErrorCode, Flags, LibPamHandle, PamModule};

            #[no_mangle]
            extern "C" fn pam_sm_acct_mgmt(
                pamh: *mut libc::c_void,
                flags: Flags,
                argc: c_int,
                argv: *const *const c_char,
            ) -> c_int {
                let args = extract_argv(argc, argv);
                ErrorCode::result_to_c(super::$ident::account_management(
                    unsafe { LibPamHandle::from_ptr(pamh) },
                    args,
                    flags,
                ))
            }

            #[no_mangle]
            extern "C" fn pam_sm_authenticate(
                pamh: *mut libc::c_void,
                flags: Flags,
                argc: c_int,
                argv: *const *const c_char,
            ) -> c_int {
                let args = extract_argv(argc, argv);
                ErrorCode::result_to_c(super::$ident::authenticate(
                    unsafe { LibPamHandle::from_ptr(pamh) },
                    args,
                    flags,
                ))
            }

            #[no_mangle]
            extern "C" fn pam_sm_chauthtok(
                pamh: *mut libc::c_void,
                flags: Flags,
                argc: c_int,
                argv: *const *const c_char,
            ) -> c_int {
                let args = extract_argv(argc, argv);
                ErrorCode::result_to_c(super::$ident::change_authtok(
                    unsafe { LibPamHandle::from_ptr(pamh) },
                    args,
                    flags,
                ))
            }

            #[no_mangle]
            extern "C" fn pam_sm_close_session(
                pamh: *mut libc::c_void,
                flags: Flags,
                argc: c_int,
                argv: *const *const c_char,
            ) -> c_int {
                let args = extract_argv(argc, argv);
                ErrorCode::result_to_c(super::$ident::close_session(
                    unsafe { LibPamHandle::from_ptr(pamh) },
                    args,
                    flags,
                ))
            }

            #[no_mangle]
            extern "C" fn pam_sm_open_session(
                pamh: *mut libc::c_void,
                flags: Flags,
                argc: c_int,
                argv: *const *const c_char,
            ) -> c_int {
                let args = extract_argv(argc, argv);
                ErrorCode::result_to_c(super::$ident::open_session(
                    unsafe { LibPamHandle::from_ptr(pamh) },
                    args,
                    flags,
                ))
            }

            #[no_mangle]
            extern "C" fn pam_sm_setcred(
                pamh: *mut libc::c_void,
                flags: Flags,
                argc: c_int,
                argv: *const *const c_char,
            ) -> c_int {
                let args = extract_argv(argc, argv);
                ErrorCode::result_to_c(super::$ident::set_credentials(
                    unsafe { LibPamHandle::from_ptr(pamh) },
                    args,
                    flags,
                ))
            }

            /// Turns `argc`/`argv` into a [Vec] of [CStr]s.
            ///
            /// # Safety
            ///
            /// We use this only with arguments we get from `libpam`, which we kind of have to trust.
            fn extract_argv<'a>(argc: c_int, argv: *const *const c_char) -> Vec<&'a CStr> {
                (0..argc)
                    .map(|o| unsafe { CStr::from_ptr(*argv.offset(o as isize) as *const c_char) })
                    .collect()
            }
        }
    };
}

#[cfg(test)]
mod test {
    use super::{
        Conversation, ConversationMux, ErrorCode, Message, Response, Result, SecureString,
    };

    /// Compile-time test that the `pam_hooks` macro compiles.
    mod hooks {
        use super::super::{PamModule, PamModuleHandle};
        struct Foo;
        impl<T: PamModuleHandle> PamModule<T> for Foo {}

        pam_hooks!(Foo);
    }

    #[test]
    fn test_mux() {
        struct MuxTester;

        impl Conversation for MuxTester {
            fn send(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
                if let [msg] = messages {
                    match msg {
                        Message::Info(info) => {
                            assert_eq!("let me tell you", *info);
                            Ok(vec![Response::NoResponse])
                        }
                        Message::Error(error) => {
                            assert_eq!("oh no", *error);
                            Ok(vec![Response::NoResponse])
                        }
                        Message::Prompt("should_error") => Err(ErrorCode::BufferError),
                        Message::Prompt(ask) => {
                            assert_eq!("question", *ask);
                            Ok(vec![Response::Text("answer".to_owned())])
                        }
                        Message::MaskedPrompt("return_wrong_type") => {
                            Ok(vec![Response::NoResponse])
                        }
                        Message::MaskedPrompt(ask) => {
                            assert_eq!("password!", *ask);
                            Ok(vec![Response::MaskedText(SecureString::from(
                                "open sesame",
                            ))])
                        }
                        Message::BinaryPrompt { data, data_type } => {
                            assert_eq!(&[1, 2, 3], data);
                            assert_eq!(69, *data_type);
                            Ok(vec![Response::Binary(super::BinaryData::new(
                                vec![3, 2, 1],
                                42,
                            ))])
                        }
                        Message::RadioPrompt(ask) => {
                            assert_eq!("radio?", *ask);
                            Ok(vec![Response::Text("yes".to_owned())])
                        }
                    }
                } else {
                    panic!("messages is the wrong size ({len})", len = messages.len())
                }
            }
        }

        let mut mux = ConversationMux(MuxTester);
        assert_eq!("answer", mux.prompt("question").unwrap());
        assert_eq!(
            SecureString::from("open sesame"),
            mux.masked_prompt("password!").unwrap()
        );
        mux.error("oh no");
        mux.info("let me tell you");
        {
            assert_eq!("yes", mux.radio_prompt("radio?").unwrap());
            assert_eq!(
                super::BinaryData::new(vec![3, 2, 1], 42),
                mux.binary_prompt(&[1, 2, 3], 69).unwrap(),
            )
        }
        assert_eq!(
            ErrorCode::BufferError,
            mux.prompt("should_error").unwrap_err(),
        );
        assert_eq!(
            ErrorCode::ConversationError,
            mux.masked_prompt("return_wrong_type").unwrap_err()
        )
    }
}