Java tutorial
/* $Id$ Copyright (C) 2003-2012 Virginia Tech. All rights reserved. SEE LICENSE FOR MORE INFORMATION Author: Middleware Services Email: middleware@vt.edu Version: $Revision$ Updated: $Date$ */ package edu.vt.middleware.ldap.ssl; import java.security.GeneralSecurityException; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.ldap.LdapName; import javax.naming.ldap.Rdn; import javax.net.SocketFactory; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import edu.vt.middleware.ldap.LdapUtil; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Hostname verifier that provides an implementation similar to what occurs with * JNDI startTLS. Verification occurs in the following order: * <ul> * <li>if hostname is IP, then cert must have exact match IP subjAltName</li> * <li>hostname must match any DNS subjAltName if any exist</li> * <li>hostname must match the first CN</li> * <li>if cert begins with a wildcard, domains are used for matching</li> * </ul> * * @author Middleware Services * @version $Revision$ $Date$ */ public class DefaultHostnameVerifier implements HostnameVerifier, CertificateHostnameVerifier { /** Log for this class. */ protected final Log logger = LogFactory.getLog(this.getClass()); /** Enum for subject alt name types. */ private enum SubjectAltNameType { /** other name (0). */ OTHER_NAME, /** ref822 name (1). */ RFC822_NAME, /** dns name (2). */ DNS_NAME, /** x400 address (3). */ X400_ADDRESS, /** directory name (4). */ DIRECTORY_NAME, /** edi party name (5). */ EDI_PARTY_NAME, /** uniform resource identifier (6). */ UNIFORM_RESOURCE_IDENTIFIER, /** ip address (7). */ IP_ADDRESS, /** registered id (8). */ REGISTERED_ID; } /** {@inheritDoc} */ public boolean verify(final String hostname, final SSLSession session) { boolean b = false; try { String name = null; if (hostname != null) { // if IPv6 strip off the "[]" if (hostname.startsWith("[") && hostname.endsWith("]")) { name = hostname.substring(1, hostname.length() - 1).trim(); } else { name = hostname.trim(); } } b = verify(name, (X509Certificate) session.getPeerCertificates()[0]); } catch (SSLPeerUnverifiedException e) { if (this.logger.isWarnEnabled()) { this.logger.warn("Could not get certificate from the SSL session", e); } } return b; } /** * Verify if the hostname is an IP address using * {@link LdapUtil#isIPAddress(String)}. Delegates to * {@link #verifyIP(String, X509Certificate)} and * {@link #verifyDNS(String, X509Certificate)} accordingly. * * @param hostname to verify * @param cert to verify hostname against * * @return whether hostname is valid for the supplied certificate */ public boolean verify(final String hostname, final X509Certificate cert) { if (this.logger.isDebugEnabled()) { this.logger.debug("Verify with the following parameters:"); this.logger.debug(" hostname = " + hostname); this.logger.debug(" cert = " + cert.getSubjectX500Principal().toString()); } boolean b = false; if (LdapUtil.isIPAddress(hostname)) { b = verifyIP(hostname, cert); } else { b = verifyDNS(hostname, cert); } return b; } /** * Verify the certificate allows use of the supplied IP address. * * From RFC2818: * In some cases, the URI is specified as an IP address rather than a * hostname. In this case, the iPAddress subjectAltName must be present * in the certificate and must exactly match the IP in the URI. * * @param ip address to match in the certificate * @param cert to inspect for the IP address * * @return whether the ip matched a subject alt name */ protected boolean verifyIP(final String ip, final X509Certificate cert) { final String[] subjAltNames = getSubjectAltNames(cert, SubjectAltNameType.IP_ADDRESS); if (this.logger.isDebugEnabled()) { this.logger.debug("verifyIP using subjectAltNames = " + Arrays.toString(subjAltNames)); } for (String name : subjAltNames) { if (ip.equalsIgnoreCase(name)) { if (this.logger.isDebugEnabled()) { this.logger.debug("verifyIP found hostname match: " + name); } return true; } } return false; } /** * Verify the certificate allows use of the supplied DNS name. Note that only * the first CN is used. * * From RFC2818: * If a subjectAltName extension of type dNSName is present, that MUST * be used as the identity. Otherwise, the (most specific) Common Name * field in the Subject field of the certificate MUST be used. Although * the use of the Common Name is existing practice, it is deprecated and * Certification Authorities are encouraged to use the dNSName instead. * * Matching is performed using the matching rules specified by * [RFC2459]. If more than one identity of a given type is present in * the certificate (e.g., more than one dNSName name, a match in any one * of the set is considered acceptable.) * * @param hostname to match in the certificate * @param cert to inspect for the hostname * * @return whether the hostname matched a subject alt name or CN */ protected boolean verifyDNS(final String hostname, final X509Certificate cert) { boolean verified = false; final String[] subjAltNames = getSubjectAltNames(cert, SubjectAltNameType.DNS_NAME); if (this.logger.isDebugEnabled()) { this.logger.debug("verifyDNS using subjectAltNames = " + Arrays.toString(subjAltNames)); } if (subjAltNames.length > 0) { // if subject alt names exist, one must match for (String name : subjAltNames) { if (isMatch(hostname, name)) { if (this.logger.isDebugEnabled()) { this.logger.debug("verifyDNS found hostname match: " + name); } verified = true; break; } } } else { final String[] cns = getCNs(cert); if (this.logger.isDebugEnabled()) { this.logger.debug("verifyDNS using CN = " + Arrays.toString(cns)); } if (cns.length > 0) { // the most specific CN refers to the last CN if (isMatch(hostname, cns[cns.length - 1])) { if (this.logger.isDebugEnabled()) { this.logger.debug("verifyDNS found hostname match: " + cns[cns.length - 1]); } verified = true; } } } return verified; } /** * Returns the subject alternative names matching the supplied name type from * the supplied certificate. * * @param cert to get subject alt names from * @param type subject alt name type * * @return subject alt names */ private String[] getSubjectAltNames(final X509Certificate cert, final SubjectAltNameType type) { final List<String> names = new ArrayList<String>(); try { final Collection<List<?>> subjAltNames = cert.getSubjectAlternativeNames(); if (subjAltNames != null) { for (List<?> generalName : subjAltNames) { final Integer nameType = (Integer) generalName.get(0); if (nameType.intValue() == type.ordinal()) { names.add((String) generalName.get(1)); } } } } catch (CertificateParsingException e) { if (this.logger.isWarnEnabled()) { this.logger.warn("Error reading subject alt names from certificate", e); } } return names.toArray(new String[names.size()]); } /** * Returns the CNs from the supplied certificate. * * @param cert to get CNs from * * @return CNs */ private String[] getCNs(final X509Certificate cert) { final List<String> names = new ArrayList<String>(); final String subjectPrincipal = cert.getSubjectX500Principal().toString(); if (subjectPrincipal != null) { try { final LdapName subjectDn = new LdapName(subjectPrincipal); for (Rdn rdn : subjectDn.getRdns()) { final Attributes attrs = rdn.toAttributes(); final NamingEnumeration<String> ids = attrs.getIDs(); while (ids.hasMore()) { final String id = ids.next(); if (id.toLowerCase().equals("cn") || id.toLowerCase().equals("commonname") || id.toLowerCase().equals("2.5.4.3")) { final Object value = attrs.get(id).get(); if (value != null) { if (value instanceof String) { names.add((String) value); } else if (value instanceof Attribute) { // for multi value RDNs the first value is used final Object multiValue = ((Attribute) value).get(); if (multiValue != null && multiValue instanceof String) { names.add((String) multiValue); } } } } } } } catch (NamingException e) { if (this.logger.isWarnEnabled()) { this.logger.warn("Could not get distinguished name from subject " + subjectPrincipal, e); } } } return names.toArray(new String[names.size()]); } /** * Determines if the supplied hostname matches a name derived from the * certificate. If the certificate name starts with '*', the domain components * after the first '.' in each name are compared. * * @param hostname to match * @param certName to match * * @return whether the hostname matched the cert name */ private boolean isMatch(final String hostname, final String certName) { // must start with '*' and contain two domain components final boolean isWildcard = certName.startsWith("*.") && certName.indexOf('.') < certName.lastIndexOf('.'); boolean match = false; if (isWildcard) { final String certNameDomain = certName.substring(certName.indexOf(".")); final int hostnameIdx = hostname.indexOf(".") != -1 ? hostname.indexOf(".") : hostname.length(); final String hostnameDomain = hostname.substring(hostnameIdx); match = certNameDomain.equalsIgnoreCase(hostnameDomain); } else { match = certName.equalsIgnoreCase(hostname); } return match; } /** * Socket factory that uses {@link DefaultHostnameVerifier}. */ public static class SSLSocketFactory extends TLSSocketFactory { /** * Creates a new socket factory that uses this hostname verifier. */ public SSLSocketFactory() { setHostnameVerifier(new DefaultHostnameVerifier()); } /** * Returns the default SSL socket factory. * * @return socket factory */ public static SocketFactory getDefault() { final SSLSocketFactory sf = new SSLSocketFactory(); try { sf.initialize(); } catch (GeneralSecurityException e) { final Log logger = LogFactory.getLog(TLSSocketFactory.class); if (logger.isErrorEnabled()) { logger.error("Error initializing socket factory", e); } } return sf; } } }