diff --git a/BUILDING.txt b/BUILDING.txt index 0159b6803af4..c5a285533cf4 100644 --- a/BUILDING.txt +++ b/BUILDING.txt @@ -570,7 +570,7 @@ NOTE: Cobertura is licensed under GPL v2 with parts of it being under Where there is no benefit in running an absolute performance test as part of a standard test run, the test will be excluded by naming it - Tester*Performance.java. + Test*Performance.java. The relative tests are included as part of a standard test run however, where the assumptions made about host capabilities are not true (e.g. on diff --git a/java/org/apache/tomcat/util/net/LocalStrings.properties b/java/org/apache/tomcat/util/net/LocalStrings.properties index 720f07bdccc6..f433c46a8158 100644 --- a/java/org/apache/tomcat/util/net/LocalStrings.properties +++ b/java/org/apache/tomcat/util/net/LocalStrings.properties @@ -44,6 +44,7 @@ channel.nio.ssl.unexpectedStatusDuringWrap=Unexpected status [{0}] during handsh channel.nio.ssl.unwrapFail=Unable to unwrap data, invalid status [{0}] channel.nio.ssl.unwrapFailResize=Unable to unwrap data because buffer is too small, invalid status [{0}] channel.nio.ssl.wrapFail=Unable to wrap data, invalid status [{0}] +channel.nio.ssl.handshakeUnwrapBufferUnderflow=BUFFER_UNDERFLOW during handshake unwrap, more data needed from the network endpoint.accept.fail=Socket accept failed endpoint.alpn.fail=Failed to configure endpoint for ALPN using [{0}] diff --git a/java/org/apache/tomcat/util/net/SecureNioChannel.java b/java/org/apache/tomcat/util/net/SecureNioChannel.java index bb43c5c0989f..28c090d5c8af 100644 --- a/java/org/apache/tomcat/util/net/SecureNioChannel.java +++ b/java/org/apache/tomcat/util/net/SecureNioChannel.java @@ -492,6 +492,9 @@ protected SSLEngineResult handshakeUnwrap(boolean doread) throws IOException { // call unwrap getBufHandler().configureReadBufferForWrite(); result = sslEngine.unwrap(netInBuffer, getBufHandler().getReadBuffer()); + if (log.isDebugEnabled() && result.getStatus() == SSLEngineResult.Status.BUFFER_UNDERFLOW) { + log.debug(sm.getString("channel.nio.ssl.handshakeUnwrapBufferUnderflow")); + } /* * ByteBuffer.compact() is an optional method but netInBuffer is created from either ByteBuffer.allocate() * or ByteBuffer.allocateDirect() and the ByteBuffers returned by those methods do implement compact(). The diff --git a/test/org/apache/catalina/authenticator/TestSSLAuthenticator.java b/test/org/apache/catalina/authenticator/TestSSLAuthenticator.java new file mode 100644 index 000000000000..346d06add831 --- /dev/null +++ b/test/org/apache/catalina/authenticator/TestSSLAuthenticator.java @@ -0,0 +1,40 @@ +/* + * 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.authenticator; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.net.TesterSupport; + +public class TestSSLAuthenticator extends TomcatBaseTest { + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=65991 + @Test + public void testBindOnInitFalseNoNPE() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + TesterSupport.configureClientCertContext(tomcat); + Assert.assertTrue(tomcat.getConnector().setProperty("bindOnInit", "false")); + + tomcat.start(); + tomcat.stop(); + } +} diff --git a/test/org/apache/catalina/connector/TestValidateClientSessionId.java b/test/org/apache/catalina/connector/TestValidateClientSessionId.java new file mode 100644 index 000000000000..dc94fc3fc369 --- /dev/null +++ b/test/org/apache/catalina/connector/TestValidateClientSessionId.java @@ -0,0 +1,89 @@ +/* + * 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.connector; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestValidateClientSessionId extends TomcatBaseTest { + + @Test + public void testMaliciousSessionIdRejected() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + Tomcat.addServlet(ctx, "snoop", new SnoopServlet()); + ctx.addServletMappingDecoded("/", "snoop"); + + tomcat.start(); + + Map> reqHead = new HashMap<>(); + reqHead.put("Cookie", List.of("JSESSIONID=DUMMY_SESSION_ID")); + + ByteChunk res = new ByteChunk(); + getUrl("http://localhost:" + getPort() + "/?createSession=true", res, reqHead, null); + + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + + String actualSessionId = requestDesc.getRequestInfo("SESSION-ID"); + Assert.assertNotEquals("DUMMY_SESSION_ID", actualSessionId); + } + + @Test + public void testValidSessionIdAcceptedAcrossContexts() throws Exception { + Tomcat tomcat = getTomcatInstance(); + + Context ctx1 = tomcat.addContext("/app1", null); + ctx1.setSessionCookiePath("/"); + Tomcat.addServlet(ctx1, "snoop", new SnoopServlet()); + ctx1.addServletMappingDecoded("/", "snoop"); + + Context ctx2 = tomcat.addContext("/app2", null); + ctx2.setSessionCookiePath("/"); + Tomcat.addServlet(ctx2, "snoop", new SnoopServlet()); + ctx2.addServletMappingDecoded("/", "snoop"); + + tomcat.start(); + + ByteChunk res = new ByteChunk(); + Map> resHead = new HashMap<>(); + getUrl("http://localhost:" + getPort() + "/app1/?createSession=true", res, null, resHead); + + RequestDescriptor requestDesc = SnoopResult.parse(res.toString()); + String sessionId1 = requestDesc.getRequestInfo("SESSION-ID"); + + Map> reqHead = new HashMap<>(); + reqHead.put("Cookie", List.of("JSESSIONID=" + sessionId1)); + + getUrl("http://localhost:" + getPort() + "/app2/?createSession=true", res, reqHead, null); + + requestDesc = SnoopResult.parse(res.toString()); + String sessionId2 = requestDesc.getRequestInfo("SESSION-ID"); + Assert.assertEquals(sessionId1, sessionId2); + } + +} diff --git a/test/org/apache/tomcat/util/net/TestLargeClientHello.java b/test/org/apache/tomcat/util/net/TestLargeClientHello.java new file mode 100644 index 000000000000..43370f9c5450 --- /dev/null +++ b/test/org/apache/tomcat/util/net/TestLargeClientHello.java @@ -0,0 +1,86 @@ +/* + * 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.File; +import java.util.logging.Level; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Context; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERUTF8String; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; + +public class TestLargeClientHello extends TomcatBaseTest { + + // https://bz.apache.org/bugzilla/show_bug.cgi?id=67938 + @Test + public void testLargeClientHelloWithSessionResumption() throws Exception { + File keystoreFile = TesterKeystoreGenerator.generateKeystore("localhost", "tomcat", + new String[]{"localhost", "*.localhost"}, + (keyPair, certBuilder) -> { + JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils(); + certBuilder.addExtension(Extension.subjectKeyIdentifier, false, + extUtils.createSubjectKeyIdentifier(keyPair.getPublic())); + certBuilder.addExtension(Extension.authorityKeyIdentifier, false, + extUtils.createAuthorityKeyIdentifier(keyPair.getPublic())); + certBuilder.addExtension(Extension.basicConstraints, true, + new BasicConstraints(true)); + certBuilder.addExtension(Extension.keyUsage, false, + new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment)); + certBuilder.addExtension(new ASN1ObjectIdentifier("2.999"), false, + new DERUTF8String("x".repeat(16922))); + }); + + Tomcat tomcat = getTomcatInstance(); + + Context ctx = getProgrammaticRootContext(); + Tomcat.addServlet(ctx, "hello", new HelloWorldServlet()); + ctx.addServletMappingDecoded("/", "hello"); + + TesterSupport.initSsl(tomcat, keystoreFile.getAbsolutePath(), false); + + try (LogCapture logCapture = attachLogCapture(Level.FINE, "org.apache.tomcat.util.net.SecureNioChannel")) { + + tomcat.start(); + + SSLContext sc = SSLContext.getInstance(Constants.SSL_PROTO_TLSv1_3); + sc.init(null, new TrustManager[]{new TesterSupport.TrustAllCerts()}, null); + javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); + + String url = "https://localhost:" + getPort() + "/"; + Assert.assertTrue(getUrl(url).toString().contains("Hello World")); + Assert.assertTrue(getUrl(url).toString().contains("Hello World")); + + Assert.assertTrue(logCapture.containsText( + TomcatBaseTest.getKeyFromPropertiesFile("org.apache.tomcat.util.net", + "channel.nio.ssl.handshakeUnwrapBufferUnderflow"))); + } + + } +} diff --git a/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java b/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java new file mode 100644 index 000000000000..9fd4affde611 --- /dev/null +++ b/test/org/apache/tomcat/util/net/TesterKeystoreGenerator.java @@ -0,0 +1,103 @@ +/* + * 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.File; +import java.io.FileOutputStream; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Date; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +public final class TesterKeystoreGenerator { + + private TesterKeystoreGenerator() {} + + @FunctionalInterface + public interface CertificateExtensionsCustomizer { + void customize(KeyPair keyPair, X509v3CertificateBuilder certBuilder) + throws Exception; + } + + /** + * Generate a temporary JKS keystore containing a self-signed RSA certificate. + * + * @param cn the Common Name for the certificate subject + * @param alias the keystore alias for the key entry + * @param sanNames DNS Subject Alternative Names to include, or {@code null} for none + * @param customizer callback to add extensions to the certificate, or {@code null} for none. + * + * @return a temporary keystore file with password {@link TesterSupport#JKS_PASS} + * + * @throws Exception if certificate generation or keystore creation fails + */ + public static File generateKeystore(String cn, String alias, String[] sanNames, + CertificateExtensionsCustomizer customizer) throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(4096); + KeyPair keyPair = kpg.generateKeyPair(); + + X500Name subject = new X500Name("CN=" + cn); + BigInteger serial = BigInteger.valueOf(System.currentTimeMillis()); + long oneDay = 86400000L; + Date notBefore = new Date(System.currentTimeMillis() - oneDay); + Date notAfter = new Date(System.currentTimeMillis() + 365L * oneDay); + + X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(subject, serial, notBefore, notAfter, + subject, keyPair.getPublic()); + + if (sanNames != null && sanNames.length > 0) { + GeneralName[] generalNames = new GeneralName[sanNames.length]; + for (int i = 0; i < sanNames.length; i++) { + generalNames[i] = new GeneralName(GeneralName.dNSName, sanNames[i]); + } + certBuilder.addExtension(Extension.subjectAlternativeName, false, new GeneralNames(generalNames)); + } + + if (customizer != null) { + customizer.customize(keyPair, certBuilder); + } + + ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate()); + X509Certificate certificate = new JcaX509CertificateConverter().getCertificate(certBuilder.build(signer)); + + KeyStore ks = KeyStore.getInstance("JKS"); + ks.load(null, null); + ks.setKeyEntry(alias, keyPair.getPrivate(), TesterSupport.JKS_PASS.toCharArray(), new X509Certificate[] { certificate }); + + File keystoreFile = File.createTempFile("test-cert-", ".jks"); + keystoreFile.deleteOnExit(); + try (FileOutputStream fos = new FileOutputStream(keystoreFile)) { + ks.store(fos, TesterSupport.JKS_PASS.toCharArray()); + } + + return keystoreFile; + } +}