diff --git a/build/checkstyle.xml b/build/checkstyle.xml index b81e43399..492bca3b4 100644 --- a/build/checkstyle.xml +++ b/build/checkstyle.xml @@ -63,7 +63,7 @@ - + diff --git a/unirest/src/main/java/kong/unirest/Config.java b/unirest/src/main/java/kong/unirest/Config.java index a64851e4a..16b36283d 100644 --- a/unirest/src/main/java/kong/unirest/Config.java +++ b/unirest/src/main/java/kong/unirest/Config.java @@ -79,6 +79,8 @@ public class Config { private UniMetric metrics = new NoopMetric(); private long ttl = -1; private SSLContext sslContext; + private String[] ciphers; + private String[] protocols; private CompoundInterceptor interceptor = new CompoundInterceptor(); private HostnameVerifier hostnameVerifier; private String defaultBaseUrl; @@ -105,6 +107,8 @@ private void setDefaults() { keystore = null; keystorePassword = null; sslContext = null; + ciphers = null; + protocols = null; interceptor = new CompoundInterceptor(); this.objectMapper = Optional.of(new JsonObjectMapper()); @@ -256,6 +260,26 @@ public Config hostnameVerifier(HostnameVerifier value) { return this; } + /** + * Set a custom array of ciphers + * @param values the array of ciphers + * @return this config object + */ + public Config ciphers(String... values) { + this.ciphers = values; + return this; + } + + /** + * Set a custom array of protocols + * @param values the array of protocols + * @return this config object + */ + public Config protocols(String... values) { + this.protocols = values; + return this; + } + private void verifySecurityConfig(Object thing) { if(thing != null){ throw new UnirestConfigException("You may only configure a SSLContext OR a Keystore, but not both"); @@ -970,6 +994,20 @@ public HostnameVerifier getHostnameVerifier() { return hostnameVerifier; } + /** + * @return the ciphers for the SSL connection configuration + */ + public String[] getCiphers() { + return ciphers; + } + + /** + * @return the protocols for the SSL connection configuration + */ + public String[] getProtocols() { + return protocols; + } + /** * @return the default base URL */ diff --git a/unirest/src/main/java/kong/unirest/apache/SecurityConfig.java b/unirest/src/main/java/kong/unirest/apache/SecurityConfig.java index a9027ce59..448b3118c 100644 --- a/unirest/src/main/java/kong/unirest/apache/SecurityConfig.java +++ b/unirest/src/main/java/kong/unirest/apache/SecurityConfig.java @@ -106,7 +106,7 @@ private Registry createDisabledSSLContext() throws Exce private SSLConnectionSocketFactory getSocketFactory() { if(sslSocketFactory == null) { - sslSocketFactory = new SSLConnectionSocketFactory(createSslContext(), getHostnameVerifier()); + sslSocketFactory = new SSLConnectionSocketFactory(createSslContext(), config.getProtocols(), config.getCiphers(), getHostnameVerifier()); } return sslSocketFactory; } diff --git a/unirest/src/test/java/BehaviorTests/CertificateTests.java b/unirest/src/test/java/BehaviorTests/CertificateTests.java index c652967b3..73eff61e4 100644 --- a/unirest/src/test/java/BehaviorTests/CertificateTests.java +++ b/unirest/src/test/java/BehaviorTests/CertificateTests.java @@ -25,8 +25,10 @@ package BehaviorTests; +import kong.unirest.GetRequest; import kong.unirest.TestUtil; import kong.unirest.Unirest; +import kong.unirest.UnirestException; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; @@ -53,13 +55,14 @@ import java.security.KeyStore; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; @Disabled // dont normally run these because they depend on badssl.com -public class CertificateTests extends BddTest { +class CertificateTests extends BddTest { @Test - public void canDoClientCertificates() throws Exception { + void canDoClientCertificates() throws Exception { Unirest.config().clientCertificateStore(readStore(), "badssl.com"); Unirest.get("https://client.badssl.com/") @@ -70,7 +73,7 @@ public void canDoClientCertificates() throws Exception { @Test - public void canLoadKeyStoreByPath() { + void canLoadKeyStoreByPath() { Unirest.config().clientCertificateStore("src/test/resources/certs/badssl.com-client.p12", "badssl.com"); Unirest.get("https://client.badssl.com/") @@ -80,7 +83,7 @@ public void canLoadKeyStoreByPath() { } @Test - public void loadWithSSLContext() throws Exception { + void loadWithSSLContext() throws Exception { SSLContext sslContext = SSLContexts.custom() .loadKeyMaterial(readStore(), "badssl.com".toCharArray()) // use null as second param if you don't have a separate key password .build(); @@ -92,7 +95,75 @@ public void loadWithSSLContext() throws Exception { } @Test - public void canSetHoestNameVerifyer() throws Exception { + void loadWithSSLContextAndProtocol() throws Exception { + SSLContext sslContext = SSLContexts.custom() + .loadKeyMaterial(readStore(), "badssl.com".toCharArray()) // use null as second param if you don't have a separate key password + .build(); + + Unirest.config().sslContext(sslContext).protocols("TLSv1.2"); + + int response = Unirest.get("https://client.badssl.com/").asEmpty().getStatus(); + assertEquals(200, response); + } + + @Test + void loadWithSSLContextAndCipher() throws Exception { + SSLContext sslContext = SSLContexts.custom() + .loadKeyMaterial(readStore(), "badssl.com".toCharArray()) // use null as second param if you don't have a separate key password + .build(); + + Unirest.config().sslContext(sslContext).ciphers("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"); + + int response = Unirest.get("https://client.badssl.com/").asEmpty().getStatus(); + assertEquals(200, response); + } + + @Test + void loadWithSSLContextAndCipherAndProtocol() throws Exception { + SSLContext sslContext = SSLContexts.custom() + .loadKeyMaterial(readStore(), "badssl.com".toCharArray()) // use null as second param if you don't have a separate key password + .build(); + + Unirest.config() + .sslContext(sslContext) + .protocols("TLSv1.2") + .ciphers("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"); + + int response = Unirest.get("https://client.badssl.com/").asEmpty().getStatus(); + assertEquals(200, response); + } + + @Test + void sslHandshakeFailsWhenServerIsReceivingAnUnsupportedCipher() throws Exception { + SSLContext sslContext = SSLContexts.custom() + .loadKeyMaterial(readStore(), "badssl.com".toCharArray()) // use null as second param if you don't have a separate key password + .build(); + + Unirest.config() + .sslContext(sslContext) + .protocols("TLSv1.2") + .ciphers("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"); + + GetRequest request = Unirest.get("https://client.badssl.com/"); + assertThrows(UnirestException.class, request::asEmpty); + } + + @Test + void clientPreventsToUseUnsafeProtocol() throws Exception { + SSLContext sslContext = SSLContexts.custom() + .loadKeyMaterial(readStore(), "badssl.com".toCharArray()) // use null as second param if you don't have a separate key password + .build(); + + Unirest.config() + .sslContext(sslContext) + .protocols("SSLv3"); + + GetRequest request = Unirest.get("https://client.badssl.com/"); + assertThrows(UnirestException.class, request::asEmpty); + } + + @Test + void canSetHoestNameVerifyer() throws Exception { Unirest.config().hostnameVerifier(new NoopHostnameVerifier()); int response = Unirest.get("https://badssl.com/").asEmpty().getStatus(); @@ -100,7 +171,7 @@ public void canSetHoestNameVerifyer() throws Exception { } @Test - public void rawApacheClientCert() throws Exception { + void rawApacheClientCert() throws Exception { SSLContext sslContext = SSLContexts.custom() .loadKeyMaterial(readStore(), "badssl.com".toCharArray()) // use null as second param if you don't have a separate key password .build(); @@ -117,7 +188,7 @@ public void rawApacheClientCert() throws Exception { } @Test - public void rawApacheWithConnectionManager() throws Exception { + void rawApacheWithConnectionManager() throws Exception { SSLContext sc = SSLContexts.custom() .loadKeyMaterial(readStore(), "badssl.com".toCharArray()) // use null as second param if you don't have a separate key password .build(); @@ -151,7 +222,7 @@ public void rawApacheWithConnectionManager() throws Exception { } @Test - public void badName() { + void badName() { fails("https://wrong.host.badssl.com/", SSLPeerUnverifiedException.class, "javax.net.ssl.SSLPeerUnverifiedException: " + @@ -162,7 +233,7 @@ public void badName() { } @Test - public void expired() { + void expired() { fails("https://expired.badssl.com/", SSLHandshakeException.class, "javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: " + @@ -172,7 +243,7 @@ public void expired() { } @Test - public void selfSigned() { + void selfSigned() { fails("https://self-signed.badssl.com/", SSLHandshakeException.class, "javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: " + @@ -183,7 +254,7 @@ public void selfSigned() { } @Test - public void badNameAsync() { + void badNameAsync() { failsAsync("https://wrong.host.badssl.com/", SSLPeerUnverifiedException.class, "javax.net.ssl.SSLPeerUnverifiedException: " + @@ -194,7 +265,7 @@ public void badNameAsync() { } @Test - public void expiredAsync() { + void expiredAsync() { failsAsync("https://expired.badssl.com/", SSLHandshakeException.class, "javax.net.ssl.SSLHandshakeException: General SSLEngine problem"); @@ -203,7 +274,7 @@ public void expiredAsync() { } @Test - public void selfSignedAsync() { + void selfSignedAsync() { failsAsync("https://self-signed.badssl.com/", SSLHandshakeException.class, "javax.net.ssl.SSLHandshakeException: General SSLEngine problem");