This is an automated email from the ASF dual-hosted git repository.
rmaucher pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/main by this push:
new c5e83991e0 Refactor certificate loading
c5e83991e0 is described below
commit c5e83991e0ac7fdc77710ef22ce59b3ed859c166
Author: remm <[email protected]>
AuthorDate: Thu Oct 15 19:23:51 2026 +0200
Refactor certificate loading
Move the certificate and private key loading out of OpenSSLContext into
a new reusable CertificateLoader utility class, to allow reuse (the
QUIC-based HTTP/3 endpoints will need the same logic). PEM (with an
ENGINE fallback for unparseable keys), PKCS#12 and Java key store
sources are supported; PKCS#12 is parsed with the JDK so a wrong
password is detected via the MAC check. The native objects of a loaded
entry (leaf, key, chain) are owned by the configuration snapshot which
loads them, and released with freeCertEntry() when it is discarded.
The Java X.509 chain view used by JMX and the certificate expiry checks
moves as well, and so does the TLS 1.3 pre-shared key find-session
callback: it is now installed on the SSL_CTX and resolves the per-host
selector per connection, since a single listener serves all hosts.
Also:
- Detect OpenSSL 4.x (OpenSSLStatus.isOpenSSL4()) which provides the
QUIC API, and add the d2i_AutoPrivateKey binding which is used to
convert Java private keys to native ones.
- Complete and align the panama string bundle: add the FIPS provider
keys which OpenSSLLibrary and AprLifecycleListener already reference,
rename opensslconf.unknownCommandType to opensslconf.typeUnknown
which is the key OpenSSLContext uses, and drop the unused panama
engine.noSession key.
---
.../tomcat/util/net/openssl/OpenSSLStatus.java | 11 +
.../util/net/openssl/panama/CertificateLoader.java | 1118 ++++++++++++++++++++
.../net/openssl/panama/LocalStrings.properties | 12 +-
.../util/net/openssl/panama/OpenSSLContext.java | 568 +++-------
.../util/net/openssl/panama/OpenSSLLibrary.java | 6 +-
java/org/apache/tomcat/util/openssl/openssl_h.java | 55 +-
.../util/openssl/openssl_h_Compatibility.java | 11 +
res/openssl/openssl-tomcat.conf | 1 +
8 files changed, 1371 insertions(+), 411 deletions(-)
diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLStatus.java
b/java/org/apache/tomcat/util/net/openssl/OpenSSLStatus.java
index 33a29eabc1..c276d8de9a 100644
--- a/java/org/apache/tomcat/util/net/openssl/OpenSSLStatus.java
+++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLStatus.java
@@ -35,6 +35,8 @@ public class OpenSSLStatus {
OPENSSL,
/** OpenSSL 3.x. */
OPENSSL3,
+ /** OpenSSL 4.x. */
+ OPENSSL4,
/** LibreSSL. */
LIBRESSL,
/** BoringSSL. */
@@ -228,6 +230,15 @@ public class OpenSSLStatus {
return Name.OPENSSL3.equals(name);
}
+ /**
+ * Checks if running with OpenSSL 4.0 or later that provides the QUIC API.
+ *
+ * @return true if running with OpenSSL 4.0+ that exposes a QUIC specific
symbol
+ */
+ public static boolean isOpenSSL4() {
+ return Name.OPENSSL4.equals(name);
+ }
+
/**
* Checks if running with BoringSSL.
*
diff --git
a/java/org/apache/tomcat/util/net/openssl/panama/CertificateLoader.java
b/java/org/apache/tomcat/util/net/openssl/panama/CertificateLoader.java
new file mode 100644
index 0000000000..6348c2d0de
--- /dev/null
+++ b/java/org/apache/tomcat/util/net/openssl/panama/CertificateLoader.java
@@ -0,0 +1,1118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tomcat.util.net.openssl.panama;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemorySegment;
+import java.lang.foreign.ValueLayout;
+import java.nio.charset.StandardCharsets;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Locale;
+import java.util.function.Function;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.util.file.ConfigFileLoader;
+import org.apache.tomcat.util.net.Constants;
+import org.apache.tomcat.util.net.SSLHostConfig;
+import org.apache.tomcat.util.net.SSLHostConfigCertificate;
+import org.apache.tomcat.util.net.openssl.OpenSSLPreSharedKeySelector;
+import org.apache.tomcat.util.openssl.SSL_psk_find_session_cb_func;
+import org.apache.tomcat.util.openssl.openssl_h;
+import org.apache.tomcat.util.openssl.pem_password_cb;
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ * Loads certificates and pre-shared keys from the connector's host
+ * configuration into native OpenSSL objects.
+ * <p>
+ * Individual {@link SSLHostConfigCertificate}s are loaded through
+ * {@link #loadCertEntry(SSLHostConfigCertificate, Arena)} and their Java
+ * X.509 chains are read through
+ * {@link #getCertificateChain(SSLHostConfigCertificate)}. The native objects
+ * of an entry are released with {@link #freeCertEntry(CertificateEntry)} when
+ * the configuration that owns them is discarded.
+ * <p>
+ * Also hosts the TLS 1.3 server-side pre-shared key support: the find-session
+ * callback installed by
+ * {@link #installFindSessionCallback(MemorySegment, Function, Arena)} sits on
+ * a context's SSL_CTX and resolves the per-host selector per connection,
+ * since a single listener serves all hosts. OpenSSL copies the context's
+ * callback to every connection SSL it creates, so a context-level callback is
+ * sufficient. The session id context constant
+ * {@link #DEFAULT_SESSION_ID_CONTEXT} is shared with {@link OpenSSLContext}.
+ */
+public final class CertificateLoader {
+
+ private static final Log log = LogFactory.getLog(CertificateLoader.class);
+ private static final StringManager sm =
StringManager.getManager(CertificateLoader.class);
+
+ /**
+ * The session id context used for TLS 1.3 pre-shared key sessions. The
+ * socket {@link OpenSSLContext} sets the same context on its server
+ * session context: a pre-shared key session created for a connection is
+ * only accepted when it carries the context of the connection's context.
+ */
+ public static final byte[] DEFAULT_SESSION_ID_CONTEXT =
+ new byte[] { 'd', 'e', 'f', 'a', 'u', 'l', 't' };
+
+ private CertificateLoader() {
+ // Utility class, not instantiable
+ }
+
+
+ /**
+ * A single native certificate + private key pair (for one key type),
+ * loaded from one {@link SSLHostConfigCertificate}. The native objects
+ * are allocated in the arena of the configuration snapshot that owns
+ * them and are
+ * referenced (not copied) when applied to a connection by the SNI
+ * callback.
+ */
+ public static final class CertificateEntry {
+
+ /**
+ * The leaf certificate ({@code X509 *}).
+ */
+ public final MemorySegment leaf;
+
+ /**
+ * The private key ({@code EVP_PKEY *}).
+ */
+ public final MemorySegment key;
+
+ /**
+ * Intermediate chain certificates ({@code X509 *}), sent after the
+ * leaf. May be empty.
+ */
+ public final MemorySegment[] chain;
+
+
+ public CertificateEntry(MemorySegment leaf, MemorySegment key,
MemorySegment[] chain) {
+ this.leaf = leaf;
+ this.key = key;
+ this.chain = chain;
+ }
+ }
+
+
+ // ------------------------------------- Certificate loading
+
+ /**
+ * Loads one {@link SSLHostConfigCertificate} (PEM, PKCS#12 or Java
+ * key store) into native objects allocated in the given arena.
+ *
+ * @param certificate the certificate configuration
+ * @param arena arena for the native allocations
+ *
+ * @return the loaded entry, or {@code null} if it could not be loaded
+ * (the failure is logged)
+ */
+ public static CertificateEntry loadCertEntry(SSLHostConfigCertificate
certificate, Arena arena) {
+ return loadCertEntry(certificate, arena, null);
+ }
+
+
+ /**
+ * Loads one {@link SSLHostConfigCertificate} (PEM, PKCS#12 or Java
+ * key store) into native objects allocated in the given arena, with an
+ * OpenSSL ENGINE fallback for PEM private keys that cannot be parsed.
+ *
+ * @param certificate the certificate configuration
+ * @param arena arena for the native allocations
+ * @param engineKeyLoader fallback invoked with the adjusted key file path
+ * when PEM parsing fails, returning a private key
+ * loaded through an OpenSSL ENGINE or
+ * {@code MemorySegment.NULL} when none is
+ * available; may be {@code null}
+ *
+ * @return the loaded entry, or {@code null} if it could not be loaded
+ * (the failure is logged)
+ */
+ public static CertificateEntry loadCertEntry(SSLHostConfigCertificate
certificate, Arena arena,
+ Function<String,MemorySegment> engineKeyLoader) {
+ String hostName = certificate.getSSLHostConfig().getHostName();
+ try {
+ // Pick the right key password
+ String keyPassToUse;
+ try {
+ keyPassToUse = resolveKeyPassword(certificate);
+ } catch (IOException ioe) {
+ String keyPassFile =
certificate.getCertificateKeyPasswordFile();
+ if (keyPassFile == null) {
+ keyPassFile =
certificate.getCertificateKeystorePasswordFile();
+ }
+
log.warn(sm.getString("certificateLoader.certificateLoadFailed", hostName,
keyPassFile), ioe);
+ return null;
+ }
+
+ String certFile = certificate.getCertificateFile();
+ if (certFile != null) {
+ certFile = SSLHostConfig.adjustRelativePath(certFile);
+ if (certFile.toLowerCase(Locale.ENGLISH).endsWith(".pkcs12")) {
+ return loadPkcs12(certificate, hostName, certFile,
keyPassToUse, arena);
+ }
+ return loadPem(certificate, hostName, certFile, keyPassToUse,
arena,
+ engineKeyLoader);
+ }
+ return loadKeyStore(certificate, hostName, keyPassToUse, arena);
+ } catch (Throwable t) {
+ log.warn(sm.getString("certificateLoader.certificateLoadFailed",
hostName,
+ t.getClass().getName() + (t.getMessage() != null ? ": " +
t.getMessage() : "")), t);
+ return null;
+ }
+ }
+
+
+ /**
+ * Resolves the password used to open the private key / key store (or
+ * PKCS#12 bundle) configured for the given certificate entry: the key
+ * password - or the password read from the key password file, one line -
+ * if configured, otherwise the keystore password (or its file). Shared
+ * by the native load path and the Java certificate-chain view exposed
+ * via the certificate's SSLContext (JMX, the Manager application).
+ *
+ * @param certificate The certificate entry to resolve the password for
+ *
+ * @return The password, or {@code null} if none is configured
+ *
+ * @throws IOException If a configured password file could not be read
+ */
+ public static String resolveKeyPassword(SSLHostConfigCertificate
certificate) throws IOException {
+ String keyPass = certificate.getCertificateKeyPassword();
+ if (keyPass == null) {
+ keyPass = certificate.getCertificateKeystorePassword();
+ }
+ String keyPassFile = certificate.getCertificateKeyPasswordFile();
+ if (keyPassFile == null) {
+ keyPassFile = certificate.getCertificateKeystorePasswordFile();
+ }
+ if (keyPassFile != null) {
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(
+
ConfigFileLoader.getSource().getResource(keyPassFile).getInputStream(),
+ StandardCharsets.UTF_8))) {
+ return reader.readLine();
+ }
+ }
+ return keyPass;
+ }
+
+
+ /**
+ * Selects the key entry alias to use in the given key store: the
+ * configured alias if one is set, otherwise the first key entry in the
+ * store. A configured alias is returned unvalidated - callers check it
+ * with {@link KeyStore#isKeyEntry(String)} so an explicit but wrong
+ * alias fails identically in every view of the store. Shared by the
+ * native load path and the Java certificate-chain view.
+ *
+ * @param keyStore The key store to select from
+ * @param configuredAlias The configured key alias, or {@code null} to
+ * use the first key entry
+ *
+ * @return The selected alias, or {@code null} if the store holds no
+ * key entry
+ *
+ * @throws KeyStoreException If the key store cannot be examined
+ */
+ public static String resolveKeyAlias(KeyStore keyStore, String
configuredAlias)
+ throws KeyStoreException {
+ if (configuredAlias != null) {
+ return configuredAlias;
+ }
+ Enumeration<?> aliases = keyStore.aliases();
+ while (aliases.hasMoreElements()) {
+ String candidate = aliases.nextElement().toString();
+ if (keyStore.isKeyEntry(candidate)) {
+ return candidate;
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * Loads a PEM certificate (and private key) with an optional separate
+ * private key file and an optional chain file.
+ *
+ * @param certificate The certificate configuration
+ * @param hostName The host name the certificate belongs to, used for
+ * logging
+ * @param certFile The (adjusted) path to the certificate file
+ * @param keyPassToUse The key passphrase, or {@code null} if none
+ * @param arena Arena for the native allocations
+ *
+ * @return The loaded entry, or {@code null} if it could not be loaded
+ * (the failure is logged)
+ *
+ * @throws IOException If a configured file could not be read
+ */
+ public static CertificateEntry loadPem(SSLHostConfigCertificate
certificate, String hostName,
+ String certFile, String keyPassToUse, Arena arena) throws
IOException {
+ return loadPem(certificate, hostName, certFile, keyPassToUse, arena,
null);
+ }
+
+
+ /**
+ * Loads a PEM certificate (and private key) with an optional separate
+ * private key file, an optional chain file and an OpenSSL ENGINE
+ * fallback for keys that cannot be parsed.
+ *
+ * @param certificate The certificate configuration
+ * @param hostName The host name the certificate belongs to, used
+ * for logging
+ * @param certFile The (adjusted) path to the certificate file
+ * @param keyPassToUse The key passphrase, or {@code null} if none
+ * @param arena Arena for the native allocations
+ * @param engineKeyLoader fallback invoked with the adjusted key file
+ * path when PEM parsing fails, returning a
+ * private key loaded through an OpenSSL ENGINE or
+ * {@code MemorySegment.NULL} when none is
+ * available; may be {@code null}
+ *
+ * @return The loaded entry, or {@code null} if it could not be loaded
+ * (the failure is logged)
+ *
+ * @throws IOException If a configured file could not be read
+ */
+ public static CertificateEntry loadPem(SSLHostConfigCertificate
certificate, String hostName,
+ String certFile, String keyPassToUse, Arena arena,
+ Function<String,MemorySegment> engineKeyLoader) throws IOException
{
+ String keyFile = certificate.getCertificateKeyFile();
+ if (keyFile == null) {
+ keyFile = certFile;
+ } else {
+ keyFile = SSLHostConfig.adjustRelativePath(keyFile);
+ }
+ String chainFile = certificate.getCertificateChainFile();
+ if (chainFile != null) {
+ chainFile = SSLHostConfig.adjustRelativePath(chainFile);
+ }
+
+ byte[] certBytes = readConfigFileBytes(certFile);
+ if (certBytes == null) {
+ log.warn(sm.getString("certificateLoader.certificateLoadFailed",
hostName, certFile));
+ return null;
+ }
+ byte[] keyBytes = readConfigFileBytes(keyFile);
+ if (keyBytes == null) {
+ log.warn(sm.getString("certificateLoader.certificateLoadFailed",
hostName, keyFile));
+ return null;
+ }
+
+ // The certificate file may hold the intermediates after the leaf (the
+ // common combined "fullchain" style); read all of them, not just the
+ // first. A file that holds no PEM certificate at all is retried as a
+ // single DER encoded certificate.
+ MemorySegment[] certFileCerts = readX509ChainFromBytes(certBytes);
+ MemorySegment leaf;
+ MemorySegment[] chain;
+ if (certFileCerts.length > 0) {
+ leaf = certFileCerts[0];
+ chain = Arrays.copyOfRange(certFileCerts, 1, certFileCerts.length);
+ } else {
+ leaf = readX509FromBytes(certBytes);
+ chain = new MemorySegment[0];
+ }
+ if (leaf.equals(MemorySegment.NULL)) {
+ for (MemorySegment extra : chain) {
+ openssl_h.X509_free(extra);
+ }
+ log.warn(sm.getString("certificateLoader.certificateLoadFailed",
hostName,
+ certFile + " (" + OpenSSLLibrary.getLastError() + ")"));
+ return null;
+ }
+ MemorySegment key = readPrivateKeyFromBytes(keyBytes, keyPassToUse,
arena);
+ if (key.equals(MemorySegment.NULL) && engineKeyLoader != null) {
+ // Not a parseable PEM key: try the configured OpenSSL ENGINE.
+ // The loader returns NULL when no ENGINE is available.
+ key = engineKeyLoader.apply(keyFile);
+ }
+ if (key.equals(MemorySegment.NULL)) {
+ for (MemorySegment extra : chain) {
+ openssl_h.X509_free(extra);
+ }
+ openssl_h.X509_free(leaf);
+ log.warn(sm.getString("certificateLoader.certificateLoadFailed",
hostName,
+ keyFile + " (" + OpenSSLLibrary.getLastError() + ")"));
+ return null;
+ }
+
+ if (chainFile != null) {
+ byte[] chainBytes = readConfigFileBytes(chainFile);
+ if (chainBytes == null) {
+
log.warn(sm.getString("certificateLoader.certificateLoadFailed", hostName,
chainFile));
+ } else {
+ // Append the separately configured chain to the intermediates
+ // (if any) read from the certificate file.
+ MemorySegment[] extraChain =
readX509ChainFromBytes(chainBytes);
+ chain = Arrays.copyOf(chain, chain.length + extraChain.length);
+ System.arraycopy(extraChain, 0, chain, chain.length -
extraChain.length,
+ extraChain.length);
+ }
+ }
+
+ return new CertificateEntry(leaf, key, chain);
+ }
+
+
+ /**
+ * Loads a PKCS#12 certificate bundle.
+ *
+ * @param certificate The certificate configuration
+ * @param hostName The host name the certificate belongs to, used for
+ * logging
+ * @param certFile The (adjusted) path to the PKCS#12 file
+ * @param keyPassToUse The bundle password, or {@code null} if none
+ * @param arena Arena for the native allocations
+ *
+ * @return The loaded entry, or {@code null} if it could not be loaded
+ * (the failure is logged)
+ */
+ public static CertificateEntry loadPkcs12(SSLHostConfigCertificate
certificate, String hostName,
+ String certFile, String keyPassToUse, Arena arena) {
+ byte[] fileBytes = readConfigFileBytes(certFile);
+ if (fileBytes == null) {
+ log.warn(sm.getString("certificateLoader.certificateLoadFailed",
hostName, certFile));
+ return null;
+ }
+
+ try {
+ // Parse with the JDK rather than OpenSSL: the MAC check doubles as
+ // the password check, the key entry selection matches the Java
+ // certificate-chain view used by JMX, and the bundle's
+ // intermediate certificates come out as part of the key entry
+ // chain without any native stack handling.
+ KeyStore keyStore = KeyStore.getInstance("PKCS12");
+ keyStore.load(new ByteArrayInputStream(fileBytes),
+ (keyPassToUse != null) ? keyPassToUse.toCharArray() :
null);
+ return loadJavaKeyStoreKey(keyStore,
certificate.getCertificateKeyAlias(), keyPassToUse,
+ certFile, hostName, arena);
+ } catch (IOException e) {
+ // Covers a wrong password: the JDK fails the MAC check
+ log.warn(sm.getString("certificateLoader.certificateLoadFailed",
hostName,
+ certFile + " (" +
sm.getString("certificateLoader.badPassword") + ")"));
+ return null;
+ } catch (Throwable t) {
+ log.warn(sm.getString("certificateLoader.certificateLoadFailed",
hostName,
+ certFile + " (" + t.getClass().getName() +
+ (t.getMessage() != null ? ": " + t.getMessage() : "") +
")"), t);
+ return null;
+ }
+ }
+
+
+ /**
+ * Loads a certificate (and private key) from a Java key store, using the
+ * configured alias or the first key entry if no alias is configured.
+ *
+ * @param certificate The certificate configuration
+ * @param hostName The host name the certificate belongs to, used for
+ * logging
+ * @param keyPassToUse The key passphrase, or {@code null} if none
+ * @param arena Arena for the native allocations
+ *
+ * @return The loaded entry, or {@code null} if it could not be loaded
+ * (the failure is logged)
+ */
+ public static CertificateEntry loadKeyStore(SSLHostConfigCertificate
certificate, String hostName,
+ String keyPassToUse, Arena arena) {
+ KeyStore keyStore;
+ try {
+ keyStore = certificate.getCertificateKeystore();
+ } catch (Throwable e) {
+ log.warn(sm.getString("certificateLoader.certificateLoadFailed",
hostName,
+ certificate.getCertificateKeystoreFile()), e);
+ return null;
+ }
+ if (keyStore == null) {
+ log.warn(sm.getString("certificateLoader.certificateLoadFailed",
hostName,
+ certificate.getCertificateKeystoreFile()));
+ return null;
+ }
+ return loadJavaKeyStoreKey(keyStore,
certificate.getCertificateKeyAlias(), keyPassToUse,
+ certificate.getCertificateKeystoreFile(), hostName, arena);
+ }
+
+
+ /**
+ * Extracts the key entry of the given Java key store (the configured
+ * alias, or the first key entry if no alias is configured) and converts
+ * it to native objects. Shared by the Java key store view and the
+ * PKCS#12 file view.
+ *
+ * @param keyStore The key store to load the entry from
+ * @param configuredAlias The configured key alias, or {@code null} to
+ * use the first key entry
+ * @param keyPassToUse The key passphrase, or {@code null} if none
+ * @param source Key store source, used for logging
+ * @param hostName The host name the entry belongs to, used for
+ * logging
+ * @param arena Arena for the native allocations
+ *
+ * @return The loaded entry, or {@code null} if it could not be loaded
+ * (the failure is logged)
+ */
+ private static CertificateEntry loadJavaKeyStoreKey(KeyStore keyStore,
String configuredAlias,
+ String keyPassToUse, String source, String hostName, Arena arena) {
+ try {
+ String alias = resolveKeyAlias(keyStore, configuredAlias);
+ if (alias == null || !keyStore.isKeyEntry(alias)) {
+
log.warn(sm.getString("certificateLoader.certificateLoadFailed", hostName,
+ source + " (" +
sm.getString("certificateLoader.noKeyEntry") + ")"));
+ return null;
+ }
+
+ char[] keyPass = (keyPassToUse != null) ?
keyPassToUse.toCharArray() : new char[0];
+ java.security.Key keyObj = keyStore.getKey(alias, keyPass);
+ if (!(keyObj instanceof PrivateKey)) {
+
log.warn(sm.getString("certificateLoader.certificateLoadFailed", hostName,
+ source + " (" +
sm.getString("certificateLoader.noKeyEntry") + ")"));
+ return null;
+ }
+ PrivateKey javaKey = (PrivateKey) keyObj;
+ Certificate[] chain = keyStore.getCertificateChain(alias);
+ if (chain == null || chain.length == 0) {
+
log.warn(sm.getString("certificateLoader.certificateLoadFailed", hostName,
+ source + " (" +
sm.getString("certificateLoader.noKeyEntry") + ")"));
+ return null;
+ }
+ X509Certificate[] x509Chain = new X509Certificate[chain.length];
+ for (int i = 0; i < chain.length; i++) {
+ if (!(chain[i] instanceof X509Certificate)) {
+
log.warn(sm.getString("certificateLoader.certificateLoadFailed", hostName,
+ source + " (" +
sm.getString("certificateLoader.noKeyEntry") + ")"));
+ return null;
+ }
+ x509Chain[i] = (X509Certificate) chain[i];
+ }
+
+ CertificateEntry entry = toCertEntry(javaKey, x509Chain, arena);
+ if (entry == null) {
+
log.warn(sm.getString("certificateLoader.certificateLoadFailed", hostName,
+ source + " (" + OpenSSLLibrary.getLastError() + ")"));
+ return null;
+ }
+ return entry;
+ } catch (Throwable e) {
+ log.warn(sm.getString("certificateLoader.certificateLoadFailed",
hostName, source), e);
+ return null;
+ }
+ }
+
+
+ /**
+ * Converts Java key material (as exposed by a {@link KeyStore}) into the
+ * native objects of a {@link CertificateEntry}. Used by the Java key store
+ * load path ({@link #loadJavaKeyStoreKey}).
+ *
+ * @param javaKey The private key (PKCS#8 encodable)
+ * @param chain The certificate chain, leaf first
+ * @param arena Arena for the native allocations
+ *
+ * @return The new entry owning freshly created native objects, or
+ * {@code null} if any object could not be created (already
+ * created objects are released; the failure is not logged)
+ */
+ public static CertificateEntry toCertEntry(PrivateKey javaKey,
X509Certificate[] chain,
+ Arena arena) {
+ if (javaKey == null || chain == null || chain.length == 0) {
+ return null;
+ }
+ MemorySegment leaf = MemorySegment.NULL;
+ MemorySegment key = MemorySegment.NULL;
+ MemorySegment[] chainCerts = new MemorySegment[chain.length - 1];
+ int chainLoaded = 0;
+ CertificateEntry entry = null;
+ try {
+ // Private key (PKCS#8 DER via d2i_AutoPrivateKey)
+ byte[] keyBytes = javaKey.getEncoded();
+ if (keyBytes == null) {
+ // Unsupported key encoding
+ return null;
+ }
+ MemorySegment keyNative =
arena.allocateFrom(ValueLayout.JAVA_BYTE, keyBytes);
+ MemorySegment keyPtr = arena.allocate(ValueLayout.ADDRESS);
+ keyPtr.set(ValueLayout.ADDRESS, 0, keyNative);
+ key = openssl_h.d2i_AutoPrivateKey(
+ MemorySegment.NULL, keyPtr, (long) keyBytes.length);
+ if (key.equals(MemorySegment.NULL)) {
+ return null;
+ }
+
+ // Leaf certificate
+ leaf = derToX509(chain[0].getEncoded(), arena);
+ if (leaf.equals(MemorySegment.NULL)) {
+ return null;
+ }
+
+ // Intermediate chain certificates (skip the leaf)
+ for (int i = 1; i < chain.length; i++) {
+ chainCerts[i - 1] = derToX509(chain[i].getEncoded(), arena);
+ if (chainCerts[i - 1].equals(MemorySegment.NULL)) {
+ return null;
+ }
+ chainLoaded++;
+ }
+
+ entry = new CertificateEntry(leaf, key, chainCerts);
+ return entry;
+ } catch (Throwable t) {
+ entry = null;
+ return null;
+ } finally {
+ if (entry == null) {
+ // Release the partially built native objects (freeCertEntry
+ // ignores a NULL leaf/key and the failed NULL element is
+ // outside the copied range).
+ freeCertEntry(new CertificateEntry(leaf, key,
+ Arrays.copyOfRange(chainCerts, 0, chainLoaded)));
+ }
+ }
+ }
+
+
+ // ------------------------------------- File and native reading helpers
+
+ /**
+ * Reads a configuration file (relative paths are resolved against the
+ * Catalina base directory) into a byte array.
+ *
+ * @param path The path of the file to read
+ *
+ * @return the file content, or {@code null} if the file cannot be read
+ */
+ public static byte[] readConfigFileBytes(String path) {
+ try (InputStream is =
ConfigFileLoader.getSource().getResource(path).getInputStream();
+ ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+ byte[] buffer = new byte[8192];
+ int read;
+ while ((read = is.read(buffer)) != -1) {
+ bos.write(buffer, 0, read);
+ }
+ return bos.toByteArray();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+
+ /**
+ * Creates a memory BIO containing the given bytes.
+ */
+ private static MemorySegment createMemoryBio(byte[] bytes) {
+ MemorySegment bio = openssl_h.BIO_new(openssl_h.BIO_s_mem());
+ if (bio.equals(MemorySegment.NULL)) {
+ return MemorySegment.NULL;
+ }
+ // BIO_write copies the data, so a short-lived arena is sufficient
+ try (Arena local = Arena.ofConfined()) {
+ MemorySegment data = local.allocateFrom(ValueLayout.JAVA_BYTE,
bytes);
+ if (openssl_h.BIO_write(bio, data, bytes.length) <= 0) {
+ openssl_h.BIO_free(bio);
+ return MemorySegment.NULL;
+ }
+ }
+ return bio;
+ }
+
+
+ /**
+ * Reads the first X.509 certificate from a byte array (PEM with DER
+ * fallback, matching the behaviour of the OpenSSL SSL endpoint).
+ */
+ private static MemorySegment readX509FromBytes(byte[] bytes) {
+ MemorySegment bio = createMemoryBio(bytes);
+ if (bio.equals(MemorySegment.NULL)) {
+ return MemorySegment.NULL;
+ }
+ try {
+ MemorySegment cert = openssl_h.PEM_read_bio_X509_AUX(bio,
MemorySegment.NULL,
+ MemorySegment.NULL, MemorySegment.NULL);
+ if (cert.equals(MemorySegment.NULL) &&
+ // EOF is accepted, then try again
+ ((openssl_h.ERR_peek_last_error() &
openssl_h.ERR_REASON_MASK())
+ == openssl_h.PEM_R_NO_START_LINE())) {
+ openssl_h.ERR_clear_error();
+ MemorySegment bio2 = createMemoryBio(bytes);
+ if (!bio2.equals(MemorySegment.NULL)) {
+ try {
+ cert = openssl_h.d2i_X509_bio(bio2,
MemorySegment.NULL);
+ } finally {
+ openssl_h.BIO_free(bio2);
+ }
+ }
+ }
+ return cert;
+ } finally {
+ openssl_h.BIO_free(bio);
+ }
+ }
+
+
+ /**
+ * Reads all X.509 certificates from a PEM byte array.
+ */
+ private static MemorySegment[] readX509ChainFromBytes(byte[] bytes) {
+ MemorySegment bio = createMemoryBio(bytes);
+ if (bio.equals(MemorySegment.NULL)) {
+ return new MemorySegment[0];
+ }
+ List<MemorySegment> certs = new ArrayList<>();
+ try {
+ MemorySegment cert = openssl_h.PEM_read_bio_X509_AUX(bio,
MemorySegment.NULL,
+ MemorySegment.NULL, MemorySegment.NULL);
+ while (!cert.equals(MemorySegment.NULL)) {
+ certs.add(cert);
+ cert = openssl_h.PEM_read_bio_X509_AUX(bio, MemorySegment.NULL,
+ MemorySegment.NULL, MemorySegment.NULL);
+ }
+ // EOF is accepted, otherwise log an error
+ if ((openssl_h.ERR_peek_last_error() & openssl_h.ERR_REASON_MASK())
+ != openssl_h.PEM_R_NO_START_LINE()) {
+ log.warn(sm.getString("certificateLoader.chainReadError",
+ OpenSSLLibrary.getLastError()));
+ } else {
+ openssl_h.ERR_clear_error();
+ }
+ } finally {
+ openssl_h.BIO_free(bio);
+ }
+ return certs.toArray(new MemorySegment[0]);
+ }
+
+
+ /**
+ * Reads a PEM encoded private key. The configured key passphrase is
+ * supplied to OpenSSL through the callback, so the read is deterministic
+ * and a single attempt is made.
+ *
+ * @return the parsed key, or {@code MemorySegment.NULL} if the key could
+ * not be read (for example, because the passphrase is wrong)
+ */
+ private static MemorySegment readPrivateKeyFromBytes(byte[] bytes, String
keyPassToUse,
+ Arena arena) {
+ MemorySegment bio = createMemoryBio(bytes);
+ if (bio.equals(MemorySegment.NULL)) {
+ return MemorySegment.NULL;
+ }
+ try {
+ return openssl_h.PEM_read_bio_PrivateKey(bio, MemorySegment.NULL,
+ pem_password_cb.allocate(
+ (buf, size, rwflag, user) -> {
+ if (keyPassToUse == null ||
keyPassToUse.isEmpty()) {
+ return 0;
+ }
+ try (Arena localArena = Arena.ofConfined()) {
+ MemorySegment passwordNative =
+
localArena.allocateFrom(keyPassToUse);
+ if (passwordNative.byteSize() > size) {
+ // The password is too long
+ return 0;
+ }
+ MemorySegment bufSegment =
buf.reinterpret(size,
+ localArena, null);
+ bufSegment.copyFrom(passwordNative);
+ // OpenSSL's contract (PEM_def_callback)
is to
+ // return the password length excluding
the NUL
+ // terminator: PEM_do_header() feeds the
returned
+ // length straight into EVP_BytesToKey, so
+ // reporting the terminator as a password
byte
+ // derives a different (wrong) key. The NUL
+ // itself is copied and can stay in buf.
Note:
+ // the upstream-derived PasswordCallback in
+ // OpenSSLContext still reports the
terminator
+ // as a password byte (byteSize(),
including the
+ // NUL) - a known upstream inconsistency
with
+ // this, correct, contract; not replicated
here.
+ return (int) passwordNative.byteSize() - 1;
+ }
+ }, arena),
+ MemorySegment.NULL);
+ } finally {
+ openssl_h.BIO_free(bio);
+ }
+ }
+
+
+ /**
+ * Decodes a DER encoded X.509 certificate.
+ */
+ private static MemorySegment derToX509(byte[] der, Arena arena) {
+ MemorySegment derNative = arena.allocateFrom(ValueLayout.JAVA_BYTE,
der);
+ MemorySegment inPtr = arena.allocate(ValueLayout.ADDRESS);
+ inPtr.set(ValueLayout.ADDRESS, 0, derNative);
+ return openssl_h.d2i_X509(MemorySegment.NULL, inPtr, der.length);
+ }
+
+
+ // ------------------------------------- Release
+
+ /**
+ * Releases the native {@code X509} and {@code EVP_PKEY} objects owned by
+ * an entry. They live on the OpenSSL heap, so closing the arena that held
+ * the entry's allocations does not free them; each entry is released
+ * exactly once when the configuration snapshot that owns it is discarded.
+ *
+ * @param entry The entry whose native objects to release (may be null)
+ */
+ public static void freeCertEntry(CertificateEntry entry) {
+ if (entry == null) {
+ return;
+ }
+ for (MemorySegment chainCert : entry.chain) {
+ if (chainCert != null && !chainCert.equals(MemorySegment.NULL)) {
+ openssl_h.X509_free(chainCert);
+ }
+ }
+ if (entry.leaf != null && !entry.leaf.equals(MemorySegment.NULL)) {
+ openssl_h.X509_free(entry.leaf);
+ }
+ if (entry.key != null && !entry.key.equals(MemorySegment.NULL)) {
+ openssl_h.EVP_PKEY_free(entry.key);
+ }
+ }
+
+
+ /**
+ * Closes the arena, ignoring any failure (the native objects may already
+ * be freed or the arena may already be closed).
+ *
+ * @param arena The arena to close (may be null)
+ */
+ public static void closeQuietly(Arena arena) {
+ if (arena != null) {
+ try {
+ arena.close();
+ } catch (Throwable t) {
+ // Ignore
+ }
+ }
+ }
+
+
+ // ------------------------------------- X.509 chain reading (JMX / expiry)
+
+ /**
+ * Reads the X.509 certificate chain for the given certificate entry from
+ * the same source used by
+ * {@link #loadCertEntry(SSLHostConfigCertificate, Arena)}, but as Java
+ * {@link X509Certificate}s rather than native objects. Used to populate
+ * the certificate data exposed via JMX and read by the certificate expiry
+ * checks, without a native OpenSSL context.
+ *
+ * @param certificate The certificate entry to read the chain from
+ *
+ * @return The certificate chain, or {@code null} if it could not be read
+ */
+ public static X509Certificate[] getCertificateChain(
+ SSLHostConfigCertificate certificate) {
+ try {
+ String certFile = certificate.getCertificateFile();
+ if (certFile != null) {
+ certFile = SSLHostConfig.adjustRelativePath(certFile);
+ if (certFile.toLowerCase(Locale.ENGLISH).endsWith(".pkcs12")) {
+ return getCertificateChainPkcs12(certificate, certFile);
+ }
+ return getCertificateChainPem(certificate, certFile);
+ }
+ return getCertificateChainKeyStore(certificate);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+
+ /**
+ * Returns the certificate chain from a PEM certificate file. The
+ * certificate file may hold the intermediate certificates after the
+ * leaf; a separately configured chain file is appended to the chain.
+ *
+ * @param certificate The certificate entry the file belongs to
+ * @param certFile The (adjusted) path to the certificate file
+ *
+ * @return The certificate chain, or {@code null} if it could not be read
+ */
+ private static X509Certificate[] getCertificateChainPem(
+ SSLHostConfigCertificate certificate, String certFile)
+ throws Exception {
+ byte[] fileBytes = readConfigFileBytes(certFile);
+ if (fileBytes == null) {
+ return null;
+ }
+
+ List<X509Certificate> chain = new ArrayList<>();
+ CertificateFactory factory = CertificateFactory.getInstance("X.509");
+ addX509Certificates(chain, factory, fileBytes);
+
+ String chainFile = certificate.getCertificateChainFile();
+ if (chainFile != null) {
+ chainFile = SSLHostConfig.adjustRelativePath(chainFile);
+ byte[] chainBytes = readConfigFileBytes(chainFile);
+ if (chainBytes != null) {
+ addX509Certificates(chain, factory, chainBytes);
+ }
+ }
+
+ return chain.isEmpty() ? null : chain.toArray(new X509Certificate[0]);
+ }
+
+
+ /**
+ * Appends every X.509 certificate found in the given (PEM or DER
+ * encoded) file to the given chain.
+ */
+ private static void addX509Certificates(List<X509Certificate> chain,
+ CertificateFactory factory, byte[] fileBytes) throws Exception {
+ for (Certificate cert : factory.generateCertificates(
+ new ByteArrayInputStream(fileBytes))) {
+ if (cert instanceof X509Certificate) {
+ chain.add((X509Certificate) cert);
+ }
+ }
+ }
+
+
+ /**
+ * Returns the certificate chain from a PKCS#12 file, using the key entry
+ * matching the configured alias or, if no alias is configured, the first
+ * key entry in the bundle.
+ *
+ * @param certificate The certificate entry the file belongs to
+ * @param certFile The (adjusted) path to the PKCS#12 file
+ *
+ * @return The certificate chain, or {@code null} if it could not be read
+ */
+ private static X509Certificate[] getCertificateChainPkcs12(
+ SSLHostConfigCertificate certificate, String certFile)
+ throws Exception {
+ byte[] fileBytes = readConfigFileBytes(certFile);
+ if (fileBytes == null) {
+ return null;
+ }
+ String password = resolveKeyPassword(certificate);
+ KeyStore keyStore = KeyStore.getInstance("PKCS12");
+ keyStore.load(new ByteArrayInputStream(fileBytes),
+ (password != null) ? password.toCharArray() : null);
+ return getCertificateChainKeyStore(keyStore,
+ certificate.getCertificateKeyAlias());
+ }
+
+
+ /**
+ * Returns the certificate chain from the Java key store configured for
+ * the given certificate entry, using the configured alias or the first
+ * key entry if no alias is configured.
+ *
+ * @param certificate The certificate entry to read the chain from
+ *
+ * @return The certificate chain, or {@code null} if it could not be read
+ */
+ private static X509Certificate[] getCertificateChainKeyStore(
+ SSLHostConfigCertificate certificate) throws Exception {
+ KeyStore keyStore = certificate.getCertificateKeystore();
+ if (keyStore == null) {
+ return null;
+ }
+ return getCertificateChainKeyStore(keyStore,
+ certificate.getCertificateKeyAlias());
+ }
+
+
+ /**
+ * Extracts the X.509 certificate chain for the given alias from the
+ * given key store. If no alias is configured, the first key entry in the
+ * key store is used.
+ *
+ * @param keyStore The key store to read the chain from
+ * @param configuredAlias The configured key alias, or {@code null} to
+ * use the first key entry
+ *
+ * @return The certificate chain, or {@code null} if it could not be read
+ */
+ private static X509Certificate[] getCertificateChainKeyStore(
+ KeyStore keyStore, String configuredAlias) throws Exception {
+ String alias = resolveKeyAlias(keyStore, configuredAlias);
+ if (alias == null || !keyStore.isKeyEntry(alias)) {
+ return null;
+ }
+ Certificate[] chain = keyStore.getCertificateChain(alias);
+ if (chain == null || chain.length == 0) {
+ return null;
+ }
+ X509Certificate[] result = new X509Certificate[chain.length];
+ for (int i = 0; i < chain.length; i++) {
+ if (!(chain[i] instanceof X509Certificate)) {
+ return null;
+ }
+ result[i] = (X509Certificate) chain[i];
+ }
+ return result;
+ }
+
+ // ------------------------------------- Pre-shared key find-session
callback
+
+ /**
+ * Installs the TLS 1.3 server-side pre-shared key callback on the given
+ * context.
+ *
+ * @param sslCtx The native SSL_CTX to configure
+ * @param resolver Maps the connection's SSL object to the selector for
+ * the pre-shared keys to use for that connection. A
+ * {@code null} result means no pre-shared keys are
+ * configured for the host the connection is served by,
+ * and the handshake continues without pre-shared key
+ * authentication
+ * @param arena The arena that owns the callback stub
+ */
+ public static void installFindSessionCallback(MemorySegment sslCtx,
+ Function<MemorySegment, OpenSSLPreSharedKeySelector> resolver,
Arena arena) {
+ openssl_h.SSL_CTX_set_psk_find_session_callback(sslCtx,
+ SSL_psk_find_session_cb_func.allocate(new
FindSessionCallback(resolver), arena));
+ }
+
+
+ /**
+ * The {@code psk_find_session} callback. Resolves the selector for the
+ * connection, looks the offered identity up and, when it matches, builds
+ * a TLS 1.3 {@code SSL_SESSION} holding the pre-shared key as the session
+ * master key. Returning {@code 1} with no session tells OpenSSL to ignore
+ * the offered identity and continue with a full handshake, mirroring the
+ * native connector's behaviour.
+ * <p>
+ * The whole callback body is guarded against any throwable: an upcall
+ * that lets one escape terminates the JVM, so a failure is logged and
+ * reported to OpenSSL as a rejected identity instead.
+ */
+ private static final class FindSessionCallback
+ implements SSL_psk_find_session_cb_func.Function {
+
+ private final Function<MemorySegment, OpenSSLPreSharedKeySelector>
resolver;
+
+ FindSessionCallback(
+ Function<MemorySegment, OpenSSLPreSharedKeySelector> resolver)
{
+ this.resolver = resolver;
+ }
+
+ @Override
+ public int apply(MemorySegment ssl, MemorySegment identity, long
identityLength,
+ MemorySegment sessionPointer) {
+ // A throwable escaping an FFM upcall ends the process ("an
+ // unchecked exception thrown by an upcall is unrecoverable"),
+ // so the callback body is fully guarded: nothing - not even an
+ // Error - may cross the native boundary. A failure rejects the
+ // offered identity (return 0, the same outcome as the malformed
+ // key/cipher failure paths below) instead of taking down the
+ // JVM from inside one handshake.
+ try {
+ return findSession(ssl, identity, identityLength,
sessionPointer);
+ } catch (Throwable t) {
+
log.error(sm.getString("certificateLoader.pskFindSessionError"), t);
+ return 0;
+ }
+ }
+
+
+ private int findSession(MemorySegment ssl, MemorySegment identity,
+ long identityLength, MemorySegment sessionPointer) {
+ try (var localArena = Arena.ofConfined()) {
+ MemorySegment sessionPointerSegment =
+
sessionPointer.reinterpret(ValueLayout.ADDRESS.byteSize(), localArena, null);
+ sessionPointerSegment.set(ValueLayout.ADDRESS, 0,
MemorySegment.NULL);
+ if (MemorySegment.NULL.equals(identity) || identityLength < 0
|| identityLength > Integer.MAX_VALUE) {
+ return 0;
+ }
+
+ OpenSSLPreSharedKeySelector selector = resolver.apply(ssl);
+ if (selector == null) {
+ // No pre-shared keys configured for the host this
+ // connection is served by; continue without PSK.
+ return 1;
+ }
+
+ byte[] identityBytes =
+ identity.reinterpret(identityLength, localArena,
null).toArray(ValueLayout.JAVA_BYTE);
+ int[] cipherSuite = new int[1];
+ byte[] key = selector.select(ssl.address(), identityBytes,
cipherSuite);
+ if (key == null) {
+ return 1;
+ }
+ if (key.length == 0 || cipherSuite[0] <= 0 || cipherSuite[0] >
0xFFFF) {
+ return 0;
+ }
+
+ byte[] cipherId = new byte[] { (byte) (cipherSuite[0] >> 8),
(byte) cipherSuite[0] };
+ MemorySegment cipher = openssl_h.SSL_CIPHER_find(ssl,
+ localArena.allocateFrom(ValueLayout.JAVA_BYTE,
cipherId));
+ if (MemorySegment.NULL.equals(cipher)
+ || !Constants.SSL_PROTO_TLSv1_3.equals(
+
openssl_h.SSL_CIPHER_get_version(cipher).getString(0))) {
+ return 0;
+ }
+
+ MemorySegment session = openssl_h.SSL_SESSION_new();
+ if (MemorySegment.NULL.equals(session)) {
+ return 0;
+ }
+ boolean success = false;
+ try {
+ MemorySegment keySegment =
localArena.allocateFrom(ValueLayout.JAVA_BYTE, key);
+ try {
+ // The session id context must be the one the server
+ // context uses; it is shared with OpenSSLContext where
+ // the socket contexts set it.
+ MemorySegment sidCtxSegment =
localArena.allocateFrom(ValueLayout.JAVA_BYTE,
+ DEFAULT_SESSION_ID_CONTEXT);
+ if (openssl_h.SSL_SESSION_set1_master_key(session,
keySegment, key.length) == 0 ||
+ openssl_h.SSL_SESSION_set_cipher(session,
cipher) == 0 ||
+
openssl_h.SSL_SESSION_set_protocol_version(session,
+ openssl_h.TLS1_3_VERSION()) == 0 ||
+ openssl_h.SSL_SESSION_set1_id_context(session,
sidCtxSegment,
+ DEFAULT_SESSION_ID_CONTEXT.length) ==
0) {
+ return 0;
+ }
+ } finally {
+ keySegment.fill((byte) 0);
+ }
+ sessionPointerSegment.set(ValueLayout.ADDRESS, 0, session);
+ success = true;
+ return 1;
+ } finally {
+ if (!success) {
+ openssl_h.SSL_SESSION_free(session);
+ }
+ }
+ }
+ }
+ }
+}
diff --git
a/java/org/apache/tomcat/util/net/openssl/panama/LocalStrings.properties
b/java/org/apache/tomcat/util/net/openssl/panama/LocalStrings.properties
index 31e163cb9c..cec66550e9 100644
--- a/java/org/apache/tomcat/util/net/openssl/panama/LocalStrings.properties
+++ b/java/org/apache/tomcat/util/net/openssl/panama/LocalStrings.properties
@@ -13,6 +13,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+aprListener.usingFIPSProvider=Using OpenSSL with the FIPS provider as the
default provider
+
+certificateLoader.badPassword=Bad PKCS#12 password
+certificateLoader.certificateLoadFailed=Certificate for host [{0}] could not
be loaded: {1}
+certificateLoader.chainReadError=Error reading the certificate chain file: {0}
+certificateLoader.noKeyEntry=No usable key entry in key store
+certificateLoader.pskFindSessionError=Error in the pre-shared key find-session
callback; the offered identity is rejected
+
engine.emptyCipherSuite=Empty cipher suite
engine.engineClosed=Engine is closed
engine.failedCipherList=Some or all of cipher list [{0}] for TLS 1.2- could
not be enabled
@@ -26,7 +34,6 @@ engine.invalidOCSPURL=The OCSP URL is invalid: [{0}]
engine.noRestrictSessionCreation=OpenSslEngine does not permit restricting the
engine to only resuming existing sessions
engine.noSSLContext=No SSL context
engine.noSSLState=No state was found in the SSL Engine for the given SSL
instance [{0}]
-engine.noSession=SSL session ID not available
engine.nullBuffer=Null buffer
engine.nullBufferInArray=Null buffer in array
engine.nullCipherSuite=Null cipher suite
@@ -87,13 +94,14 @@ opensslconf.failedCommand=OpenSSLConf failed command (name
[{0}], value [{1}]) w
opensslconf.finishFailed=OpenSSLConf finish failed with result [{0}]
opensslconf.noCommandName=OpenSSLConf no command name - will be ignored
(command value [{0}])
opensslconf.resultCommand=OpenSSLConf command (name [{0}], value [{1}])
returned [{2}]
-opensslconf.unknownCommandType=SSL_CONF command [{0}] type unknown
+opensslconf.typeUnknown=SSL_CONF command [{0}] type unknown
opensslconf.unsupported=The OpenSSL implementation does not support OpenSSLConf
openssllibrary.ciphersFailure=Failed getting cipher list
openssllibrary.currentFIPSMode=Current FIPS mode: [{0}]
openssllibrary.engineError=Error creating engine
openssllibrary.enterAlreadyInFIPSMode=AprLifecycleListener is configured to
force entering FIPS mode, but library is already in FIPS mode [{0}]
+openssllibrary.FIPSProviderNotDefault=The FIPS provider must be configured as
the default provider when FIPS mode [{0}] is configured
openssllibrary.errorSettingSSLRandomSeed=Setting random seed [{0}] caused
error [{1}]
openssllibrary.initializeFIPSFailed=Failed to enter FIPS mode
openssllibrary.initializeFIPSSuccess=Successfully entered FIPS mode
diff --git a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java
b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java
index 97fb815128..2e7e75d459 100644
--- a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java
+++ b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLContext.java
@@ -16,10 +16,7 @@
*/
package org.apache.tomcat.util.net.openssl.panama;
-import java.io.BufferedReader;
import java.io.File;
-import java.io.IOException;
-import java.io.InputStreamReader;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
@@ -33,7 +30,6 @@ import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Base64;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -56,8 +52,6 @@ import static
org.apache.tomcat.util.openssl.openssl_h_Macros.*;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.ExceptionUtils;
-import org.apache.tomcat.util.file.ConfigFileLoader;
-import org.apache.tomcat.util.file.ConfigurationSource.Resource;
import org.apache.tomcat.util.net.Constants;
import org.apache.tomcat.util.net.SSLHostConfig;
import org.apache.tomcat.util.net.SSLHostConfig.CertificateVerification;
@@ -76,7 +70,6 @@ import
org.apache.tomcat.util.openssl.SSL_CTX_set_cert_verify_callback$cb;
import org.apache.tomcat.util.openssl.SSL_CTX_set_tmp_dh_callback$dh;
import org.apache.tomcat.util.openssl.SSL_CTX_set_verify$callback;
import org.apache.tomcat.util.openssl.SSL_psk_client_cb_func;
-import org.apache.tomcat.util.openssl.SSL_psk_find_session_cb_func;
import org.apache.tomcat.util.openssl.SSL_psk_server_cb_func;
import org.apache.tomcat.util.openssl.SSL_psk_use_session_cb_func;
import org.apache.tomcat.util.openssl.openssl_h;
@@ -105,12 +98,9 @@ public class OpenSSLContext implements
org.apache.tomcat.util.net.SSLContext {
static final int OPTIONAL_NO_CA = 3;
- private static final String BEGIN_KEY = "-----BEGIN PRIVATE KEY-----\n";
- private static final String END_KEY = "\n-----END PRIVATE KEY-----";
-
private static final byte[] HTTP_11_PROTOCOL = new byte[] { 'h', 't', 't',
'p', '/', '1', '.', '1' };
- private static final byte[] DEFAULT_SESSION_ID_CONTEXT = new byte[] { 'd',
'e', 'f', 'a', 'u', 'l', 't' };
+ private static final byte[] DEFAULT_SESSION_ID_CONTEXT =
CertificateLoader.DEFAULT_SESSION_ID_CONTEXT;
static final CertificateFactory X509_CERT_FACTORY;
static {
@@ -144,7 +134,7 @@ public class OpenSSLContext implements
org.apache.tomcat.util.net.SSLContext {
private final Arena contextArena;
private final Cleanable cleanable;
- private static String[] getCiphers(MemorySegment sslCtx) {
+ public static String[] getCiphers(MemorySegment sslCtx) {
MemorySegment sk = SSL_CTX_get_ciphers(sslCtx);
int len = openssl_h_Compatibility.OPENSSL_sk_num(sk);
if (len <= 0) {
@@ -683,8 +673,11 @@ public class OpenSSLContext implements
org.apache.tomcat.util.net.SSLContext {
if (openssl_h_Compatibility.LIBRESSL ||
openssl_h_Compatibility.BORINGSSL) {
throw new
SSLException(sm.getString("openssl.pskTls13Unsupported"));
}
- SSL_CTX_set_psk_find_session_callback(state.sslCtx,
SSL_psk_find_session_cb_func
- .allocate(new
PskFindSessionCallback(selector), contextArena));
+ // Shared with the QUIC endpoint. The context is
+ // created for a single host so the resolver always
+ // returns that host's selector.
+
CertificateLoader.installFindSessionCallback(state.sslCtx, sslConnection ->
selector,
+ contextArena);
}
}
}
@@ -898,75 +891,6 @@ public class OpenSSLContext implements
org.apache.tomcat.util.net.SSLContext {
}
}
- private static class PskFindSessionCallback implements
SSL_psk_find_session_cb_func.Function {
-
- private final OpenSSLPreSharedKeySelector selector;
-
- PskFindSessionCallback(OpenSSLPreSharedKeySelector selector) {
- this.selector = selector;
- }
-
- @Override
- public int apply(MemorySegment ssl, MemorySegment identity, long
identityLength, MemorySegment sessionPointer) {
- try (var localArena = Arena.ofConfined()) {
- MemorySegment sessionPointerSegment =
-
sessionPointer.reinterpret(ValueLayout.ADDRESS.byteSize(), localArena, null);
- sessionPointerSegment.set(ValueLayout.ADDRESS, 0,
MemorySegment.NULL);
- if (MemorySegment.NULL.equals(identity) || identityLength < 0
|| identityLength > Integer.MAX_VALUE) {
- return 0;
- }
-
- byte[] identityBytes =
- identity.reinterpret(identityLength, localArena,
null).toArray(ValueLayout.JAVA_BYTE);
- int[] cipherSuite = new int[1];
- byte[] key = selector.select(ssl.address(), identityBytes,
cipherSuite);
- if (key == null) {
- return 1;
- }
- if (key.length == 0 || cipherSuite[0] <= 0 || cipherSuite[0] >
0xFFFF) {
- return 0;
- }
-
- byte[] cipherId = new byte[] { (byte) (cipherSuite[0] >> 8),
(byte) cipherSuite[0] };
- MemorySegment cipher =
- SSL_CIPHER_find(ssl,
localArena.allocateFrom(ValueLayout.JAVA_BYTE, cipherId));
- if (MemorySegment.NULL.equals(cipher)
- ||
!Constants.SSL_PROTO_TLSv1_3.equals(SSL_CIPHER_get_version(cipher).getString(0)))
{
- return 0;
- }
-
- MemorySegment session = SSL_SESSION_new();
- if (MemorySegment.NULL.equals(session)) {
- return 0;
- }
- boolean success = false;
- try {
- MemorySegment keySegment =
localArena.allocateFrom(ValueLayout.JAVA_BYTE, key);
- try {
- MemorySegment sidCtxSegment =
- localArena.allocateFrom(ValueLayout.JAVA_BYTE,
DEFAULT_SESSION_ID_CONTEXT);
- if (SSL_SESSION_set1_master_key(session, keySegment,
key.length) == 0 ||
- SSL_SESSION_set_cipher(session, cipher) == 0 ||
- SSL_SESSION_set_protocol_version(session,
TLS1_3_VERSION()) == 0 ||
- SSL_SESSION_set1_id_context(session,
sidCtxSegment,
- DEFAULT_SESSION_ID_CONTEXT.length) ==
0) {
- return 0;
- }
- } finally {
- keySegment.fill((byte) 0);
- }
- sessionPointerSegment.set(ValueLayout.ADDRESS, 0, session);
- success = true;
- return 1;
- } finally {
- if (!success) {
- SSL_SESSION_free(session);
- }
- }
- }
- }
- }
-
private static class PskUseSessionCallback implements
SSL_psk_use_session_cb_func.Function {
private final OpenSSLPreSharedKeySelector selector;
@@ -1194,272 +1118,101 @@ public class OpenSSLContext implements
org.apache.tomcat.util.net.SSLContext {
public boolean addCertificate(SSLHostConfigCertificate certificate, Arena
localArena) throws Exception {
// Load Server key and certificate
if (certificate.getCertificateFile() != null) {
- // Pick right key password
- String keyPassToUse;
- String keyPass = certificate.getCertificateKeyPassword();
- if (keyPass == null) {
- keyPass = certificate.getCertificateKeystorePassword();
- }
- String keyPassFile = certificate.getCertificateKeyPasswordFile();
- if (keyPassFile == null) {
- keyPassFile = certificate.getCertificateKeystorePasswordFile();
- }
- if (keyPassFile != null) {
- try (BufferedReader reader = new BufferedReader(
- new
InputStreamReader(ConfigFileLoader.getSource().getResource(keyPassFile).getInputStream(),
- StandardCharsets.UTF_8))) {
- keyPassToUse = reader.readLine();
- } catch (IOException ioe) {
- log.error(sm.getString("openssl.errorLoadingPassword",
keyPassFile), ioe);
- return false;
- }
- } else {
- keyPassToUse = keyPass;
+ // Reading the material (PEM certificate and key with an optional
+ // ENGINE fallback for the key, or a PKCS#12 bundle) is delegated
+ // to CertificateLoader, which logs failures. The entry owns the
+ // native leaf, key and chain objects; applying it below transfers
+ // the references OpenSSL needs.
+ CertificateLoader.CertificateEntry entry =
CertificateLoader.loadCertEntry(certificate,
+ localArena, keyFile -> {
+ if
(MemorySegment.NULL.equals(OpenSSLLibrary.enginePointer)) {
+ return MemorySegment.NULL;
+ }
+ // This needs a real file
+ return
ENGINE_load_private_key(OpenSSLLibrary.enginePointer,
+ localArena.allocateFrom(keyFile),
MemorySegment.NULL, MemorySegment.NULL);
+ });
+ if (entry == null) {
+ return false;
}
- // Set certificate
- byte[] certificateFileBytes;
- try (Resource resource =
ConfigFileLoader.getSource().getResource(certificate.getCertificateFile())) {
- certificateFileBytes =
resource.getInputStream().readAllBytes();
- } catch (IOException ioe) {
- log.error(sm.getString("openssl.errorLoadingCertificate",
certificate.getCertificateFile()), ioe);
+ if (!applyCertEntry(entry)) {
return false;
}
- MemorySegment certificateFileBytesNative =
- localArena.allocateFrom(ValueLayout.JAVA_BYTE,
certificateFileBytes);
- MemorySegment certificateBIO = BIO_new(BIO_s_mem());
- try {
- if (BIO_write(certificateBIO, certificateFileBytesNative,
certificateFileBytes.length) <= 0) {
-
log.error(sm.getString("openssl.errorLoadingCertificateWithError",
certificate.getCertificateFile(),
- OpenSSLLibrary.getLastError()));
- return false;
- }
- MemorySegment cert;
- MemorySegment key;
- if (certificate.getCertificateFile().endsWith(".pkcs12")) {
- // Load pkcs12
- MemorySegment p12 = d2i_PKCS12_bio(certificateBIO,
MemorySegment.NULL);
- if (MemorySegment.NULL.equals(p12)) {
-
log.error(sm.getString("openssl.errorLoadingCertificateWithError",
- certificate.getCertificateFile(),
OpenSSLLibrary.getLastError()));
- return false;
- }
- MemorySegment passwordAddress = MemorySegment.NULL;
- int passwordLength = 0;
- if (keyPassToUse != null && !keyPassToUse.isEmpty()) {
- passwordAddress =
localArena.allocateFrom(keyPassToUse);
- passwordLength = (int) (passwordAddress.byteSize() -
1);
- }
- if (PKCS12_verify_mac(p12, passwordAddress,
passwordLength) <= 0) {
- // Bad password
-
log.error(sm.getString("openssl.errorLoadingCertificateWithError",
- certificate.getCertificateFile(),
OpenSSLLibrary.getLastError()));
- PKCS12_free(p12);
- return false;
- }
- MemorySegment certPointer =
localArena.allocate(ValueLayout.ADDRESS);
- MemorySegment keyPointer =
localArena.allocate(ValueLayout.ADDRESS);
- if (PKCS12_parse(p12, passwordAddress, keyPointer,
certPointer, MemorySegment.NULL) <= 0) {
-
log.error(sm.getString("openssl.errorLoadingCertificateWithError",
- certificate.getCertificateFile(),
OpenSSLLibrary.getLastError()));
- PKCS12_free(p12);
- return false;
- }
- PKCS12_free(p12);
- cert = certPointer.get(ValueLayout.ADDRESS, 0);
- key = keyPointer.get(ValueLayout.ADDRESS, 0);
- } else {
- String certificateKeyFileName =
- (certificate.getCertificateKeyFile() == null) ?
certificate.getCertificateFile() :
- certificate.getCertificateKeyFile();
- // Load key
- byte[] certificateKeyFileBytes;
- try (Resource resource =
ConfigFileLoader.getSource().getResource(certificateKeyFileName)) {
- certificateKeyFileBytes =
resource.getInputStream().readAllBytes();
- } catch (IOException ioe) {
-
log.error(sm.getString("openssl.errorLoadingCertificate",
certificateKeyFileName), ioe);
- return false;
- }
- MemorySegment certificateKeyFileBytesNative =
- localArena.allocateFrom(ValueLayout.JAVA_BYTE,
certificateKeyFileBytes);
- MemorySegment keyBIO = BIO_new(BIO_s_mem());
- try {
- if (BIO_write(keyBIO, certificateKeyFileBytesNative,
certificateKeyFileBytes.length) <= 0) {
-
log.error(sm.getString("openssl.errorLoadingCertificateWithError",
certificateKeyFileName,
- OpenSSLLibrary.getLastError()));
- return false;
- }
- key = MemorySegment.NULL;
- for (int i = 0; i < 3; i++) {
- key = PEM_read_bio_PrivateKey(keyBIO,
MemorySegment.NULL,
- pem_password_cb.allocate(new
PasswordCallback(keyPassToUse), contextArena),
- MemorySegment.NULL);
- if (!MemorySegment.NULL.equals(key)) {
- break;
+ // Try to read DH parameters from the SSLCertificateFile, and
+ // similarly the ECDH curve name
+ byte[] certificateFileBytes =
CertificateLoader.readConfigFileBytes(
+
SSLHostConfig.adjustRelativePath(certificate.getCertificateFile()));
+ MemorySegment certificateBIO = certificateFileBytes == null ?
MemorySegment.NULL
+ : createMemoryBio(localArena, certificateFileBytes);
+ if (!MemorySegment.NULL.equals(certificateBIO)) {
+ try {
+ if (certificate.getType() == Type.RSA) {
+ BIO_reset(certificateBIO);
+ if (!openssl_h_Compatibility.BORINGSSL) {
+ if (!openssl_h_Compatibility.OPENSSL3) {
+ var dh = PEM_read_bio_DHparams(certificateBIO,
MemorySegment.NULL,
+ MemorySegment.NULL,
MemorySegment.NULL);
+ if (!MemorySegment.NULL.equals(dh)) {
+ SSL_CTX_set_tmp_dh(state.sslCtx, dh);
+ DH_free(dh);
+ }
+ } else {
+ var pkey =
PEM_read_bio_Parameters(certificateBIO, MemorySegment.NULL);
+ if (!MemorySegment.NULL.equals(pkey)) {
+ int numBits = EVP_PKEY_get_bits(pkey);
+ if (SSL_CTX_set0_tmp_dh_pkey(state.sslCtx,
pkey) <= 0) {
+ EVP_PKEY_free(pkey);
+ } else {
+
log.debug(sm.getString("openssl.setCustomDHParameters",
+ Integer.valueOf(numBits),
certificate.getCertificateFile()));
+ }
+ } else {
+ String errMessage =
OpenSSLLibrary.getLastError();
+ if (errMessage != null) {
+
log.debug(sm.getString("openssl.errorReadingPEMParameters", errMessage,
+
certificate.getCertificateFile()));
+ }
+ SSL_CTX_set_dh_auto(state.sslCtx, 1);
+ }
}
- BIO_reset(keyBIO);
}
- } finally {
- BIO_free(keyBIO);
- }
- if (MemorySegment.NULL.equals(key)) {
- if
(!MemorySegment.NULL.equals(OpenSSLLibrary.enginePointer)) {
- // This needs a real file
- key =
ENGINE_load_private_key(OpenSSLLibrary.enginePointer,
-
localArena.allocateFrom(SSLHostConfig.adjustRelativePath(certificateKeyFileName)),
- MemorySegment.NULL, MemorySegment.NULL);
- }
- }
- if (MemorySegment.NULL.equals(key)) {
-
log.error(sm.getString("openssl.errorLoadingCertificateWithError",
certificateKeyFileName,
- OpenSSLLibrary.getLastError()));
- return false;
- }
- // Load certificate
- cert = PEM_read_bio_X509_AUX(certificateBIO,
MemorySegment.NULL,
- pem_password_cb.allocate(new
PasswordCallback(keyPassToUse), contextArena),
- MemorySegment.NULL);
- if (MemorySegment.NULL.equals(cert) &&
- // EOF is accepted, then try again
- ((ERR_peek_last_error() & ERR_REASON_MASK()) ==
PEM_R_NO_START_LINE())) {
- ERR_clear_error();
- BIO_reset(certificateBIO);
- cert = d2i_X509_bio(certificateBIO,
MemorySegment.NULL);
- }
- if (MemorySegment.NULL.equals(cert)) {
-
log.error(sm.getString("openssl.errorLoadingCertificateWithError",
- certificate.getCertificateFile(),
OpenSSLLibrary.getLastError()));
- EVP_PKEY_free(key);
- return false;
}
- }
- if (SSL_CTX_use_certificate(state.sslCtx, cert) <= 0) {
- logLastError("openssl.errorLoadingCertificate");
- EVP_PKEY_free(key);
- X509_free(cert);
- return false;
- }
- X509_free(cert);
- if (SSL_CTX_use_PrivateKey(state.sslCtx, key) <= 0) {
- logLastError("openssl.errorLoadingPrivateKey");
- EVP_PKEY_free(key);
- return false;
- }
- EVP_PKEY_free(key);
- if (SSL_CTX_check_private_key(state.sslCtx) <= 0) {
- logLastError("openssl.errorPrivateKeyCheck");
- return false;
- }
- // Try to read DH parameters from the SSLCertificateFile
- if (certificate.getType() == Type.RSA) {
+ // Similarly, try to read the ECDH curve name from
SSLCertificateFile...
BIO_reset(certificateBIO);
if (!openssl_h_Compatibility.BORINGSSL) {
if (!openssl_h_Compatibility.OPENSSL3) {
- var dh = PEM_read_bio_DHparams(certificateBIO,
MemorySegment.NULL, MemorySegment.NULL,
- MemorySegment.NULL);
- if (!MemorySegment.NULL.equals(dh)) {
- SSL_CTX_set_tmp_dh(state.sslCtx, dh);
- DH_free(dh);
+ var ecparams =
PEM_read_bio_ECPKParameters(certificateBIO, MemorySegment.NULL,
+ MemorySegment.NULL, MemorySegment.NULL);
+ if (!MemorySegment.NULL.equals(ecparams)) {
+ int nid = EC_GROUP_get_curve_name(ecparams);
+ var eckey = EC_KEY_new_by_curve_name(nid);
+ SSL_CTX_set_tmp_ecdh(state.sslCtx, eckey);
+ EC_KEY_free(eckey);
+ EC_GROUP_free(ecparams);
}
+ // Set callback for DH parameters
+ SSL_CTX_set_tmp_dh_callback(state.sslCtx,
+
SSL_CTX_set_tmp_dh_callback$dh.allocate(new TmpDHCallback(), contextArena));
} else {
- var pkey = PEM_read_bio_Parameters(certificateBIO,
MemorySegment.NULL);
- if (!MemorySegment.NULL.equals(pkey)) {
- int numBits = EVP_PKEY_get_bits(pkey);
- if (SSL_CTX_set0_tmp_dh_pkey(state.sslCtx,
pkey) <= 0) {
- EVP_PKEY_free(pkey);
- } else {
-
log.debug(sm.getString("openssl.setCustomDHParameters",
Integer.valueOf(numBits),
- certificate.getCertificateFile()));
+ var ecparams =
PEM_ASN1_read_bio(d2i_ECPKParameters$SYMBOL(), PEM_STRING_ECPARAMETERS(),
+ certificateBIO, MemorySegment.NULL,
MemorySegment.NULL, MemorySegment.NULL);
+ if (!MemorySegment.NULL.equals(ecparams)) {
+ int curveNid =
EC_GROUP_get_curve_name(ecparams);
+ var curveNidAddress =
localArena.allocateFrom(ValueLayout.JAVA_INT, curveNid);
+ if (SSL_CTX_set1_groups(state.sslCtx,
curveNidAddress, 1) <= 0) {
+ curveNid = 0;
}
- } else {
- String errMessage =
OpenSSLLibrary.getLastError();
- if (errMessage != null) {
-
log.debug(sm.getString("openssl.errorReadingPEMParameters", errMessage,
+ if (log.isDebugEnabled()) {
+
log.debug(sm.getString("openssl.setECDHCurve", Integer.valueOf(curveNid),
certificate.getCertificateFile()));
}
- SSL_CTX_set_dh_auto(state.sslCtx, 1);
+ EC_GROUP_free(ecparams);
}
}
}
+ } finally {
+ BIO_free(certificateBIO);
}
- // Similarly, try to read the ECDH curve name from
SSLCertificateFile...
- BIO_reset(certificateBIO);
- if (!openssl_h_Compatibility.BORINGSSL) {
- if (!openssl_h_Compatibility.OPENSSL3) {
- var ecparams =
PEM_read_bio_ECPKParameters(certificateBIO, MemorySegment.NULL,
- MemorySegment.NULL, MemorySegment.NULL);
- if (!MemorySegment.NULL.equals(ecparams)) {
- int nid = EC_GROUP_get_curve_name(ecparams);
- var eckey = EC_KEY_new_by_curve_name(nid);
- SSL_CTX_set_tmp_ecdh(state.sslCtx, eckey);
- EC_KEY_free(eckey);
- EC_GROUP_free(ecparams);
- }
- // Set callback for DH parameters
- SSL_CTX_set_tmp_dh_callback(state.sslCtx,
- SSL_CTX_set_tmp_dh_callback$dh.allocate(new
TmpDHCallback(), contextArena));
- } else {
- var ecparams =
PEM_ASN1_read_bio(d2i_ECPKParameters$SYMBOL(), PEM_STRING_ECPARAMETERS(),
- certificateBIO, MemorySegment.NULL,
MemorySegment.NULL, MemorySegment.NULL);
- if (!MemorySegment.NULL.equals(ecparams)) {
- int curveNid = EC_GROUP_get_curve_name(ecparams);
- var curveNidAddress =
localArena.allocateFrom(ValueLayout.JAVA_INT, curveNid);
- if (SSL_CTX_set1_groups(state.sslCtx,
curveNidAddress, 1) <= 0) {
- curveNid = 0;
- }
- if (log.isDebugEnabled()) {
- log.debug(sm.getString("openssl.setECDHCurve",
Integer.valueOf(curveNid),
- certificate.getCertificateFile()));
- }
- EC_GROUP_free(ecparams);
- }
- }
- }
- // Set certificate chain file
- if (certificate.getCertificateChainFile() != null) {
- byte[] certificateChainBytes;
- try (Resource resource =
-
ConfigFileLoader.getSource().getResource(certificate.getCertificateChainFile()))
{
- certificateChainBytes =
resource.getInputStream().readAllBytes();
- } catch (IOException ioe) {
- log.error(
-
sm.getString("openssl.errorLoadingCertificate",
certificate.getCertificateChainFile()),
- ioe);
- return false;
- }
- MemorySegment certificateChainBytesNative =
- localArena.allocateFrom(ValueLayout.JAVA_BYTE,
certificateChainBytes);
- MemorySegment certificateChainBIO = BIO_new(BIO_s_mem());
- try {
- if (BIO_write(certificateChainBIO,
certificateChainBytesNative,
- certificateChainBytes.length) <= 0) {
-
log.error(sm.getString("openssl.errorLoadingCertificateWithError",
- certificate.getCertificateChainFile(),
OpenSSLLibrary.getLastError()));
- return false;
- }
- MemorySegment certChainEntry =
PEM_read_bio_X509_AUX(certificateChainBIO, MemorySegment.NULL,
- MemorySegment.NULL, MemorySegment.NULL);
- while (!MemorySegment.NULL.equals(certChainEntry)) {
- if (SSL_CTX_add0_chain_cert(state.sslCtx,
certChainEntry) <= 0) {
-
log.error(sm.getString("openssl.errorLoadingCertificateWithError",
- certificate.getCertificateChainFile(),
OpenSSLLibrary.getLastError()));
- X509_free(certChainEntry);
- }
- certChainEntry =
PEM_read_bio_X509_AUX(certificateChainBIO, MemorySegment.NULL,
- MemorySegment.NULL, MemorySegment.NULL);
- }
- // EOF is accepted, otherwise log an error
- if ((ERR_peek_last_error() & ERR_REASON_MASK()) ==
PEM_R_NO_START_LINE()) {
- ERR_clear_error();
- } else {
-
log.error(sm.getString("openssl.errorLoadingCertificateWithError",
- certificate.getCertificateChainFile(),
OpenSSLLibrary.getLastError()));
- }
- } finally {
- BIO_free(certificateChainBIO);
- }
- }
- } finally {
- BIO_free(certificateBIO);
}
} else {
String alias = certificate.getCertificateKeyAlias();
@@ -1472,84 +1225,26 @@ public class OpenSSLContext implements
org.apache.tomcat.util.net.SSLContext {
alias = findAlias(x509KeyManager, certificate);
chain = x509KeyManager.getCertificateChain(alias);
}
- String encodedKey = BEGIN_KEY + Base64.getMimeEncoder(64, new
byte[] { '\n' })
-
.encodeToString(x509KeyManager.getPrivateKey(alias).getEncoded()) + END_KEY;
- var rawCertificate =
localArena.allocateFrom(ValueLayout.JAVA_BYTE, chain[0].getEncoded());
- var rawCertificatePointer =
localArena.allocateFrom(ValueLayout.ADDRESS, rawCertificate);
- var rawKey = localArena.allocateFrom(ValueLayout.JAVA_BYTE,
encodedKey.getBytes(StandardCharsets.US_ASCII));
- var x509cert = d2i_X509(MemorySegment.NULL, rawCertificatePointer,
rawCertificate.byteSize());
- if (MemorySegment.NULL.equals(x509cert)) {
+ // Convert the in-memory key manager entry to native objects with
+ // the shared loader (PKCS#8 key via d2i_AutoPrivateKey,
+ // certificates via d2i_X509)
+ CertificateLoader.CertificateEntry entry =
+
CertificateLoader.toCertEntry(x509KeyManager.getPrivateKey(alias), chain,
localArena);
+ if (entry == null) {
logLastError("openssl.errorLoadingCertificate");
return false;
}
- MemorySegment keyBIO = BIO_new(BIO_s_mem());
- try {
- BIO_write(keyBIO, rawKey, (int) rawKey.byteSize());
- MemorySegment privateKeyAddress =
- PEM_read_bio_PrivateKey(keyBIO, MemorySegment.NULL,
MemorySegment.NULL, MemorySegment.NULL);
- if (MemorySegment.NULL.equals(privateKeyAddress)) {
- logLastError("openssl.errorLoadingPrivateKey");
- X509_free(x509cert);
- return false;
- }
- if (SSL_CTX_use_certificate(state.sslCtx, x509cert) <= 0) {
- logLastError("openssl.errorLoadingCertificate");
- EVP_PKEY_free(privateKeyAddress);
- X509_free(x509cert);
- return false;
- }
- X509_free(x509cert);
- if (SSL_CTX_use_PrivateKey(state.sslCtx, privateKeyAddress) <=
0) {
- logLastError("openssl.errorLoadingPrivateKey");
- EVP_PKEY_free(privateKeyAddress);
- return false;
- }
- EVP_PKEY_free(privateKeyAddress);
- if (SSL_CTX_check_private_key(state.sslCtx) <= 0) {
- logLastError("openssl.errorPrivateKeyCheck");
- return false;
- }
- if (!openssl_h_Compatibility.OPENSSL3) {
- // Set callback for DH parameters
- SSL_CTX_set_tmp_dh_callback(state.sslCtx,
- SSL_CTX_set_tmp_dh_callback$dh.allocate(new
TmpDHCallback(), contextArena));
- } else {
- BIO_reset(keyBIO);
- var pkey = PEM_read_bio_Parameters(keyBIO,
MemorySegment.NULL);
- if (!MemorySegment.NULL.equals(pkey)) {
- int numBits = EVP_PKEY_get_bits(pkey);
- if (SSL_CTX_set0_tmp_dh_pkey(state.sslCtx, pkey) <= 0)
{
- EVP_PKEY_free(pkey);
- } else {
-
log.debug(sm.getString("openssl.setCustomDHParameters",
Integer.valueOf(numBits),
- x509KeyManager.toString()));
- }
- } else {
- String errMessage = OpenSSLLibrary.getLastError();
- if (errMessage != null) {
-
log.debug(sm.getString("openssl.errorReadingPEMParameters", errMessage,
- x509KeyManager.toString()));
- }
- SSL_CTX_set_dh_auto(state.sslCtx, 1);
- }
- }
- for (int i = 1; i < chain.length; i++) {
- var rawCertificateChain =
localArena.allocateFrom(ValueLayout.JAVA_BYTE, chain[i].getEncoded());
- var rawCertificateChainPointer =
localArena.allocateFrom(ValueLayout.ADDRESS, rawCertificateChain);
- var x509certChain =
- d2i_X509(MemorySegment.NULL,
rawCertificateChainPointer, rawCertificateChain.byteSize());
- if (MemorySegment.NULL.equals(x509certChain)) {
- logLastError("openssl.errorLoadingCertificate");
- return false;
- }
- if (SSL_CTX_add0_chain_cert(state.sslCtx, x509certChain)
<= 0) {
- logLastError("openssl.errorAddingCertificate");
- X509_free(x509certChain);
- return false;
- }
- }
- } finally {
- BIO_free(keyBIO);
+ if (!applyCertEntry(entry)) {
+ return false;
+ }
+ if (!openssl_h_Compatibility.OPENSSL3) {
+ // Set callback for DH parameters
+ SSL_CTX_set_tmp_dh_callback(state.sslCtx,
+ SSL_CTX_set_tmp_dh_callback$dh.allocate(new
TmpDHCallback(), contextArena));
+ } else {
+ // The key material is in memory only: no parameters can be
+ // read from a file, generate DH parameters automatically
+ SSL_CTX_set_dh_auto(state.sslCtx, 1);
}
}
// Set revocation
@@ -1586,6 +1281,62 @@ public class OpenSSLContext implements
org.apache.tomcat.util.net.SSLContext {
}
+ /*
+ * Apply a loaded certificate entry to the SSL context. Ownership:
+ * SSL_CTX_use_certificate() and SSL_CTX_use_PrivateKey() take their own
+ * references, so the entry's leaf and key are always released here.
+ * SSL_CTX_add0_chain_cert() takes ownership of the certificate passed to
+ * it; certificates that cannot be added are released here. On failure the
+ * entry is fully released either way.
+ */
+ private boolean applyCertEntry(CertificateLoader.CertificateEntry entry) {
+ boolean ok = true;
+ if (SSL_CTX_use_certificate(state.sslCtx, entry.leaf) <= 0) {
+ logLastError("openssl.errorLoadingCertificate");
+ ok = false;
+ }
+ if (ok && SSL_CTX_use_PrivateKey(state.sslCtx, entry.key) <= 0) {
+ logLastError("openssl.errorLoadingPrivateKey");
+ ok = false;
+ }
+ if (ok && SSL_CTX_check_private_key(state.sslCtx) <= 0) {
+ logLastError("openssl.errorPrivateKeyCheck");
+ ok = false;
+ }
+ X509_free(entry.leaf);
+ EVP_PKEY_free(entry.key);
+ for (MemorySegment chainCert : entry.chain) {
+ if (ok && SSL_CTX_add0_chain_cert(state.sslCtx, chainCert) > 0) {
+ continue;
+ }
+ if (ok) {
+ logLastError("openssl.errorAddingCertificate");
+ ok = false;
+ }
+ X509_free(chainCert);
+ }
+ return ok;
+ }
+
+
+ /*
+ * Create a memory BIO holding the given bytes. BIO_write() copies the
+ * data, so the source allocation may be short-lived.
+ */
+ private static MemorySegment createMemoryBio(Arena arena, byte[] bytes) {
+ MemorySegment bio = BIO_new(BIO_s_mem());
+ if (MemorySegment.NULL.equals(bio)) {
+ return MemorySegment.NULL;
+ }
+ MemorySegment data = arena.allocateFrom(ValueLayout.JAVA_BYTE, bytes);
+ if (BIO_write(bio, data, bytes.length) <= 0) {
+ BIO_free(bio);
+ return MemorySegment.NULL;
+ }
+ return bio;
+ }
+
+
/*
* Find a valid alias when none was specified in the config.
*/
@@ -1682,6 +1433,11 @@ public class OpenSSLContext implements
org.apache.tomcat.util.net.SSLContext {
alias = findAlias(x509KeyManager, certificate);
chain = x509KeyManager.getCertificateChain(alias);
}
+ } else {
+ // No key manager (e.g. a PEM or PKCS#12 file configuration): read
+ // the chain from the configured source with the shared loader so
+ // the certificate data is available via JMX and for expiry checks
+ chain = CertificateLoader.getCertificateChain(certificate);
}
return chain;
diff --git a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java
b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java
index b8691992d2..61baf56e99 100644
--- a/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java
+++ b/java/org/apache/tomcat/util/net/openssl/panama/OpenSSLLibrary.java
@@ -78,7 +78,7 @@ public class OpenSSLLibrary {
static MemorySegment enginePointer = MemorySegment.NULL;
- static void initLibrary() {
+ public static void initLibrary() {
synchronized (lock) {
if (OpenSSLStatus.isLibraryInitialized()) {
return;
@@ -181,7 +181,9 @@ public class OpenSSLLibrary {
OpenSSLStatus.setVersion(OpenSSL_version_num());
OpenSSLStatus.setMajorVersion(openssl_h_Compatibility.MAJOR);
OpenSSLStatus.setMinorVersion(openssl_h_Compatibility.MINOR);
- if (openssl_h_Compatibility.OPENSSL3) {
+ if (openssl_h_Compatibility.OPENSSL4) {
+ OpenSSLStatus.setName(OpenSSLStatus.Name.OPENSSL4);
+ } else if (openssl_h_Compatibility.OPENSSL3) {
OpenSSLStatus.setName(OpenSSLStatus.Name.OPENSSL3);
} else if (openssl_h_Compatibility.OPENSSL) {
OpenSSLStatus.setName(OpenSSLStatus.Name.OPENSSL);
diff --git a/java/org/apache/tomcat/util/openssl/openssl_h.java
b/java/org/apache/tomcat/util/openssl/openssl_h.java
index b91b7bbf40..b7cd824c84 100644
--- a/java/org/apache/tomcat/util/openssl/openssl_h.java
+++ b/java/org/apache/tomcat/util/openssl/openssl_h.java
@@ -82,7 +82,7 @@ public class openssl_h {
public static final String LIBRARY_NAME =
System.getProperty("org.apache.tomcat.util.openssl.LIBRARY_NAME",
(JrePlatform.IS_MAC_OS) ? "ssl.48" : "ssl");
- static final SymbolLookup SYMBOL_LOOKUP;
+ public static final SymbolLookup SYMBOL_LOOKUP;
static {
if (USE_SYSTEM_LOAD_LIBRARY) {
if (CRYPTO_LIBRARY_NAME != null) {
@@ -3858,6 +3858,59 @@ public class openssl_h {
}
}
+ private static class d2i_AutoPrivateKey {
+ public static final FunctionDescriptor DESC =
+ FunctionDescriptor.of(openssl_h.C_POINTER,
openssl_h.C_POINTER, openssl_h.C_POINTER,
+ openssl_h.C_LONG);
+
+ public static final MemorySegment ADDR =
openssl_h.findOrThrow("d2i_AutoPrivateKey");
+
+ public static final MethodHandle HANDLE =
Linker.nativeLinker().downcallHandle(ADDR, DESC);
+ }
+
+ /**
+ * Function descriptor for:
+ * {@snippet lang = c : * EVP_PKEY *d2i_AutoPrivateKey(EVP_PKEY **a, const
unsigned char **in, long len)
+ * }
+ */
+ public static FunctionDescriptor d2i_AutoPrivateKey$descriptor() {
+ return d2i_AutoPrivateKey.DESC;
+ }
+
+ /**
+ * Downcall method handle for:
+ * {@snippet lang = c : * EVP_PKEY *d2i_AutoPrivateKey(EVP_PKEY **a, const
unsigned char **in, long len)
+ * }
+ */
+ public static MethodHandle d2i_AutoPrivateKey$handle() {
+ return d2i_AutoPrivateKey.HANDLE;
+ }
+
+ /**
+ * Address for:
+ * {@snippet lang = c : * EVP_PKEY *d2i_AutoPrivateKey(EVP_PKEY **a, const
unsigned char **in, long len)
+ * }
+ */
+ public static MemorySegment d2i_AutoPrivateKey$address() {
+ return d2i_AutoPrivateKey.ADDR;
+ }
+
+ /**
+ * {@snippet lang = c : * EVP_PKEY *d2i_AutoPrivateKey(EVP_PKEY **a, const
unsigned char **in, long len)
+ * }
+ */
+ public static MemorySegment d2i_AutoPrivateKey(MemorySegment a,
MemorySegment in, long len) {
+ var mh$ = d2i_AutoPrivateKey.HANDLE;
+ try {
+ if (TRACE_DOWNCALLS) {
+ traceDowncall("d2i_AutoPrivateKey", a, in, len);
+ }
+ return (MemorySegment) mh$.invokeExact(a, in, len);
+ } catch (Throwable ex$) {
+ throw new AssertionError("should not reach here", ex$);
+ }
+ }
+
private static class X509_free {
public static final FunctionDescriptor DESC =
FunctionDescriptor.ofVoid(openssl_h.C_POINTER);
diff --git a/java/org/apache/tomcat/util/openssl/openssl_h_Compatibility.java
b/java/org/apache/tomcat/util/openssl/openssl_h_Compatibility.java
index c04bb3e92f..11ace77b79 100644
--- a/java/org/apache/tomcat/util/openssl/openssl_h_Compatibility.java
+++ b/java/org/apache/tomcat/util/openssl/openssl_h_Compatibility.java
@@ -33,17 +33,28 @@ public class openssl_h_Compatibility {
public static final boolean OPENSSL;
public static final boolean OPENSSL1;
public static final boolean OPENSSL3;
+ public static final boolean OPENSSL4;
public static final boolean BORINGSSL;
public static final boolean LIBRESSL;
public static final int MAJOR;
public static final int MINOR;
+ /*
+ * A simple, QUIC specific function (const SSL_METHOD
*OSSL_QUIC_method(void)) that is only present in OpenSSL
+ * builds that include the QUIC implementation. Its absence, even on a
4.0+ version number, means the QUIC API is
+ * not available. Note: OSSL_QUIC_server_method and the other symbols used
by QuicBindings are deliberately not
+ * referenced here so this check is independent of the HTTP/3 bindings.
+ */
+ private static final String QUIC_PROBE_SYMBOL = "OSSL_QUIC_method";
+
static {
String versionString = OpenSSL_version(0).getString(0);
OPENSSL = versionString.contains("OpenSSL");
OPENSSL1 = OPENSSL && OpenSSL_version_num() < 0x3000000fL;
OPENSSL3 = OPENSSL && OpenSSL_version_num() >= 0x3000000fL;
+ OPENSSL4 = OPENSSL && OpenSSL_version_num() >= 0x40000000L &&
+ openssl_h.SYMBOL_LOOKUP.find(QUIC_PROBE_SYMBOL).isPresent();
BORINGSSL = versionString.contains("BoringSSL");
LIBRESSL = versionString.contains("LibreSSL");
int majorVersion = 0;
diff --git a/res/openssl/openssl-tomcat.conf b/res/openssl/openssl-tomcat.conf
index 7d72a01cfb..df7bfb08bf 100644
--- a/res/openssl/openssl-tomcat.conf
+++ b/res/openssl/openssl-tomcat.conf
@@ -341,6 +341,7 @@
--include-function i2d_X509 # header:
/usr/include/openssl/x509.h
--include-function d2i_X509 # header:
/usr/include/openssl/x509.h
--include-function d2i_X509_bio # header:
/usr/include/openssl/x509.h
+--include-function d2i_AutoPrivateKey # header:
/usr/include/openssl/x509.h
--include-function X509_EXTENSION_get_data # header:
/usr/include/openssl/x509.h
--include-function X509_free # header:
/usr/include/openssl/x509.h
--include-function X509_get_ext # header:
/usr/include/openssl/x509.h
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]