>From Michael Blow <[email protected]>: Michael Blow has uploaded this change for review. ( https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/21349?usp=email )
Change subject: [NO ISSUE][HYR][NET] Add TLS peer-certificate revocation hook ...................................................................... [NO ISSUE][HYR][NET] Add TLS peer-certificate revocation hook - user model changes: no - storage format changes: no - interface changes: yes (INetworkSecurityConfig) Details: - Introduce ICertificateRevocationChecker (hyracks-api), a generic hook invoked after PKIX path validation with the verified chain and authType, so embedders can reject revoked peer certificates without coupling hyracks to their auth layer. - Add INetworkSecurityConfig#getCertificateRevocationChecker(), a default (empty) accessor; existing configs are unaffected. - Add RevocationCheckingTrustManager (hyracks-ipc), wrapping a delegate X509ExtendedTrustManager and consulting the checker in every checkClientTrusted/checkServerTrusted overload, enforcing revocation for both inbound and outbound (node-to-node) connections. - NetworkSecurityManager wraps the PKIX trust managers with the checker when one is supplied by the config. - Add RevocationCheckingTrustManagerTest covering delegate-then-checker ordering, exception propagation, and PKIX-failure short-circuiting. Ext-ref: MB-72053 Co-Authored-By: Claude Opus 4.8 via Claude-UI <[email protected]> Change-Id: I45c77d77e7ab2a0e5d827a6e8ee744d38b914a89 --- A hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/ICertificateRevocationChecker.java M hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/INetworkSecurityConfig.java M hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/NetworkSecurityManager.java A hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/RevocationCheckingTrustManager.java A hyracks-fullstack/hyracks/hyracks-ipc/src/test/java/org/apache/hyracks/ipc/security/RevocationCheckingTrustManagerTest.java 5 files changed, 354 insertions(+), 1 deletion(-) git pull ssh://asterix-gerrit.ics.uci.edu:29418/asterixdb refs/changes/49/21349/1 diff --git a/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/ICertificateRevocationChecker.java b/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/ICertificateRevocationChecker.java new file mode 100644 index 0000000..c3696ff --- /dev/null +++ b/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/ICertificateRevocationChecker.java @@ -0,0 +1,42 @@ +/* + * 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.hyracks.api.network; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +// import org.apache.hyracks.util.annotations.AiProvenance; // TODO(helios): uncomment with @AiProvenance below (absent on this branch) + +/** + * Hook invoked during the TLS handshake (after standard PKIX path validation) to check a peer certificate chain for + * revocation. Implementations are provided by the embedding application via + * {@link INetworkSecurityConfig#getCertificateRevocationChecker()} and are kept free of any application-specific types + * so {@code hyracks-ipc} stays decoupled from the auth layer. + */ +// @AiProvenance(agent = AiProvenance.Agent.CLAUDE_OPUS_4_8, tool = AiProvenance.Tool.CLAUDE_UI, contributionKind = AiProvenance.ContributionKind.GENERATED) +@FunctionalInterface +public interface ICertificateRevocationChecker { + + /** + * @param chain the peer certificate chain that has already passed PKIX validation (end-entity first) + * @param authType the key-exchange / authentication type, as passed to the delegate {@code X509TrustManager} + * @throws CertificateException if the connection should be rejected on revocation grounds + */ + void checkRevocation(X509Certificate[] chain, String authType) throws CertificateException; +} diff --git a/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/INetworkSecurityConfig.java b/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/INetworkSecurityConfig.java index 2e00e4d..cd993ac 100644 --- a/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/INetworkSecurityConfig.java +++ b/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/network/INetworkSecurityConfig.java @@ -24,6 +24,7 @@ import java.security.KeyStore; import java.util.Optional; +// import org.apache.hyracks.util.annotations.AiProvenance; // TODO(helios): uncomment with @AiProvenance below (absent on this branch) import io.netty.handler.ssl.ClientAuth; public interface INetworkSecurityConfig extends Serializable { @@ -109,4 +110,15 @@ * @return the optional bind address */ Optional<InetAddress> getRMIBindAddress(); + + /** + * An optional hook to check peer certificates for revocation during the TLS handshake (after PKIX validation). + * When present, it is applied to both inbound and outbound node-to-node connections. + * + * @return the revocation checker, or {@link Optional#empty()} to disable revocation checking + */ + // @AiProvenance(agent = AiProvenance.Agent.CLAUDE_OPUS_4_8, tool = AiProvenance.Tool.CLAUDE_UI, contributionKind = AiProvenance.ContributionKind.GENERATED) + default Optional<ICertificateRevocationChecker> getCertificateRevocationChecker() { + return Optional.empty(); + } } diff --git a/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/NetworkSecurityManager.java b/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/NetworkSecurityManager.java index d8f5cff..519c948 100644 --- a/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/NetworkSecurityManager.java +++ b/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/NetworkSecurityManager.java @@ -22,13 +22,18 @@ import java.io.FileInputStream; import java.security.KeyStore; import java.security.SecureRandom; +import java.util.Optional; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; +import org.apache.hyracks.api.network.ICertificateRevocationChecker; import org.apache.hyracks.api.network.INetworkSecurityConfig; +// import org.apache.hyracks.util.annotations.AiProvenance; // TODO(helios): uncomment with @AiProvenance below (absent on this branch) import org.apache.hyracks.api.network.INetworkSecurityManager; import org.apache.hyracks.api.network.ISocketChannelFactory; import org.apache.hyracks.ipc.sockets.PlainSocketChannelFactory; @@ -104,8 +109,19 @@ trustStore = loadKeyStoreFromFile(password, config.getTrustStoreFile()); } trustManagerFactory.init(trustStore); + final TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + // @AiProvenance(agent = AiProvenance.Agent.CLAUDE_OPUS_4_8, tool = AiProvenance.Tool.CLAUDE_UI, contributionKind = AiProvenance.ContributionKind.GENERATED) + final Optional<ICertificateRevocationChecker> revocationChecker = config.getCertificateRevocationChecker(); + if (revocationChecker.isPresent()) { + for (int i = 0; i < trustManagers.length; i++) { + if (trustManagers[i] instanceof X509ExtendedTrustManager) { + trustManagers[i] = new RevocationCheckingTrustManager( + (X509ExtendedTrustManager) trustManagers[i], revocationChecker.get()); + } + } + } SSLContext ctx = SSLContext.getInstance(TLS_VERSION); - ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom()); + ctx.init(keyManagerFactory.getKeyManagers(), trustManagers, new SecureRandom()); return ctx; } catch (Exception ex) { throw new IllegalStateException("Failed to create SSLEngine", ex); diff --git a/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/RevocationCheckingTrustManager.java b/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/RevocationCheckingTrustManager.java new file mode 100644 index 0000000..2aff1c8 --- /dev/null +++ b/hyracks-fullstack/hyracks/hyracks-ipc/src/main/java/org/apache/hyracks/ipc/security/RevocationCheckingTrustManager.java @@ -0,0 +1,93 @@ +/* + * 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.hyracks.ipc.security; + +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; + +import org.apache.hyracks.api.network.ICertificateRevocationChecker; + +// import org.apache.hyracks.util.annotations.AiProvenance; // TODO(helios): uncomment with @AiProvenance below (absent on this branch) + +/** + * Wraps a delegate {@link X509ExtendedTrustManager} so that, after the delegate performs standard PKIX path validation, + * an {@link ICertificateRevocationChecker} is consulted to reject revoked peer certificates. Both + * {@code checkClientTrusted} and {@code checkServerTrusted} are intercepted, so revocation is enforced for both + * inbound and outbound connections (mirroring a {@code VerifyPeerCertificate} callback). + */ +// @AiProvenance(agent = AiProvenance.Agent.CLAUDE_OPUS_4_8, tool = AiProvenance.Tool.CLAUDE_UI, contributionKind = AiProvenance.ContributionKind.GENERATED) +public class RevocationCheckingTrustManager extends X509ExtendedTrustManager { + + private final X509ExtendedTrustManager delegate; + private final ICertificateRevocationChecker checker; + + public RevocationCheckingTrustManager(X509ExtendedTrustManager delegate, ICertificateRevocationChecker checker) { + this.delegate = delegate; + this.checker = checker; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + delegate.checkClientTrusted(chain, authType); + checker.checkRevocation(chain, authType); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + delegate.checkClientTrusted(chain, authType, socket); + checker.checkRevocation(chain, authType); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + delegate.checkClientTrusted(chain, authType, engine); + checker.checkRevocation(chain, authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + delegate.checkServerTrusted(chain, authType); + checker.checkRevocation(chain, authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + delegate.checkServerTrusted(chain, authType, socket); + checker.checkRevocation(chain, authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + delegate.checkServerTrusted(chain, authType, engine); + checker.checkRevocation(chain, authType); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return delegate.getAcceptedIssuers(); + } +} diff --git a/hyracks-fullstack/hyracks/hyracks-ipc/src/test/java/org/apache/hyracks/ipc/security/RevocationCheckingTrustManagerTest.java b/hyracks-fullstack/hyracks/hyracks-ipc/src/test/java/org/apache/hyracks/ipc/security/RevocationCheckingTrustManagerTest.java new file mode 100644 index 0000000..6b51f94 --- /dev/null +++ b/hyracks-fullstack/hyracks/hyracks-ipc/src/test/java/org/apache/hyracks/ipc/security/RevocationCheckingTrustManagerTest.java @@ -0,0 +1,190 @@ +/* + * 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.hyracks.ipc.security; + +import java.net.Socket; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; + +import org.apache.hyracks.api.network.ICertificateRevocationChecker; +// import org.apache.hyracks.util.annotations.AiProvenance; // TODO(helios): uncomment with @AiProvenance below (absent on this branch) +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +// @AiProvenance(agent = AiProvenance.Agent.CLAUDE_OPUS_4_8, tool = AiProvenance.Tool.CLAUDE_UI, contributionKind = AiProvenance.ContributionKind.TEST_GENERATED) +public class RevocationCheckingTrustManagerTest { + + private static final X509Certificate[] CHAIN = new X509Certificate[0]; + private static final String AUTH_TYPE = "RSA"; + + private RecordingTrustManager delegate; + private RecordingChecker checker; + private RevocationCheckingTrustManager tm; + + @Before + public void setup() { + delegate = new RecordingTrustManager(); + checker = new RecordingChecker(); + tm = new RevocationCheckingTrustManager(delegate, checker); + } + + // -- success path: delegate (PKIX) runs first, then the revocation checker, for every overload -- + + @Test + public void checkClientTrusted_callsDelegateThenChecker() throws Exception { + tm.checkClientTrusted(CHAIN, AUTH_TYPE); + assertDelegateThenChecker("checkClientTrusted"); + } + + @Test + public void checkClientTrusted_socket_callsDelegateThenChecker() throws Exception { + tm.checkClientTrusted(CHAIN, AUTH_TYPE, (Socket) null); + assertDelegateThenChecker("checkClientTrusted"); + } + + @Test + public void checkClientTrusted_engine_callsDelegateThenChecker() throws Exception { + tm.checkClientTrusted(CHAIN, AUTH_TYPE, (SSLEngine) null); + assertDelegateThenChecker("checkClientTrusted"); + } + + @Test + public void checkServerTrusted_callsDelegateThenChecker() throws Exception { + tm.checkServerTrusted(CHAIN, AUTH_TYPE); + assertDelegateThenChecker("checkServerTrusted"); + } + + @Test + public void checkServerTrusted_socket_callsDelegateThenChecker() throws Exception { + tm.checkServerTrusted(CHAIN, AUTH_TYPE, (Socket) null); + assertDelegateThenChecker("checkServerTrusted"); + } + + @Test + public void checkServerTrusted_engine_callsDelegateThenChecker() throws Exception { + tm.checkServerTrusted(CHAIN, AUTH_TYPE, (SSLEngine) null); + assertDelegateThenChecker("checkServerTrusted"); + } + + // -- a revoked cert (checker throws) aborts the handshake -- + + @Test + public void checkerException_propagates() { + checker.toThrow = new CertificateException("revoked"); + Assert.assertThrows(CertificateException.class, () -> tm.checkClientTrusted(CHAIN, AUTH_TYPE)); + Assert.assertThrows(CertificateException.class, () -> tm.checkServerTrusted(CHAIN, AUTH_TYPE)); + } + + // -- if PKIX validation fails, the checker is never consulted -- + + @Test + public void delegateException_shortCircuitsChecker() { + delegate.toThrow = new CertificateException("not trusted"); + Assert.assertThrows(CertificateException.class, () -> tm.checkClientTrusted(CHAIN, AUTH_TYPE)); + Assert.assertEquals(0, checker.calls); + } + + @Test + public void getAcceptedIssuers_delegates() { + Assert.assertSame(RecordingTrustManager.ISSUERS, tm.getAcceptedIssuers()); + } + + private void assertDelegateThenChecker(String expectedDelegateMethod) { + Assert.assertEquals("delegate should be called exactly once", 1, delegate.calls.size()); + Assert.assertEquals(expectedDelegateMethod, delegate.calls.get(0)); + Assert.assertEquals("checker should be called exactly once", 1, checker.calls); + Assert.assertSame("checker must receive the same chain", CHAIN, checker.lastChain); + Assert.assertEquals(AUTH_TYPE, checker.lastAuthType); + } + + private static final class RecordingChecker implements ICertificateRevocationChecker { + int calls; + X509Certificate[] lastChain; + String lastAuthType; + CertificateException toThrow; + + @Override + public void checkRevocation(X509Certificate[] chain, String authType) throws CertificateException { + calls++; + lastChain = chain; + lastAuthType = authType; + if (toThrow != null) { + throw toThrow; + } + } + } + + private static final class RecordingTrustManager extends X509ExtendedTrustManager { + static final X509Certificate[] ISSUERS = new X509Certificate[0]; + final List<String> calls = new ArrayList<>(); + CertificateException toThrow; + + private void record(String method) throws CertificateException { + calls.add(method); + if (toThrow != null) { + throw toThrow; + } + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + record("checkClientTrusted"); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + record("checkClientTrusted"); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + record("checkClientTrusted"); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + record("checkServerTrusted"); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + record("checkServerTrusted"); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + record("checkServerTrusted"); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return ISSUERS; + } + } +} -- To view, visit https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/21349?usp=email To unsubscribe, or for help writing mail filters, visit https://asterix-gerrit.ics.uci.edu/settings?usp=email Gerrit-MessageType: newchange Gerrit-Project: asterixdb Gerrit-Branch: morpheus Gerrit-Change-Id: I45c77d77e7ab2a0e5d827a6e8ee744d38b914a89 Gerrit-Change-Number: 21349 Gerrit-PatchSet: 1 Gerrit-Owner: Michael Blow <[email protected]>
