-
Notifications
You must be signed in to change notification settings - Fork 269
feat: Adds an X509 certificate provider in the auth libraries. #1624
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
Merged
Merged
Changes from 6 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
bc27d41
Adds an X509 certificate provider in the auth libraries.
aeitzman 111f2f1
temporarily remove test case
aeitzman 27c497e
Merge branch 'x509' into x509-java
aeitzman 3756021
fixing formatting
aeitzman 74aaca6
Responding to comments and updating tests
aeitzman c544c88
lint fix
aeitzman c54fedb
Update oauth2_http/java/com/google/auth/mtls/X509Provider.java
aeitzman 889015c
responding to review comments
aeitzman b46d2a1
formatting and changing the tests to use assertThrows
aeitzman b7903d0
Responding to PR comments and adding a new exception type.
aeitzman 008425d
removed catch block
aeitzman 2a21fe4
Add comment and fix test asserts
aeitzman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
64 changes: 64 additions & 0 deletions
64
oauth2_http/java/com/google/auth/mtls/WorkloadCertificateConfiguration.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| package com.google.auth.mtls; | ||
|
|
||
| import com.google.api.client.json.GenericJson; | ||
| import com.google.api.client.json.JsonFactory; | ||
| import com.google.api.client.json.JsonObjectParser; | ||
| import com.google.api.client.json.gson.GsonFactory; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.Map; | ||
|
|
||
| public class WorkloadCertificateConfiguration { | ||
aeitzman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| private String certPath; | ||
| private String privateKeyPath; | ||
|
|
||
| public WorkloadCertificateConfiguration(String certPath, String privateKeyPath) { | ||
| this.certPath = certPath; | ||
| this.privateKeyPath = privateKeyPath; | ||
| } | ||
|
|
||
| public String getCertPath() { | ||
| return certPath; | ||
| } | ||
|
|
||
| public String getPrivateKeyPath() { | ||
| return privateKeyPath; | ||
| } | ||
|
|
||
| public static WorkloadCertificateConfiguration fromCertificateConfigurationStream( | ||
| InputStream certConfigStream) throws IOException { | ||
lqiu96 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| JsonFactory jsonFactory = GsonFactory.getDefaultInstance(); | ||
| JsonObjectParser parser = new JsonObjectParser(jsonFactory); | ||
aeitzman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| GenericJson fileContents = | ||
| parser.parseAndClose(certConfigStream, StandardCharsets.UTF_8, GenericJson.class); | ||
|
|
||
| Map<String, Object> certConfigs = (Map<String, Object>) fileContents.get("cert_configs"); | ||
aeitzman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (certConfigs == null) { | ||
| throw new IllegalArgumentException( | ||
| "The cert_configs object must be provided in the certificate configuration file."); | ||
| } | ||
|
|
||
| Map<String, Object> workloadConfig = (Map<String, Object>) certConfigs.get("workload"); | ||
| if (workloadConfig == null) { | ||
| throw new IllegalArgumentException( | ||
| "A workload certificate configuration must be provided in the cert_configs object."); | ||
| } | ||
aeitzman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| String certPath = (String) workloadConfig.get("cert_path"); | ||
| if (certPath.isEmpty() || certPath == null) { | ||
aeitzman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| throw new IllegalArgumentException( | ||
| "The cert_path field must be provided in the workload certificate configuration."); | ||
| } | ||
|
|
||
| String privateKeyPath = (String) workloadConfig.get("key_path"); | ||
| if (privateKeyPath.isEmpty() || privateKeyPath == null) { | ||
| throw new IllegalArgumentException( | ||
| "The key_path field must be provided in the workload certificate configuration."); | ||
| } | ||
|
|
||
| return new WorkloadCertificateConfiguration(certPath, privateKeyPath); | ||
| } | ||
| } | ||
127 changes: 127 additions & 0 deletions
127
oauth2_http/java/com/google/auth/mtls/X509Provider.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| package com.google.auth.mtls; | ||
lqiu96 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| import com.google.api.client.util.SecurityUtils; | ||
| import java.io.File; | ||
| import java.io.FileInputStream; | ||
| import java.io.FileNotFoundException; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.io.SequenceInputStream; | ||
| import java.security.KeyStore; | ||
| import java.util.Locale; | ||
|
|
||
| public class X509Provider { | ||
aeitzman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| static final String CERTIFICATE_CONFIGURATION_ENV_VARIABLE = "GOOGLE_API_CERTIFICATE_CONFIG"; | ||
| static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json"; | ||
| static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud"; | ||
|
|
||
| private String certConfigPathOverride; | ||
|
|
||
| public X509Provider(String certConfigPathOverride) { | ||
| this.certConfigPathOverride = certConfigPathOverride; | ||
| } | ||
|
|
||
| public X509Provider() { | ||
| this.certConfigPathOverride = null; | ||
aeitzman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| public KeyStore getKeyStore() throws IOException { | ||
lqiu96 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| WorkloadCertificateConfiguration workloadCertConfig = getWorkloadCertificateConfiguration(); | ||
|
|
||
| try { | ||
| // Read the certificate and private key file paths into separate streams. | ||
| File certFile = new File(workloadCertConfig.getCertPath()); | ||
| File privateKeyFile = new File(workloadCertConfig.getPrivateKeyPath()); | ||
| InputStream certStream = readStream(certFile); | ||
| InputStream privateKeyStream = readStream(privateKeyFile); | ||
|
|
||
| // Merge the two streams into a single stream. | ||
| SequenceInputStream certAndPrivateKeyStream = | ||
| new SequenceInputStream(certStream, privateKeyStream); | ||
aeitzman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // Build a key store using the combined stream. | ||
| return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream); | ||
| } catch (Exception e) { | ||
| throw new IOException(e); | ||
| } | ||
lqiu96 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| private WorkloadCertificateConfiguration getWorkloadCertificateConfiguration() | ||
| throws IOException { | ||
| File certConfig; | ||
| if (this.certConfigPathOverride != null) { | ||
| certConfig = new File(certConfigPathOverride); | ||
| } else { | ||
| String envCredentialsPath = getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE); | ||
| if (envCredentialsPath != null && !envCredentialsPath.isEmpty()) { | ||
aeitzman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| certConfig = new File(envCredentialsPath); | ||
| } else { | ||
| certConfig = getWellKnownCertificateConfigFile(); | ||
| } | ||
| } | ||
| InputStream certConfigStream = null; | ||
| try { | ||
| if (!isFile(certConfig)) { | ||
andyrzhao marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Path will be put in the message from the catch block below | ||
| throw new IOException("File does not exist."); | ||
| } | ||
| certConfigStream = readStream(certConfig); | ||
| return WorkloadCertificateConfiguration.fromCertificateConfigurationStream(certConfigStream); | ||
| } catch (Exception e) { | ||
| // Although it is also the cause, the message of the caught exception can have very | ||
| // important information for diagnosing errors, so include its message in the | ||
| // outer exception message also. | ||
| throw new IOException( | ||
| String.format( | ||
| "Error reading certificate configuration file value '%s': %s", | ||
| certConfig.getPath(), e.getMessage()), | ||
| e); | ||
| } finally { | ||
| if (certConfigStream != null) { | ||
| certConfigStream.close(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /* | ||
| * Start of methods to allow overriding in the test code to isolate from the environment. | ||
| */ | ||
| boolean isFile(File file) { | ||
| return file.isFile(); | ||
andyrzhao marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| InputStream readStream(File file) throws FileNotFoundException { | ||
| return new FileInputStream(file); | ||
| } | ||
aeitzman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| String getEnv(String name) { | ||
| return System.getenv(name); | ||
| } | ||
|
|
||
| String getOsName() { | ||
| return getProperty("os.name", "").toLowerCase(Locale.US); | ||
| } | ||
|
|
||
| String getProperty(String property, String def) { | ||
| return System.getProperty(property, def); | ||
| } | ||
| /* | ||
| * End of methods to allow overriding in the test code to isolate from the environment. | ||
| */ | ||
|
|
||
| private File getWellKnownCertificateConfigFile() { | ||
lqiu96 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| File cloudConfigPath; | ||
| String envPath = getEnv("CLOUDSDK_CONFIG"); | ||
| if (envPath != null) { | ||
| cloudConfigPath = new File(envPath); | ||
| } else if (getOsName().indexOf("windows") >= 0) { | ||
| File appDataPath = new File(getEnv("APPDATA")); | ||
| cloudConfigPath = new File(appDataPath, CLOUDSDK_CONFIG_DIRECTORY); | ||
| } else { | ||
| File configPath = new File(getProperty("user.home", ""), ".config"); | ||
| cloudConfigPath = new File(configPath, CLOUDSDK_CONFIG_DIRECTORY); | ||
| } | ||
| return new File(cloudConfigPath, WELL_KNOWN_CERTIFICATE_CONFIG_FILE); | ||
| } | ||
| } | ||
146 changes: 146 additions & 0 deletions
146
oauth2_http/javatests/com/google/auth/mtls/WorkloadCertificateConfigurationTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| /* | ||
| * Copyright 2025, Google Inc. All rights reserved. | ||
| * | ||
| * Redistribution and use in source and binary forms, with or without | ||
| * modification, are permitted provided that the following conditions are | ||
| * met: | ||
| * | ||
| * * Redistributions of source code must retain the above copyright | ||
| * notice, this list of conditions and the following disclaimer. | ||
| * * Redistributions in binary form must reproduce the above | ||
| * copyright notice, this list of conditions and the following disclaimer | ||
| * in the documentation and/or other materials provided with the | ||
| * distribution. | ||
| * | ||
| * * Neither the name of Google Inc. nor the names of its | ||
| * contributors may be used to endorse or promote products derived from | ||
| * this software without specific prior written permission. | ||
| * | ||
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
| */ | ||
|
|
||
| package com.google.auth.mtls; | ||
|
|
||
| import static org.junit.Assert.assertEquals; | ||
| import static org.junit.Assert.assertNotNull; | ||
| import static org.junit.Assert.fail; | ||
|
|
||
| import com.google.api.client.json.GenericJson; | ||
| import com.google.auth.TestUtils; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import org.junit.Test; | ||
|
|
||
| public class WorkloadCertificateConfigurationTest { | ||
|
|
||
| @Test | ||
| public void workloadCertificateConfig_fromStream_Succeeds() throws IOException { | ||
| String certPath = "cert.crt"; | ||
| String privateKeyPath = "key.crt"; | ||
| InputStream configStream = writeWorkloadCertificateConfigStream(certPath, privateKeyPath); | ||
|
|
||
| WorkloadCertificateConfiguration config = | ||
| WorkloadCertificateConfiguration.fromCertificateConfigurationStream(configStream); | ||
| assertNotNull(config); | ||
| } | ||
|
|
||
| @Test | ||
| public void workloadCertificateConfig_fromStreamMissingCertPath_Fails() throws IOException { | ||
| String certPath = ""; | ||
| String privateKeyPath = "key.crt"; | ||
| InputStream configStream = writeWorkloadCertificateConfigStream(certPath, privateKeyPath); | ||
|
|
||
| try { | ||
| WorkloadCertificateConfiguration config = | ||
| WorkloadCertificateConfiguration.fromCertificateConfigurationStream(configStream); | ||
| fail("Expected an error due to missing the certificate path field."); | ||
| } catch (IllegalArgumentException e) { | ||
| assertEquals( | ||
| "The cert_path field must be provided in the workload certificate configuration.", | ||
| e.getMessage()); | ||
| } | ||
aeitzman marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| @Test | ||
| public void workloadCertificateConfig_fromStreamMissingPrivateKeyPath_Fails() throws IOException { | ||
| String certPath = "cert.crt"; | ||
| String privateKeyPath = ""; | ||
| InputStream configStream = writeWorkloadCertificateConfigStream(certPath, privateKeyPath); | ||
|
|
||
| try { | ||
| WorkloadCertificateConfiguration config = | ||
| WorkloadCertificateConfiguration.fromCertificateConfigurationStream(configStream); | ||
| fail("Expected an error due to missing the private key path field."); | ||
| } catch (IllegalArgumentException e) { | ||
| assertEquals( | ||
| "The key_path field must be provided in the workload certificate configuration.", | ||
| e.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void workloadCertificateConfig_fromStreamMissingWorkload_Fails() throws IOException { | ||
| GenericJson json = new GenericJson(); | ||
| json.put("cert_configs", new GenericJson()); | ||
| InputStream configStream = TestUtils.jsonToInputStream(json); | ||
|
|
||
| try { | ||
| WorkloadCertificateConfiguration config = | ||
| WorkloadCertificateConfiguration.fromCertificateConfigurationStream(configStream); | ||
| fail("Expected an error due to missing the workload field."); | ||
| } catch (IllegalArgumentException e) { | ||
| assertEquals( | ||
| "A workload certificate configuration must be provided in the cert_configs object.", | ||
| e.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void workloadCertificateConfig_fromStreamMissingCertConfig_Fails() throws IOException { | ||
| GenericJson json = new GenericJson(); | ||
| InputStream configStream = TestUtils.jsonToInputStream(json); | ||
|
|
||
| try { | ||
| WorkloadCertificateConfiguration config = | ||
| WorkloadCertificateConfiguration.fromCertificateConfigurationStream(configStream); | ||
| fail("Expected an error due to missing the cert_config field."); | ||
| } catch (IllegalArgumentException e) { | ||
| assertEquals( | ||
| "The cert_configs object must be provided in the certificate configuration file.", | ||
| e.getMessage()); | ||
| } | ||
| } | ||
|
|
||
| static InputStream writeWorkloadCertificateConfigStream(String certPath, String privateKeyPath) | ||
| throws IOException { | ||
| GenericJson json = writeWorkloadCertificateConfigJson(certPath, privateKeyPath); | ||
| return TestUtils.jsonToInputStream(json); | ||
| } | ||
|
|
||
| private static GenericJson writeWorkloadCertificateConfigJson( | ||
| String certPath, String privateKeyPath) { | ||
| GenericJson json = new GenericJson(); | ||
| json.put("version", 1); | ||
| GenericJson certConfigs = new GenericJson(); | ||
| GenericJson workloadConfig = new GenericJson(); | ||
| if (certPath != null) { | ||
| workloadConfig.put("cert_path", certPath); | ||
| } | ||
| if (privateKeyPath != null) { | ||
| workloadConfig.put("key_path", privateKeyPath); | ||
| } | ||
| certConfigs.put("workload", workloadConfig); | ||
| json.put("cert_configs", certConfigs); | ||
| return json; | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.