Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add mtls support to GoogleNetHttpTransport and GoogleApacheHttpTransport #1619

Merged
merged 33 commits into from Nov 10, 2020
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4205102
feat: add mtls support
arithmetic1728 Oct 23, 2020
fa08020
add run cert command
arithmetic1728 Oct 27, 2020
b2d8891
javanet
arithmetic1728 Oct 30, 2020
c7a0e71
Merge branch 'master' of https://github.com/googleapis/google-api-jav…
arithmetic1728 Oct 30, 2020
79e9ac1
add apache
arithmetic1728 Oct 30, 2020
f860389
update
arithmetic1728 Oct 30, 2020
43655ab
fix test
arithmetic1728 Oct 30, 2020
2849ee7
tmp
arithmetic1728 Oct 30, 2020
46d8973
fix
arithmetic1728 Oct 30, 2020
5e063b3
added tests
arithmetic1728 Oct 31, 2020
dceaacf
add apache
arithmetic1728 Oct 31, 2020
0f4f878
finished apache
arithmetic1728 Oct 31, 2020
9e33fac
doc string
arithmetic1728 Nov 1, 2020
547a6c5
Merge branch 'master' of https://github.com/googleapis/google-api-jav…
arithmetic1728 Nov 4, 2020
0442025
update code
arithmetic1728 Nov 4, 2020
d83c115
lint
arithmetic1728 Nov 4, 2020
d92d3b3
refactor: implement MtlsUtils.MtlsProvider interface with default imp…
chingor13 Nov 5, 2020
aa72510
Merge pull request #1 from chingor13/mtls-refactor
arithmetic1728 Nov 5, 2020
05e052c
refactor mtls to a module
arithmetic1728 Nov 5, 2020
312e73a
rename
arithmetic1728 Nov 5, 2020
5209bec
fix exceptions
arithmetic1728 Nov 5, 2020
1edf060
add timeout
arithmetic1728 Nov 6, 2020
daeb967
fix util test
arithmetic1728 Nov 6, 2020
a3cea68
Merge pull request #3 from arithmetic1728/refactor
arithmetic1728 Nov 6, 2020
377f944
add tests
arithmetic1728 Nov 6, 2020
533c76e
lint
arithmetic1728 Nov 6, 2020
9ad39a5
make constructor public
arithmetic1728 Nov 6, 2020
47f8c07
lint
arithmetic1728 Nov 6, 2020
7bf8d8c
make constructor public
arithmetic1728 Nov 6, 2020
55d4eb5
update
arithmetic1728 Nov 9, 2020
46dd6b8
use bigger try catch block
arithmetic1728 Nov 9, 2020
9eee919
update code
arithmetic1728 Nov 10, 2020
5593a90
Update google-api-client/src/main/java/com/google/api/client/googleap…
arithmetic1728 Nov 10, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions google-api-client/pom.xml
Expand Up @@ -117,6 +117,11 @@
</resources>
</build>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
<version>2.8.6</version>
</dependency>
<dependency>
<groupId>com.google.oauth-client</groupId>
<artifactId>google-oauth-client</artifactId>
Expand Down Expand Up @@ -154,5 +159,17 @@
<groupId>com.google.http-client</groupId>
<artifactId>google-http-client-apache-v2</artifactId>
</dependency>
<dependency>
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.6.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>1.6.4</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Expand Up @@ -15,16 +15,18 @@
package com.google.api.client.googleapis.apache.v2;

import com.google.api.client.googleapis.GoogleUtils;
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.io.InputStream;
import java.net.ProxySelector;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
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;
Expand All @@ -41,9 +43,31 @@ public final class GoogleApacheHttpTransport {
/**
* 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 from {@link Utils#loadDefaultCertificate()}
* 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(null);
}

/**
* {@link Beta} <br>
* 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",
* the function looks for user provided client certificate first from
* clientCertificateSource InputStream, if not exists, then the default from
* {@link Utils#loadDefaultCertificate()}. If client certificate exists,
* the transport uses it and is mutual TLS.
*
* @param clientCertificateSource InputStream for mutual TLS client certificate and private key
*/
@Beta
public static ApacheHttpTransport newTrustedTransport(InputStream clientCertificateSource)
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
throws GeneralSecurityException, IOException {
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager(-1, TimeUnit.MILLISECONDS);

Expand All @@ -53,7 +77,19 @@ 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());
KeyStore mtlsKeyStore = Utils.loadMtlsKeyStore(clientCertificateSource);
boolean isMtls = (mtlsKeyStore != null) && (mtlsKeyStore.size() > 0);
if (isMtls) {
SslUtils.initSslContext(
sslContext,
trustStore,
SslUtils.getPkixTrustManagerFactory(),
mtlsKeyStore,
"",
SslUtils.getDefaultKeyManagerFactory());
} else {
SslUtils.initSslContext(sslContext, trustStore, SslUtils.getPkixTrustManagerFactory());
}
LayeredConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext);

HttpClient client = HttpClientBuilder.create()
Expand All @@ -66,9 +102,8 @@ public static ApacheHttpTransport newTrustedTransport() throws GeneralSecurityEx
.disableRedirectHandling()
.disableAutomaticRetries()
.build();
return new ApacheHttpTransport(client);
return new ApacheHttpTransport(client, isMtls);
}

private GoogleApacheHttpTransport() {
}
private GoogleApacheHttpTransport() {}
}
Expand Up @@ -15,8 +15,12 @@
package com.google.api.client.googleapis.javanet;

import com.google.api.client.googleapis.GoogleUtils;
import com.google.api.client.googleapis.util.Utils;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.util.Beta;
import com.google.api.client.util.SecurityUtils;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.KeyStore;

Expand All @@ -32,7 +36,10 @@ 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)}
* .
* . If `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "true",
* and the default client certificate from {@link Utils#loadDefaultCertificate()}
* is not null, then the transport uses the default client certificate and
* is mutual TLS.
*
* <p>
* This helper method doesn't provide for customization of the {@link NetHttpTransport}, such as
Expand All @@ -51,10 +58,54 @@ static HttpTransport newProxyTransport() throws GeneralSecurityException, IOExce
*/
public static NetHttpTransport newTrustedTransport()
throws GeneralSecurityException, IOException {
return new NetHttpTransport.Builder().trustCertificates(GoogleUtils.getCertificateTrustStore())
.build();
return newTrustedTransport(null);
}

private GoogleNetHttpTransport() {
/**
* {@link Beta} <br>
* 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",
* the function looks for user provided client certificate first from
* clientCertificateSource InputStream, if not exists, then the default from
* {@link Utils#loadDefaultCertificate()}. If client certificate exists,
* the transport uses it and is mutual TLS.
*
* @param clientCertificateSource InputStream for mutual TLS client certificate and private key
*/
@Beta
public static NetHttpTransport newTrustedTransport(InputStream clientCertificateSource)
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
throws GeneralSecurityException, IOException {
return newTrustedTransportBuilder(clientCertificateSource).build();
}

/**
* {@link Beta} <br>
* Returns a new instance of {@link NetHttpTransport.Builder} 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",
* the function looks for user provided client certificate first from
* clientCertificateSource InputStream, if not exists, then the default from
* {@link Utils#loadDefaultCertificate()}. If client certificate exists,
* the transport uses it and is mutual TLS. Note that mutual TLS may not work properly
* if you specify a proxy with the Builder instance.
*
* @param clientCertificateSource InputStream for mutual TLS client certificate and private key
*/
@Beta
public static NetHttpTransport.Builder newTrustedTransportBuilder(
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
InputStream clientCertificateSource)
throws GeneralSecurityException, IOException {
KeyStore mtlsKeyStore = Utils.loadMtlsKeyStore(clientCertificateSource);

if (mtlsKeyStore != null) {
return new NetHttpTransport.Builder()
.trustCertificates(GoogleUtils.getCertificateTrustStore(), mtlsKeyStore, "");
}
return new NetHttpTransport.Builder().trustCertificates(GoogleUtils.getCertificateTrustStore());
}

private GoogleNetHttpTransport() {}
}
Expand Up @@ -14,11 +14,23 @@

package com.google.api.client.googleapis.util;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.ArrayList;

import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.Beta;
import com.google.api.client.util.SecurityUtils;
import com.google.gson.GsonBuilder;
import com.google.gson.internal.LinkedTreeMap;

/**
* {@link Beta} <br/>
Expand All @@ -28,6 +40,10 @@
*/
@Beta
public final class Utils {
private static final String 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";

/**
* Returns a cached default implementation of the JsonFactory interface.
Expand Down Expand Up @@ -55,6 +71,82 @@ private static class TransportInstanceHolder {
static final HttpTransport INSTANCE = new NetHttpTransport();
}

/**
* Returns the `cert_provider_command` field in context_aware_metadata.json file.
*
* @param contextAwareMetadataJson ~/.secureConnect/context_aware_metadata.json file content.
* @return `cert_provider_command` field
* @since 1.31
*/
@SuppressWarnings("unchecked")
public static ArrayList<String> extractCertificateProviderCommand(String contextAwareMetadataJson) {
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
GsonBuilder builder = new GsonBuilder();
LinkedTreeMap<String, Object> map = (LinkedTreeMap<String, Object>)builder.create().fromJson(contextAwareMetadataJson, Object.class);
return (ArrayList<String>)map.get("cert_provider_command");
}

/**
* Returns the default client certificate by running the cert_provider_command commands
* from ~/.secureConnect/context_aware_metadata.json file.
*
* @return The default client certificate input stream
* @since 1.31
*/
public static InputStream loadDefaultCertificate() throws IOException, GeneralSecurityException {
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
File file = new File(CONTEXT_AWARE_METADATA_PATH);
if (!file.exists()) {
return null;
}

// Load the cert provider command from the json file.
String json = new String(Files.readAllBytes(Paths.get(CONTEXT_AWARE_METADATA_PATH)));
ArrayList<String> command = extractCertificateProviderCommand(json);

// Call the command.
Process process = new ProcessBuilder(command).start();
int exitCode = 0;
try {
exitCode = process.waitFor();
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
} catch (InterruptedException exception) {
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
throw new GeneralSecurityException("Failed to execute cert provider command", exception);
arithmetic1728 marked this conversation as resolved.
Show resolved Hide resolved
}
if (exitCode != 0) {
throw new GeneralSecurityException("Failed to execute cert provider command");
}

return process.getInputStream();
}

/**
* Returns the KeyStore for mutual TLS.
*
* If `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is set to "true", and
* either the client certificate is provided via clientCertificateSource, or the default
* client certificate exists via {@link Utils#loadDefaultCertificate()}, a KeyStore object
* will be created using the client certificate (clientCertificateSource takes precedence).
* Otherwise, this function return null.
*
* @param clientCertificateSource InputStream for mutual TLS client certificate and private key
* @return KeyStore for mutual TLS.
* @since 1.31
*/
public static KeyStore loadMtlsKeyStore(InputStream clientCertificateSource) throws IOException, GeneralSecurityException {
String useClientCertificate = System.getenv(GOOGLE_API_USE_CLIENT_CERTIFICATE);
if ("true".equals(useClientCertificate)) {
InputStream certificateToUse = null;
if (clientCertificateSource != null) {
certificateToUse = clientCertificateSource;
} else {
certificateToUse = loadDefaultCertificate();
}

if (certificateToUse != null) {
return SecurityUtils.createMtlsKeyStore(certificateToUse);
}
}
return null;
}

private Utils() {
}
}
@@ -0,0 +1,45 @@
package com.google.api.client.googleapis.apache.v2;

import java.io.InputStream;
import java.security.KeyStore;

import com.google.api.client.googleapis.util.Utils;
import com.google.api.client.http.apache.v2.ApacheHttpTransport;
import com.google.api.client.util.SecurityUtils;

import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import junit.framework.TestCase;

@RunWith(PowerMockRunner.class)
@PrepareForTest({Utils.class, GoogleApacheHttpTransport.class})
@PowerMockIgnore({"jdk.internal.reflect.*", "javax.net.ssl.*"})
public class GoogleApacheHttpTransportTest extends TestCase {
public InputStream getCertAndKey() throws Exception {
return getClass()
.getClassLoader()
.getResourceAsStream("com/google/api/client/googleapis/util/mtlsCertAndKey.pem");
}

// mTLS key store is provided, so mTLS transport is created
public void testWithMtlsKeyStore() throws Exception {
KeyStore mtlsKeyStore = SecurityUtils.createMtlsKeyStore(getCertAndKey());
PowerMockito.mockStatic(Utils.class);
PowerMockito.when(Utils.loadMtlsKeyStore(Mockito.any(InputStream.class))).thenReturn(mtlsKeyStore);
ApacheHttpTransport transport = GoogleApacheHttpTransport.newTrustedTransport(getCertAndKey());
assertTrue(transport.isMtls());
}

// mTLS key store doesn't exist, so transport is not mTLS
public void testWithoutMtlsKeyStore() throws Exception {
PowerMockito.mockStatic(Utils.class);
PowerMockito.when(Utils.loadMtlsKeyStore(Mockito.any(InputStream.class))).thenReturn(null);
ApacheHttpTransport transport = GoogleApacheHttpTransport.newTrustedTransport(getCertAndKey());
assertFalse(transport.isMtls());
}
}