Index: TODO
===================================================================
RCS file: /cvs/openssh/TODO,v
retrieving revision 1.51
diff -u -r1.51 TODO
--- TODO	5 Sep 2002 06:32:03 -0000	1.51
+++ TODO	9 Jan 2003 10:44:00 -0000
@@ -15,8 +15,6 @@
 
 - Replacement for setproctitle() - HP-UX support only currently
 
-- Handle changing passwords for the non-PAM expired password case
-
 - Improve PAM support (a pam_lastlog module will cause sshd to exit)
   and maybe support alternate forms of authentications like OPIE via
   pam?
Index: acconfig.h
===================================================================
RCS file: /cvs/openssh/acconfig.h,v
retrieving revision 1.146
diff -u -r1.146 acconfig.h
--- acconfig.h	7 Jan 2003 04:18:33 -0000	1.146
+++ acconfig.h	9 Jan 2003 10:44:01 -0000
@@ -25,6 +25,9 @@
 /* from environment and PATH */
 #undef LOGIN_PROGRAM_FALLBACK
 
+/* Path to passwd program */
+#undef PASSWD_PROGRAM_PATH
+
 /* Define if your password has a pw_class field */
 #undef HAVE_PW_CLASS_IN_PASSWD
 
Index: auth-pam.c
===================================================================
RCS file: /cvs/openssh/auth-pam.c,v
retrieving revision 1.54
diff -u -r1.54 auth-pam.c
--- auth-pam.c	28 Jul 2002 20:24:08 -0000	1.54
+++ auth-pam.c	9 Jan 2003 10:44:01 -0000
@@ -60,7 +60,7 @@
 /* states for do_pam_conversation() */
 enum { INITIAL_LOGIN, OTHER } pamstate = INITIAL_LOGIN;
 /* remember whether pam_acct_mgmt() returned PAM_NEW_AUTHTOK_REQD */
-static int password_change_required = 0;
+extern int password_change_required;
 /* remember whether the last pam_authenticate() succeeded or not */
 static int was_authenticated = 0;
 
Index: auth-passwd.c
===================================================================
RCS file: /cvs/openssh/auth-passwd.c,v
retrieving revision 1.48
diff -u -r1.48 auth-passwd.c
--- auth-passwd.c	25 Sep 2002 23:14:16 -0000	1.48
+++ auth-passwd.c	9 Jan 2003 10:44:01 -0000
@@ -42,6 +42,9 @@
 #include "log.h"
 #include "servconf.h"
 #include "auth.h"
+#include "buffer.h"
+#include "misc.h"
+#include "monitor_wrap.h"
 
 #if !defined(USE_PAM) && !defined(HAVE_OSF_SIA)
 /* Don't need any of these headers for the PAM or SIA cases */
@@ -81,9 +84,8 @@
 #endif /* !USE_PAM && !HAVE_OSF_SIA */
 
 extern ServerOptions options;
-#ifdef WITH_AIXAUTHENTICATE
-extern char *aixloginmsg;
-#endif
+extern Buffer login_message;
+extern int password_change_required;
 
 /*
  * Tries to authenticate the user using password.  Returns true if
@@ -123,6 +125,7 @@
 	/* deny if no user. */
 	if (pw == NULL)
 		return 0;
+	buffer_init(&login_message);
 #ifndef HAVE_CYGWIN
        if (pw->pw_uid == 0 && options.permit_root_login != PERMIT_YES)
 		return 0;
@@ -149,13 +152,25 @@
 #endif
 #ifdef WITH_AIXAUTHENTICATE
 	authsuccess = (authenticate(pw->pw_name,password,&reenter,&authmsg) == 0);
+	aix_remove_embedded_newlines(authmsg);
+
+	if (authsuccess) {
+		char *msg;
 
-	if (authsuccess)
+		debug("authenticate() succeeded for user %s: %.100s",
+		    pw->pw_name, authmsg);
 	        /* We don't have a pty yet, so just label the line as "ssh" */
 	        if (loginsuccess(authctxt->user,
-			get_canonical_hostname(options.verify_reverse_mapping),
-			"ssh", &aixloginmsg) < 0)
-				aixloginmsg = NULL;
+		    get_canonical_hostname(options.verify_reverse_mapping),
+		    "ssh", &msg) < 0)
+			msg = NULL;
+		buffer_append(&login_message, msg, strlen(msg));
+	} else {
+		debug("AIX authenticate() failed for user %s: %.100s",
+		    pw->pw_name, authmsg);
+	}
+	if (authmsg)
+		xfree(authmsg);
 
 	return(authsuccess);
 #endif
@@ -232,4 +247,53 @@
 	/* Authentication is accepted if the encrypted passwords are identical. */
 	return (strcmp(encrypted_password, pw_password) == 0);
 #endif /* !USE_PAM && !HAVE_OSF_SIA */
+}
+
+/*
+ * Perform generic password change via tty. Like do_pam_chauthtok(),
+ * it throws a fatal error if the password can't be changed.
+ */
+int
+do_tty_change_password(struct passwd *pw)
+{
+	pid_t pid;
+	int status;
+	mysig_t old_signal;
+
+	old_signal = mysignal(SIGCHLD, SIG_DFL);
+
+	if ((pid = fork()) == -1)
+		fatal("Couldn't fork: %s", strerror(errno));
+
+	if (pid == 0) {
+		setuid(pw->pw_uid);
+		if (geteuid() == 0) 
+			execl(PASSWD_PROGRAM_PATH, "passwd", pw->pw_name,
+			    (char *)NULL);
+		else
+			execl(PASSWD_PROGRAM_PATH, "passwd", (char *)NULL);
+			
+		/* execl shouldn't return */
+		fatal("Couldn't exec %s", PASSWD_PROGRAM_PATH);
+		exit(1);
+	}
+
+	if (waitpid(pid, &status, 0) == -1)
+		fatal("Couldn't wait for child: %s", strerror(errno));
+	mysignal(SIGCHLD, old_signal);
+
+	/*
+	 * passwd sometimes returns 0 when the password has not been changed
+	 * so we re-test via getpwnamallow
+	 */
+	if (WIFEXITED(status) && (WEXITSTATUS(status) == 0) &&
+	    PRIVSEP(getpwnamallow(pw->pw_name)) != NULL) {
+		debug("password changed sucessfully");
+		password_change_required = 0;
+		return 1;
+	} else {
+		fatal("Failed to change password for %s, passwd returned %d",
+		    pw->pw_name, status);
+		return 0;
+	}
 }
Index: auth.c
===================================================================
RCS file: /cvs/openssh/auth.c,v
retrieving revision 1.66
diff -u -r1.66 auth.c
--- auth.c	9 Jan 2003 04:04:28 -0000	1.66
+++ auth.c	9 Jan 2003 10:44:02 -0000
@@ -58,6 +58,8 @@
 /* Debugging messages */
 Buffer auth_debug;
 int auth_debug_init;
+extern int password_change_required;
+extern Buffer expire_message;
 
 /*
  * Check if the user is allowed to log in via ssh. If user is listed
@@ -75,9 +77,6 @@
 	const char *hostname = NULL, *ipaddr = NULL, *passwd;
 	char *shell;
 	int i;
-#ifdef WITH_AIXAUTHENTICATE
-	char *loginmsg;
-#endif /* WITH_AIXAUTHENTICATE */
 #if defined(HAVE_SHADOW_H) && !defined(DISABLE_SHADOW)
 	struct spwd *spw;
 #if !defined(USE_PAM) && defined(HAS_SHADOW_EXPIRE)
@@ -99,12 +98,15 @@
 	passwd = pw->pw_passwd;
 #endif
 
+	debug("%s: before locked password check", __func__);
+
 	/* check for locked account */
 	if (strcmp(passwd, "*LK*") == 0 || passwd[0] == '!') {
 		log("User %.100s not allowed because account is locked",
 		    pw->pw_name);
 		return 0;
 	}
+	debug("%s: after locked password check", __func__);
 
 #if !defined(USE_PAM) && defined(HAVE_SHADOW_H) && \
     !defined(DISABLE_SHADOW) && defined(HAS_SHADOW_EXPIRE)
@@ -222,27 +224,58 @@
 	 * PermitRootLogin to control logins via ssh), or if running as
 	 * non-root user (since loginrestrictions will always fail).
 	 */
-	if ( (pw->pw_uid != 0) && (geteuid() == 0) &&
-	    loginrestrictions(pw->pw_name, S_RLOGIN, NULL, &loginmsg) != 0) {
+	if ( (pw->pw_uid != 0) && (geteuid() == 0) ) {
 		int loginrestrict_errno = errno;
+		char *msg;
 
-		if (loginmsg && *loginmsg) {
-			/* Remove embedded newlines (if any) */
-			char *p;
-			for (p = loginmsg; *p; p++) {
-				if (*p == '\n')
-					*p = ' ';
+		/* check for AIX account restrictions */
+		if (loginrestrictions(pw->pw_name, S_RLOGIN, NULL, &msg) != 0) {
+			if (msg && *msg) {
+				aix_remove_embedded_newlines(msg);
+				log("Login restricted for %s: %.100s",
+				    pw->pw_name, msg);
+				xfree(msg);
 			}
-			/* Remove trailing newline */
-			*--p = '\0';
-			log("Login restricted for %s: %.100s", pw->pw_name, 
-			    loginmsg);
-		}
-		/* Don't fail if /etc/nologin  set */
-	    	if (!(loginrestrict_errno == EPERM && 
-		    stat(_PATH_NOLOGIN, &st) == 0))
-			return 0;
+
+			/* Don't fail if /etc/nologin  set */
+	   	 	if (!(loginrestrict_errno == EPERM && 
+			    stat(_PATH_NOLOGIN, &st) == 0))
+				return 0;
+		}
 	}
+
+	/*
+	 * Check AIX password expiry.  Only check when running as root.
+	 * Unpriv'ed users can't access /etc/security/passwd or
+	 * /etc/security/user so passwdexpired will always fail.
+	 */
+	if (geteuid() == 0) {
+		char *msg;
+		int passexpcode;
+
+		passexpcode = passwdexpired(pw->pw_name, &msg);
+		buffer_append(&expire_message, msg, strlen(msg));
+		if (msg && *msg)
+			aix_remove_embedded_newlines(msg);
+		debug("AIX passwdexpired returned %d, msg %.100s",
+		    passexpcode, msg);
+
+		switch (passexpcode) {
+			case 0:	/* success, password not expired */
+				break;
+			case 1:	/* expired, password change required */
+				password_change_required = 1;
+				break;
+			default: /* only admin can change (2) or other error (-1) */
+				log("Password can't be changed for user %s: %.100s",
+				    pw->pw_name, msg);
+				if (msg)
+					xfree(msg);
+				return 0;
+  		}
+		if (msg)
+			xfree(msg);
+  	}
 #endif /* WITH_AIXAUTHENTICATE */
 
 	/* We found no reason not to let this user try to log on... */
@@ -508,6 +541,7 @@
 #endif
 	struct passwd *pw;
 
+	buffer_init(&expire_message);
 	pw = getpwnam(user);
 	if (pw == NULL) {
 		log("Illegal user %.100s from %.100s",
Index: auth.h
===================================================================
RCS file: /cvs/openssh/auth.h,v
retrieving revision 1.44
diff -u -r1.44 auth.h
--- auth.h	27 Sep 2002 03:26:01 -0000	1.44
+++ auth.h	9 Jan 2003 10:44:02 -0000
@@ -101,6 +101,7 @@
 
 int	 auth_rhosts_rsa(struct passwd *, char *, Key *);
 int      auth_password(Authctxt *, const char *);
+int	 do_tty_change_password(struct passwd *pw);
 int      auth_rsa(struct passwd *, BIGNUM *);
 int      auth_rsa_challenge_dialog(Key *);
 BIGNUM	*auth_rsa_generate_challenge(Key *);
Index: configure.ac
===================================================================
RCS file: /cvs/openssh/configure.ac,v
retrieving revision 1.97
diff -u -r1.97 configure.ac
--- configure.ac	9 Jan 2003 01:22:59 -0000	1.97
+++ configure.ac	9 Jan 2003 10:44:03 -0000
@@ -41,6 +41,13 @@
 	fi
 fi
 
+AC_PATH_PROG(PASSWD_PROGRAM_PATH, passwd)
+if test ! -z "$PASSWD_PROGRAM_PATH" ; then
+	AC_DEFINE_UNQUOTED(PASSWD_PROGRAM_PATH, "$PASSWD_PROGRAM_PATH")
+else
+	AC_MSG_ERROR([*** passwd command not found - check config.log ***])
+fi
+
 if test -z "$LD" ; then
 	LD=$CC
 fi
Index: servconf.c
===================================================================
RCS file: /cvs/openssh/servconf.c,v
retrieving revision 1.97
diff -u -r1.97 servconf.c
--- servconf.c	5 Sep 2002 04:35:15 -0000	1.97
+++ servconf.c	9 Jan 2003 10:44:04 -0000
@@ -123,6 +123,8 @@
 	options->client_alive_count_max = -1;
 	options->authorized_keys_file = NULL;
 	options->authorized_keys_file2 = NULL;
+	options->passwd_expire_warn_days = -1;
+	options->forced_passwd_change = -1;
 
 	/* Needs to be accessable in many places */
 	use_privsep = -1;
@@ -255,6 +257,10 @@
 	}
 	if (options->authorized_keys_file == NULL)
 		options->authorized_keys_file = _PATH_SSH_USER_PERMITTED_KEYS;
+	if (options->passwd_expire_warn_days == -1)
+		options->passwd_expire_warn_days = 14;
+	if (options->forced_passwd_change == -1)
+		options->forced_passwd_change = 1;
 
 	/* Turn privilege separation on by default */
 	if (use_privsep == -1)
@@ -302,6 +308,7 @@
 	sHostbasedUsesNameFromPacketOnly, sClientAliveInterval,
 	sClientAliveCountMax, sAuthorizedKeysFile, sAuthorizedKeysFile2,
 	sUsePrivilegeSeparation,
+	sPasswordExpireWarningDays, sForcedPasswdChange,
 	sDeprecated
 } ServerOpCodes;
 
@@ -380,6 +387,8 @@
 	{ "authorizedkeysfile", sAuthorizedKeysFile },
 	{ "authorizedkeysfile2", sAuthorizedKeysFile2 },
 	{ "useprivilegeseparation", sUsePrivilegeSeparation},
+	{ "passwordexpirewarningdays", sPasswordExpireWarningDays},
+	{ "forcedpasswdchange", sForcedPasswdChange},
 	{ NULL, sBadOption }
 };
 
@@ -908,6 +917,14 @@
 	case sClientAliveCountMax:
 		intptr = &options->client_alive_count_max;
 		goto parse_int;
+
+        case sPasswordExpireWarningDays:
+                intptr = &options->passwd_expire_warn_days;
+                goto parse_int;
+
+        case sForcedPasswdChange:
+                intptr = &options->forced_passwd_change;
+                goto parse_flag;
 
 	case sDeprecated:
 		log("%s line %d: Deprecated option %s",
Index: servconf.h
===================================================================
RCS file: /cvs/openssh/servconf.h,v
retrieving revision 1.50
diff -u -r1.50 servconf.h
--- servconf.h	1 Aug 2002 01:28:39 -0000	1.50
+++ servconf.h	9 Jan 2003 10:44:04 -0000
@@ -132,6 +132,9 @@
 	char   *authorized_keys_file;	/* File containing public keys */
 	char   *authorized_keys_file2;
 	int	pam_authentication_via_kbd_int;
+	int	passwd_expire_warn_days;
+	int	acct_expire_warn_days;
+	int	forced_passwd_change;
 }       ServerOptions;
 
 void	 initialize_server_options(ServerOptions *);
Index: session.c
===================================================================
RCS file: /cvs/openssh/session.c,v
retrieving revision 1.227
diff -u -r1.227 session.c
--- session.c	7 Jan 2003 06:38:59 -0000	1.227
+++ session.c	9 Jan 2003 10:44:05 -0000
@@ -102,10 +102,9 @@
 /* data */
 #define MAX_SESSIONS 10
 Session	sessions[MAX_SESSIONS];
-
-#ifdef WITH_AIXAUTHENTICATE
-char *aixloginmsg;
-#endif /* WITH_AIXAUTHENTICATE */
+Buffer expire_message;	/* "password will expire/has expired" messages */
+Buffer login_message;	/* message to be displayed after login */
+int password_change_required = 0;
 
 #ifdef HAVE_LOGIN_CAP
 login_cap_t *lc;
@@ -456,10 +455,11 @@
 #if defined(USE_PAM)
 	do_pam_session(s->pw->pw_name, NULL);
 	do_pam_setcred(1);
-	if (is_pam_password_change_required())
+#endif /* USE_PAM */
+
+	if (password_change_required)
 		packet_disconnect("Password change required but no "
 		    "TTY available");
-#endif /* USE_PAM */
 
 	/* Fork the child. */
 	if ((pid = fork()) == 0) {
@@ -723,6 +723,7 @@
 	socklen_t fromlen;
 	struct sockaddr_storage from;
 	struct passwd * pw = s->pw;
+	int password_changed = 0;
 	pid_t pid = getpid();
 
 	/*
@@ -746,16 +747,23 @@
 		    options.verify_reverse_mapping),
 		    (struct sockaddr *)&from, fromlen);
 
-#ifdef USE_PAM
 	/*
 	 * If password change is needed, do it now.
 	 * This needs to occur before the ~/.hushlogin check.
 	 */
+#ifdef USE_PAM
 	if (is_pam_password_change_required()) {
 		print_pam_messages();
-		do_pam_chauthtok();
+		if (!use_privsep)
+			do_pam_chauthtok();
 	}
 #endif
+	buffer_append(&expire_message, "\0", 1);
+	if (options.forced_passwd_change && password_change_required) {
+		puts((char *)buffer_ptr(&expire_message));
+		do_tty_change_password(pw);
+		password_changed = 1;
+	}
 
 	if (check_quietlogin(s, command))
 		return;
@@ -764,10 +772,12 @@
 	if (!is_pam_password_change_required())
 		print_pam_messages();
 #endif /* USE_PAM */
-#ifdef WITH_AIXAUTHENTICATE
-	if (aixloginmsg && *aixloginmsg)
-		printf("%s\n", aixloginmsg);
-#endif /* WITH_AIXAUTHENTICATE */
+	if (!password_changed)
+		printf("%s", (char *)buffer_ptr(&expire_message));
+
+	/* display post-login message */
+	buffer_append(&login_message, "\0", 1);
+	puts((char *)buffer_ptr(&login_message));
 
 #ifndef NO_SSH_LASTLOG
 	if (options.print_lastlog && s->last_login_time != 0) {
Index: sshd_config
===================================================================
RCS file: /cvs/openssh/sshd_config,v
retrieving revision 1.55
diff -u -r1.55 sshd_config
--- sshd_config	27 Sep 2002 03:21:58 -0000	1.55
+++ sshd_config	9 Jan 2003 10:44:05 -0000
@@ -55,6 +55,8 @@
 # To disable tunneled clear text passwords, change to no here!
 #PasswordAuthentication yes
 #PermitEmptyPasswords no
+#ForcedPasswdChange yes
+#PasswordExpireWarningDays 14
 
 # Change to no to disable s/key passwords
 #ChallengeResponseAuthentication yes
Index: sshd_config.5
===================================================================
RCS file: /cvs/openssh/sshd_config.5,v
retrieving revision 1.12
diff -u -r1.12 sshd_config.5
--- sshd_config.5	19 Sep 2002 01:51:22 -0000	1.12
+++ sshd_config.5	9 Jan 2003 10:44:06 -0000
@@ -206,6 +206,10 @@
 If the pattern takes the form USER@HOST then USER and HOST
 are separately checked, restricting logins to particular
 users from particular hosts.
+.It Cm ForcedPasswdChange
+Specifies whether or not a password change will be forced when the
+password is expired.  The default is
+.Dq yes .
 .It Cm GatewayPorts
 Specifies whether remote hosts are allowed to connect to ports
 forwarded for the client.
@@ -431,6 +435,12 @@
 Specifies whether password authentication is allowed.
 The default is
 .Dq yes .
+.It Cm PasswordExpireWarningDays
+If the password will expire in less than the specified number of days, a
+warning will be printed at login.  Note that on some platforms (eg AIX)
+this specified by the operating system and this parameter is ignored.
+The default is
+.Dq 14 .
 .It Cm PermitEmptyPasswords
 When password authentication is allowed, it specifies whether the
 server allows login to accounts with empty password strings.
Index: openbsd-compat/port-aix.c
===================================================================
RCS file: /cvs/openssh/openbsd-compat/port-aix.c,v
retrieving revision 1.6
diff -u -r1.6 port-aix.c
--- openbsd-compat/port-aix.c	7 Jul 2002 02:17:36 -0000	1.6
+++ openbsd-compat/port-aix.c	9 Jan 2003 10:44:06 -0000
@@ -52,5 +52,25 @@
 	xfree(cp);
 }
 
-#endif /* _AIX */
+#ifdef WITH_AIXAUTHENTICATE
+/*
+ * Remove embedded newlines in string (if any).
+ * Used before logging messages returned by AIX authentication functions
+ * so the message is logged on one line.
+ */
+void
+aix_remove_embedded_newlines(char *p)
+{
+	if (p == NULL)
+		return;
+
+	for (; *p; p++) {
+		if (*p == '\n')
+			*p = ' ';
+	}
+	/* Remove trailing newline */
+	*--p = '\0';
+}
+#endif /* WITH_AIXAUTHENTICATE */
 
+#endif /* _AIX */
Index: openbsd-compat/port-aix.h
===================================================================
RCS file: /cvs/openssh/openbsd-compat/port-aix.h,v
retrieving revision 1.6
diff -u -r1.6 port-aix.h
--- openbsd-compat/port-aix.h	7 Jul 2002 02:17:36 -0000	1.6
+++ openbsd-compat/port-aix.h	9 Jan 2003 10:44:06 -0000
@@ -26,4 +26,5 @@
 
 #ifdef _AIX
 void aix_usrinfo(struct passwd *pw);
+void aix_remove_embedded_newlines(char *);
 #endif /* _AIX */
