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