org.opensaml.security.httpclient.impl.SecurityEnhancedTLSSocketFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.opensaml.security.httpclient.impl.SecurityEnhancedTLSSocketFactory.java

Source

/*
 * Licensed to the University Corporation for Advanced Internet Development, 
 * Inc. (UCAID) under one or more contributor license agreements.  See the 
 * NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The UCAID 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.opensaml.security.httpclient.impl;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;

import net.shibboleth.utilities.java.support.logic.Constraint;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;

import org.apache.http.HttpHost;
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.protocol.HttpContext;
import org.opensaml.security.SecurityException;
import org.opensaml.security.credential.UsageType;
import org.opensaml.security.criteria.UsageCriterion;
import org.opensaml.security.httpclient.HttpClientSecurityConstants;
import org.opensaml.security.trust.TrustEngine;
import org.opensaml.security.x509.BasicX509Credential;
import org.opensaml.security.x509.X509Credential;
import org.opensaml.security.x509.tls.impl.ThreadLocalX509CredentialContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An security-enhanced implementation of HttpClient's TLS-capable {@link LayeredConnectionSocketFactory}.
 * 
 * <p>
 * This implementation wraps an existing TLS socket factory instance, decorating it with additional support for:
 * <ul>
 *     <li>Verifying the server TLS certificate and chain via a {@link TrustEngine<Credential>}
 *         and {@link CriteriaSet} supplied by the HttpClient caller via the {@link HttpContext}.</li>
 *         
 *     <li>Loading and clearing a thread-local instance of {@link X509Credential} used for client TLS.</li>
 * </ul>
 * </p>
 * 
 * <p>
 * The context keys used by this component are as follows, defined in {@link HttpClientSecurityConstants}:
 * <ul>
 *   <li>{@link HttpClientSecurityConstants#CONTEXT_KEY_TRUST_ENGINE}: The trust engine instance used. 
 *        Supplied by the HttpClient caller. Must be an instance of {@link TrustEngine<Credential>}.</li>
 *   <li>{@link HttpClientSecurityConstants#CONTEXT_KEY_CRITERIA_SET}: The criteria set instance used. 
 *        Supplied by the HttpClient caller. Must be an instance of {@link CriteriaSet}. </li>
 *   <li>{@link HttpClientSecurityConstants#CONTEXT_KEY_SERVER_TLS_CREDENTIAL_TRUSTED}: The result of the 
 *       trust evaluation, if it was performed.  Populated by this component.  Will be a {@link Boolean}, 
 *       where <code>true</code> means the server TLS was evaluated as trusted, <code>false</code> means 
 *       the credential was evaluated as untrusted.  A null or missing value means that trust engine 
 *       evaluation was not performed.</li>
 *   <li>{@link HttpClientSecurityConstants#CONTEXT_KEY_CLIENT_TLS_CREDENTIAL}: The client TLS credential used.
 *        Supplied by the HttpClient caller. Must be an instance of {@link X509Credential}.</li>
 * </ul>
 * </p>
 * 
 * <p>
 * If the trust engine context attribute is not populated by the caller, then no trust 
 * evaluation is performed.  This allows use of this implementation with use cases where, given a particular 
 * HttpClient instance, sometimes trust engine evaluation is to be performed, and sometimes not.
 * </p>
 * 
 * <p>
 * Since this implementation may typically be used with and wrap a "no trust" SSL socket factory,
 * an optional instance of {@link X509HostnameVerifier} may also be supplied.  If supplied, hostname 
 * verification will be performed against the new {@link SSLSocket} via 
 * {@link X509HostnameVerifier#verify(String, SSLSocket)}.
 * </p>
 * 
 * <p>
 * If the client TLS context attribute is not populated by the caller, then client TLS is not attempted.
 * </p>
 * 
 * <p>
 * Client TLS support requires use of a compatible {@link java.net.ssl.KeyManager} implementation configured in the 
 * {@link java.net.ssl.SSLContext} of the wrapped {@link LayeredConnectionSocketFactory}, such as
 * {@link org.opensaml.security.x509.tls.impl.ThreadLocalX509CredentialKeyManager}.
 * </p>
 * 
 */
public class SecurityEnhancedTLSSocketFactory implements LayeredConnectionSocketFactory {

    /** Logger. */
    private final Logger log = LoggerFactory.getLogger(SecurityEnhancedTLSSocketFactory.class);

    /** The HttpClient socket factory instance wrapped by this implementation. */
    @Nonnull
    private LayeredConnectionSocketFactory wrappedFactory;

    /** The hostname verifier evaluated by this implementation. */
    @Nullable
    private X509HostnameVerifier hostnameVerifier;

    /**
     * Constructor. 
     * 
     * <p>No hostname verifier is configured in this implementation. (Does not affect whether hostname 
     * is or is not evaluated by the wrapped socket factory).</p>
     * 
     * @param factory the underlying HttpClient socket factory wrapped by this implementation.
     */
    public SecurityEnhancedTLSSocketFactory(@Nonnull final LayeredConnectionSocketFactory factory) {
        this(factory, null);
    }

    /**
     * Constructor. 
     * 
     * @param factory the underlying HttpClient socket factory wrapped by this implementation.
     * @param verifier the hostname verifier evaluated by this implementation
     */
    public SecurityEnhancedTLSSocketFactory(@Nonnull final LayeredConnectionSocketFactory factory,
            @Nullable final X509HostnameVerifier verifier) {
        wrappedFactory = Constraint.isNotNull(factory, "Socket factory was null");
        hostnameVerifier = verifier;
    }

    /** {@inheritDoc} */
    public Socket createSocket(HttpContext context) throws IOException {
        log.trace("In createSocket");
        return wrappedFactory.createSocket(context);
    }

    /** {@inheritDoc} */
    public Socket connectSocket(int connectTimeout, Socket sock, HttpHost host, InetSocketAddress remoteAddress,
            InetSocketAddress localAddress, HttpContext context) throws IOException {

        log.trace("In connectSocket");
        try {
            setup(context);
            Socket socket = wrappedFactory.connectSocket(connectTimeout, sock, host, remoteAddress, localAddress,
                    context);
            performTrustEval(socket, context);
            performHostnameVerification(socket, host.getHostName(), context);
            return socket;
        } finally {
            teardown(context);
        }
    }

    /** {@inheritDoc} */
    public Socket createLayeredSocket(Socket socket, String target, int port, HttpContext context)
            throws IOException {
        log.trace("In createLayeredSocket");
        try {
            setup(context);
            Socket layeredSocket = wrappedFactory.createLayeredSocket(socket, target, port, context);
            performTrustEval(layeredSocket, context);
            performHostnameVerification(layeredSocket, target, context);
            return layeredSocket;
        } finally {
            teardown(context);
        }
    }

    /**
     * Perform trust evaluation by extracting the server TLS {@link X509Credential} from the 
     * {@link SSLSession} and evaluating it via a {@link TrustEngine<Credential>} 
     * and {@link CriteriaSet} supplied by the caller via the {@link HttpContext}.
     * 
     * @param socket the socket instance being processed
     * @param context the HttpClient context being processed
     * 
     * @throws IOException if the server TLS credential is untrusted, or if there is a fatal error
     *           attempting trust evaluation.
     */
    protected void performTrustEval(@Nonnull final Socket socket, @Nonnull final HttpContext context)
            throws IOException {
        if (!(socket instanceof SSLSocket)) {
            log.debug("Socket was not an instance of SSLSocket, skipping trust eval");
            return;
        }
        SSLSocket sslSocket = (SSLSocket) socket;

        log.debug("Attempting to evaluate server TLS credential against supplied TrustEngine and CriteriaSet");

        @SuppressWarnings("unchecked")
        TrustEngine<? super X509Credential> trustEngine = (TrustEngine<? super X509Credential>) context
                .getAttribute(HttpClientSecurityConstants.CONTEXT_KEY_TRUST_ENGINE);
        if (trustEngine == null) {
            log.debug("No trust engine supplied by caller, skipping trust eval");
            return;
        } else {
            log.trace("Saw trust engine of type: {}", trustEngine.getClass().getName());
        }

        CriteriaSet criteriaSet = (CriteriaSet) context
                .getAttribute(HttpClientSecurityConstants.CONTEXT_KEY_CRITERIA_SET);
        if (criteriaSet == null) {
            log.debug("No criteria set supplied by caller, building new criteria set with signing criteria");
            criteriaSet = new CriteriaSet(new UsageCriterion(UsageType.SIGNING));
        } else {
            log.trace("Saw CriteriaSet: {}", criteriaSet);
        }

        X509Credential credential = extractCredential(sslSocket);

        try {
            if (trustEngine.validate(credential, criteriaSet)) {
                log.debug("Credential evaluated as trusted");
                context.setAttribute(HttpClientSecurityConstants.CONTEXT_KEY_SERVER_TLS_CREDENTIAL_TRUSTED,
                        Boolean.TRUE);
            } else {
                log.debug("Credential evaluated as untrusted");
                context.setAttribute(HttpClientSecurityConstants.CONTEXT_KEY_SERVER_TLS_CREDENTIAL_TRUSTED,
                        Boolean.FALSE);
                throw new SSLPeerUnverifiedException(
                        "Trust engine could not establish trust of server TLS credential");
            }
        } catch (SecurityException e) {
            log.error("Trust engine error evaluating credential", e);
            throw new IOException("Trust engine error evaluating credential", e);
        }

    }

    /**
     * Extract the server TLS {@link X509Credential} from the supplied {@link SSLSocket}.
     * 
     * @param sslSocket the SSL socket instance to process
     * @return an X509Credential representing the server TLS entity certificate as well as the 
     *          supplied supporting intermediate certificate chain (if any)
     * @throws IOException if credential data can not be extracted from the socket
     */
    @Nonnull
    protected X509Credential extractCredential(@Nonnull final SSLSocket sslSocket) throws IOException {
        SSLSession session = sslSocket.getSession();
        final Certificate[] peerCertificates = session.getPeerCertificates();
        if (peerCertificates == null || peerCertificates.length < 1) {
            throw new SSLPeerUnverifiedException("SSLSession peer certificates array was null or empty");
        }

        ArrayList<X509Certificate> certChain = new ArrayList<>();
        for (Certificate cert : peerCertificates) {
            certChain.add((X509Certificate) cert);
        }

        final X509Certificate entityCert = certChain.get(0);

        BasicX509Credential credential = new BasicX509Credential(entityCert);
        credential.setEntityCertificateChain(certChain);

        return credential;
    }

    /**
     * Perform hostname verification on the connection represented by the supplied socket.
     * 
     * @param socket the socket instance being processed
     * @param hostname the hostname against which to verify
     * @param context the current HttpClient context instance
     * @throws IOException if an I/O error occurs or the verification process fails
     */
    protected void performHostnameVerification(Socket socket, String hostname, HttpContext context)
            throws IOException {
        if (hostnameVerifier != null && socket instanceof SSLSocket) {
            hostnameVerifier.verify(hostname, (SSLSocket) socket);
        }
    }

    /**
     * Load the {@link ThreadLocalX509CredentialContext} with the client TLS credential obtained from 
     * the {@link HttpContext}.
     * 
     * @param context the HttpContext instance
     */
    protected void setup(@Nullable final HttpContext context) {
        log.trace("Attempting to setup thread-local client TLS X509Credential");
        if (context == null) {
            log.trace("HttpContext was null, skipping thread-local setup");
            return;
        }
        if (!ThreadLocalX509CredentialContext.haveCurrent()) {
            X509Credential credential = (X509Credential) context
                    .getAttribute(HttpClientSecurityConstants.CONTEXT_KEY_CLIENT_TLS_CREDENTIAL);
            if (credential != null) {
                log.trace("Loading ThreadLocalX509CredentialContext with client TLS credential: {}", credential);
                ThreadLocalX509CredentialContext.loadCurrent(credential);
            } else {
                log.trace("HttpContext did not contain a client TLS credential, nothing to do");
            }
        } else {
            log.trace(
                    "ThreadLocalX509CredentialContext was already loaded with client TLS credential, skipping setup");
        }
    }

    /**
     * Clear the {@link ThreadLocalX509CredentialContext} of the client TLS credential obtained from 
     * the {@link HttpContext}.
     * 
     * @param context the HttpContext instance
     */
    protected void teardown(@Nullable final HttpContext context) {
        log.trace("Clearing thread-local client TLS X509Credential");
        ThreadLocalX509CredentialContext.clearCurrent();
    }

}