001 /*
002 * Copyright 2012-2013 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2012-2013 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.security.cert.CertificateException;
028 import java.security.cert.X509Certificate;
029 import java.util.Collection;
030 import java.util.Collections;
031 import java.util.LinkedHashSet;
032 import java.util.List;
033 import java.util.Set;
034 import javax.net.ssl.X509TrustManager;
035 import javax.security.auth.x500.X500Principal;
036
037 import com.unboundid.ldap.sdk.DN;
038 import com.unboundid.ldap.sdk.RDN;
039 import com.unboundid.util.NotMutable;
040 import com.unboundid.util.StaticUtils;
041 import com.unboundid.util.ThreadSafety;
042 import com.unboundid.util.ThreadSafetyLevel;
043 import com.unboundid.util.Validator;
044
045 import static com.unboundid.util.Debug.*;
046 import static com.unboundid.util.ssl.SSLMessages.*;
047
048
049
050 /**
051 * This class provides an SSL trust manager that will only accept certificates
052 * whose hostname (as contained in the CN subject attribute or a subjectAltName
053 * extension) matches an expected value. Only the dNSName, iPAddress, and
054 * uniformResourceIdentifier subjectAltName formats are supported.
055 * <BR><BR>
056 * This implementation optionally supports wildcard certificates, which have a
057 * hostname that starts with an asterisk followed by a period and domain or
058 * subdomain. For example, "*.example.com" could be considered a match for
059 * anything in the "example.com" domain. If wildcards are allowed, then only
060 * the CN subject attribute and dNSName subjectAltName extension will be
061 * examined, and only the leftmost element of a hostname may be a wildcard
062 * character.
063 * <BR><BR>
064 * Note that no other elements of the certificate are examined, so it is
065 * strongly recommended that this trust manager be used in an
066 * {@link AggregateTrustManager} in conjunction with other trust managers that
067 * perform other forms of validation.
068 */
069 @NotMutable()
070 @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
071 public final class HostNameTrustManager
072 implements X509TrustManager
073 {
074 /**
075 * A pre-allocated empty certificate array.
076 */
077 private static final X509Certificate[] NO_CERTIFICATES =
078 new X509Certificate[0];
079
080
081
082 // Indicates whether to allow wildcard certificates (which
083 private final boolean allowWildcards;
084
085 // The set of hostname values that will be considered acceptable.
086 private final Set<String> acceptableHostNames;
087
088
089
090 /**
091 * Creates a new hostname trust manager with the provided information.
092 *
093 * @param allowWildcards Indicates whether to allow wildcard
094 * certificates which contain an asterisk as the
095 * first component of a CN subject attribute or
096 * dNSName subjectAltName extension.
097 * @param acceptableHostNames The set of hostnames and/or IP addresses that
098 * will be considered acceptable. Only
099 * certificates with a CN or subjectAltName value
100 * that exactly matches one of these names
101 * (ignoring differences in capitalization) will
102 * be considered acceptable. It must not be
103 * {@code null} or empty.
104 */
105 public HostNameTrustManager(final boolean allowWildcards,
106 final String... acceptableHostNames)
107 {
108 this(allowWildcards, StaticUtils.toList(acceptableHostNames));
109 }
110
111
112
113 /**
114 * Creates a new hostname trust manager with the provided information.
115 *
116 * @param allowWildcards Indicates whether to allow wildcard
117 * certificates which contain an asterisk as the
118 * first component of a CN subject attribute or
119 * dNSName subjectAltName extension.
120 * @param acceptableHostNames The set of hostnames and/or IP addresses that
121 * will be considered acceptable. Only
122 * certificates with a CN or subjectAltName value
123 * that exactly matches one of these names
124 * (ignoring differences in capitalization) will
125 * be considered acceptable. It must not be
126 * {@code null} or empty.
127 */
128 public HostNameTrustManager(final boolean allowWildcards,
129 final Collection<String> acceptableHostNames)
130 {
131 Validator.ensureNotNull(acceptableHostNames);
132 Validator.ensureFalse(acceptableHostNames.isEmpty(),
133 "The set of acceptable host names must not be empty.");
134
135 this.allowWildcards = allowWildcards;
136
137 final LinkedHashSet<String> nameSet =
138 new LinkedHashSet<String>(acceptableHostNames.size());
139 for (final String s : acceptableHostNames)
140 {
141 nameSet.add(StaticUtils.toLowerCase(s));
142 }
143
144 this.acceptableHostNames = Collections.unmodifiableSet(nameSet);
145 }
146
147
148
149 /**
150 * Indicates whether wildcard certificates should be allowed, which may
151 * match multiple hosts in a given domain or subdomain.
152 *
153 * @return {@code true} if wildcard certificates should be allowed, or
154 * {@code false} if not.
155 */
156 public boolean allowWildcards()
157 {
158 return allowWildcards;
159 }
160
161
162
163 /**
164 * Retrieves the set of hostnames that will be considered acceptable.
165 *
166 * @return The set of hostnames that will be considered acceptable.
167 */
168 public Set<String> getAcceptableHostNames()
169 {
170 return acceptableHostNames;
171 }
172
173
174
175 /**
176 * Checks to determine whether the provided client certificate chain should be
177 * trusted.
178 *
179 * @param chain The client certificate chain for which to make the
180 * determination.
181 * @param authType The authentication type based on the client certificate.
182 *
183 * @throws CertificateException If the provided client certificate chain
184 * should not be trusted.
185 */
186 public void checkClientTrusted(final X509Certificate[] chain,
187 final String authType)
188 throws CertificateException
189 {
190 checkCertificate(chain[0]);
191 }
192
193
194
195 /**
196 * Checks to determine whether the provided server certificate chain should be
197 * trusted.
198 *
199 * @param chain The server certificate chain for which to make the
200 * determination.
201 * @param authType The key exchange algorithm used.
202 *
203 * @throws CertificateException If the provided server certificate chain
204 * should not be trusted.
205 */
206 public void checkServerTrusted(final X509Certificate[] chain,
207 final String authType)
208 throws CertificateException
209 {
210 checkCertificate(chain[0]);
211 }
212
213
214
215 /**
216 * Performs the appropriate validation for the given certificate.
217 *
218 * @param c The certificate to be validated.
219 *
220 * @throws CertificateException If the provided certificate does not have a
221 * CN or subjectAltName value that matches one
222 * of the acceptable hostnames.
223 */
224 private void checkCertificate(final X509Certificate c)
225 throws CertificateException
226 {
227 // First, check the CN from the certificate subject.
228 final String subjectDN =
229 c.getSubjectX500Principal().getName(X500Principal.RFC2253);
230 try
231 {
232 final DN dn = new DN(subjectDN);
233 for (final RDN rdn : dn.getRDNs())
234 {
235 final String[] names = rdn.getAttributeNames();
236 final String[] values = rdn.getAttributeValues();
237 for (int i=0; i < names.length; i++)
238 {
239 final String lowerName = StaticUtils.toLowerCase(names[i]);
240 if (lowerName.equals("cn") || lowerName.equals("commonname") ||
241 lowerName.equals("2.5.4.3"))
242 {
243 final String lowerValue = StaticUtils.toLowerCase(values[i]);
244 if (acceptableHostNames.contains(lowerValue))
245 {
246 return;
247 }
248
249 if (allowWildcards && lowerValue.startsWith("*."))
250 {
251 final String withoutWildcard = lowerValue.substring(1);
252 for (final String s : acceptableHostNames)
253 {
254 if (s.endsWith(withoutWildcard))
255 {
256 return;
257 }
258 }
259 }
260 }
261 }
262 }
263 }
264 catch (final Exception e)
265 {
266 // This shouldn't happen for a well-formed certificate subject, but we
267 // have to handle it anyway.
268 debugException(e);
269 }
270
271
272 // Next, check any subjectAltName extension values.
273 final Collection<List<?>> subjectAltNames = c.getSubjectAlternativeNames();
274 if (subjectAltNames != null)
275 {
276 for (final List<?> l : subjectAltNames)
277 {
278 try
279 {
280 final Integer type = (Integer) l.get(0);
281 switch (type)
282 {
283 case 2: // dNSName
284 final String dnsName = StaticUtils.toLowerCase((String) l.get(1));
285 if (acceptableHostNames.contains(dnsName))
286 {
287 // We found a matching DNS host name.
288 return;
289 }
290
291 // If the given DNS name starts with a "*.", then it's a wildcard
292 // certificate. See if that's allowed, and if so whether it
293 // matches any acceptable name.
294 if (allowWildcards && dnsName.startsWith("*."))
295 {
296 final String withoutWildcard = dnsName.substring(1);
297 for (final String s : acceptableHostNames)
298 {
299 if (s.endsWith(withoutWildcard))
300 {
301 return;
302 }
303 }
304 }
305 break;
306
307 case 6: // uniformResourceIdentifier
308 final URI uri = new URI((String) l.get(1));
309 if (acceptableHostNames.contains(
310 StaticUtils.toLowerCase(uri.getHost())))
311 {
312 // The URI had a matching address.
313 return;
314 }
315 break;
316
317 case 7: // iPAddress
318 final InetAddress inetAddress =
319 InetAddress.getByName((String) l.get(1));
320 for (final String s : acceptableHostNames)
321 {
322 if (Character.isDigit(s.charAt(0)) || (s.indexOf(':') >= 0))
323 {
324 final InetAddress a = InetAddress.getByName(s);
325 if (inetAddress.equals(a))
326 {
327 return;
328 }
329 }
330 }
331 break;
332
333 case 0: // otherName
334 case 1: // rfc822Name
335 case 3: // x400Address
336 case 4: // directoryName
337 case 5: // ediPartyName
338 case 8: // registeredID
339 default:
340 // We won't do any checking for any of these formats.
341 break;
342 }
343 }
344 catch (final Exception e)
345 {
346 debugException(e);
347 }
348 }
349 }
350
351
352 // If we've gotten here, then we didn't find a match.
353 throw new CertificateException(ERR_HOSTNAME_NOT_FOUND.get(subjectDN));
354 }
355
356
357
358 /**
359 * Retrieves the accepted issuer certificates for this trust manager. This
360 * will always return an empty array.
361 *
362 * @return The accepted issuer certificates for this trust manager.
363 */
364 public X509Certificate[] getAcceptedIssuers()
365 {
366 return NO_CERTIFICATES;
367 }
368 }