#pragma module password_policy "X-5" /* ************************************************************************* */ /* * * */ /* * VMS SOFTWARE, INC. CONFIDENTIAL. This software is confidential * */ /* * proprietary software licensed by VMS Software, Inc., and is not * */ /* * authorized to be used, duplicated or disclosed to anyone without * */ /* * the prior written permission of VMS Software, Inc. * */ /* * Copyright 2017-2018 VMS Software, Inc. * */ /* * * */ /* ************************************************************************* */ /*++ * FACILITY: * * SYS$EXAMPLES * * MODULE DESCRIPTION: * * This module illustrates how to write a site-specific password filter in * C. * * To build your own site-specific password policy shareable image, use the * following commands: * * $ CC/LIST SYS$EXAMPLES:VMS$PASSWORD_POLICY+SYS$LIBRARY:SYS$LIB_C.TLB/LIBRARY * $ @SYS$EXAMPLES:VMS$PASSWORD_POLICY_LNK * * Once you've built the image you must then copy it to SYS$LIBRARY, install * the image, and enable the callout by setting the SYSGEN parameter * LOAD_PWD_POLICY to 1: * * $ COPY VMS$PASSWORD_POLICY.EXE SYS$COMMON:[SYSLIB]/PROT=(W:RE) * $ INSTALL ADD SYS$LIBRARY:VMS$PASSWORD_POLICY/OPEN/HEAD/SHARE * $ MCR SYSGEN * SYSGEN> USE ACTIVE * SYSGEN> SET LOAD_PWD_POLICY 1 * SYSGEN> WRITE ACTIVE * SYSGEN> WRITE CURRENT * * Any time the password policy module is updated and replaced, the ACME * server must be restarted as follows: * * $ SET SERVER ACME/RESTART * * Please consult the "VMS System Generation Utility Manual" for further * information on using the SYSGEN utility. You might also want to add the * following line to SYS$SYSTEM:MODPARAMS.DAT: * * LOAD_PWD_POLICY = 1 ! enable site-specific password filters * * and insure that your system startup procedures install VMS$PASSWORD_POLICY * at each reboot. * ***************************************************************************** ***************************************************************************** * IMPORTANT ***************************************************************************** * The three global routines used by SET PASSWORD let you obtain the user's * original password in plaintext, the proposed new plaintext password, and * its equivalent quadword hash value. All security administrators should be * aware of this feature because its subversion by a malicious privileged user * will compromise the system's security. VSI recommends that you place * security alarm ACEs on the password filter image and its parent directories. * * $ SET SECURITY/ACL=(AUDIT=SECURITY,ACCESS=W+D+C+S) SYS$LIBRARY:VMS$PASSWORD_POLICY.EXE * $ SET SECURITY/ACL=(ALARM=SECURITY,ACCESS=W+D+C+S) SYS$LIBRARY:VMS$PASSWORD_POLICY.EXE * $ SET SECURITY/ACL=(AUDIT=SECURITY,ACCESS=W+D+C+S) SYS$COMMON:[000000]SYSLIB.DIR * $ SET SECURITY/ACL=(ALARM=SECURITY,ACCESS=W+D+C+S) SYS$COMMON:[000000]SYSLIB.DIR * $ SET SECURITY/ACL=(AUDIT=SECURITY,ACCESS=W+D+C+S) SYS$SPECIFIC:[000000]SYSLIB.DIR * $ SET SECURITY/ACL=(ALARM=SECURITY,ACCESS=W+D+C+S) SYS$SPECIFIC:[000000]SYSLIB.DIR ***************************************************************************** ***************************************************************************** * * AUTHOR: * * Doug Gordon May 2017 * * MODIFICATION HISTORY: * * X-1 DAG Doug Gordon * Original * * X-2 DAG Doug Gordon 20-Nov-2017 * Remove $GETUAI context. * * X-3 DAG Doug Gordon 30-Jan-2018 * Correct typos in comments * * X-4 DAG Doug Gordon 27-Jul-2018 * General code clean-up. Replace PWDWEAK with PWDPOLICY. * * X-5 DAG Doug Gordon 11-Oct-2018 * Fix copy & paste issue in the debug code. * * X-6 DAG Doug Gordon 9-Nov-2018 * Expand on policy module building and installing comments. *-- */ /* * Uncomment the next line to allow dynamic debugging. Do not under any * circumstances use a DEBUG-enabled image on a production system. */ /* #define COMPILE_DEBUG */ /* Include files */ /* * Presume NEW_STARLET for all VMS include files. */ #define __NEW_STARLET 1 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* from LIB, not STARLET */ /* Local macros */ #define OK(code) $VMS_STATUS_SUCCESS(code) #define NULLPAR 0 #define INIT_ILE(VAR, IDX, LEN, CODE, BUF, RETLEN) \ VAR[IDX].ile3$w_length = LEN; \ VAR[IDX].ile3$w_code = CODE; \ VAR[IDX].ile3$ps_bufaddr = (void *) BUF; \ VAR[IDX].ile3$ps_retlen_addr = (unsigned short int *) RETLEN /* * UAIDEF doesn't define a constant for the size of usernames so we'll have * to fake it from the non-public interface: UAFDEF. In practice, other * parts of the system limit username length to 12 even up through V8.x */ #define UAI$C_USERNAME_LEN (sizeof (((UAF *) 0)->uaf$t_username)) /* * Table of Contents */ char *GetLogical(char *logname); int IntFromLogical(char *logname, int defval); int policy_hash(__int64 *hash, struct dsc$descriptor *username); int policy_plaintext(struct dsc$descriptor *password, struct dsc$descriptor *username); int policy_changes(struct dsc$descriptor *oldpwd, struct dsc$descriptor *newpwd, struct dsc$descriptor *username); /* * GetLogical: * * Attempts a case-blind translation of the supplied string as a logical name. * Returns NULL on error and the null string on no translation. * Caller is responsible for freeing the returned memory. * * For this application, do a case-blind EXEC-mode translation * only. */ char *GetLogical(char *logname) { $DESCRIPTOR(tablename, "LNM$FILE_DEV"); ILE3 list[2]; struct dsc$descriptor ln_desc; char *xlation; unsigned char lnm_mode = PSL$C_EXEC; long length = 0; unsigned int lnm_flags = LNM$M_CASE_BLIND, status; ln_desc.dsc$w_length = strlen(logname); ln_desc.dsc$b_dtype = DSC$K_DTYPE_T; ln_desc.dsc$b_class = DSC$K_CLASS_S; ln_desc.dsc$a_pointer = logname; if ((xlation = (char *) calloc(LNM$C_NAMLENGTH+1, 1)) != NULL) { memset(list, 0, sizeof(list)); INIT_ILE(list, 0, LNM$C_NAMLENGTH, LNM$_STRING, xlation, &length); if (OK(sys$trnlnm(&lnm_flags, &tablename, &ln_desc, &lnm_mode, list))) { xlation[length] = '\0'; } else { free(xlation); xlation = NULL; } } return (xlation); } /* * IntFromLogical: * * Translate the supplied string as a logical name and then attempt * to turn the resultant string into an integer. * * If anything fails, return the default value. * */ int IntFromLogical(char *logname, int defval) { char *translation = NULL; int retval = defval; unsigned int ignored_status; translation = GetLogical(logname); if (translation != NULL) { if (translation[0] != '0') { ignored_status = lib$cvt_dtb(strlen(translation), translation, &retval); } free(translation); } return retval; } /* **************************************************************** */ /* * policy_hash. A stub that always succeeds... */ int policy_hash(__int64 *hash, struct dsc$descriptor *username) { return SS$_NORMAL; } /* **************************************************************** */ /* * policy_plaintext * * Implement a custom password policy filter using system logical names to * define the parameters. * * Parameters: * * password User's proposed password, passed by descriptor. * username Username, passed by descriptor. * * External Input: * * This routine checks the following logical names. All logical * names are translated in EXEC mode from LNM$FILE_DEV. All logical * names are expected to translate to a decimal numeric (probably * single-digit) string. Failure to translate is treated as 0. * Non-numeric or malformed translations are also treated as 0. * * LGI$MIN_PASSWORD_UC_CHAR Minimum number of upper-case characters * isupper() in the current locale * * LGI$MIN_PASSWORD_LC_CHAR Minimum number of lower-case characters * islower() in the current locale * * LGI$MIN_PASSWORD_NUMERIC Minimum number of numeric characters * isdigit() in the current locale. * * LGI$MIN_PASSWORD_SYMBOLS Minimum number of symbol characters. For the * purpose of this module, a symbol is any character * that doesn't fall into the previous categories. * * LGI$MIN_PASSWORD_CATEGORIES Minimum number of categories that must be present. * The acceptable range is 0-4 * * Return values: * * SS$_NORMAL - Password conforms. * SS$_PWDPOLICY = Password fails one or more constraints. * * Philosophy: * * This module allows the implementation of a password complexity policy * simply by defining logical names. With one exception, no attempt * is made here to insure that the logical names define a rational policy. * It is possible to set the logical names such that the sum of the * requirements exceeds the length of a VMS password. Such settings are * not recommended. * * The lone exception is that a user restricted to the default VMS password * character set (A-Z,0-9,$_) will not check for lower-case character * minimums nor be required to meet more than 3 categories. * * Password minimum length is checked before this module is called. No * need to check password length here. * * Note that if you define non-zero values for all four category tests then * there is no need to define LGI$MIN_PASSWORD_CATEGORIES. */ int policy_plaintext(struct dsc$descriptor *password, struct dsc$descriptor *username) { #ifdef COMPILE_DEBUG #define DBG if (debug) #define DBG_LOG "LGI$PASSWORD_POLICY_DEBUG" #endif #define UC_LOG "LGI$MIN_PASSWORD_UC_CHAR" #define LC_LOG "LGI$MIN_PASSWORD_LC_CHAR" #define NUM_LOG "LGI$MIN_PASSWORD_NUMERIC" #define SYM_LOG "LGI$MIN_PASSWORD_SYMBOLS" #define CAT_LOG "LGI$MIN_PASSWORD_CATEGORIES" ILE3 uai_list[2]; int upper, lower, numeric, symbols, categories, debug = 0, i, nupper = 0, nlower = 0, nnumeric = 0, nsymbols = 0, ncategories = 0; long flags, flags_len; unsigned short user_length, pwd_length; char *user, *pwd, *c, local_user[UAI$C_USERNAME_LEN+1], local_pwd[UAI$C_MAX_PWD_LENGTH+1]; unsigned int status; /* * Get the rules from the system-wide logical names. */ #ifdef COMPILE_DEBUG debug = IntFromLogical(DBG_LOG, 0); #endif upper = IntFromLogical(UC_LOG, 0); lower = IntFromLogical(LC_LOG, 0); numeric = IntFromLogical(NUM_LOG, 0); symbols = IntFromLogical(SYM_LOG, 0); categories = IntFromLogical(CAT_LOG, 0); #ifdef COMPILE_DEBUG DBG { printf("\n%s = %d\n", UC_LOG, upper); printf("%s = %d\n", LC_LOG, lower); printf("%s = %d\n", NUM_LOG, numeric); printf("%s = %d\n", SYM_LOG, symbols); printf("%s = %d\n", CAT_LOG, categories); } #endif str$analyze_sdesc(password, &pwd_length, &pwd); str$analyze_sdesc(username, &user_length, &user); #ifdef COMPILE_DEBUG DBG printf("password len = %d, User len = %d\n", pwd_length, user_length); #endif strncpy(local_user, user, user_length); local_user[user_length] = '\0'; strncpy(local_pwd, pwd, pwd_length); local_pwd[pwd_length] = '\0'; #ifdef COMPILE_DEBUG DBG { printf("User %s \n", local_user); printf("Password %s \n", local_pwd); } #endif /* * Clear the item list for $GETUAI and fill it in. */ memset(uai_list, 0, sizeof(uai_list)); INIT_ILE(uai_list, 0, sizeof(flags), UAI$_FLAGS, &flags, &flags_len); status = sys$getuai(NULLPAR, NULLPAR, username, uai_list, NULLPAR, NULLPAR, NULLPAR); if (!OK(status)) lib$signal(status); /* * Check to see if the user is permitted to have any character as a * password (PWDMIX flag is set) or just A-Z0-9$_ * * If PWDMIX is not set, make sure we allow as much of the policy as * possible. */ if ((flags & UAI$M_PWDMIX) != 0) { #ifdef COMPILE_DEBUG DBG printf("Account supports mixed-case passwords.\n"); #endif } else { #ifdef COMPILE_DEBUG DBG printf("Account supports upper-case passwords.\n"); #endif lower = 0; if (categories > 3) categories = 3; } /* * Walk the password, counting the categories. */ for (c=pwd,i=0; i old_len) ? old_len : new_len; while (compare-- && goal) { #ifdef COMPILE_DEBUG DBG printf ("%2d -- O: %c, N: %c, ", compare, *old, *new); #endif if (*old++ != *new++) goal--; #ifdef COMPILE_DEBUG DBG printf ("%d\n", goal); #endif } if (goal > 0) { #ifdef COMPILE_DEBUG DBG printf ("Goal not met: %d. Password is non-compliant!\n", goal); #endif return SS$_PWDPOLICY; } return SS$_NORMAL; }