From 7e903dd9752d93d3bb8c4b75b2e96bbea6eef6c6 Mon Sep 17 00:00:00 2001 From: Matias Peyroti Date: Tue, 10 Nov 2020 12:55:45 -0300 Subject: [PATCH] Optionally bypass certificate validation Certificate validation can be skipped setting a context variable (HTTP_TRUST_ALL_CERTS by default). Intended for development environments. Use with caution, can create a security breach! --- .../java/org/jpos/http/client/HttpQuery.java | 80 +++++++++++++++++-- .../org/jpos/http/client/HttpClientTest.java | 68 ++++++++++++++++ 2 files changed, 141 insertions(+), 7 deletions(-) diff --git a/modules/http-client/src/main/java/org/jpos/http/client/HttpQuery.java b/modules/http-client/src/main/java/org/jpos/http/client/HttpQuery.java index 810960587b..eff3442dad 100644 --- a/modules/http-client/src/main/java/org/jpos/http/client/HttpQuery.java +++ b/modules/http-client/src/main/java/org/jpos/http/client/HttpQuery.java @@ -18,11 +18,26 @@ package org.jpos.http.client; +import static org.jpos.util.Logger.log; + import java.io.IOException; import java.io.Serializable; import java.net.URI; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; import java.util.*; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import javax.security.cert.CertificateExpiredException; + import org.apache.http.*; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; @@ -33,6 +48,8 @@ import org.apache.http.client.methods.*; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.concurrent.FutureCallback; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.TrustAllStrategy; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; @@ -51,6 +68,7 @@ import org.jpos.transaction.AbortParticipant; import org.jpos.util.Destroyable; import org.jpos.util.Log; +import org.jpos.util.LogEvent; import org.jpos.core.Configurable; import org.jpos.core.Configuration; import org.jpos.transaction.Context; @@ -79,13 +97,16 @@ public class HttpQuery extends Log implements AbortParticipant, Configurable, De private String responseName; private String statusName; private String contentTypeName; + private String trustAllCertsName; private String basicAuthenticationName; private RedirectStrategy redirectStrategy; private boolean ignoreNullRequest; - // A shared client for the instance. + // Shared clients for the instance. // Created at configuration time; destroyed when this participant is destroyed. private CloseableHttpAsyncClient httpClient = null; + // This client will be used when certificate validation bypassing is needed + private CloseableHttpAsyncClient unsecureHttpClient = null; public HttpQuery () { super(); @@ -129,7 +150,8 @@ public int prepare (long id, Serializable o) { } } - getHttpClient().execute(httpRequest, httpCtx, new FutureCallback() { + boolean trustAllCerts = ctx.getString(trustAllCertsName, "false").equals("true"); + getHttpClient(trustAllCerts).execute(httpRequest, httpCtx, new FutureCallback() { @Override public void completed(HttpResponse result) { ctx.log (result.getStatusLine()); @@ -197,6 +219,8 @@ public void setConfiguration (Configuration cfg) throws ConfigurationException { preemptiveAuth = cfg.getBoolean("preemptiveAuth", preemptiveAuth); basicAuthenticationName = cfg.get("basicAuthenticationName", ".HTTP_BASIC_AUTHENTICATION"); + trustAllCertsName = cfg.get("trustAllCerts", "HTTP_TRUST_ALL_CERTS"); + // ctx name under which extra http headers could exist at runtime // the object could be a List or String[] (in the "name:value" format) // or a Map @@ -223,20 +247,62 @@ else if ("lax".equals(redirProp)) throw new ConfigurationException("'redirect-strategy' must be 'lax' or 'default'"); } - public CloseableHttpAsyncClient getHttpClient() { + public CloseableHttpAsyncClient getHttpClient(boolean trustAllCerts) { if (httpClient == null) { - setHttpClient(getClientBuilder().build()); + setHttpClient(getClientBuilder(false).build()); httpClient.start(); } - return httpClient; + if (unsecureHttpClient == null) { + setUnsecureHttpClient(getClientBuilder(true).build()); + unsecureHttpClient.start(); + } + return trustAllCerts ? unsecureHttpClient: httpClient; } public void setHttpClient(CloseableHttpAsyncClient httpClient) { this.httpClient = httpClient; } - protected HttpAsyncClientBuilder getClientBuilder() { - return HttpAsyncClients.custom().useSystemProperties().setRedirectStrategy(redirectStrategy); + public void setUnsecureHttpClient(CloseableHttpAsyncClient httpClient) { + this.unsecureHttpClient = httpClient; + } + + protected HttpAsyncClientBuilder getClientBuilder(boolean trustAllCerts) { + HttpAsyncClientBuilder builder = HttpAsyncClients.custom().useSystemProperties() + .setRedirectStrategy(redirectStrategy); + if (trustAllCerts) { + disableSSLVerification(builder); + } + return builder; + } + + private HttpAsyncClientBuilder disableSSLVerification(HttpAsyncClientBuilder builder) { + TrustManager[] wrappedTrustManagers = new TrustManager[] { + new X509TrustManager() { + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void checkClientTrusted(X509Certificate[] certs, String authType) { + } + + public void checkServerTrusted(X509Certificate[] certs, String authType) { + log(new LogEvent(certs.toString() + " " + authType)); + } + } + }; + + SSLContext sc; + try { + sc = SSLContext.getInstance("TLS"); + sc.init(null, wrappedTrustManagers, new SecureRandom()); + return builder.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE).setSSLContext(sc); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + LogEvent evt = new LogEvent(this, "warn"); + evt.addMessage(e); + log(evt); + return builder; + } } private String getURL (Context ctx) { diff --git a/modules/http-client/src/test/java/org/jpos/http/client/HttpClientTest.java b/modules/http-client/src/test/java/org/jpos/http/client/HttpClientTest.java index 6c7a34d934..4c99cd801f 100644 --- a/modules/http-client/src/test/java/org/jpos/http/client/HttpClientTest.java +++ b/modules/http-client/src/test/java/org/jpos/http/client/HttpClientTest.java @@ -23,6 +23,8 @@ import org.jpos.q2.Q2; import org.jpos.transaction.Context; import org.jpos.transaction.TransactionManager; +import org.jpos.transaction.TransactionStatusEvent; +import org.jpos.transaction.TransactionStatusListener; import org.jpos.util.NameRegistrar; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -124,4 +126,70 @@ public void testBasicAuthBadPasswd() { Integer sc = ctx.get ("HTTP_STATUS", 10000L); assertEquals (Integer.valueOf(HttpStatus.SC_UNAUTHORIZED), sc, "Status code should be 401"); } + + @Test + public void testTrustRevokedCertificate() { + Context ctx = new Context(); + ctx.put("HTTP_URL", "https://revoked.badssl.com/"); + ctx.put("HTTP_METHOD", "GET"); + ctx.put("HTTP_TRUST_ALL_CERTS", "true"); + mgr.queue(ctx); + Integer sc = ctx.get ("HTTP_STATUS", 10000L); + assertEquals (Integer.valueOf(HttpStatus.SC_OK), sc, "Status code should be 200"); + } + + @Test + public void testTrustPinnedKeyNotInCertificate() { + Context ctx = new Context(); + ctx.put("HTTP_URL", "https://pinning-test.badssl.com/"); + ctx.put("HTTP_METHOD", "GET"); + ctx.put("HTTP_TRUST_ALL_CERTS", "true"); + mgr.queue(ctx); + Integer sc = ctx.get ("HTTP_STATUS", 10000L); + assertEquals (Integer.valueOf(HttpStatus.SC_OK), sc, "Status code should be 200"); + } + + @Test + public void testTrustSelfSignedCertificate() { + Context ctx = new Context(); + ctx.put("HTTP_URL", "https://self-signed.badssl.com/"); + ctx.put("HTTP_METHOD", "GET"); + ctx.put("HTTP_TRUST_ALL_CERTS", "true"); + mgr.queue(ctx); + Integer sc = ctx.get ("HTTP_STATUS", 10000L); + assertEquals (Integer.valueOf(HttpStatus.SC_OK), sc, "Status code should be 200"); + } + + @Test + public void testTrustUntrustedRootCertificate() { + Context ctx = new Context(); + ctx.put("HTTP_URL", "https://untrusted-root.badssl.com/"); + ctx.put("HTTP_METHOD", "GET"); + ctx.put("HTTP_TRUST_ALL_CERTS", "true"); + mgr.queue(ctx); + Integer sc = ctx.get ("HTTP_STATUS", 10000L); + assertEquals (Integer.valueOf(HttpStatus.SC_OK), sc, "Status code should be 200"); + } + + @Test + public void testTrustExpiredCertificate() { + Context ctx = new Context(); + ctx.put("HTTP_URL", "https://expired.badssl.com/"); + ctx.put("HTTP_METHOD", "GET"); + ctx.put("HTTP_TRUST_ALL_CERTS", "true"); + mgr.queue(ctx); + Integer sc = ctx.get ("HTTP_STATUS", 10000L); + assertEquals (Integer.valueOf(HttpStatus.SC_OK), sc, "Status code should be 200"); + } + + @Test + public void testTrustWrongHostCertificate() { + Context ctx = new Context(); + ctx.put("HTTP_URL", "https://wrong.host.badssl.com/"); + ctx.put("HTTP_METHOD", "GET"); + ctx.put("HTTP_TRUST_ALL_CERTS", "true"); + mgr.queue(ctx); + Integer sc = ctx.get ("HTTP_STATUS", 10000L); + assertEquals (Integer.valueOf(HttpStatus.SC_OK), sc, "Status code should be 200"); + } }