Skip to content

Commit

Permalink
Do cert revocation improvement
Browse files Browse the repository at this point in the history
  • Loading branch information
Akila94 committed Dec 1, 2023
1 parent ce6c782 commit 973a6d8
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,21 @@
import org.apache.synapse.transport.certificatevalidation.ocsp.OCSPCache;
import org.apache.synapse.transport.certificatevalidation.ocsp.OCSPVerifier;
import org.apache.synapse.transport.certificatevalidation.pathvalidation.CertificatePathValidator;
import org.apache.synapse.transport.util.ConfigurationBuilderUtil;
import org.wso2.securevault.KeyStoreType;

import java.io.ByteArrayInputStream;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Optional;

/**
* Manager class responsible for verifying certificates. This class will use the available verifiers according to
Expand All @@ -37,9 +49,11 @@ public class RevocationVerificationManager {

private int cacheSize = Constants.CACHE_DEFAULT_ALLOCATED_SIZE;
private int cacheDelayMins = Constants.CACHE_DEFAULT_DELAY_MINS;
private boolean isFullCertChainValidationEnabled = false;
private static final Log log = LogFactory.getLog(RevocationVerificationManager.class);

public RevocationVerificationManager(Integer cacheAllocatedSize, Integer cacheDelayMins) {
public RevocationVerificationManager(Integer cacheAllocatedSize, Integer cacheDelayMins,
boolean isFullCertChainValidationEnabled) {

if (cacheAllocatedSize != null && cacheAllocatedSize > Constants.CACHE_MIN_ALLOCATED_SIZE
&& cacheAllocatedSize < Constants.CACHE_MAX_ALLOCATED_SIZE) {
Expand All @@ -49,6 +63,7 @@ public RevocationVerificationManager(Integer cacheAllocatedSize, Integer cacheDe
&& cacheDelayMins < Constants.CACHE_MAX_DELAY_MINS) {
this.cacheDelayMins = cacheDelayMins;
}
this.isFullCertChainValidationEnabled = isFullCertChainValidationEnabled;
}

/**
Expand All @@ -62,6 +77,69 @@ public void verifyRevocationStatus(javax.security.cert.X509Certificate[] peerCer

X509Certificate[] convertedCertificates = convert(peerCertificates);

Optional<X509Certificate> peerCertOpt;
X509Certificate peerCert = null;
X509Certificate issuerCert = null;
String alias;

if (!isFullCertChainValidationEnabled) {

if (log.isDebugEnabled()) {
log.debug("Retrieving the issuer certificate from client truststore since full certificate chain " +
"validation is disabled");
}

KeyStore trustStore;
Enumeration<String> aliases;
String truststorePath = System.getProperty("javax.net.ssl.trustStore");
String truststorePassword = System.getProperty("javax.net.ssl.trustStorePassword");;

try {
trustStore = ConfigurationBuilderUtil.getKeyStore(truststorePath, truststorePassword,
KeyStoreType.JKS.toString());
} catch (KeyStoreException e) {
throw new CertificateVerificationException("Error loading the truststore", e);
}

try {
aliases = trustStore.aliases();
} catch (KeyStoreException e) {
throw new CertificateVerificationException("Error while retrieving aliases from truststore", e);
}

while (aliases.hasMoreElements()) {
alias = aliases.nextElement();
try {
issuerCert = (X509Certificate) trustStore.getCertificate(alias);
} catch (KeyStoreException e) {
throw new CertificateVerificationException("Unable to read the certificate from truststore with " +
"the alias: " + alias, e);
}

if (issuerCert == null) {
throw new CertificateVerificationException("Issuer certificate not found in truststore");
}

// When full chain validation is disabled, only one cert is expected
peerCertOpt = Arrays.stream(convertedCertificates).findFirst();
if (peerCertOpt.isPresent()) {
peerCert = peerCertOpt.get();
} else {
throw new CertificateVerificationException("Peer certificate is not provided");
}

try {
peerCert.verify(issuerCert.getPublicKey());
log.debug("Valid issuer certificate found in the client truststore");
break;
} catch (SignatureException | CertificateException | NoSuchAlgorithmException | InvalidKeyException |
NoSuchProviderException e) {
// Unable to verify the signature. Check with the next certificate.
log.error("Signature not matching for alias : " + alias + ", validate with next cert");
}
}
}

long start = System.currentTimeMillis();

OCSPCache ocspCache = OCSPCache.getCache();
Expand All @@ -73,9 +151,19 @@ public void verifyRevocationStatus(javax.security.cert.X509Certificate[] peerCer

for (RevocationVerifier verifier : verifiers) {
try {
CertificatePathValidator pathValidator = new CertificatePathValidator(convertedCertificates, verifier);
pathValidator.validatePath();
log.info("Path verification Successful. Took " + (System.currentTimeMillis() - start) + " ms.");
if (isFullCertChainValidationEnabled) {
log.debug("Doing full certificate chain validation");
CertificatePathValidator pathValidator = new CertificatePathValidator(convertedCertificates,
verifier);
pathValidator.validatePath();
log.info("Path verification Successful. Took " + (System.currentTimeMillis() - start) + " ms.");
} else {
if (log.isDebugEnabled()) {
log.debug("Validating client certificate with the issuer certificate retrieved from" +
"the trust store");
}
verifier.checkRevocationStatus(peerCert, issuerCert);
}
return;
} catch (Exception e) {
log.info(verifier.getClass().getSimpleName() + " failed.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ private synchronized void replaceNewCacheValue(OCSPCacheValue cacheValue){
try {
String serviceUrl = cacheValue.serviceUrl;
OCSPReq request = cacheValue.request;
OCSPResp response= ocspVerifier.getOCSPResponce(serviceUrl, request);
OCSPResp response= ocspVerifier.getOCSPResponse(serviceUrl, request);

if (OCSPResponseStatus.SUCCESSFUL != response.getStatus())
throw new CertificateVerificationException("OCSP response status not SUCCESSFUL");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.bouncycastle.asn1.*;
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
import org.bouncycastle.asn1.ocsp.OCSPResponseStatus;
Expand Down Expand Up @@ -52,6 +59,12 @@ public OCSPVerifier(OCSPCache cache) {
this.cache = cache;
}

public static final String CONTENT_TYPE = "Content-Type";
public static final String JSON_TYPE ="application/json";
public static final String ACCEPT_TYPE = "Accept";
public static final String OCSP_REQUEST_TYPE = "application/ocsp-request";
public static final String OCSP_RESPONSE_TYPE = "application/ocsp-response";

/**
* Gets the revocation status (Good, Revoked or Unknown) of the given peer certificate.
*
Expand Down Expand Up @@ -83,7 +96,7 @@ public RevocationStatus checkRevocationStatus(X509Certificate peerCert, X509Cert

SingleResp[] responses;
try {
OCSPResp ocspResponse = getOCSPResponce(serviceUrl, request);
OCSPResp ocspResponse = getOCSPResponse(serviceUrl, request);
if (OCSPResponseStatus.SUCCESSFUL != ocspResponse.getStatus()) {
continue; // Server didn't give the response right.
}
Expand Down Expand Up @@ -128,37 +141,34 @@ private RevocationStatus getRevocationStatus(SingleResp resp) throws Certificate
* @throws CertificateVerificationException
*
*/
protected OCSPResp getOCSPResponce(String serviceUrl, OCSPReq request) throws CertificateVerificationException {
protected OCSPResp getOCSPResponse(String serviceUrl, OCSPReq request) throws CertificateVerificationException {

try {
//Todo: Use http client.
byte[] array = request.getEncoded();
if (serviceUrl.startsWith("http")) {
HttpURLConnection con;
URL url = new URL(serviceUrl);
con = (HttpURLConnection) url.openConnection();
con.setRequestProperty("Content-Type", "application/ocsp-request");
con.setRequestProperty("Accept", "application/ocsp-response");
con.setDoOutput(true);
OutputStream out = con.getOutputStream();
DataOutputStream dataOut = new DataOutputStream(new BufferedOutputStream(out));
dataOut.write(array);

dataOut.flush();
dataOut.close();

//Check errors in response:
if (con.getResponseCode() / 100 != 2) {
throw new CertificateVerificationException("Error getting ocsp response." +
"Response code is " + con.getResponseCode());
}
if (log.isDebugEnabled()) {
log.debug("Initiating HTTP request to URL: " + serviceUrl + " to get the OCSP response");
}

try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
HttpPost httpPost = new HttpPost(serviceUrl);

// adding request timeout configurations
if (httpPost.getConfig() == null) {
httpPost.setConfig(RequestConfig.custom().build());
}

//Get Response
InputStream in = (InputStream) con.getContent();
return new OCSPResp(in);
} else {
throw new CertificateVerificationException("Only http is supported for ocsp calls");
httpPost.addHeader(CONTENT_TYPE, OCSP_REQUEST_TYPE);
httpPost.addHeader(ACCEPT_TYPE, OCSP_RESPONSE_TYPE);
httpPost.setEntity(new ByteArrayEntity(request.getEncoded(), ContentType.create(JSON_TYPE)));
HttpResponse httpResponse = client.execute(httpPost);

//Check errors in response, if response status code is not 200 (success) range, throws exception
// eg: if response code is 200 (success) or 201 (accepted) return true,
// if response code is 404 (not found) or 500 throw exception
if (httpResponse.getStatusLine().getStatusCode() / 100 != 2) {
throw new CertificateVerificationException("Error getting ocsp response." +
"Response code is " + httpResponse.getStatusLine().getStatusCode());
}
InputStream in = httpResponse.getEntity().getContent();
return new OCSPResp(in);
} catch (IOException e) {
throw new CertificateVerificationException("Cannot get ocspResponse from url: " + serviceUrl, e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public ClientConnFactoryBuilder parseSSL() throws AxisFault {
cacheDelay = new Integer(cacheDelayString);
} catch (NumberFormatException e) {
}
revocationVerifier = new RevocationVerificationManager(cacheSize, cacheDelay);
revocationVerifier = new RevocationVerificationManager(cacheSize, cacheDelay, true);
}

// Process HttpProtocols
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.apache.axis2.description.Parameter;
import org.apache.axis2.description.TransportInDescription;
import org.apache.axis2.transport.base.ParamUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpHost;
Expand Down Expand Up @@ -305,8 +306,22 @@ public ServerConnFactoryBuilder parseSSL() throws AxisFault {
cacheSize = new Integer(cacheSizeString);
cacheDelay = new Integer(cacheDelayString);
}
catch (NumberFormatException e) {}
revocationVerifier = new RevocationVerificationManager(cacheSize, cacheDelay);
catch (NumberFormatException e) {
throw new AxisFault("Cache size or Cache delay values are malformed", e);
}

// Checking whether the full certificate chain validation is enabled or not.
boolean isFullCertChainValidationEnabled = true;
OMElement isFullCertChainValidationConfig = cvp.getParameterElement()
.getFirstChildWithName(new QName("FullChainValidation"));

if (isFullCertChainValidationConfig != null
&& StringUtils.equals("false", isFullCertChainValidationConfig.getText())) {
isFullCertChainValidationEnabled = false;
}

revocationVerifier = new RevocationVerificationManager(cacheSize, cacheDelay,
isFullCertChainValidationEnabled);
}

ssl = createSSLContext(keyStoreEl, trustStoreEl, clientAuthEl, httpsProtocolsEl, preferredCiphersEl,
Expand All @@ -324,6 +339,8 @@ public ServerConnFactoryBuilder parseMultiProfileSSL() throws AxisFault {
OMElement profilesEl = profileParam.getParameterElement();
SecretResolver secretResolver = SecretResolverFactory.create(profilesEl, true);
Iterator<?> profiles = profilesEl.getChildrenWithName(new QName("profile"));
RevocationVerificationManager revocationVerifier = null;

while (profiles.hasNext()) {
OMElement profileEl = (OMElement) profiles.next();
OMElement bindAddressEl = profileEl.getFirstChildWithName(new QName("bindAddress"));
Expand All @@ -341,8 +358,48 @@ public ServerConnFactoryBuilder parseMultiProfileSSL() throws AxisFault {
OMElement preferredCiphersEl = profileEl.getFirstChildWithName(new QName(NhttpConstants.PREFERRED_CIPHERS));
final Parameter sslpParameter = transportIn.getParameter("SSLProtocol");
final String sslProtocol = sslpParameter != null ? sslpParameter.getValue().toString() : "TLS";

/* If multi SSL profiles are configured, checking whether the certificate revocation verifier is
configured and full certificate chain validation is enabled or not. */
if (profileEl.getFirstChildWithName(new QName("CertificateRevocationVerifier")) != null) {

Integer cacheSize = null;
Integer cacheDelay = null;

OMElement revocationVerifierConfig = profileEl
.getFirstChildWithName(new QName("CertificateRevocationVerifier"));
OMElement revocationEnabled = revocationVerifierConfig
.getFirstChildWithName(new QName("Enable"));

if (revocationEnabled != null && "true".equals(revocationEnabled.getText())) {
String cacheSizeString = revocationVerifierConfig
.getFirstChildWithName(new QName("CacheSize")).getText();
String cacheDelayString = revocationVerifierConfig
.getFirstChildWithName(new QName("CacheDelay")).getText();

try {
cacheSize = new Integer(cacheSizeString);
cacheDelay = new Integer(cacheDelayString);
} catch (NumberFormatException e) {
throw new AxisFault("Cache size or Cache delay values are malformed", e);
}
}

boolean isFullCertChainValidationEnabled = true;
OMElement isFullCertChainValidationConfig = revocationVerifierConfig
.getFirstChildWithName(new QName("FullChainValidation"));

if (isFullCertChainValidationConfig != null
&& StringUtils.equals("false", isFullCertChainValidationConfig.getText())) {
isFullCertChainValidationEnabled = false;
}

revocationVerifier = new RevocationVerificationManager(cacheSize, cacheDelay,
isFullCertChainValidationEnabled);
}

SSLContextDetails ssl = createSSLContext(keyStoreEl, trustStoreEl, clientAuthEl, httpsProtocolsEl,
preferredCiphersEl, null, sslProtocol, secretResolver);
preferredCiphersEl, revocationVerifier, sslProtocol, secretResolver);
if (sslByIPMap == null) {
sslByIPMap = new HashMap<InetSocketAddress, SSLContextDetails>();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public ServerConnFactoryBuilder parseSSL(OMElement keyStoreEl, OMElement trustSt
} catch (NumberFormatException e) {
log.error("Please specify correct Integer numbers for CacheDelay and CacheSize");
}
revocationVerifier = new RevocationVerificationManager(cacheSize, cacheDelay);
revocationVerifier = new RevocationVerificationManager(cacheSize, cacheDelay, true);
}
ssl = createSSLContext(keyStoreEl, trustStoreEl, clientAuthEl, httpsProtocolsEl, preferredCiphers,
revocationVerifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.Properties;

public class ConfigurationBuilderUtil {
Expand Down Expand Up @@ -171,4 +178,28 @@ public static String getStringProperty(String name, String def, Properties props
return val == null ? def : val;
}

/**
* Returns the keystore of the given file path.
*
* @param keyStoreFilePath the keystore file path
* @param keyStorePassword the keystore password
* @param keyStoreType the keystore type
* @return KeyStore
* @throws KeyStoreException On error while creating keystore
*/
public static KeyStore getKeyStore(String keyStoreFilePath, String keyStorePassword, String keyStoreType)
throws KeyStoreException {

String file = new File(keyStoreFilePath).getAbsolutePath();
try (FileInputStream keyStoreFileInputStream = new FileInputStream(file)) {
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(keyStoreFileInputStream, keyStorePassword.toCharArray());
return keyStore;
} catch (IOException | NoSuchAlgorithmException | CertificateException e) {
String errorMessage = String.format("Keystore file does not exist in the path as configured " +
"in '%s' property.", keyStoreFilePath);
throw new KeyStoreException(errorMessage);
}
}

}

0 comments on commit 973a6d8

Please sign in to comment.