Skip to content
Merged
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 {

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 {
JsonFactory jsonFactory = GsonFactory.getDefaultInstance();
JsonObjectParser parser = new JsonObjectParser(jsonFactory);

GenericJson fileContents =
parser.parseAndClose(certConfigStream, StandardCharsets.UTF_8, GenericJson.class);

Map<String, Object> certConfigs = (Map<String, Object>) fileContents.get("cert_configs");
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.");
}

String certPath = (String) workloadConfig.get("cert_path");
if (certPath.isEmpty() || certPath == null) {
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 oauth2_http/java/com/google/auth/mtls/X509Provider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.google.auth.mtls;

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 {
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;
}

public KeyStore getKeyStore() throws IOException {

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);

// Build a key store using the combined stream.
return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream);
} catch (Exception e) {
throw new IOException(e);
}
}

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()) {
certConfig = new File(envCredentialsPath);
} else {
certConfig = getWellKnownCertificateConfigFile();
}
}
InputStream certConfigStream = null;
try {
if (!isFile(certConfig)) {
// 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();
}

InputStream readStream(File file) throws FileNotFoundException {
return new FileInputStream(file);
}

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() {
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);
}
}
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());
}
}

@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;
}
}
Loading
Loading