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 return !remainder.contains(".");
223 }
224 return true;
225 }
226 return host.equalsIgnoreCase(identity);
227 }
228
229 static boolean matchIdentity(final String host, final String identity,
230 final PublicSuffixMatcher publicSuffixMatcher) {
231 return matchIdentity(host, identity, publicSuffixMatcher, null, false);
232 }
233
234 static boolean matchIdentity(final String host, final String identity) {
235 return matchIdentity(host, identity, null, null, false);
236 }
237
238 static boolean matchIdentityStrict(final String host, final String identity,
239 final PublicSuffixMatcher publicSuffixMatcher) {
240 return matchIdentity(host, identity, publicSuffixMatcher, null, true);
241 }
242
243 static boolean matchIdentityStrict(final String host, final String identity) {
244 return matchIdentity(host, identity, null, null, true);
245 }
246
247 static boolean matchIdentity(final String host, final String identity,
248 final PublicSuffixMatcher publicSuffixMatcher,
249 final DomainType domainType) {
250 return matchIdentity(host, identity, publicSuffixMatcher, domainType, false);
251 }
252
253 static boolean matchIdentityStrict(final String host, final String identity,
254 final PublicSuffixMatcher publicSuffixMatcher,
255 final DomainType domainType) {
256 return matchIdentity(host, identity, publicSuffixMatcher, domainType, true);
257 }
258
259 static String extractCN(final String subjectPrincipal) throws SSLException {
260 if (subjectPrincipal == null) {
261 return null;
262 }
263 final List<NameValuePair> attributes = DistinguishedNameParser.INSTANCE.parse(subjectPrincipal);
264 for (final NameValuePair attribute: attributes) {
265 if (TextUtils.isBlank(attribute.getName()) || attribute.getValue() == null) {
266 throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name");
267 }
268 if (attribute.getName().equalsIgnoreCase("cn")) {
269 return attribute.getValue();
270 }
271 }
272 return null;
273 }
274
275 static HostNameType determineHostFormat(final String host) {
276 if (InetAddressUtils.isIPv4Address(host)) {
277 return HostNameType.IPv4;
278 }
279 String s = host;
280 if (s.startsWith("[") && s.endsWith("]")) {
281 s = host.substring(1, host.length() - 1);
282 }
283 if (InetAddressUtils.isIPv6Address(s)) {
284 return HostNameType.IPv6;
285 }
286 return HostNameType.DNS;
287 }
288
289 static List<SubjectName> getSubjectAltNames(final X509Certificate cert) {
290 return getSubjectAltNames(cert, -1);
291 }
292
293 static List<SubjectName> getSubjectAltNames(final X509Certificate cert, final int subjectName) {
294 try {
295 final Collection<List<?>> entries = cert.getSubjectAlternativeNames();
296 if (entries == null) {
297 return Collections.emptyList();
298 }
299 final List<SubjectName> result = new ArrayList<>();
300 for (final List<?> entry : entries) {
301 final Integer type = entry.size() >= 2 ? (Integer) entry.get(0) : null;
302 if (type != null) {
303 if (type == subjectName || -1 == subjectName) {
304 final Object o = entry.get(1);
305 if (o instanceof String) {
306 result.add(new SubjectName((String) o, type));
307 } else if (o instanceof byte[]) {
308
309 }
310 }
311 }
312 }
313 return result;
314 } catch (final CertificateParsingException ignore) {
315 return Collections.emptyList();
316 }
317 }
318
319
320
321
322 static String normaliseAddress(final String hostname) {
323 if (hostname == null) {
324 return hostname;
325 }
326 try {
327 final InetAddress inetAddress = InetAddress.getByName(hostname);
328 return inetAddress.getHostAddress();
329 } catch (final UnknownHostException unexpected) {
330 return hostname;
331 }
332 }
333 }