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.List;
39
40 import javax.net.ssl.SSLException;
41 import javax.net.ssl.SSLPeerUnverifiedException;
42 import javax.net.ssl.SSLSession;
43 import javax.security.auth.x500.X500Principal;
44
45 import org.apache.hc.client5.http.psl.DomainType;
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 public DefaultHostnameVerifier(final PublicSuffixMatcher publicSuffixMatcher) {
81 this.publicSuffixMatcher = publicSuffixMatcher;
82 }
83
84 public DefaultHostnameVerifier() {
85 this(null);
86 }
87
88 @Override
89 public boolean verify(final String host, final SSLSession session) {
90 try {
91 final Certificate[] certs = session.getPeerCertificates();
92 final X509Certificate x509 = (X509Certificate) certs[0];
93 verify(host, x509);
94 return true;
95 } catch (final SSLException ex) {
96 if (LOG.isDebugEnabled()) {
97 LOG.debug(ex.getMessage(), ex);
98 }
99 return false;
100 }
101 }
102
103 @Override
104 public void verify(final String host, final X509Certificate cert) throws SSLException {
105 final HostNameType hostType = determineHostFormat(host);
106 switch (hostType) {
107 case IPv4:
108 matchIPAddress(host, getSubjectAltNames(cert, SubjectName.IP));
109 break;
110 case IPv6:
111 matchIPv6Address(host, getSubjectAltNames(cert, SubjectName.IP));
112 break;
113 default:
114 final List<SubjectName> subjectAlts = getSubjectAltNames(cert, SubjectName.DNS);
115 if (subjectAlts.isEmpty()) {
116
117
118 matchCN(host, cert, this.publicSuffixMatcher);
119 } else {
120 matchDNSName(host, subjectAlts, this.publicSuffixMatcher);
121 }
122 }
123 }
124
125 static void matchIPAddress(final String host, final List<SubjectName> subjectAlts) throws SSLException {
126 for (int i = 0; i < subjectAlts.size(); i++) {
127 final SubjectName subjectAlt = subjectAlts.get(i);
128 if (subjectAlt.getType() == SubjectName.IP) {
129 if (host.equals(subjectAlt.getValue())) {
130 return;
131 }
132 }
133 }
134 throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " +
135 "of the subject alternative names: " + subjectAlts);
136 }
137
138 static void matchIPv6Address(final String host, final List<SubjectName> subjectAlts) throws SSLException {
139 final String normalisedHost = normaliseAddress(host);
140 for (int i = 0; i < subjectAlts.size(); i++) {
141 final SubjectName subjectAlt = subjectAlts.get(i);
142 if (subjectAlt.getType() == SubjectName.IP) {
143 final String normalizedSubjectAlt = normaliseAddress(subjectAlt.getValue());
144 if (normalisedHost.equals(normalizedSubjectAlt)) {
145 return;
146 }
147 }
148 }
149 throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " +
150 "of the subject alternative names: " + subjectAlts);
151 }
152
153 static void matchDNSName(final String host, final List<SubjectName> subjectAlts,
154 final PublicSuffixMatcher publicSuffixMatcher) throws SSLException {
155 final String normalizedHost = DnsUtils.normalize(host);
156 for (int i = 0; i < subjectAlts.size(); i++) {
157 final SubjectName subjectAlt = subjectAlts.get(i);
158 if (subjectAlt.getType() == SubjectName.DNS) {
159 final String normalizedSubjectAlt = DnsUtils.normalize(subjectAlt.getValue());
160 if (matchIdentityStrict(normalizedHost, normalizedSubjectAlt, publicSuffixMatcher)) {
161 return;
162 }
163 }
164 }
165 throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " +
166 "of the subject alternative names: " + subjectAlts);
167 }
168
169 static void matchCN(final String host, final X509Certificate cert,
170 final PublicSuffixMatcher publicSuffixMatcher) throws SSLException {
171 final X500Principal subjectPrincipal = cert.getSubjectX500Principal();
172 final String cn = extractCN(subjectPrincipal.getName(X500Principal.RFC2253));
173 if (cn == null) {
174 throw new SSLPeerUnverifiedException("Certificate subject for <" + host + "> doesn't contain " +
175 "a common name and does not have alternative names");
176 }
177 final String normalizedHost = DnsUtils.normalize(host);
178 final String normalizedCn = DnsUtils.normalize(cn);
179 if (!matchIdentityStrict(normalizedHost, normalizedCn, publicSuffixMatcher)) {
180 throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match " +
181 "common name of the certificate subject: " + cn);
182 }
183 }
184
185 static boolean matchDomainRoot(final String host, final String domainRoot) {
186 if (domainRoot == null) {
187 return false;
188 }
189 return host.endsWith(domainRoot) && (host.length() == domainRoot.length()
190 || host.charAt(host.length() - domainRoot.length() - 1) == '.');
191 }
192
193 private static boolean matchIdentity(final String host, final String identity,
194 final PublicSuffixMatcher publicSuffixMatcher,
195 final DomainType domainType,
196 final boolean strict) {
197 if (publicSuffixMatcher != null && host.contains(".")) {
198 if (!matchDomainRoot(host, publicSuffixMatcher.getDomainRoot(identity, domainType))) {
199 return false;
200 }
201 }
202
203
204
205
206
207
208 final int asteriskIdx = identity.indexOf('*');
209 if (asteriskIdx != -1) {
210 final String prefix = identity.substring(0, asteriskIdx);
211 final String suffix = identity.substring(asteriskIdx + 1);
212 if (!prefix.isEmpty() && !host.startsWith(prefix)) {
213 return false;
214 }
215 if (!suffix.isEmpty() && !host.endsWith(suffix)) {
216 return false;
217 }
218
219 if (strict) {
220 final String remainder = host.substring(
221 prefix.length(), host.length() - suffix.length());
222 if (remainder.contains(".")) {
223 return false;
224 }
225 }
226 return true;
227 }
228 return host.equalsIgnoreCase(identity);
229 }
230
231 static boolean matchIdentity(final String host, final String identity,
232 final PublicSuffixMatcher publicSuffixMatcher) {
233 return matchIdentity(host, identity, publicSuffixMatcher, null, false);
234 }
235
236 static boolean matchIdentity(final String host, final String identity) {
237 return matchIdentity(host, identity, null, null, false);
238 }
239
240 static boolean matchIdentityStrict(final String host, final String identity,
241 final PublicSuffixMatcher publicSuffixMatcher) {
242 return matchIdentity(host, identity, publicSuffixMatcher, null, true);
243 }
244
245 static boolean matchIdentityStrict(final String host, final String identity) {
246 return matchIdentity(host, identity, null, null, true);
247 }
248
249 static boolean matchIdentity(final String host, final String identity,
250 final PublicSuffixMatcher publicSuffixMatcher,
251 final DomainType domainType) {
252 return matchIdentity(host, identity, publicSuffixMatcher, domainType, false);
253 }
254
255 static boolean matchIdentityStrict(final String host, final String identity,
256 final PublicSuffixMatcher publicSuffixMatcher,
257 final DomainType domainType) {
258 return matchIdentity(host, identity, publicSuffixMatcher, domainType, true);
259 }
260
261 static String extractCN(final String subjectPrincipal) throws SSLException {
262 if (subjectPrincipal == null) {
263 return null;
264 }
265 final List<NameValuePair> attributes = DistinguishedNameParser.INSTANCE.parse(subjectPrincipal);
266 for (final NameValuePair attribute: attributes) {
267 if (TextUtils.isBlank(attribute.getName()) || attribute.getValue() == null) {
268 throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name");
269 }
270 if (attribute.getName().equalsIgnoreCase("cn")) {
271 return attribute.getValue();
272 }
273 }
274 return null;
275 }
276
277 static HostNameType determineHostFormat(final String host) {
278 if (InetAddressUtils.isIPv4Address(host)) {
279 return HostNameType.IPv4;
280 }
281 String s = host;
282 if (s.startsWith("[") && s.endsWith("]")) {
283 s = host.substring(1, host.length() - 1);
284 }
285 if (InetAddressUtils.isIPv6Address(s)) {
286 return HostNameType.IPv6;
287 }
288 return HostNameType.DNS;
289 }
290
291 static List<SubjectName> getSubjectAltNames(final X509Certificate cert) {
292 return getSubjectAltNames(cert, -1);
293 }
294
295 static List<SubjectName> getSubjectAltNames(final X509Certificate cert, final int subjectName) {
296 try {
297 final Collection<List<?>> entries = cert.getSubjectAlternativeNames();
298 if (entries == null) {
299 return Collections.emptyList();
300 }
301 final List<SubjectName> result = new ArrayList<>();
302 for (final List<?> entry : entries) {
303 final Integer type = entry.size() >= 2 ? (Integer) entry.get(0) : null;
304 if (type != null) {
305 if (type == subjectName || -1 == subjectName) {
306 final Object o = entry.get(1);
307 if (o instanceof String) {
308 result.add(new SubjectName((String) o, type));
309 } else if (o instanceof byte[]) {
310
311 }
312 }
313 }
314 }
315 return result;
316 } catch (final CertificateParsingException ignore) {
317 return Collections.emptyList();
318 }
319 }
320
321
322
323
324 static String normaliseAddress(final String hostname) {
325 if (hostname == null) {
326 return hostname;
327 }
328 try {
329 final InetAddress inetAddress = InetAddress.getByName(hostname);
330 return inetAddress.getHostAddress();
331 } catch (final UnknownHostException unexpected) {
332 return hostname;
333 }
334 }
335 }