Home | History | Annotate | Download | only in smack
      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 
     21 package org.jivesoftware.smack;
     22 
     23 import javax.net.ssl.X509TrustManager;
     24 
     25 import java.io.FileInputStream;
     26 import java.io.InputStream;
     27 import java.io.IOException;
     28 import java.security.*;
     29 import java.security.cert.CertificateException;
     30 import java.security.cert.CertificateParsingException;
     31 import java.security.cert.X509Certificate;
     32 import java.util.*;
     33 import java.util.regex.Matcher;
     34 import 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  */
     44 class 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