changeset 163:a75a66cb4181

Add end-to-end tests; fix issues found by tests. - Create tests and installer/remover shell script - Fix Pointer/pointee problems - Add Debug formatting - Misc cleanup
author Paul Fisher <paul@pfish.zone>
date Mon, 14 Jul 2025 17:40:11 -0400
parents 180237d0b498
children d0bba0117456
files libpam-sys/src/lib.rs src/libpam/conversation.rs src/libpam/environ.rs src/libpam/handle.rs src/libpam/items.rs testharness/install-test-harness.sh testharness/nonstick_testharness.conf testharness/src/bin/testharness.rs testharness/src/lib.rs
diffstat 9 files changed, 282 insertions(+), 93 deletions(-) [+]
line wrap: on
line diff
--- a/libpam-sys/src/lib.rs	Mon Jul 14 15:07:16 2025 -0400
+++ b/libpam-sys/src/lib.rs	Mon Jul 14 17:40:11 2025 -0400
@@ -44,6 +44,7 @@
 
 /// Used by PAM to communicate between the module and the application.
 #[repr(C)]
+#[derive(Debug)]
 pub struct pam_conv {
     pub conv: unsafe extern "C" fn(
         num_msg: c_int,
@@ -56,6 +57,7 @@
 
 /// A message sent into a PAM conversation.
 #[repr(C)]
+#[derive(Debug)]
 pub struct pam_message {
     pub msg_style: c_int,
     pub msg: *const c_char,
@@ -63,6 +65,7 @@
 
 /// A response returned from a PAM conversation.
 #[repr(C)]
+#[derive(Debug)]
 pub struct pam_response {
     pub resp: *mut c_char,
     /// Completely unused.
--- a/src/libpam/conversation.rs	Mon Jul 14 15:07:16 2025 -0400
+++ b/src/libpam/conversation.rs	Mon Jul 14 17:40:11 2025 -0400
@@ -69,6 +69,7 @@
 }
 
 /// A conversation owned by a PAM handle and lent to us.
+#[derive(Debug)]
 pub struct PamConv(libpam_sys::pam_conv);
 
 impl Conversation for PamConv {
--- a/src/libpam/environ.rs	Mon Jul 14 15:07:16 2025 -0400
+++ b/src/libpam/environ.rs	Mon Jul 14 17:40:11 2025 -0400
@@ -11,7 +11,7 @@
     fn environ_get(&self, key: &OsStr) -> Option<OsString> {
         let key = CString::new(key.as_bytes()).ok()?;
         // SAFETY: We are a valid handle and are calling with a good key.
-        let src = unsafe { libpam_sys::pam_getenv(self.raw_ref(), key.as_ptr()) };
+        let src = unsafe { libpam_sys::pam_getenv(self.inner(), key.as_ptr()) };
         let val = match NonNull::new(src) {
             None => return None,
             Some(ptr) => ptr.as_ptr(),
@@ -39,14 +39,14 @@
         let put = CString::new(result).unwrap();
         // SAFETY: This is a valid handle and a valid environment string.
         // pam_putenv is only ever going to
-        let _ = unsafe { libpam_sys::pam_putenv(self.raw_mut(), put.as_ptr()) };
+        let _ = unsafe { libpam_sys::pam_putenv(self.inner_mut(), put.as_ptr()) };
         old
     }
 
     fn environ_iter(&self) -> impl Iterator<Item = (OsString, OsString)> {
         // SAFETY: This is a valid PAM handle. It will return valid data.
         unsafe {
-            NonNull::new(libpam_sys::pam_getenvlist(self.raw_ref()))
+            NonNull::new(libpam_sys::pam_getenvlist(self.inner()))
                 .map(|ptr| EnvList::from_ptr(ptr.cast()))
                 .unwrap_or_else(EnvList::empty)
         }
--- a/src/libpam/handle.rs	Mon Jul 14 15:07:16 2025 -0400
+++ b/src/libpam/handle.rs	Mon Jul 14 17:40:11 2025 -0400
@@ -18,12 +18,12 @@
 use std::mem::ManuallyDrop;
 use std::os::unix::ffi::OsStrExt;
 use std::ptr::NonNull;
-use std::{fmt, ptr};
+use std::{any, fmt, ptr};
 
 /// An owned PAM handle.
 pub struct LibPamTransaction<C: Conversation> {
-    /// The handle itself.
-    handle: ManuallyDrop<LibPamHandle>,
+    /// The handle itself.  We guarantee this will not be null.
+    handle: *mut LibPamHandle,
     /// The last return value from the handle.
     last_return: Cell<Result<()>>,
     /// If set, the Conversation that this PAM handle owns.
@@ -36,6 +36,16 @@
     conversation: Box<OwnedConversation<C>>,
 }
 
+impl<C: Conversation> fmt::Debug for LibPamTransaction<C> {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct(any::type_name::<Self>())
+            .field("handle", &format!("{:p}", self.handle))
+            .field("last_return", &self.last_return.get())
+            .field("conversation", &format!("{:p}", self.conversation))
+            .finish()
+    }
+}
+
 #[derive(Debug, PartialEq)]
 pub struct TransactionBuilder {
     service_name: OsString,
@@ -76,35 +86,34 @@
     }
 
     /// Builds the PAM handle and starts the transaction.
-    pub fn build(self, conv: impl Conversation) -> Result<LibPamTransaction<impl Conversation>> {
+    pub fn build<C: Conversation>(self, conv: C) -> Result<LibPamTransaction<C>> {
         LibPamTransaction::start(self.service_name, self.username, conv)
     }
 }
 
 impl<C: Conversation> LibPamTransaction<C> {
     fn start(service_name: OsString, username: Option<OsString>, conversation: C) -> Result<Self> {
-        let conv = Box::new(OwnedConversation::new(conversation));
+        let mut conv = Box::new(OwnedConversation::new(conversation));
         let service_cstr = CString::new(service_name.as_bytes()).expect("null is forbidden");
         let username_cstr = memory::option_cstr_os(username.as_deref());
         let username_cstr = memory::prompt_ptr(username_cstr.as_deref());
 
         let mut handle: *mut libpam_sys::pam_handle = ptr::null_mut();
+        let conv_ptr: *mut OwnedConversation<_> = conv.as_mut() as _;
         // SAFETY: We've set everything up properly to call `pam_start`.
         // The returned value will be a valid pointer provided the result is OK.
         let result = unsafe {
             libpam_sys::pam_start(
                 service_cstr.as_ptr(),
                 username_cstr,
-                (conv.as_ref() as *const OwnedConversation<C>)
-                    .cast_mut()
-                    .cast(),
+                conv_ptr.cast(),
                 &mut handle,
             )
         };
         ErrorCode::result_from(result)?;
         let handle = NonNull::new(handle).ok_or(ErrorCode::BufferError)?;
         Ok(Self {
-            handle: ManuallyDrop::new(LibPamHandle(handle)),
+            handle: handle.as_ptr().cast(),
             last_return: Cell::new(Ok(())),
             conversation: conv,
         })
@@ -139,14 +148,16 @@
     /// Internal "end" function, which binary-ORs the status with `or_with`.
     fn end_internal(&mut self, or_with: i32) {
         let result = ErrorCode::result_to_c(self.last_return.get()) | or_with;
-        unsafe { libpam_sys::pam_end(self.handle.raw_mut(), result) };
+        unsafe { libpam_sys::pam_end(self.handle.cast(), result) };
     }
 }
 
 macro_rules! wrap {
     (fn $name:ident { $pam_func:ident }) => {
         fn $name(&mut self, flags: Flags) -> Result<()> {
-            ErrorCode::result_from(unsafe { libpam_sys::$pam_func(self.0.as_mut(), flags.bits()) })
+            ErrorCode::result_from(unsafe {
+                libpam_sys::$pam_func((self as *mut Self).cast(), flags.bits())
+            })
         }
     };
 }
@@ -160,7 +171,6 @@
 // TODO: pam_setcred - app
 //       pam_open_session - app
 //       pam_close_session - app
-//       pam_set/get_data - module
 
 impl<C: Conversation> Drop for LibPamTransaction<C> {
     /// Closes the PAM session on an owned PAM handle.
@@ -181,14 +191,14 @@
     // First have the kind that save the result after delegation.
     (fn $meth:ident(&self $(, $param:ident: $typ:ty)*) -> Result<$ret:ty>) => {
         fn $meth(&self $(, $param: $typ)*) -> Result<$ret> {
-            let result = self.handle.$meth($($param),*);
+            let result = unsafe { &*self.handle }.$meth($($param),*);
             self.last_return.set(split(&result));
             result
         }
     };
     (fn $meth:ident(&mut self $(, $param:ident: $typ:ty)*) -> Result<$ret:ty>) => {
         fn $meth(&mut self $(, $param: $typ)*) -> Result<$ret> {
-            let result = self.handle.$meth($($param),*);
+            let result = unsafe { &mut *self.handle }.$meth($($param),*);
             self.last_return.set(split(&result));
             result
         }
@@ -196,12 +206,12 @@
     // Then have the kind that are just raw delegates
     (fn $meth:ident(&self $(, $param:ident: $typ:ty)*) -> $ret:ty) => {
         fn $meth(&self $(, $param: $typ)*) -> $ret {
-            self.handle.$meth($($param),*)
+            unsafe { &*self.handle }.$meth($($param),*)
         }
     };
     (fn $meth:ident(&mut self $(, $param:ident: $typ:ty)*) -> $ret:ty) => {
         fn $meth(&mut self $(, $param: $typ)*) -> $ret {
-            self.handle.$meth($($param),*)
+            unsafe { &mut *self.handle }.$meth($($param),*)
         }
     };
     // Then have item getters / setters
@@ -245,22 +255,9 @@
 /// If [`Self::end`] is not called, this will always call `pam_end` reporting
 /// successful completion.
 #[repr(transparent)]
-pub struct LibPamHandle(NonNull<libpam_sys::pam_handle>);
+pub struct LibPamHandle(libpam_sys::pam_handle);
 
 impl LibPamHandle {
-    /// Takes ownership of the pointer to the given PAM handle.
-    ///
-    /// **Do not use this just to get a reference to a PAM handle.**
-    ///
-    /// # Safety
-    ///
-    /// - The pointer must point to a valid PAM handle.
-    /// - The conversation associated with the handle must remain valid
-    ///   for as long as the handle is open.
-    pub unsafe fn from_ptr(handle: NonNull<libpam_sys::pam_handle>) -> Self {
-        Self(handle)
-    }
-
     /// Ends the transaction, reporting `error_code` to cleanup callbacks.
     ///
     /// # References
@@ -268,9 +265,8 @@
     ///
     #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")]
     #[doc = stdlinks!(3 pam_end)]
-    pub fn end(self, result: Result<()>) {
-        let mut me = ManuallyDrop::new(self);
-        unsafe { libpam_sys::pam_end(me.raw_mut(), ErrorCode::result_to_c(result)) };
+    pub fn end(&mut self, result: Result<()>) {
+        unsafe { libpam_sys::pam_end(self.inner_mut(), ErrorCode::result_to_c(result)) };
     }
 
     #[cfg_attr(
@@ -294,35 +290,22 @@
     ///
     #[doc = guide!(adg: "adg-interface-by-app-expected.html#adg-pam_end")]
     #[doc = stdlinks!(3 pam_end)]
-    pub fn end_silent(self, result: Result<()>) {
-        let mut me = ManuallyDrop::new(self);
+    pub fn end_silent(&mut self, result: Result<()>) {
         let result = ErrorCode::result_to_c(result);
         #[cfg(pam_impl = "LinuxPam")]
         let result = result | libpam_sys::PAM_DATA_SILENT;
         unsafe {
-            libpam_sys::pam_end(me.raw_mut(), result);
+            libpam_sys::pam_end(self.inner_mut(), result);
         }
     }
 
-    /// Consumes this and gives you back the raw PAM handle.
-    pub fn into_inner(self) -> NonNull<libpam_sys::pam_handle> {
-        let me = ManuallyDrop::new(self);
-        me.0
-    }
-
     /// Gets a reference to the inner PAM handle.
-    pub fn raw_ref(&self) -> &libpam_sys::pam_handle {
-        unsafe { self.0.as_ref() }
+    pub fn inner(&self) -> &libpam_sys::pam_handle {
+        &self.0
     }
     /// Gets a mutable reference to the inner PAM handle.
-    pub fn raw_mut(&mut self) -> &mut libpam_sys::pam_handle {
-        unsafe { self.0.as_mut() }
-    }
-}
-
-impl Drop for LibPamHandle {
-    fn drop(&mut self) {
-        unsafe { libpam_sys::pam_end(self.0.as_mut(), 0) };
+    pub fn inner_mut(&mut self) -> &mut libpam_sys::pam_handle {
+        &mut self.0
     }
 }
 
@@ -344,12 +327,7 @@
             // SAFETY: We're calling this function with a known value.
             #[cfg(pam_impl = "LinuxPam")]
             unsafe {
-                libpam_sys::pam_syslog(
-                    self.raw_ref(),
-                    level,
-                    b"%s\0".as_ptr().cast(),
-                    entry.as_ptr(),
-                )
+                libpam_sys::pam_syslog(self.inner(), level, b"%s\0".as_ptr().cast(), entry.as_ptr())
             }
             #[cfg(pam_impl = "Sun")]
             unsafe {
@@ -384,7 +362,7 @@
         let mut output: *const c_char = ptr::null();
         let ret = unsafe {
             libpam_sys::pam_get_user(
-                self.raw_mut(),
+                self.inner_mut(),
                 &mut output,
                 memory::prompt_ptr(prompt.as_deref()),
             )
@@ -440,7 +418,7 @@
         let mut ptr: *const c_void = ptr::null();
         unsafe {
             ErrorCode::result_from(libpam_sys::pam_get_data(
-                self.raw_ref(),
+                self.inner(),
                 full_key.as_ptr(),
                 &mut ptr,
             ))
@@ -455,7 +433,7 @@
         let data = Box::new(data);
         ErrorCode::result_from(unsafe {
             libpam_sys::pam_set_data(
-                self.raw_mut(),
+                self.inner_mut(),
                 full_key.as_ptr(),
                 Box::into_raw(data).cast(),
                 drop_module_data::<T>,
@@ -504,7 +482,7 @@
         // SAFETY: We're calling this with known-good values.
         let res = unsafe {
             libpam_sys::pam_get_authtok(
-                self.raw_mut(),
+                self.inner_mut(),
                 item_type.into(),
                 &mut output,
                 memory::prompt_ptr(prompt.as_deref()),
@@ -525,7 +503,7 @@
         let mut output: *mut c_char = ptr::null_mut();
         let result = unsafe {
             libpam_sys::__pam_get_authtok(
-                self.raw_mut(),
+                self.inner_mut(),
                 libpam_sys::PAM_HANDLE,
                 item_type.into(),
                 ptr::null(),
@@ -543,7 +521,7 @@
         let prompt = memory::option_cstr_os(prompt);
         let result = unsafe {
             libpam_sys::__pam_get_authtok(
-                self.raw_mut(),
+                self.inner_mut(),
                 libpam_sys::PAM_PROMPT,
                 item_type.into(),
                 memory::prompt_ptr(prompt.as_deref()),
@@ -559,15 +537,12 @@
 
     /// Gets the `PAM_CONV` item from the handle.
     fn conversation_item(&self) -> Result<&PamConv> {
-        let output: *const PamConv = ptr::null_mut();
+        let mut output: *const c_void = ptr::null();
         let result = unsafe {
-            libpam_sys::pam_get_item(
-                self.raw_ref(),
-                ItemType::Conversation.into(),
-                &mut output.cast(),
-            )
+            libpam_sys::pam_get_item(self.inner(), ItemType::Conversation.into(), &mut output)
         };
         ErrorCode::result_from(result)?;
+        let output: *const PamConv = output.cast();
         // SAFETY: We got this result from PAM, and we're checking if it's null.
         unsafe { output.as_ref() }.ok_or(ErrorCode::ConversationError)
     }
--- a/src/libpam/items.rs	Mon Jul 14 15:07:16 2025 -0400
+++ b/src/libpam/items.rs	Mon Jul 14 17:40:11 2025 -0400
@@ -58,12 +58,9 @@
 /// # Safety
 ///
 /// You better be requesting an item which is a C string.
-pub unsafe fn get_cstr_item(
-    hdl: &LibPamHandle,
-    item_type: ItemType,
-) -> crate::Result<Option<OsString>> {
+pub unsafe fn get_cstr_item(hdl: &LibPamHandle, item_type: ItemType) -> Result<Option<OsString>> {
     let mut output = ptr::null();
-    let ret = unsafe { libpam_sys::pam_get_item(hdl.raw_ref(), item_type as c_int, &mut output) };
+    let ret = unsafe { libpam_sys::pam_get_item(hdl.inner(), item_type as c_int, &mut output) };
     ErrorCode::result_from(ret)?;
     Ok(memory::copy_pam_string(output.cast()))
 }
@@ -77,11 +74,11 @@
     hdl: &mut LibPamHandle,
     item_type: ItemType,
     data: Option<&OsStr>,
-) -> crate::Result<()> {
+) -> Result<()> {
     let data_str = memory::option_cstr_os(data);
     let ret = unsafe {
         libpam_sys::pam_set_item(
-            hdl.raw_mut(),
+            hdl.inner_mut(),
             item_type as c_int,
             memory::prompt_ptr(data_str.as_deref()).cast(),
         )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/testharness/install-test-harness.sh	Mon Jul 14 17:40:11 2025 -0400
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+set -eo pipefail
+
+HERE="$(dirname -- "$0")"
+echo "$HERE"
+cargo build --release
+
+sudo mkdir -p /lib/security
+sudo cp ../target/release/libnonstick_testharness.so /lib/security/pam_testharness.so
+sudo cp nonstick_testharness.conf /etc/pam.d/nonstick-testharness
+trap 'sudo rm /etc/pam.d/nonstick-testharness; sudo rm /lib/security/pam_testharness.so' EXIT
+"$@" && echo "SUCCESS!!!" || echo "FAILURE: $?"
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/testharness/nonstick_testharness.conf	Mon Jul 14 17:40:11 2025 -0400
@@ -0,0 +1,5 @@
+# PAM configuration file for nonstick_testharness
+auth required pam_testharness.so param param2
+account required pam_testharness.so
+password required pam_testharness.so
+session required pam_testharness.so
--- a/testharness/src/bin/testharness.rs	Mon Jul 14 15:07:16 2025 -0400
+++ b/testharness/src/bin/testharness.rs	Mon Jul 14 17:40:11 2025 -0400
@@ -1,3 +1,121 @@
 //! The actual program which runs the tests.
 
-fn main() {}
+use nonstick::conv::Exchange;
+use nonstick::items::Items;
+use nonstick::libpam::TransactionBuilder;
+use nonstick::{Conversation, ErrorCode, Flags, LibPamTransaction, PamShared, Transaction};
+use std::cell::Cell;
+use std::ffi::OsString;
+use std::os::unix::ffi::OsStrExt;
+
+fn main() {
+    test_wrong_user();
+    test_wrong_password();
+    test_correct();
+}
+
+#[derive(Debug, Default)]
+struct TestHarness {
+    username_requested: Cell<bool>,
+    wrong_username: bool,
+    wrong_password: bool,
+    changing_password: Cell<bool>,
+    change_prompt_count: Cell<u8>,
+}
+
+impl Conversation for &TestHarness {
+    fn communicate(&self, messages: &[Exchange]) {
+        if let [only_msg] = messages {
+            match only_msg {
+                Exchange::Prompt(p) => {
+                    if self.username_requested.get() {
+                        panic!("username already requested!")
+                    }
+                    if self.wrong_username {
+                        p.set_answer(Ok(OsString::from("not-right")))
+                    } else {
+                        p.set_answer(Ok(OsString::from("initial")))
+                    }
+                    self.username_requested.set(true)
+                }
+                Exchange::MaskedPrompt(p) => {
+                    let answer = if self.changing_password.get() {
+                        let prompts = self.change_prompt_count.get();
+                        self.change_prompt_count.set(prompts + 1);
+                        match prompts {
+                            0 => "mistake",
+                            1 => "mismatch",
+                            2 => "acceptable",
+                            3 => "acceptable",
+                            _ => panic!("unexpected number of prompts!"),
+                        }
+                    } else if self.wrong_password {
+                        "bogus"
+                    } else {
+                        "valid"
+                    };
+                    p.set_answer(Ok(OsString::from(answer)));
+                }
+                Exchange::Error(e) if self.changing_password.get() => e.set_answer(Ok(())),
+                other => panic!("Unknown message {other:?}!"),
+            }
+        } else {
+            for msg in messages {
+                match msg {
+                    Exchange::Info(i) => i.set_answer(Ok(())),
+                    Exchange::Error(e) => e.set_answer(Ok(())),
+                    Exchange::Prompt(p) => match p.question().as_bytes() {
+                        b"How many?" => p.set_answer(Ok(OsString::from("123"))),
+                        _ => p.set_answer(Err(ErrorCode::ConversationError)),
+                    },
+                    Exchange::MaskedPrompt(p) => match p.question().as_bytes() {
+                        b"Where?" => p.set_answer(Ok(OsString::from("abc"))),
+                        _ => p.set_answer(Err(ErrorCode::ConversationError)),
+                    },
+                    other => other.set_error(ErrorCode::Abort),
+                }
+            }
+        }
+    }
+}
+
+impl TestHarness {
+    fn start(&self) -> LibPamTransaction<&Self> {
+        TransactionBuilder::new_with_service("nonstick-testharness")
+            .build(self)
+            .expect("expected build success")
+    }
+}
+
+fn test_wrong_user() {
+    let harness = TestHarness {
+        wrong_username: true,
+        ..Default::default()
+    };
+    let mut tx = harness.start();
+    let auth = tx.authenticate(Flags::empty());
+    assert_eq!(auth, Err(ErrorCode::UserUnknown));
+}
+
+fn test_wrong_password() {
+    let harness = TestHarness {
+        wrong_password: true,
+        ..Default::default()
+    };
+    let mut tx = harness.start();
+    let auth = tx.authenticate(Flags::empty());
+    assert_eq!(auth, Err(ErrorCode::AuthenticationError));
+}
+
+fn test_correct() {
+    let harness = TestHarness::default();
+    let mut tx = harness.start();
+    tx.authenticate(Flags::empty()).unwrap();
+    assert_eq!(tx.items().user().unwrap().unwrap(), "updated-in-process");
+    let result = tx.account_management(Flags::empty());
+    assert_eq!(result, Err(ErrorCode::NewAuthTokRequired));
+    harness.changing_password.set(true);
+    let change = tx.change_authtok(Flags::CHANGE_EXPIRED_AUTHTOK);
+    assert_eq!(change, Err(ErrorCode::TryAgain));
+    tx.change_authtok(Flags::CHANGE_EXPIRED_AUTHTOK).unwrap();
+}
--- a/testharness/src/lib.rs	Mon Jul 14 15:07:16 2025 -0400
+++ b/testharness/src/lib.rs	Mon Jul 14 17:40:11 2025 -0400
@@ -1,26 +1,103 @@
 //! The nonstick library
+
+use crate::nonstick::items::ItemsMut;
+use std::cell::Cell;
 extern crate nonstick;
 
-use nonstick::{pam_hooks, Flags, ModuleClient, PamModule};
-use std::ffi::CStr;
+use nonstick::conv::{ErrorMsg, InfoMsg, MaskedQAndA, QAndA};
+use nonstick::{error, info, pam_hooks, ErrorCode, Flags, ModuleClient, PamModule};
+use std::ffi::{CStr, OsString};
+use std::os::unix::ffi::OsStrExt;
 
 struct TestHarness;
 
 impl<M: ModuleClient> PamModule<M> for TestHarness {
-    fn authenticate(_handle: &mut M, _args: Vec<&CStr>, _flags: Flags) -> nonstick::Result<()> {
-        Ok(())
+    fn authenticate(handle: &mut M, args: Vec<&CStr>, _: Flags) -> nonstick::Result<()> {
+        let strings: Vec<_> = args.iter().map(|&a| Vec::from(a.to_bytes())).collect();
+        if strings != vec![Vec::from(b"param"), Vec::from(b"param2")] {
+            return Err(ErrorCode::SystemError);
+        }
+        let username = handle.username(None)?;
+        if username != "initial" {
+            return Err(ErrorCode::UserUnknown);
+        }
+        handle
+            .items_mut()
+            .set_user(Some("updated-in-process".as_ref()))?;
+        handle.set_module_data("florgus", Cell::new(99))?;
+        let authtok = handle.authtok(Some("custom".as_ref()))?;
+        if authtok.as_bytes() != b"valid" {
+            return Err(ErrorCode::AuthenticationError);
+        }
+        let info = InfoMsg::new("Watch out!".as_ref());
+        let err = ErrorMsg::new("It's broken!".as_ref());
+        let public = QAndA::new("How many?".as_ref());
+        let private = MaskedQAndA::new("Where?".as_ref());
+        let msgs = &[
+            info.exchange(),
+            err.exchange(),
+            public.exchange(),
+            private.exchange(),
+        ];
+        handle.communicate(msgs);
+        let public = public.answer()?;
+        info!(handle, "public question: {:?}", public);
+        let private = private.answer()?;
+        info!(handle, "private question: {:?}", private);
+        if public.as_bytes() == b"123" && private.as_bytes() == b"abc" {
+            Ok(())
+        } else {
+            Err(ErrorCode::Abort)
+        }
     }
 
-    fn account_management(
-        _handle: &mut M,
-        _args: Vec<&CStr>,
-        _flags: Flags,
-    ) -> nonstick::Result<()> {
-        Ok(())
+    fn account_management(handle: &mut M, _: Vec<&CStr>, _: Flags) -> nonstick::Result<()> {
+        let value: &Cell<i32> = match handle.username(None)?.as_bytes() {
+            b"initial" => return Err(ErrorCode::AccountExpired),
+            b"updated-in-process" => handle.get_module_data("florgus"),
+            _ => return Err(ErrorCode::UserUnknown),
+        }
+        .ok_or(ErrorCode::SessionError)?;
+        let florgus_str: Option<&i32> = handle.get_module_data("florgus");
+        if let Some(s) = florgus_str {
+            error!(
+                handle,
+                "module_data type mismatch: florgus = <{s}> but should not be set"
+            )
+        }
+        if value.get() != 99 {
+            error!(handle, "wrong value! {}", value.get());
+            return Err(ErrorCode::AuthTokError);
+        }
+        let password = handle.authtok(None)?;
+        if password.as_bytes() == b"valid" {
+            Err(ErrorCode::NewAuthTokRequired)
+        } else {
+            Ok(())
+        }
     }
 
-    fn change_authtok(_handle: &mut M, _args: Vec<&CStr>, _flags: Flags) -> nonstick::Result<()> {
-        todo!()
+    fn change_authtok(handle: &mut M, _: Vec<&CStr>, flags: Flags) -> nonstick::Result<()> {
+        if flags.contains(Flags::PRELIMINARY_CHECK) {
+            let password = handle.authtok(None)?;
+            if password.as_bytes() != b"acceptable" {
+                return Err(ErrorCode::PermissionDenied);
+            }
+            handle.set_module_data("checked_pass", password)
+        } else if flags.contains(Flags::UPDATE_AUTHTOK) {
+            let password = handle.authtok(None)?;
+            let checked: &OsString = handle
+                .get_module_data("checked_pass")
+                .ok_or(ErrorCode::SystemError)?;
+            if password != *checked {
+                error!(handle, "password mismatch? {password:?} {checked:?}");
+                return Err(ErrorCode::AuthenticationError);
+            }
+            Ok(())
+        } else {
+            error!(handle, "invalid flag state: {flags:?}");
+            Err(ErrorCode::SystemError)
+        }
     }
 }