Skip to content

Commit

Permalink
Add cert approval callback for JSSTrustManager
Browse files Browse the repository at this point in the history
Previously the JSSTrustManager would throw an exception to
reject a cert if it found an issue in the cert. The code has
been changed to collect all issues, then pass them into an
optional callback object. If the callback approves it, the
cert will be considered trusted. Otherwise, or if there is
no callback, the code will throw an exception based on the
first issue.
  • Loading branch information
edewata committed Aug 8, 2024
1 parent 2e9695e commit 3853fee
Showing 1 changed file with 84 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@
package org.mozilla.jss.provider.javax.crypto;

import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;

import javax.net.ssl.X509TrustManager;
Expand All @@ -32,6 +35,9 @@
import org.mozilla.jss.NotInitializedException;
import org.mozilla.jss.netscape.security.util.Cert;
import org.mozilla.jss.pkcs11.PK11Cert;
import org.mozilla.jss.ssl.SSLCertificateApprovalCallback;
import org.mozilla.jss.ssl.SSLCertificateApprovalCallback.ValidityItem;
import org.mozilla.jss.ssl.SSLCertificateApprovalCallback.ValidityStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -43,11 +49,20 @@ public class JSSTrustManager implements X509TrustManager {
public static final String CLIENT_AUTH_OID = "1.3.6.1.5.5.7.3.2";

private boolean allowMissingExtendedKeyUsage = false;
private SSLCertificateApprovalCallback callback;

public void configureAllowMissingExtendedKeyUsage(boolean allow) {
allowMissingExtendedKeyUsage = allow;
}

public SSLCertificateApprovalCallback getCallback() {
return callback;
}

public void setCallback(SSLCertificateApprovalCallback certCallback) {
this.callback = certCallback;
}

public void checkCertChain(X509Certificate[] certChain, String keyUsage) throws Exception {

logger.debug("JSSTrustManager: checkCertChain(" + keyUsage + ")");
Expand All @@ -59,13 +74,47 @@ public void checkCertChain(X509Certificate[] certChain, String keyUsage) throws
logger.debug("JSSTrustManager: - " + cert.getSubjectX500Principal());
}

X509Certificate leafCert = certChain[certChain.length - 1];

ValidityStatus status = new ValidityStatus();
checkCertChain(certChain, keyUsage, status);

Enumeration<ValidityItem> reasons = status.getReasons();
if (!reasons.hasMoreElements()) {
logger.debug("JSSTrustManager: Trusted cert: " + leafCert.getSubjectX500Principal());
return;
}

if (callback != null && callback.approve(leafCert, status)) {
logger.debug("JSSTrustManager: Approved cert: " + leafCert.getSubjectX500Principal());
return;
}

// throw an exception based on the first issue
ValidityItem issue = reasons.nextElement();

// TODO: use enum
switch (issue.getReason()) {
case ValidityStatus.EXPIRED_CERTIFICATE:
throw new CertificateExpiredException("Expired certificate: " + leafCert.getSubjectX500Principal());
case ValidityStatus.INADEQUATE_KEY_USAGE:
throw new CertificateException("Inadequate key usage: " + leafCert.getSubjectX500Principal());
case ValidityStatus.UNTRUSTED_ISSUER:
throw new CertificateException("Untrusted issuer: " + leafCert.getSubjectX500Principal());
default:
throw new CertificateException("Invalid certificate: " + leafCert.getSubjectX500Principal());
}
}

public void checkCertChain(X509Certificate[] certChain, String keyUsage, ValidityStatus status) throws Exception {

if (!isTrustedPeer(certChain)) {
checkIssuerTrusted(certChain);
checkIssuerTrusted(certChain, status);
}

checkValidityDates(certChain);
checkValidityDates(certChain, status);

checkKeyUsage(certChain, keyUsage);
checkKeyUsage(certChain, keyUsage, status);
}

public boolean isTrustedPeer(X509Certificate[] certChain) throws Exception {
Expand All @@ -89,21 +138,21 @@ public boolean isTrustedPeer(X509Certificate[] certChain) throws Exception {
sslTrust);
}

public void checkIssuerTrusted(X509Certificate[] certChain) throws Exception {
public void checkIssuerTrusted(X509Certificate[] certChain, ValidityStatus status) throws Exception {

// get CA certs
X509Certificate[] caCerts = getAcceptedIssuers();

// validating signature from root to leaf
for (X509Certificate cert : certChain) {
checkSignature(cert, caCerts);
checkSignature(cert, caCerts, status);

// use the current cert as the CA cert for the next cert in the chain
caCerts = new X509Certificate[] { cert };
}
}

public void checkSignature(X509Certificate cert, X509Certificate[] caCerts) throws Exception {
public void checkSignature(X509Certificate cert, X509Certificate[] caCerts, ValidityStatus status) throws Exception {

logger.debug("JSSTrustManager: Checking signature of cert 0x" + cert.getSerialNumber().toString(16));
logger.debug("JSSTrustManager: - subject: " + cert.getSubjectX500Principal());
Expand Down Expand Up @@ -131,25 +180,45 @@ public void checkSignature(X509Certificate cert, X509Certificate[] caCerts) thro
}

if (issuer == null) {
throw new CertificateException("Unable to validate signature: " + cert.getSubjectX500Principal());
logger.debug("JSSTrustManager: Untrusted issuer: " + cert.getIssuerX500Principal());

// TODO: fix depth param
status.addReason(ValidityStatus.UNTRUSTED_ISSUER, cert, 0);

return;
}

logger.debug("JSSTrustManager: cert signed by " + issuer.getSubjectX500Principal());
logger.debug("JSSTrustManager: Trusted issuer: " + issuer.getSubjectX500Principal());
}

public void checkValidityDates(X509Certificate[] certChain) throws Exception {
public void checkValidityDates(X509Certificate[] certChain, ValidityStatus status) throws Exception {

for (X509Certificate cert : certChain) {

logger.debug("JSSTrustManager: Checking validity dates of cert 0x" + cert.getSerialNumber().toString(16));
logger.debug("JSSTrustManager: - not before: " + cert.getNotBefore());
logger.debug("JSSTrustManager: - not after: " + cert.getNotAfter());

cert.checkValidity();
try {
cert.checkValidity();

} catch (CertificateNotYetValidException e) {
logger.debug("JSSTrustManager: Cert not yet valid: " + cert.getSubjectX500Principal());

// NSS uses EXPIRED_CERTIFICATE for this case in CERT_CheckCertValidTimes()
// TODO: fix depth param
status.addReason(ValidityStatus.EXPIRED_CERTIFICATE, cert, 0);

} catch (CertificateExpiredException e) {
logger.debug("JSSTrustManager: Cert has expired: " + cert.getSubjectX500Principal());

// TODO: fix depth param
status.addReason(ValidityStatus.EXPIRED_CERTIFICATE, cert, 0);
}
}
}

public void checkKeyUsage(X509Certificate[] certChain, String keyUsage) throws Exception {
public void checkKeyUsage(X509Certificate[] certChain, String keyUsage, ValidityStatus status) throws Exception {

// validating key usage on leaf cert only
X509Certificate cert = certChain[certChain.length - 1];
Expand All @@ -175,25 +244,15 @@ public void checkKeyUsage(X509Certificate[] certChain, String keyUsage) throws E
return;
}

String msg = "Missing EKU: " + keyUsage +
". Certificate with subject DN `" + cert.getSubjectX500Principal() + "` had ";

if (extendedKeyUsages == null) {
msg += "no EKU extension";
logger.debug("JSSTrustManager: Missing extended key usage extension");

} else {
msg += "EKUs { ";
boolean first = true;
for (String eku : extendedKeyUsages) {
if (!first) msg += " , ";
msg += eku;
first = false;
}
msg += " }";
logger.debug("JSSTrustManager: Missing " + keyUsage + " key usage");
}

msg += ". class = " + cert.getClass();
throw new CertificateException(msg);
// TODO: fix depth param
status.addReason(ValidityStatus.INADEQUATE_KEY_USAGE, cert, 0);
}

@Override
Expand Down

0 comments on commit 3853fee

Please sign in to comment.