From 1d8304c6311e910293b800ffdbf0bb6f19cf7fff Mon Sep 17 00:00:00 2001 From: arithmetic1728 <58957152+arithmetic1728@users.noreply.github.com> Date: Tue, 10 Nov 2020 13:32:50 -0800 Subject: [PATCH] feat: add mtls support to GoogleNetHttpTransport and GoogleApacheHttpTransport (#1619) --- .../apache/v2/GoogleApacheHttpTransport.java | 78 ++++++-- .../javanet/GoogleNetHttpTransport.java | 68 +++++-- .../mtls/ContextAwareMetadataJson.java | 43 +++++ .../client/googleapis/mtls/MtlsProvider.java | 45 +++++ .../api/client/googleapis/mtls/MtlsUtils.java | 155 +++++++++++++++ .../client/googleapis/mtls/package-info.java | 20 ++ .../api/client/googleapis/util/Utils.java | 11 +- .../v2/GoogleApacheHttpTransportTest.java | 29 +++ .../javanet/GoogleNetHttpTransportTest.java | 29 +++ .../mtls/MtlsTransportBaseTest.java | 112 +++++++++++ .../client/googleapis/mtls/MtlsUtilsTest.java | 182 ++++++++++++++++++ .../client/googleapis/util/mtlsCertAndKey.pem | 30 +++ .../util/mtls_context_aware_metadata.json | 9 + ...ls_context_aware_metadata_bad_command.json | 9 + 14 files changed, 775 insertions(+), 45 deletions(-) create mode 100644 google-api-client/src/main/java/com/google/api/client/googleapis/mtls/ContextAwareMetadataJson.java create mode 100644 google-api-client/src/main/java/com/google/api/client/googleapis/mtls/MtlsProvider.java create mode 100644 google-api-client/src/main/java/com/google/api/client/googleapis/mtls/MtlsUtils.java create mode 100644 google-api-client/src/main/java/com/google/api/client/googleapis/mtls/package-info.java create mode 100644 google-api-client/src/test/java/com/google/api/client/googleapis/apache/v2/GoogleApacheHttpTransportTest.java create mode 100644 google-api-client/src/test/java/com/google/api/client/googleapis/javanet/GoogleNetHttpTransportTest.java create mode 100644 google-api-client/src/test/java/com/google/api/client/googleapis/mtls/MtlsTransportBaseTest.java create mode 100644 google-api-client/src/test/java/com/google/api/client/googleapis/mtls/MtlsUtilsTest.java create mode 100644 google-api-client/src/test/resources/com/google/api/client/googleapis/util/mtlsCertAndKey.pem create mode 100644 google-api-client/src/test/resources/com/google/api/client/googleapis/util/mtls_context_aware_metadata.json create mode 100644 google-api-client/src/test/resources/com/google/api/client/googleapis/util/mtls_context_aware_metadata_bad_command.json 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