This is an automated email from the ASF dual-hosted git repository. tballison pushed a commit to branch improve-tls in repository https://gitbox.apache.org/repos/asf/tika.git
commit 0747aa22f9bdbf34987506d2a00f58e32abdaa1c Author: tallison <[email protected]> AuthorDate: Wed Jul 29 09:44:26 2026 -0400 improve tls --- .../org/apache/tika/pipes/grpc/TikaGrpcServer.java | 15 ++- ...PipesBiDirectionalStreamingIntegrationTest.java | 25 +++++ .../tika/pipes/grpc/TikaGrpcServerTlsTest.java | 102 +++++++++++++++++++++ .../apache/tika/server/core/TikaServerConfig.java | 1 + .../apache/tika/server/core/TikaServerProcess.java | 9 +- .../tika/server/core/TikaServerConfigTest.java | 72 +++++++++++++++ .../server/core/TikaServerProcessTlsGuardTest.java | 82 +++++++++++++++++ ...onfig-server-tls-client-auth-no-truststore.json | 15 +++ .../tika-config-server-tls-client-auth-valid.json | 18 ++++ .../tika-config-server-tls-missing-keystore.json | 14 +++ .../tika-config-server-tls-partial-truststore.json | 15 +++ 11 files changed, 363 insertions(+), 5 deletions(-) diff --git a/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServer.java b/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServer.java index 3d59800c8c..79f64ea539 100644 --- a/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServer.java +++ b/tika-grpc/src/main/java/org/apache/tika/pipes/grpc/TikaGrpcServer.java @@ -85,14 +85,21 @@ public class TikaGrpcServer { if (secure) { TlsServerCredentials.Builder channelCredBuilder = TlsServerCredentials.newBuilder(); channelCredBuilder.keyManager(certChain, privateKey, privateKeyPassword); - if (trustCertCollection != null && trustCertCollection.exists()) { - channelCredBuilder.trustManager(trustCertCollection); - if (clientAuthRequired) { - channelCredBuilder.clientAuth(TlsServerCredentials.ClientAuth.REQUIRE); + if (clientAuthRequired) { + if (trustCertCollection == null || !trustCertCollection.isFile() || !trustCertCollection.canRead()) { + throw new IllegalArgumentException("--client-auth-required is set but --trust-cert-collection is " + + "missing, not a file, or unreadable; refusing to start"); } + channelCredBuilder.trustManager(trustCertCollection); + channelCredBuilder.clientAuth(TlsServerCredentials.ClientAuth.REQUIRE); + } else if (trustCertCollection != null && trustCertCollection.exists()) { + channelCredBuilder.trustManager(trustCertCollection); } creds = channelCredBuilder.build(); } else { + if (clientAuthRequired) { + throw new IllegalArgumentException("--client-auth-required requires --secure; refusing to start"); + } creds = InsecureServerCredentials.create(); } if (tikaConfig == null) { diff --git a/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/PipesBiDirectionalStreamingIntegrationTest.java b/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/PipesBiDirectionalStreamingIntegrationTest.java index 508c672e73..77335fb381 100644 --- a/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/PipesBiDirectionalStreamingIntegrationTest.java +++ b/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/PipesBiDirectionalStreamingIntegrationTest.java @@ -36,6 +36,7 @@ import java.util.concurrent.atomic.AtomicInteger; import com.fasterxml.jackson.databind.ObjectMapper; import io.grpc.Grpc; import io.grpc.ManagedChannel; +import io.grpc.StatusRuntimeException; import io.grpc.TlsChannelCredentials; import io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.grpc.stub.StreamObserver; @@ -236,4 +237,28 @@ class PipesBiDirectionalStreamingIntegrationTest { Assertions.assertEquals(files.size(), numParsed.get()); Assertions.assertEquals(files.size(), result.size()); } + + @Test + void testClientAuthRequiredRejectsCertlessClient() throws Exception { + // Same running server as the other tests, but this channel presents no client + // certificate, so the call should fail at the handshake. + String target = InetAddress + .getByName("localhost") + .getHostAddress() + ":" + grpcPort; + TlsChannelCredentials.Builder channelCredBuilder = TlsChannelCredentials.newBuilder(); + channelCredBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE.getTrustManagers()); + ManagedChannel certlessChannel = Grpc + .newChannelBuilder(target, channelCredBuilder.build()) + .build(); + try { + TikaGrpc.TikaBlockingStub certlessStub = TikaGrpc.newBlockingStub(certlessChannel); + Assertions.assertThrows(StatusRuntimeException.class, () -> certlessStub.fetchAndParse(FetchAndParseRequest + .newBuilder() + .setFetcherId(httpFetcherId) + .setFetchKey(httpServerUrl + "/" + files.get(0)) + .build())); + } finally { + certlessChannel.shutdownNow(); + } + } } diff --git a/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/TikaGrpcServerTlsTest.java b/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/TikaGrpcServerTlsTest.java new file mode 100644 index 0000000000..d6edd326e7 --- /dev/null +++ b/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/TikaGrpcServerTlsTest.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.tika.pipes.grpc; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Covers the trust-cert-collection states that {@link TikaGrpcServer#start()} should refuse + * to start on when {@code --client-auth-required} is set: omitted, nonexistent, unreadable, + * and invalid/corrupt content, plus {@code --client-auth-required} without {@code --secure}. + */ +class TikaGrpcServerTlsTest { + private static final File CERT_CHAIN = Paths.get("src", "test", "resources", "certs", "server1.pem").toFile(); + private static final File PRIVATE_KEY = Paths.get("src", "test", "resources", "certs", "server1.key").toFile(); + private static final File VALID_TRUST_COLLECTION = Paths.get("src", "test", "resources", "certs", "ca.pem").toFile(); + + @Test + void clientAuthRequiredWithoutSecureRefusesToStart() { + TikaGrpcServer server = new TikaGrpcServer() + .setSecure(false) + .setClientAuthRequired(true); + assertThrows(IllegalArgumentException.class, server::start); + } + + @Test + void clientAuthRequiredWithOmittedTrustCollectionRefusesToStart() { + TikaGrpcServer server = new TikaGrpcServer() + .setSecure(true) + .setCertChain(CERT_CHAIN) + .setPrivateKey(PRIVATE_KEY) + .setClientAuthRequired(true); + // trustCertCollection intentionally left unset + assertThrows(IllegalArgumentException.class, server::start); + } + + @Test + void clientAuthRequiredWithNonexistentTrustCollectionRefusesToStart() { + TikaGrpcServer server = new TikaGrpcServer() + .setSecure(true) + .setCertChain(CERT_CHAIN) + .setPrivateKey(PRIVATE_KEY) + .setTrustCertCollection(new File("does-not-exist-" + System.nanoTime() + ".pem")) + .setClientAuthRequired(true); + assertThrows(IllegalArgumentException.class, server::start); + } + + @Test + void clientAuthRequiredWithUnreadableTrustCollectionRefusesToStart(@TempDir Path tempDir) throws Exception { + Path unreadable = tempDir.resolve("unreadable-ca.pem"); + Files.copy(VALID_TRUST_COLLECTION.toPath(), unreadable); + boolean changed = unreadable.toFile().setReadable(false, false); + Assumptions.assumeTrue(changed && !unreadable.toFile().canRead(), + "cannot simulate an unreadable file as the current user (likely running as root)"); + + TikaGrpcServer server = new TikaGrpcServer() + .setSecure(true) + .setCertChain(CERT_CHAIN) + .setPrivateKey(PRIVATE_KEY) + .setTrustCertCollection(unreadable.toFile()) + .setClientAuthRequired(true); + assertThrows(IllegalArgumentException.class, server::start); + } + + @Test + void clientAuthRequiredWithCorruptTrustCollectionRefusesToStart(@TempDir Path tempDir) throws Exception { + Path corrupt = tempDir.resolve("corrupt-ca.pem"); + Files.write(corrupt, "this is not a valid PEM certificate".getBytes(java.nio.charset.StandardCharsets.UTF_8)); + + TikaGrpcServer server = new TikaGrpcServer() + .setSecure(true) + .setCertChain(CERT_CHAIN) + .setPrivateKey(PRIVATE_KEY) + .setTrustCertCollection(corrupt.toFile()) + .setClientAuthRequired(true); + // Content validation happens deeper in grpc's TLS credential building, so this + // surfaces as a propagated exception rather than our explicit IllegalArgumentException. + assertThrows(Exception.class, server::start); + } +} diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerConfig.java b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerConfig.java index 14fd249468..d4b22726cd 100644 --- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerConfig.java +++ b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerConfig.java @@ -205,6 +205,7 @@ private long forkedProcessShutdownMillis = DEFAULT_FORKED_PROCESS_SHUTDOWN_MILLI "files and reach network resources."); } } + tlsConfig.checkInitialization(); } public String getHost() { diff --git a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java index d5d9d859a3..5826421f1d 100644 --- a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java +++ b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java @@ -240,7 +240,14 @@ public class TikaServerProcess { return details; } - private static TLSServerParameters getTlsParams(TlsConfig tlsConfig) throws GeneralSecurityException, IOException { + private static TLSServerParameters getTlsParams(TlsConfig tlsConfig) + throws GeneralSecurityException, IOException, TikaConfigException { + // Also checked in TlsConfig.checkInitialization() at config-load time; kept here too + // since this is where the TLS credentials are actually built. + if (tlsConfig.isClientAuthenticationRequired() && !tlsConfig.hasTrustStore()) { + throw new TikaConfigException( + "requiring client authentication, but no trust store has been specified"); + } KeyStoreType keyStore = new KeyStoreType(); keyStore.setType(tlsConfig.getKeyStoreType()); keyStore.setPassword(tlsConfig.getKeyStorePassword()); diff --git a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerConfigTest.java b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerConfigTest.java index 56b5ffd0cb..24510aecde 100644 --- a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerConfigTest.java +++ b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerConfigTest.java @@ -157,4 +157,76 @@ public class TikaServerConfigTest extends TikaTest { assertEquals("pass2", tlsConfig.getTrustStorePassword()); assertEquals("/something/or/other2", tlsConfig.getTrustStoreFile()); } + + // The four cases below exercise TLS config validation through the CommandLine-based + // TikaServerConfig.load() overload, rather than the Path-based overload testTlsConfig() + // above uses. + + @Test + public void testClientAuthRequiredWithoutTrustStoreRefusesToLoad() throws Exception { + CommandLineParser parser = new DefaultParser(); + Path path = getConfigPath(getClass(), "tika-config-server-tls-client-auth-no-truststore.json"); + CommandLine commandLine = parser.parse(new Options() + .addOption(Option + .builder("c") + .longOpt("config") + .hasArg() + .get()), new String[]{"-c", ProcessUtils.escapeCommandLine(path + .toAbsolutePath() + .toString())}); + TikaConfigException ex = assertThrows(TikaConfigException.class, + () -> TikaServerConfig.load(commandLine)); + assertContains("client authentication", ex.getMessage()); + assertContains("no trust store", ex.getMessage()); + } + + @Test + public void testPartialTrustStoreConfigRefusesToLoad() throws Exception { + CommandLineParser parser = new DefaultParser(); + Path path = getConfigPath(getClass(), "tika-config-server-tls-partial-truststore.json"); + CommandLine commandLine = parser.parse(new Options() + .addOption(Option + .builder("c") + .longOpt("config") + .hasArg() + .get()), new String[]{"-c", ProcessUtils.escapeCommandLine(path + .toAbsolutePath() + .toString())}); + TikaConfigException ex = assertThrows(TikaConfigException.class, + () -> TikaServerConfig.load(commandLine)); + assertContains("Partial truststore configuration", ex.getMessage()); + } + + @Test + public void testMissingKeyStoreFileRefusesToLoad() throws Exception { + CommandLineParser parser = new DefaultParser(); + Path path = getConfigPath(getClass(), "tika-config-server-tls-missing-keystore.json"); + CommandLine commandLine = parser.parse(new Options() + .addOption(Option + .builder("c") + .longOpt("config") + .hasArg() + .get()), new String[]{"-c", ProcessUtils.escapeCommandLine(path + .toAbsolutePath() + .toString())}); + TikaConfigException ex = assertThrows(TikaConfigException.class, + () -> TikaServerConfig.load(commandLine)); + assertContains("keyStoreFile does not exist", ex.getMessage()); + } + + @Test + public void testClientAuthRequiredWithValidTrustStoreLoadsSuccessfully() throws Exception { + CommandLineParser parser = new DefaultParser(); + Path path = getConfigPath(getClass(), "tika-config-server-tls-client-auth-valid.json"); + CommandLine commandLine = parser.parse(new Options() + .addOption(Option + .builder("c") + .longOpt("config") + .hasArg() + .get()), new String[]{"-c", ProcessUtils.escapeCommandLine(path + .toAbsolutePath() + .toString())}); + TikaServerConfig config = TikaServerConfig.load(commandLine); + assertTrue(config.getTlsConfig().isClientAuthenticationRequired()); + } } diff --git a/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerProcessTlsGuardTest.java b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerProcessTlsGuardTest.java new file mode 100644 index 0000000000..687783396d --- /dev/null +++ b/tika-server/tika-server-core/src/test/java/org/apache/tika/server/core/TikaServerProcessTlsGuardTest.java @@ -0,0 +1,82 @@ +/* + * 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.tika.server.core; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Paths; + +import org.apache.cxf.configuration.jsse.TLSServerParameters; +import org.junit.jupiter.api.Test; + +import org.apache.tika.exception.TikaConfigException; + +/** + * Exercises TikaServerProcess.getTlsParams() directly via reflection, independent of + * TikaServerConfig.load(), to confirm its own TLS config checks hold on their own. + */ +class TikaServerProcessTlsGuardTest { + + private static TLSServerParameters invokeGetTlsParams(TlsConfig tlsConfig) throws Throwable { + try { + Method m = TikaServerProcess.class.getDeclaredMethod("getTlsParams", TlsConfig.class); + m.setAccessible(true); + return (TLSServerParameters) m.invoke(null, tlsConfig); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } + + private static TlsConfig validKeyStoreOnlyConfig() { + TlsConfig tlsConfig = new TlsConfig(); + tlsConfig.setActive(true); + tlsConfig.setKeyStoreType("PKCS12"); + tlsConfig.setKeyStorePassword("tika-secret"); + tlsConfig.setKeyStoreFile(Paths + .get("src", "test", "resources", "ssl-keys", "tika-server-keystore.p12") + .toString()); + return tlsConfig; + } + + @Test + void getTlsParamsRefusesClientAuthRequiredWithoutTrustStore() throws Throwable { + TlsConfig tlsConfig = validKeyStoreOnlyConfig(); + tlsConfig.setClientAuthenticationRequired(true); + // trust store intentionally left unset + + TikaConfigException ex = assertThrows(TikaConfigException.class, + () -> invokeGetTlsParams(tlsConfig)); + assertTrue(ex.getMessage().contains("no trust store")); + } + + @Test + void getTlsParamsAllowsClientAuthRequiredWithTrustStore() throws Throwable { + TlsConfig tlsConfig = validKeyStoreOnlyConfig(); + tlsConfig.setTrustStoreType("PKCS12"); + tlsConfig.setTrustStorePassword("tika-secret"); + tlsConfig.setTrustStoreFile(Paths + .get("src", "test", "resources", "ssl-keys", "tika-server-truststore.p12") + .toString()); + tlsConfig.setClientAuthenticationRequired(true); + + TLSServerParameters params = invokeGetTlsParams(tlsConfig); + assertTrue(params.getClientAuthentication().isRequired()); + } +} diff --git a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-tls-client-auth-no-truststore.json b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-tls-client-auth-no-truststore.json new file mode 100644 index 0000000000..6a727f7c27 --- /dev/null +++ b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-tls-client-auth-no-truststore.json @@ -0,0 +1,15 @@ +{ + "server": { + "port": 9999, + "endpoints": [ + "status" + ], + "tlsConfig": { + "active": true, + "keyStoreType": "PKCS12", + "keyStorePassword": "tika-secret", + "keyStoreFile": "src/test/resources/ssl-keys/tika-server-keystore.p12", + "clientAuthenticationRequired": true + } + } +} diff --git a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-tls-client-auth-valid.json b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-tls-client-auth-valid.json new file mode 100644 index 0000000000..c054650c67 --- /dev/null +++ b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-tls-client-auth-valid.json @@ -0,0 +1,18 @@ +{ + "server": { + "port": 9999, + "endpoints": [ + "status" + ], + "tlsConfig": { + "active": true, + "keyStoreType": "PKCS12", + "keyStorePassword": "tika-secret", + "keyStoreFile": "src/test/resources/ssl-keys/tika-server-keystore.p12", + "trustStoreType": "PKCS12", + "trustStorePassword": "tika-secret", + "trustStoreFile": "src/test/resources/ssl-keys/tika-server-truststore.p12", + "clientAuthenticationRequired": true + } + } +} diff --git a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-tls-missing-keystore.json b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-tls-missing-keystore.json new file mode 100644 index 0000000000..8084bbfc23 --- /dev/null +++ b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-tls-missing-keystore.json @@ -0,0 +1,14 @@ +{ + "server": { + "port": 9999, + "endpoints": [ + "status" + ], + "tlsConfig": { + "active": true, + "keyStoreType": "PKCS12", + "keyStorePassword": "tika-secret", + "keyStoreFile": "src/test/resources/ssl-keys/does-not-exist.p12" + } + } +} diff --git a/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-tls-partial-truststore.json b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-tls-partial-truststore.json new file mode 100644 index 0000000000..f65eda2d1c --- /dev/null +++ b/tika-server/tika-server-core/src/test/resources/configs/tika-config-server-tls-partial-truststore.json @@ -0,0 +1,15 @@ +{ + "server": { + "port": 9999, + "endpoints": [ + "status" + ], + "tlsConfig": { + "active": true, + "keyStoreType": "PKCS12", + "keyStorePassword": "tika-secret", + "keyStoreFile": "src/test/resources/ssl-keys/tika-server-keystore.p12", + "trustStoreFile": "src/test/resources/ssl-keys/tika-server-truststore.p12" + } + } +}
