SSLUtilsTest.java

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.cxf.transport.https;

import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import javax.net.ssl.ExtendedSSLSession;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.X509TrustManager;

import org.apache.cxf.configuration.jsse.TLSClientParameters;
import org.apache.cxf.configuration.jsse.TLSServerParameters;
import org.apache.cxf.configuration.security.ClientAuthentication;
import org.apache.cxf.transport.https.SSLUtils.SSLEngineWrapper;
import org.apache.cxf.transport.https.SSLUtils.X509TrustManagerWrapper;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;


public class SSLUtilsTest {

    private SSLEngine engine;

    @Test
    public void testProtocolConstraints() throws Exception {
        String[] protocols = SSLUtils.getProtocolsToInclude(
            Arrays.asList("TLSv1.3", "TLSv1"),
            Collections.singletonList("TLSv1.3"),
            new String[] {"TLSv1.3", "TLSv1.2"},
            new String[] {"TLSv1", "TLSv1.2", "TLSv1.3"});

        assertThat(protocols, is(new String[] {"TLSv1"}));
    }

    @Test(expected = GeneralSecurityException.class)
    public void testProtocolConstraintsRejectEmptyResult() throws Exception {
        SSLUtils.getProtocolsToInclude(
            Collections.singletonList("TLSv1.3"),
            Collections.emptyList(),
            new String[] {"TLSv1.2"},
            new String[] {"TLSv1.2"});
    }

    @Test
    public void testProtocolConstraintsUseConfiguredContext() throws Exception {
        SSLContext context = SSLContext.getInstance("TLSv1.2");
        context.init(null, null, null);

        String[] protocols = SSLUtils.getProtocolsToInclude(
            Collections.emptyList(),
            Collections.emptyList(),
            context.getDefaultSSLParameters().getProtocols(),
            context.getSupportedSSLParameters().getProtocols());

        assertThat(protocols, is(new String[] {"TLSv1.2"}));
    }
    
    @Before
    public void setUp() throws NoSuchAlgorithmException {
        engine = SSLContext.getDefault().createSSLEngine();
    }
    
    @After
    public void tearDown() throws Exception {
        engine.closeInbound();
        engine.closeOutbound();
        engine = null;
    }

    @Test
    public void testCXF9065() throws NoSuchAlgorithmException, InterruptedException {
        SSLEngineWrapper wrapper = new SSLEngineWrapper(engine);

        for (int i = 0; i < 15000; ++i) {
            wrapper = new SSLEngineWrapper(wrapper);
        }
    
        assertThat(wrapper.getSSLParameters(), is(not(nullValue())));
    }

    @Test
    public void testCreateClientSSLEngineBoundToPeer() throws Exception {
        TLSClientParameters parameters = new TLSClientParameters();
        SSLEngine clientEngine = SSLUtils.createClientSSLEngine(parameters, "api.example.com", 443);

        assertThat(clientEngine.getPeerHost(), is("api.example.com"));
        assertThat(clientEngine.getPeerPort(), is(443));
        assertThat(clientEngine.getUseClientMode(), is(true));
    }

    @Test
    public void testHostnameVerifierWrapsConfiguredTrustManager() throws Exception {
        X509TrustManager trustManager = mock(X509TrustManager.class);
        HostnameVerifier verifier = mock(HostnameVerifier.class);
        TLSClientParameters parameters = new TLSClientParameters();
        parameters.setTrustManagers(new X509TrustManager[] {trustManager});
        parameters.setHostnameVerifier(verifier);

        var initParameters = SSLUtils.getSSLContextInitParameters(parameters, true);

        assertThat(initParameters.getTrustManagers()[0] instanceof X509TrustManagerWrapper, is(true));
        assertThat(parameters.getTrustManagers()[0], is(trustManager));
    }

    /**
     * Regression test: a plain (non-extended) X509TrustManager delegate must still get
     * hostname verification. JSSE endpoint identification is suppressed by
     * SSLEngineWrapper, so skipping the CXF HostnameVerifier for plain delegates left
     * no hostname check at all - any certificate the trust manager accepted (e.g. any
     * cert from a pinned corporate CA, issued for any host) enabled silent MITM.
     */
    @Test
    public void testPlainTrustManagerStillGetsHostnameVerification() throws Exception {
        X509TrustManager plainTrustManager = mock(X509TrustManager.class);
        HostnameVerifier failingVerifier = mock(HostnameVerifier.class);
        when(failingVerifier.verify(anyString(), any())).thenReturn(false);

        ExtendedSSLSession session = mock(ExtendedSSLSession.class);
        when(session.getPeerHost()).thenReturn("evil.example.net");
        when(session.getRequestedServerNames()).thenReturn(Collections.emptyList());
        SSLEngine mockEngine = mock(SSLEngine.class);
        when(mockEngine.getHandshakeSession()).thenReturn(session);

        X509TrustManagerWrapper wrapper =
            new X509TrustManagerWrapper(plainTrustManager, failingVerifier);
        X509Certificate[] chain = new X509Certificate[0];
        try {
            wrapper.checkServerTrusted(chain, "RSA", mockEngine);
            fail("hostname verification must run for plain X509TrustManager delegates");
        } catch (CertificateException expected) {
            // expected: no name matching evil.example.net
        }
        // the delegate's chain validation was still consulted
        verify(plainTrustManager).checkServerTrusted(chain, "RSA");
    }

    @Test
    public void testPlainTrustManagerAcceptedWhenHostnameMatches() throws Exception {
        X509TrustManager plainTrustManager = mock(X509TrustManager.class);
        HostnameVerifier passingVerifier = mock(HostnameVerifier.class);
        when(passingVerifier.verify(anyString(), any())).thenReturn(true);

        ExtendedSSLSession session = mock(ExtendedSSLSession.class);
        when(session.getPeerHost()).thenReturn("service.example.com");
        when(session.getRequestedServerNames()).thenReturn(Collections.emptyList());
        SSLEngine mockEngine = mock(SSLEngine.class);
        when(mockEngine.getHandshakeSession()).thenReturn(session);

        X509TrustManagerWrapper wrapper =
            new X509TrustManagerWrapper(plainTrustManager, passingVerifier);
        wrapper.checkServerTrusted(new X509Certificate[0], "RSA", mockEngine);
        verify(passingVerifier).verify(eq("service.example.com"), any());
    }

    @Test
    public void testServerEngineHonorsConfiguredConstraints() throws Exception {
        TLSServerParameters parameters = new TLSServerParameters();
        parameters.setExcludeProtocols(Arrays.asList("SSLv3", "TLSv1", "TLSv1.1"));
        ClientAuthentication clientAuth = new ClientAuthentication();
        clientAuth.setWant(Boolean.TRUE);
        parameters.setClientAuthentication(clientAuth);

        SSLEngine serverEngine = SSLUtils.createServerSSLEngine(parameters);

        assertThat(serverEngine.getUseClientMode(), is(false));
        assertThat(serverEngine.getWantClientAuth(), is(true));
        List<String> enabled = Arrays.asList(serverEngine.getEnabledProtocols());
        assertThat(enabled.contains("SSLv3"), is(false));
        assertThat(enabled.contains("TLSv1"), is(false));
        assertThat(enabled.contains("TLSv1.1"), is(false));
    }
}