001/*
002 * Copyright © 2025 CUI-OpenSource-Software (info@cuioss.de)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package de.cuioss.http.client.handler;
017
018import de.cuioss.http.client.HttpLogMessages;
019import de.cuioss.tools.collect.CollectionLiterals;
020import de.cuioss.tools.logging.CuiLogger;
021import org.jspecify.annotations.Nullable;
022
023import javax.net.ssl.SSLContext;
024import javax.net.ssl.TrustManager;
025import javax.net.ssl.TrustManagerFactory;
026import java.security.*;
027import java.util.Set;
028
029/**
030 * Provider for secure SSL contexts used in HTTPS communications.
031 * <p>
032 * This class enforces secure TLS versions when establishing connections to JWKS endpoints
033 * and other services. It ensures that only modern, secure TLS protocols are used:
034 * <ul>
035 *   <li>TLS 1.2 - Minimum recommended version</li>
036 *   <li>TLS 1.3 - Preferred when available</li>
037 * </ul>
038 * <p>
039 * The class prevents the use of insecure, deprecated protocols:
040 * <ul>
041 *   <li>TLS 1.0 - Deprecated due to security vulnerabilities</li>
042 *   <li>TLS 1.1 - Deprecated due to security vulnerabilities</li>
043 *   <li>SSL 3.0 - Deprecated due to security vulnerabilities (POODLE attack)</li>
044 * </ul>
045 * <p>
046 * For more details on the security aspects, see the
047 * <a href="https://github.com/cuioss/cui-jwt-validation/tree/main/doc/specification/security.adoc">Security Specification</a>
048 *
049 * @param minimumTlsVersion The minimum TLS version that is considered secure for this instance.
050 * @author Oliver Wolff
051 * @since 1.0
052 */
053public record SecureSSLContextProvider(String minimumTlsVersion) {
054
055    private static final CuiLogger LOGGER = new CuiLogger(SecureSSLContextProvider.class);
056
057    /**
058     * TLS version 1.2 - Secure
059     */
060    public static final String TLS_V1_2 = "TLSv1.2";
061
062    /**
063     * TLS version 1.3 - Secure
064     */
065    public static final String TLS_V1_3 = "TLSv1.3";
066
067    /**
068     * Generic TLS - Secure if implemented correctly by the JVM
069     */
070    public static final String TLS = "TLS";
071
072    /**
073     * Default secure TLS version to use when creating a new context
074     */
075    public static final String DEFAULT_TLS_VERSION = TLS_V1_2;
076
077    /**
078     * TLS version 1.0 - Insecure, deprecated
079     */
080    public static final String TLS_V1_0 = "TLSv1.0";
081
082    /**
083     * TLS version 1.1 - Insecure, deprecated
084     */
085    public static final String TLS_V1_1 = "TLSv1.1";
086
087    /**
088     * SSL version 3 - Insecure, deprecated
089     */
090    public static final String SSL_V3 = "SSLv3";
091
092    /**
093     * Set of allowed (secure) TLS versions
094     */
095    public static final Set<String> ALLOWED_TLS_VERSIONS = CollectionLiterals.immutableSet(TLS_V1_2, TLS_V1_3, TLS);
096
097    /**
098     * Set of forbidden (insecure) TLS versions
099     */
100    public static final Set<String> FORBIDDEN_TLS_VERSIONS = CollectionLiterals.immutableSet(TLS_V1_0, TLS_V1_1, SSL_V3);
101
102    /**
103     * Creates a new SecureSSLContextProvider instance with the default minimum TLS version (TLS 1.2).
104     */
105    public SecureSSLContextProvider() {
106        this(DEFAULT_TLS_VERSION);
107    }
108
109    /**
110     * Creates a new SecureSSLContextProvider instance with the specified minimum TLS version.
111     *
112     * @param minimumTlsVersion the minimum TLS version to consider secure
113     * @throws IllegalArgumentException if the specified version is not in the allowed set
114     */
115    public SecureSSLContextProvider {
116        if (!ALLOWED_TLS_VERSIONS.contains(minimumTlsVersion)) {
117            throw new IllegalArgumentException("Minimum TLS version must be one of the allowed versions: " + ALLOWED_TLS_VERSIONS);
118        }
119    }
120
121    /**
122     * Checks if the given protocol is a secure TLS version according to the minimum version set for this instance.
123     * <p>
124     * For TLS_V1_2 and TLS_V1_3, the comparison is based on the version number.
125     * For TLS (generic), it's considered secure if it's in the allowed versions set.
126     *
127     * @param protocol the protocol to check
128     * @return true if the protocol is a secure TLS version, false otherwise
129     */
130    public boolean isSecureTlsVersion(@Nullable String protocol) {
131        if (protocol == null) {
132            return false;
133        }
134
135        if (!ALLOWED_TLS_VERSIONS.contains(protocol)) {
136            return false;
137        }
138
139        // If the minimum is TLS_V1_3, only TLS_V1_3 and TLS are considered secure
140        if (TLS_V1_3.equals(minimumTlsVersion)) {
141            return TLS_V1_3.equals(protocol) || TLS.equals(protocol);
142        }
143
144        // If the minimum is TLS_V1_2, all allowed versions are secure
145        return true;
146    }
147
148    /**
149     * Creates a secure SSLContext configured with the minimum TLS version set for this instance.
150     * <p>
151     * This method:
152     * <ol>
153     *   <li>Creates an SSLContext instance with the secure protocol version</li>
154     *   <li>Initializes a TrustManagerFactory with the default algorithm</li>
155     *   <li>Configures the TrustManagerFactory to use the default trust store</li>
156     *   <li>Initializes the SSLContext with the trust managers and a secure random source</li>
157     * </ol>
158     * <p>
159     * The resulting SSLContext is configured to trust the certificates in the JVM's default trust store
160     * and does not perform client authentication (no KeyManager is provided).
161     *
162     * @return a configured SSLContext that uses a secure TLS protocol version
163     * @throws NoSuchAlgorithmException if the specified protocol or trust manager algorithm is not available
164     * @throws KeyStoreException        if there's an issue accessing the default trust store
165     * @throws KeyManagementException   if there's an issue initializing the SSLContext
166     */
167    public SSLContext createSecureSSLContext() throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
168        // Create a secure SSL context with the minimum TLS version
169        SSLContext secureContext = SSLContext.getInstance(minimumTlsVersion);
170        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
171        trustManagerFactory.init((KeyStore) null);
172        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
173        secureContext.init(null, trustManagers, new SecureRandom());
174        return secureContext;
175    }
176
177    /**
178     * Validates the provided SSLContext and returns a secure SSLContext.
179     * <p>
180     * This method:
181     * <ol>
182     *   <li>If the provided SSLContext is null, creates a new secure SSLContext</li>
183     *   <li>If the provided SSLContext is not null, checks if its protocol is secure</li>
184     *   <li>If the protocol is secure, returns the provided SSLContext</li>
185     *   <li>If the protocol is not secure, creates a new secure SSLContext</li>
186     *   <li>If an exception occurs during validation or creation, falls back to the provided SSLContext or the default SSLContext</li>
187     * </ol>
188     *
189     * @param sslContext the SSLContext to validate, may be null
190     * @return a secure SSLContext, either the validated input or a newly created one (never null)
191     */
192    public SSLContext getOrCreateSecureSSLContext(@Nullable SSLContext sslContext) {
193        try {
194            if (sslContext != null) {
195                // Validate the provided SSL context
196                String protocol = sslContext.getProtocol();
197                LOGGER.debug("SSL context protocol: %s", protocol);
198
199                // Check if the protocol is secure according to the configured TLS versions
200                if (isSecureTlsVersion(protocol)) {
201                    // The provided context was secure and is being used
202                    LOGGER.debug("Using provided SSL context with protocol: %s", protocol);
203                    return sslContext;
204                }
205
206                // If not secure, create a new secure context
207                LOGGER.warn(HttpLogMessages.WARN.SSL_INSECURE_PROTOCOL.format(protocol));
208                SSLContext secureContext = createSecureSSLContext();
209                LOGGER.debug("Created secure SSL context with minimum TLS version: %s", minimumTlsVersion);
210                return secureContext;
211            } else {
212                // If no context provided, create a new secure one
213                SSLContext secureContext = createSecureSSLContext();
214                LOGGER.debug("No SSL context provided, created secure context with minimum TLS version: %s", minimumTlsVersion);
215                return secureContext;
216            }
217        } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
218            // If a secure context cannot be created, we must fail hard.
219            throw new IllegalStateException("Failed to create a secure SSL context", e);
220        }
221    }
222}