diff --git a/core/BUILD.bazel b/core/BUILD.bazel index c50e86a511c..60a08798d58 100644 --- a/core/BUILD.bazel +++ b/core/BUILD.bazel @@ -60,6 +60,7 @@ java_library( "@com_google_code_findbugs_jsr305//jar", "@com_google_guava_guava//jar", "@com_google_j2objc_j2objc_annotations//jar", + "@org_codehaus_mojo_animal_sniffer_annotations//jar", ], ) diff --git a/core/src/main/java/io/grpc/util/AdvancedTlsX509KeyManager.java b/core/src/main/java/io/grpc/util/AdvancedTlsX509KeyManager.java new file mode 100644 index 00000000000..adaa1e6e69a --- /dev/null +++ b/core/src/main/java/io/grpc/util/AdvancedTlsX509KeyManager.java @@ -0,0 +1,234 @@ +/* + * Copyright 2021 The gRPC Authors + * + * Licensed 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 io.grpc.util; + +import static com.google.common.base.Preconditions.checkNotNull; + +import io.grpc.ExperimentalApi; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.Socket; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedKeyManager; + +/** + * AdvancedTlsX509KeyManager is an {@code X509ExtendedKeyManager} that allows users to configure + * advanced TLS features, such as private key and certificate chain reloading, etc. + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8024") +public final class AdvancedTlsX509KeyManager extends X509ExtendedKeyManager { + private static final Logger log = Logger.getLogger(AdvancedTlsX509KeyManager.class.getName()); + + // The credential information sent to peers to prove our identity. + private volatile KeyInfo keyInfo; + + /** + * Constructs an AdvancedTlsX509KeyManager. + */ + public AdvancedTlsX509KeyManager() throws CertificateException { } + + @Override + public PrivateKey getPrivateKey(String alias) { + if (alias.equals("default")) { + return this.keyInfo.key; + } + return null; + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + if (alias.equals("default")) { + return Arrays.copyOf(this.keyInfo.certs, this.keyInfo.certs.length); + } + return null; + } + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return new String[] {"default"}; + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + return "default"; + } + + @Override + public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) { + return "default"; + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return new String[] {"default"}; + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return "default"; + } + + @Override + public String chooseEngineServerAlias(String keyType, Principal[] issuers, + SSLEngine engine) { + return "default"; + } + + /** + * Updates the current cached private key and cert chains. + * + * @param key the private key that is going to be used + * @param certs the certificate chain that is going to be used + */ + public void updateIdentityCredentials(PrivateKey key, X509Certificate[] certs) + throws CertificateException { + // TODO(ZhenLian): explore possibilities to do a crypto check here. + this.keyInfo = new KeyInfo(checkNotNull(key, "key"), checkNotNull(certs, "certs")); + } + + /** + * Schedules a {@code ScheduledExecutorService} to read private key and certificate chains from + * the local file paths periodically, and update the cached identity credentials if they are both + * updated. + * + * @param keyFile the file on disk holding the private key + * @param certFile the file on disk holding the certificate chain + * @param period the period between successive read-and-update executions + * @param unit the time unit of the initialDelay and period parameters + * @param executor the execute service we use to read and update the credentials + * @return an object that caller should close when the file refreshes are not needed + */ + public Closeable updateIdentityCredentialsFromFile(File keyFile, File certFile, + long period, TimeUnit unit, ScheduledExecutorService executor) { + final ScheduledFuture future = + executor.scheduleWithFixedDelay( + new LoadFilePathExecution(keyFile, certFile), 0, period, unit); + return new Closeable() { + @Override public void close() { + future.cancel(false); + } + }; + } + + private static class KeyInfo { + // The private key and the cert chain we will use to send to peers to prove our identity. + final PrivateKey key; + final X509Certificate[] certs; + + public KeyInfo(PrivateKey key, X509Certificate[] certs) { + this.key = key; + this.certs = certs; + } + } + + private class LoadFilePathExecution implements Runnable { + File keyFile; + File certFile; + long currentKeyTime; + long currentCertTime; + + public LoadFilePathExecution(File keyFile, File certFile) { + this.keyFile = keyFile; + this.certFile = certFile; + this.currentKeyTime = 0; + this.currentCertTime = 0; + } + + @Override + public void run() { + try { + UpdateResult newResult = readAndUpdate(this.keyFile, this.certFile, this.currentKeyTime, + this.currentCertTime); + if (newResult.success) { + this.currentKeyTime = newResult.keyTime; + this.currentCertTime = newResult.certTime; + } + } catch (CertificateException | IOException | NoSuchAlgorithmException + | InvalidKeySpecException e) { + log.log(Level.SEVERE, "Failed refreshing private key and certificate chain from files. " + + "Using previous ones", e); + } + } + } + + private static class UpdateResult { + boolean success; + long keyTime; + long certTime; + + public UpdateResult(boolean success, long keyTime, long certTime) { + this.success = success; + this.keyTime = keyTime; + this.certTime = certTime; + } + } + + /** + * Reads the private key and certificates specified in the path locations. Updates {@code key} and + * {@code cert} if both of their modified time changed since last read. + * + * @param keyFile the file on disk holding the private key + * @param certFile the file on disk holding the certificate chain + * @param oldKeyTime the time when the private key file is modified during last execution + * @param oldCertTime the time when the certificate chain file is modified during last execution + * @return the result of this update execution + */ + private UpdateResult readAndUpdate(File keyFile, File certFile, long oldKeyTime, long oldCertTime) + throws IOException, CertificateException, NoSuchAlgorithmException, InvalidKeySpecException { + long newKeyTime = keyFile.lastModified(); + long newCertTime = certFile.lastModified(); + // We only update when both the key and the certs are updated. + if (newKeyTime != oldKeyTime && newCertTime != oldCertTime) { + FileInputStream keyInputStream = new FileInputStream(keyFile); + try { + PrivateKey key = CertificateUtils.getPrivateKey(keyInputStream); + FileInputStream certInputStream = new FileInputStream(certFile); + try { + X509Certificate[] certs = CertificateUtils.getX509Certificates(certInputStream); + updateIdentityCredentials(key, certs); + return new UpdateResult(true, newKeyTime, newCertTime); + } finally { + certInputStream.close(); + } + } finally { + keyInputStream.close(); + } + } + return new UpdateResult(false, oldKeyTime, oldCertTime); + } + + /** + * Mainly used to avoid throwing IO Exceptions in java.io.Closeable. + */ + public interface Closeable extends java.io.Closeable { + @Override public void close(); + } +} + diff --git a/core/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java b/core/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java new file mode 100644 index 00000000000..ea8e74b1a9e --- /dev/null +++ b/core/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java @@ -0,0 +1,361 @@ +/* + * Copyright 2021 The gRPC Authors + * + * Licensed 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 io.grpc.util; + +import io.grpc.ExperimentalApi; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.Socket; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; +import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement; + +/** + * AdvancedTlsX509TrustManager is an {@code X509ExtendedTrustManager} that allows users to configure + * advanced TLS features, such as root certificate reloading, peer cert custom verification, etc. + * For Android users: this class is only supported in API level 24 and above. + */ +@ExperimentalApi("https://github.com/grpc/grpc-java/issues/8024") +@IgnoreJRERequirement +public final class AdvancedTlsX509TrustManager extends X509ExtendedTrustManager { + private static final Logger log = Logger.getLogger(AdvancedTlsX509TrustManager.class.getName()); + + private final Verification verification; + private final SslSocketAndEnginePeerVerifier socketAndEnginePeerVerifier; + + // The delegated trust manager used to perform traditional certificate verification. + private volatile X509ExtendedTrustManager delegateManager = null; + + private AdvancedTlsX509TrustManager(Verification verification, + SslSocketAndEnginePeerVerifier socketAndEnginePeerVerifier) throws CertificateException { + this.verification = verification; + this.socketAndEnginePeerVerifier = socketAndEnginePeerVerifier; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + throw new CertificateException( + "Not enough information to validate peer. SSLEngine or Socket required."); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + checkTrusted(chain, authType, null, socket, false); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + checkTrusted(chain, authType, engine, null, false); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) + throws CertificateException { + checkTrusted(chain, authType, engine, null, true); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + throw new CertificateException( + "Not enough information to validate peer. SSLEngine or Socket required."); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) + throws CertificateException { + checkTrusted(chain, authType, null, socket, true); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + if (this.delegateManager == null) { + return new X509Certificate[0]; + } + return this.delegateManager.getAcceptedIssuers(); + } + + /** + * Uses the default trust certificates stored on user's local system. + * After this is used, functions that will provide new credential + * data(e.g. updateTrustCredentials(), updateTrustCredentialsFromFile()) should not be called. + */ + public void useSystemDefaultTrustCerts() throws CertificateException, KeyStoreException, + NoSuchAlgorithmException { + // Passing a null value of KeyStore would make {@code TrustManagerFactory} attempt to use + // system-default trust CA certs. + this.delegateManager = createDelegateTrustManager(null); + } + + /** + * Updates the current cached trust certificates as well as the key store. + * + * @param trustCerts the trust certificates that are going to be used + */ + public void updateTrustCredentials(X509Certificate[] trustCerts) throws CertificateException, + KeyStoreException, NoSuchAlgorithmException, IOException { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null, null); + int i = 1; + for (X509Certificate cert: trustCerts) { + String alias = Integer.toString(i); + keyStore.setCertificateEntry(alias, cert); + i++; + } + X509ExtendedTrustManager newDelegateManager = createDelegateTrustManager(keyStore); + this.delegateManager = newDelegateManager; + } + + private static X509ExtendedTrustManager createDelegateTrustManager(KeyStore keyStore) + throws CertificateException, KeyStoreException, NoSuchAlgorithmException { + TrustManagerFactory tmf = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(keyStore); + X509ExtendedTrustManager delegateManager = null; + TrustManager[] tms = tmf.getTrustManagers(); + // Iterate over the returned trust managers, looking for an instance of X509TrustManager. + // If found, use that as the delegate trust manager. + for (int j = 0; j < tms.length; j++) { + if (tms[j] instanceof X509ExtendedTrustManager) { + delegateManager = (X509ExtendedTrustManager) tms[j]; + break; + } + } + if (delegateManager == null) { + throw new CertificateException( + "Failed to find X509ExtendedTrustManager with default TrustManager algorithm " + + TrustManagerFactory.getDefaultAlgorithm()); + } + return delegateManager; + } + + private void checkTrusted(X509Certificate[] chain, String authType, SSLEngine sslEngine, + Socket socket, boolean checkingServer) throws CertificateException { + if (chain == null || chain.length == 0) { + throw new IllegalArgumentException( + "Want certificate verification but got null or empty certificates"); + } + if (sslEngine == null && socket == null) { + throw new CertificateException( + "Not enough information to validate peer. SSLEngine or Socket required."); + } + if (this.verification != Verification.InsecurelySkipAllVerification) { + X509ExtendedTrustManager currentDelegateManager = this.delegateManager; + if (currentDelegateManager == null) { + throw new CertificateException("No trust roots configured"); + } + if (checkingServer) { + String algorithm = this.verification == Verification.CertificateAndHostNameVerification + ? "HTTPS" : ""; + if (sslEngine != null) { + SSLParameters sslParams = sslEngine.getSSLParameters(); + sslParams.setEndpointIdentificationAlgorithm(algorithm); + sslEngine.setSSLParameters(sslParams); + currentDelegateManager.checkServerTrusted(chain, authType, sslEngine); + } else { + if (!(socket instanceof SSLSocket)) { + throw new CertificateException("socket is not a type of SSLSocket"); + } + SSLSocket sslSocket = (SSLSocket)socket; + SSLParameters sslParams = sslSocket.getSSLParameters(); + sslParams.setEndpointIdentificationAlgorithm(algorithm); + sslSocket.setSSLParameters(sslParams); + currentDelegateManager.checkServerTrusted(chain, authType, sslSocket); + } + } else { + currentDelegateManager.checkClientTrusted(chain, authType, sslEngine); + } + } + // Perform the additional peer cert check. + if (socketAndEnginePeerVerifier != null) { + if (sslEngine != null) { + socketAndEnginePeerVerifier.verifyPeerCertificate(chain, authType, sslEngine); + } else { + socketAndEnginePeerVerifier.verifyPeerCertificate(chain, authType, socket); + } + } + } + + /** + * Schedules a {@code ScheduledExecutorService} to read trust certificates from a local file path + * periodically, and update the cached trust certs if there is an update. + * + * @param trustCertFile the file on disk holding the trust certificates + * @param period the period between successive read-and-update executions + * @param unit the time unit of the initialDelay and period parameters + * @param executor the execute service we use to read and update the credentials + * @return an object that caller should close when the file refreshes are not needed + */ + public Closeable updateTrustCredentialsFromFile(File trustCertFile, long period, TimeUnit unit, + ScheduledExecutorService executor) { + final ScheduledFuture future = + executor.scheduleWithFixedDelay( + new LoadFilePathExecution(trustCertFile), 0, period, unit); + return new Closeable() { + @Override public void close() { + future.cancel(false); + } + }; + } + + private class LoadFilePathExecution implements Runnable { + File file; + long currentTime; + + public LoadFilePathExecution(File file) { + this.file = file; + this.currentTime = 0; + } + + @Override + public void run() { + try { + this.currentTime = readAndUpdate(this.file, this.currentTime); + } catch (CertificateException | IOException | KeyStoreException + | NoSuchAlgorithmException e) { + log.log(Level.SEVERE, "Failed refreshing trust CAs from file. Using previous CAs", e); + } + } + } + + /** + * Reads the trust certificates specified in the path location, and update the key store if the + * modified time has changed since last read. + * + * @param trustCertFile the file on disk holding the trust certificates + * @param oldTime the time when the trust file is modified during last execution + * @return oldTime if failed or the modified time is not changed, otherwise the new modified time + */ + private long readAndUpdate(File trustCertFile, long oldTime) + throws CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException { + long newTime = trustCertFile.lastModified(); + if (newTime == oldTime) { + return oldTime; + } + FileInputStream inputStream = new FileInputStream(trustCertFile); + try { + X509Certificate[] certificates = CertificateUtils.getX509Certificates(inputStream); + updateTrustCredentials(certificates); + return newTime; + } finally { + inputStream.close(); + } + } + + // Mainly used to avoid throwing IO Exceptions in java.io.Closeable. + public interface Closeable extends java.io.Closeable { + @Override public void close(); + } + + public static Builder newBuilder() { + return new Builder(); + } + + // The verification mode when authenticating the peer certificate. + public enum Verification { + // This is the DEFAULT and RECOMMENDED mode for most applications. + // Setting this on the client side will do the certificate and hostname verification, while + // setting this on the server side will only do the certificate verification. + CertificateAndHostNameVerification, + // This SHOULD be chosen only when you know what the implication this will bring, and have a + // basic understanding about TLS. + // It SHOULD be accompanied with proper additional peer identity checks set through + // {@code PeerVerifier}(nit: why this @code not working?). Failing to do so will leave + // applications to MITM attack. + // Also note that this will only take effect if the underlying SDK implementation invokes + // checkClientTrusted/checkServerTrusted with the {@code SSLEngine} parameter while doing + // verification. + // Setting this on either side will only do the certificate verification. + CertificateOnlyVerification, + // Setting is very DANGEROUS. Please try to avoid this in a real production environment, unless + // you are a super advanced user intended to re-implement the whole verification logic on your + // own. A secure verification might include: + // 1. proper verification on the peer certificate chain + // 2. proper checks on the identity of the peer certificate + InsecurelySkipAllVerification, + } + + // Additional custom peer verification check. + // It will be used when checkClientTrusted/checkServerTrusted is called with the {@code Socket} or + // the {@code SSLEngine} parameter. + public interface SslSocketAndEnginePeerVerifier { + /** + * Verifies the peer certificate chain. For more information, please refer to + * {@code X509ExtendedTrustManager}. + * + * @param peerCertChain the certificate chain sent from the peer + * @param authType the key exchange algorithm used, e.g. "RSA", "DHE_DSS", etc + * @param socket the socket used for this connection. This parameter can be null, which + * indicates that implementations need not check the ssl parameters + */ + void verifyPeerCertificate(X509Certificate[] peerCertChain, String authType, Socket socket) + throws CertificateException; + + /** + * Verifies the peer certificate chain. For more information, please refer to + * {@code X509ExtendedTrustManager}. + * + * @param peerCertChain the certificate chain sent from the peer + * @param authType the key exchange algorithm used, e.g. "RSA", "DHE_DSS", etc + * @param engine the engine used for this connection. This parameter can be null, which + * indicates that implementations need not check the ssl parameters + */ + void verifyPeerCertificate(X509Certificate[] peerCertChain, String authType, SSLEngine engine) + throws CertificateException; + } + + public static final class Builder { + + private Verification verification = Verification.CertificateAndHostNameVerification; + private SslSocketAndEnginePeerVerifier socketAndEnginePeerVerifier; + + private Builder() {} + + public Builder setVerification(Verification verification) { + this.verification = verification; + return this; + } + + public Builder setSslSocketAndEnginePeerVerifier(SslSocketAndEnginePeerVerifier verifier) { + this.socketAndEnginePeerVerifier = verifier; + return this; + } + + public AdvancedTlsX509TrustManager build() throws CertificateException { + return new AdvancedTlsX509TrustManager(this.verification, this.socketAndEnginePeerVerifier); + } + } +} + diff --git a/netty/src/test/java/io/grpc/netty/AdvancedTlsTest.java b/netty/src/test/java/io/grpc/netty/AdvancedTlsTest.java new file mode 100644 index 00000000000..294fbcd4a9a --- /dev/null +++ b/netty/src/test/java/io/grpc/netty/AdvancedTlsTest.java @@ -0,0 +1,477 @@ +/* + * Copyright 2021 The gRPC Authors + * + * Licensed 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 io.grpc.netty; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import com.google.common.util.concurrent.MoreExecutors; +import io.grpc.ChannelCredentials; +import io.grpc.Grpc; +import io.grpc.ManagedChannel; +import io.grpc.Server; +import io.grpc.ServerCredentials; +import io.grpc.StatusRuntimeException; +import io.grpc.TlsChannelCredentials; +import io.grpc.TlsServerCredentials; +import io.grpc.TlsServerCredentials.ClientAuth; +import io.grpc.internal.testing.TestUtils; +import io.grpc.stub.StreamObserver; +import io.grpc.testing.protobuf.SimpleRequest; +import io.grpc.testing.protobuf.SimpleResponse; +import io.grpc.testing.protobuf.SimpleServiceGrpc; +import io.grpc.util.AdvancedTlsX509KeyManager; +import io.grpc.util.AdvancedTlsX509TrustManager; +import io.grpc.util.AdvancedTlsX509TrustManager.SslSocketAndEnginePeerVerifier; +import io.grpc.util.AdvancedTlsX509TrustManager.Verification; +import io.grpc.util.CertificateUtils; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.net.Socket; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import javax.net.ssl.SSLEngine; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class AdvancedTlsTest { + public static final String SERVER_0_KEY_FILE = "server0.key"; + public static final String SERVER_0_PEM_FILE = "server0.pem"; + public static final String CLIENT_0_KEY_FILE = "client.key"; + public static final String CLIENT_0_PEM_FILE = "client.pem"; + public static final String CA_PEM_FILE = "ca.pem"; + public static final String SERVER_BAD_KEY_FILE = "badserver.key"; + public static final String SERVER_BAD_PEM_FILE = "badserver.pem"; + + private ScheduledExecutorService executor; + private Server server; + private ManagedChannel channel; + + private File caCertFile; + private File serverKey0File; + private File serverCert0File; + private File clientKey0File; + private File clientCert0File; + private X509Certificate[] caCert; + private PrivateKey serverKey0; + private X509Certificate[] serverCert0; + private PrivateKey clientKey0; + private X509Certificate[] clientCert0; + private PrivateKey serverKeyBad; + private X509Certificate[] serverCertBad; + + @Rule + public ExpectedException exceptionRule = ExpectedException.none(); + + @Before + public void setUp() + throws NoSuchAlgorithmException, IOException, CertificateException, InvalidKeySpecException { + executor = Executors.newSingleThreadScheduledExecutor(); + caCertFile = TestUtils.loadCert(CA_PEM_FILE); + serverKey0File = TestUtils.loadCert(SERVER_0_KEY_FILE); + serverCert0File = TestUtils.loadCert(SERVER_0_PEM_FILE); + clientKey0File = TestUtils.loadCert(CLIENT_0_KEY_FILE); + clientCert0File = TestUtils.loadCert(CLIENT_0_PEM_FILE); + caCert = CertificateUtils.getX509Certificates( + TestUtils.class.getResourceAsStream("/certs/" + CA_PEM_FILE)); + serverKey0 = CertificateUtils.getPrivateKey( + TestUtils.class.getResourceAsStream("/certs/" + SERVER_0_KEY_FILE)); + serverCert0 = CertificateUtils.getX509Certificates( + TestUtils.class.getResourceAsStream("/certs/" + SERVER_0_PEM_FILE)); + clientKey0 = CertificateUtils.getPrivateKey( + TestUtils.class.getResourceAsStream("/certs/" + CLIENT_0_KEY_FILE)); + clientCert0 = CertificateUtils.getX509Certificates( + TestUtils.class.getResourceAsStream("/certs/" + CLIENT_0_PEM_FILE)); + serverKeyBad = CertificateUtils.getPrivateKey( + TestUtils.class.getResourceAsStream("/certs/" + SERVER_BAD_KEY_FILE)); + serverCertBad = CertificateUtils.getX509Certificates( + TestUtils.class.getResourceAsStream("/certs/" + SERVER_BAD_PEM_FILE)); + } + + @After + public void tearDown() { + if (server != null) { + server.shutdown(); + } + if (channel != null) { + channel.shutdown(); + } + MoreExecutors.shutdownAndAwaitTermination(executor, 5, TimeUnit.SECONDS); + } + + @Test + public void basicMutualTlsTest() throws Exception { + // Create & start a server. + ServerCredentials serverCredentials = TlsServerCredentials.newBuilder() + .keyManager(serverCert0File, serverKey0File).trustManager(caCertFile) + .clientAuth(ClientAuth.REQUIRE).build(); + server = Grpc.newServerBuilderForPort(0, serverCredentials).addService( + new SimpleServiceImpl()).build().start(); + // Create a client to connect. + ChannelCredentials channelCredentials = TlsChannelCredentials.newBuilder() + .keyManager(clientCert0File, clientKey0File).trustManager(caCertFile).build(); + channel = Grpc.newChannelBuilderForAddress("localhost", server.getPort(), channelCredentials) + .overrideAuthority("foo.test.google.com.au").build(); + // Start the connection. + try { + SimpleServiceGrpc.SimpleServiceBlockingStub client = + SimpleServiceGrpc.newBlockingStub(channel); + // Send an actual request, via the full GRPC & network stack, and check that a proper + // response comes back. + client.unaryRpc(SimpleRequest.getDefaultInstance()); + } catch (StatusRuntimeException e) { + e.printStackTrace(); + fail("Failed to make a connection"); + e.printStackTrace(); + } + } + + @Test + public void advancedTlsKeyManagerTrustManagerMutualTlsTest() throws Exception { + // Create a server with the key manager and trust manager. + AdvancedTlsX509KeyManager serverKeyManager = new AdvancedTlsX509KeyManager(); + serverKeyManager.updateIdentityCredentials(serverKey0, serverCert0); + AdvancedTlsX509TrustManager serverTrustManager = AdvancedTlsX509TrustManager.newBuilder() + .setVerification(Verification.CertificateOnlyVerification) + .build(); + serverTrustManager.updateTrustCredentials(caCert); + ServerCredentials serverCredentials = TlsServerCredentials.newBuilder() + .keyManager(serverKeyManager).trustManager(serverTrustManager) + .clientAuth(ClientAuth.REQUIRE).build(); + server = Grpc.newServerBuilderForPort(0, serverCredentials).addService( + new SimpleServiceImpl()).build().start(); + TimeUnit.SECONDS.sleep(5); + // Create a client with the key manager and trust manager. + AdvancedTlsX509KeyManager clientKeyManager = new AdvancedTlsX509KeyManager(); + clientKeyManager.updateIdentityCredentials(clientKey0, clientCert0); + AdvancedTlsX509TrustManager clientTrustManager = AdvancedTlsX509TrustManager.newBuilder() + .setVerification(Verification.CertificateAndHostNameVerification) + .build(); + clientTrustManager.updateTrustCredentials(caCert); + ChannelCredentials channelCredentials = TlsChannelCredentials.newBuilder() + .keyManager(clientKeyManager).trustManager(clientTrustManager).build(); + channel = Grpc.newChannelBuilderForAddress("localhost", server.getPort(), channelCredentials) + .overrideAuthority("foo.test.google.com.au").build(); + // Start the connection. + try { + SimpleServiceGrpc.SimpleServiceBlockingStub client = + SimpleServiceGrpc.newBlockingStub(channel); + client.unaryRpc(SimpleRequest.getDefaultInstance()); + } catch (StatusRuntimeException e) { + fail("Failed to make a connection"); + e.printStackTrace(); + } + } + + @Test + public void trustManagerCustomVerifierMutualTlsTest() throws Exception { + AdvancedTlsX509KeyManager serverKeyManager = new AdvancedTlsX509KeyManager(); + serverKeyManager.updateIdentityCredentials(serverKey0, serverCert0); + // Set server's custom verification based on the information of clientCert0. + AdvancedTlsX509TrustManager serverTrustManager = AdvancedTlsX509TrustManager.newBuilder() + .setVerification(Verification.CertificateOnlyVerification) + .setSslSocketAndEnginePeerVerifier( + new SslSocketAndEnginePeerVerifier() { + @Override + public void verifyPeerCertificate(X509Certificate[] peerCertChain, String authType, + Socket socket) throws CertificateException { + if (peerCertChain == null || peerCertChain.length == 0) { + throw new CertificateException("peerCertChain is empty"); + } + X509Certificate leafCert = peerCertChain[0]; + if (!leafCert.getSubjectDN().getName().contains("testclient")) { + throw new CertificateException("SslSocketAndEnginePeerVerifier failed"); + } + } + + @Override + public void verifyPeerCertificate(X509Certificate[] peerCertChain, String authType, + SSLEngine engine) throws CertificateException { + if (peerCertChain == null || peerCertChain.length == 0) { + throw new CertificateException("peerCertChain is empty"); + } + X509Certificate leafCert = peerCertChain[0]; + if (!leafCert.getSubjectDN().getName().contains("testclient")) { + throw new CertificateException("SslSocketAndEnginePeerVerifier failed"); + } + } + }) + .build(); + serverTrustManager.updateTrustCredentials(caCert); + ServerCredentials serverCredentials = TlsServerCredentials.newBuilder() + .keyManager(serverKeyManager).trustManager(serverTrustManager) + .clientAuth(ClientAuth.REQUIRE).build(); + server = Grpc.newServerBuilderForPort(0, serverCredentials).addService( + new SimpleServiceImpl()).build().start(); + TimeUnit.SECONDS.sleep(5); + + AdvancedTlsX509KeyManager clientKeyManager = new AdvancedTlsX509KeyManager(); + clientKeyManager.updateIdentityCredentials(clientKey0, clientCert0); + // Set client's custom verification based on the information of serverCert0. + AdvancedTlsX509TrustManager clientTrustManager = AdvancedTlsX509TrustManager.newBuilder() + .setVerification(Verification.CertificateOnlyVerification) + .setSslSocketAndEnginePeerVerifier( + new SslSocketAndEnginePeerVerifier() { + @Override + public void verifyPeerCertificate(X509Certificate[] peerCertChain, String authType, + Socket socket) throws CertificateException { + if (peerCertChain == null || peerCertChain.length == 0) { + throw new CertificateException("peerCertChain is empty"); + } + X509Certificate leafCert = peerCertChain[0]; + if (!leafCert.getSubjectDN().getName().contains("*.test.google.com.au")) { + throw new CertificateException("SslSocketAndEnginePeerVerifier failed"); + } + } + + @Override + public void verifyPeerCertificate(X509Certificate[] peerCertChain, String authType, + SSLEngine engine) throws CertificateException { + if (peerCertChain == null || peerCertChain.length == 0) { + throw new CertificateException("peerCertChain is empty"); + } + X509Certificate leafCert = peerCertChain[0]; + if (!leafCert.getSubjectDN().getName().contains("*.test.google.com.au")) { + throw new CertificateException("SslSocketAndEnginePeerVerifier failed"); + } + } + }) + .build(); + clientTrustManager.updateTrustCredentials(caCert); + ChannelCredentials channelCredentials = TlsChannelCredentials.newBuilder() + .keyManager(clientKeyManager).trustManager(clientTrustManager).build(); + channel = Grpc.newChannelBuilderForAddress( + "localhost", server.getPort(), channelCredentials).build(); + // Start the connection. + try { + SimpleServiceGrpc.SimpleServiceBlockingStub client = + SimpleServiceGrpc.newBlockingStub(channel); + client.unaryRpc(SimpleRequest.getDefaultInstance()); + } catch (StatusRuntimeException e) { + fail("Failed to make a connection"); + e.printStackTrace(); + } + } + + @Test + public void trustManagerInsecurelySkipAllTest() throws Exception { + AdvancedTlsX509KeyManager serverKeyManager = new AdvancedTlsX509KeyManager(); + // Even if we provide bad credentials for the server, the test should still pass, because we + // will configure the client to skip all checks later. + serverKeyManager.updateIdentityCredentials(serverKeyBad, serverCertBad); + AdvancedTlsX509TrustManager serverTrustManager = AdvancedTlsX509TrustManager.newBuilder() + .setVerification(Verification.CertificateOnlyVerification) + .setSslSocketAndEnginePeerVerifier( + new SslSocketAndEnginePeerVerifier() { + @Override + public void verifyPeerCertificate(X509Certificate[] peerCertChain, String authType, + Socket socket) throws CertificateException { } + + @Override + public void verifyPeerCertificate(X509Certificate[] peerCertChain, String authType, + SSLEngine engine) throws CertificateException { } + }) + .build(); + serverTrustManager.updateTrustCredentials(caCert); + ServerCredentials serverCredentials = TlsServerCredentials.newBuilder() + .keyManager(serverKeyManager).trustManager(serverTrustManager) + .clientAuth(ClientAuth.REQUIRE).build(); + server = Grpc.newServerBuilderForPort(0, serverCredentials).addService( + new SimpleServiceImpl()).build().start(); + TimeUnit.SECONDS.sleep(5); + + AdvancedTlsX509KeyManager clientKeyManager = new AdvancedTlsX509KeyManager(); + clientKeyManager.updateIdentityCredentials(clientKey0, clientCert0); + // Set the client to skip all checks, including traditional certificate verification. + // Note this is very dangerous in production environment - only do so if you are confident on + // what you are doing! + AdvancedTlsX509TrustManager clientTrustManager = AdvancedTlsX509TrustManager.newBuilder() + .setVerification(Verification.InsecurelySkipAllVerification) + .setSslSocketAndEnginePeerVerifier( + new SslSocketAndEnginePeerVerifier() { + @Override + public void verifyPeerCertificate(X509Certificate[] peerCertChain, String authType, + Socket socket) throws CertificateException { } + + @Override + public void verifyPeerCertificate(X509Certificate[] peerCertChain, String authType, + SSLEngine engine) throws CertificateException { } + }) + .build(); + clientTrustManager.updateTrustCredentials(caCert); + ChannelCredentials channelCredentials = TlsChannelCredentials.newBuilder() + .keyManager(clientKeyManager).trustManager(clientTrustManager).build(); + channel = Grpc.newChannelBuilderForAddress( + "localhost", server.getPort(), channelCredentials).build(); + // Start the connection. + try { + SimpleServiceGrpc.SimpleServiceBlockingStub client = + SimpleServiceGrpc.newBlockingStub(channel); + client.unaryRpc(SimpleRequest.getDefaultInstance()); + } catch (StatusRuntimeException e) { + fail("Failed to make a connection"); + e.printStackTrace(); + } + } + + @Test + public void onFileReloadingKeyManagerTrustManagerTest() throws Exception { + // Create & start a server. + AdvancedTlsX509KeyManager serverKeyManager = new AdvancedTlsX509KeyManager(); + Closeable serverKeyShutdown = serverKeyManager.updateIdentityCredentialsFromFile(serverKey0File, + serverCert0File, 100, TimeUnit.MILLISECONDS, executor); + AdvancedTlsX509TrustManager serverTrustManager = AdvancedTlsX509TrustManager.newBuilder() + .setVerification(Verification.CertificateOnlyVerification) + .build(); + Closeable serverTrustShutdown = serverTrustManager.updateTrustCredentialsFromFile(caCertFile, + 100, TimeUnit.MILLISECONDS, executor); + ServerCredentials serverCredentials = TlsServerCredentials.newBuilder() + .keyManager(serverKeyManager).trustManager(serverTrustManager) + .clientAuth(ClientAuth.REQUIRE).build(); + server = Grpc.newServerBuilderForPort(0, serverCredentials).addService( + new SimpleServiceImpl()).build().start(); + TimeUnit.SECONDS.sleep(5); + // Create a client to connect. + AdvancedTlsX509KeyManager clientKeyManager = new AdvancedTlsX509KeyManager(); + Closeable clientKeyShutdown = clientKeyManager.updateIdentityCredentialsFromFile(clientKey0File, + clientCert0File,100, TimeUnit.MILLISECONDS, executor); + AdvancedTlsX509TrustManager clientTrustManager = AdvancedTlsX509TrustManager.newBuilder() + .setVerification(Verification.CertificateAndHostNameVerification) + .build(); + Closeable clientTrustShutdown = clientTrustManager.updateTrustCredentialsFromFile(caCertFile, + 100, TimeUnit.MILLISECONDS, executor); + ChannelCredentials channelCredentials = TlsChannelCredentials.newBuilder() + .keyManager(clientKeyManager).trustManager(clientTrustManager).build(); + channel = Grpc.newChannelBuilderForAddress("localhost", server.getPort(), channelCredentials) + .overrideAuthority("foo.test.google.com.au").build(); + // Start the connection. + try { + SimpleServiceGrpc.SimpleServiceBlockingStub client = + SimpleServiceGrpc.newBlockingStub(channel); + // Send an actual request, via the full GRPC & network stack, and check that a proper + // response comes back. + client.unaryRpc(SimpleRequest.getDefaultInstance()); + } catch (StatusRuntimeException e) { + e.printStackTrace(); + fail("Find error: " + e.getMessage()); + } + // Clean up. + serverKeyShutdown.close(); + serverTrustShutdown.close(); + clientKeyShutdown.close(); + clientTrustShutdown.close(); + } + + @Test + public void keyManagerAliasesTest() throws Exception { + AdvancedTlsX509KeyManager km = new AdvancedTlsX509KeyManager(); + assertArrayEquals( + new String[] {"default"}, km.getClientAliases("", null)); + assertEquals( + "default", km.chooseClientAlias(new String[] {"default"}, null, null)); + assertArrayEquals( + new String[] {"default"}, km.getServerAliases("", null)); + assertEquals( + "default", km.chooseServerAlias("default", null, null)); + } + + @Test + public void trustManagerCheckTrustedWithSocketTest() throws Exception { + AdvancedTlsX509TrustManager tm = AdvancedTlsX509TrustManager.newBuilder() + .setVerification(Verification.InsecurelySkipAllVerification).build(); + tm.updateTrustCredentials(caCert); + tm.checkClientTrusted(serverCert0, "RSA", new Socket()); + tm.useSystemDefaultTrustCerts(); + tm.checkServerTrusted(clientCert0, "RSA", new Socket()); + } + + @Test + public void trustManagerCheckClientTrustedWithoutParameterTest() throws Exception { + exceptionRule.expect(CertificateException.class); + exceptionRule.expectMessage( + "Not enough information to validate peer. SSLEngine or Socket required."); + AdvancedTlsX509TrustManager tm = AdvancedTlsX509TrustManager.newBuilder() + .setVerification(Verification.InsecurelySkipAllVerification).build(); + tm.checkClientTrusted(serverCert0, "RSA"); + } + + @Test + public void trustManagerCheckServerTrustedWithoutParameterTest() throws Exception { + exceptionRule.expect(CertificateException.class); + exceptionRule.expectMessage( + "Not enough information to validate peer. SSLEngine or Socket required."); + AdvancedTlsX509TrustManager tm = AdvancedTlsX509TrustManager.newBuilder() + .setVerification(Verification.InsecurelySkipAllVerification).build(); + tm.checkServerTrusted(serverCert0, "RSA"); + } + + @Test + public void trustManagerEmptyChainTest() throws Exception { + exceptionRule.expect(IllegalArgumentException.class); + exceptionRule.expectMessage( + "Want certificate verification but got null or empty certificates"); + AdvancedTlsX509TrustManager tm = AdvancedTlsX509TrustManager.newBuilder() + .setVerification(Verification.CertificateOnlyVerification) + .build(); + tm.updateTrustCredentials(caCert); + tm.checkClientTrusted(null, "RSA", (SSLEngine) null); + } + + @Test + public void trustManagerBadCustomVerificationTest() throws Exception { + exceptionRule.expect(CertificateException.class); + exceptionRule.expectMessage("Bad Custom Verification"); + AdvancedTlsX509TrustManager tm = AdvancedTlsX509TrustManager.newBuilder() + .setVerification(Verification.CertificateOnlyVerification) + .setSslSocketAndEnginePeerVerifier( + new SslSocketAndEnginePeerVerifier() { + @Override + public void verifyPeerCertificate(X509Certificate[] peerCertChain, String authType, + Socket socket) throws CertificateException { + throw new CertificateException("Bad Custom Verification"); + } + + @Override + public void verifyPeerCertificate(X509Certificate[] peerCertChain, String authType, + SSLEngine engine) throws CertificateException { + throw new CertificateException("Bad Custom Verification"); + } + }).build(); + tm.updateTrustCredentials(caCert); + tm.checkClientTrusted(serverCert0, "RSA", new Socket()); + } + + private static class SimpleServiceImpl extends SimpleServiceGrpc.SimpleServiceImplBase { + @Override + public void unaryRpc(SimpleRequest req, StreamObserver respOb) { + respOb.onNext(SimpleResponse.getDefaultInstance()); + respOb.onCompleted(); + } + } +}