Skip to content

Commit bf926fb

Browse files
diegomarquezplqiu96
authored andcommitted
fix(gdch): support EC private keys (#1896)
* fix: allow for ES algorithm in GdchCredentials * test: partially adapt tests * test: finish adjusting tests * chore: format * fix: restore credential name * docs: restore license * fix: restore removed code * test: increase coverage * test(gdch): parameterize test * test: remove unused var * chore: remove unused throw clause * test: parameterize more * fix: remove unused parameter * fix: make variables final as intended * fix: remove unused throw clause * fix: remove unused throw clause * fix: make OAuth2Credentials clock package private for production code * fix: use non deprecated base 64 encoder * test: parameterize flagged tests * chore: format * test: fix assertion * build: remove unused dependency * test: run linux gce only on linux envs * fix: sonarqube flags (use java.util.Base64) * fix: improve error message template * fix: keep overload of "with audience" that takes an URI * fix: restore public getter of getApiAudience * docs: add javadoc for signing logic * test: test private signature and decode methods * fix: add null and empty check for audience string * docs: add javadoc for audience getters * fix: use enum for possible algorithms * fix: use obsolete javadoc instead of @deprecated * refactor: use OAuth2Utils validate methods in GdchCredentials * fix: restore GoogleAuthException throwing in GdchCredentials * refactor: downgrade Pkcs8Algorithm and privateKeyFromPkcs8 to package-private * refactor: split parseBody into parseJson and parseQuery in test utilities * refactor: remove validation reflection by making signUsingEsSha256 package-private * test: use hardcoded string literal for gdch api audience in test * test: refactor to use assertThrows in GdchCredentialsTest and remove host OS check in DefaultCredentialsProviderTest * fix: add comment about EC algorithm support in GdchCredentials * fix: update GDCH audience error message to be more descriptive * refactor: rename getApiAudienceString to getGdchAudience * fix: Remove unused import * docs: update GDCH audience getter javadocs * test: add null-checks to builder and corresponding tests * refactor: consolidate token type constants using OAuth2Utils * refactor: throw GoogleAuthException for signing and transcoding errors * docs: add javadoc to related test utils * fix: use GoogleAuthException * test: use assertThrows where applicable * refactor: replace Preconditions with Strings.isNullOrEmpty for audience checks * fix: consistent exception message * chore: format * test: use lowercase os name * chore: address review comments for PR #1896 * chore: format * Finalizing GDCH credentials support by addressing reviewer comments * chore: format * fix: parse EC private keys with SEC1 algorithm * chore: format * fix: separate PKCs8 vs SEC1 logic in GdchCredentials * fix: improved exception message, added comments to extractPrivateKeyValue Original-PR: googleapis/google-auth-library-java#1896
1 parent 40c9b0b commit bf926fb

File tree

8 files changed

+885
-103
lines changed

8 files changed

+885
-103
lines changed

google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/GdchCredentials.java

Lines changed: 375 additions & 36 deletions
Large diffs are not rendered by default.

google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/OAuth2Credentials.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public class OAuth2Credentials extends Credentials {
8686
// Change listeners are not serialized
8787
private transient List<CredentialsChangedListener> changeListeners;
8888
// Until we expose this to the users it can remain transient and non-serializable
89-
@VisibleForTesting transient Clock clock = Clock.SYSTEM;
89+
transient Clock clock = Clock.SYSTEM;
9090

9191
/**
9292
* Returns the credentials instance from the given access token.

google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/OAuth2Utils.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
import com.google.api.client.json.gson.GsonFactory;
4141
import com.google.api.client.util.PemReader;
4242
import com.google.api.client.util.PemReader.Section;
43-
import com.google.api.client.util.SecurityUtils;
4443
import com.google.api.core.InternalApi;
4544
import com.google.auth.http.AuthHttpConstants;
4645
import com.google.auth.http.HttpTransportFactory;
@@ -82,6 +81,11 @@
8281
@InternalApi
8382
public class OAuth2Utils {
8483

84+
enum Pkcs8Algorithm {
85+
RSA,
86+
EC
87+
}
88+
8589
static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
8690

8791
public static final String TOKEN_TYPE_ACCESS_TOKEN =
@@ -269,6 +273,24 @@ static Map<String, Object> validateMap(Map<String, Object> map, String key, Stri
269273
* key creation.
270274
*/
271275
public static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOException {
276+
return privateKeyFromPkcs8(privateKeyPkcs8, Pkcs8Algorithm.RSA);
277+
}
278+
279+
/**
280+
* Reads a private key from a PKCS#8 encoded string.
281+
*
282+
* <p>If the key is labeled with "-----BEGIN PRIVATE KEY-----", it is parsed as PKCS#8 as per RFC
283+
* 7468 Section 10.
284+
*
285+
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7468#section-10">RFC 7468 Section 10</a>
286+
* @param privateKeyPkcs8 base64 encoded private key string
287+
* @param algorithm expected algorithm of the private key
288+
* @return the private key.
289+
* @throws IOException if the private key data is invalid or if an unexpected exception occurs
290+
* during key creation.
291+
*/
292+
public static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8, Pkcs8Algorithm algorithm)
293+
throws IOException {
272294
Reader reader = new StringReader(privateKeyPkcs8);
273295
Section section = PemReader.readFirstSectionAndClose(reader, "PRIVATE KEY");
274296
if (section == null) {
@@ -278,7 +300,7 @@ public static PrivateKey privateKeyFromPkcs8(String privateKeyPkcs8) throws IOEx
278300
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(bytes);
279301
Exception unexpectedException;
280302
try {
281-
KeyFactory keyFactory = SecurityUtils.getRsaKeyFactory();
303+
KeyFactory keyFactory = KeyFactory.getInstance(algorithm.toString());
282304
return keyFactory.generatePrivate(keySpec);
283305
} catch (NoSuchAlgorithmException | InvalidKeySpecException exception) {
284306
unexpectedException = exception;

google-auth-library-java/oauth2_http/javatests/com/google/auth/TestUtils.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ public static InputStream stringToInputStream(String text) {
9797
return new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8));
9898
}
9999

100+
/**
101+
* Parses a URI query string into a map of key-value pairs.
102+
*
103+
* @param query The URI query string (e.g., "key1=val1&key2=val2").
104+
* @return A map of decoded keys to decoded values.
105+
* @throws IOException If the query string is malformed.
106+
*/
100107
public static Map<String, String> parseQuery(String query) throws IOException {
101108
Map<String, String> map = new HashMap<>();
102109
Iterable<String> entries = Splitter.on('&').split(query);
@@ -112,6 +119,23 @@ public static Map<String, String> parseQuery(String query) throws IOException {
112119
return map;
113120
}
114121

122+
/**
123+
* Parses a JSON string into a map of key-value pairs.
124+
*
125+
* @param content The JSON string representation of a flat object.
126+
* @return A map of keys to string representations of their values.
127+
* @throws IOException If the JSON is malformed.
128+
*/
129+
public static Map<String, String> parseJson(String content) throws IOException {
130+
GenericJson json = JSON_FACTORY.fromString(content, GenericJson.class);
131+
Map<String, String> map = new HashMap<>();
132+
for (Map.Entry<String, Object> entry : json.entrySet()) {
133+
Object value = entry.getValue();
134+
map.put(entry.getKey(), value == null ? null : value.toString());
135+
}
136+
return map;
137+
}
138+
115139
public static String errorJson(String message) throws IOException {
116140
GenericJson errorResponse = new GenericJson();
117141
errorResponse.setFactory(JSON_FACTORY);

google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java

Lines changed: 15 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class DefaultCredentialsProviderTest {
8585
private static final String SA_PRIVATE_KEY_ID = "d84a4fefcf50791d4a90f2d7af17469d6282df9d";
8686
private static final String SA_PRIVATE_KEY_PKCS8 =
8787
ServiceAccountCredentialsTest.PRIVATE_KEY_PKCS8;
88-
private static final String GDCH_SA_FORMAT_VERSION = GdchCredentials.SUPPORTED_FORMAT_VERSION;
88+
8989
private static final String GDCH_SA_PROJECT_ID = "gdch-service-account-project-id";
9090
private static final String GDCH_SA_PRIVATE_KEY_ID = "d84a4fefcf50791d4a90f2d7af17469d6282df9d";
9191
private static final String GDCH_SA_PRIVATE_KEY_PKC8 = GdchCredentialsTest.PRIVATE_KEY_PKCS8;
@@ -96,7 +96,7 @@ class DefaultCredentialsProviderTest {
9696
private static final String GDCH_SA_CA_CERT_FILE_NAME = "cert.pem";
9797
private static final String GDCH_SA_CA_CERT_PATH =
9898
GdchCredentialsTest.class.getClassLoader().getResource(GDCH_SA_CA_CERT_FILE_NAME).getPath();
99-
private static final URI GDCH_SA_API_AUDIENCE = URI.create("https://gdch-api-audience");
99+
private static final String GDCH_SA_API_AUDIENCE = "https://gdch-api-audience";
100100
private static final Collection<String> SCOPES = Collections.singletonList("dummy.scope");
101101
private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo");
102102
private static final String QUOTA_PROJECT = "sample-quota-project-id";
@@ -166,43 +166,32 @@ void getDefaultCredentials_noCredentials_singleGceTestRequest() {
166166

167167
@Test
168168
void getDefaultCredentials_noCredentials_linuxNotGce() {
169-
TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider();
170-
testProvider.setProperty("os.name", "Linux");
171-
InputStream productStream = new ByteArrayInputStream("test".getBytes());
172-
testProvider.addFile(SMBIOS_PATH_LINUX, productStream);
173-
174-
assertFalse(ComputeEngineCredentials.checkStaticGceDetection(testProvider));
169+
checkStaticGceDetection("linux", "test", false);
175170
}
176171

177172
@Test
178173
void getDefaultCredentials_static_linux() {
179-
TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider();
180-
testProvider.setProperty("os.name", "Linux");
181-
File productFile = new File(SMBIOS_PATH_LINUX);
182-
InputStream productStream = new ByteArrayInputStream("Googlekdjsfhg".getBytes());
183-
testProvider.addFile(productFile.getAbsolutePath(), productStream);
184-
185-
assertTrue(ComputeEngineCredentials.checkStaticGceDetection(testProvider));
174+
checkStaticGceDetection("linux", "Googlekdjsfhg", true);
186175
}
187176

188177
@Test
189178
void getDefaultCredentials_static_windows_configuredAsLinux_notGce() {
190-
TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider();
191-
testProvider.setProperty("os.name", "windows");
192-
InputStream productStream = new ByteArrayInputStream("Googlekdjsfhg".getBytes());
193-
testProvider.addFile(SMBIOS_PATH_LINUX, productStream);
194-
195-
assertFalse(ComputeEngineCredentials.checkStaticGceDetection(testProvider));
179+
checkStaticGceDetection("windows", "Googlekdjsfhg", false);
196180
}
197181

198182
@Test
199183
void getDefaultCredentials_static_unsupportedPlatform_notGce() {
184+
checkStaticGceDetection("macos", "Googlekdjsfhg", false);
185+
}
186+
187+
private void checkStaticGceDetection(String osName, String productContent, boolean expected) {
200188
TestDefaultCredentialsProvider testProvider = new TestDefaultCredentialsProvider();
201-
testProvider.setProperty("os.name", "macos");
202-
InputStream productStream = new ByteArrayInputStream("Googlekdjsfhg".getBytes());
203-
testProvider.addFile(SMBIOS_PATH_LINUX, productStream);
189+
testProvider.setProperty("os.name", osName);
190+
String productFilePath = SMBIOS_PATH_LINUX;
191+
InputStream productStream = new ByteArrayInputStream(productContent.getBytes());
192+
testProvider.addFile(productFilePath, productStream);
204193

205-
assertFalse(ComputeEngineCredentials.checkStaticGceDetection(testProvider));
194+
assertEquals(expected, ComputeEngineCredentials.checkStaticGceDetection(testProvider));
206195
}
207196

208197
@Test
@@ -359,7 +348,7 @@ void getDefaultCredentials_GdchServiceAccount() throws IOException {
359348
MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory();
360349
InputStream gdchServiceAccountStream =
361350
GdchCredentialsTest.writeGdchServiceAccountStream(
362-
GDCH_SA_FORMAT_VERSION,
351+
GdchCredentials.SUPPORTED_JSON_FORMAT_VERSION,
363352
GDCH_SA_PROJECT_ID,
364353
GDCH_SA_PRIVATE_KEY_ID,
365354
GDCH_SA_PRIVATE_KEY_PKC8,

0 commit comments

Comments
 (0)