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