001/**
002 *
003 * Copyright 2014-2020 Florian Schmaus
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smack.util;
018
019import java.io.DataInputStream;
020import java.io.File;
021import java.io.FileInputStream;
022import java.io.FileNotFoundException;
023import java.io.IOException;
024import java.io.InputStream;
025import java.security.MessageDigest;
026import java.security.NoSuchAlgorithmException;
027import java.security.cert.Certificate;
028import java.security.cert.CertificateEncodingException;
029import java.security.cert.CertificateException;
030import java.security.cert.X509Certificate;
031import java.util.Arrays;
032import java.util.HashSet;
033import java.util.Set;
034import java.util.logging.Level;
035import java.util.logging.Logger;
036
037import javax.net.ssl.SSLPeerUnverifiedException;
038import javax.net.ssl.SSLSession;
039import javax.net.ssl.SSLSocket;
040import javax.net.ssl.X509TrustManager;
041
042import org.jivesoftware.smack.ConnectionConfiguration;
043import org.jivesoftware.smack.SmackException.SecurityNotPossibleException;
044
045
046public class TLSUtils {
047
048    private static final Logger LOGGER = Logger.getLogger(TLSUtils.class.getName());
049
050    public static final String SSL = "SSL";
051    public static final String TLS = "TLS";
052    public static final String PROTO_SSL3 = SSL + "v3";
053    public static final String PROTO_TLSV1 = TLS + "v1";
054    public static final String PROTO_TLSV1_1 = TLS + "v1.1";
055    public static final String PROTO_TLSV1_2 = TLS + "v1.2";
056    public static final String PROTO_TLSV1_3 = TLS + "v1.3";
057
058    /**
059     * Enable the recommended TLS protocols.
060     *
061     * @param builder the configuration builder to apply this setting to
062     * @param <B> Type of the ConnectionConfiguration builder.
063     *
064     * @return the given builder
065     */
066    public static <B extends ConnectionConfiguration.Builder<B, ?>> B setEnabledTlsProtocolsToRecommended(B builder) {
067        builder.setEnabledSSLProtocols(new String[] { PROTO_TLSV1_3, PROTO_TLSV1_2 });
068        return builder;
069    }
070
071    /**
072     * Enable only TLS. Connections created with the given ConnectionConfiguration will only support TLS.
073     * <p>
074     * According to the <a
075     * href="https://raw.githubusercontent.com/stpeter/manifesto/master/manifesto.txt">Encrypted
076     * XMPP Manifesto</a>, TLSv1.2 shall be deployed, providing fallback support for SSLv3 and
077     * TLSv1.1. This method goes one step beyond and upgrades the handshake to use TLSv1 or better.
078     * This method requires the underlying OS to support all of TLSv1.2 , 1.1 and 1.0.
079     * </p>
080     *
081     * @param builder the configuration builder to apply this setting to
082     * @param <B> Type of the ConnectionConfiguration builder.
083     *
084     * @return the given builder
085     * @deprecated use {@link #setEnabledTlsProtocolsToRecommended(org.jivesoftware.smack.ConnectionConfiguration.Builder)} instead.
086     */
087    // TODO: Remove in Smack 4.5.
088    @Deprecated
089    public static <B extends ConnectionConfiguration.Builder<B, ?>> B setTLSOnly(B builder) {
090        builder.setEnabledSSLProtocols(new String[] { PROTO_TLSV1_2,  PROTO_TLSV1_1, PROTO_TLSV1 });
091        return builder;
092    }
093
094    /**
095     * Enable only TLS and SSLv3. Connections created with the given ConnectionConfiguration will
096     * only support TLS and SSLv3.
097     * <p>
098     * According to the <a
099     * href="https://raw.githubusercontent.com/stpeter/manifesto/master/manifesto.txt">Encrypted
100     * XMPP Manifesto</a>, TLSv1.2 shall be deployed, providing fallback support for SSLv3 and
101     * TLSv1.1.
102     * </p>
103     *
104     * @param builder the configuration builder to apply this setting to
105     * @param <B> Type of the ConnectionConfiguration builder.
106     *
107     * @return the given builder
108     * @deprecated use {@link #setEnabledTlsProtocolsToRecommended(org.jivesoftware.smack.ConnectionConfiguration.Builder)} instead.
109     */
110    // TODO: Remove in Smack 4.5.
111    @Deprecated
112    public static <B extends ConnectionConfiguration.Builder<B, ?>> B setSSLv3AndTLSOnly(B builder) {
113        builder.setEnabledSSLProtocols(new String[] { PROTO_TLSV1_2,  PROTO_TLSV1_1, PROTO_TLSV1, PROTO_SSL3 });
114        return builder;
115    }
116
117    /**
118     * Accept all TLS certificates.
119     * <p>
120     * <b>Warning:</b> Use with care. This method make the Connection use {@link AcceptAllTrustManager} and essentially
121     * <b>invalidates all security guarantees provided by TLS</b>. Only use this method if you understand the
122     * implications.
123     * </p>
124     *
125     * @param builder a connection configuration builder.
126     * @param <B> Type of the ConnectionConfiguration builder.
127     * @return the given builder.
128     */
129    public static <B extends ConnectionConfiguration.Builder<B, ?>> B acceptAllCertificates(B builder) {
130        builder.setCustomX509TrustManager(new AcceptAllTrustManager());
131        return builder;
132    }
133
134    /**
135     * Disable the hostname verification of TLS certificates.
136     * <p>
137     * <b>Warning:</b> Use with care. This disables hostname verification of TLS certificates and essentially
138     * <b>invalidates all security guarantees provided by TLS</b>. Only use this method if you understand the
139     * implications.
140     * </p>
141     *
142     * @param builder a connection configuration builder.
143     * @param <B> Type of the ConnectionConfiguration builder.
144     * @return the given builder.
145     */
146    public static <B extends ConnectionConfiguration.Builder<B, ?>> B disableHostnameVerificationForTlsCertificates(B builder) {
147        builder.setHostnameVerifier((hostname, session) -> {
148            return true;
149        });
150        return builder;
151    }
152
153    public static void setEnabledProtocolsAndCiphers(final SSLSocket sslSocket,
154                    String[] enabledProtocols, String[] enabledCiphers)
155                    throws SecurityNotPossibleException {
156        if (enabledProtocols != null) {
157            Set<String> enabledProtocolsSet = new HashSet<String>(Arrays.asList(enabledProtocols));
158            Set<String> supportedProtocolsSet = new HashSet<String>(
159                            Arrays.asList(sslSocket.getSupportedProtocols()));
160            Set<String> protocolsIntersection = new HashSet<String>(supportedProtocolsSet);
161            protocolsIntersection.retainAll(enabledProtocolsSet);
162            if (protocolsIntersection.isEmpty()) {
163                throw new SecurityNotPossibleException("Request to enable SSL/TLS protocols '"
164                                + StringUtils.collectionToString(enabledProtocolsSet)
165                                + "', but only '"
166                                + StringUtils.collectionToString(supportedProtocolsSet)
167                                + "' are supported.");
168            }
169
170            // Set the enabled protocols
171            enabledProtocols = new String[protocolsIntersection.size()];
172            enabledProtocols = protocolsIntersection.toArray(enabledProtocols);
173            sslSocket.setEnabledProtocols(enabledProtocols);
174        }
175
176        if (enabledCiphers != null) {
177            Set<String> enabledCiphersSet = new HashSet<String>(Arrays.asList(enabledCiphers));
178            Set<String> supportedCiphersSet = new HashSet<String>(
179                            Arrays.asList(sslSocket.getEnabledCipherSuites()));
180            Set<String> ciphersIntersection = new HashSet<String>(supportedCiphersSet);
181            ciphersIntersection.retainAll(enabledCiphersSet);
182            if (ciphersIntersection.isEmpty()) {
183                throw new SecurityNotPossibleException("Request to enable SSL/TLS ciphers '"
184                                + StringUtils.collectionToString(enabledCiphersSet)
185                                + "', but only '"
186                                + StringUtils.collectionToString(supportedCiphersSet)
187                                + "' are supported.");
188            }
189
190            enabledCiphers = new String[ciphersIntersection.size()];
191            enabledCiphers = ciphersIntersection.toArray(enabledCiphers);
192            sslSocket.setEnabledCipherSuites(enabledCiphers);
193        }
194    }
195
196    /**
197     * Get the channel binding data for the 'tls-server-end-point' channel binding type. This channel binding type is
198     * defined in RFC 5929 § 4.
199     *
200     * @param sslSession the SSL/TLS session from which the data should be retrieved.
201     * @return the channel binding data.
202     * @throws SSLPeerUnverifiedException if we TLS peer could not be verified.
203     * @throws CertificateEncodingException if there was an encoding error with the certificate.
204     * @throws NoSuchAlgorithmException if no such algorithm is available.
205     * @see <a href="https://tools.ietf.org/html/rfc5929#section-4">RFC 5929 § 4.</a>
206     */
207    public static byte[] getChannelBindingTlsServerEndPoint(final SSLSession sslSession)
208                    throws SSLPeerUnverifiedException, CertificateEncodingException, NoSuchAlgorithmException {
209        final Certificate[] peerCertificates = sslSession.getPeerCertificates();
210        final Certificate certificate = peerCertificates[0];
211        final String certificateAlgorithm = certificate.getPublicKey().getAlgorithm();
212
213        // RFC 5929 § 4.1 hash function selection.
214        String algorithm;
215        switch (certificateAlgorithm) {
216        case "MD5":
217        case "SHA-1":
218            algorithm = "SHA-256";
219            break;
220        default:
221            algorithm = certificateAlgorithm;
222            break;
223        }
224
225        final MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
226        final byte[] certificateDerEncoded = certificate.getEncoded();
227        messageDigest.update(certificateDerEncoded);
228        return messageDigest.digest();
229    }
230
231    /**
232     * A {@link X509TrustManager} that <b>doesn't validate</b> X.509 certificates.
233     * <p>
234     * Connections that use this TrustManager will just be encrypted, without any guarantee that the
235     * counter part is actually the intended one. Man-in-the-Middle attacks will be possible, since
236     * any certificate presented by the attacker will be considered valid.
237     * </p>
238     */
239    public static class AcceptAllTrustManager implements X509TrustManager {
240
241        @Override
242        public void checkClientTrusted(X509Certificate[] arg0, String arg1)
243                        throws CertificateException {
244            // Nothing to do here
245        }
246
247        @Override
248        public void checkServerTrusted(X509Certificate[] arg0, String arg1)
249                        throws CertificateException {
250            // Nothing to do here
251        }
252
253        @Override
254        public X509Certificate[] getAcceptedIssuers() {
255            return new X509Certificate[0];
256        }
257    }
258
259    private static final File DEFAULT_TRUSTSTORE_PATH;
260
261    static {
262        String javaHome = System.getProperty("java.home");
263        String defaultTruststorePath = javaHome + File.separator + "lib" + File.separator + "security" + File.separator + "cacerts";
264        DEFAULT_TRUSTSTORE_PATH = new File(defaultTruststorePath);
265    }
266
267    public static FileInputStream getDefaultTruststoreStreamIfPossible() {
268        try {
269            return new FileInputStream(DEFAULT_TRUSTSTORE_PATH);
270        } catch (FileNotFoundException e) {
271            LOGGER.log(Level.WARNING, "Could not open default truststore at " + DEFAULT_TRUSTSTORE_PATH, e);
272            return null;
273        }
274    }
275
276    enum DefaultTrustStoreType {
277        jks,
278        unknown,
279        no_default,
280    }
281
282    private static final int JKS_MAGIC = 0xfeedfeed;
283    private static final int JKS_VERSION_1 = 1;
284    private static final int JKS_VERSION_2 = 2;
285
286    public static DefaultTrustStoreType getDefaultTruststoreType() throws IOException {
287        try (InputStream inputStream = getDefaultTruststoreStreamIfPossible()) {
288            if (inputStream == null) {
289                return DefaultTrustStoreType.no_default;
290            }
291
292            DataInputStream dis = new DataInputStream(inputStream);
293            int magic = dis.readInt();
294            int version = dis.readInt();
295
296            if (magic == JKS_MAGIC && (version == JKS_VERSION_1 || version == JKS_VERSION_2)) {
297                return DefaultTrustStoreType.jks;
298            }
299        }
300
301        return DefaultTrustStoreType.unknown;
302    }
303
304    /**
305     * Tries to determine if the default truststore type is of type jks and sets the javax.net.ssl.trustStoreType system
306     * property to 'JKS' if so. This is meant as workaround in situations where the default truststore type is (still)
307     * 'jks' but we run on a newer JRE/JDK which uses PKCS#12 as type. See for example <a href="https://bugs.gentoo.org/712290">Gentoo bug #712290</a>.
308     */
309    public static void setDefaultTrustStoreTypeToJksIfRequired() {
310        DefaultTrustStoreType defaultTrustStoreType;
311        try {
312            defaultTrustStoreType = getDefaultTruststoreType();
313        } catch (IOException e) {
314            LOGGER.log(Level.WARNING, "Could not set keystore type to jks if required", e);
315            return;
316        }
317
318        if (defaultTrustStoreType == DefaultTrustStoreType.jks) {
319            System.setProperty("javax.net.ssl.trustStoreType", "JKS");
320        }
321    }
322}