org.glite.security.util.HostNameChecker.java Source code

Java tutorial

Introduction

Here is the source code for org.glite.security.util.HostNameChecker.java

Source

/*
 * Copyright (c) Members of the EGEE Collaboration. 2004. See
 * http://www.eu-egee.org/partners/ for details on the copyright holders.
 * 
 * Licensed 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.glite.security.util;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.cert.Certificate;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;

import javax.net.ssl.SSLSocket;
import javax.security.auth.x500.X500Principal;

import org.apache.log4j.Logger;
import org.bouncycastle.asn1.x509.GeneralName;

/**
 * A class to do hostname checking against a certificate to check whether the server answers with a certificate that is
 * allowed for that host. Follows the server identity part of RFC 2818.
 * 
 * @author Joni Hahkala
 */
public class HostNameChecker {
    /** Logging facility. */
    private static final Logger LOGGER = Logger.getLogger(HostNameChecker.class);

    /** The pattern to check whether the string appears to be an IP address. */
    public static final Pattern ipPattern = Pattern.compile("[\\d\\.]+|[\\d\\:]+");

    /** The localhost IPv4 address (only the exact address supported, not the whole block 127.0.0.0/8 as recognized in RFC 3330). */
    public static final byte[] localhostIPv4 = IPAddressComparator.parseIP("127.0.0.1");

    /** The localhost IPv6 address */
    public static final byte[] localhostIPv6 = IPAddressComparator.parseIP("::1");

    /**
     * Given a hostname and an open socket checks if the host presented a certificate that allows it to act as the host.
     * Notice that this routine does not do certificate path checking.
     * 
     * @param hostname The name (or in rare cases an IP address) the connection was opened to.
     * @param socket The socket where to get the host certificate.
     * @throws IOException Thrown if the socket is not open, if the certificate was not understood or if the certificate
     *             vs hostname check failed.
     */
    public static void checkHostname(String hostname, SSLSocket socket) throws IOException {
        if (!socket.isConnected()) {
            throw new IOException("Socket is not open, can't check the host certificate!");
        }

        Certificate[] certs = socket.getSession().getPeerCertificates();

        if (!(certs[0] instanceof X509Certificate)) {
            socket.close();
            throw new IOException(
                    "Non X509 certificate given during SSL/TLS handshake, couldn't handle it. Class was: "
                            + certs[0].getClass().getName());
        }

        // find the end entity cert, the real host certificate.
        X509Certificate[] hostCerts = (X509Certificate[]) certs;
        int hostCertIndex = CertUtil.findClientCert(hostCerts);
        X509Certificate hostCert = (X509Certificate) certs[hostCertIndex];

        try {
            if (!HostNameChecker.checkHostName(hostname, hostCert)) {
                socket.close();
                throw new IOException("Hostname " + hostname + " not allowed with certificate for DN: "
                        + DNHandler.getSubject(hostCert).getRFCDN());
            }
        } catch (CertificateParsingException e) {
            socket.close();
            throw new IOException("Invalid certificate received, error was: " + e.getMessage());
        }

    }

    /**
     * Checks whether the hostname is allowed by the certificate. Checks the certificate altnames and subject DN
     * according to the RFC 2818. Wildcard '*' is supported both in dnsName altName and in the DN. Service prefix in DN
     * CN format "[service name]/[hostname]" is recognized, but ignored. Localhost defined as "localhost", "127.0.0.1"
     * or "::1" bypasses the check.
     * 
     * @param inHostname
     *            The hostname to check against the certificate. Can be a DNS name, IP address or an URL.
     * @param cert
     *            The certificate the hostname is checked against.
     * @return True in case the hostname is allowed by the certificate.
     * @throws CertificateParsingException
     *             Thrown in case the certificate parsing fails.
     */
    public static boolean checkHostName(String inHostname, X509Certificate cert)
            throws CertificateParsingException {
        // Dig the hostname if the given string is an URL.
        String hostname = null;
        // check whether an URL is given (contains a slash).
        if (inHostname.indexOf('/') < 0) {
            // Not an URL, assume it's a hostname
            hostname = inHostname.trim().toLowerCase();
        } else {
            // if not, assume an URL
            try {
                URL url = new URL(inHostname.trim());
                hostname = url.getHost().toLowerCase();
            } catch (MalformedURLException e) {
                throw new IllegalArgumentException(
                        "Illegal URL given for the certificate host check: " + inHostname);
            }

        }

        // check if the input is ip address.
        boolean ipAsHostname = false;
        if (ipPattern.matcher(hostname).matches()) {
            ipAsHostname = true;
        }

        // Check if localhost. If yes, accept automatically.
        if (ipAsHostname) {
            byte[] hostnameIPBytes = IPAddressComparator.parseIP(hostname);
            if (hostnameIPBytes.length < 6) {
                if (IPAddressComparator.compare(hostnameIPBytes, localhostIPv4)) {
                    LOGGER.debug("Localhost IPv4 address given, bypassing hostname - certificate matching.");
                    return true;
                }
            } else {
                if (IPAddressComparator.compare(hostnameIPBytes, localhostIPv6)) {
                    LOGGER.debug("Localhost IPv6 address given, bypassing hostname - certificate matching.");
                    return true;
                }
            }
        } else {
            if (hostname.equals("localhost")) {
                LOGGER.debug("Localhost address given, bypassing hostname - certificate matching.");
                return true;
            }
        }

        // If there are subject alternative names, check the hostname against
        // them first.
        Collection<List<?>> collection = cert.getSubjectAlternativeNames();
        if (collection != null) {

            // If there are, go through them and check for matches.
            Iterator<List<?>> collIter = collection.iterator();
            while (collIter.hasNext()) {
                List<?> item = collIter.next();
                int type = ((Integer) item.get(0)).intValue();

                if (type == GeneralName.dNSName) { // check against DNS name
                    if (!ipAsHostname) { // only if the hostname was not given
                                         // as IP address
                        String dnsName = (String) item.get(1);
                        if (checkDNS(hostname, dnsName)) {
                            return true;
                        } else {
                            LOGGER.debug("Hostname \"" + hostname + "\" does not match \"" + dnsName + "\".");
                        }
                    }
                } else {
                    if (type == GeneralName.iPAddress) { // Check against IP
                        // address
                        if (ipAsHostname) { // only if hostname was given as IP
                            // address
                            String ipString = (String) item.get(1);
                            if (checkIP(hostname, ipString)) {
                                return true;
                            } else {
                                LOGGER.debug("Hostname \"" + hostname + "\" does not match \"" + ipString + "\".");
                            }
                        }
                    }
                }
            }
        }

        // If no match was found in subjectAltName, or they were not present,
        // check against the DN.
        if (checkBasedOnDN(hostname, cert)) {
            return true;
        } else {
            LOGGER.debug("Hostname \"" + hostname + "\" does not match DN \""
                    + DNHandler.getSubject(cert).getRFCDN() + "\".");
        }

        return false;
    }

    /**
     * Checks whether the hostname matches the most specific CN in the certificate subject DN. Wildcard '*' is supported
     * and service name prefix is ignored.
     * 
     * @param hostname The hostname to check.
     * @param cert The certificate to get the subject DN from.
     * @return True in case the hostname matches the most specific CN in the certificate subject DN.
     */
    private static boolean checkBasedOnDN(String hostname, X509Certificate cert) {
        // First check whether the DN contains a match for the hostname
        X500Principal principal = cert.getSubjectX500Principal();
        if (principal != null && !"".equals(principal.getName())) {
            // separated the DN checking to make it easier later to add functionality for DN altName, if necessary.
            if (checkDN(hostname, DNHandler.getDN(principal))) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks the hostname given in four dot separated decimal format (IPv4) or in number-colon format (IPv6) against
     * the given IP address.
     * 
     * @param hostname The hostname in IP format.
     * @param ip The IP address to match against.
     * @return True if the IP addresses match.
     */
    private static boolean checkIP(String hostname, String ip) {
        byte[] ipAltName = IPAddressComparator.parseIP(ip);
        byte[] ipHostname = IPAddressComparator.parseIP(hostname);
        if (ipAltName.length == ipHostname.length) {
            return IPAddressComparator.compare(ipAltName, ipHostname);
        }
        return false;
    }

    /**
     * Checks the hostname against the given dnsName. Wildcard '*' is supported, but can only match one part of the
     * hostname. E.g. dnsName "*.foobar.org" matches "aaa.foobar.org" but not "aaa.bbb.foobar.org".
     * 
     * @param hostname The hostname to match against the given dnsName.
     * @param dnsName The dnsName to match against.
     * @return True in case the hostname matches the dnsName.
     */
    private static boolean checkDNS(String hostname, String dnsName) {
        // check if the dnsName doesn't have wildcards
        if (dnsName.indexOf('*') < 0) {
            if (hostname.trim().equalsIgnoreCase(dnsName)) {
                return true;
            }

        } else { // there is a wildcard
            // exclude dots as the wildcard can only be one dns part as said in RFC 2818.
            String regexp = dnsName.replaceAll("\\*", "[^\\.]*");
            if (hostname.toLowerCase().matches(regexp.toLowerCase())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks the hostname against the DN. Uses only the most specific CN as described in RFC 2818.
     * "[service name]/[hostname]" format is supported, and also wildcards.
     * 
     * @param hostname The hostname from e.g. URL.
     * @param dn The DN to search for a match for the hostname.
     * @return True in case the hostname matches with the hostname in the DN.
     */
    private static boolean checkDN(String hostname, DN dn) {
        String cnValue = dn.getLastCNValue();
        if (cnValue == null) {// no CN found, no match can be valid
            return false;
        }
        // check whether the name is prepended by service type, if yes, remove it.
        int index = cnValue.indexOf('/');
        if (index >= 0) {
            cnValue = cnValue.substring(index + 1, cnValue.length());
        }

        return checkDNS(hostname, cnValue);
    }
}