blob: 63da3e7b94c184b9f8adf89aef005d4664b9025d [file] [log] [blame]
Shuyi Chend7955ce2013-05-22 14:51:55 -07001/**
2 * $RCSfile$
3 * $Revision$
4 * $Date$
5 *
6 * Copyright 2003-2005 Jive Software.
7 *
8 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 */
20
21package org.jivesoftware.smack;
22
23import javax.net.ssl.X509TrustManager;
24
25import java.io.FileInputStream;
26import java.io.InputStream;
27import java.io.IOException;
28import java.security.*;
29import java.security.cert.CertificateException;
30import java.security.cert.CertificateParsingException;
31import java.security.cert.X509Certificate;
32import java.util.*;
33import java.util.regex.Matcher;
34import java.util.regex.Pattern;
35
36/**
37 * Trust manager that checks all certificates presented by the server. This class
38 * is used during TLS negotiation. It is possible to disable/enable some or all checkings
39 * by configuring the {@link ConnectionConfiguration}. The truststore file that contains
40 * knows and trusted CA root certificates can also be configure in {@link ConnectionConfiguration}.
41 *
42 * @author Gaston Dombiak
43 */
44class ServerTrustManager implements X509TrustManager {
45
46 private static Pattern cnPattern = Pattern.compile("(?i)(cn=)([^,]*)");
47
48 private ConnectionConfiguration configuration;
49
50 /**
51 * Holds the domain of the remote server we are trying to connect
52 */
53 private String server;
54 private KeyStore trustStore;
55
56 private static Map<KeyStoreOptions, KeyStore> stores = new HashMap<KeyStoreOptions, KeyStore>();
57
58 public ServerTrustManager(String server, ConnectionConfiguration configuration) {
59 this.configuration = configuration;
60 this.server = server;
61
62 InputStream in = null;
63 synchronized (stores) {
64 KeyStoreOptions options = new KeyStoreOptions(configuration.getTruststoreType(),
65 configuration.getTruststorePath(), configuration.getTruststorePassword());
66 if (stores.containsKey(options)) {
67 trustStore = stores.get(options);
68 } else {
69 try {
70 trustStore = KeyStore.getInstance(options.getType());
71 in = new FileInputStream(options.getPath());
72 trustStore.load(in, options.getPassword().toCharArray());
73 } catch (Exception e) {
74 trustStore = null;
75 e.printStackTrace();
76 } finally {
77 if (in != null) {
78 try {
79 in.close();
80 } catch (IOException ioe) {
81 // Ignore.
82 }
83 }
84 }
85 stores.put(options, trustStore);
86 }
87 if (trustStore == null)
88 // Disable root CA checking
89 configuration.setVerifyRootCAEnabled(false);
90 }
91 }
92
93 public X509Certificate[] getAcceptedIssuers() {
94 return new X509Certificate[0];
95 }
96
97 public void checkClientTrusted(X509Certificate[] arg0, String arg1)
98 throws CertificateException {
99 }
100
101 public void checkServerTrusted(X509Certificate[] x509Certificates, String arg1)
102 throws CertificateException {
103
104 int nSize = x509Certificates.length;
105
106 List<String> peerIdentities = getPeerIdentity(x509Certificates[0]);
107
108 if (configuration.isVerifyChainEnabled()) {
109 // Working down the chain, for every certificate in the chain,
110 // verify that the subject of the certificate is the issuer of the
111 // next certificate in the chain.
112 Principal principalLast = null;
113 for (int i = nSize -1; i >= 0 ; i--) {
114 X509Certificate x509certificate = x509Certificates[i];
115 Principal principalIssuer = x509certificate.getIssuerDN();
116 Principal principalSubject = x509certificate.getSubjectDN();
117 if (principalLast != null) {
118 if (principalIssuer.equals(principalLast)) {
119 try {
120 PublicKey publickey =
121 x509Certificates[i + 1].getPublicKey();
122 x509Certificates[i].verify(publickey);
123 }
124 catch (GeneralSecurityException generalsecurityexception) {
125 throw new CertificateException(
126 "signature verification failed of " + peerIdentities);
127 }
128 }
129 else {
130 throw new CertificateException(
131 "subject/issuer verification failed of " + peerIdentities);
132 }
133 }
134 principalLast = principalSubject;
135 }
136 }
137
138 if (configuration.isVerifyRootCAEnabled()) {
139 // Verify that the the last certificate in the chain was issued
140 // by a third-party that the client trusts.
141 boolean trusted = false;
142 try {
143 trusted = trustStore.getCertificateAlias(x509Certificates[nSize - 1]) != null;
144 if (!trusted && nSize == 1 && configuration.isSelfSignedCertificateEnabled())
145 {
146 System.out.println("Accepting self-signed certificate of remote server: " +
147 peerIdentities);
148 trusted = true;
149 }
150 }
151 catch (KeyStoreException e) {
152 e.printStackTrace();
153 }
154 if (!trusted) {
155 throw new CertificateException("root certificate not trusted of " + peerIdentities);
156 }
157 }
158
159 if (configuration.isNotMatchingDomainCheckEnabled()) {
160 // Verify that the first certificate in the chain corresponds to
161 // the server we desire to authenticate.
162 // Check if the certificate uses a wildcard indicating that subdomains are valid
163 if (peerIdentities.size() == 1 && peerIdentities.get(0).startsWith("*.")) {
164 // Remove the wildcard
165 String peerIdentity = peerIdentities.get(0).replace("*.", "");
166 // Check if the requested subdomain matches the certified domain
167 if (!server.endsWith(peerIdentity)) {
168 throw new CertificateException("target verification failed of " + peerIdentities);
169 }
170 }
171 else if (!peerIdentities.contains(server)) {
172 throw new CertificateException("target verification failed of " + peerIdentities);
173 }
174 }
175
176 if (configuration.isExpiredCertificatesCheckEnabled()) {
177 // For every certificate in the chain, verify that the certificate
178 // is valid at the current time.
179 Date date = new Date();
180 for (int i = 0; i < nSize; i++) {
181 try {
182 x509Certificates[i].checkValidity(date);
183 }
184 catch (GeneralSecurityException generalsecurityexception) {
185 throw new CertificateException("invalid date of " + server);
186 }
187 }
188 }
189
190 }
191
192 /**
193 * Returns the identity of the remote server as defined in the specified certificate. The
194 * identity is defined in the subjectDN of the certificate and it can also be defined in
195 * the subjectAltName extension of type "xmpp". When the extension is being used then the
196 * identity defined in the extension in going to be returned. Otherwise, the value stored in
197 * the subjectDN is returned.
198 *
199 * @param x509Certificate the certificate the holds the identity of the remote server.
200 * @return the identity of the remote server as defined in the specified certificate.
201 */
202 public static List<String> getPeerIdentity(X509Certificate x509Certificate) {
203 // Look the identity in the subjectAltName extension if available
204 List<String> names = getSubjectAlternativeNames(x509Certificate);
205 if (names.isEmpty()) {
206 String name = x509Certificate.getSubjectDN().getName();
207 Matcher matcher = cnPattern.matcher(name);
208 if (matcher.find()) {
209 name = matcher.group(2);
210 }
211 // Create an array with the unique identity
212 names = new ArrayList<String>();
213 names.add(name);
214 }
215 return names;
216 }
217
218 /**
219 * Returns the JID representation of an XMPP entity contained as a SubjectAltName extension
220 * in the certificate. If none was found then return <tt>null</tt>.
221 *
222 * @param certificate the certificate presented by the remote entity.
223 * @return the JID representation of an XMPP entity contained as a SubjectAltName extension
224 * in the certificate. If none was found then return <tt>null</tt>.
225 */
226 private static List<String> getSubjectAlternativeNames(X509Certificate certificate) {
227 List<String> identities = new ArrayList<String>();
228 try {
229 Collection<List<?>> altNames = certificate.getSubjectAlternativeNames();
230 // Check that the certificate includes the SubjectAltName extension
231 if (altNames == null) {
232 return Collections.emptyList();
233 }
234 // Use the type OtherName to search for the certified server name
235 /*for (List item : altNames) {
236 Integer type = (Integer) item.get(0);
237 if (type == 0) {
238 // Type OtherName found so return the associated value
239 try {
240 // Value is encoded using ASN.1 so decode it to get the server's identity
241 ASN1InputStream decoder = new ASN1InputStream((byte[]) item.toArray()[1]);
242 DEREncodable encoded = decoder.readObject();
243 encoded = ((DERSequence) encoded).getObjectAt(1);
244 encoded = ((DERTaggedObject) encoded).getObject();
245 encoded = ((DERTaggedObject) encoded).getObject();
246 String identity = ((DERUTF8String) encoded).getString();
247 // Add the decoded server name to the list of identities
248 identities.add(identity);
249 }
250 catch (UnsupportedEncodingException e) {
251 // Ignore
252 }
253 catch (IOException e) {
254 // Ignore
255 }
256 catch (Exception e) {
257 e.printStackTrace();
258 }
259 }
260 // Other types are not good for XMPP so ignore them
261 System.out.println("SubjectAltName of invalid type found: " + certificate);
262 }*/
263 }
264 catch (CertificateParsingException e) {
265 e.printStackTrace();
266 }
267 return identities;
268 }
269
270 private static class KeyStoreOptions {
271 private final String type;
272 private final String path;
273 private final String password;
274
275 public KeyStoreOptions(String type, String path, String password) {
276 super();
277 this.type = type;
278 this.path = path;
279 this.password = password;
280 }
281
282 public String getType() {
283 return type;
284 }
285
286 public String getPath() {
287 return path;
288 }
289
290 public String getPassword() {
291 return password;
292 }
293
294 @Override
295 public int hashCode() {
296 final int prime = 31;
297 int result = 1;
298 result = prime * result + ((password == null) ? 0 : password.hashCode());
299 result = prime * result + ((path == null) ? 0 : path.hashCode());
300 result = prime * result + ((type == null) ? 0 : type.hashCode());
301 return result;
302 }
303
304 @Override
305 public boolean equals(Object obj) {
306 if (this == obj)
307 return true;
308 if (obj == null)
309 return false;
310 if (getClass() != obj.getClass())
311 return false;
312 KeyStoreOptions other = (KeyStoreOptions) obj;
313 if (password == null) {
314 if (other.password != null)
315 return false;
316 } else if (!password.equals(other.password))
317 return false;
318 if (path == null) {
319 if (other.path != null)
320 return false;
321 } else if (!path.equals(other.path))
322 return false;
323 if (type == null) {
324 if (other.type != null)
325 return false;
326 } else if (!type.equals(other.type))
327 return false;
328 return true;
329 }
330 }
331}