This is an automated email from the ASF dual-hosted git repository.

markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit b8d2b2593661aff2f12643839f17f5baea8c92c7
Author: Mark Thomas <[email protected]>
AuthorDate: Mon Sep 21 20:40:53 2026 +0100

    Add support for pre-shared keys with Tomcat Native TLS connections.
    
    Adds a new sub-component SSLHostConfigPreSharedKey that is configured
    via a new <PreSharedKey ... /> element in server.xml along with
    storeconfig support.
    
    Supports pre-shared key only (no cert) configuration for a TLS
    connector.
    
    Co-authored-by: GitHub Copilot (GPT-5.6 Sol) <[email protected]>
---
 java/org/apache/catalina/startup/Catalina.java     |   5 +
 .../catalina/startup/PreSharedKeyCreateRule.java   |  56 ++++++++
 .../catalina/storeconfig/SSLHostConfigSF.java      |  14 +-
 .../catalina/storeconfig/server-registry.xml       |   9 +-
 .../apache/tomcat/util/net/AbstractEndpoint.java   |   5 +-
 java/org/apache/tomcat/util/net/SSLHostConfig.java |  38 ++++++
 .../tomcat/util/net/SSLHostConfigPreSharedKey.java | 141 +++++++++++++++++++++
 java/org/apache/tomcat/util/net/jsse/JSSEUtil.java |   4 +
 .../tomcat/util/net/jsse/LocalStrings.properties   |   1 +
 .../util/net/openssl/LocalStrings.properties       |   3 +
 .../tomcat/util/net/openssl/OpenSSLContext.java    |  28 +++-
 .../net/openssl/OpenSSLPreSharedKeySelector.java   | 102 +++++++++++++++
 .../tomcat/util/net/openssl/OpenSSLUtil.java       |   3 +
 .../catalina/storeconfig/TestStoreConfig.java      |   8 ++
 .../apache/tomcat/util/net/TestSSLHostConfig.java  |  68 ++++++++++
 webapps/docs/changelog.xml                         |   4 +
 webapps/docs/config/http.xml                       |  32 ++++-
 17 files changed, 509 insertions(+), 12 deletions(-)

diff --git a/java/org/apache/catalina/startup/Catalina.java 
b/java/org/apache/catalina/startup/Catalina.java
index 1feded95d6..798bec3724 100644
--- a/java/org/apache/catalina/startup/Catalina.java
+++ b/java/org/apache/catalina/startup/Catalina.java
@@ -548,6 +548,11 @@ public class Catalina {
         
digester.addSetNext("Server/Service/Connector/SSLHostConfig/Certificate", 
"addCertificate",
                 "org.apache.tomcat.util.net.SSLHostConfigCertificate");
 
+        
digester.addRule("Server/Service/Connector/SSLHostConfig/PreSharedKey", new 
PreSharedKeyCreateRule());
+        
digester.addSetProperties("Server/Service/Connector/SSLHostConfig/PreSharedKey");
+        
digester.addSetNext("Server/Service/Connector/SSLHostConfig/PreSharedKey", 
"addPreSharedKey",
+                "org.apache.tomcat.util.net.SSLHostConfigPreSharedKey");
+
         
digester.addObjectCreate("Server/Service/Connector/SSLHostConfig/OpenSSLConf",
                 "org.apache.tomcat.util.net.openssl.OpenSSLConf");
         
digester.addSetProperties("Server/Service/Connector/SSLHostConfig/OpenSSLConf");
diff --git a/java/org/apache/catalina/startup/PreSharedKeyCreateRule.java 
b/java/org/apache/catalina/startup/PreSharedKeyCreateRule.java
new file mode 100644
index 0000000000..24fd7fce8f
--- /dev/null
+++ b/java/org/apache/catalina/startup/PreSharedKeyCreateRule.java
@@ -0,0 +1,56 @@
+/*
+ * 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.catalina.startup;
+
+import org.apache.tomcat.util.digester.Rule;
+import org.apache.tomcat.util.net.SSLHostConfig;
+import org.apache.tomcat.util.net.SSLHostConfigPreSharedKey;
+import org.xml.sax.Attributes;
+
+/**
+ * Rule implementation that creates an SSLHostConfigPreSharedKey.
+ */
+public class PreSharedKeyCreateRule extends Rule {
+
+    /**
+     * Default constructor.
+     */
+    public PreSharedKeyCreateRule() {
+    }
+
+    @Override
+    public void begin(String namespace, String name, Attributes attributes) 
throws Exception {
+        SSLHostConfig sslHostConfig = (SSLHostConfig) digester.peek();
+        SSLHostConfigPreSharedKey preSharedKey = new 
SSLHostConfigPreSharedKey(sslHostConfig);
+
+        digester.push(preSharedKey);
+
+        StringBuilder code = digester.getGeneratedCode();
+        if (code != null) {
+            code.append(SSLHostConfigPreSharedKey.class.getName()).append(' ')
+                    .append(digester.toVariableName(preSharedKey));
+            code.append(" = new 
").append(SSLHostConfigPreSharedKey.class.getName());
+            
code.append('(').append(digester.toVariableName(sslHostConfig)).append(");");
+            code.append(System.lineSeparator());
+        }
+    }
+
+    @Override
+    public void end(String namespace, String name) throws Exception {
+        digester.pop();
+    }
+}
diff --git a/java/org/apache/catalina/storeconfig/SSLHostConfigSF.java 
b/java/org/apache/catalina/storeconfig/SSLHostConfigSF.java
index cc5d61ec2f..ab8722a6b4 100644
--- a/java/org/apache/catalina/storeconfig/SSLHostConfigSF.java
+++ b/java/org/apache/catalina/storeconfig/SSLHostConfigSF.java
@@ -24,6 +24,7 @@ import org.apache.juli.logging.LogFactory;
 import org.apache.tomcat.util.net.SSLHostConfig;
 import org.apache.tomcat.util.net.SSLHostConfigCertificate;
 import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type;
+import org.apache.tomcat.util.net.SSLHostConfigPreSharedKey;
 import org.apache.tomcat.util.net.openssl.OpenSSLConf;
 
 /**
@@ -84,8 +85,13 @@ public class SSLHostConfigSF extends StoreFactoryBase {
             throws Exception {
         if (aSSLHostConfig instanceof SSLHostConfig sslHostConfig) {
             // Store nested <SSLHostConfigCertificate> elements
-            SSLHostConfigCertificate[] hostConfigsCertificates =
-                    sslHostConfig.getCertificates().toArray(new 
SSLHostConfigCertificate[0]);
+            SSLHostConfigCertificate[] hostConfigsCertificates;
+            if (sslHostConfig.isPreSharedKeyOnly()) {
+                hostConfigsCertificates = new SSLHostConfigCertificate[0];
+            } else {
+                hostConfigsCertificates =
+                        sslHostConfig.getCertificates().toArray(new 
SSLHostConfigCertificate[0]);
+            }
             // Remove a possible default UNDEFINED certificate
             if (hostConfigsCertificates.length > 1) {
                 ArrayList<SSLHostConfigCertificate> certificates = new 
ArrayList<>();
@@ -97,6 +103,10 @@ public class SSLHostConfigSF extends StoreFactoryBase {
                 hostConfigsCertificates = certificates.toArray(new 
SSLHostConfigCertificate[0]);
             }
             storeElementArray(aWriter, indent, hostConfigsCertificates);
+            // Store nested <PreSharedKey> elements
+            SSLHostConfigPreSharedKey[] preSharedKeys =
+                    sslHostConfig.getPreSharedKeys().toArray(new 
SSLHostConfigPreSharedKey[0]);
+            storeElementArray(aWriter, indent, preSharedKeys);
             // Store nested <OpenSSLConf> element
             OpenSSLConf openSslConf = sslHostConfig.getOpenSslConf();
             storeElement(aWriter, indent, openSslConf);
diff --git a/java/org/apache/catalina/storeconfig/server-registry.xml 
b/java/org/apache/catalina/storeconfig/server-registry.xml
index b728402000..eb5c46ff37 100644
--- a/java/org/apache/catalina/storeconfig/server-registry.xml
+++ b/java/org/apache/catalina/storeconfig/server-registry.xml
@@ -132,6 +132,14 @@
         storeFactoryClass="org.apache.catalina.storeconfig.StoreFactoryBase"
         
storeAppenderClass="org.apache.catalina.storeconfig.CertificateStoreAppender">
      </Description>
+     <Description
+        tag="PreSharedKey"
+        standard="true"
+        default="true"
+        tagClass="org.apache.tomcat.util.net.SSLHostConfigPreSharedKey"
+        children="false"
+        storeFactoryClass="org.apache.catalina.storeconfig.StoreFactoryBase">
+     </Description>
      <Description
         tag="OpenSSLConf"
         standard="true"
@@ -495,4 +503,3 @@
         storeFactoryClass="org.apache.catalina.storeconfig.StoreFactoryBase">
      </Description>
 </Registry>
-
diff --git a/java/org/apache/tomcat/util/net/AbstractEndpoint.java 
b/java/org/apache/tomcat/util/net/AbstractEndpoint.java
index a38278809f..30d4fb8b11 100644
--- a/java/org/apache/tomcat/util/net/AbstractEndpoint.java
+++ b/java/org/apache/tomcat/util/net/AbstractEndpoint.java
@@ -593,7 +593,9 @@ public abstract class AbstractEndpoint<S, U> {
                 certificate.setSslContextGenerated(sslContext);
             }
 
-            logCertificate(certificate);
+            if (!sslHostConfig.isPreSharedKeyOnly()) {
+                logCertificate(certificate);
+            }
         }
 
     }
@@ -2533,4 +2535,3 @@ public abstract class AbstractEndpoint<S, U> {
      */
     protected abstract void destroySocket(U socket);
 }
-
diff --git a/java/org/apache/tomcat/util/net/SSLHostConfig.java 
b/java/org/apache/tomcat/util/net/SSLHostConfig.java
index 04c1b9e7ca..0303376aaf 100644
--- a/java/org/apache/tomcat/util/net/SSLHostConfig.java
+++ b/java/org/apache/tomcat/util/net/SSLHostConfig.java
@@ -153,6 +153,10 @@ public class SSLHostConfig implements Serializable {
      * The set of certificate configurations.
      */
     private final Set<SSLHostConfigCertificate> certificates = new 
LinkedHashSet<>(4);
+    /**
+     * The set of pre-shared key configurations.
+     */
+    private final Set<SSLHostConfigPreSharedKey> preSharedKeys = new 
LinkedHashSet<>();
     // Common
     /**
      * The path to the certificate revocation list file.
@@ -576,6 +580,40 @@ public class SSLHostConfig implements Serializable {
     }
 
 
+    /**
+     * Adds a pre-shared key to this SSL host configuration.
+     *
+     * @param preSharedKey the pre-shared key to add
+     */
+    public void addPreSharedKey(SSLHostConfigPreSharedKey preSharedKey) {
+        preSharedKeys.add(preSharedKey);
+    }
+
+
+    /**
+     * Returns the set of pre-shared keys.
+     *
+     * @return the pre-shared keys
+     */
+    public Set<SSLHostConfigPreSharedKey> getPreSharedKeys() {
+        return preSharedKeys;
+    }
+
+
+    /**
+     * Determines whether this configuration uses pre-shared keys without a 
certificate.
+     *
+     * @return {@code true} if pre-shared keys are configured and no 
certificate was explicitly configured
+     */
+    public boolean isPreSharedKeyOnly() {
+        if (preSharedKeys.isEmpty()) {
+            return false;
+        }
+        return certificates.isEmpty() ||
+                certificates.size() == 1 && 
certificates.contains(defaultCertificate);
+    }
+
+
     // ----------------------------------------- Common configuration 
properties
 
     /**
diff --git a/java/org/apache/tomcat/util/net/SSLHostConfigPreSharedKey.java 
b/java/org/apache/tomcat/util/net/SSLHostConfigPreSharedKey.java
new file mode 100644
index 0000000000..7660490631
--- /dev/null
+++ b/java/org/apache/tomcat/util/net/SSLHostConfigPreSharedKey.java
@@ -0,0 +1,141 @@
+/*
+ *  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;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Locale;
+
+import org.apache.tomcat.util.buf.HexUtils;
+import org.apache.tomcat.util.net.openssl.ciphers.MessageDigest;
+
+/**
+ * Represents a pre-shared key configuration for a virtual host.
+ */
+public class SSLHostConfigPreSharedKey implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    private final SSLHostConfig sslHostConfig;
+
+    private String identity;
+    private byte[] key;
+    private MessageDigest digest = MessageDigest.SHA256;
+
+    /**
+     * Creates a new pre-shared key configuration for the given host.
+     *
+     * @param sslHostConfig the parent SSL host configuration
+     */
+    public SSLHostConfigPreSharedKey(SSLHostConfig sslHostConfig) {
+        this.sslHostConfig = sslHostConfig;
+    }
+
+    /**
+     * Returns the SSLHostConfig that owns this pre-shared key configuration.
+     *
+     * @return the parent SSLHostConfig
+     */
+    public SSLHostConfig getSSLHostConfig() {
+        return sslHostConfig;
+    }
+
+    /**
+     * Returns the identity associated with the pre-shared key.
+     *
+     * @return the pre-shared key identity
+     */
+    public String getIdentity() {
+        return identity;
+    }
+
+    /**
+     * Sets the identity associated with the pre-shared key.
+     *
+     * @param identity the pre-shared key identity
+     */
+    public void setIdentity(String identity) {
+        this.identity = identity;
+    }
+
+    /**
+     * Returns the pre-shared key as a hexadecimal string.
+     *
+     * @return the hexadecimal pre-shared key
+     */
+    public String getKey() {
+        return HexUtils.toHexString(key);
+    }
+
+    /**
+     * Sets the pre-shared key from a hexadecimal string.
+     *
+     * @param key the hexadecimal pre-shared key
+     *
+     * @throws IllegalArgumentException if the key is not valid hexadecimal
+     */
+    public void setKey(String key) {
+        this.key = HexUtils.fromHexString(key);
+    }
+
+    /**
+     * Returns the pre-shared key.
+     *
+     * @return the pre-shared key
+     */
+    public byte[] getKeyInternal() {
+        return key;
+    }
+
+    /**
+     * Returns the message digest algorithm name.
+     *
+     * @return the message digest algorithm name
+     */
+    public String getDigest() {
+        if (digest == null) {
+            return null;
+        }
+        return digest.name();
+    }
+
+    /**
+     * Sets the message digest algorithm.
+     *
+     * @param digest the message digest name
+     *
+     * @throws IllegalArgumentException if the message digest is not recognized
+     */
+    public void setDigest(String digest) {
+        if (digest == null) {
+            this.digest = null;
+            return;
+        }
+        // Remove all "-". Handles SHA-256 as well as odd variations like 
Sh-A256 but there is no harm in that.
+        this.digest = MessageDigest.valueOf(digest.replaceAll("-", 
"").toUpperCase(Locale.ENGLISH));
+    }
+
+    /**
+     * Returns the message digest.
+     *
+     * @return the message digest
+     */
+    public MessageDigest getDigestInternal() {
+        return digest;
+    }
+}
diff --git a/java/org/apache/tomcat/util/net/jsse/JSSEUtil.java 
b/java/org/apache/tomcat/util/net/jsse/JSSEUtil.java
index 62215b6ddf..5d830db28e 100644
--- a/java/org/apache/tomcat/util/net/jsse/JSSEUtil.java
+++ b/java/org/apache/tomcat/util/net/jsse/JSSEUtil.java
@@ -158,6 +158,10 @@ public class JSSEUtil extends SSLUtilBase {
                         log.warn(sm.getString("jsseUtil.opensslconf.present"));
                     }
 
+                    if (!sslHostConfig.getPreSharedKeys().isEmpty()) {
+                        log.warn(sm.getString("jsseUtil.psk.present"));
+                    }
+
                     initialized = true;
                 }
             }
diff --git a/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties 
b/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties
index 10f6d8e301..ceb1739aef 100644
--- a/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties
+++ b/java/org/apache/tomcat/util/net/jsse/LocalStrings.properties
@@ -19,6 +19,7 @@ jsseSupport.clientCertError=Error trying to obtain a 
certificate from the client
 jsseUtil.excludeProtocol=The SSL protocol [{0}] which is supported in this JRE 
was excluded from the protocols available to Tomcat
 jsseUtil.noDefaultProtocols=Unable to determine a default for 
sslEnabledProtocols. Set an explicit value to ensure the connector can start.
 jsseUtil.opensslconf.present=A connector is configured to use a JSSE TLS 
implementation with OpenSSL specific OpenSSLConf configuration elements. The 
OpenSSLConf configuration elements will be ignored.
+jsseUtil.psk.present=A connector is configured to use a JSSE TLS 
implementation with OpenSSL specific pre-shared key configuration elements. The 
pre-shared key configuration elements will be ignored.
 
 pemFile.encryption.broken=The PEM file [{0}] is using [{1}] which is 
considered broken because a brute force attack is trivial with current hardware
 pemFile.encryption.insecure=The PEM file [{0}] is using [{1}] which is 
considered insecure as it is vulnerable to a brute-force attack
diff --git a/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties 
b/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties
index 333a84b3cc..dc73269ae3 100644
--- a/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties
+++ b/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties
@@ -66,4 +66,7 @@ opensslconf.resultCommand=OpenSSLConf command (name [{0}], 
value [{1}]) returned
 
 opensslImplementation.notAvailable=A suitable Tomcat Native implementation is 
not available
 
+opensslPreSharedKeySelector.identity.duplicate=The identity [{0}] has been 
defined multiple times for SSL virtual host [{1}]
+opensslPreSharedKeySelector.truncate=The PSK for identity [{0}] has been 
truncated for [{1}] bytes for compatibility with OpenSSL.
+
 sessionContext.nullTicketKeys=Null keys
diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java 
b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java
index 18102be9c7..05976269d9 100644
--- a/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java
+++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLContext.java
@@ -33,6 +33,7 @@ import java.util.Arrays;
 import java.util.Base64;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.locks.Lock;
 
 import javax.net.ssl.KeyManager;
@@ -49,6 +50,7 @@ import org.apache.juli.logging.Log;
 import org.apache.juli.logging.LogFactory;
 import org.apache.tomcat.jni.AprStatus;
 import org.apache.tomcat.jni.Pool;
+import org.apache.tomcat.jni.PreSharedKeySelector;
 import org.apache.tomcat.jni.SSL;
 import org.apache.tomcat.jni.SSLConf;
 import org.apache.tomcat.jni.SSLContext;
@@ -57,6 +59,7 @@ import org.apache.tomcat.util.net.SSLHostConfig;
 import org.apache.tomcat.util.net.SSLHostConfig.CertificateVerification;
 import org.apache.tomcat.util.net.SSLHostConfigCertificate;
 import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type;
+import org.apache.tomcat.util.net.SSLHostConfigPreSharedKey;
 import org.apache.tomcat.util.net.SSLUtilBase;
 import org.apache.tomcat.util.res.StringManager;
 
@@ -372,12 +375,14 @@ public class OpenSSLContext implements 
org.apache.tomcat.util.net.SSLContext {
             SSLContext.setCipherSuite(state.ctx, sslHostConfig.getCiphers());
             SSLContext.setCipherSuitesEx(state.ctx, 
sslHostConfig.getCipherSuites());
 
-            // If there is no certificate file must be using a KeyStore so a 
KeyManager is required.
-            // If there is a certificate file a KeyManager is helpful but not 
strictly necessary.
-            certificate.setCertificateKeyManager(
-                    OpenSSLUtil.chooseKeyManager(kms, 
certificate.getCertificateFile() == null));
+            if (!sslHostConfig.isPreSharedKeyOnly()) {
+                // If there is no certificate file must be using a KeyStore so 
a KeyManager is required.
+                // If there is a certificate file a KeyManager is helpful but 
not strictly necessary.
+                certificate.setCertificateKeyManager(
+                        OpenSSLUtil.chooseKeyManager(kms, 
certificate.getCertificateFile() == null));
 
-            addCertificate(certificate);
+                addCertificate(certificate);
+            }
 
             // Client certificate verification
             int value = switch (sslHostConfig.getCertificateVerification()) {
@@ -440,6 +445,19 @@ public class OpenSSLContext implements 
org.apache.tomcat.util.net.SSLContext {
                 }
             }
 
+            Set<SSLHostConfigPreSharedKey> psks = 
sslHostConfig.getPreSharedKeys();
+            if (!psks.isEmpty()) {
+                PreSharedKeySelector selector = new 
OpenSSLPreSharedKeySelector(psks);
+
+                for (String protocol : sslHostConfig.getEnabledProtocols()) {
+                    if (Constants.SSL_PROTO_TLSv1_2.equals(protocol)) {
+                        SSLContext.setPskServerCallback(state.ctx, selector);
+                    } else if (Constants.SSL_PROTO_TLSv1_3.equals(protocol)) {
+                        SSLContext.setPskFindSessionCallback(state.ctx, 
selector);
+                    }
+                }
+            }
+
             if (negotiableProtocols != null && !negotiableProtocols.isEmpty()) 
{
                 List<String> protocols = new ArrayList<>(negotiableProtocols);
                 protocols.add("http/1.1");
diff --git 
a/java/org/apache/tomcat/util/net/openssl/OpenSSLPreSharedKeySelector.java 
b/java/org/apache/tomcat/util/net/openssl/OpenSSLPreSharedKeySelector.java
new file mode 100644
index 0000000000..33208edbfc
--- /dev/null
+++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLPreSharedKeySelector.java
@@ -0,0 +1,102 @@
+/*
+ * 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;
+
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.juli.logging.Log;
+import org.apache.juli.logging.LogFactory;
+import org.apache.tomcat.jni.PreSharedKeySelector;
+import org.apache.tomcat.util.net.SSLHostConfigPreSharedKey;
+import org.apache.tomcat.util.res.StringManager;
+
+/**
+ * Callback implementation that Tomcat uses to select a pre-shared key for a 
given connection based on the identity
+ * provided by the client.
+ */
+public class OpenSSLPreSharedKeySelector implements PreSharedKeySelector {
+
+    private static final Log log = 
LogFactory.getLog(OpenSSLPreSharedKeySelector.class);
+    private static final StringManager sm = 
StringManager.getManager(OpenSSLPreSharedKeySelector.class);
+
+    private final Map<String,SSLHostConfigPreSharedKey> identityToKeyMap = new 
HashMap<>();
+
+    public OpenSSLPreSharedKeySelector(Set<SSLHostConfigPreSharedKey> psks) {
+        for (SSLHostConfigPreSharedKey psk : psks) {
+            SSLHostConfigPreSharedKey old = 
identityToKeyMap.put(psk.getIdentity(), psk);
+            if (old != null) {
+                throw new 
IllegalArgumentException(sm.getString("opensslPreSharedKeySelector.identity.duplicate",
+                        psk.getIdentity(), 
psk.getSSLHostConfig().getHostName()));
+            }
+        }
+    }
+
+    @Override
+    public byte[] select(long ssl, String identity) {
+        SSLHostConfigPreSharedKey psk = identityToKeyMap.get(identity);
+        if (psk == null) {
+            return null;
+        }
+        // Need to limit keys to 48 bytes for TLS 1.3
+        return truncateToLength(identity, psk.getKeyInternal(), 512);
+    }
+
+    @Override
+    public byte[] select(long ssl, byte[] identity, int[] cipherSuite) {
+        String identityString = new String(identity, StandardCharsets.UTF_8);
+        SSLHostConfigPreSharedKey psk = identityToKeyMap.get(identityString);
+        if (psk == null) {
+            return null;
+        }
+        switch (psk.getDigestInternal()) {
+            case SHA256:
+                // Any TLS ciphersuite that OpenSSL recognises that uses 
SHA256 works here
+                cipherSuite[0] = 0x1301;
+                break;
+            case SHA384:
+                // Any TLS ciphersuite that OpenSSL recognises that uses 
SHA384 works here
+                cipherSuite[0] = 0x1302;
+                break;
+            case AEAD:
+            case GOST89MAC:
+            case GOST94:
+            case MD5:
+            case SHA1:
+            default:
+                // Unsupported digest
+                return null;
+        }
+        // Need to limit keys to 48 bytes for TLS 1.3
+        return truncateToLength(identityString, psk.getKeyInternal(), 48);
+    }
+
+
+    private byte[] truncateToLength(String identity, byte[] input, int length) 
{
+        byte[] result;
+        if (input.length > length) {
+            result = new byte[length];
+            System.arraycopy(input, 0, result, 0, length);
+            log.warn(sm.getString("opensslPreSharedKeySelector.truncate", 
identity, Integer.toString(length)));
+        } else {
+            result = input;
+        }
+        return result;
+    }
+}
diff --git a/java/org/apache/tomcat/util/net/openssl/OpenSSLUtil.java 
b/java/org/apache/tomcat/util/net/openssl/OpenSSLUtil.java
index 3ddae0bc90..5749cfe679 100644
--- a/java/org/apache/tomcat/util/net/openssl/OpenSSLUtil.java
+++ b/java/org/apache/tomcat/util/net/openssl/OpenSSLUtil.java
@@ -117,6 +117,9 @@ public class OpenSSLUtil extends SSLUtilBase {
 
     @Override
     public KeyManager[] getKeyManagers() throws Exception {
+        if (sslHostConfig.isPreSharedKeyOnly()) {
+            return null;
+        }
         try {
             return super.getKeyManagers();
         } catch (IllegalArgumentException e) {
diff --git a/test/org/apache/catalina/storeconfig/TestStoreConfig.java 
b/test/org/apache/catalina/storeconfig/TestStoreConfig.java
index e63c34a342..09fe161b36 100644
--- a/test/org/apache/catalina/storeconfig/TestStoreConfig.java
+++ b/test/org/apache/catalina/storeconfig/TestStoreConfig.java
@@ -40,6 +40,7 @@ import org.apache.catalina.startup.Tomcat;
 import org.apache.catalina.startup.TomcatBaseTest;
 import org.apache.catalina.util.IOTools;
 import org.apache.catalina.valves.AccessLogValve;
+import org.apache.tomcat.util.net.SSLHostConfigPreSharedKey;
 import org.xml.sax.InputSource;
 
 public class TestStoreConfig extends TomcatBaseTest {
@@ -89,6 +90,11 @@ public class TestStoreConfig extends TomcatBaseTest {
         
tc_SSLHostConfigCertificate_23.setCertificateKeystoreFile("conf/localhost-rsa.jks");
         
tc_SSLHostConfigCertificate_23.setCertificateKeystorePassword("mypassword");
         tc_SSLHostConfig_22.addCertificate(tc_SSLHostConfigCertificate_23);
+        SSLHostConfigPreSharedKey preSharedKey = new 
SSLHostConfigPreSharedKey(tc_SSLHostConfig_22);
+        preSharedKey.setIdentity("test");
+        preSharedKey.setKey("000102030405060708090a0b0c0d0e0f");
+        preSharedKey.setDigest("SHA256");
+        tc_SSLHostConfig_22.addPreSharedKey(preSharedKey);
         tomcat.getConnector().addSslHostConfig(tc_SSLHostConfig_22);
 
         org.apache.catalina.ha.tcp.SimpleTcpCluster tc_SimpleTcpCluster_51 = 
new org.apache.catalina.ha.tcp.SimpleTcpCluster();
@@ -149,6 +155,8 @@ public class TestStoreConfig extends TomcatBaseTest {
         Assert.assertTrue(serverXmlDump.contains("UserDatabaseRealm"));
         
Assert.assertTrue(serverXmlDump.contains("SecretKeyCredentialHandler"));
         
Assert.assertTrue(serverXmlDump.contains("certificateKeystorePassword="));
+        Assert.assertTrue(serverXmlDump.contains(
+                "<PreSharedKey digest=\"SHA256\" identity=\"test\" 
key=\"000102030405060708090a0b0c0d0e0f\""));
         Assert.assertTrue(serverXmlDump.contains("+TLSv1.1"));
         SAXParserFactory.newInstance().newSAXParser().getXMLReader().parse(new 
InputSource(new StringReader(serverXmlDump)));
 
diff --git a/test/org/apache/tomcat/util/net/TestSSLHostConfig.java 
b/test/org/apache/tomcat/util/net/TestSSLHostConfig.java
index 9bfdbfef94..40fe272231 100644
--- a/test/org/apache/tomcat/util/net/TestSSLHostConfig.java
+++ b/test/org/apache/tomcat/util/net/TestSSLHostConfig.java
@@ -29,6 +29,7 @@ import org.junit.Test;
 import org.apache.tomcat.util.net.openssl.OpenSSLConf;
 import org.apache.tomcat.util.net.openssl.OpenSSLConfCmd;
 import org.apache.tomcat.util.net.openssl.ciphers.Cipher;
+import org.apache.tomcat.util.net.openssl.ciphers.MessageDigest;
 
 public class TestSSLHostConfig {
 
@@ -146,6 +147,61 @@ public class TestSSLHostConfig {
     }
 
 
+    @Test
+    public void testPreSharedKey() {
+        SSLHostConfig sslHostConfig = new SSLHostConfig();
+        SSLHostConfigPreSharedKey preSharedKey = new 
SSLHostConfigPreSharedKey(sslHostConfig);
+        Assert.assertEquals("SHA256", preSharedKey.getDigest());
+        Assert.assertEquals(MessageDigest.SHA256, 
preSharedKey.getDigestInternal());
+        preSharedKey.setIdentity("test");
+        preSharedKey.setKey("00010203");
+        preSharedKey.setDigest("SHA256");
+        sslHostConfig.addPreSharedKey(preSharedKey);
+
+        Assert.assertSame(sslHostConfig, preSharedKey.getSSLHostConfig());
+        Assert.assertEquals("test", preSharedKey.getIdentity());
+        Assert.assertEquals("00010203", preSharedKey.getKey());
+        Assert.assertArrayEquals(new byte[] { 0, 1, 2, 3 }, 
preSharedKey.getKeyInternal());
+        Assert.assertEquals("SHA256", preSharedKey.getDigest());
+        Assert.assertEquals(MessageDigest.SHA256, 
preSharedKey.getDigestInternal());
+        Assert.assertSame(preSharedKey, 
sslHostConfig.getPreSharedKeys().iterator().next());
+    }
+
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testPreSharedKeyInvalidKey() {
+        SSLHostConfigPreSharedKey preSharedKey = new 
SSLHostConfigPreSharedKey(null);
+        preSharedKey.setKey("invalid");
+    }
+
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testPreSharedKeyInvalidDigest() {
+        SSLHostConfigPreSharedKey preSharedKey = new 
SSLHostConfigPreSharedKey(null);
+        preSharedKey.setDigest("invalid");
+    }
+
+
+    @Test
+    public void testPreSharedKeyOnly() {
+        SSLHostConfig sslHostConfig = new SSLHostConfig();
+        SSLHostConfigPreSharedKey preSharedKey = new 
SSLHostConfigPreSharedKey(sslHostConfig);
+        sslHostConfig.addPreSharedKey(preSharedKey);
+
+        Assert.assertTrue(sslHostConfig.isPreSharedKeyOnly());
+
+        // Creating the default certificate used to hold the SSLContext must 
not change the result.
+        sslHostConfig.getCertificates(true);
+        Assert.assertTrue(sslHostConfig.isPreSharedKeyOnly());
+
+        SSLHostConfig withCertificate = new SSLHostConfig();
+        withCertificate.addPreSharedKey(new 
SSLHostConfigPreSharedKey(withCertificate));
+        withCertificate.addCertificate(
+                new SSLHostConfigCertificate(withCertificate, 
SSLHostConfigCertificate.Type.UNDEFINED));
+        Assert.assertFalse(withCertificate.isPreSharedKeyOnly());
+    }
+
+
     @Test
     public void testSerialization() throws IOException, ClassNotFoundException 
{
         // Dummy OpenSSL command name/value pair
@@ -160,6 +216,11 @@ public class TestSSLHostConfig {
         openSSLConfCmd.setValue(value);
         openSSLConf.addCmd(openSSLConfCmd);
         sslHostConfig.setOpenSslConf(openSSLConf);
+        SSLHostConfigPreSharedKey preSharedKey = new 
SSLHostConfigPreSharedKey(sslHostConfig);
+        preSharedKey.setIdentity("test");
+        preSharedKey.setKey("00010203");
+        preSharedKey.setDigest("SHA256");
+        sslHostConfig.addPreSharedKey(preSharedKey);
 
         // Serialize
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -178,5 +239,12 @@ public class TestSSLHostConfig {
         OpenSSLConfCmd command = commands.get(0);
         Assert.assertEquals(name, command.getName());
         Assert.assertEquals(value, command.getValue());
+        SSLHostConfigPreSharedKey outputPreSharedKey = 
output.getPreSharedKeys().iterator().next();
+        Assert.assertSame(output, outputPreSharedKey.getSSLHostConfig());
+        Assert.assertEquals("test", outputPreSharedKey.getIdentity());
+        Assert.assertEquals("00010203", outputPreSharedKey.getKey());
+        Assert.assertEquals("SHA256", outputPreSharedKey.getDigest());
+        Assert.assertArrayEquals(new byte[] { 0, 1, 2, 3 }, 
outputPreSharedKey.getKeyInternal());
+        Assert.assertEquals(MessageDigest.SHA256, 
outputPreSharedKey.getDigestInternal());
     }
 }
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 6a01bb7fc2..d7ff714eb2 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -133,6 +133,10 @@
         Prevent duplicate log messages when clustering JARs are not present on
         startup. (csutherl)
       </fix>
+      <add>
+        Add support for configuring TLS pre-shared keys under an
+        <code>SSLHostConfig</code>. (markt)
+      </add>
       <add>
         Add the Jakarta EE 12 XML schemas. (markt)
       </add>
diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml
index 119883db87..3c7512b4aa 100644
--- a/webapps/docs/config/http.xml
+++ b/webapps/docs/config/http.xml
@@ -1253,8 +1253,10 @@
   <strong>Connector</strong>.</p>
 
   <p>Each <strong>SSLHostConfig</strong> must in turn define at least one
-  <strong>Certificate</strong>. The types of the <strong>Certificate</strong>s
-  must be unique.</p>
+  <strong>Certificate</strong> or <strong>PreSharedKey</strong>. A 
configuration
+  with no <strong>Certificate</strong> requires the Tomcat Native OpenSSL
+  implementation. The types of the <strong>Certificate</strong>s must be
+  unique.</p>
 
   <p>In addition to the standard TLS related request attributes defined in
   section 3.10 of the Servlet specification, Tomcat supports a number of
@@ -1585,6 +1587,32 @@
 
   </subsection>
 
+  <subsection name="SSL Support - PreSharedKey">
+
+  <p>A <strong>PreSharedKey</strong> element represents a pre-shared key that
+  may be used by an OpenSSL-based connector. It must be nested in an
+  <strong>SSLHostConfig</strong> element.</p>
+
+  <attributes>
+
+    <attribute name="identity" required="true">
+      <p>The identity associated with this pre-shared key.</p>
+    </attribute>
+
+    <attribute name="key" required="true">
+      <p>The pre-shared key encoded as hexadecimal characters.</p>
+    </attribute>
+
+    <attribute name="digest" required="false">
+      <p>The message digest algorithm associated with this pre-shared key for
+      TLS 1.3, for example <code>SHA256</code> or <code>SHA384</code>. If not
+      specified, the default is <code>SHA256</code>.</p>
+    </attribute>
+
+  </attributes>
+
+  </subsection>
+
   <subsection name="SSL Support - Certificate">
 
   <p></p>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to