From 08459a98b30fbdf27117e4eef9bdb4724b0c6c6c Mon Sep 17 00:00:00 2001 From: Ganna Starovoytova Date: Tue, 23 Jun 2026 13:41:49 +0200 Subject: [PATCH] Add allowing/disabling character classes --- allowed-character-classes.patch | 294 ++++++++++++++++++++++++++++++++ libpwquality.spec | 8 +- 2 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 allowed-character-classes.patch diff --git a/allowed-character-classes.patch b/allowed-character-classes.patch new file mode 100644 index 0000000..0acf995 --- /dev/null +++ b/allowed-character-classes.patch @@ -0,0 +1,294 @@ +diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/doc/man/pam_pwquality.8.pod b/doc/man/pam_pwquality.8.pod +--- a/doc/man/pam_pwquality.8.pod 2026-06-24 13:39:51.505182998 +0200 ++++ b/doc/man/pam_pwquality.8.pod 2026-06-24 13:47:32.042603712 +0200 +@@ -250,6 +250,13 @@ + a new password but use the one provided by the previously stacked + B module. + ++=item BI<< "ludo" >> ++ ++The list of letters that represent allowed character classes that ++are allowed in a password (digits, uppercase, lowercase, others). ++"d" is for digits, "u" is for uppercase letters, "l" is for ++lowercase letters, "o" is for other characters. ++ + =back + + =head1 MODULE TYPES PROVIDED +diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/doc/man/pwquality.conf.5.pod b/doc/man/pwquality.conf.5.pod +--- a/doc/man/pwquality.conf.5.pod 2026-06-24 13:39:51.505235241 +0200 ++++ b/doc/man/pwquality.conf.5.pod 2026-06-24 13:50:30.694360330 +0200 +@@ -85,7 +85,7 @@ + Examples of such sequence are '12345' or 'fedcb'. Note + that most such passwords will not pass the simplicity check unless + the sequence is only a minor part of the password. +-The check is disabled if the value is 0. (default 0) ++The check is disabled if the value is 0. (default 0) + + =item B + +@@ -155,6 +155,13 @@ + the following modules in the stack can use the B option. + This option is off by default. + ++=item B ++ ++The list of letters that represent allowed character classes that ++are allowed in a password (digits, uppercase, lowercase, others). ++"d" is for digits, "u" is for uppercase letters, "l" is for ++lowercase letters, "o" is for other characters. (default "ludo") ++ + =back + + =head1 SEE ALSO +diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/src/check.c b/src/check.c +--- a/src/check.c 2026-06-24 13:39:51.504779475 +0200 ++++ b/src/check.c 2026-06-24 13:45:06.118168891 +0200 +@@ -49,7 +49,7 @@ + * the other + */ + +-static int ++static int + distdifferent(const char *old, const char *new, + size_t i, size_t j) + { +@@ -202,6 +202,7 @@ + int others = 0; + int size; + int i; ++ const char *allow_classes; + enum { NONE, DIGIT, UCASE, LCASE, OTHER } prevclass = NONE; + int sameclass = 0; + +@@ -244,6 +245,35 @@ + return PWQ_ERROR_MAX_CLASS_REPEAT; + } + } ++ pwquality_get_str_value(pwq, PWQ_SETTING_ALLOW_CLASSES, &allow_classes); ++ if (digits > 0) { ++ if (strpbrk(allow_classes, "d") == NULL) { ++ if (auxerror) ++ *auxerror = strdup("digits"); ++ return PWQ_ERROR_DISALLOWED_CLASS; ++ } ++ } ++ if (uppers > 0) { ++ if (strpbrk(allow_classes, "u") == NULL) { ++ if (auxerror) ++ *auxerror = strdup("uppercase"); ++ return PWQ_ERROR_DISALLOWED_CLASS; ++ } ++ } ++ if (lowers > 0) { ++ if (strpbrk(allow_classes, "l") == NULL) { ++ if (auxerror) ++ *auxerror = strdup("lowercase"); ++ return PWQ_ERROR_DISALLOWED_CLASS; ++ } ++ } ++ if (others > 0) { ++ if (strpbrk(allow_classes, "o") == NULL) { ++ if (auxerror) ++ *auxerror = strdup("other"); ++ return PWQ_ERROR_DISALLOWED_CLASS; ++ } ++ } + + if ((pwq->dig_credit >= 0) && (digits > pwq->dig_credit)) + digits = pwq->dig_credit; +diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/src/error.c b/src/error.c +--- a/src/error.c 2026-06-24 13:39:51.504839606 +0200 ++++ b/src/error.c 2026-06-24 13:45:29.032394205 +0200 +@@ -149,6 +149,13 @@ + return _("The configuration file is malformed"); + case PWQ_ERROR_FATAL_FAILURE: + return _("Fatal failure"); ++ case PWQ_ERROR_DISALLOWED_CLASS: ++ if (auxerror) { ++ snprintf(buf, len, _("The %s class of characters is not allowed"), (const char *)auxerror); ++ free(auxerror); ++ return buf; ++ } ++ return _("The password contains disallowed character class"); + default: + return _("Unknown error"); + } +@@ -188,4 +195,3 @@ + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ +- +diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/src/generate.c b/src/generate.c +--- a/src/generate.c 2026-06-24 13:39:51.504750650 +0200 ++++ b/src/generate.c 2026-06-24 13:45:38.556487851 +0200 +@@ -95,7 +95,7 @@ + } + + *offset += bits; +- return low; ++ return low; + } + + /* generate a random password according to the settings */ +diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/src/pam_pwquality.c b/src/pam_pwquality.c +--- a/src/pam_pwquality.c 2026-06-24 13:39:51.504861183 +0200 ++++ b/src/pam_pwquality.c 2026-06-24 13:46:16.791863795 +0200 +@@ -88,7 +88,7 @@ + } else if (!strncmp(*argv, "try_first_pass", 14)) { + /* for pam_get_authtok, ignore */; + } else if (pwquality_set_option(pwq, *argv)) { +- pam_syslog(pamh, LOG_ERR, ++ pam_syslog(pamh, LOG_ERR, + "pam_parse: unknown or broken option; %s", *argv); + } + } +@@ -213,7 +213,7 @@ + if (retval != PAM_SUCCESS || newtoken == NULL) { + if (retval == PAM_AUTHTOK_ERR || newtoken == NULL) + pam_syslog(pamh, LOG_INFO, "user aborted password change"); +- else ++ else + pam_syslog(pamh, LOG_ERR, "pam_get_authtok_noverify returned error: %s", + pam_strerror(pamh, retval)); + pwquality_free_settings(options.pwq); +@@ -262,7 +262,7 @@ + continue; + if (retval == PAM_AUTHTOK_ERR || newtoken == NULL) + pam_syslog(pamh, LOG_INFO, "user aborted password change"); +- else ++ else + pam_syslog(pamh, LOG_ERR, "pam_get_authtok_verify returned error: %s", + pam_strerror(pamh, retval)); + pwquality_free_settings(options.pwq); +diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/src/pwqprivate.h b/src/pwqprivate.h +--- a/src/pwqprivate.h 2026-06-24 13:39:51.504670235 +0200 ++++ b/src/pwqprivate.h 2026-06-24 13:43:52.963449598 +0200 +@@ -33,6 +33,7 @@ + int local_users_only; + char *bad_words; + char *dict_path; ++ char *allow_classes; + }; + + struct setting_mapping { +@@ -47,6 +48,7 @@ + #define PWQ_DEFAULT_UP_CREDIT 0 + #define PWQ_DEFAULT_LOW_CREDIT 0 + #define PWQ_DEFAULT_OTH_CREDIT 0 ++#define PWQ_DEFAULT_ALLOWCLASSES "ludo" + + #ifdef HAVE_CRACK_H + #define PWQ_DEFAULT_DICT_CHECK 1 +diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/src/pwquality.conf b/src/pwquality.conf +--- a/src/pwquality.conf 2026-06-24 13:39:51.504930249 +0200 ++++ b/src/pwquality.conf 2026-06-24 13:50:46.129512097 +0200 +@@ -58,6 +58,10 @@ + # The check is enabled if the value is greater than 0 and usercheck is enabled. + # usersubstr = 0 + # ++# The allowed classes of characters (digits, uppercase, lowercase, others) ++# to be used in the password. ++# allowclasses = "ludo" ++# + # Whether the check is enforced by the PAM module and possibly other + # applications. + # The new password is rejected if it fails the check and the value is not 0. +diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/src/pwquality.h b/src/pwquality.h +--- a/src/pwquality.h 2026-06-24 13:39:51.504650547 +0200 ++++ b/src/pwquality.h 2026-06-24 13:47:07.084358309 +0200 +@@ -36,6 +36,7 @@ + #define PWQ_SETTING_ENFORCE_ROOT 19 + #define PWQ_SETTING_LOCAL_USERS 20 + #define PWQ_SETTING_USER_SUBSTR 21 ++#define PWQ_SETTING_ALLOW_CLASSES 22 + + #define PWQ_MAX_ENTROPY_BITS 256 + #define PWQ_MIN_ENTROPY_BITS 56 +@@ -72,6 +73,7 @@ + #define PWQ_ERROR_MAX_CLASS_REPEAT -27 + #define PWQ_ERROR_BAD_WORDS -28 + #define PWQ_ERROR_MAX_SEQUENCE -29 ++#define PWQ_ERROR_DISALLOWED_CLASS -30 + + typedef struct pwquality_settings pwquality_settings_t; + +@@ -135,7 +137,7 @@ + * is not returned. + * Not passing the *auxerror into pwquality_strerror() can lead to memory leaks. + * The score depends on PWQ_SETTING_MIN_LENGTH. If it is set higher, +- * the score for the same passwords will be lower. */ ++ * the score for the same passwords will be lower. */ + int + pwquality_check(pwquality_settings_t *pwq, const char *password, + const char *oldpassword, const char *user, void **auxerror); +diff -ruN '--exclude=*.rej' '--exclude=*.orig' a/src/settings.c b/src/settings.c +--- a/src/settings.c 2026-06-24 13:39:51.504810433 +0200 ++++ b/src/settings.c 2026-06-24 13:44:29.312806994 +0200 +@@ -26,9 +26,15 @@ + { + pwquality_settings_t *pwq; + ++ char *allow_classes; ++ + pwq = calloc(1, sizeof(*pwq)); +- if (!pwq) ++ allow_classes = strdup(PWQ_DEFAULT_ALLOWCLASSES); ++ if (!pwq || !allow_classes) { ++ free(pwq); ++ free(allow_classes); + return NULL; ++ } + + pwq->diff_ok = PWQ_DEFAULT_DIFF_OK; + pwq->min_length = PWQ_DEFAULT_MIN_LENGTH; +@@ -43,6 +49,7 @@ + pwq->retry_times = PWQ_DEFAULT_RETRY_TIMES; + pwq->enforce_for_root = PWQ_DEFAULT_ENFORCE_ROOT; + pwq->local_users_only = PWQ_DEFAULT_LOCAL_USERS; ++ pwq->allow_classes = allow_classes; + + return pwq; + } +@@ -54,6 +61,7 @@ + if (pwq) { + free(pwq->dict_path); + free(pwq->bad_words); ++ free(pwq->allow_classes); + free(pwq); + } + } +@@ -79,7 +87,8 @@ + { "dictpath", PWQ_SETTING_DICT_PATH, PWQ_TYPE_STR}, + { "retry", PWQ_SETTING_RETRY_TIMES, PWQ_TYPE_INT}, + { "enforce_for_root", PWQ_SETTING_ENFORCE_ROOT, PWQ_TYPE_SET}, +- { "local_users_only", PWQ_SETTING_LOCAL_USERS, PWQ_TYPE_SET} ++ { "local_users_only", PWQ_SETTING_LOCAL_USERS, PWQ_TYPE_SET}, ++ { "allowclasses", PWQ_SETTING_ALLOW_CLASSES, PWQ_TYPE_STR} + }; + + /* set setting name with value */ +@@ -399,6 +408,10 @@ + free(pwq->dict_path); + pwq->dict_path = dup; + break; ++ case PWQ_SETTING_ALLOW_CLASSES: ++ free(pwq->allow_classes); ++ pwq->allow_classes = dup; ++ break; + default: + free(dup); + return PWQ_ERROR_NON_STR_SETTING; +@@ -490,6 +503,12 @@ + *value = NULL; + #endif + break; ++ case PWQ_SETTING_ALLOW_CLASSES: ++ if (pwq->allow_classes) ++ *value = pwq->allow_classes; ++ else ++ *value = PWQ_DEFAULT_ALLOWCLASSES; ++ break; + default: + return PWQ_ERROR_NON_STR_SETTING; + } diff --git a/libpwquality.spec b/libpwquality.spec index f1a827b..6d8b96d 100644 --- a/libpwquality.spec +++ b/libpwquality.spec @@ -1,7 +1,7 @@ Summary: A library for password generation and password quality checking Name: libpwquality Version: 1.4.5 -Release: 12%{?dist} +Release: 13%{?dist} URL: https://github.com/libpwquality/libpwquality/ Source0: https://github.com/libpwquality/libpwquality/releases/download/libpwquality-%{version}/libpwquality-%{version}.tar.bz2 @@ -10,6 +10,7 @@ Source0: https://github.com/libpwquality/libpwquality/releases/download/libpwqua # https://bugzilla.redhat.com/2165572 # Upstream PR: https://github.com/libpwquality/libpwquality/pull/74 Patch1: setuptools.patch +Patch2: allowed-character-classes.patch # The package is BSD licensed with option to relicense as GPLv2+ # - this option is redundant as the BSD license allows that anyway. @@ -24,6 +25,7 @@ BuildRequires: gcc make BuildRequires: cracklib-devel BuildRequires: gettext BuildRequires: pam-devel +BuildRequires: /usr/bin/pod2man BuildRequires: python3-devel BuildRequires: python3-setuptools @@ -105,6 +107,10 @@ mkdir %{buildroot}%{_secconfdir}/pwquality.conf.d %{python3_sitearch}/*.egg-info %changelog +* Tue Jun 23 2026 Ganna Starovoytova - 1.4.5-13 +- Adds allowing / disabling classes of characters + Resolves: RHEL-78718 + * Tue Oct 29 2024 Troy Dawson - 1.4.5-12 - Bump release for October 2024 mass rebuild: Resolves: RHEL-64018