diff --git a/google-api-client/src/main/java/com/google/api/client/googleapis/apache/v2/GoogleApacheHttpTransport.java b/google-api-client/src/main/java/com/google/api/client/googleapis/apache/v2/GoogleApacheHttpTransport.java
index b5f91f6ed..f5f41fddf 100644
--- a/google-api-client/src/main/java/com/google/api/client/googleapis/apache/v2/GoogleApacheHttpTransport.java
+++ b/google-api-client/src/main/java/com/google/api/client/googleapis/apache/v2/GoogleApacheHttpTransport.java
@@ -15,7 +15,11 @@
package com.google.api.client.googleapis.apache.v2;
import com.google.api.client.googleapis.GoogleUtils;
+import com.google.api.client.googleapis.mtls.MtlsProvider;
+import com.google.api.client.googleapis.mtls.MtlsUtils;
+import com.google.api.client.googleapis.util.Utils;
import com.google.api.client.http.apache.v2.ApacheHttpTransport;
+import com.google.api.client.util.Beta;
import com.google.api.client.util.SslUtils;
import java.io.IOException;
import java.net.ProxySelector;
@@ -24,7 +28,6 @@
import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import org.apache.http.client.HttpClient;
-import org.apache.http.config.SocketConfig;
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.HttpClientBuilder;
@@ -39,11 +42,35 @@
public final class GoogleApacheHttpTransport {
/**
- * Returns a new instance of {@link ApacheHttpTransport} that uses
- * {@link GoogleUtils#getCertificateTrustStore()} for the trusted certificates.
+ * Returns a new instance of {@link ApacheHttpTransport} that uses {@link
+ * GoogleUtils#getCertificateTrustStore()} for the trusted certificates. If
+ * `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "true", and the default
+ * client certificate key store from {@link Utils#loadDefaultMtlsKeyStore()} is not null, then the
+ * transport uses the default client certificate and is mutual TLS.
*/
- public static ApacheHttpTransport newTrustedTransport() throws GeneralSecurityException,
- IOException {
+ public static ApacheHttpTransport newTrustedTransport()
+ throws GeneralSecurityException, IOException {
+ return newTrustedTransport(MtlsUtils.getDefaultMtlsProvider());
+ }
+
+ /**
+ * {@link Beta}
+ * Returns a new instance of {@link ApacheHttpTransport} that uses {@link
+ * GoogleUtils#getCertificateTrustStore()} for the trusted certificates. mtlsProvider can be used
+ * to configure mutual TLS for the transport.
+ *
+ * @param mtlsProvider MtlsProvider to configure mutual TLS for the transport
+ */
+ @Beta
+ public static ApacheHttpTransport newTrustedTransport(MtlsProvider mtlsProvider)
+ throws GeneralSecurityException, IOException {
+ KeyStore mtlsKeyStore = null;
+ String mtlsKeyStorePassword = null;
+ if (mtlsProvider.useMtlsClientCertificate()) {
+ mtlsKeyStore = mtlsProvider.getKeyStore();
+ mtlsKeyStorePassword = mtlsProvider.getKeyStorePassword();
+ }
+
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager(-1, TimeUnit.MILLISECONDS);
@@ -53,22 +80,35 @@ public static ApacheHttpTransport newTrustedTransport() throws GeneralSecurityEx
// Use the included trust store
KeyStore trustStore = GoogleUtils.getCertificateTrustStore();
SSLContext sslContext = SslUtils.getTlsSslContext();
- SslUtils.initSslContext(sslContext, trustStore, SslUtils.getPkixTrustManagerFactory());
+
+ boolean isMtls = false;
+ if (mtlsKeyStore != null && mtlsKeyStorePassword != null) {
+ isMtls = true;
+ SslUtils.initSslContext(
+ sslContext,
+ trustStore,
+ SslUtils.getPkixTrustManagerFactory(),
+ mtlsKeyStore,
+ mtlsKeyStorePassword,
+ SslUtils.getDefaultKeyManagerFactory());
+ } else {
+ SslUtils.initSslContext(sslContext, trustStore, SslUtils.getPkixTrustManagerFactory());
+ }
LayeredConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext);
- HttpClient client = HttpClientBuilder.create()
- .useSystemProperties()
- .setSSLSocketFactory(socketFactory)
- .setMaxConnTotal(200)
- .setMaxConnPerRoute(20)
- .setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault()))
- .setConnectionManager(connectionManager)
- .disableRedirectHandling()
- .disableAutomaticRetries()
- .build();
- return new ApacheHttpTransport(client);
+ HttpClient client =
+ HttpClientBuilder.create()
+ .useSystemProperties()
+ .setSSLSocketFactory(socketFactory)
+ .setMaxConnTotal(200)
+ .setMaxConnPerRoute(20)
+ .setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault()))
+ .setConnectionManager(connectionManager)
+ .disableRedirectHandling()
+ .disableAutomaticRetries()
+ .build();
+ return new ApacheHttpTransport(client, isMtls);
}
- private GoogleApacheHttpTransport() {
- }
+ private GoogleApacheHttpTransport() {}
}
diff --git a/google-api-client/src/main/java/com/google/api/client/googleapis/javanet/GoogleNetHttpTransport.java b/google-api-client/src/main/java/com/google/api/client/googleapis/javanet/GoogleNetHttpTransport.java
index 47d536eb9..e6099014e 100644
--- a/google-api-client/src/main/java/com/google/api/client/googleapis/javanet/GoogleNetHttpTransport.java
+++ b/google-api-client/src/main/java/com/google/api/client/googleapis/javanet/GoogleNetHttpTransport.java
@@ -15,7 +15,11 @@
package com.google.api.client.googleapis.javanet;
import com.google.api.client.googleapis.GoogleUtils;
+import com.google.api.client.googleapis.mtls.MtlsProvider;
+import com.google.api.client.googleapis.mtls.MtlsUtils;
+import com.google.api.client.googleapis.util.Utils;
import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.util.Beta;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
@@ -29,32 +33,60 @@
public class GoogleNetHttpTransport {
/**
- * Returns a new instance of {@link NetHttpTransport} that uses
- * {@link GoogleUtils#getCertificateTrustStore()} for the trusted certificates using
- * {@link com.google.api.client.http.javanet.NetHttpTransport.Builder#trustCertificates(KeyStore)}
- * .
+ * Returns a new instance of {@link NetHttpTransport} that uses {@link
+ * GoogleUtils#getCertificateTrustStore()} for the trusted certificates using {@link
+ * com.google.api.client.http.javanet.NetHttpTransport.Builder#trustCertificates(KeyStore)}. If
+ * `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "true", and the default
+ * client certificate key store from {@link Utils#loadDefaultMtlsKeyStore()} is not null, then the
+ * transport uses the default client certificate and is mutual TLS.
*
- *
- * This helper method doesn't provide for customization of the {@link NetHttpTransport}, such as
- * the ability to specify a proxy. To do use, use
- * {@link com.google.api.client.http.javanet.NetHttpTransport.Builder}, for example:
- *
+ * This helper method doesn't provide for customization of the {@link NetHttpTransport}, such
+ * as the ability to specify a proxy. To do use, use {@link
+ * com.google.api.client.http.javanet.NetHttpTransport.Builder}, for example:
*
*
- static HttpTransport newProxyTransport() throws GeneralSecurityException, IOException {
- NetHttpTransport.Builder builder = new NetHttpTransport.Builder();
- builder.trustCertificates(GoogleUtils.getCertificateTrustStore());
- builder.setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 3128)));
- return builder.build();
- }
+ * static HttpTransport newProxyTransport() throws GeneralSecurityException, IOException {
+ * NetHttpTransport.Builder builder = new NetHttpTransport.Builder();
+ * builder.trustCertificates(GoogleUtils.getCertificateTrustStore());
+ * builder.setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 3128)));
+ * return builder.build();
+ * }
*
*/
public static NetHttpTransport newTrustedTransport()
throws GeneralSecurityException, IOException {
- return new NetHttpTransport.Builder().trustCertificates(GoogleUtils.getCertificateTrustStore())
- .build();
+ return newTrustedTransport(MtlsUtils.getDefaultMtlsProvider());
}
- private GoogleNetHttpTransport() {
+ /**
+ * {@link Beta}
+ * Returns a new instance of {@link NetHttpTransport} that uses {@link
+ * GoogleUtils#getCertificateTrustStore()} for the trusted certificates using {@link
+ * com.google.api.client.http.javanet.NetHttpTransport.Builder#trustCertificates(KeyStore)}.
+ * mtlsProvider can be used to configure mutual TLS for the transport.
+ *
+ * @param mtlsProvider MtlsProvider to configure mutual TLS for the transport
+ */
+ @Beta
+ public static NetHttpTransport newTrustedTransport(MtlsProvider mtlsProvider)
+ throws GeneralSecurityException, IOException {
+ KeyStore mtlsKeyStore = null;
+ String mtlsKeyStorePassword = null;
+ if (mtlsProvider.useMtlsClientCertificate()) {
+ mtlsKeyStore = mtlsProvider.getKeyStore();
+ mtlsKeyStorePassword = mtlsProvider.getKeyStorePassword();
+ }
+
+ if (mtlsKeyStore != null && mtlsKeyStorePassword != null) {
+ return new NetHttpTransport.Builder()
+ .trustCertificates(
+ GoogleUtils.getCertificateTrustStore(), mtlsKeyStore, mtlsKeyStorePassword)
+ .build();
+ }
+ return new NetHttpTransport.Builder()
+ .trustCertificates(GoogleUtils.getCertificateTrustStore())
+ .build();
}
+
+ private GoogleNetHttpTransport() {}
}
diff --git a/google-api-client/src/main/java/com/google/api/client/googleapis/mtls/ContextAwareMetadataJson.java b/google-api-client/src/main/java/com/google/api/client/googleapis/mtls/ContextAwareMetadataJson.java
new file mode 100644
index 000000000..ce3ad0036
--- /dev/null
+++ b/google-api-client/src/main/java/com/google/api/client/googleapis/mtls/ContextAwareMetadataJson.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.api.client.googleapis.mtls;
+
+import java.util.List;
+
+import com.google.api.client.json.GenericJson;
+import com.google.api.client.util.Beta;
+import com.google.api.client.util.Key;
+
+/**
+ * {@link Beta}
+ * Data class representing context_aware_metadata.json file.
+ *
+ * @since 1.31
+ */
+@Beta
+public class ContextAwareMetadataJson extends GenericJson {
+ /** Cert provider command */
+ @Key("cert_provider_command")
+ private List commands;
+
+ /**
+ * Returns the cert provider command.
+ *
+ * @since 1.31
+ */
+ public final List getCommands() {
+ return commands;
+ }
+}
diff --git a/google-api-client/src/main/java/com/google/api/client/googleapis/mtls/MtlsProvider.java b/google-api-client/src/main/java/com/google/api/client/googleapis/mtls/MtlsProvider.java
new file mode 100644
index 000000000..cb0813b45
--- /dev/null
+++ b/google-api-client/src/main/java/com/google/api/client/googleapis/mtls/MtlsProvider.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.api.client.googleapis.mtls;
+
+import com.google.api.client.util.Beta;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+
+/**
+ * {@link Beta}
+ * Provider interface for mutual TLS. It is used in {@link
+ * GoogleApacheHttpTransport#newTrustedTransport(MtlsProvider)} and {@link
+ * GoogleNetHttpTransport#newTrustedTransport(MtlsProvider)} to configure the mutual TLS in the
+ * transport.
+ *
+ * @since 1.31
+ */
+@Beta
+public interface MtlsProvider {
+ /**
+ * Returns if mutual TLS client certificate should be used. If the value is true, the key store
+ * from {@link #getKeyStore()} and key store password from {@link #getKeyStorePassword()} will be
+ * used to configure mutual TLS transport.
+ */
+ boolean useMtlsClientCertificate();
+
+ /** The key store to use for mutual TLS. */
+ String getKeyStorePassword();
+
+ /** The password for mutual TLS key store. */
+ KeyStore getKeyStore() throws IOException, GeneralSecurityException;
+}
diff --git a/google-api-client/src/main/java/com/google/api/client/googleapis/mtls/MtlsUtils.java b/google-api-client/src/main/java/com/google/api/client/googleapis/mtls/MtlsUtils.java
new file mode 100644
index 000000000..f08913e7f
--- /dev/null
+++ b/google-api-client/src/main/java/com/google/api/client/googleapis/mtls/MtlsUtils.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.api.client.googleapis.mtls;
+
+import com.google.api.client.googleapis.util.Utils;
+import com.google.api.client.json.JsonParser;
+import com.google.api.client.util.Beta;
+import com.google.api.client.util.SecurityUtils;
+import com.google.common.annotations.VisibleForTesting;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.util.List;
+
+/**
+ * {@link Beta}
+ * Utilities for mutual TLS.
+ *
+ * @since 1.31
+ */
+@Beta
+public class MtlsUtils {
+ @VisibleForTesting
+ static class DefaultMtlsProvider implements MtlsProvider {
+ private static final String DEFAULT_CONTEXT_AWARE_METADATA_PATH =
+ System.getProperty("user.home") + "/.secureConnect/context_aware_metadata.json";
+
+ /** GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable. */
+ public static final String GOOGLE_API_USE_CLIENT_CERTIFICATE =
+ "GOOGLE_API_USE_CLIENT_CERTIFICATE";
+
+ interface EnvironmentProvider {
+ String getenv(String name);
+ }
+
+ static class SystemEnvironmentProvider implements EnvironmentProvider {
+ @Override
+ public String getenv(String name) {
+ return System.getenv(name);
+ }
+ }
+
+ DefaultMtlsProvider() {
+ this(new SystemEnvironmentProvider(), DEFAULT_CONTEXT_AWARE_METADATA_PATH);
+ }
+
+ private EnvironmentProvider envProvider;
+ private String metadataPath;
+
+ @VisibleForTesting
+ DefaultMtlsProvider(EnvironmentProvider envProvider, String metadataPath) {
+ this.envProvider = envProvider;
+ this.metadataPath = metadataPath;
+ }
+
+ @Override
+ public boolean useMtlsClientCertificate() {
+ String useClientCertificate = envProvider.getenv(GOOGLE_API_USE_CLIENT_CERTIFICATE);
+ return "true".equals(useClientCertificate);
+ }
+
+ @Override
+ public String getKeyStorePassword() {
+ return "";
+ }
+
+ @Override
+ public KeyStore getKeyStore() throws IOException, GeneralSecurityException {
+ try {
+ // Load the cert provider command from the json file.
+ InputStream stream = new FileInputStream(metadataPath);
+ List command = extractCertificateProviderCommand(stream);
+
+ // Run the command and timeout after 1000 milliseconds.
+ Process process = new ProcessBuilder(command).start();
+ int exitCode = runCertificateProviderCommand(process, 1000);
+ if (exitCode != 0) {
+ throw new IOException("Cert provider command failed with exit code: " + exitCode);
+ }
+
+ // Create mTLS key store with the input certificates from shell command.
+ return SecurityUtils.createMtlsKeyStore(process.getInputStream());
+ } catch (FileNotFoundException ignored) {
+ // file doesn't exist
+ return null;
+ } catch (InterruptedException e) {
+ throw new IOException("Interrupted executing certificate provider command", e);
+ }
+ }
+
+ @VisibleForTesting
+ static List extractCertificateProviderCommand(InputStream contextAwareMetadata)
+ throws IOException {
+ JsonParser parser = Utils.getDefaultJsonFactory().createJsonParser(contextAwareMetadata);
+ ContextAwareMetadataJson json = parser.parse(ContextAwareMetadataJson.class);
+ return json.getCommands();
+ }
+
+ @VisibleForTesting
+ static int runCertificateProviderCommand(Process commandProcess, long timeoutMilliseconds)
+ throws IOException, InterruptedException {
+ long startTime = System.currentTimeMillis();
+ long remainTime = timeoutMilliseconds;
+ boolean terminated = false;
+
+ do {
+ try {
+ // Check if process is terminated by polling the exitValue, which throws
+ // IllegalThreadStateException if not terminated.
+ commandProcess.exitValue();
+ terminated = true;
+ break;
+ } catch (IllegalThreadStateException ex) {
+ if (remainTime > 0) {
+ Thread.sleep(Math.min(remainTime + 1, 100));
+ }
+ }
+ remainTime = remainTime - (System.currentTimeMillis() - startTime);
+ } while (remainTime > 0);
+
+ if (!terminated) {
+ commandProcess.destroy();
+ throw new IOException("cert provider command timed out");
+ }
+
+ return commandProcess.exitValue();
+ }
+ }
+
+ private static final MtlsProvider MTLS_PROVIDER = new DefaultMtlsProvider();
+
+ /**
+ * Returns the default MtlsProvider instance.
+ *
+ * @return The default MtlsProvider instance
+ */
+ public static MtlsProvider getDefaultMtlsProvider() {
+ return MTLS_PROVIDER;
+ }
+}
diff --git a/google-api-client/src/main/java/com/google/api/client/googleapis/mtls/package-info.java b/google-api-client/src/main/java/com/google/api/client/googleapis/mtls/package-info.java
new file mode 100644
index 000000000..fce7f4e26
--- /dev/null
+++ b/google-api-client/src/main/java/com/google/api/client/googleapis/mtls/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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.
+ */
+
+/**
+ * Mutual TLS utilities for the Google API Client Library.
+ *
+ * @since 1.31
+ */
+package com.google.api.client.googleapis.mtls;
diff --git a/google-api-client/src/main/java/com/google/api/client/googleapis/util/Utils.java b/google-api-client/src/main/java/com/google/api/client/googleapis/util/Utils.java
index 97c27e0e1..115da760e 100644
--- a/google-api-client/src/main/java/com/google/api/client/googleapis/util/Utils.java
+++ b/google-api-client/src/main/java/com/google/api/client/googleapis/util/Utils.java
@@ -29,9 +29,7 @@
@Beta
public final class Utils {
- /**
- * Returns a cached default implementation of the JsonFactory interface.
- */
+ /** Returns a cached default implementation of the JsonFactory interface. */
public static JsonFactory getDefaultJsonFactory() {
return JsonFactoryInstanceHolder.INSTANCE;
}
@@ -44,9 +42,7 @@ private static class JsonFactoryInstanceHolder {
static final JsonFactory INSTANCE = new JacksonFactory();
}
- /**
- * Returns a cached default implementation of the HttpTransport interface.
- */
+ /** Returns a cached default implementation of the HttpTransport interface. */
public static HttpTransport getDefaultTransport() {
return TransportInstanceHolder.INSTANCE;
}
@@ -55,6 +51,5 @@ private static class TransportInstanceHolder {
static final HttpTransport INSTANCE = new NetHttpTransport();
}
- private Utils() {
- }
+ private Utils() {}
}
diff --git a/google-api-client/src/test/java/com/google/api/client/googleapis/apache/v2/GoogleApacheHttpTransportTest.java b/google-api-client/src/test/java/com/google/api/client/googleapis/apache/v2/GoogleApacheHttpTransportTest.java
new file mode 100644
index 000000000..8963b532b
--- /dev/null
+++ b/google-api-client/src/test/java/com/google/api/client/googleapis/apache/v2/GoogleApacheHttpTransportTest.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.api.client.googleapis.apache.v2;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+import com.google.api.client.googleapis.mtls.MtlsTransportBaseTest;
+import com.google.api.client.googleapis.mtls.MtlsProvider;
+import com.google.api.client.http.HttpTransport;
+
+public class GoogleApacheHttpTransportTest extends MtlsTransportBaseTest {
+ @Override
+ protected HttpTransport buildTrustedTransport(MtlsProvider mtlsProvider) throws GeneralSecurityException, IOException {
+ return GoogleApacheHttpTransport.newTrustedTransport(mtlsProvider);
+ }
+}
diff --git a/google-api-client/src/test/java/com/google/api/client/googleapis/javanet/GoogleNetHttpTransportTest.java b/google-api-client/src/test/java/com/google/api/client/googleapis/javanet/GoogleNetHttpTransportTest.java
new file mode 100644
index 000000000..1dd970d24
--- /dev/null
+++ b/google-api-client/src/test/java/com/google/api/client/googleapis/javanet/GoogleNetHttpTransportTest.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.api.client.googleapis.javanet;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+import com.google.api.client.googleapis.mtls.MtlsTransportBaseTest;
+import com.google.api.client.googleapis.mtls.MtlsProvider;
+import com.google.api.client.http.HttpTransport;
+
+public class GoogleNetHttpTransportTest extends MtlsTransportBaseTest {
+ @Override
+ protected HttpTransport buildTrustedTransport(MtlsProvider mtlsProvider) throws GeneralSecurityException, IOException {
+ return GoogleNetHttpTransport.newTrustedTransport(mtlsProvider);
+ }
+}
diff --git a/google-api-client/src/test/java/com/google/api/client/googleapis/mtls/MtlsTransportBaseTest.java b/google-api-client/src/test/java/com/google/api/client/googleapis/mtls/MtlsTransportBaseTest.java
new file mode 100644
index 000000000..60dae51da
--- /dev/null
+++ b/google-api-client/src/test/java/com/google/api/client/googleapis/mtls/MtlsTransportBaseTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.api.client.googleapis.mtls;
+
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.util.SecurityUtils;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public abstract class MtlsTransportBaseTest {
+ protected KeyStore createTestMtlsKeyStore() throws IOException, GeneralSecurityException {
+ InputStream certAndKey = getClass()
+ .getClassLoader()
+ .getResourceAsStream("com/google/api/client/googleapis/util/mtlsCertAndKey.pem");
+ return SecurityUtils.createMtlsKeyStore(certAndKey);
+ }
+
+ protected static class TestMtlsProvider implements MtlsProvider {
+ private boolean useClientCertificate;
+ private KeyStore keyStore;
+ private String keyStorePassword;
+ private boolean throwExceptionForGetKeyStore;
+
+ TestMtlsProvider(boolean useClientCertificate, KeyStore keystore, String keyStorePassword, boolean throwExceptionForGetKeyStore) {
+ this.useClientCertificate = useClientCertificate;
+ this.keyStore = keystore;
+ this.keyStorePassword = keyStorePassword;
+ this.throwExceptionForGetKeyStore = throwExceptionForGetKeyStore;
+ }
+
+ @Override
+ public boolean useMtlsClientCertificate() {
+ return useClientCertificate;
+ }
+
+ @Override
+ public String getKeyStorePassword() {
+ return keyStorePassword;
+ }
+
+ @Override
+ public KeyStore getKeyStore() throws IOException, GeneralSecurityException {
+ if (throwExceptionForGetKeyStore) {
+ throw new IOException("getKeyStore throws exception");
+ }
+ return keyStore;
+ }
+ }
+
+ abstract protected HttpTransport buildTrustedTransport(MtlsProvider mtlsProvider) throws IOException, GeneralSecurityException;
+
+ // If client certificate shouldn't be used, then neither the provided mtlsKeyStore
+ // nor the default mtls key store should be used.
+ @Test
+ public void testNotUseCertificate() throws IOException, GeneralSecurityException {
+ MtlsProvider mtlsProvider = new TestMtlsProvider(false, createTestMtlsKeyStore(), "", false);
+ HttpTransport transport = buildTrustedTransport(mtlsProvider);
+ assertFalse(transport.isMtls());
+ }
+
+ // If client certificate should be used, and mtlsKeyStore is provided, then the
+ // provided key store should be used.
+ @Test
+ public void testUseProvidedCertificate() throws IOException, GeneralSecurityException {
+ MtlsProvider mtlsProvider = new TestMtlsProvider(true, createTestMtlsKeyStore(), "", false);
+ HttpTransport transport = buildTrustedTransport(mtlsProvider);
+ assertTrue(transport.isMtls());
+ }
+
+ // If client certificate should be used, but no mtls key store is available, then
+ // the transport created is not mtls.
+ @Test
+ public void testNoCertificate() throws IOException, GeneralSecurityException {
+ MtlsProvider mtlsProvider = new TestMtlsProvider(true, null, "", false);
+ HttpTransport transport = buildTrustedTransport(mtlsProvider);
+ assertFalse(transport.isMtls());
+ }
+
+ // Test the case where mtlsProvider.getKeyStore() throws.
+ @Test
+ public void testGetKeyStoreThrows() throws GeneralSecurityException {
+ MtlsProvider mtlsProvider = new TestMtlsProvider(true, null, "", true);
+ try {
+ buildTrustedTransport(mtlsProvider);
+ fail("should throw and exception");
+ } catch (IOException e) {
+ assertTrue(
+ "expected to fail with exception",
+ e.getMessage().contains("getKeyStore throws exception"));
+ }
+ }
+}
diff --git a/google-api-client/src/test/java/com/google/api/client/googleapis/mtls/MtlsUtilsTest.java b/google-api-client/src/test/java/com/google/api/client/googleapis/mtls/MtlsUtilsTest.java
new file mode 100644
index 000000000..7cc85a2ac
--- /dev/null
+++ b/google-api-client/src/test/java/com/google/api/client/googleapis/mtls/MtlsUtilsTest.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * 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 com.google.api.client.googleapis.mtls;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.util.List;
+import org.junit.Test;
+
+public class MtlsUtilsTest {
+ static class TestEnvironmentProvider
+ implements MtlsUtils.DefaultMtlsProvider.EnvironmentProvider {
+ private final String value;
+
+ TestEnvironmentProvider(String value) {
+ this.value = value;
+ }
+
+ @Override
+ public String getenv(String name) {
+ return value;
+ }
+ }
+
+ @Test
+ public void testUseMtlsClientCertificateEmpty() {
+ MtlsProvider mtlsProvider =
+ new MtlsUtils.DefaultMtlsProvider(new TestEnvironmentProvider(""), "/path/to/missing/file");
+ assertFalse(mtlsProvider.useMtlsClientCertificate());
+ }
+
+ @Test
+ public void testUseMtlsClientCertificateNull() {
+ MtlsProvider mtlsProvider =
+ new MtlsUtils.DefaultMtlsProvider(
+ new TestEnvironmentProvider(null), "/path/to/missing/file");
+ assertFalse(mtlsProvider.useMtlsClientCertificate());
+ }
+
+ @Test
+ public void testUseMtlsClientCertificateTrue() {
+ MtlsProvider mtlsProvider =
+ new MtlsUtils.DefaultMtlsProvider(
+ new TestEnvironmentProvider("true"), "/path/to/missing/file");
+ assertTrue(mtlsProvider.useMtlsClientCertificate());
+ }
+
+ @Test
+ public void testLoadDefaultKeyStoreMissingFile()
+ throws InterruptedException, GeneralSecurityException, IOException {
+ MtlsProvider mtlsProvider =
+ new MtlsUtils.DefaultMtlsProvider(
+ new TestEnvironmentProvider("true"), "/path/to/missing/file");
+ KeyStore keyStore = mtlsProvider.getKeyStore();
+ assertNull(keyStore);
+ }
+
+ @Test
+ public void testLoadDefaultKeyStore()
+ throws InterruptedException, GeneralSecurityException, IOException {
+ MtlsProvider mtlsProvider =
+ new MtlsUtils.DefaultMtlsProvider(
+ new TestEnvironmentProvider("true"),
+ "src/test/resources/com/google/api/client/googleapis/util/mtls_context_aware_metadata.json");
+ KeyStore keyStore = mtlsProvider.getKeyStore();
+ assertNotNull(keyStore);
+ }
+
+ @Test
+ public void testLoadDefaultKeyStoreBadCertificate()
+ throws InterruptedException, GeneralSecurityException, IOException {
+ MtlsProvider mtlsProvider =
+ new MtlsUtils.DefaultMtlsProvider(
+ new TestEnvironmentProvider("true"),
+ "src/test/resources/com/google/api/client/googleapis/util/mtls_context_aware_metadata_bad_command.json");
+ try {
+ mtlsProvider.getKeyStore();
+ fail("should throw and exception");
+ } catch (IllegalArgumentException e) {
+ assertTrue(
+ "expected to fail with certificate is missing",
+ e.getMessage().contains("certificate is missing"));
+ }
+ }
+
+ @Test
+ public void testExtractCertificateProviderCommand() throws IOException {
+ InputStream inputStream =
+ this.getClass().getClassLoader().getResourceAsStream("com/google/api/client/googleapis/util/mtls_context_aware_metadata.json");
+ List command =
+ MtlsUtils.DefaultMtlsProvider.extractCertificateProviderCommand(inputStream);
+ assertEquals(2, command.size());
+ assertEquals("cat", command.get(0));
+ assertEquals(
+ "src/test/resources/com/google/api/client/googleapis/util/mtlsCertAndKey.pem",
+ command.get(1));
+ }
+
+ static class TestCertProviderCommandProcess extends Process {
+ private boolean runForever;
+ private int exitValue;
+
+ public TestCertProviderCommandProcess(int exitValue, boolean runForever) {
+ this.runForever = runForever;
+ this.exitValue = exitValue;
+ }
+
+ @Override
+ public OutputStream getOutputStream() {
+ return null;
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return null;
+ }
+
+ @Override
+ public InputStream getErrorStream() {
+ return null;
+ }
+
+ @Override
+ public int waitFor() throws InterruptedException {
+ return 0;
+ }
+
+ @Override
+ public int exitValue() {
+ if (runForever) {
+ throw new IllegalThreadStateException();
+ }
+ return exitValue;
+ }
+
+ @Override
+ public void destroy() {}
+ }
+
+ @Test
+ public void testRunCertificateProviderCommandSuccess() throws IOException, InterruptedException {
+ Process certCommandProcess = new TestCertProviderCommandProcess(0, false);
+ int exitValue =
+ MtlsUtils.DefaultMtlsProvider.runCertificateProviderCommand(certCommandProcess, 100);
+ assertEquals(0, exitValue);
+ }
+
+ @Test
+ public void testRunCertificateProviderCommandTimeout() throws InterruptedException {
+ Process certCommandProcess = new TestCertProviderCommandProcess(0, true);
+ try {
+ MtlsUtils.DefaultMtlsProvider.runCertificateProviderCommand(certCommandProcess, 100);
+ fail("should throw and exception");
+ } catch (IOException e) {
+ assertTrue(
+ "expected to fail with timeout",
+ e.getMessage().contains("cert provider command timed out"));
+ }
+ }
+}
diff --git a/google-api-client/src/test/resources/com/google/api/client/googleapis/util/mtlsCertAndKey.pem b/google-api-client/src/test/resources/com/google/api/client/googleapis/util/mtlsCertAndKey.pem
new file mode 100644
index 000000000..d6c045125
--- /dev/null
+++ b/google-api-client/src/test/resources/com/google/api/client/googleapis/util/mtlsCertAndKey.pem
@@ -0,0 +1,30 @@
+-----BEGIN CERTIFICATE-----
+MIICGzCCAYSgAwIBAgIIWrt6xtmHPs4wDQYJKoZIhvcNAQEFBQAwMzExMC8GA1UE
+AxMoMTAwOTEyMDcyNjg3OC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbTAeFw0x
+MjEyMDExNjEwNDRaFw0yMjExMjkxNjEwNDRaMDMxMTAvBgNVBAMTKDEwMDkxMjA3
+MjY4NzguYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20wgZ8wDQYJKoZIhvcNAQEB
+BQADgY0AMIGJAoGBAL1SdY8jTUVU7O4/XrZLYTw0ON1lV6MQRGajFDFCqD2Fd9tQ
+GLW8Iftx9wfXe1zuaehJSgLcyCxazfyJoN3RiONBihBqWY6d3lQKqkgsRTNZkdFJ
+Wdzl/6CxhK9sojh2p0r3tydtv9iwq5fuuWIvtODtT98EgphhncQAqkKoF3zVAgMB
+AAGjODA2MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQM
+MAoGCCsGAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4GBAD8XQEqzGePa9VrvtEGpf+R4
+fkxKbcYAzqYq202nKu0kfjhIYkYSBj6gi348YaxE64yu60TVl42l5HThmswUheW4
+uQIaq36JvwvsDP5Zoj5BgiNSnDAFQp+jJFBRUA5vooJKgKgMDf/r/DCOsbO6VJF1
+kWwa9n19NFiV0z3m6isj
+-----END CERTIFICATE-----
+-----BEGIN PRIVATE KEY-----
+MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAL1SdY8jTUVU7O4/
+XrZLYTw0ON1lV6MQRGajFDFCqD2Fd9tQGLW8Iftx9wfXe1zuaehJSgLcyCxazfyJ
+oN3RiONBihBqWY6d3lQKqkgsRTNZkdFJWdzl/6CxhK9sojh2p0r3tydtv9iwq5fu
+uWIvtODtT98EgphhncQAqkKoF3zVAgMBAAECgYB51B9cXe4yiGTzJ4pOKpHGySAy
+sC1F/IjXt2eeD3PuKv4m/hL4l7kScpLx0+NJuQ4j8U2UK/kQOdrGANapB1ZbMZAK
+/q0xmIUzdNIDiGSoTXGN2mEfdsEpQ/Xiv0lyhYBBPC/K4sYIpHccnhSRQUZlWLLY
+lE5cFNKC9b7226mNvQJBAPt0hfCNIN0kUYOA9jdLtx7CE4ySGMPf5KPBuzPd8ty1
+fxaFm9PB7B76VZQYmHcWy8rT5XjoLJHrmGW1ZvP+iDsCQQDAvnKoarPOGb5iJfkq
+RrA4flf1TOlf+1+uqIOJ94959jkkJeb0gv/TshDnm6/bWn+1kJylQaKygCizwPwB
+Z84vAkA0Duur4YvsPJijoQ9YY1SGCagCcjyuUKwFOxaGpmyhRPIKt56LOJqpzyno
+fy8ReKa4VyYq4eZYT249oFCwMwIBAkAROPNF2UL3x5UbcAkznd1hLujtIlI4IV4L
+XUNjsJtBap7we/KHJq11XRPlniO4lf2TW7iji5neGVWJulTKS1xBAkAerktk4Hsw
+ErUaUG1s/d+Sgc8e/KMeBElV+NxGhcWEeZtfHMn/6VOlbzY82JyvC9OKC80A5CAE
+VUV6b25kqrcu
+-----END PRIVATE KEY-----
\ No newline at end of file
diff --git a/google-api-client/src/test/resources/com/google/api/client/googleapis/util/mtls_context_aware_metadata.json b/google-api-client/src/test/resources/com/google/api/client/googleapis/util/mtls_context_aware_metadata.json
new file mode 100644
index 000000000..c7729cf1b
--- /dev/null
+++ b/google-api-client/src/test/resources/com/google/api/client/googleapis/util/mtls_context_aware_metadata.json
@@ -0,0 +1,9 @@
+{
+ "cert_provider_command": [
+ "cat",
+ "src/test/resources/com/google/api/client/googleapis/util/mtlsCertAndKey.pem"
+ ],
+ "device_resource_ids": [
+ "123"
+ ]
+}
\ No newline at end of file
diff --git a/google-api-client/src/test/resources/com/google/api/client/googleapis/util/mtls_context_aware_metadata_bad_command.json b/google-api-client/src/test/resources/com/google/api/client/googleapis/util/mtls_context_aware_metadata_bad_command.json
new file mode 100644
index 000000000..69367d719
--- /dev/null
+++ b/google-api-client/src/test/resources/com/google/api/client/googleapis/util/mtls_context_aware_metadata_bad_command.json
@@ -0,0 +1,9 @@
+{
+ "cert_provider_command": [
+ "echo",
+ "\"foo\""
+ ],
+ "device_resource_ids": [
+ "123"
+ ]
+}
\ No newline at end of file