/** * $RCSfile$ * $Revision$ * $Date$ * * Copyright 2003-2005 Jive Software. * * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.smack; import javax.net.ssl.X509TrustManager; import java.io.FileInputStream; import java.io.InputStream; import java.io.IOException; import java.security.*; import java.security.cert.CertificateException; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Trust manager that checks all certificates presented by the server. This class * is used during TLS negotiation. It is possible to disable/enable some or all checkings * by configuring the {@link ConnectionConfiguration}. The truststore file that contains * knows and trusted CA root certificates can also be configure in {@link ConnectionConfiguration}. * * @author Gaston Dombiak */ class ServerTrustManager implements X509TrustManager { private static Pattern cnPattern = Pattern.compile("(?i)(cn=)([^,]*)"); private ConnectionConfiguration configuration; /** * Holds the domain of the remote server we are trying to connect */ private String server; private KeyStore trustStore; private static Map stores = new HashMap(); public ServerTrustManager(String server, ConnectionConfiguration configuration) { this.configuration = configuration; this.server = server; InputStream in = null; synchronized (stores) { KeyStoreOptions options = new KeyStoreOptions(configuration.getTruststoreType(), configuration.getTruststorePath(), configuration.getTruststorePassword()); if (stores.containsKey(options)) { trustStore = stores.get(options); } else { try { trustStore = KeyStore.getInstance(options.getType()); in = new FileInputStream(options.getPath()); trustStore.load(in, options.getPassword().toCharArray()); } catch (Exception e) { trustStore = null; e.printStackTrace(); } finally { if (in != null) { try { in.close(); } catch (IOException ioe) { // Ignore. } } } stores.put(options, trustStore); } if (trustStore == null) // Disable root CA checking configuration.setVerifyRootCAEnabled(false); } } public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { } public void checkServerTrusted(X509Certificate[] x509Certificates, String arg1) throws CertificateException { int nSize = x509Certificates.length; List peerIdentities = getPeerIdentity(x509Certificates[0]); if (configuration.isVerifyChainEnabled()) { // Working down the chain, for every certificate in the chain, // verify that the subject of the certificate is the issuer of the // next certificate in the chain. Principal principalLast = null; for (int i = nSize -1; i >= 0 ; i--) { X509Certificate x509certificate = x509Certificates[i]; Principal principalIssuer = x509certificate.getIssuerDN(); Principal principalSubject = x509certificate.getSubjectDN(); if (principalLast != null) { if (principalIssuer.equals(principalLast)) { try { PublicKey publickey = x509Certificates[i + 1].getPublicKey(); x509Certificates[i].verify(publickey); } catch (GeneralSecurityException generalsecurityexception) { throw new CertificateException( "signature verification failed of " + peerIdentities); } } else { throw new CertificateException( "subject/issuer verification failed of " + peerIdentities); } } principalLast = principalSubject; } } if (configuration.isVerifyRootCAEnabled()) { // Verify that the the last certificate in the chain was issued // by a third-party that the client trusts. boolean trusted = false; try { trusted = trustStore.getCertificateAlias(x509Certificates[nSize - 1]) != null; if (!trusted && nSize == 1 && configuration.isSelfSignedCertificateEnabled()) { System.out.println("Accepting self-signed certificate of remote server: " + peerIdentities); trusted = true; } } catch (KeyStoreException e) { e.printStackTrace(); } if (!trusted) { throw new CertificateException("root certificate not trusted of " + peerIdentities); } } if (configuration.isNotMatchingDomainCheckEnabled()) { // Verify that the first certificate in the chain corresponds to // the server we desire to authenticate. // Check if the certificate uses a wildcard indicating that subdomains are valid if (peerIdentities.size() == 1 && peerIdentities.get(0).startsWith("*.")) { // Remove the wildcard String peerIdentity = peerIdentities.get(0).replace("*.", ""); // Check if the requested subdomain matches the certified domain if (!server.endsWith(peerIdentity)) { throw new CertificateException("target verification failed of " + peerIdentities); } } else if (!peerIdentities.contains(server)) { throw new CertificateException("target verification failed of " + peerIdentities); } } if (configuration.isExpiredCertificatesCheckEnabled()) { // For every certificate in the chain, verify that the certificate // is valid at the current time. Date date = new Date(); for (int i = 0; i < nSize; i++) { try { x509Certificates[i].checkValidity(date); } catch (GeneralSecurityException generalsecurityexception) { throw new CertificateException("invalid date of " + server); } } } } /** * Returns the identity of the remote server as defined in the specified certificate. The * identity is defined in the subjectDN of the certificate and it can also be defined in * the subjectAltName extension of type "xmpp". When the extension is being used then the * identity defined in the extension in going to be returned. Otherwise, the value stored in * the subjectDN is returned. * * @param x509Certificate the certificate the holds the identity of the remote server. * @return the identity of the remote server as defined in the specified certificate. */ public static List getPeerIdentity(X509Certificate x509Certificate) { // Look the identity in the subjectAltName extension if available List names = getSubjectAlternativeNames(x509Certificate); if (names.isEmpty()) { String name = x509Certificate.getSubjectDN().getName(); Matcher matcher = cnPattern.matcher(name); if (matcher.find()) { name = matcher.group(2); } // Create an array with the unique identity names = new ArrayList(); names.add(name); } return names; } /** * Returns the JID representation of an XMPP entity contained as a SubjectAltName extension * in the certificate. If none was found then return null. * * @param certificate the certificate presented by the remote entity. * @return the JID representation of an XMPP entity contained as a SubjectAltName extension * in the certificate. If none was found then return null. */ private static List getSubjectAlternativeNames(X509Certificate certificate) { List identities = new ArrayList(); try { Collection> altNames = certificate.getSubjectAlternativeNames(); // Check that the certificate includes the SubjectAltName extension if (altNames == null) { return Collections.emptyList(); } // Use the type OtherName to search for the certified server name /*for (List item : altNames) { Integer type = (Integer) item.get(0); if (type == 0) { // Type OtherName found so return the associated value try { // Value is encoded using ASN.1 so decode it to get the server's identity ASN1InputStream decoder = new ASN1InputStream((byte[]) item.toArray()[1]); DEREncodable encoded = decoder.readObject(); encoded = ((DERSequence) encoded).getObjectAt(1); encoded = ((DERTaggedObject) encoded).getObject(); encoded = ((DERTaggedObject) encoded).getObject(); String identity = ((DERUTF8String) encoded).getString(); // Add the decoded server name to the list of identities identities.add(identity); } catch (UnsupportedEncodingException e) { // Ignore } catch (IOException e) { // Ignore } catch (Exception e) { e.printStackTrace(); } } // Other types are not good for XMPP so ignore them System.out.println("SubjectAltName of invalid type found: " + certificate); }*/ } catch (CertificateParsingException e) { e.printStackTrace(); } return identities; } private static class KeyStoreOptions { private final String type; private final String path; private final String password; public KeyStoreOptions(String type, String path, String password) { super(); this.type = type; this.path = path; this.password = password; } public String getType() { return type; } public String getPath() { return path; } public String getPassword() { return password; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((password == null) ? 0 : password.hashCode()); result = prime * result + ((path == null) ? 0 : path.hashCode()); result = prime * result + ((type == null) ? 0 : type.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; KeyStoreOptions other = (KeyStoreOptions) obj; if (password == null) { if (other.password != null) return false; } else if (!password.equals(other.password)) return false; if (path == null) { if (other.path != null) return false; } else if (!path.equals(other.path)) return false; if (type == null) { if (other.type != null) return false; } else if (!type.equals(other.type)) return false; return true; } } }