001 /*
002 * Copyright 2014-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2014-2016 UnboundID Corp.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021 package com.unboundid.util.ssl;
022
023
024
025 import java.net.InetAddress;
026 import java.net.URI;
027 import java.util.Collection;
028 import java.util.List;
029 import java.security.cert.Certificate;
030 import java.security.cert.X509Certificate;
031 import javax.net.ssl.SSLSession;
032 import javax.net.ssl.SSLSocket;
033 import javax.security.auth.x500.X500Principal;
034
035 import com.unboundid.ldap.sdk.DN;
036 import com.unboundid.ldap.sdk.LDAPException;
037 import com.unboundid.ldap.sdk.RDN;
038 import com.unboundid.ldap.sdk.ResultCode;
039 import com.unboundid.util.Debug;
040 import com.unboundid.util.NotMutable;
041 import com.unboundid.util.StaticUtils;
042 import com.unboundid.util.ThreadSafety;
043 import com.unboundid.util.ThreadSafetyLevel;
044
045 import static com.unboundid.util.ssl.SSLMessages.*;
046
047
048
049 /**
050 * This class provides an implementation of an {@code SSLSocket} verifier that
051 * will verify that the presented server certificate includes the address to
052 * which the client intended to establish a connection. It will check the CN
053 * attribute of the certificate subject, as well as certain subjectAltName
054 * extensions, including dNSName, uniformResourceIdentifier, and iPAddress.
055 */
056 @NotMutable()
057 @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
058 public final class HostNameSSLSocketVerifier
059 extends SSLSocketVerifier
060 {
061 // Indicates whether to allow wildcard certificates which contain an asterisk
062 // as the first component of a CN subject attribute or dNSName subjectAltName
063 // extension.
064 private final boolean allowWildcards;
065
066
067
068 /**
069 * Creates a new instance of this {@code SSLSocket} verifier.
070 *
071 * @param allowWildcards Indicates whether to allow wildcard certificates
072 * which contain an asterisk as the first component of
073 * a CN subject attribute or dNSName subjectAltName
074 * extension.
075 */
076 public HostNameSSLSocketVerifier(final boolean allowWildcards)
077 {
078 this.allowWildcards = allowWildcards;
079 }
080
081
082
083 /**
084 * Verifies that the provided {@code SSLSocket} is acceptable and the
085 * connection should be allowed to remain established.
086 *
087 * @param host The address to which the client intended the connection
088 * to be established.
089 * @param port The port to which the client intended the connection to
090 * be established.
091 * @param sslSocket The {@code SSLSocket} that should be verified.
092 *
093 * @throws LDAPException If a problem is identified that should prevent the
094 * provided {@code SSLSocket} from remaining
095 * established.
096 */
097 @Override()
098 public void verifySSLSocket(final String host, final int port,
099 final SSLSocket sslSocket)
100 throws LDAPException
101 {
102 try
103 {
104 // Get the certificates presented during negotiation. The certificates
105 // will be ordered so that the server certificate comes first.
106 final SSLSession sslSession = sslSocket.getSession();
107 if (sslSession == null)
108 {
109 throw new LDAPException(ResultCode.CONNECT_ERROR,
110 ERR_HOST_NAME_SSL_SOCKET_VERIFIER_NO_SESSION.get(host, port));
111 }
112
113 final Certificate[] peerCertificates = sslSession.getPeerCertificates();
114 if ((peerCertificates == null) || (peerCertificates.length == 0))
115 {
116 throw new LDAPException(ResultCode.CONNECT_ERROR,
117 ERR_HOST_NAME_SSL_SOCKET_VERIFIER_NO_PEER_CERTS.get(host, port));
118 }
119
120 if (peerCertificates[0] instanceof X509Certificate)
121 {
122 final StringBuilder certInfo = new StringBuilder();
123 if (! certificateIncludesHostname(host,
124 (X509Certificate) peerCertificates[0], allowWildcards, certInfo))
125 {
126 throw new LDAPException(ResultCode.CONNECT_ERROR,
127 ERR_HOST_NAME_SSL_SOCKET_VERIFIER_HOSTNAME_NOT_FOUND.get(host,
128 certInfo.toString()));
129 }
130 }
131 else
132 {
133 throw new LDAPException(ResultCode.CONNECT_ERROR,
134 ERR_HOST_NAME_SSL_SOCKET_VERIFIER_PEER_NOT_X509.get(host, port,
135 peerCertificates[0].getType()));
136 }
137 }
138 catch (final LDAPException le)
139 {
140 Debug.debugException(le);
141 throw le;
142 }
143 catch (final Exception e)
144 {
145 Debug.debugException(e);
146 throw new LDAPException(ResultCode.CONNECT_ERROR,
147 ERR_HOST_NAME_SSL_SOCKET_VERIFIER_EXCEPTION.get(host, port,
148 StaticUtils.getExceptionMessage(e)),
149 e);
150 }
151 }
152
153
154
155 /**
156 * Determines whether the provided certificate contains the specified
157 * hostname.
158 *
159 * @param host The address expected to be found in the provided
160 * certificate.
161 * @param certificate The peer certificate to be validated.
162 * @param allowWildcards Indicates whether to allow wildcard certificates
163 * which contain an asterisk as the first component of
164 * a CN subject attribute or dNSName subjectAltName
165 * extension.
166 * @param certInfo A buffer into which information will be provided
167 * about the provided certificate.
168 *
169 * @return {@code true} if the expected hostname was found in the
170 * certificate, or {@code false} if not.
171 */
172 static boolean certificateIncludesHostname(final String host,
173 final X509Certificate certificate,
174 final boolean allowWildcards,
175 final StringBuilder certInfo)
176 {
177 final String lowerHost = StaticUtils.toLowerCase(host);
178
179 // First, check the CN from the certificate subject.
180 final String subjectDN =
181 certificate.getSubjectX500Principal().getName(X500Principal.RFC2253);
182 certInfo.append("subject='");
183 certInfo.append(subjectDN);
184 certInfo.append('\'');
185
186 try
187 {
188 final DN dn = new DN(subjectDN);
189 for (final RDN rdn : dn.getRDNs())
190 {
191 final String[] names = rdn.getAttributeNames();
192 final String[] values = rdn.getAttributeValues();
193 for (int i=0; i < names.length; i++)
194 {
195 final String lowerName = StaticUtils.toLowerCase(names[i]);
196 if (lowerName.equals("cn") || lowerName.equals("commonname") ||
197 lowerName.equals("2.5.4.3"))
198 {
199 final String lowerValue = StaticUtils.toLowerCase(values[i]);
200 if (lowerHost.equals(lowerValue))
201 {
202 return true;
203 }
204
205 if (allowWildcards && lowerValue.startsWith("*."))
206 {
207 final String withoutWildcard = lowerValue.substring(1);
208 if (lowerHost.endsWith(withoutWildcard))
209 {
210 return true;
211 }
212 }
213 }
214 }
215 }
216 }
217 catch (final Exception e)
218 {
219 // This shouldn't happen for a well-formed certificate subject, but we
220 // have to handle it anyway.
221 Debug.debugException(e);
222 }
223
224
225 // Next, check any supported subjectAltName extension values.
226 final Collection<List<?>> subjectAltNames;
227 try
228 {
229 subjectAltNames = certificate.getSubjectAlternativeNames();
230 }
231 catch (final Exception e)
232 {
233 Debug.debugException(e);
234 return false;
235 }
236
237 if (subjectAltNames != null)
238 {
239 for (final List<?> l : subjectAltNames)
240 {
241 try
242 {
243 final Integer type = (Integer) l.get(0);
244 switch (type)
245 {
246 case 2: // dNSName
247 final String dnsName = (String) l.get(1);
248 certInfo.append(" dNSName='");
249 certInfo.append(dnsName);
250 certInfo.append('\'');
251
252 final String lowerDNSName = StaticUtils.toLowerCase(dnsName);
253 if (lowerHost.equals(lowerDNSName))
254 {
255 return true;
256 }
257
258 // If the given DNS name starts with a "*.", then it's a wildcard
259 // certificate. See if that's allowed, and if so whether it
260 // matches any acceptable name.
261 if (allowWildcards && lowerDNSName.startsWith("*."))
262 {
263 final String withoutWildcard = lowerDNSName.substring(1);
264 if (lowerHost.endsWith(withoutWildcard))
265 {
266 return true;
267 }
268 }
269 break;
270
271 case 6: // uniformResourceIdentifier
272 final String uriString = (String) l.get(1);
273 certInfo.append(" uniformResourceIdentifier='");
274 certInfo.append(uriString);
275 certInfo.append('\'');
276
277 final URI uri = new URI(uriString);
278 if (lowerHost.equals(StaticUtils.toLowerCase(uri.getHost())))
279 {
280 return true;
281 }
282 break;
283
284 case 7: // iPAddress
285 final String ipAddressString = (String) l.get(1);
286 certInfo.append(" iPAddress='");
287 certInfo.append(ipAddressString);
288 certInfo.append('\'');
289
290 final InetAddress inetAddress =
291 InetAddress.getByName(ipAddressString);
292 if (Character.isDigit(host.charAt(0)) || (host.indexOf(':') >= 0))
293 {
294 final InetAddress a = InetAddress.getByName(host);
295 if (inetAddress.equals(a))
296 {
297 return true;
298 }
299 }
300 break;
301
302 case 0: // otherName
303 case 1: // rfc822Name
304 case 3: // x400Address
305 case 4: // directoryName
306 case 5: // ediPartyName
307 case 8: // registeredID
308 default:
309 // We won't do any checking for any of these formats.
310 break;
311 }
312 }
313 catch (final Exception e)
314 {
315 Debug.debugException(e);
316 }
317 }
318 }
319
320 return false;
321 }
322 }