CertificateTests.java

/**
 * The MIT License
 *
 * Copyright for portions of unirest-java are held by Kong Inc (c) 2013.
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package BehaviorTests;

import kong.unirest.core.GetRequest;

import kong.unirest.core.Unirest;
import kong.unirest.core.UnirestException;
import kong.unirest.core.java.SSLContextBuilder;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLPeerUnverifiedException;

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
class CertificateTests extends BddTest {

    @Test
    void canDoClientCertificates() throws Exception {
        Unirest.config().clientCertificateStore(TestUtil.readStore(), "badssl.com");

        Unirest.get("https://client.badssl.com/")
                .asString()
                .ifFailure(r -> fail(r.getStatus() + " request failed " + r.getBody()))
                .ifSuccess(r -> System.out.println(" woot "));
        ;
    }


    @Test
    void canLoadKeyStoreByPath() {
        Unirest.config().clientCertificateStore("src/test/resources/certs/badssl.com-client.p12", "badssl.com");

        Unirest.get("https://client.badssl.com/")
                .asString()
                .ifFailure(r -> fail(r.getStatus() + " request failed " + r.getBody()))
                .ifSuccess(r -> System.out.println(" woot "));
        ;
    }

    @Test
    void loadWithSSLContext() throws Exception {
        SSLContext sslContext = SSLContextBuilder.create()
                .loadKeyMaterial(TestUtil.readStore(), "badssl.com".toCharArray()) // use null as second param if you don't have a separate key password
                .build();

        Unirest.config().sslContext(sslContext);

        int response = Unirest.get("https://client.badssl.com/").asEmpty().getStatus();
        assertEquals(200, response);
    }

    @Test
    void loadWithSSLContextAndProtocol() throws Exception {
        SSLContext sslContext = SSLContextBuilder.create()
                .loadKeyMaterial(TestUtil.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 = SSLContextBuilder.create()
                .loadKeyMaterial(TestUtil.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 = SSLContextBuilder.create()
                .loadKeyMaterial(TestUtil.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 = SSLContextBuilder.create()
                .loadKeyMaterial(TestUtil.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 = SSLContextBuilder.create()
                .loadKeyMaterial(TestUtil.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 badName() {
        fails("https://wrong.host.badssl.com/",
                SSLPeerUnverifiedException.class,
                "java.io.IOException: " +
                        "No subject alternative DNS name matching wrong.host.badssl.com found.");
        disableSsl();
        canCall("https://wrong.host.badssl.com/");
    }

    @Test
    void expired() {
        fails("https://expired.badssl.com/",
                SSLHandshakeException.class,
                "java.io.IOException: " +
                        "PKIX path validation failed: " +
                        "java.security.cert.CertPathValidatorException: " +
                        "validity check failed");
        disableSsl();
        canCall("https://expired.badssl.com/");
    }

    @Test
    void selfSigned() {
        fails("https://self-signed.badssl.com/",
                SSLHandshakeException.class,
                "java.io.IOException: PKIX path building failed: " +
                        "sun.security.provider.certpath.SunCertPathBuilderException: " +
                        "unable to find valid certification path to requested target");
        disableSsl();
        canCall("https://self-signed.badssl.com/");
    }

    @Test
    void badNameAsync() {
        failsAsync("https://wrong.host.badssl.com/",
                SSLHandshakeException.class,
                "javax.net.ssl.SSLHandshakeException: " +
                        "No subject alternative DNS name matching wrong.host.badssl.com found.");
        disableSsl();
        canCallAsync("https://wrong.host.badssl.com/");
    }

    @Test
    void expiredAsync() {
        failsAsync("https://expired.badssl.com/",
                SSLHandshakeException.class,
                "javax.net.ssl.SSLHandshakeException: PKIX path validation failed: java.security.cert.CertPathValidatorException: validity check failed");
        disableSsl();
        canCallAsync("https://expired.badssl.com/");
    }

    @Test
    void selfSignedAsync() {
        failsAsync("https://self-signed.badssl.com/",
                SSLHandshakeException.class,
                "javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: " +
                        "unable to find valid certification path to requested target");
        disableSsl();
        canCallAsync("https://self-signed.badssl.com/");
    }

    private void disableSsl() {
        Unirest.config().reset().verifySsl(false);
    }

    private void failsAsync(String url, Class<? extends Throwable> exClass, String error) {
        Exception e = assertThrows(Exception.class, () -> Unirest.get(url).asEmptyAsync().get());
        if (!e.getCause().getClass().isAssignableFrom(exClass)) {
            fail("Expected wrong exception type \n Expected: " + exClass + "\n but got " + e.getCause().getClass());
        }
        assertEquals(error, e.getMessage(), "Wrong Error Message");
    }

    private void fails(String url, Class<? extends Throwable> exClass, String error) {
        Exception e = assertThrows(Exception.class, () -> Unirest.get(url).asEmpty());
        if (!e.getCause().getClass().isAssignableFrom(exClass)) {
            fail("Expected wrong exception type \n Expected: " + exClass + "\n but got " + e.getCause().getClass());
        }
        assertEquals(error, e.getMessage(), "Wrong Error Message");
    }

    private void canCall(String url) {
        assertEquals(200, Unirest.get(url).asEmpty().getStatus());
    }

    private void canCallAsync(String url) {
        try {
            assertEquals(200, Unirest.get(url).asEmptyAsync().get().getStatus());
        } catch (Exception e) {
            fail(e.getMessage());
        }
    }

}