Skip to content

Commit

Permalink
A new policy constraint to enforce p12 password quality.
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
fmarco76 committed Feb 5, 2025
1 parent bc2aa36 commit 3d1d58c
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 22 deletions.
5 changes: 4 additions & 1 deletion base/ca/shared/conf/registry.cfg
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,10 @@ public void execute(Request request) throws EProfileException, ERejectException
logger.info("CAEnrollProfile: Processing server-side keygen enrollment");
request.setExtData(Request.SSK_STAGE, Request.SSK_STAGE_KEYGEN);

Map<String, byte[]> passwordsGenerated = convertP12Password(request);
transWrappedSessionKey = passwordsGenerated.get("serverSideKeygenP12PasswdTransSession");
Map<String, byte[]> p12PasswordInfo = processP12Password(request);
transWrappedSessionKey = p12PasswordInfo.get("serverSideKeygenP12PasswdTransSession");

sessionWrappedPassphrase = passwordsGenerated.get("serverSideKeygenP12PasswdEnc");
sessionWrappedPassphrase = p12PasswordInfo.get("serverSideKeygenP12PasswdEnc");


try {
Expand Down Expand Up @@ -532,8 +532,17 @@ public void execute(Request request) throws EProfileException, ERejectException
}
}

private Map<String, byte[]> convertP12Password (Request request) throws EProfileException {
String method = "CAEnrollProfile: convertP12Password: ";
/**
* Read the p12 password and generate symmetric keys
*
* The password is read from the request and removed after the keys are generated.
*
* @param request
* @return symmetric keys
* @throws EProfileException
*/
private Map<String, byte[]> processP12Password (Request request) throws EProfileException {
String method = "CAEnrollProfile: processP12Password: ";
Map<String, byte[]> returnPass = null;

String p12passwd = request.getExtDataInString("serverSideKeygenP12Passwd");
Expand All @@ -551,29 +560,15 @@ private Map<String, byte[]> convertP12Password (Request request) throws EProfile
String transportNickname = kraConnectorConfig.getString("transportCertNickname", "KRA Transport Certificate");
transCert = cm.findCertByNickname(transportNickname);
} catch (Exception e) {
logger.debug(method + "'KRA transport certificate' not found in nssdb; need to be manually setup for Server-Side keygen enrollment");
logger.error(method + "'KRA transport certificate' not found in nssdb; need to be manually setup for Server-Side keygen enrollment");
throw new EProfileException(CMS.getUserMessage("CMS_MISSING_KRA_TRANSPORT_CERT_IN_CA_NSSDB"));

/* future; cert nickname can't be controlled yet at import in jss
logger.debug(method + "KRA transport certificate not found in nssdb; getting from CS.cfg");
transportCertStr = connectorsConfig.getString("KRA.transportCert", "");
logger.debug(method + "transportCert found in CS.cfg: " + transportCertStr);
byte[] transportCertB = Utils.base64decode(transportCertStr);
logger.debug(method + "transportCertB.length=" + transportCertB.length);
// hmmm, can't yet control the nickname
transCert = cm.importCACertPackage(transportCertB);
logger.debug(method + "KRA transport certificate imported");
*/
}

try
{
// todo: make things configurable in CS.cfg or profile
CryptoToken ct =
CryptoUtil.getCryptoToken(CryptoUtil.INTERNAL_TOKEN_NAME);
if (ct == null)
logger.debug(method + "crypto token null");

EncryptionAlgorithm encryptAlgorithm =
EncryptionAlgorithm.AES_128_CBC_PAD;
Expand All @@ -582,7 +577,7 @@ private Map<String, byte[]> convertP12Password (Request request) throws EProfile
boolean useOAEP = caCfg.getUseOAEPKeyWrap();

KeyWrapAlgorithm wrapAlgorithm = KeyWrapAlgorithm.RSA;
if(useOAEP == true) {
if(useOAEP) {
wrapAlgorithm = KeyWrapAlgorithm.RSA_OAEP;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// --- 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) 2025 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 .............
*
* @author Marco Fargetta {@literal <mfargett@redhat.com>}
*/
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);
}
}
6 changes: 6 additions & 0 deletions base/server/src/main/resources/UserMessages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
10 changes: 10 additions & 0 deletions docs/changes/v11.6.0/Server-Changes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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).

0 comments on commit 3d1d58c

Please sign in to comment.