diff --git a/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/certificatevalidation/RevocationVerificationManager.java b/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/certificatevalidation/RevocationVerificationManager.java index 338b8a8a36..e2aa54498a 100644 --- a/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/certificatevalidation/RevocationVerificationManager.java +++ b/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/certificatevalidation/RevocationVerificationManager.java @@ -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 @@ -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) { @@ -49,6 +63,7 @@ public RevocationVerificationManager(Integer cacheAllocatedSize, Integer cacheDe && cacheDelayMins < Constants.CACHE_MAX_DELAY_MINS) { this.cacheDelayMins = cacheDelayMins; } + this.isFullCertChainValidationEnabled = isFullCertChainValidationEnabled; } /** @@ -62,6 +77,69 @@ public void verifyRevocationStatus(javax.security.cert.X509Certificate[] peerCer X509Certificate[] convertedCertificates = convert(peerCertificates); + Optional 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 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(); @@ -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."); diff --git a/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/certificatevalidation/ocsp/OCSPCache.java b/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/certificatevalidation/ocsp/OCSPCache.java index b01d509867..471323e6b3 100644 --- a/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/certificatevalidation/ocsp/OCSPCache.java +++ b/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/certificatevalidation/ocsp/OCSPCache.java @@ -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"); diff --git a/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/certificatevalidation/ocsp/OCSPVerifier.java b/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/certificatevalidation/ocsp/OCSPVerifier.java index d9a3a11039..393127d1b8 100644 --- a/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/certificatevalidation/ocsp/OCSPVerifier.java +++ b/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/certificatevalidation/ocsp/OCSPVerifier.java @@ -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; @@ -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. * @@ -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. } @@ -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); } diff --git a/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/nhttp/config/ClientConnFactoryBuilder.java b/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/nhttp/config/ClientConnFactoryBuilder.java index bf504b9999..5219a29fe5 100644 --- a/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/nhttp/config/ClientConnFactoryBuilder.java +++ b/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/nhttp/config/ClientConnFactoryBuilder.java @@ -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 diff --git a/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/nhttp/config/ServerConnFactoryBuilder.java b/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/nhttp/config/ServerConnFactoryBuilder.java index 817c11e99b..fd4a9fe133 100644 --- a/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/nhttp/config/ServerConnFactoryBuilder.java +++ b/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/nhttp/config/ServerConnFactoryBuilder.java @@ -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; @@ -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, @@ -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")); @@ -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(); } diff --git a/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/passthru/core/ssl/SSLServerConnFactoryBuilder.java b/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/passthru/core/ssl/SSLServerConnFactoryBuilder.java index 282900b956..4b552bd799 100644 --- a/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/passthru/core/ssl/SSLServerConnFactoryBuilder.java +++ b/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/passthru/core/ssl/SSLServerConnFactoryBuilder.java @@ -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, diff --git a/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/util/ConfigurationBuilderUtil.java b/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/util/ConfigurationBuilderUtil.java index f5a81c1572..cadb9e499b 100644 --- a/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/util/ConfigurationBuilderUtil.java +++ b/modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/util/ConfigurationBuilderUtil.java @@ -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 { @@ -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); + } + } + }