diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 17:54:12 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 17:54:12 +0000 |
commit | b527294153be3b79563c82c66102adc0004736c0 (patch) | |
tree | 9b423a224848441885190b5ea7cf0feb23510c9d /contrib/slapd-modules/ppm | |
parent | Initial commit. (diff) | |
download | openldap-b527294153be3b79563c82c66102adc0004736c0.tar.xz openldap-b527294153be3b79563c82c66102adc0004736c0.zip |
Adding upstream version 2.6.7+dfsg.upstream/2.6.7+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'contrib/slapd-modules/ppm')
-rw-r--r-- | contrib/slapd-modules/ppm/CHANGELOG.md | 48 | ||||
-rw-r--r-- | contrib/slapd-modules/ppm/CONTRIBUTIONS.md | 5 | ||||
-rw-r--r-- | contrib/slapd-modules/ppm/INSTALL.md | 51 | ||||
-rw-r--r-- | contrib/slapd-modules/ppm/LICENSE | 50 | ||||
-rw-r--r-- | contrib/slapd-modules/ppm/Makefile | 97 | ||||
-rw-r--r-- | contrib/slapd-modules/ppm/README.md | 1 | ||||
-rw-r--r-- | contrib/slapd-modules/ppm/ppm.c | 895 | ||||
-rw-r--r-- | contrib/slapd-modules/ppm/ppm.example | 97 | ||||
-rw-r--r-- | contrib/slapd-modules/ppm/ppm.h | 145 | ||||
-rw-r--r-- | contrib/slapd-modules/ppm/ppm.md | 334 | ||||
-rw-r--r-- | contrib/slapd-modules/ppm/ppm_test.c | 80 | ||||
-rw-r--r-- | contrib/slapd-modules/ppm/slapm-ppm.5 | 353 | ||||
-rwxr-xr-x | contrib/slapd-modules/ppm/unit_tests.sh | 126 |
13 files changed, 2282 insertions, 0 deletions
diff --git a/contrib/slapd-modules/ppm/CHANGELOG.md b/contrib/slapd-modules/ppm/CHANGELOG.md new file mode 100644 index 0000000..e3d2ac0 --- /dev/null +++ b/contrib/slapd-modules/ppm/CHANGELOG.md @@ -0,0 +1,48 @@ +# CHANGELOG + +* 2022-05-17 David Coutadeur <david.coutadeur@gmail.com> + implement a maximum number of characters for each class #18 + upgrade documentation for new olcPPolicyCheckModule in OpenLDAP 2.6 #30 + Make one unique code of development for 2.5 and 2.6 OpenLDAP versions #35 + fix segmentation fault in ppm_test #36 + various minor fixes and optimizations + Version 2.2 +* 2022-03-22 David Coutadeur <david.coutadeur@gmail.com> + Reject password if it contains tokens from an attribute of the LDAP entry #17 + Version 2.1 +* 2021-02-23 David Coutadeur <david.coutadeur@gmail.com> + remove maxLength attribute (#21) + adapt the readme and documentation of ppm (#22) + prepare ppolicy10 in OpenLDAP 2.5 (#20, #23 and #24) + add pwdCheckModuleArg feature + Version 2.0 +* 2019-08-20 David Coutadeur <david.coutadeur@gmail.com> + adding debug symbols for ppm_test, + improve tests with the possibility to add username, + fix openldap crash when checkRDN=1 and username contains too short parts + Version 1.8 +* 2018-03-30 David Coutadeur <david.coutadeur@gmail.com> + various minor improvements provided by Tim Bishop (tdb) (compilation, test program, + imprvts in Makefile: new OLDAP_SOURCES variable pointing to OLDAP install. directory + Version 1.7 +* 2017-05-19 David Coutadeur <david.coutadeur@gmail.com> + Adds cracklib support + Readme adaptations and cleaning + Version 1.6 +* 2017-02-07 David Coutadeur <david.coutadeur@gmail.com> + Adds maxConsecutivePerClass (idea from Trevor Vaughan / tvaughan@onyxpoint.com) + Version 1.5 +* 2016-08-22 David Coutadeur <david.coutadeur@gmail.com> + Get config file from environment variable + Version 1.4 +* 2014-12-20 Daly Chikhaoui <dchikhaoui@janua.fr> + Adding checkRDN parameter + Version 1.3 +* 2014-10-28 David Coutadeur <david.coutadeur@gmail.com> + Adding maxLength parameter + Version 1.2 +* 2014-07-27 David Coutadeur <david.coutadeur@gmail.com> + Changing the configuration file and the configuration data structure + Version 1.1 +* 2014-04-04 David Coutadeur <david.coutadeur@gmail.com> + Version 1.0 diff --git a/contrib/slapd-modules/ppm/CONTRIBUTIONS.md b/contrib/slapd-modules/ppm/CONTRIBUTIONS.md new file mode 100644 index 0000000..3d8b8ac --- /dev/null +++ b/contrib/slapd-modules/ppm/CONTRIBUTIONS.md @@ -0,0 +1,5 @@ +# CONTRIBUTIONS + +* 2014 - 2022 - David Coutadeur <david.coutadeur@gmail.com> - maintainer +* 2015 - Daly Chikhaoui - Janua <dchikhaoui@janua.fr> - contribution on RDN checks +* 2017 - tdb - Tim Bishop - contribution on some compilation improvements diff --git a/contrib/slapd-modules/ppm/INSTALL.md b/contrib/slapd-modules/ppm/INSTALL.md new file mode 100644 index 0000000..b4cba40 --- /dev/null +++ b/contrib/slapd-modules/ppm/INSTALL.md @@ -0,0 +1,51 @@ +INSTALLATION +============ + +Dependencies +------------------ +ppm is provided along with OpenLDAP sources. By default, it is available into contrib/slapd-modules. + - make sure both OpenLDAP sources and ppm are available for building. + - install cracklib development files if you want to test passwords against cracklib + - install pandoc if you want to build the man page + + +Build +----- +Enter contrib/slapd-modules/ppm directory + +You can optionally customize some variables if you don't want the default ones: +- prefix: prefix of the path where ppm is to be installed (defaults to /usr/local) +- ldap_subdir: OpenLDAP specific subdirectory for modules and configurations (defaults to openldap ) +- moduledir: where the ppm module is to be deployed (defaults to $prefix/$libexecdir/$ldap_subdir) +- etcdir: used to compose default sysconfdir location (defaults to $prefix/etc) +- sysconfdir: where the ppm example policy is to be deployed (defaults to $prefix/$etcdir/$ldap_subdir) +- LDAP_SRC: path to OpenLDAP source directory +- Options in DEFS variable: + CONFIG_FILE: (DEPRECATED) path to a ppm configuration file (see PPM_READ_FILE in ppm.h) + note: ppm configuration now lies into pwdCheckModuleArg password policy attribute + provided example file is only helpful as an example or for testing + CRACKLIB: if defined, link against cracklib + DEBUG: If defined, ppm logs its actions with syslog + + +To build ppm, simply run these commands: +(based upon the default prefix /usr/local of OpenLDAP) + +``` +make clean +make +make test +make doc +make install +``` + +Here is an illustrative example showing how to overload some options: + +``` +make clean +make LDAP_SRC=../../.. prefix=/usr/local libdir=/usr/local/lib +make test LDAP_SRC=../../.. +make doc prefix=/usr/local +make install prefix=/usr/local libdir=/usr/local/lib +``` + diff --git a/contrib/slapd-modules/ppm/LICENSE b/contrib/slapd-modules/ppm/LICENSE new file mode 100644 index 0000000..03f692b --- /dev/null +++ b/contrib/slapd-modules/ppm/LICENSE @@ -0,0 +1,50 @@ +OpenLDAP Public License + +The OpenLDAP Public License + Version 2.8.1, 25 November 2003 + +Redistribution and use of this software and associated documentation +("Software"), with or without modification, are permitted provided +that the following conditions are met: + +1. Redistributions in source form must retain copyright statements + and notices, + +2. Redistributions in binary form must reproduce applicable copyright + statements and notices, this list of conditions, and the following + disclaimer in the documentation and/or other materials provided + with the distribution, and + +3. Redistributions must contain a verbatim copy of this document. + +The OpenLDAP Foundation may revise this license from time to time. +Each revision is distinguished by a version number. You may use +this Software under terms of this license revision or under the +terms of any subsequent revision of the license. + +THIS SOFTWARE IS PROVIDED BY THE OPENLDAP FOUNDATION AND ITS +CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE OPENLDAP FOUNDATION, ITS CONTRIBUTORS, OR THE AUTHOR(S) +OR OWNER(S) OF THE SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +The names of the authors and copyright holders must not be used in +advertising or otherwise to promote the sale, use or other dealing +in this Software without specific, written prior permission. Title +to copyright in this Software shall at all times remain with copyright +holders. + +OpenLDAP is a registered trademark of the OpenLDAP Foundation. + +Copyright 1999-2003 The OpenLDAP Foundation, Redwood City, +California, USA. All rights reserved. Permission to copy and +distribute verbatim copies of this document is granted. + diff --git a/contrib/slapd-modules/ppm/Makefile b/contrib/slapd-modules/ppm/Makefile new file mode 100644 index 0000000..835dd67 --- /dev/null +++ b/contrib/slapd-modules/ppm/Makefile @@ -0,0 +1,97 @@ +# $OpenLDAP$ +# Copyright 2014 David Coutadeur, Paris. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted only as authorized by the OpenLDAP +# Public License. +# +# A copy of this license is available in the file LICENSE in the +# top-level directory of the distribution or, alternatively, at +# <http://www.OpenLDAP.org/license.html>. + +LDAP_SRC = ../../.. +LDAP_BUILD = $(LDAP_SRC) +LDAP_INC = -I$(LDAP_BUILD)/include -I$(LDAP_SRC)/include -I$(LDAP_SRC)/servers/slapd +LDAP_LIB = $(LDAP_BUILD)/libraries/liblber/liblber.la $(LDAP_BUILD)/libraries/libldap/libldap.la + +LIBTOOL = $(LDAP_BUILD)/libtool +INSTALL = /usr/bin/install +CC = gcc +OPT = -g -O2 -fpic + +# To skip linking against CRACKLIB make CRACK=no +CRACK=yes +CRACKDEF_yes= -DCRACKLIB +CRACKDEF_no= + +CRACKLIB_yes= -lcrack +CRACKLIB_no= + +CRACKDEF=$(CRACKDEF_$(CRACK)) +CRACKLIB=$(CRACKLIB_$(CRACK)) + +DEFS = -DDEBUG $(CRACKDEF) +# Define if using a config file: +# -DCONFIG_FILE="\"$(sysconfdir)/$(EXAMPLE)\"" + +INCS = $(LDAP_INC) +LIBS = $(LDAP_LIB) + +PROGRAMS=ppm.so +LTVER = 0:0:0 + +LDAP_LIBS = -L$(LDAP_BUILD)/libraries/liblber/.libs -L$(LDAP_BUILD)/libraries/libldap/.libs -lldap -llber + +prefix=/usr/local +exec_prefix=$(prefix) +ldap_subdir=/openldap + +libdir=$(exec_prefix)/lib +libexecdir=$(exec_prefix)/libexec +moduledir = $(libexecdir)$(ldap_subdir) +mandir = $(exec_prefix)/share/man +man5dir = $(mandir)/man5 +etcdir = $(exec_prefix)/etc +sysconfdir = $(etcdir)$(ldap_subdir) + +TEST=ppm_test +EXAMPLE=ppm.example +TESTS=./unit_tests.sh + +MANDOC=slapm-ppm.5 +MDDOC=ppm.md + +all: ppm $(TEST) + +$(TEST): ppm + $(CC) $(CFLAGS) $(OPT) $(CPPFLAGS) $(DEFS) $(LDFLAGS) $(INCS) -Wl,-rpath=. -o $(TEST) ppm_test.c $(PROGRAMS) $(LDAP_LIBS) $(CRACKLIB) + +ppm.o: + $(CC) $(CFLAGS) $(OPT) $(CPPFLAGS) $(DEFS) -c $(INCS) ppm.c + +ppm: ppm.o + $(CC) $(LDFLAGS) $(INCS) -shared -o $(PROGRAMS) ppm.o $(CRACKLIB) + +install: ppm + mkdir -p $(DESTDIR)$(moduledir) + for p in $(PROGRAMS); do \ + $(LIBTOOL) --mode=install cp $$p $(DESTDIR)/$(moduledir) ; \ + done + $(INSTALL) -m 644 $(EXAMPLE) $(DESTDIR)$(sysconfdir)/ + $(INSTALL) -m 644 $(MANDOC) $(DESTDIR)$(man5dir)/ +# $(INSTALL) -m 755 $(TEST) $(libdir) + +.PHONY: clean + +clean: + $(RM) -f ppm.o $(PROGRAMS) ppm.lo $(TEST) + $(RM) -rf .libs + +test: ppm $(TEST) + LDAP_SRC=$(LDAP_SRC) $(TESTS) + +doc: + pandoc $(MDDOC) -s -t man -o $(MANDOC) + sed -i -e 's#ETCDIR#$(DESTDIR)$(sysconfdir)#g' $(MANDOC) + diff --git a/contrib/slapd-modules/ppm/README.md b/contrib/slapd-modules/ppm/README.md new file mode 100644 index 0000000..129f788 --- /dev/null +++ b/contrib/slapd-modules/ppm/README.md @@ -0,0 +1 @@ +See ppm.md manual and INSTALL.md diff --git a/contrib/slapd-modules/ppm/ppm.c b/contrib/slapd-modules/ppm/ppm.c new file mode 100644 index 0000000..de8a49c --- /dev/null +++ b/contrib/slapd-modules/ppm/ppm.c @@ -0,0 +1,895 @@ +/* + * ppm.c for OpenLDAP + * + * See LICENSE, README and INSTALL files + */ + + +/* + password policy module is called with (openldap 2.6): + int check_password (char *pPasswd, struct berval *ppErrmsg, Entry *e, void *pArg) + + password policy module is called with (openldap 2.5): + int check_password (char *pPasswd, char **ppErrStr, Entry *e, void *pArg) + + *pPasswd: new password + **ppErrStr: pointer to the string containing the error message + *ppErrmsg: pointer to a struct berval containing space for an error message of length bv_len + *e: pointer to the current user entry + *pArg: pointer to a struct berval holding the value of pwdCheckModuleArg attr +*/ + +#include <stdlib.h> // for type conversion, such as atoi... +#include <regex.h> // for matching allowedParameters / conf file +#include <string.h> +#include <ctype.h> +#include <portable.h> +#include <slap.h> +#include <stdarg.h> // for variable nb of arguments functions +#include "ppm.h" + +#ifdef CRACKLIB +#include "crack.h" // use cracklib to check password +#endif + +void +ppm_log(int priority, const char *format, ...) +{ + // if DEBUG flag is set + // logs into syslog (for OpenLDAP) or to stdout (for tests) +#if defined(DEBUG) + if(ppm_test != 1) + { + va_list syslog_args; + va_start(syslog_args, format); + vsyslog(priority, format, syslog_args); + va_end(syslog_args); + } + else + { + va_list stdout_args; + va_start(stdout_args, format); + vprintf(format, stdout_args); + printf("\n"); + fflush(stdout); + va_end(stdout_args); + } +#endif +} + +void +strcpy_safe(char *dest, char *src, int length_dest) +{ + if(src == NULL) + { + dest[0] = '\0'; + } + else + { + int length_src = strlen(src); + int n = (length_dest < length_src) ? length_dest : length_src; + // Copy the string — don’t copy too many bytes. + strncpy(dest, src, n); + // Ensure null-termination. + dest[n] = '\0'; + } +} + +genValue* +getValue(conf *fileConf, int numParam, char* param) +{ + int i = 0; + + // First scan parameters + for (i = 0; i < numParam; i++) { + if ((strlen(param) == strlen(fileConf[i].param)) + && (strncmp(param, fileConf[i].param, strlen(fileConf[i].param)) + == 0)) { + return &(fileConf[i].value); + } + } + return NULL; +} + +int maxConsPerClass(char *password, char *charClass) +{ + // find maximum number of consecutive class characters in the password + + int bestMax = 0; + int max = 0; + int i; + + for(i=0 ; i<strlen(password) ; i++) + { + if(strchr(charClass,password[i]) != NULL) + { + // current character is in class + max++; + // is the new max a better candidate to maxConsecutivePerClass? + if(max > bestMax) + { + // found a better maxConsecutivePerClass + bestMax = max; + } + } + else + { + // current character is not in class + // reinitialize max + max=0; + } + } + return bestMax; +} + +void +storeEntry(char *param, char *value, valueType valType, + char *min, char *minForPoint, char *max, conf * fileConf, + int *numParam) +{ + int i = 0; + int iMin; + int iMinForPoint; + int iMax; + if (min == NULL || strcmp(min,"") == 0) + iMin = 0; + else + iMin = atoi(min); + + if (minForPoint == NULL || strcmp(minForPoint,"") == 0) + iMinForPoint = 0; + else + iMinForPoint = atoi(minForPoint); + + if (max == NULL || strcmp(max,"") == 0) + iMax = 0; + else + iMax = atoi(max); + + // First scan parameters + for (i = 0; i < *numParam; i++) { + if ((strlen(param) == strlen(fileConf[i].param)) + && (strncmp(param, fileConf[i].param, strlen(fileConf[i].param)) + == 0)) { + // entry found, replace values + if(valType == typeInt) + fileConf[i].value.iVal = atoi(value); + else + strcpy_safe(fileConf[i].value.sVal, value, VALUE_MAX_LEN); + fileConf[i].min = iMin; + fileConf[i].minForPoint = iMinForPoint; + fileConf[i].max = iMax; + if(valType == typeInt) + ppm_log(LOG_NOTICE, "ppm: Accepted replaced value: %d", + fileConf[i].value.iVal); + else + ppm_log(LOG_NOTICE, "ppm: Accepted replaced value: %s", + fileConf[i].value.sVal); + return; + } + } + // entry not found, add values + strcpy_safe(fileConf[*numParam].param, param, PARAM_MAX_LEN); + fileConf[*numParam].iType = valType; + if(valType == typeInt) + fileConf[i].value.iVal = atoi(value); + else + strcpy_safe(fileConf[i].value.sVal, value, VALUE_MAX_LEN); + fileConf[*numParam].min = iMin; + fileConf[*numParam].minForPoint = iMinForPoint; + fileConf[*numParam].max = iMax; + ++(*numParam); + if(valType == typeInt) + ppm_log(LOG_NOTICE, "ppm: Accepted new value: %d", + fileConf[*numParam].value.iVal); + else + ppm_log(LOG_NOTICE, "ppm: Accepted new value: %s", + fileConf[*numParam].value.sVal); +} + +int +typeParam(char* param) +{ + int i; + int n = sizeof(allowedParameters)/sizeof(params); + + regex_t regex; + int reti; + + for(i = 0 ; i < n ; i++ ) + { + // Compile regular expression + reti = regcomp(®ex, allowedParameters[i].param, 0); + if (reti) { + ppm_log(LOG_ERR, "ppm: Cannot compile regex: %s", + allowedParameters[i].param); + return n; + } + + // Execute regular expression + reti = regexec(®ex, param, 0, NULL, 0); + if (!reti) + { + regfree(®ex); + return i; + } + regfree(®ex); + } + return n; +} + +#ifndef PPM_READ_FILE + + /* + * read configuration into pwdCheckModuleArg attribute + * */ + static void + read_config_attr(conf * fileConf, int *numParam, char *ppm_config_attr) + { + int nParam = 0; // position of found parameter in allowedParameters + int sAllowedParameters = sizeof(allowedParameters)/sizeof(params); + char arg[260*256]; + char *token; + char *saveptr1; + char *saveptr2; + + strcpy_safe(arg, ppm_config_attr, 260*256); + ppm_log(LOG_NOTICE, "ppm: Parsing pwdCheckModuleArg attribute"); + token = strtok_r(arg, "\n", &saveptr1); + + while (token != NULL) { + ppm_log(LOG_NOTICE, "ppm: get line: %s",token); + char *start = token; + char *word, *value; + char *min, *minForPoint, *max; + + while (isspace(*start) && isascii(*start)) + start++; + + if (!isascii(*start)) + { + token = strtok_r(NULL, "\n", &saveptr1); + continue; + } + if (start[0] == '#') + { + token = strtok_r(NULL, "\n", &saveptr1); + continue; + } + + if ((word = strtok_r(start, " \t", &saveptr2))) { + if ((value = strtok_r(NULL, " \t", &saveptr2)) == NULL) + { + saveptr2 = NULL; + ppm_log(LOG_NOTICE, "ppm: No value, goto next parameter"); + token = strtok_r(NULL, "\n", &saveptr1); + continue; + } + if (strchr(value, '\n') != NULL) + strchr(value, '\n')[0] = '\0'; + min = strtok_r(NULL, " \t", &saveptr2); + if (min != NULL) + if (strchr(min, '\n') != NULL) + strchr(min, '\n')[0] = '\0'; + minForPoint = strtok_r(NULL, " \t", &saveptr2); + if (minForPoint != NULL) + if (strchr(minForPoint, '\n') != NULL) + strchr(minForPoint, '\n')[0] = '\0'; + max = strtok_r(NULL, " \t", &saveptr2); + if (max != NULL) + if (strchr(max, '\n') != NULL) + strchr(max, '\n')[0] = '\0'; + + + nParam = typeParam(word); // search for param in allowedParameters + if (nParam != sAllowedParameters) // param has been found + { + ppm_log(LOG_NOTICE, + "ppm: Param = %s, value = %s, min = %s, minForPoint = %s, max = %s", + word, value, min, minForPoint, max); + + storeEntry(word, value, allowedParameters[nParam].iType, + min, minForPoint, max, fileConf, numParam); + } + else + { + ppm_log(LOG_NOTICE, + "ppm: Parameter '%s' rejected", word); + } + + } + token = strtok_r(NULL, "\n", &saveptr1); + } + + } + +#endif + +#ifdef PPM_READ_FILE + + /* + * read configuration file (DEPRECATED) + * */ + static void + read_config_file(conf * fileConf, int *numParam, char *ppm_config_file) + { + FILE *config; + char line[260] = ""; + int nParam = 0; // position of found parameter in allowedParameters + int sAllowedParameters = sizeof(allowedParameters)/sizeof(params); + + ppm_log(LOG_NOTICE, "ppm: Opening file %s", ppm_config_file); + if ((config = fopen(ppm_config_file, "r")) == NULL) { + ppm_log(LOG_ERR, "ppm: Opening file %s failed", ppm_config_file); + exit(EXIT_FAILURE); + } + + while (fgets(line, 256, config) != NULL) { + char *start = line; + char *word, *value; + char *min, *minForPoint, *max; + + while (isspace(*start) && isascii(*start)) + start++; + + if (!isascii(*start)) + continue; + if (start[0] == '#') + continue; + + if ((word = strtok(start, " \t"))) { + if ((value = strtok(NULL, " \t")) == NULL) + continue; + if (strchr(value, '\n') != NULL) + strchr(value, '\n')[0] = '\0'; + min = strtok(NULL, " \t"); + if (min != NULL) + if (strchr(min, '\n') != NULL) + strchr(min, '\n')[0] = '\0'; + minForPoint = strtok(NULL, " \t"); + if (minForPoint != NULL) + if (strchr(minForPoint, '\n') != NULL) + strchr(minForPoint, '\n')[0] = '\0'; + max = strtok(NULL, " \t"); + if (max != NULL) + if (strchr(max, '\n') != NULL) + strchr(max, '\n')[0] = '\0'; + + + nParam = typeParam(word); // search for param in allowedParameters + if (nParam != sAllowedParameters) // param has been found + { + ppm_log(LOG_NOTICE, + "ppm: Param = %s, value = %s, min = %s, minForPoint = %s, max = %s", + word, value, min, minForPoint, max); + + storeEntry(word, value, allowedParameters[nParam].iType, + min, minForPoint, max, fileConf, numParam); + } + else + { + ppm_log(LOG_NOTICE, + "ppm: Parameter '%s' rejected", word); + } + + } + } + + fclose(config); + } + +#endif + +static int +#if OLDAP_VERSION == 0x0205 +realloc_error_message(char **target, int curlen, int nextlen) +#else +realloc_error_message(const char *orig, char **target, int curlen, int nextlen) +#endif +{ + if (curlen < nextlen + MEMORY_MARGIN) { + ppm_log(LOG_WARNING, + "ppm: Reallocating szErrStr from %d to %d", curlen, + nextlen + MEMORY_MARGIN); +#if OLDAP_VERSION == 0x0205 + ber_memfree(*target); +#else + if (*target != orig) + ber_memfree(*target); +#endif + curlen = nextlen + MEMORY_MARGIN; + *target = (char *) ber_memalloc(curlen); + } + + return curlen; +} + +// Does the password contains a token from the RDN ? +int +containsRDN(char* passwd, char* DN) +{ + char lDN[DN_MAX_LEN]; + char * tmpToken; + char * token; + regex_t regex; + int reti; + + strcpy_safe(lDN, DN, DN_MAX_LEN); + + // Extract the RDN from the DN + tmpToken = strtok(lDN, ",+"); + tmpToken = strtok(tmpToken, "="); + tmpToken = strtok(NULL, "="); + + // Search for each token in the password */ + token = strtok(tmpToken, TOKENS_DELIMITERS); + + while (token != NULL) + { + if (strlen(token) > 2) + { + ppm_log(LOG_NOTICE, "ppm: Checking if %s part of RDN matches the password", token); + // Compile regular expression + reti = regcomp(®ex, token, REG_ICASE); + if (reti) { + ppm_log(LOG_ERR, "ppm: Cannot compile regex: %s", token); + return 0; + } + + // Execute regular expression + reti = regexec(®ex, passwd, 0, NULL, 0); + if (!reti) + { + regfree(®ex); + return 1; + } + + regfree(®ex); + } + else + { + ppm_log(LOG_NOTICE, "ppm: %s part of RDN is too short to be checked", token); + } + token = strtok(NULL, TOKENS_DELIMITERS); + } + + return 0; +} + +// Does the password contains a token from an attribute? +int +containsAttributes(char* passwd, Entry* pEntry, char* checkAttributes) +{ + char checkAttrs[VALUE_MAX_LEN]; + char val[VALUE_MAX_LEN]; + char * token; + char * tk; + Attribute *a; + int i; + regex_t regex; + int reti; + + // Parse all attributes in the LDAP entry + for ( a = pEntry->e_attrs; a != NULL; a = a->a_next ) { + + // Parse all attributes in checkAttributes parameter + strcpy_safe(checkAttrs, checkAttributes, VALUE_MAX_LEN); + token = strtok(checkAttrs, ",\0"); + while (token != NULL) + { + // if attribute to check is found in LDAP entry + if( strcmp(a->a_desc->ad_cname.bv_val, token) == 0 ) + { + ppm_log(LOG_NOTICE, "ppm: check password against %s attribute", token); + // parse attribute values + for ( i = 0; a->a_vals[i].bv_val != NULL; i++ ) + { + strcpy_safe(val, a->a_vals[i].bv_val, VALUE_MAX_LEN); + ppm_log(LOG_NOTICE, + "ppm: check password against %s attribute value", + a->a_vals[i].bv_val); + + // Search for each token in the attribute + tk = strtok(val, ATTR_TOKENS_DELIMITERS); + + while (tk != NULL) + { + if (strlen(tk) > 2) + { + ppm_log(LOG_NOTICE, + "ppm: Checking if %s part of value %s of attribute %s matches the password", + tk, + a->a_vals[i].bv_val, + a->a_desc->ad_cname.bv_val); + // Compile regular expression + reti = regcomp(®ex, tk, REG_ICASE); + if (reti) { + ppm_log(LOG_ERR, "ppm: Cannot compile regex: %s", tk); + return 0; + } + + // Execute regular expression: does password + // contains the token extracted from the attr value? + reti = regexec(®ex, passwd, 0, NULL, 0); + if (!reti) + { + regfree(®ex); + return 1; + } + + regfree(®ex); + } + else + { + ppm_log(LOG_NOTICE, + "ppm: %s part of value %s of attribute %s is too short to be checked", + tk, + a->a_vals[i].bv_val, + a->a_desc->ad_cname.bv_val); + } + tk = strtok(NULL, ATTR_TOKENS_DELIMITERS); + } + } + } + token = strtok(NULL, ",\0"); + } + } + + return 0; +} + + +int +#if OLDAP_VERSION == 0x0205 +check_password(char *pPasswd, char **ppErrStr, Entry *e, void *pArg) +#else +check_password(char *pPasswd, struct berval *ppErrmsg, Entry *e, void *pArg) +#endif +{ + + Entry *pEntry = e; + struct berval *pwdCheckModuleArg = pArg; +#if OLDAP_VERSION == 0x0205 + char *szErrStr = (char *) ber_memalloc(MEM_INIT_SZ); + int mem_len = MEM_INIT_SZ; +#else + char *origmsg = ppErrmsg->bv_val; + char *szErrStr = origmsg; + int mem_len = ppErrmsg->bv_len; +#endif + int numParam = 0; // Number of params in current configuration + + int useCracklib; + char cracklibDict[VALUE_MAX_LEN]; + char cracklibDictFiles[3][(VALUE_MAX_LEN+5)]; + char const* cracklibExt[] = { ".hwm", ".pwd", ".pwi" }; + FILE* fd; + char* res; + int minQuality; + int checkRDN; + char checkAttributes[VALUE_MAX_LEN]; + char forbiddenChars[VALUE_MAX_LEN]; + int nForbiddenChars = 0; + int nQuality = 0; + int maxConsecutivePerClass; + int nbInClass[CONF_MAX_SIZE]; + int i,j; + + ppm_log(LOG_NOTICE, "ppm: entry %s", pEntry->e_nname.bv_val); + +#ifdef PPM_READ_FILE + /* Determine if config file is to be read (DEPRECATED) */ + char ppm_config_file[FILENAME_MAX_LEN]; + + ppm_log(LOG_NOTICE, "ppm: Not reading pwdCheckModuleArg attribute"); + ppm_log(LOG_NOTICE, "ppm: instead, read configuration file (deprecated)"); + + strcpy_safe(ppm_config_file, getenv("PPM_CONFIG_FILE"), FILENAME_MAX_LEN); + if (ppm_config_file[0] == '\0') { + strcpy_safe(ppm_config_file, CONFIG_FILE, FILENAME_MAX_LEN); + } + ppm_log(LOG_NOTICE, "ppm: reading config file from %s", ppm_config_file); +#else + if ( !pwdCheckModuleArg || !pwdCheckModuleArg->bv_val ) { + ppm_log(LOG_ERR, "ppm: No config provided in pwdCheckModuleArg"); +#if OLDAP_VERSION == 0x0205 + mem_len = realloc_error_message(&szErrStr, mem_len, +#else + mem_len = realloc_error_message(origmsg, &szErrStr, mem_len, +#endif + strlen(GENERIC_ERROR)); + sprintf(szErrStr, GENERIC_ERROR); + goto fail; + } + + ppm_log(LOG_NOTICE, "ppm: Reading pwdCheckModuleArg attribute"); + ppm_log(LOG_NOTICE, "ppm: RAW configuration: %s", pwdCheckModuleArg->bv_val); +#endif + + /* Set default values */ + conf fileConf[CONF_MAX_SIZE] = { + {"minQuality", typeInt, {.iVal = DEFAULT_QUALITY}, 0, 0, 0 + } + , + {"checkRDN", typeInt, {.iVal = 0}, 0, 0, 0 + } + , + {"forbiddenChars", typeStr, {.sVal = ""}, 0, 0, 0 + } + , + {"maxConsecutivePerClass", typeInt, {.iVal = 0}, 0, 0, 0 + } + , + {"useCracklib", typeInt, {.iVal = 0}, 0, 0, 0 + } + , + {"cracklibDict", typeStr, {.sVal = "/var/cache/cracklib/cracklib_dict"}, 0, 0, 0 + } + , + {"class-upperCase", typeStr, {.sVal = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"}, 0, 1, 0 + } + , + {"class-lowerCase", typeStr, {.sVal = "abcdefghijklmnopqrstuvwxyz"}, 0, 1, 0 + } + , + {"class-digit", typeStr, {.sVal = "0123456789"}, 0, 1, 0 + } + , + {"class-special", typeStr, + {.sVal = "<>,?;.:/!§ù%*µ^¨$£²&é~\"#'{([-|è`_\\ç^à@)]°=}+"}, 0, 1, 0 + } + , + {"checkAttributes", typeStr, {.sVal = ""}, 0, 0, 0 + } + }; + numParam = 11; + + #ifdef PPM_READ_FILE + /* Read configuration file (DEPRECATED) */ + read_config_file(fileConf, &numParam, ppm_config_file); + #else + /* Read configuration attribute (pwdCheckModuleArg) */ + read_config_attr(fileConf, &numParam, (*(struct berval*)pwdCheckModuleArg).bv_val); + #endif + + minQuality = getValue(fileConf, numParam, "minQuality")->iVal; + checkRDN = getValue(fileConf, numParam, "checkRDN")->iVal; + strcpy_safe(forbiddenChars, + getValue(fileConf, numParam, "forbiddenChars")->sVal, + VALUE_MAX_LEN); + maxConsecutivePerClass = getValue(fileConf, numParam, "maxConsecutivePerClass")->iVal; + useCracklib = getValue(fileConf, numParam, "useCracklib")->iVal; + strcpy_safe(cracklibDict, + getValue(fileConf, numParam, "cracklibDict")->sVal, + VALUE_MAX_LEN); + strcpy_safe(checkAttributes, + getValue(fileConf, numParam, "checkAttributes")->sVal, + VALUE_MAX_LEN); + + for (i = 0; i < numParam; i++) + nbInClass[i] = 0; + + + /*The password must have at least minQuality strength points with one + * point granted if the password contains at least minForPoint characters for each class + * It must contains at least min chars of each class + * It must contains at most max chars of each class + * It must not contain any char in forbiddenChar */ + + for (i = 0; i < strlen(pPasswd); i++) { + + int n; + for (n = 0; n < numParam; n++) { + if (strstr(fileConf[n].param, "class-") != NULL) { + if (strchr(fileConf[n].value.sVal, pPasswd[i]) != NULL) { + ++(nbInClass[n]); + } + } + } + if (strchr(forbiddenChars, pPasswd[i]) != NULL) { + nForbiddenChars++; + } + } + + // Password checking done, now loocking for minForPoint criteria + for (i = 0; i < numParam; i++) { + if (strstr(fileConf[i].param, "class-") != NULL) { + if ((nbInClass[i] >= fileConf[i].minForPoint) + && strlen(fileConf[i].value.sVal) != 0) { + // 1 point granted + ++nQuality; + ppm_log(LOG_NOTICE, "ppm: 1 point granted for class %s", + fileConf[i].param); + } + } + } + + if (nQuality < minQuality) { +#if OLDAP_VERSION == 0x0205 + mem_len = realloc_error_message(&szErrStr, mem_len, +#else + mem_len = realloc_error_message(origmsg, &szErrStr, mem_len, +#endif + strlen(PASSWORD_QUALITY_SZ) + + strlen(pEntry->e_nname.bv_val) + 4); + sprintf(szErrStr, PASSWORD_QUALITY_SZ, pEntry->e_nname.bv_val, + nQuality, minQuality); + goto fail; + } + + // Password checking done, now loocking for minimum criteria + for (i = 0; i < numParam; i++) { + if (strstr(fileConf[i].param, "class-") != NULL) { + if ((nbInClass[i] < fileConf[i].min) && + strlen(fileConf[i].value.sVal) != 0) { + // constraint is not satisfied... goto fail +#if OLDAP_VERSION == 0x0205 + mem_len = realloc_error_message(&szErrStr, mem_len, +#else + mem_len = realloc_error_message(origmsg, &szErrStr, mem_len, +#endif + strlen(PASSWORD_MIN_CRITERIA) + + strlen(pEntry->e_nname.bv_val) + + 2 + PARAM_MAX_LEN); + sprintf(szErrStr, PASSWORD_MIN_CRITERIA, pEntry->e_nname.bv_val, + fileConf[i].min, fileConf[i].param); + goto fail; + } + } + } + + // Password checking done, now loocking for maximum criteria + for (i = 0; i < numParam; i++) { + if (strstr(fileConf[i].param, "class-") != NULL) { + if ( (fileConf[i].max != 0) && + (nbInClass[i] > fileConf[i].max) && + strlen(fileConf[i].value.sVal) != 0) { + // constraint is not satisfied... goto fail +#if OLDAP_VERSION == 0x0205 + mem_len = realloc_error_message(&szErrStr, mem_len, +#else + mem_len = realloc_error_message(origmsg, &szErrStr, mem_len, +#endif + strlen(PASSWORD_MAX_CRITERIA) + + strlen(pEntry->e_nname.bv_val) + + 2 + PARAM_MAX_LEN); + sprintf(szErrStr, PASSWORD_MAX_CRITERIA, pEntry->e_nname.bv_val, + fileConf[i].max, fileConf[i].param); + goto fail; + } + } + } + + // Password checking done, now loocking for forbiddenChars criteria + if (nForbiddenChars > 0) { // at least 1 forbidden char... goto fail +#if OLDAP_VERSION == 0x0205 + mem_len = realloc_error_message(&szErrStr, mem_len, +#else + mem_len = realloc_error_message(origmsg, &szErrStr, mem_len, +#endif + strlen(PASSWORD_FORBIDDENCHARS) + + strlen(pEntry->e_nname.bv_val) + 2 + + VALUE_MAX_LEN); + sprintf(szErrStr, PASSWORD_FORBIDDENCHARS, pEntry->e_nname.bv_val, + nForbiddenChars, forbiddenChars); + goto fail; + } + + // Password checking done, now loocking for maxConsecutivePerClass criteria + for (i = 0; i < numParam; i++) { + if (strstr(fileConf[i].param, "class-") != NULL) { + if ( maxConsecutivePerClass != 0 && + (maxConsPerClass(pPasswd,fileConf[i].value.sVal) + > maxConsecutivePerClass)) { + // Too much consecutive characters of the same class + ppm_log(LOG_NOTICE, "ppm: Too much consecutive chars for class %s", + fileConf[i].param); +#if OLDAP_VERSION == 0x0205 + mem_len = realloc_error_message(&szErrStr, mem_len, +#else + mem_len = realloc_error_message(origmsg, &szErrStr, mem_len, +#endif + strlen(PASSWORD_MAXCONSECUTIVEPERCLASS) + + strlen(pEntry->e_nname.bv_val) + 2 + + PARAM_MAX_LEN); + sprintf(szErrStr, PASSWORD_MAXCONSECUTIVEPERCLASS, pEntry->e_nname.bv_val, + maxConsecutivePerClass, fileConf[i].param); + goto fail; + } + } + } +#ifdef CRACKLIB + // Password checking done, now loocking for cracklib criteria + if ( useCracklib > 0 ) { + + for( j = 0 ; j < 3 ; j++) { + strcpy_safe(cracklibDictFiles[j], cracklibDict, VALUE_MAX_LEN); + strcat(cracklibDictFiles[j], cracklibExt[j]); + if (( fd = fopen ( cracklibDictFiles[j], "r")) == NULL ) { + ppm_log(LOG_NOTICE, "ppm: Error while reading %s file", + cracklibDictFiles[j]); +#if OLDAP_VERSION == 0x0205 + mem_len = realloc_error_message(&szErrStr, mem_len, +#else + mem_len = realloc_error_message(origmsg, &szErrStr, mem_len, +#endif + strlen(GENERIC_ERROR)); + sprintf(szErrStr, GENERIC_ERROR); + goto fail; + + } + else { + fclose (fd); + } + } + res = (char *) FascistCheck (pPasswd, cracklibDict); + if ( res != NULL ) { + ppm_log(LOG_NOTICE, "ppm: cracklib does not validate password for entry %s", + pEntry->e_nname.bv_val); +#if OLDAP_VERSION == 0x0205 + mem_len = realloc_error_message(&szErrStr, mem_len, +#else + mem_len = realloc_error_message(origmsg, &szErrStr, mem_len, +#endif + strlen(PASSWORD_CRACKLIB) + + strlen(pEntry->e_nname.bv_val)); + sprintf(szErrStr, PASSWORD_CRACKLIB, pEntry->e_nname.bv_val); + goto fail; + + } + + } +#endif + + // Password checking done, now looking for checkRDN criteria + if (checkRDN == 1 && containsRDN(pPasswd, pEntry->e_nname.bv_val)) + // RDN check enabled and a token from RDN is found in password: goto fail + { +#if OLDAP_VERSION == 0x0205 + mem_len = realloc_error_message(&szErrStr, mem_len, +#else + mem_len = realloc_error_message(origmsg, &szErrStr, mem_len, +#endif + strlen(RDN_TOKEN_FOUND) + + strlen(pEntry->e_nname.bv_val)); + sprintf(szErrStr, RDN_TOKEN_FOUND, pEntry->e_nname.bv_val); + + goto fail; + } + + // Password checking done, now looking for checkAttributes criteria + if ( strcmp(checkAttributes, "") !=0 && + containsAttributes(pPasswd, pEntry, checkAttributes)) + // A token from an attribute is found in password: goto fail + { +#if OLDAP_VERSION == 0x0205 + mem_len = realloc_error_message(&szErrStr, mem_len, +#else + mem_len = realloc_error_message(origmsg, &szErrStr, mem_len, +#endif + strlen(ATTR_TOKEN_FOUND) + + strlen(pEntry->e_nname.bv_val)); + sprintf(szErrStr, ATTR_TOKEN_FOUND, pEntry->e_nname.bv_val); + + goto fail; + } + +#if OLDAP_VERSION == 0x0205 + *ppErrStr = strdup(""); + ber_memfree(szErrStr); +#else + szErrStr[0] = '\0'; +#endif + return (LDAP_SUCCESS); + + fail: +#if OLDAP_VERSION == 0x0205 + *ppErrStr = strdup(szErrStr); + ber_memfree(szErrStr); +#else + ppErrmsg->bv_val = szErrStr; + ppErrmsg->bv_len = mem_len; +#endif + return (EXIT_FAILURE); + +} diff --git a/contrib/slapd-modules/ppm/ppm.example b/contrib/slapd-modules/ppm/ppm.example new file mode 100644 index 0000000..9507348 --- /dev/null +++ b/contrib/slapd-modules/ppm/ppm.example @@ -0,0 +1,97 @@ +# Example of ppm configuration + +# Such configuration must be stored into pwdCheckModuleArg attribute +# of a password policy entry +# See slapo-ppolicy for more details +# Here is an example of such password policy: +# dn: cn=default,ou=policies,dc=my-domain,dc=com +# objectClass: pwdPolicy +# objectClass: top +# objectClass: pwdPolicyChecker +# objectClass: person +# pwdCheckQuality: 2 +# pwdAttribute: userPassword +# sn: default +# cn: default +# pwdMinLength: 6 +# pwdCheckModule: /usr/local/lib/ppm.so +# pwdCheckModuleArg:: bWluUXVhbGl0eSAzCmNoZWNrUkROIDAKY2hlY2tBdHRyaWJ1dGVzCmZvcmJpZGRlbkNoYXJzCm1heENvbnNlY3V0aXZlUGVyQ2xhc3MgMAp1c2VDcmFja2xpYiAwCmNyYWNrbGliRGljdCAvdmFyL2NhY2hlL2NyYWNrbGliL2NyYWNrbGliX2RpY3QKY2xhc3MtdXBwZXJDYXNlIEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaIDAgMSAwCmNsYXNzLWxvd2VyQ2FzZSBhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5eiAwIDEgMApjbGFzcy1kaWdpdCAwMTIzNDU2Nzg5IDAgMSAwCmNsYXNzLXNwZWNpYWwgPD4sPzsuOi8hwqfDuSUqwrVewqgkwqPCsibDqX4iIyd7KFstfMOoYF9cw6dew6BAKV3CsD19KyAwIDEgMAo= +# +# Different parameters are separated by a linefeed (\n) +# Parameters starting with a # are ignored +# Use a base64 tool to code / decode the content of pwdCheckModuleArg + + + +# Parameters + +# minQuality parameter +# Format: +# minQuality [NUMBER] +# Description: +# One point is granted for each class for which MIN_FOR_POINT criteria is fulfilled. +# defines the minimum point numbers for the password to be accepted. +minQuality 3 + +# checkRDN parameter +# Format: +# checkRDN [0 | 1] +# Description: +# If set to 1, password must not contain a token from the RDN. +# Tokens are separated by these delimiters : space tabulation _ - , ; £ +checkRDN 0 + +# checkAttributes parameter +# Format: +# checkAttributes [ATTR1,ATTR2,...] +# Description: +# Password must not contain a token from the values in the given list of attributes +# Tokens are substrings of the values of the given attributes, +# delimited by: space tabulation _ - , ; @ +# For example, if uid="the wonderful entry", +# password must not contain "the", nor "wonderful", nor "entry" +checkAttributes + +# forbiddenChars parameter +# Format: +# forbiddenChars [CHARACTERS_FORBIDDEN] +# Description: +# Defines the forbidden characters list (no separator). +# If one of them is found in the password, then it is rejected. +forbiddenChars + +# maxConsecutivePerClass parameter +# Format: +# maxConsecutivePerClass [NUMBER] +# Description: +# Defines the maximum number of consecutive character allowed for any class +maxConsecutivePerClass 0 + +# useCracklib parameter +# Format: +# useCracklib [0 | 1] +# Description: +# If set to 1, the password must pass the cracklib check +useCracklib 0 + +# cracklibDict parameter +# Format: +# cracklibDict [path_to_cracklib_dictionary] +# Description: +# directory+filename-prefix that your version of CrackLib will go hunting for +# For example, /var/pw_dict resolves as /var/pw_dict.pwd, +# /var/pw_dict.pwi and /var/pw_dict.hwm dictionary files +cracklibDict /var/cache/cracklib/cracklib_dict + +# classes parameter +# Format: +# class-[CLASS_NAME] [CHARACTERS_DEFINING_CLASS] [MIN] [MIN_FOR_POINT] +# Description: +# [CHARACTERS_DEFINING_CLASS]: characters defining the class (no separator) +# [MIN]: If at least [MIN] characters of this class is not found in the password, then it is rejected +# [MIN_FOR_POINT]: one point is granted if password contains at least [MIN_FOR_POINT] character numbers of this class +# [MAX]: if > [MAX] occurrences of characters from this class are found, then the password is rejected (0 means no maximum) +class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0 +class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0 +class-digit 0123456789 0 1 0 +class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+ 0 1 0 diff --git a/contrib/slapd-modules/ppm/ppm.h b/contrib/slapd-modules/ppm/ppm.h new file mode 100644 index 0000000..5e07d98 --- /dev/null +++ b/contrib/slapd-modules/ppm/ppm.h @@ -0,0 +1,145 @@ +/* + * ppm.h for OpenLDAP + * + * See LICENSE, README and INSTALL files + */ + +#ifndef PPM_H_ +#define PPM_H_ + +#include <stdlib.h> // for type conversion, such as atoi... +#include <regex.h> // for matching allowedParameters / conf file +#include <string.h> +#include <ctype.h> +#include <portable.h> +#include <slap.h> + +#if defined(DEBUG) +#include <syslog.h> +#endif + +// Get OpenLDAP version +#define OLDAP_VERSION ((LDAP_VENDOR_VERSION_MAJOR << 8) | LDAP_VENDOR_VERSION_MINOR) +// OLDAP_VERSION = 0x0205 // (v2.5) +// OLDAP_VERSION = 0x0206 // (v2.6) + +//#define PPM_READ_FILE 1 // old deprecated configuration mode + // 1: (deprecated) don't read pwdCheckModuleArg + // attribute, instead read config file + // 0: read pwdCheckModuleArg attribute + +/* config file parameters (DEPRECATED) */ +#ifndef CONFIG_FILE +#define CONFIG_FILE "/etc/openldap/ppm.example" +#endif +#define FILENAME_MAX_LEN 512 + +#define DEFAULT_QUALITY 3 +#define MEMORY_MARGIN 50 +#if OLDAP_VERSION == 0x0205 + #define MEM_INIT_SZ 64 +#endif +#define DN_MAX_LEN 512 + +#define CONF_MAX_SIZE 50 +#define PARAM_MAX_LEN 32 +#define VALUE_MAX_LEN 512 +#define ATTR_NAME_MAX_LEN 150 + +#define PARAM_PREFIX_CLASS "class-" +#define TOKENS_DELIMITERS " ,;-_£\t" +#define ATTR_TOKENS_DELIMITERS " ,;-_@\t" + + +#define DEBUG_MSG_MAX_LEN 256 + +#define PASSWORD_QUALITY_SZ \ + "Password for dn=\"%s\" does not pass required number of strength checks (%d of %d)" +#define PASSWORD_MIN_CRITERIA \ + "Password for dn=\"%s\" has not reached the minimum number of characters (%d) for class %s" +#define PASSWORD_MAX_CRITERIA \ + "Password for dn=\"%s\" has reached the maximum number of characters (%d) for class %s" +#define PASSWORD_MAXCONSECUTIVEPERCLASS \ + "Password for dn=\"%s\" has reached the maximum number of characters (%d) for class %s" +#define PASSWORD_FORBIDDENCHARS \ + "Password for dn=\"%s\" contains %d forbidden characters in %s" +#define RDN_TOKEN_FOUND \ + "Password for dn=\"%s\" contains tokens from the RDN" +#define ATTR_TOKEN_FOUND \ + "Password for dn=\"%s\" is too simple: it contains part of an attribute" +#define GENERIC_ERROR \ + "Error while checking password" +#define PASSWORD_CRACKLIB \ + "Password for dn=\"%s\" is too weak" +#define BAD_PASSWORD_SZ \ + "Bad password for dn=\"%s\" because %s" + + + +typedef union genValue { + int iVal; + char sVal[VALUE_MAX_LEN]; +} genValue; + +typedef enum { + typeInt, + typeStr +} valueType; + +typedef struct params { + char param[PARAM_MAX_LEN]; + valueType iType; +} params; + +// allowed parameters loaded into configuration structure +// it also contains the type of the corresponding value +params allowedParameters[8] = { + {"^minQuality", typeInt}, + {"^checkRDN", typeInt}, + {"^checkAttributes", typeStr}, + {"^forbiddenChars", typeStr}, + {"^maxConsecutivePerClass", typeInt}, + {"^useCracklib", typeInt}, + {"^cracklibDict", typeStr}, + {"^class-.*", typeStr} +}; + + +// configuration structure, containing a parameter, a value, +// a corresponding min and minForPoint indicators if necessary +// and a type for the value (typeInt or typeStr) +typedef struct conf { + char param[PARAM_MAX_LEN]; + valueType iType; + genValue value; + int min; + int minForPoint; + int max; +} conf; + +void ppm_log(int priority, const char *format, ...); +int min(char *str1, char *str2); +#ifndef PPM_READ_FILE + static void read_config_attr(conf * fileConf, int *numParam, char *ppm_config_attr); +#endif +#ifdef PPM_READ_FILE + static void read_config_file(conf * fileConf, int *numParam, char *ppm_config_file); +#endif + +#if OLDAP_VERSION == 0x0205 + int check_password(char *pPasswd, char **ppErrStr, Entry *e, void *pArg); +#else + int check_password(char *pPasswd, struct berval *ppErrmsg, Entry *e, void *pArg); +#endif +int maxConsPerClass(char *password, char *charClass); +void storeEntry(char *param, char *value, valueType valType, + char *min, char *minForPoint, char *max, conf * fileConf, + int *numParam); +int typeParam(char* param); +genValue* getValue(conf *fileConf, int numParam, char* param); +void strcpy_safe(char *dest, char *src, int length_dest); + + +int ppm_test = 0; + +#endif diff --git a/contrib/slapd-modules/ppm/ppm.md b/contrib/slapd-modules/ppm/ppm.md new file mode 100644 index 0000000..f5ec921 --- /dev/null +++ b/contrib/slapd-modules/ppm/ppm.md @@ -0,0 +1,334 @@ +--- +title: ppm +section: 5 +header: File Formats Manual +footer: ppm +date: August 24, 2021 +--- + +# NAME + +ppm (Password Policy Module) - extension of the password policy overlay + +# SYNOPSIS + +ETCDIR/ppm.example + +# DESCRIPTION + +**ppm** is an OpenLDAP module for checking password quality when they are modified. +Passwords are checked against the presence or absence of certain character classes. + +This module is used as an extension of the OpenLDAP password policy controls, +see slapo-ppolicy(5) section **pwdCheckModule**. + + +# USAGE + +Create a password policy entry and indicate the path of the ppm.so library +and the content of the desired policy. +Use a base64 tool to code / decode the content of the policy stored into +**pwdCheckModuleArg**. + +Here is an example for OpenLDAP 2.6: + +``` +dn: cn=default,ou=policies,dc=my-domain,dc=com +objectClass: pwdPolicy +objectClass: top +objectClass: pwdPolicyChecker +objectClass: person +pwdCheckQuality: 2 +pwdAttribute: userPassword +sn: default +cn: default +pwdMinLength: 6 +pwdCheckModuleArg:: bWluUXVhbGl0eSAzCmNoZWNrUkROIDAKY2hlY2tBdHRyaWJ1dGVzCmZvcmJpZGRlbkNoYXJzCm1heENvbnNlY3V0aXZlUGVyQ2xhc3MgMAp1c2VDcmFja2xpYiAwCmNyYWNrbGliRGljdCAvdmFyL2NhY2hlL2NyYWNrbGliL2NyYWNrbGliX2RpY3QKY2xhc3MtdXBwZXJDYXNlIEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaIDAgMSAwCmNsYXNzLWxvd2VyQ2FzZSBhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5eiAwIDEgMApjbGFzcy1kaWdpdCAwMTIzNDU2Nzg5IDAgMSAwCmNsYXNzLXNwZWNpYWwgPD4sPzsuOi8hwqfDuSUqwrVewqgkwqPCsibDqX4iIyd7KFstfMOoYF9cw6dew6BAKV3CsD19KyAwIDEgMAoK +pwdUseCheckModule: TRUE +``` + +For OpenLDAP 2.5, you must add a **pwdCheckModule** attribute pointing +to the ppm module (for example /usr/local/lib/ppm.so), +and remove the **pwdUseCheckModule** attribute. + + + +See **slapo-ppolicy** for more information, but to sum up: + +- enable ppolicy overlay in your database. +- define a default password policy in OpenLDAP configuration or use pwdPolicySubentry attribute to point to the given policy. + +This example show the activation for a **slapd.conf** file for OpenLDAP 2.6 +(see **slapd-config** and **slapo-ppolicy** for more information for + **cn=config** configuration) + +``` +overlay ppolicy +ppolicy_default "cn=default,ou=policies,dc=my-domain,dc=com" +ppolicy_check_module /usr/local/openldap/libexec/openldap/ppm.so +#ppolicy_use_lockout # for having more infos about the lockout +``` + +For OpenLDAP 2.5, you must remove **ppolicy_check_module** parameter as +it is managed in the password policy definition + + +# FEATURES + +Here are the main features: + +- 4 character classes are defined by default: +upper case, lower case, digits and special characters. + +- more character classes can be defined, just write your own. + +- passwords must match the amount of quality points. +A point is validated when at least m characters of the corresponding +character class are present in the password. + +- passwords must have at least n of the corresponding character class +present, else they are rejected. + +- passwords must have at the most x occurrences of characters from the +corresponding character class, else they are rejected. + +- the three previous criteria are checked against any specific character class +defined. + +- if a password contains any of the forbidden characters, then it is +rejected. + +- if a password contains tokens from the RDN, then it is rejected. + +- if a password contains tokens from defined attributes, then it is rejected. + +- if a password does not pass cracklib check, then it is rejected. + + +# CONFIGURATION + +Since OpenLDAP 2.5 version, ppm configuration is held in a binary +attribute of the password policy: **pwdCheckModuleArg** + +The example file (**ETCDIR/ppm.example** by default) is to be +considered as an example configuration, to import in the **pwdCheckModuleArg** +attribute. It is also used for testing passwords with the test program +provided. + +If for some reasons, any parameter is not found, it will be given its +default value. + +Note: you can still compile ppm to use the configuration file, by enabling +**PPM_READ_FILE** in **ppm.h** (but this is deprecated now). If you decide to do so, +you can use the **PPM_CONFIG_FILE** environment variable for overloading the +configuration file path. + +The syntax of a configuration line is: + +``` +parameter value [min] [minForPoint] [max] +``` + +with spaces being delimiters and Line Feed (LF) ending the line. + +Parameter names **are** case sensitive. + +Lines beginning by a **#** are considered as comments. + +The default configuration is the following: + +``` +# minQuality parameter +# Format: +# minQuality [NUMBER] +# Description: +# One point is granted for each class for which MIN_FOR_POINT criteria is fulfilled. +# defines the minimum point numbers for the password to be accepted. +minQuality 3 + +# checkRDN parameter +# Format: +# checkRDN [0 | 1] +# Description: +# If set to 1, password must not contain a token from the RDN. +# Tokens are separated by the following delimiters : space tabulation _ - , ; £ +checkRDN 0 + +# checkAttributes parameter +# Format: +# checkAttributes [ATTR1,ATTR2,...] +# Description: +# Password must not contain a token from the values in the given list of attributes +# Tokens are substrings of the values of the given attributes, +# delimited by: space tabulation _ - , ; @ +# For example, if uid="the wonderful entry", +# password must not contain "the", nor "wonderful", nor "entry" +checkAttributes + +# forbiddenChars parameter +# Format: +# forbiddenChars [CHARACTERS_FORBIDDEN] +# Description: +# Defines the forbidden characters list (no separator). +# If one of them is found in the password, then it is rejected. +forbiddenChars + +# maxConsecutivePerClass parameter +# Format: +# maxConsecutivePerClass [NUMBER] +# Description: +# Defines the maximum number of consecutive character allowed for any class +maxConsecutivePerClass 0 + +# useCracklib parameter +# Format: +# useCracklib [0 | 1] +# Description: +# If set to 1, the password must pass the cracklib check +useCracklib 0 + +# cracklibDict parameter +# Format: +# cracklibDict [path_to_cracklib_dictionary] +# Description: +# directory+filename-prefix that your version of CrackLib will go hunting for +# For example, /var/pw_dict resolves as /var/pw_dict.pwd, +# /var/pw_dict.pwi and /var/pw_dict.hwm dictionary files +cracklibDict /var/cache/cracklib/cracklib_dict + +# classes parameter +# Format: +# class-[CLASS_NAME] [CHARACTERS_DEFINING_CLASS] [MIN] [MIN_FOR_POINT] [MAX] +# Description: +# [CHARACTERS_DEFINING_CLASS]: characters defining the class (no separator) +# [MIN]: If at least [MIN] characters of this class is not found in the password, then it is rejected +# [MIN_FOR_POINT]: one point is granted if password contains at least [MIN_FOR_POINT] character numbers of this class +# [MAX]: if > [MAX] occurrences of characters from this class are found, then the password is rejected (0 means no maximum) +class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0 +class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0 +class-digit 0123456789 0 1 0 +class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+ 0 1 0 +``` + +# EXAMPLE + +With this policy: +``` +minQuality 4 +forbiddenChars .?, +checkRDN 1 +checkAttributes mail +class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 5 0 +class-lowerCase abcdefghijklmnopqrstuvwxyz 0 12 0 +class-digit 0123456789 0 1 0 +class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+ 0 1 0 +class-myClass :) 1 1 0 +``` + +the password **ThereIsNoCowLevel)** is working, because: + +- it has 4 character classes validated : upper, lower, special, and myClass +- it has no character among .?, +- it has at least one character among : or ) + +but it won't work for the user uid=John Cowlevel,ou=people,cn=example,cn=com, +because the token "Cowlevel" from his RDN exists in the password (case insensitive). + +Also, it won't work for a mail attribute containing: "thereis@domain.com" +because the part "thereis" matches the password. + + +# LOGS + +If a user password is rejected by **ppm**, the user will get this type of message: + +Typical user message from ldappasswd(5): + +``` + Result: Constraint violation (19) + Additional info: Password for dn=\"%s\" does not pass required number of strength checks (2 of 3) +``` + +A more detailed message is written to the server log. + +While evaluating a password change, you should observe something looking at this in the logs: + +``` +ppm: entry uid=jack.oneill,ou=people,dc=my-domain,dc=com +ppm: Reading pwdCheckModuleArg attribute +ppm: RAW configuration: minQuality 3#012checkRDN 0#012checkAttributes mail,uid#012forbiddenChars#012maxConsecutivePerClass 0#012useCracklib 0#012cracklibDict /var/cache/cracklib/cracklib_dict#012class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0#012class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0#012class-digit 0123456789 0 1 0#012class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+ 0 1 0 +ppm: Parsing pwdCheckModuleArg attribute +ppm: get line: minQuality 3 +ppm: Param = minQuality, value = 3, min = (null), minForPoint= (null) +ppm: Accepted replaced value: 3 +ppm: get line: checkRDN 0 +ppm: Param = checkRDN, value = 0, min = (null), minForPoint= (null) +ppm: Accepted replaced value: 0 +ppm: get line: checkAttributes mail,uid +ppm: Param = checkAttributes, value = mail,uid, min = (null), minForPoint= (null) +ppm: Accepted replaced value: mail,uid +ppm: get line: forbiddenChars +ppm: No value, goto next parameter +ppm: get line: maxConsecutivePerClass 0 +ppm: Param = maxConsecutivePerClass, value = 0, min = (null), minForPoint= (null) +ppm: Accepted replaced value: 0 +ppm: get line: useCracklib 0 +ppm: Param = useCracklib, value = 0, min = (null), minForPoint= (null) +ppm: Accepted replaced value: 0 +ppm: get line: cracklibDict /var/cache/cracklib/cracklib_dict +ppm: Param = cracklibDict, value = /var/cache/cracklib/cracklib_dict, min = (null), minForPoint= (null) +ppm: Accepted replaced value: /var/cache/cracklib/cracklib_dict +ppm: get line: class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0 +ppm: Param = class-upperCase, value = ABCDEFGHIJKLMNOPQRSTUVWXYZ, min = 0, minForPoint = 1, max = 0 +ppm: Accepted replaced value: ABCDEFGHIJKLMNOPQRSTUVWXYZ +ppm: get line: class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0 +ppm: Param = class-lowerCase, value = abcdefghijklmnopqrstuvwxyz, min = 0, minForPoint = 1, max = 0 +ppm: Accepted replaced value: abcdefghijklmnopqrstuvwxyz +ppm: get line: class-digit 0123456789 0 1 0 +ppm: Param = class-digit, value = 0123456789, min = 0, minForPoint = 1, max = 0 +ppm: Accepted replaced value: 0123456789 +ppm: get line: class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+ 0 1 0 +ppm: Param = class-special, value = <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+, min = 0, minForPoint = 1, max = 0 +ppm: Accepted replaced value: <>,?;.:/!§ù%*µ^¨$£²&é~"#'{([-|è`_\ç^à@)]°=}+ +ppm: 1 point granted for class class-upperCase +ppm: 1 point granted for class class-lowerCase +ppm: Reallocating szErrStr from 64 to 179 +check_password_quality: module error: (/usr/local/openldap/libexec/openldap/ppm.so) Password for dn="uid=jack.oneill,ou=people,dc=my-domain,dc=com" does not pass required number of strength checks (2 of 3).[1] +``` + + +# TESTS + +There is a unit test script: **unit_tests.sh** that illustrates checking some passwords. + +It is possible to test one particular password using directly the test program: + +``` +cd /usr/local/lib +LD_LIBRARY_PATH=. ./ppm_test "uid=test,ou=users,dc=my-domain,dc=com" "my_password" "/usr/local/etc/openldap/ppm.example" && echo OK +``` + + +# FILES + +**ETCDIR/ppm.example** + +> example of ppm configuration to be inserted in **pwdCheckModuleArg** attribute of given password policy + +**ppm.so** + +> ppm library, loaded by the **pwdCheckModule** attribute of given password policy (OpenLDAP 2.5) +> or by the **ppolicy_check_module** / **olcPPolicyCheckModule** parameters of the ppolicy overlay (OpenLDAP 2.6) + +**ppm_test** + +> small test program for checking password in a command-line + + +# SEE ALSO + +**slapo-ppolicy**(5), **slapd-config**(5), **slapd.conf**(5) + +# ACKNOWLEDGEMENTS + +This module was developed in 2014-2022 by David Coutadeur. diff --git a/contrib/slapd-modules/ppm/ppm_test.c b/contrib/slapd-modules/ppm/ppm_test.c new file mode 100644 index 0000000..637c13c --- /dev/null +++ b/contrib/slapd-modules/ppm/ppm_test.c @@ -0,0 +1,80 @@ +#include <stdio.h> +#include <stdlib.h> +#include "ppm.h" + +int main(int argc, char *argv[]) +{ + /* + * argv[1]: user + * argv[2]: password + * argv[3]: configuration file + */ + + int ret = 1; + + if(argc > 2) + { + printf("Testing user %s password: '%s' against %s policy config file \n", + argv[1], argv[2], argv[3] + ); + + /* format user entry */ +#if OLDAP_VERSION == 0x0205 + char *errmsg = NULL; +#else + char errbuf[256]; + struct berval errmsg = { sizeof(errbuf)-1, errbuf }; +#endif + Entry pEntry; + pEntry.e_nname.bv_val=argv[1]; + pEntry.e_name.bv_val=argv[1]; + + /* get configuration file content */ + struct berval pArg; + FILE *fp; + if ((fp = fopen(argv[3],"r")) == NULL) + { + fprintf(stderr,"Unable to open config file for reading\n"); + return ret; + } + char *fcontent = NULL; + fseek(fp, 0, SEEK_END); + long fsize = ftell(fp); + fseek(fp, 0, SEEK_SET); + fcontent = malloc(fsize); + fread(fcontent, 1, fsize, fp); + fclose(fp); + pArg.bv_val = fcontent; + + ppm_test=1; // enable ppm_test for informing ppm not to use syslog + + ret = check_password(argv[2], &errmsg, &pEntry, &pArg); + + if(ret == 0) + { + printf("Password is OK!\n"); + } + else + { +#if OLDAP_VERSION == 0x0205 + printf("Password failed checks : %s\n", errmsg); +#else + printf("Password failed checks : %s\n", errmsg.bv_val); +#endif + } + +#if OLDAP_VERSION == 0x0205 + ber_memfree(errmsg); +#else + if (errmsg.bv_val != errbuf) + ber_memfree(errmsg.bv_val); +#endif + return ret; + + } + + return ret; +} + + + diff --git a/contrib/slapd-modules/ppm/slapm-ppm.5 b/contrib/slapd-modules/ppm/slapm-ppm.5 new file mode 100644 index 0000000..a852037 --- /dev/null +++ b/contrib/slapd-modules/ppm/slapm-ppm.5 @@ -0,0 +1,353 @@ +.\" Automatically generated by Pandoc 2.9.2.1 +.\" +.TH "ppm" "5" "August 24, 2021" "ppm" "File Formats Manual" +.hy +.SH NAME +.PP +ppm (Password Policy Module) - extension of the password policy overlay +.SH SYNOPSIS +.PP +ETCDIR/ppm.example +.SH DESCRIPTION +.PP +\f[B]ppm\f[R] is an OpenLDAP module for checking password quality when +they are modified. +Passwords are checked against the presence or absence of certain +character classes. +.PP +This module is used as an extension of the OpenLDAP password policy +controls, see slapo-ppolicy(5) section \f[B]pwdCheckModule\f[R]. +.SH USAGE +.PP +Create a password policy entry and indicate the path of the ppm.so +library and the content of the desired policy. +Use a base64 tool to code / decode the content of the policy stored into +\f[B]pwdCheckModuleArg\f[R]. +.PP +Here is an example for OpenLDAP 2.6: +.IP +.nf +\f[C] +dn: cn=default,ou=policies,dc=my-domain,dc=com +objectClass: pwdPolicy +objectClass: top +objectClass: pwdPolicyChecker +objectClass: person +pwdCheckQuality: 2 +pwdAttribute: userPassword +sn: default +cn: default +pwdMinLength: 6 +pwdCheckModuleArg:: bWluUXVhbGl0eSAzCmNoZWNrUkROIDAKY2hlY2tBdHRyaWJ1dGVzCmZvcmJpZGRlbkNoYXJzCm1heENvbnNlY3V0aXZlUGVyQ2xhc3MgMAp1c2VDcmFja2xpYiAwCmNyYWNrbGliRGljdCAvdmFyL2NhY2hlL2NyYWNrbGliL2NyYWNrbGliX2RpY3QKY2xhc3MtdXBwZXJDYXNlIEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaIDAgMSAwCmNsYXNzLWxvd2VyQ2FzZSBhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5eiAwIDEgMApjbGFzcy1kaWdpdCAwMTIzNDU2Nzg5IDAgMSAwCmNsYXNzLXNwZWNpYWwgPD4sPzsuOi8hwqfDuSUqwrVewqgkwqPCsibDqX4iIyd7KFstfMOoYF9cw6dew6BAKV3CsD19KyAwIDEgMAoK +pwdUseCheckModule: TRUE +\f[R] +.fi +.PP +For OpenLDAP 2.5, you must add a \f[B]pwdCheckModule\f[R] attribute +pointing to the ppm module (for example /usr/local/lib/ppm.so), and +remove the \f[B]pwdUseCheckModule\f[R] attribute. +.PP +See \f[B]slapo-ppolicy\f[R] for more information, but to sum up: +.IP \[bu] 2 +enable ppolicy overlay in your database. +.IP \[bu] 2 +define a default password policy in OpenLDAP configuration or use +pwdPolicySubentry attribute to point to the given policy. +.PP +This example show the activation for a \f[B]slapd.conf\f[R] file for +OpenLDAP 2.6 (see \f[B]slapd-config\f[R] and \f[B]slapo-ppolicy\f[R] for +more information for \f[B]cn=config\f[R] configuration) +.IP +.nf +\f[C] +overlay ppolicy +ppolicy_default \[dq]cn=default,ou=policies,dc=my-domain,dc=com\[dq] +ppolicy_check_module /usr/local/openldap/libexec/openldap/ppm.so +#ppolicy_use_lockout # for having more infos about the lockout +\f[R] +.fi +.PP +For OpenLDAP 2.5, you must remove \f[B]ppolicy_check_module\f[R] +parameter as it is managed in the password policy definition +.SH FEATURES +.PP +Here are the main features: +.IP \[bu] 2 +4 character classes are defined by default: upper case, lower case, +digits and special characters. +.IP \[bu] 2 +more character classes can be defined, just write your own. +.IP \[bu] 2 +passwords must match the amount of quality points. +A point is validated when at least m characters of the corresponding +character class are present in the password. +.IP \[bu] 2 +passwords must have at least n of the corresponding character class +present, else they are rejected. +.IP \[bu] 2 +passwords must have at the most x occurrences of characters from the +corresponding character class, else they are rejected. +.IP \[bu] 2 +the three previous criteria are checked against any specific character +class defined. +.IP \[bu] 2 +if a password contains any of the forbidden characters, then it is +rejected. +.IP \[bu] 2 +if a password contains tokens from the RDN, then it is rejected. +.IP \[bu] 2 +if a password contains tokens from defined attributes, then it is +rejected. +.IP \[bu] 2 +if a password does not pass cracklib check, then it is rejected. +.SH CONFIGURATION +.PP +Since OpenLDAP 2.5 version, ppm configuration is held in a binary +attribute of the password policy: \f[B]pwdCheckModuleArg\f[R] +.PP +The example file (\f[B]/usr/local/etc/openldap/ppm.example\f[R] by default) is to be +considered as an example configuration, to import in the +\f[B]pwdCheckModuleArg\f[R] attribute. +It is also used for testing passwords with the test program provided. +.PP +If for some reasons, any parameter is not found, it will be given its +default value. +.PP +Note: you can still compile ppm to use the configuration file, by +enabling \f[B]PPM_READ_FILE\f[R] in \f[B]ppm.h\f[R] (but this is +deprecated now). +If you decide to do so, you can use the \f[B]PPM_CONFIG_FILE\f[R] +environment variable for overloading the configuration file path. +.PP +The syntax of a configuration line is: +.IP +.nf +\f[C] +parameter value [min] [minForPoint] [max] +\f[R] +.fi +.PP +with spaces being delimiters and Line Feed (LF) ending the line. +.PP +Parameter names \f[B]are\f[R] case sensitive. +.PP +Lines beginning by a \f[B]#\f[R] are considered as comments. +.PP +The default configuration is the following: +.IP +.nf +\f[C] +# minQuality parameter +# Format: +# minQuality [NUMBER] +# Description: +# One point is granted for each class for which MIN_FOR_POINT criteria is fulfilled. +# defines the minimum point numbers for the password to be accepted. +minQuality 3 + +# checkRDN parameter +# Format: +# checkRDN [0 | 1] +# Description: +# If set to 1, password must not contain a token from the RDN. +# Tokens are separated by the following delimiters : space tabulation _ - , ; \[Po] +checkRDN 0 + +# checkAttributes parameter +# Format: +# checkAttributes [ATTR1,ATTR2,...] +# Description: +# Password must not contain a token from the values in the given list of attributes +# Tokens are substrings of the values of the given attributes, +# delimited by: space tabulation _ - , ; \[at] +# For example, if uid=\[dq]the wonderful entry\[dq], +# password must not contain \[dq]the\[dq], nor \[dq]wonderful\[dq], nor \[dq]entry\[dq] +checkAttributes + +# forbiddenChars parameter +# Format: +# forbiddenChars [CHARACTERS_FORBIDDEN] +# Description: +# Defines the forbidden characters list (no separator). +# If one of them is found in the password, then it is rejected. +forbiddenChars + +# maxConsecutivePerClass parameter +# Format: +# maxConsecutivePerClass [NUMBER] +# Description: +# Defines the maximum number of consecutive character allowed for any class +maxConsecutivePerClass 0 + +# useCracklib parameter +# Format: +# useCracklib [0 | 1] +# Description: +# If set to 1, the password must pass the cracklib check +useCracklib 0 + +# cracklibDict parameter +# Format: +# cracklibDict [path_to_cracklib_dictionary] +# Description: +# directory+filename-prefix that your version of CrackLib will go hunting for +# For example, /var/pw_dict resolves as /var/pw_dict.pwd, +# /var/pw_dict.pwi and /var/pw_dict.hwm dictionary files +cracklibDict /var/cache/cracklib/cracklib_dict + +# classes parameter +# Format: +# class-[CLASS_NAME] [CHARACTERS_DEFINING_CLASS] [MIN] [MIN_FOR_POINT] [MAX] +# Description: +# [CHARACTERS_DEFINING_CLASS]: characters defining the class (no separator) +# [MIN]: If at least [MIN] characters of this class is not found in the password, then it is rejected +# [MIN_FOR_POINT]: one point is granted if password contains at least [MIN_FOR_POINT] character numbers of this class +# [MAX]: if > [MAX] occurrences of characters from this class are found, then the password is rejected (0 means no maximum) +class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0 +class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0 +class-digit 0123456789 0 1 0 +class-special <>,?;.:/!\[sc]\[`u]%*\[mc]\[ha]\[ad]$\[Po]\[S2]&\['e]\[ti]\[dq]#\[aq]{([-|\[`e]\[ga]_\[rs]\[,c]\[ha]\[`a]\[at])]\[de]=}+ 0 1 0 +\f[R] +.fi +.SH EXAMPLE +.PP +With this policy: +.IP +.nf +\f[C] +minQuality 4 +forbiddenChars .?, +checkRDN 1 +checkAttributes mail +class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 5 0 +class-lowerCase abcdefghijklmnopqrstuvwxyz 0 12 0 +class-digit 0123456789 0 1 0 +class-special <>,?;.:/!\[sc]\[`u]%*\[mc]\[ha]\[ad]$\[Po]\[S2]&\['e]\[ti]\[dq]#\[aq]{([-|\[`e]\[ga]_\[rs]\[,c]\[ha]\[`a]\[at])]\[de]=}+ 0 1 0 +class-myClass :) 1 1 0 +\f[R] +.fi +.PP +the password \f[B]ThereIsNoCowLevel)\f[R] is working, because: +.IP \[bu] 2 +it has 4 character classes validated : upper, lower, special, and +myClass +.IP \[bu] 2 +it has no character among .?, +.IP \[bu] 2 +it has at least one character among : or ) +.PP +but it won\[cq]t work for the user uid=John +Cowlevel,ou=people,cn=example,cn=com, because the token +\[lq]Cowlevel\[rq] from his RDN exists in the password (case +insensitive). +.PP +Also, it won\[cq]t work for a mail attribute containing: +\[lq]thereis\[at]domain.com\[rq] because the part \[lq]thereis\[rq] +matches the password. +.SH LOGS +.PP +If a user password is rejected by \f[B]ppm\f[R], the user will get this +type of message: +.PP +Typical user message from ldappasswd(5): +.IP +.nf +\f[C] + Result: Constraint violation (19) + Additional info: Password for dn=\[rs]\[dq]%s\[rs]\[dq] does not pass required number of strength checks (2 of 3) +\f[R] +.fi +.PP +A more detailed message is written to the server log. +.PP +While evaluating a password change, you should observe something looking +at this in the logs: +.IP +.nf +\f[C] +ppm: entry uid=jack.oneill,ou=people,dc=my-domain,dc=com +ppm: Reading pwdCheckModuleArg attribute +ppm: RAW configuration: minQuality 3#012checkRDN 0#012checkAttributes mail,uid#012forbiddenChars#012maxConsecutivePerClass 0#012useCracklib 0#012cracklibDict /var/cache/cracklib/cracklib_dict#012class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0#012class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0#012class-digit 0123456789 0 1 0#012class-special <>,?;.:/!\[sc]\[`u]%*\[mc]\[ha]\[ad]$\[Po]\[S2]&\['e]\[ti]\[dq]#\[aq]{([-|\[`e]\[ga]_\[rs]\[,c]\[ha]\[`a]\[at])]\[de]=}+ 0 1 0 +ppm: Parsing pwdCheckModuleArg attribute +ppm: get line: minQuality 3 +ppm: Param = minQuality, value = 3, min = (null), minForPoint= (null) +ppm: Accepted replaced value: 3 +ppm: get line: checkRDN 0 +ppm: Param = checkRDN, value = 0, min = (null), minForPoint= (null) +ppm: Accepted replaced value: 0 +ppm: get line: checkAttributes mail,uid +ppm: Param = checkAttributes, value = mail,uid, min = (null), minForPoint= (null) +ppm: Accepted replaced value: mail,uid +ppm: get line: forbiddenChars +ppm: No value, goto next parameter +ppm: get line: maxConsecutivePerClass 0 +ppm: Param = maxConsecutivePerClass, value = 0, min = (null), minForPoint= (null) +ppm: Accepted replaced value: 0 +ppm: get line: useCracklib 0 +ppm: Param = useCracklib, value = 0, min = (null), minForPoint= (null) +ppm: Accepted replaced value: 0 +ppm: get line: cracklibDict /var/cache/cracklib/cracklib_dict +ppm: Param = cracklibDict, value = /var/cache/cracklib/cracklib_dict, min = (null), minForPoint= (null) +ppm: Accepted replaced value: /var/cache/cracklib/cracklib_dict +ppm: get line: class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0 +ppm: Param = class-upperCase, value = ABCDEFGHIJKLMNOPQRSTUVWXYZ, min = 0, minForPoint = 1, max = 0 +ppm: Accepted replaced value: ABCDEFGHIJKLMNOPQRSTUVWXYZ +ppm: get line: class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0 +ppm: Param = class-lowerCase, value = abcdefghijklmnopqrstuvwxyz, min = 0, minForPoint = 1, max = 0 +ppm: Accepted replaced value: abcdefghijklmnopqrstuvwxyz +ppm: get line: class-digit 0123456789 0 1 0 +ppm: Param = class-digit, value = 0123456789, min = 0, minForPoint = 1, max = 0 +ppm: Accepted replaced value: 0123456789 +ppm: get line: class-special <>,?;.:/!\[sc]\[`u]%*\[mc]\[ha]\[ad]$\[Po]\[S2]&\['e]\[ti]\[dq]#\[aq]{([-|\[`e]\[ga]_\[rs]\[,c]\[ha]\[`a]\[at])]\[de]=}+ 0 1 0 +ppm: Param = class-special, value = <>,?;.:/!\[sc]\[`u]%*\[mc]\[ha]\[ad]$\[Po]\[S2]&\['e]\[ti]\[dq]#\[aq]{([-|\[`e]\[ga]_\[rs]\[,c]\[ha]\[`a]\[at])]\[de]=}+, min = 0, minForPoint = 1, max = 0 +ppm: Accepted replaced value: <>,?;.:/!\[sc]\[`u]%*\[mc]\[ha]\[ad]$\[Po]\[S2]&\['e]\[ti]\[dq]#\[aq]{([-|\[`e]\[ga]_\[rs]\[,c]\[ha]\[`a]\[at])]\[de]=}+ +ppm: 1 point granted for class class-upperCase +ppm: 1 point granted for class class-lowerCase +ppm: Reallocating szErrStr from 64 to 179 +check_password_quality: module error: (/usr/local/openldap/libexec/openldap/ppm.so) Password for dn=\[dq]uid=jack.oneill,ou=people,dc=my-domain,dc=com\[dq] does not pass required number of strength checks (2 of 3).[1] +\f[R] +.fi +.SH TESTS +.PP +There is a unit test script: \f[B]unit_tests.sh\f[R] that illustrates +checking some passwords. +.PP +It is possible to test one particular password using directly the test +program: +.IP +.nf +\f[C] +cd /usr/local/lib +LD_LIBRARY_PATH=. ./ppm_test \[dq]uid=test,ou=users,dc=my-domain,dc=com\[dq] \[dq]my_password\[dq] \[dq]/usr/local/etc/openldap/ppm.example\[dq] && echo OK +\f[R] +.fi +.SH FILES +.PP +\f[B]/usr/local/etc/openldap/ppm.example\f[R] +.RS +.PP +example of ppm configuration to be inserted in +\f[B]pwdCheckModuleArg\f[R] attribute of given password policy +.RE +.PP +\f[B]ppm.so\f[R] +.RS +.PP +ppm library, loaded by the \f[B]pwdCheckModule\f[R] attribute of given +password policy (OpenLDAP 2.5) or by the \f[B]ppolicy_check_module\f[R] +/ \f[B]olcPPolicyCheckModule\f[R] parameters of the ppolicy overlay +(OpenLDAP 2.6) +.RE +.PP +\f[B]ppm_test\f[R] +.RS +.PP +small test program for checking password in a command-line +.RE +.SH SEE ALSO +.PP +\f[B]slapo-ppolicy\f[R](5), \f[B]slapd-config\f[R](5), +\f[B]slapd.conf\f[R](5) +.SH ACKNOWLEDGEMENTS +.PP +This module was developed in 2014-2022 by David Coutadeur. diff --git a/contrib/slapd-modules/ppm/unit_tests.sh b/contrib/slapd-modules/ppm/unit_tests.sh new file mode 100755 index 0000000..31a3aee --- /dev/null +++ b/contrib/slapd-modules/ppm/unit_tests.sh @@ -0,0 +1,126 @@ +#!/bin/bash + +# Launch unitary tests +# + + +CONFIG_FILE="ppm.example" + +LDAP_SRC="${LDAP_SRC:-../../..}" +LDAP_BUILD=${LDAP_BUILD:-${LDAP_SRC}} +CURRENT_DIR=$( dirname $0 ) +LIB_PATH="${LD_LIBRARY_PATH}:${CURRENT_DIR}:${LDAP_BUILD}/libraries/liblber/.libs:${LDAP_BUILD}/libraries/libldap/.libs" + +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +RESULT=0 + +PPM_CONF_1='minQuality 3 +checkRDN 0 +forbiddenChars +maxConsecutivePerClass 0 +useCracklib 0 +cracklibDict /var/cache/cracklib/cracklib_dict +class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0 +class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0 +class-digit 0123456789 0 1 0 +class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'\''{([-|è`_\ç^à@)]°=}+ 0 1 0' + +PPM_CONF_2='minQuality 3 +checkRDN 0 +forbiddenChars à +maxConsecutivePerClass 5 +useCracklib 0 +cracklibDict /var/cache/cracklib/cracklib_dict +class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 2 4 10 +class-lowerCase abcdefghijklmnopqrstuvwxyz 3 4 12 +class-digit 0123456789 2 4 10 +class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'\''{([-|è`_\ç^à@)]°=}+ 0 4 10' + +PPM_CONF_3='minQuality 3 +checkRDN 1 +forbiddenChars +maxConsecutivePerClass 0 +useCracklib 0 +cracklibDict /var/cache/cracklib/cracklib_dict +class-upperCase ABCDEFGHIJKLMNOPQRSTUVWXYZ 0 1 0 +class-lowerCase abcdefghijklmnopqrstuvwxyz 0 1 0 +class-digit 0123456789 0 1 0 +class-special <>,?;.:/!§ù%*µ^¨$£²&é~"#'\''{([-|è`_\ç^à@)]°=}+ 0 1 0' + + +echo "$PPM_CONF_1" > ppm1.conf +echo "$PPM_CONF_2" > ppm2.conf +echo "$PPM_CONF_3" > ppm3.conf + + +launch_test() +{ + # launch tests + # FORMAT: launch_test [conf_file] [password] [expected_result] + # [expected_result] = [PASS|FAIL] + + local CONF="$1" + local USER="$2" + local PASS="$3" + local EXPECT="$4" + + [[ $EXPECT == "PASS" ]] && EXP="0" || EXP="1" + + LD_LIBRARY_PATH="${LIB_PATH}" ./ppm_test "${USER}" "${PASS}" "${CONF}" + RES="$?" + + if [ "$RES" -eq "$EXP" ] ; then + echo -e "conf=${CONF} user=${USER} pass=${PASS} expect=${EXPECT}... ${GREEN}PASS${NC}" + else + echo -e "conf=${CONF} user=${USER} pass=${PASS} expect=${EXPECT}... ${RED}FAIL${NC}" + ((RESULT+=1)) + fi + + echo +} + + + + +launch_test "ppm1.conf" "uid=test,ou=users,dc=my-domain,dc=com" "azerty" "FAIL" +launch_test "ppm1.conf" "uid=test,ou=users,dc=my-domain,dc=com" "azeRTY" "FAIL" +launch_test "ppm1.conf" "uid=test,ou=users,dc=my-domain,dc=com" "azeRTY123" "PASS" +launch_test "ppm1.conf" "uid=test,ou=users,dc=my-domain,dc=com" "azeRTY." "PASS" + + +launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "AAaaa01AAaaa01AAaaa0" "PASS" +# forbidden char +launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "AAaaa01AAaaa01AAaaaà" "FAIL" +# too much consecutive for upper +launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "AAaaa01AAaaa01AAAAAA" "FAIL" +# not enough upper +launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "Aaaaa01aaaaa01aa.;.;" "FAIL" +# not enough lower/ +launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "aaAAA01BB0123AAA.;.;" "FAIL" +# not enough digit +launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "1AAAA.;BBB.;.;AA.;.;" "FAIL" +# not enough points (no point for digit) +launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "AAaaaBBBBaaa01AAaaaa" "FAIL" +# too much upper +launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "AAaa01AAaa01AA..AA..AAAA" "FAIL" +# too much lower +launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "AAaaa01AAaaa01AAaaa0aaaa" "FAIL" +# too much digit +launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "AA11aa11AA11aa11..11..11" "FAIL" +# too much special +launch_test "ppm2.conf" "uid=test,ou=users,dc=my-domain,dc=com" "AA..aa..11..AA..aa..11.." "FAIL" + +# password in RDN +launch_test "ppm3.conf" "uid=User_Password10-test,ou=users,dc=my-domain,dc=com" "Password10" "FAIL" +launch_test "ppm3.conf" "uid=User_Passw0rd-test,ou=users,dc=my-domain,dc=com" "Password10" "PASS" +launch_test "ppm3.conf" "uid=User-Pw-Test,ou=users,dc=my-domain,dc=com" "Password10" "PASS" + + +echo "${RESULT} error(s) encountered" + +rm ppm1.conf ppm2.conf ppm3.conf +exit ${RESULT} + |