From e7bc6352074574b7437db7ecb08259dfbd93fe1e Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Fri, 13 Mar 2026 21:13:34 -0300 Subject: [PATCH] The internal SSL utility classes (`SslContext`, `JdkSslContext`, `PemReader`) currently rely on legacy defaults and need to be modernized to fully leverage Java 21 standards. These classes are essential for zero-dependency PEM parsing and SSL configuration across supported servers, but they currently default to the outdated `JKS` keystore format, prioritize older TLS versions, and utilize legacy cipher suites. * **Prioritize TLS 1.3:** Update the protocol fallback logic in `JdkSslContext` to prioritize `TLSv1.3` and drop deprecated protocols. * **Modernize Ciphers:** Remove outdated ciphers (e.g., 3DES) and ensure high-security TLS 1.3 ciphers are at the top of the preference list. * **Update Keystore Format:** Replace hardcoded `"JKS"` instances with `KeyStore.getDefaultType()` to utilize the modern PKCS12 standard. * **Optimize PEM Parsing:** Refactor `PemReader` to use `Base64.getMimeDecoder()` for native, efficient handling of line breaks, replacing the manual regex string manipulation. * **Syntax Cleanup:** Apply the diamond operator (`<>`) across all files to clean up legacy generics. fix #3877 --- .../io/jooby/internal/x509/JdkSslContext.java | 62 +++++------------ .../io/jooby/internal/x509/PemReader.java | 11 ++-- .../io/jooby/internal/x509/SslContext.java | 66 ++----------------- 3 files changed, 26 insertions(+), 113 deletions(-) diff --git a/jooby/src/main/java/io/jooby/internal/x509/JdkSslContext.java b/jooby/src/main/java/io/jooby/internal/x509/JdkSslContext.java index 3b30370968..0000224ccd 100644 --- a/jooby/src/main/java/io/jooby/internal/x509/JdkSslContext.java +++ b/jooby/src/main/java/io/jooby/internal/x509/JdkSslContext.java @@ -51,7 +51,6 @@ public abstract class JdkSslContext extends SslContext { static { SSLContext context; - int i; try { context = SSLContext.getInstance(PROTOCOL); context.init(null, null, null); @@ -63,12 +62,11 @@ public abstract class JdkSslContext extends SslContext { // Choose the sensible default list of protocols. final String[] supportedProtocols = engine.getSupportedProtocols(); - Set supportedProtocolsSet = new HashSet(supportedProtocols.length); - for (i = 0; i < supportedProtocols.length; ++i) { - supportedProtocolsSet.add(supportedProtocols[i]); - } - List protocols = new ArrayList(); - addIfSupported(supportedProtocolsSet, protocols, "TLSv1.2", "TLSv1.1", "TLSv1"); + Set supportedProtocolsSet = new HashSet<>(Arrays.asList(supportedProtocols)); + List protocols = new ArrayList<>(); + + // Modernized for Java 21: prioritize TLS 1.3 and TLS 1.2 + addIfSupported(supportedProtocolsSet, protocols, "TLSv1.3", "TLSv1.2"); if (!protocols.isEmpty()) { PROTOCOLS = protocols.toArray(new String[0]); @@ -78,26 +76,26 @@ public abstract class JdkSslContext extends SslContext { // Choose the sensible default list of cipher suites. final String[] supportedCiphers = engine.getSupportedCipherSuites(); - SUPPORTED_CIPHERS = new HashSet(supportedCiphers.length); - for (i = 0; i < supportedCiphers.length; ++i) { - SUPPORTED_CIPHERS.add(supportedCiphers[i]); - } - List ciphers = new ArrayList(); + SUPPORTED_CIPHERS = new HashSet<>(Arrays.asList(supportedCiphers)); + List ciphers = new ArrayList<>(); + addIfSupported( SUPPORTED_CIPHERS, ciphers, - // XXX: Make sure to sync this list with OpenSslEngineFactory. - // GCM (Galois/Counter Mode) requires JDK 8. + // TLS 1.3 Ciphers + "TLS_AES_256_GCM_SHA384", + "TLS_AES_128_GCM_SHA256", + "TLS_CHACHA20_POLY1305_SHA256", + // Modern TLS 1.2 Ciphers + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", - // AES256 requires JCE unlimited strength jurisdiction policy files. "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", - // GCM (Galois/Counter Mode) requires JDK 8. "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA", - // AES256 requires JCE unlimited strength jurisdiction policy files. - "TLS_RSA_WITH_AES_256_CBC_SHA", - "SSL_RSA_WITH_3DES_EDE_CBC_SHA"); + "TLS_RSA_WITH_AES_256_CBC_SHA"); if (ciphers.isEmpty()) { // Use the default from JDK as fallback. @@ -125,7 +123,6 @@ private static void addIfSupported( } } - /** Returns the JDK {@link SSLSessionContext} object held by this context. */ @Override public final SSLSessionContext sessionContext() { return context().getServerSessionContext(); @@ -141,18 +138,6 @@ public final long sessionTimeout() { return sessionContext().getSessionTimeout(); } - /** - * Build a {@link KeyManagerFactory} based upon a key file, key file password, and a certificate - * chain. - * - * @param certChainFile a X.509 certificate chain file in PEM format - * @param keyFile a PKCS#8 private key file in PEM format - * @param keyPassword the password of the {@code keyFile}. {@code null} if it's not - * password-protected. - * @param kmf The existing {@link KeyManagerFactory} that will be used if not {@code null} - * @return A {@link KeyManagerFactory} based upon a key file, key file password, and a certificate - * chain. - */ protected static KeyManagerFactory buildKeyManagerFactory( final InputStream certChainFile, final InputStream keyFile, final String keyPassword) throws UnrecoverableKeyException, @@ -171,19 +156,6 @@ protected static KeyManagerFactory buildKeyManagerFactory( return buildKeyManagerFactory(certChainFile, algorithm, keyFile, keyPassword); } - /** - * Build a {@link KeyManagerFactory} based upon a key algorithm, key file, key file password, and - * a certificate chain. - * - * @param certChainFile a X.509 certificate chain file in PEM format - * @param keyAlgorithm the standard name of the requested algorithm. See the Java Secure Socket - * Extension Reference Guide for information about standard algorithm names. - * @param keyFile a PKCS#8 private key file in PEM format - * @param keyPassword the password of the {@code keyFile}. {@code null} if it's not - * password-protected. - * @return A {@link KeyManagerFactory} based upon a key algorithm, key file, key file password, - * and a certificate chain. - */ protected static KeyManagerFactory buildKeyManagerFactory( final InputStream certChainFile, final String keyAlgorithm, diff --git a/jooby/src/main/java/io/jooby/internal/x509/PemReader.java b/jooby/src/main/java/io/jooby/internal/x509/PemReader.java index c96d36a88a..b37b3b1598 100644 --- a/jooby/src/main/java/io/jooby/internal/x509/PemReader.java +++ b/jooby/src/main/java/io/jooby/internal/x509/PemReader.java @@ -10,7 +10,6 @@ import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.KeyException; -import java.security.KeyStore; import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Base64; @@ -21,8 +20,7 @@ import io.jooby.internal.IOUtils; /** - * Reads a PEM file and converts it into a list of DERs so that they are imported into a {@link - * KeyStore} easily. + * Reads a PEM file and converts it into a list of DERs. * *

Borrowed from Netty */ @@ -36,6 +34,7 @@ final class PemReader { + // Base64 text "-+END\\s+.*CERTIFICATE[^-]*-+", // Footer Pattern.CASE_INSENSITIVE); + private static final Pattern KEY_PATTERN = Pattern.compile( "-+BEGIN\\s+.*PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" @@ -49,13 +48,12 @@ static List readCertificates(final InputStream file) throws CertificateException, IOException { String content = IOUtils.toString(file, StandardCharsets.UTF_8); - List certs = new ArrayList(); + List certs = new ArrayList<>(); Matcher m = CERT_PATTERN.matcher(content); int start = 0; while (m.find(start)) { ByteBuffer buffer = ByteBuffer.wrap(decode(m.group(1))); certs.add(buffer); - start = m.end(); } @@ -67,7 +65,8 @@ static List readCertificates(final InputStream file) } private static byte[] decode(String value) { - return Base64.getDecoder().decode(value.replaceAll("(?:\\r\\n|\\n\\r|\\n|\\r)", "")); + // MimeDecoder automatically strips out \r and \n characters + return Base64.getMimeDecoder().decode(value); } static ByteBuffer readPrivateKey(final InputStream file) throws KeyException, IOException { diff --git a/jooby/src/main/java/io/jooby/internal/x509/SslContext.java b/jooby/src/main/java/io/jooby/internal/x509/SslContext.java index 0e81fae1df..7dd2b79c39 100644 --- a/jooby/src/main/java/io/jooby/internal/x509/SslContext.java +++ b/jooby/src/main/java/io/jooby/internal/x509/SslContext.java @@ -6,7 +6,6 @@ package io.jooby.internal.x509; import java.io.ByteArrayInputStream; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; @@ -45,27 +44,7 @@ * SslHandler}. Internally, it is implemented via JDK's {@link SSLContext} or OpenSSL's {@code * SSL_CTX}. * - *

Making your server support SSL/TLS

- * - *
- * // In your {@link ChannelInitializer}:
- * {@link ChannelPipeline} p = channel.pipeline();
- * {@link SslContext} sslCtx = {@link SslContextBuilder#forServer(File, File) SslContextBuilder.forServer(...)}.build();
- * p.addLast("ssl", {@link #newEngine(ByteBufAllocator) sslCtx.newEngine(channel.alloc())});
- * ...
- * 
- * - *

Making your client support SSL/TLS

- * - *
- * // In your {@link ChannelInitializer}:
- * {@link ChannelPipeline} p = channel.pipeline();
- * {@link SslContext} sslCtx = {@link SslContextBuilder#forClient() SslContextBuilder.forClient()}.build();
- * p.addLast("ssl", {@link #newEngine(ByteBufAllocator, String, int) sslCtx.newEngine(channel.alloc(), host, port)});
- * ...
- * 
- * - * Borrowed from Netty + *

Borrowed from Netty */ public abstract class SslContext { static final CertificateFactory X509_CERT_FACTORY; @@ -97,33 +76,14 @@ public static SslContext newServerContextInternal( sessionTimeout); } - /** Returns the size of the cache used for storing SSL session objects. */ public abstract long sessionCacheSize(); public abstract long sessionTimeout(); public abstract SSLContext context(); - /** Returns the {@link SSLSessionContext} object held by this context. */ public abstract SSLSessionContext sessionContext(); - /** - * Generates a key specification for an (encrypted) private key. - * - * @param password characters, if {@code null} or empty an unencrypted key is assumed - * @param key bytes of the DER encoded private key - * @return a key specification - * @throws IOException if parsing {@code key} fails - * @throws NoSuchAlgorithmException if the algorithm used to encrypt {@code key} is unkown - * @throws NoSuchPaddingException if the padding scheme specified in the decryption algorithm is - * unkown - * @throws InvalidKeySpecException if the decryption key based on {@code password} cannot be - * generated - * @throws InvalidKeyException if the decryption key based on {@code password} cannot be used to - * decrypt {@code key} - * @throws InvalidAlgorithmParameterException if decryption algorithm parameters are somehow - * faulty - */ protected static PKCS8EncodedKeySpec generateKeySpec(final char[] password, final byte[] key) throws IOException, NoSuchAlgorithmException, @@ -148,15 +108,6 @@ protected static PKCS8EncodedKeySpec generateKeySpec(final char[] password, fina return encryptedPrivateKeyInfo.getKeySpec(cipher); } - /** - * Generates a new {@link KeyStore}. - * - * @param certChainFile a X.509 certificate chain file in PEM format, - * @param keyFile a PKCS#8 private key file in PEM format, - * @param keyPasswordChars the password of the {@code keyFile}. {@code null} if it's not - * password-protected. - * @return generated {@link KeyStore}. - */ static KeyStore buildKeyStore( final InputStream certChainFile, final InputStream keyFile, final char[] keyPasswordChars) throws KeyStoreException, @@ -189,30 +140,22 @@ static KeyStore buildKeyStore( CertificateFactory cf = CertificateFactory.getInstance("X.509"); List certs = PemReader.readCertificates(certChainFile); - List certChain = new ArrayList(certs.size()); + List certChain = new ArrayList<>(certs.size()); for (ByteBuffer buf : certs) { certChain.add(cf.generateCertificate(new ByteArrayInputStream(buf.array()))); } - KeyStore ks = KeyStore.getInstance("JKS"); + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); ks.load(null, null); ks.setKeyEntry("key", key, keyPasswordChars, certChain.toArray(new Certificate[0])); return ks; } - /** - * Build a {@link TrustManagerFactory} from a certificate chain file. - * - * @param certChainFile The certificate file to build from. - * @param trustManagerFactory The existing {@link TrustManagerFactory} that will be used if not - * {@code null}. - * @return A {@link TrustManagerFactory} which contains the certificates in {@code certChainFile} - */ protected static TrustManagerFactory buildTrustManagerFactory( final InputStream certChainFile, TrustManagerFactory trustManagerFactory) throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException { - KeyStore ks = KeyStore.getInstance("JKS"); + KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); ks.load(null, null); CertificateFactory cf = CertificateFactory.getInstance("X.509"); @@ -225,7 +168,6 @@ protected static TrustManagerFactory buildTrustManagerFactory( ks.setCertificateEntry(principal.getName("RFC2253"), cert); } - // Set up trust manager factory to use our key store. if (trustManagerFactory == null) { trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());