1/** 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} 332