From ffbabfc5ec8e135a16e18e28e21d059a5ffcfb1b Mon Sep 17 00:00:00 2001 From: Marco Fargetta Date: Tue, 4 Feb 2025 19:07:20 +0100 Subject: [PATCH] A new policy constraint to enforce p12 password quality. The new *p12ExportPasswordConstraintImpl* constraint allows to check: * `password.minSize` - the minimum size for the password; * `password.minCharCategory` - a csv of minimum characters in each category, they are _capital_, _lower_, _digit_ and _punctuation_; * `password.substringMatch` - the size of substring sequence which cannot be repeated; * `password.maxRepeatedChar` - maximum number of repeating for each character; * `password.cracklibCheck` - a boolean to request an additional check with *cracklib* (it has to be installed if not present). --- base/ca/shared/conf/registry.cfg | 5 +- .../P12ExportPasswordConstraint.java | 174 ++++++++++++++++++ .../main/resources/UserMessages.properties | 6 + docs/changes/v11.6.0/Server-Changes.adoc | 10 + 4 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 base/ca/src/main/java/com/netscape/cms/profile/constraint/P12ExportPasswordConstraint.java diff --git a/base/ca/shared/conf/registry.cfg b/base/ca/shared/conf/registry.cfg index 08fafde2dfe..fe13ad25334 100644 --- a/base/ca/shared/conf/registry.cfg +++ b/base/ca/shared/conf/registry.cfg @@ -1,5 +1,5 @@ types=profile,defaultPolicy,constraintPolicy,profileInput,profileOutput,profileUpdater -constraintPolicy.ids=noConstraintImpl,subjectNameConstraintImpl,uniqueSubjectNameConstraintImpl,userSubjectNameConstraintImpl,cmcSharedTokenSubjectNameConstraintImpl,cmcUserSignedSubjectNameConstraintImpl,caValidityConstraintImpl,validityConstraintImpl,keyUsageExtConstraintImpl,nsCertTypeExtConstraintImpl,extendedKeyUsageExtConstraintImpl,keyConstraintImpl,basicConstraintsExtConstraintImpl,extensionConstraintImpl,signingAlgConstraintImpl,uniqueKeyConstraintImpl,renewGracePeriodConstraintImpl,authzRealmConstraintImpl,externalProcessConstraintImpl +constraintPolicy.ids=noConstraintImpl,subjectNameConstraintImpl,uniqueSubjectNameConstraintImpl,userSubjectNameConstraintImpl,cmcSharedTokenSubjectNameConstraintImpl,cmcUserSignedSubjectNameConstraintImpl,caValidityConstraintImpl,validityConstraintImpl,keyUsageExtConstraintImpl,nsCertTypeExtConstraintImpl,extendedKeyUsageExtConstraintImpl,keyConstraintImpl,basicConstraintsExtConstraintImpl,extensionConstraintImpl,signingAlgConstraintImpl,uniqueKeyConstraintImpl,renewGracePeriodConstraintImpl,authzRealmConstraintImpl,externalProcessConstraintImpl,p12ExportPasswordConstraintImpl constraintPolicy.signingAlgConstraintImpl.class=com.netscape.cms.profile.constraint.SigningAlgConstraint constraintPolicy.signingAlgConstraintImpl.desc=Signing Algorithm Constraint constraintPolicy.signingAlgConstraintImpl.name=Signing Algorithm Constraint @@ -27,6 +27,9 @@ constraintPolicy.nsCertTypeExtConstraintImpl.name=Netscape Certificate Type Exte constraintPolicy.noConstraintImpl.class=com.netscape.cms.profile.constraint.NoConstraint constraintPolicy.noConstraintImpl.desc=No Constraint constraintPolicy.noConstraintImpl.name=No Constraint +constraintPolicy.p12ExportPasswordConstraintImpl.class=com.netscape.cms.profile.constraint.P12ExportPasswordConstraint +constraintPolicy.p12ExportPasswordConstraintImpl.desc=Generated PKCS12 Constraint +constraintPolicy.p12ExportPasswordConstraintImpl.name=Generated PKCS12 Constraint constraintPolicy.subjectNameConstraintImpl.class=com.netscape.cms.profile.constraint.SubjectNameConstraint constraintPolicy.subjectNameConstraintImpl.desc=Subject Name Constraint constraintPolicy.subjectNameConstraintImpl.name=Subject Name Constraint diff --git a/base/ca/src/main/java/com/netscape/cms/profile/constraint/P12ExportPasswordConstraint.java b/base/ca/src/main/java/com/netscape/cms/profile/constraint/P12ExportPasswordConstraint.java new file mode 100644 index 00000000000..1e31573af95 --- /dev/null +++ b/base/ca/src/main/java/com/netscape/cms/profile/constraint/P12ExportPasswordConstraint.java @@ -0,0 +1,174 @@ +// --- BEGIN COPYRIGHT BLOCK --- +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; version 2 of the License. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +// +// (C) 2007 Red Hat, Inc. +// All rights reserved. +// --- END COPYRIGHT BLOCK --- +package com.netscape.cms.profile.constraint; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +import org.mozilla.jss.netscape.security.x509.X509CertInfo; + +import com.netscape.certsrv.profile.ERejectException; +import com.netscape.certsrv.property.EPropertyException; +import com.netscape.certsrv.property.IDescriptor; +import com.netscape.cms.profile.def.NoDefault; +import com.netscape.cms.profile.def.PolicyDefault; +import com.netscape.cmscore.apps.CMS; +import com.netscape.cmscore.request.Request; + +/** + * This class supports renewal grace period, which has two + * parameters: graceBefore and graceAfter + * + * @author Christina Fu + * @version $Revision$, $Date$ + */ +public class P12ExportPasswordConstraint extends EnrollConstraint { + + public static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(P12ExportPasswordConstraint.class); + + public static final String CONFIG_PASSWORD_MIN_SIZE = "password.minSize"; + public static final String CONFIG_PASSWORD_MIN_CHAR_CATEGORY = "password.minCharCategory"; + public static final String CONFIG_PASSWORD_SUBSTRING_MATCH = "password.substringMatch"; + public static final String CONFIG_PASSWORD_MAX_REPEATED_CHAR = "password.maxRepeatedChar"; + public static final String CONFIG_PASSWORD_CRACKLIB_CHECK = "password.cracklibCheck"; + + public P12ExportPasswordConstraint() { + super(); + addConfigName(CONFIG_PASSWORD_MIN_SIZE); + } + + @Override + public void setConfig(String name, String value) + throws EPropertyException { + if (name.equals(CONFIG_PASSWORD_MIN_SIZE) || + name.equals(CONFIG_PASSWORD_SUBSTRING_MATCH) || + name.equals(CONFIG_PASSWORD_MAX_REPEATED_CHAR)) { + try { + Integer.parseInt(value); + } catch (Exception e) { + throw new EPropertyException(CMS.getUserMessage( + "CMS_INVALID_PROPERTY", name)); + } + } + super.setConfig(name, value); + } + + @Override + public IDescriptor getConfigDescriptor(Locale locale, String name) { + return null; + } + + @Override + public void validate(Request req, X509CertInfo info) + throws ERejectException { + String method = "P12ExportPasswordConstraint: validate: "; + String password = req.getExtDataInString("serverSideKeygenP12Passwd"); + String param = getConfig(CONFIG_PASSWORD_MIN_SIZE); + if (!param.equals("") && password.length() < Integer.parseInt(param)) { + throw new ERejectException(CMS.getUserMessage(getLocale(req), + "CMS_PROFILE_P12EXPORT_PASSWORD_TOO_SHORT", param)); + } + + param = getConfig(CONFIG_PASSWORD_MIN_CHAR_CATEGORY); + if (!param.equals("")) { + String[] catCharSize = param.split(","); + String passwordSubset = password.replaceAll("^\\p{Upper}*", "").replaceAll("\\d*$", ""); + int[] catChars = {0, 0, 0, 0}; + if (!password.isEmpty()) { + catChars[0] = passwordSubset.split("(?<=\\p{Upper})", -6).length -1; + catChars[1] = passwordSubset.split("(?<=\\p{Lower})", -6).length -1; + catChars[2] = passwordSubset.split("(?<=\\d)", -6).length -1; + catChars[3] = passwordSubset.split("(?<=\\p{Punct})", -6).length -1; + } + for (int i = 0; i < 4; i++) { + if (catChars[i] < Integer.parseInt(catCharSize[i])) { + throw new ERejectException(CMS.getUserMessage(getLocale(req), + "CMS_PROFILE_P12EXPORT_PASSWORD_FEW_CHARS_CATEGORY", catCharSize)); + } + } + } + + param = getConfig(CONFIG_PASSWORD_SUBSTRING_MATCH); + if (!param.equals("")) { + int seqSize = Integer.parseInt(param); + for (int i = 0; i< password.length() - (seqSize * 2); i++) { + String seq = password.substring(i, i + seqSize); + String invSeq = new StringBuilder(seq).reverse().toString(); + if(password.indexOf(seq, i + seqSize) > 0 || password.indexOf(invSeq, i + seqSize) > 0){ + throw new ERejectException(CMS.getUserMessage(getLocale(req), + "CMS_PROFILE_P12EXPORT_PASSWORD_SEQUENCE")); + } + } + } + + param = getConfig(CONFIG_PASSWORD_MAX_REPEATED_CHAR); + if (!param.equals("")) { + int maxRepeat = Integer.parseInt(param); + int mostRepeatedChar = password.chars().mapToObj(x -> (char) x) + .collect(Collectors.groupingBy(x -> x, Collectors.counting())) + .entrySet().stream().max(Map.Entry.comparingByValue()) + .get().getValue().intValue(); + + if (maxRepeat < mostRepeatedChar) { + throw new ERejectException(CMS.getUserMessage(getLocale(req), + "CMS_PROFILE_P12EXPORT_PASSWORD_REPEATED_CHAR", param)); + } + } + + if (getConfigBoolean(CONFIG_PASSWORD_CRACKLIB_CHECK)) { + try { + Process crack = new ProcessBuilder("/usr/sbin/cracklib-check").start(); + BufferedWriter crackIn = crack.outputWriter(); + BufferedReader crackOut = crack.inputReader(); + crackIn.write(password); + crackIn.close(); + + String crackResult = crackOut.readLine().substring(password.length() + 2); + if (!crackResult.equals("OK")) { + throw new ERejectException(CMS.getUserMessage(getLocale(req), + "CMS_PROFILE_P12EXPORT_PASSWORD_CRACKLIB_FAILS", crackResult)); + } + + } catch (IOException e) { + logger.error(method + "impossible check password with cracklib.", e); + } + + } + } + + @Override + public String getText(Locale locale) { + String[] params = { + getConfig(CONFIG_PASSWORD_MIN_SIZE, "-1"), + getConfig(CONFIG_PASSWORD_MIN_CHAR_CATEGORY, "-1"), + getConfig(CONFIG_PASSWORD_SUBSTRING_MATCH, "-1"), + getConfig(CONFIG_PASSWORD_MAX_REPEATED_CHAR, "-1"), + getConfig(CONFIG_PASSWORD_CRACKLIB_CHECK, "False") + }; + return CMS.getUserMessage(locale, "CMS_PROFILE_CONSTRAINT_P12EXPORT_PASSWORD_TEXT", params); + } + + @Override + public boolean isApplicable(PolicyDefault def) { + return (def instanceof NoDefault); + } +} diff --git a/base/server/src/main/resources/UserMessages.properties b/base/server/src/main/resources/UserMessages.properties index 3b445bebee7..279d28504c6 100644 --- a/base/server/src/main/resources/UserMessages.properties +++ b/base/server/src/main/resources/UserMessages.properties @@ -966,6 +966,12 @@ CMS_PROFILE_CONSTRAINT_VALIDITY_TEXT=This constraint rejects the validity that i CMS_PROFILE_CONSTRAINT_RENEWAL_GRACE_PERIOD_TEXT=This constraint rejects the renewal requests that are outside of the grace period {0} CMS_PROFILE_CONSTRAINT_VALIDITY_RENEWAL_TEXT=This constraint rejects the validity that is not between {0} days. If renewal, grace period is {1} days before and {2} days after the expiration date of the original certificate. CMS_PROFILE_CONSTRAINT_REALM_TEXT=This constraint accepts only specified authorization realms. +CMS_PROFILE_CONSTRAINT_P12EXPORT_PASSWORD_TEXT=This constraint accept export password with minimum size {0}, minimum characters per category {1} (capital,lower,digit,others) excluding initial capital and final digits, no repeated substrings of size {2}, no more {3} repeated character and verified with cracklib {4}. +CMS_PROFILE_P12EXPORT_PASSWORD_TOO_SHORT=PKCS12 password too short. Minimum is {0} +CMS_PROFILE_P12EXPORT_PASSWORD_FEW_CHARS_CATEGORY=PKCS12 password not contain enough character variations. The minimum, beside the initial capitals and the final digits, are: {0} capital, {1} lower, {2} digit and {3} others. +CMS_PROFILE_P12EXPORT_PASSWORD_SEQUENCE=PKCS12 password cannot have repeated sequences (check also inverted). +CMS_PROFILE_P12EXPORT_PASSWORD_REPEATED_CHAR=PKCS12 password cannot contain the same character more then {0} times. +CMS_PROFILE_P12EXPORT_PASSWORD_CRACKLIB_FAILS=PKCS12 password did not pass cracklib test with the message: {0}. CMS_PROFILE_DEF_SIA_TEXT=This default populates a Subject Info Access Extension (1.3.6.1.5.5.7.1.11) to the request. The default values are Criticality={0}, {1} CMS_PROFILE_DEF_AIA_TEXT=This default populates a Authority Info Access Extension (1.3.6.1.5.5.7.1.1) to the request. The default values are Criticality={0}, {1} diff --git a/docs/changes/v11.6.0/Server-Changes.adoc b/docs/changes/v11.6.0/Server-Changes.adoc index be69889f0d7..af82570cf4e 100644 --- a/docs/changes/v11.6.0/Server-Changes.adoc +++ b/docs/changes/v11.6.0/Server-Changes.adoc @@ -40,3 +40,13 @@ A new `pki_authdb_url` parameter has been added for `pkispawn` to replace the fo * `pki_authdb_hostname` * `pki_authdb_port` * `pki_authdb_secure_conn` + +== Implement new policy constraint for p12 password == + +A new policy constraint is defined to enforce the password quality: *p12ExportPasswordConstraintImpl*. The constraint allows to check: + +* `password.minSize` - the minimum size for the password; +* `password.minCharCategory` - a csv of minimum characters in each category, they are _capital_, _lower_, _digit_ and _punctuation_; +* `password.substringMatch` - the size of substring sequence which cannot be repeated; +* `password.maxRepeatedChar` - maximum number of repeating for each character; +* `password.cracklibCheck` - a boolean to request an additional check with *cracklib* (it has to be installed if not present).