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]
