001/**
002 *
003 * Copyright 2014-2024 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     * Accept all TLS certificates.
073     * <p>
074     * <b>Warning:</b> Use with care. This method make the Connection use {@link AcceptAllTrustManager} and essentially
075     * <b>invalidates all security guarantees provided by TLS</b>. Only use this method if you understand the
076     * implications.
077     * </p>
078     *
079     * @param builder a connection configuration builder.
080     * @param <B> Type of the ConnectionConfiguration builder.
081     * @return the given builder.
082     */
083    public static <B extends ConnectionConfiguration.Builder<B, ?>> B acceptAllCertificates(B builder) {
084        builder.setCustomX509TrustManager(new AcceptAllTrustManager());
085        return builder;
086    }
087
088    /**
089     * Disable the hostname verification of TLS certificates.
090     * <p>
091     * <b>Warning:</b> Use with care. This disables hostname verification of TLS certificates and essentially
092     * <b>invalidates all security guarantees provided by TLS</b>. Only use this method if you understand the
093     * implications.
094     * </p>
095     *
096     * @param builder a connection configuration builder.
097     * @param <B> Type of the ConnectionConfiguration builder.
098     * @return the given builder.
099     */
100    public static <B extends ConnectionConfiguration.Builder<B, ?>> B disableHostnameVerificationForTlsCertificates(B builder) {
101        builder.setHostnameVerifier((hostname, session) -> {
102            return true;
103        });
104        return builder;
105    }
106
107    public static void setEnabledProtocolsAndCiphers(final SSLSocket sslSocket,
108                    String[] enabledProtocols, String[] enabledCiphers)
109                    throws SecurityNotPossibleException {
110        if (enabledProtocols != null) {
111            Set<String> enabledProtocolsSet = new HashSet<String>(Arrays.asList(enabledProtocols));
112            Set<String> supportedProtocolsSet = new HashSet<String>(
113                            Arrays.asList(sslSocket.getSupportedProtocols()));
114            Set<String> protocolsIntersection = new HashSet<String>(supportedProtocolsSet);
115            protocolsIntersection.retainAll(enabledProtocolsSet);
116            if (protocolsIntersection.isEmpty()) {
117                throw new SecurityNotPossibleException("Request to enable SSL/TLS protocols '"
118                                + StringUtils.collectionToString(enabledProtocolsSet)
119                                + "', but only '"
120                                + StringUtils.collectionToString(supportedProtocolsSet)
121                                + "' are supported.");
122            }
123
124            // Set the enabled protocols
125            enabledProtocols = new String[protocolsIntersection.size()];
126            enabledProtocols = protocolsIntersection.toArray(enabledProtocols);
127            sslSocket.setEnabledProtocols(enabledProtocols);
128        }
129
130        if (enabledCiphers != null) {
131            Set<String> enabledCiphersSet = new HashSet<String>(Arrays.asList(enabledCiphers));
132            Set<String> supportedCiphersSet = new HashSet<String>(
133                            Arrays.asList(sslSocket.getEnabledCipherSuites()));
134            Set<String> ciphersIntersection = new HashSet<String>(supportedCiphersSet);
135            ciphersIntersection.retainAll(enabledCiphersSet);
136            if (ciphersIntersection.isEmpty()) {
137                throw new SecurityNotPossibleException("Request to enable SSL/TLS ciphers '"
138                                + StringUtils.collectionToString(enabledCiphersSet)
139                                + "', but only '"
140                                + StringUtils.collectionToString(supportedCiphersSet)
141                                + "' are supported.");
142            }
143
144            enabledCiphers = new String[ciphersIntersection.size()];
145            enabledCiphers = ciphersIntersection.toArray(enabledCiphers);
146            sslSocket.setEnabledCipherSuites(enabledCiphers);
147        }
148    }
149
150    /**
151     * Get the channel binding data for the 'tls-server-end-point' channel binding type. This channel binding type is
152     * defined in RFC 5929 § 4.
153     *
154     * @param sslSession the SSL/TLS session from which the data should be retrieved.
155     * @return the channel binding data.
156     * @throws SSLPeerUnverifiedException if we TLS peer could not be verified.
157     * @throws CertificateEncodingException if there was an encoding error with the certificate.
158     * @throws NoSuchAlgorithmException if no such algorithm is available.
159     * @see <a href="https://tools.ietf.org/html/rfc5929#section-4">RFC 5929 § 4.</a>
160     */
161    public static byte[] getChannelBindingTlsServerEndPoint(final SSLSession sslSession)
162                    throws SSLPeerUnverifiedException, CertificateEncodingException, NoSuchAlgorithmException {
163        final Certificate[] peerCertificates = sslSession.getPeerCertificates();
164        final Certificate certificate = peerCertificates[0];
165        final String certificateAlgorithm = certificate.getPublicKey().getAlgorithm();
166
167        // RFC 5929 § 4.1 hash function selection.
168        String algorithm;
169        switch (certificateAlgorithm) {
170        case "MD5":
171        case "SHA-1":
172            algorithm = "SHA-256";
173            break;
174        default:
175            algorithm = certificateAlgorithm;
176            break;
177        }
178
179        final MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
180        final byte[] certificateDerEncoded = certificate.getEncoded();
181        messageDigest.update(certificateDerEncoded);
182        return messageDigest.digest();
183    }
184
185    /**
186     * A {@link X509TrustManager} that <b>doesn't validate</b> X.509 certificates.
187     * <p>
188     * Connections that use this TrustManager will just be encrypted, without any guarantee that the
189     * counter part is actually the intended one. Man-in-the-Middle attacks will be possible, since
190     * any certificate presented by the attacker will be considered valid.
191     * </p>
192     */
193    public static class AcceptAllTrustManager implements X509TrustManager {
194
195        @Override
196        public void checkClientTrusted(X509Certificate[] arg0, String arg1)
197                        throws CertificateException {
198            // Nothing to do here
199        }
200
201        @Override
202        public void checkServerTrusted(X509Certificate[] arg0, String arg1)
203                        throws CertificateException {
204            // Nothing to do here
205        }
206
207        @Override
208        public X509Certificate[] getAcceptedIssuers() {
209            return new X509Certificate[0];
210        }
211    }
212
213    private static final File DEFAULT_TRUSTSTORE_PATH;
214
215    static {
216        String javaHome = System.getProperty("java.home");
217        String defaultTruststorePath = javaHome + File.separator + "lib" + File.separator + "security" + File.separator + "cacerts";
218        DEFAULT_TRUSTSTORE_PATH = new File(defaultTruststorePath);
219    }
220
221    public static FileInputStream getDefaultTruststoreStreamIfPossible() {
222        try {
223            return new FileInputStream(DEFAULT_TRUSTSTORE_PATH);
224        } catch (FileNotFoundException e) {
225            LOGGER.log(Level.WARNING, "Could not open default truststore at " + DEFAULT_TRUSTSTORE_PATH, e);
226            return null;
227        }
228    }
229
230    enum DefaultTrustStoreType {
231        jks,
232        unknown,
233        no_default,
234    }
235
236    private static final int JKS_MAGIC = 0xfeedfeed;
237    private static final int JKS_VERSION_1 = 1;
238    private static final int JKS_VERSION_2 = 2;
239
240    public static DefaultTrustStoreType getDefaultTruststoreType() throws IOException {
241        try (InputStream inputStream = getDefaultTruststoreStreamIfPossible()) {
242            if (inputStream == null) {
243                return DefaultTrustStoreType.no_default;
244            }
245
246            DataInputStream dis = new DataInputStream(inputStream);
247            int magic = dis.readInt();
248            int version = dis.readInt();
249
250            if (magic == JKS_MAGIC && (version == JKS_VERSION_1 || version == JKS_VERSION_2)) {
251                return DefaultTrustStoreType.jks;
252            }
253        }
254
255        return DefaultTrustStoreType.unknown;
256    }
257
258    /**
259     * Tries to determine if the default truststore type is of type jks and sets the javax.net.ssl.trustStoreType system
260     * property to 'JKS' if so. This is meant as workaround in situations where the default truststore type is (still)
261     * '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>.
262     */
263    public static void setDefaultTrustStoreTypeToJksIfRequired() {
264        DefaultTrustStoreType defaultTrustStoreType;
265        try {
266            defaultTrustStoreType = getDefaultTruststoreType();
267        } catch (IOException e) {
268            LOGGER.log(Level.WARNING, "Could not set keystore type to jks if required", e);
269            return;
270        }
271
272        if (defaultTrustStoreType == DefaultTrustStoreType.jks) {
273            System.setProperty("javax.net.ssl.trustStoreType", "JKS");
274        }
275    }
276}