diff --git a/docs/oauth-2.0.md b/docs/oauth-2.0.md index 56a72b165..247750b32 100644 --- a/docs/oauth-2.0.md +++ b/docs/oauth-2.0.md @@ -47,7 +47,7 @@ For instructions on setting up your credentials properly, see the already have an access token, you can make a request in the following way: ```java -import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.gson.GsonFactory; import com.google.api.services.books.Books; import com.google.auth.http.HttpCredentialsAdapter; @@ -59,7 +59,7 @@ GoogleCredentials credentials = Books books = new Books.Builder( - GoogleNetHttpTransport.newTrustedTransport(), + Gnew NetHttpTransport(), GsonFactory.getDefaultInstance(), new HttpCredentialsAdapter(credentials)) .setApplicationName("BooksExample/1.0") @@ -79,7 +79,7 @@ App Engine takes care of all of the details. You only specify the OAuth 2.0 scope you need. ```java -import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.gson.GsonFactory; import com.google.api.services.books.Books; import com.google.appengine.api.appidentity.AppIdentityService; @@ -99,7 +99,7 @@ GoogleCredentials credentials = Books books = new Books.Builder( - GoogleNetHttpTransport.newTrustedTransport(), + new NetHttpTransport(), GsonFactory.getDefaultInstance(), new HttpCredentialsAdapter(credentials)) .setApplicationName("BooksExample/1.0") @@ -373,7 +373,7 @@ a private key downloaded from the [Google API Console][console]. For example, you can make a request in the following way: ```java -HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport(); +HttpTransport httpTransport = new NetHttpTransport(); JsonFactory jsonFactory = GsonFactory.getDefaultInstance(); //Build service account credential @@ -405,12 +405,12 @@ additionally call [`GoogleCredential.Builder.setServiceAccountUser(String)`][set This is the command-line authorization code flow described in [Using OAuth 2.0 for Installed Applications][oauth2-installed-app]. -Example snippet from [plus-cmdline-sample][plus-sample]: +Example usage: ```java public static void main(String[] args) { try { - httpTransport = GoogleNetHttpTransport.newTrustedTransport(); + httpTransport = new NetHttpTransport(); dataStoreFactory = new FileDataStoreFactory(DATA_STORE_DIR); // authorization Credential credential = authorize(); diff --git a/google-api-client-apache-v5/src/test/java/com/google/api/client/googleapis/apache/v5/ITGoogleApache5HttpTransportTest.java b/google-api-client-apache-v5/src/test/java/com/google/api/client/googleapis/apache/v5/ITGoogleApache5HttpTransportTest.java index f8e9cbed1..db03ab720 100644 --- a/google-api-client-apache-v5/src/test/java/com/google/api/client/googleapis/apache/v5/ITGoogleApache5HttpTransportTest.java +++ b/google-api-client-apache-v5/src/test/java/com/google/api/client/googleapis/apache/v5/ITGoogleApache5HttpTransportTest.java @@ -30,10 +30,11 @@ public class ITGoogleApache5HttpTransportTest { @Test - public void testHttpRequestFailsWhenMakingRequestToSiteWithoutGoogleCerts() + public void testHttpRequestFailsWhenMakingRequestToSiteWithoutDefaultJdkCerts() throws GeneralSecurityException, IOException { Apache5HttpTransport apache5HttpTransport = GoogleApache5HttpTransport.newTrustedTransport(); - HttpGet httpGet = new HttpGet("https://maven.com/"); + // Use a self-signed certificate site that won't be trusted by default trust store + HttpGet httpGet = new HttpGet("https://self-signed.badssl.com/"); Exception exception = null; try { apache5HttpTransport @@ -43,7 +44,7 @@ public void testHttpRequestFailsWhenMakingRequestToSiteWithoutGoogleCerts() new HttpClientResponseHandler() { @Override public Void handleResponse(ClassicHttpResponse response) { - fail("Should not have been able to complete SSL request on non google site."); + fail("Should not have been able to complete SSL request with untrusted cert."); return null; } }); diff --git a/google-api-client/src/main/java/com/google/api/client/googleapis/GoogleUtils.java b/google-api-client/src/main/java/com/google/api/client/googleapis/GoogleUtils.java index c15c8a458..e1cb2d9de 100644 --- a/google-api-client/src/main/java/com/google/api/client/googleapis/GoogleUtils.java +++ b/google-api-client/src/main/java/com/google/api/client/googleapis/GoogleUtils.java @@ -16,6 +16,8 @@ import com.google.api.client.util.SecurityUtils; import com.google.common.annotations.VisibleForTesting; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.security.GeneralSecurityException; @@ -69,21 +71,96 @@ public final class GoogleUtils { } /** Cached value for {@link #getCertificateTrustStore()}. */ - static KeyStore certTrustStore; + @VisibleForTesting static KeyStore certTrustStore; + + /** Name of bundled keystore. */ + static final String BUNDLED_KEYSTORE = "google.p12"; + + /** Bundled keystore password. */ + static final String BUNDLED_KEYSTORE_PASSWORD = "notasecret"; + + /** Default JDK cacerts file path relative to java.home. */ + @VisibleForTesting + static String[] possibleJdkPaths = { + "lib/security/cacerts", // Java 9+ + "jre/lib/security/cacerts" // Java 8 and earlier + }; + + /** Java home system property key. */ + static final String JAVA_HOME_KEY = "java.home"; + + /** Default password for JDK cacerts file. */ + static final String DEFAULT_CACERTS_PASSWORD = "changeit"; + + /** + * Loads the bundled google.p12 keystore containing trusted root certificates. + * + * @return the loaded keystore + */ + @VisibleForTesting + static KeyStore getBundledKeystore() throws IOException, GeneralSecurityException { + KeyStore ks = SecurityUtils.getPkcs12KeyStore(); + InputStream is = GoogleUtils.class.getResourceAsStream(BUNDLED_KEYSTORE); + SecurityUtils.loadKeyStore(ks, is, BUNDLED_KEYSTORE_PASSWORD); + return ks; + } /** - * Returns the key store for trusted root certificates to use for Google APIs. + * Loads the default JDK keystore (cacerts) containing trusted root certificates. Uses Java's + * system properties + known cert locations to locate the default trust store. + * + * @return the loaded keystore + */ + @VisibleForTesting + static KeyStore getJdkDefaultKeyStore() throws IOException, GeneralSecurityException { + KeyStore keyStore = SecurityUtils.getDefaultKeyStore(); + + // Find the default JDK cacerts location + String javaHome = System.getProperty(JAVA_HOME_KEY); + + for (String path : possibleJdkPaths) { + File cacertsFile = new File(javaHome, path); + try (FileInputStream fis = new FileInputStream(cacertsFile)) { + keyStore.load(fis, DEFAULT_CACERTS_PASSWORD.toCharArray()); + return keyStore; + } catch (IOException e) { + // File doesn't exist or can't be read, try next path + } + } + + throw new IOException("Unable to load default JDK cacerts file"); + } + + /** + * Returns a keystore for trusted root certificates to use for Google APIs. * *

Value is cached, so subsequent access is fast. * + *

This method first attempts to load the JDK default keystore. If that fails or is not + * available, it falls back to loading the bundled Google certificate store. + * * @since 1.14 + * @deprecated Depending on your build environment this method potentially can contain outdated + * certs if loading jdk default certs fails. Instead of getting trusted certs directly use an + * HttpTransport wrapper such as {@link NetHttpTransport} + * which uses java jdk internal classes to load default jdk certs specifically for a build + * environment. If you need to access the keystore directly please create your own keystore + * file. */ + @Deprecated public static synchronized KeyStore getCertificateTrustStore() throws IOException, GeneralSecurityException { if (certTrustStore == null) { - certTrustStore = SecurityUtils.getPkcs12KeyStore(); - InputStream keyStoreStream = GoogleUtils.class.getResourceAsStream("google.p12"); - SecurityUtils.loadKeyStore(certTrustStore, keyStoreStream, "notasecret"); + try { + certTrustStore = getJdkDefaultKeyStore(); + } catch (Exception e) { + // If unable to retrieve default JDK keystore, fall through to bundled certificates + } + + if (certTrustStore == null) { + certTrustStore = getBundledKeystore(); + } } return certTrustStore; } 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 66907fc72..eb4d00b46 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 @@ -29,7 +29,10 @@ * * @since 1.14 * @author Yaniv Inbar + * @deprecated This legacy HttpTransport implementation is no longer being maintained. + * Please use {@link NetHttpTransport instead. */ +@Deprecated public class GoogleNetHttpTransport { /** diff --git a/google-api-client/src/test/java/com/google/api/client/googleapis/GoogleUtilsTest.java b/google-api-client/src/test/java/com/google/api/client/googleapis/GoogleUtilsTest.java index ef8aa80b1..9e531ab30 100644 --- a/google-api-client/src/test/java/com/google/api/client/googleapis/GoogleUtilsTest.java +++ b/google-api-client/src/test/java/com/google/api/client/googleapis/GoogleUtilsTest.java @@ -14,8 +14,9 @@ package com.google.api.client.googleapis; +import static org.junit.Assert.assertNotEquals; + import java.security.KeyStore; -import java.util.Enumeration; import java.util.regex.Matcher; import junit.framework.TestCase; @@ -26,16 +27,43 @@ */ public class GoogleUtilsTest extends TestCase { - public void testGetCertificateTrustStore() throws Exception { + public void testGetCertificateTrustStore_LoadsJdkDefaultFirst() throws Exception { + GoogleUtils.certTrustStore = null; + KeyStore trustStore = GoogleUtils.getCertificateTrustStore(); + + // Load bundled keystore to compare + KeyStore bundled = GoogleUtils.getBundledKeystore(); + + assertNotEquals( + "Certificate truststore should NOT contain the same amount of certificates as the bundled keystore", + bundled.size(), + trustStore.size()); + } + + public void testGetCertificateTrustStore_LoadsBundledKeystoreIfJdkDefaultLoadFails() + throws Exception { + GoogleUtils.certTrustStore = null; + String[] originalPaths = GoogleUtils.possibleJdkPaths; + GoogleUtils.possibleJdkPaths = new String[0]; + KeyStore trustStore = GoogleUtils.getCertificateTrustStore(); - Enumeration aliases = trustStore.aliases(); - while (aliases.hasMoreElements()) { - String alias = aliases.nextElement(); - assertTrue(trustStore.isCertificateEntry(alias)); - } - // intentionally check the count of certificates, so it can help us detect if a new certificate - // has been added or removed - assertEquals(71, trustStore.size()); + + // Load bundled keystore to compare + KeyStore bundled = GoogleUtils.getBundledKeystore(); + assertEquals( + "Certificate truststore should contain the same amount of certificates as the bundled keystore", + trustStore.size(), + bundled.size()); + + GoogleUtils.possibleJdkPaths = originalPaths; + } + + public void testGetCertificateTrustStore_IsCached() throws Exception { + KeyStore trustStore1 = GoogleUtils.getCertificateTrustStore(); + KeyStore trustStore2 = GoogleUtils.getCertificateTrustStore(); + + // Should return the exact same instance due to caching + assertSame("Trust store should be cached", trustStore1, trustStore2); } public void testVersionMatcher() {