1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 package org.apache.hc.client5.http.ssl;
29
30 import java.net.InetAddress;
31 import java.net.UnknownHostException;
32 import java.security.cert.Certificate;
33 import java.security.cert.CertificateParsingException;
34 import java.security.cert.X509Certificate;
35 import java.util.ArrayList;
36 import java.util.Collection;
37 import java.util.Collections;
38 import java.util.LinkedList;
39 import java.util.List;
40
41 import javax.net.ssl.SSLException;
42 import javax.net.ssl.SSLPeerUnverifiedException;
43 import javax.net.ssl.SSLSession;
44 import javax.security.auth.x500.X500Principal;
45
46 import org.apache.hc.client5.http.psl.PublicSuffixMatcher;
47 import org.apache.hc.client5.http.utils.DnsUtils;
48 import org.apache.hc.core5.annotation.Contract;
49 import org.apache.hc.core5.annotation.ThreadingBehavior;
50 import org.apache.hc.core5.http.NameValuePair;
51 import org.apache.hc.core5.net.InetAddressUtils;
52 import org.apache.hc.core5.util.TextUtils;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56
57
58
59
60
61 @Contract(threading = ThreadingBehavior.STATELESS)
62 public final class DefaultHostnameVerifier implements HttpClientHostnameVerifier {
63
64 enum HostNameType {
65
66 IPv4(7), IPv6(7), DNS(2);
67
68 final int subjectType;
69
70 HostNameType(final int subjectType) {
71 this.subjectType = subjectType;
72 }
73
74 }
75
76 private static final Logger LOG = LoggerFactory.getLogger(DefaultHostnameVerifier.class);
77
78 private final PublicSuffixMatcher publicSuffixMatcher;
79
80
81
82
83
84
85 public DefaultHostnameVerifier(final PublicSuffixMatcher publicSuffixMatcher) {
86 this.publicSuffixMatcher = publicSuffixMatcher;
87 }
88
89
90
91
92 public DefaultHostnameVerifier() {
93 this(null);
94 }
95
96 @Override
97 public boolean verify(final String host, final SSLSession session) {
98 try {
99 final Certificate[] certs = session.getPeerCertificates();
100 final X509Certificate x509 = (X509Certificate) certs[0];
101 verify(host, x509);
102 return true;
103 } catch (final SSLException ex) {
104 if (LOG.isDebugEnabled()) {
105 LOG.debug(ex.getMessage(), ex);
106 }
107 return false;
108 }
109 }
110
111 @Override
112 public void verify(final String host, final X509Certificate cert) throws SSLException {
113 final HostNameType hostType = determineHostFormat(host);
114 switch (hostType) {
115 case IPv4:
116 matchIPAddress(host, getSubjectAltNames(cert, SubjectName.IP));
117 break;
118 case IPv6:
119 matchIPv6Address(host, getSubjectAltNames(cert, SubjectName.IP));
120 break;
121 default:
122 final List<SubjectName> subjectAlts = getSubjectAltNames(cert, SubjectName.DNS);
123 if (subjectAlts.isEmpty()) {
124
125
126 matchCN(host, cert, this.publicSuffixMatcher);
127 } else {
128 matchDNSName(host, subjectAlts, this.publicSuffixMatcher);
129 }
130 }
131 }
132
133 static void matchIPAddress(final String host, final List<SubjectName> subjectAlts) throws SSLPeerUnverifiedException {
134 for (final SubjectName subjectAlt : subjectAlts) {
135 if (subjectAlt.getType() == SubjectName.IP) {
136 if (host.equals(subjectAlt.getValue())) {
137 return;
138 }
139 }
140 }
141 throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " +
142 "of the subject alternative names: " + subjectAlts);
143 }
144
145 static void matchIPv6Address(final String host, final List<SubjectName> subjectAlts) throws SSLPeerUnverifiedException {
146 final String normalisedHost = normaliseAddress(host);
147 for (final SubjectName subjectAlt : subjectAlts) {
148 if (subjectAlt.getType() == SubjectName.IP) {
149 final String normalizedSubjectAlt = normaliseAddress(subjectAlt.getValue());
150 if (normalisedHost.equals(normalizedSubjectAlt)) {
151 return;
152 }
153 }
154 }
155 throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " +
156 "of the subject alternative names: " + subjectAlts);
157 }
158
159 static void matchDNSName(final String host, final List<SubjectName> subjectAlts,
160 final PublicSuffixMatcher publicSuffixMatcher) throws SSLPeerUnverifiedException {
161 final String normalizedHost = DnsUtils.normalizeUnicode(host);
162 for (final SubjectName subjectAlt : subjectAlts) {
163 if (subjectAlt.getType() == SubjectName.DNS) {
164 final String normalizedSubjectAlt = DnsUtils.normalizeUnicode(subjectAlt.getValue());
165 if (matchIdentity(normalizedHost, normalizedSubjectAlt, publicSuffixMatcher, true)) {
166 return;
167 }
168 }
169 }
170 throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " +
171 "of the subject alternative names: " + subjectAlts);
172 }
173
174 static void matchCN(final String host, final X509Certificate cert,
175 final PublicSuffixMatcher publicSuffixMatcher) throws SSLException {
176 final X500Principal subjectPrincipal = cert.getSubjectX500Principal();
177 final String cn = extractCN(subjectPrincipal.getName(X500Principal.RFC2253));
178 if (cn == null) {
179 throw new SSLPeerUnverifiedException("Certificate subject for <" + host + "> doesn't contain " +
180 "a common name and does not have alternative names");
181 }
182 final String normalizedHost = DnsUtils.normalizeUnicode(host);
183 final String normalizedCn = DnsUtils.normalizeUnicode(cn);
184 if (!matchIdentity(normalizedHost, normalizedCn, publicSuffixMatcher, true)) {
185 throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match " +
186 "common name of the certificate subject: " + cn);
187 }
188 }
189
190 static List<CharSequence> parseFQDN(final CharSequence s) {
191 if (s == null) {
192 return null;
193 }
194 final LinkedList<CharSequence> elements = new LinkedList<>();
195 int pos = 0;
196 for (int i = 0; i < s.length(); i++) {
197 final char ch = s.charAt(i);
198 if (ch == '.') {
199 elements.addFirst(s.subSequence(pos, i));
200 pos = i + 1;
201 }
202 }
203 elements.addFirst(s.subSequence(pos, s.length()));
204 return elements;
205 }
206
207 static boolean matchDomainRoot(final String host, final String domainRoot) {
208 if (domainRoot == null) {
209 return false;
210 }
211 final List<CharSequence> hostElements = parseFQDN(host);
212 final List<CharSequence> rootElements = parseFQDN(domainRoot);
213 if (hostElements.size() >= rootElements.size()) {
214 for (int i = 0; i < rootElements.size(); i++) {
215 final CharSequence s1 = rootElements.get(i);
216 final CharSequence s2 = hostElements.get(i);
217 if (!s1.equals(s2)) {
218 return false;
219 }
220 }
221 return true;
222 }
223 return false;
224 }
225
226 static boolean matchIdentity(final String host, final String identity,
227 final PublicSuffixMatcher publicSuffixMatcher,
228 final boolean strict) {
229 if (publicSuffixMatcher != null && host.contains(".")) {
230 if (!publicSuffixMatcher.verifyInternal(identity)) {
231 if (LOG.isDebugEnabled()) {
232 LOG.debug("Public Suffix List verification failed for identity '{}'", identity);
233 }
234 return false;
235 }
236 }
237
238
239
240
241
242
243 final int asteriskIdx = identity.indexOf('*');
244 if (asteriskIdx != -1) {
245 final String prefix = identity.substring(0, asteriskIdx);
246 final String suffix = identity.substring(asteriskIdx + 1);
247
248 if (!prefix.isEmpty() && !host.startsWith(prefix)) {
249 return false;
250 }
251 if (!suffix.isEmpty() && !host.endsWith(suffix)) {
252 return false;
253 }
254
255 if (strict) {
256 final String remainder = host.substring(
257 prefix.length(),
258 host.length() - suffix.length()
259 );
260 return !remainder.contains(".");
261 }
262 return true;
263 }
264
265
266 return host.equalsIgnoreCase(identity);
267 }
268
269 static String extractCN(final String subjectPrincipal) throws SSLException {
270 if (subjectPrincipal == null) {
271 return null;
272 }
273 final List<NameValuePair> attributes = DistinguishedNameParser.INSTANCE.parse(subjectPrincipal);
274 for (final NameValuePair attribute: attributes) {
275 if (TextUtils.isBlank(attribute.getName()) || attribute.getValue() == null) {
276 throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name");
277 }
278 if (attribute.getName().equalsIgnoreCase("cn")) {
279 return attribute.getValue();
280 }
281 }
282 return null;
283 }
284
285 static HostNameType determineHostFormat(final String host) {
286 if (InetAddressUtils.isIPv4(host)) {
287 return HostNameType.IPv4;
288 }
289 String s = host;
290 if (s.startsWith("[") && s.endsWith("]")) {
291 s = host.substring(1, host.length() - 1);
292 }
293 if (InetAddressUtils.isIPv6(s)) {
294 return HostNameType.IPv6;
295 }
296 return HostNameType.DNS;
297 }
298
299 static List<SubjectName> getSubjectAltNames(final X509Certificate cert) {
300 return getSubjectAltNames(cert, -1);
301 }
302
303 static List<SubjectName> getSubjectAltNames(final X509Certificate cert, final int subjectName) {
304 try {
305 final Collection<List<?>> entries = cert.getSubjectAlternativeNames();
306 if (entries == null) {
307 return Collections.emptyList();
308 }
309 final List<SubjectName> result = new ArrayList<>();
310 for (final List<?> entry : entries) {
311 final Integer type = entry.size() >= 2 ? (Integer) entry.get(0) : null;
312 if (type != null) {
313 if (type == subjectName || -1 == subjectName) {
314 final Object o = entry.get(1);
315 if (o instanceof String) {
316 result.add(new SubjectName((String) o, type));
317 } else if (o instanceof byte[]) {
318 final byte[] bytes = (byte[]) o;
319 if (type == SubjectName.IP) {
320 if (bytes.length == 4) {
321 result.add(new SubjectName(byteArrayToIp(bytes), type));
322 } else if (bytes.length == 16) {
323 result.add(new SubjectName(byteArrayToIPv6(bytes), type));
324 }
325 }
326 }
327 }
328 }
329 }
330 return result;
331 } catch (final CertificateParsingException ignore) {
332 return Collections.emptyList();
333 }
334 }
335
336
337
338
339 static String normaliseAddress(final String hostname) {
340 if (hostname == null) {
341 return hostname;
342 }
343 try {
344 final InetAddress inetAddress = InetAddress.getByName(hostname);
345 return inetAddress.getHostAddress();
346 } catch (final UnknownHostException unexpected) {
347 return hostname;
348 }
349 }
350
351 private static String byteArrayToIp(final byte[] bytes) {
352 if (bytes.length != 4) {
353 throw new IllegalArgumentException("Invalid byte array length for IPv4 address");
354 }
355 return (bytes[0] & 0xFF) + "." +
356 (bytes[1] & 0xFF) + "." +
357 (bytes[2] & 0xFF) + "." +
358 (bytes[3] & 0xFF);
359 }
360
361 private static String byteArrayToIPv6(final byte[] bytes) {
362 if (bytes.length != 16) {
363 throw new IllegalArgumentException("Invalid byte array length for IPv6 address");
364 }
365 final StringBuilder sb = new StringBuilder();
366 for (int i = 0; i < bytes.length; i += 2) {
367 sb.append(String.format("%02x%02x", bytes[i], bytes[i + 1]));
368 if (i < bytes.length - 2) {
369 sb.append(":");
370 }
371 }
372 return sb.toString();
373 }
374
375 }