Skip to content

Commit bc27d41

Browse files
committed
Adds an X509 certificate provider in the auth libraries.
1 parent 9a7c2e0 commit bc27d41

File tree

3 files changed

+355
-0
lines changed

3 files changed

+355
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.google.auth.mtls;
2+
3+
import com.google.api.client.json.GenericJson;
4+
import com.google.api.client.json.JsonFactory;
5+
import com.google.api.client.json.JsonObjectParser;
6+
import com.google.api.client.json.gson.GsonFactory;
7+
import java.io.IOException;
8+
import java.io.InputStream;
9+
import java.nio.charset.StandardCharsets;
10+
import java.util.Map;
11+
12+
public class WorkloadCertificateConfiguration {
13+
14+
private String certPath;
15+
private String privateKeyPath;
16+
17+
public WorkloadCertificateConfiguration(String certPath, String privateKeyPath) {
18+
this.certPath = certPath;
19+
this.privateKeyPath = privateKeyPath;
20+
}
21+
22+
public String getCertPath() {
23+
return certPath;
24+
}
25+
26+
public String getPrivateKeyPath() {
27+
return privateKeyPath;
28+
}
29+
30+
public static WorkloadCertificateConfiguration fromCertificateConfigurationStream (InputStream certConfigStream) throws IOException {
31+
JsonFactory jsonFactory = GsonFactory.getDefaultInstance();
32+
JsonObjectParser parser = new JsonObjectParser(jsonFactory);
33+
34+
GenericJson fileContents =
35+
parser.parseAndClose(certConfigStream, StandardCharsets.UTF_8, GenericJson.class);
36+
37+
Map<String, Object> certConfigs = (Map<String, Object>) fileContents.get("cert_configs");
38+
Map<String, Object> workloadConfig = (Map<String, Object>) certConfigs.get("workload");
39+
40+
String certPath = (String) workloadConfig.get("cert_path");
41+
String privateKeyPath = (String) workloadConfig.get("key_path");
42+
43+
return new WorkloadCertificateConfiguration(certPath, privateKeyPath);
44+
}
45+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package com.google.auth.mtls;
2+
3+
import com.google.api.client.util.SecurityUtils;
4+
import java.io.File;
5+
import java.io.FileInputStream;
6+
import java.io.FileNotFoundException;
7+
import java.io.IOException;
8+
import java.io.InputStream;
9+
import java.io.SequenceInputStream;
10+
import java.security.KeyStore;
11+
import java.util.Locale;
12+
13+
public class X509Provider {
14+
static final String CERTIFICATE_CONFIGURATION_ENV_VARIABLE = "GOOGLE_API_CERTIFICATE_CONFIG";
15+
static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json";
16+
static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud";
17+
18+
private String certConfigPathOverride;
19+
20+
public X509Provider(String certConfigPathOverride) {
21+
this.certConfigPathOverride = certConfigPathOverride;
22+
}
23+
24+
public X509Provider(){
25+
this.certConfigPathOverride = null;
26+
}
27+
28+
public KeyStore getKeyStore() throws IOException {
29+
30+
WorkloadCertificateConfiguration workloadCertConfig = getWorkloadCertificateConfiguration();
31+
32+
try {
33+
// Read the certificate and private key file paths into separate streams.
34+
File certFile = new File(workloadCertConfig.getCertPath());
35+
File privateKeyFile = new File(workloadCertConfig.getPrivateKeyPath());
36+
InputStream certStream = readStream(certFile);
37+
InputStream privateKeyStream = readStream(privateKeyFile);
38+
39+
// Merge the two streams into a single stream.
40+
SequenceInputStream certAndPrivateKeyStream = new SequenceInputStream(certStream, privateKeyStream);
41+
42+
// Build a key store using the combined stream.
43+
return SecurityUtils.createMtlsKeyStore(certAndPrivateKeyStream);
44+
} catch (Exception e) {
45+
throw new IOException(e);
46+
}
47+
}
48+
49+
private WorkloadCertificateConfiguration getWorkloadCertificateConfiguration() throws IOException {
50+
String envCredentialsPath = getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE);
51+
File certConfig;
52+
if (this.certConfigPathOverride != null) {
53+
certConfig = new File(certConfigPathOverride);
54+
} else if (envCredentialsPath != null && envCredentialsPath.length() > 0) {
55+
certConfig = new File(envCredentialsPath);
56+
} else {
57+
certConfig = getWellKnownCertificateConfigFile();
58+
}
59+
InputStream certConfigStream = null;
60+
try {
61+
if (!isFile(certConfig)) {
62+
// Path will be put in the message from the catch block below
63+
throw new IOException("File does not exist.");
64+
}
65+
certConfigStream = readStream(certConfig);
66+
return WorkloadCertificateConfiguration.fromCertificateConfigurationStream(certConfigStream);
67+
} catch (Exception e) {
68+
// Although it is also the cause, the message of the caught exception can have very
69+
// important information for diagnosing errors, so include its message in the
70+
// outer exception message also.
71+
throw new IOException(
72+
String.format(
73+
"Error reading certificate configuration file value '%s': %s",
74+
certConfig.getPath(), e.getMessage()),
75+
e);
76+
} finally {
77+
if (certConfigStream != null) {
78+
certConfigStream.close();
79+
}
80+
}
81+
}
82+
83+
boolean isFile(File file) {
84+
return file.isFile();
85+
}
86+
87+
InputStream readStream(File file) throws FileNotFoundException {
88+
return new FileInputStream(file);
89+
}
90+
91+
String getEnv(String name) {
92+
return System.getenv(name);
93+
}
94+
95+
String getOsName() {
96+
return getProperty("os.name", "").toLowerCase(Locale.US);
97+
}
98+
99+
String getProperty(String property, String def) {
100+
return System.getProperty(property, def);
101+
}
102+
103+
File getWellKnownCertificateConfigFile() {
104+
File cloudConfigPath;
105+
String envPath = getEnv("CLOUDSDK_CONFIG");
106+
if (envPath != null) {
107+
cloudConfigPath = new File(envPath);
108+
} else if (getOsName().indexOf("windows") >= 0) {
109+
File appDataPath = new File(getEnv("APPDATA"));
110+
cloudConfigPath = new File(appDataPath, CLOUDSDK_CONFIG_DIRECTORY);
111+
} else {
112+
File configPath = new File(getProperty("user.home", ""), ".config");
113+
cloudConfigPath = new File(configPath, CLOUDSDK_CONFIG_DIRECTORY);
114+
}
115+
return new File(cloudConfigPath, WELL_KNOWN_CERTIFICATE_CONFIG_FILE);
116+
}
117+
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package com.google.auth.mtls;
2+
3+
import static org.junit.Assert.assertTrue;
4+
import static org.junit.Assert.fail;
5+
6+
import com.google.api.client.json.GenericJson;
7+
import com.google.auth.TestUtils;
8+
import java.io.ByteArrayInputStream;
9+
import java.io.File;
10+
import java.io.FileNotFoundException;
11+
import java.io.IOException;
12+
import java.io.InputStream;
13+
import java.security.KeyStore;
14+
import java.security.KeyStoreException;
15+
import java.security.cert.Certificate;
16+
import java.security.cert.CertificateException;
17+
import java.security.cert.CertificateFactory;
18+
import java.util.HashMap;
19+
import java.util.Map;
20+
import org.junit.Test;
21+
22+
public class X509ProviderTest {
23+
24+
static String TEST_CERT = "-----BEGIN CERTIFICATE-----\n"
25+
+ "MIICGzCCAYSgAwIBAgIIWrt6xtmHPs4wDQYJKoZIhvcNAQEFBQAwMzExMC8GA1UE\n"
26+
+ "AxMoMTAwOTEyMDcyNjg3OC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbTAeFw0x\n"
27+
+ "MjEyMDExNjEwNDRaFw0yMjExMjkxNjEwNDRaMDMxMTAvBgNVBAMTKDEwMDkxMjA3\n"
28+
+ "MjY4NzguYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20wgZ8wDQYJKoZIhvcNAQEB\n"
29+
+ "BQADgY0AMIGJAoGBAL1SdY8jTUVU7O4/XrZLYTw0ON1lV6MQRGajFDFCqD2Fd9tQ\n"
30+
+ "GLW8Iftx9wfXe1zuaehJSgLcyCxazfyJoN3RiONBihBqWY6d3lQKqkgsRTNZkdFJ\n"
31+
+ "Wdzl/6CxhK9sojh2p0r3tydtv9iwq5fuuWIvtODtT98EgphhncQAqkKoF3zVAgMB\n"
32+
+ "AAGjODA2MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQM\n"
33+
+ "MAoGCCsGAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4GBAD8XQEqzGePa9VrvtEGpf+R4\n"
34+
+ "fkxKbcYAzqYq202nKu0kfjhIYkYSBj6gi348YaxE64yu60TVl42l5HThmswUheW4\n"
35+
+ "uQIaq36JvwvsDP5Zoj5BgiNSnDAFQp+jJFBRUA5vooJKgKgMDf/r/DCOsbO6VJF1\n"
36+
+ "kWwa9n19NFiV0z3m6isj\n"
37+
+ "-----END CERTIFICATE-----\n";
38+
39+
static String TEST_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n"
40+
+ "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAL1SdY8jTUVU7O4/\n"
41+
+ "XrZLYTw0ON1lV6MQRGajFDFCqD2Fd9tQGLW8Iftx9wfXe1zuaehJSgLcyCxazfyJ\n"
42+
+ "oN3RiONBihBqWY6d3lQKqkgsRTNZkdFJWdzl/6CxhK9sojh2p0r3tydtv9iwq5fu\n"
43+
+ "uWIvtODtT98EgphhncQAqkKoF3zVAgMBAAECgYB51B9cXe4yiGTzJ4pOKpHGySAy\n"
44+
+ "sC1F/IjXt2eeD3PuKv4m/hL4l7kScpLx0+NJuQ4j8U2UK/kQOdrGANapB1ZbMZAK\n"
45+
+ "/q0xmIUzdNIDiGSoTXGN2mEfdsEpQ/Xiv0lyhYBBPC/K4sYIpHccnhSRQUZlWLLY\n"
46+
+ "lE5cFNKC9b7226mNvQJBAPt0hfCNIN0kUYOA9jdLtx7CE4ySGMPf5KPBuzPd8ty1\n"
47+
+ "fxaFm9PB7B76VZQYmHcWy8rT5XjoLJHrmGW1ZvP+iDsCQQDAvnKoarPOGb5iJfkq\n"
48+
+ "RrA4flf1TOlf+1+uqIOJ94959jkkJeb0gv/TshDnm6/bWn+1kJylQaKygCizwPwB\n"
49+
+ "Z84vAkA0Duur4YvsPJijoQ9YY1SGCagCcjyuUKwFOxaGpmyhRPIKt56LOJqpzyno\n"
50+
+ "fy8ReKa4VyYq4eZYT249oFCwMwIBAkAROPNF2UL3x5UbcAkznd1hLujtIlI4IV4L\n"
51+
+ "XUNjsJtBap7we/KHJq11XRPlniO4lf2TW7iji5neGVWJulTKS1xBAkAerktk4Hsw\n"
52+
+ "ErUaUG1s/d+Sgc8e/KMeBElV+NxGhcWEeZtfHMn/6VOlbzY82JyvC9OKC80A5CAE\n"
53+
+ "VUV6b25kqrcu\n"
54+
+ "-----END PRIVATE KEY-----";
55+
56+
@Test
57+
public void X509Provider_FileDoesntExist_Throws() {
58+
String certConfigPath = "badfile.txt";
59+
X509Provider testProvider = new TestX509Provider(certConfigPath);
60+
String expectedErrorMessage = String.format("Error reading certificate configuration file value '%s': File does not exist.", certConfigPath);
61+
62+
try {
63+
testProvider.getKeyStore();
64+
fail("No key stores expected.");
65+
} catch (IOException e) {
66+
String message = e.getMessage();
67+
assertTrue(message.equals(expectedErrorMessage));
68+
}
69+
}
70+
71+
@Test
72+
public void X509Provider_EmptyFile_Throws() {
73+
String certConfigPath = "certConfig.txt";
74+
InputStream certConfigStream = new ByteArrayInputStream("".getBytes());
75+
TestX509Provider testProvider = new TestX509Provider(certConfigPath);
76+
testProvider.addFile(certConfigPath, certConfigStream);
77+
String expectedErrorMessage = String.format("Error reading certificate configuration file value '%s': no JSON input found", certConfigPath);
78+
79+
try {
80+
testProvider.getKeyStore();
81+
fail("No key store expected.");
82+
} catch (IOException e) {
83+
String message = e.getMessage();
84+
assertTrue(message.equals(expectedErrorMessage));
85+
}
86+
}
87+
88+
@Test
89+
public void X509Provider_EmptyCertFile_Throws() throws IOException {
90+
String certConfigPath = "certConfig.txt";
91+
String certPath = "cert.crt";
92+
String keyPath = "key.crt";
93+
InputStream certConfigStream = writeWorkloadCertificateConfigStream(certPath, keyPath);
94+
95+
TestX509Provider testProvider = new TestX509Provider(certConfigPath);
96+
testProvider.addFile(certConfigPath, certConfigStream);
97+
testProvider.addFile(keyPath, new ByteArrayInputStream(TEST_PRIVATE_KEY.getBytes()));
98+
String expectedErrorMessage = String.format("Error reading certificate configuration file value '%s': no JSON input found", certConfigPath);
99+
100+
try {
101+
testProvider.getKeyStore();
102+
fail("No key store expected.");
103+
} catch (IOException e) {
104+
String message = e.getMessage();
105+
assertTrue(message.equals(expectedErrorMessage));
106+
}
107+
}
108+
109+
@Test
110+
public void X509Provider_Succeeds() throws IOException, KeyStoreException, CertificateException {
111+
String certConfigPath = "certConfig.txt";
112+
String certPath = "cert.crt";
113+
String keyPath = "key.crt";
114+
InputStream certConfigStream = writeWorkloadCertificateConfigStream(certPath, keyPath);
115+
116+
TestX509Provider testProvider = new TestX509Provider(certConfigPath);
117+
testProvider.addFile(certConfigPath, certConfigStream);
118+
testProvider.addFile(certPath, new ByteArrayInputStream(TEST_CERT.getBytes()));
119+
testProvider.addFile(keyPath, new ByteArrayInputStream(TEST_PRIVATE_KEY.getBytes()));
120+
121+
CertificateFactory cf = CertificateFactory.getInstance("X.509");
122+
Certificate expectedCert = cf.generateCertificate(new ByteArrayInputStream(TEST_CERT.getBytes()));
123+
124+
// Assert that the store has the expected certificate and only the expected certificate.
125+
KeyStore store = testProvider.getKeyStore();
126+
assertTrue(store.size() == 1);
127+
assertTrue(store.getCertificateAlias(expectedCert) != null);
128+
}
129+
130+
static InputStream writeWorkloadCertificateConfigStream(
131+
String certPath,
132+
String privateKeyPath)
133+
throws IOException {
134+
GenericJson json =
135+
writeWorkloadCertificateConfigJson(certPath, privateKeyPath);
136+
return TestUtils.jsonToInputStream(json);
137+
}
138+
139+
static GenericJson writeWorkloadCertificateConfigJson(
140+
String certPath,
141+
String privateKeyPath) {
142+
GenericJson json = new GenericJson();
143+
json.put("version", 1);
144+
GenericJson certConfigs = new GenericJson();
145+
GenericJson workloadConfig = new GenericJson();
146+
if (certPath != null) {
147+
workloadConfig.put("cert_path", certPath);
148+
}
149+
if (privateKeyPath != null) {
150+
workloadConfig.put("key_path", privateKeyPath);
151+
}
152+
certConfigs.put("workload", workloadConfig);
153+
json.put("cert_configs", certConfigs);
154+
return json;
155+
}
156+
157+
static class TestX509Provider extends X509Provider {
158+
private final Map<String, InputStream> files = new HashMap<>();
159+
160+
TestX509Provider () {}
161+
162+
TestX509Provider (String filePathOverride) {
163+
super(filePathOverride);
164+
}
165+
166+
void addFile(String file, InputStream stream) {
167+
files.put(file, stream);
168+
}
169+
170+
//@Override
171+
//String getEnv(String name) {
172+
// return variables.get(name);
173+
//}
174+
175+
//void setEnv(String name, String value) {
176+
// variables.put(name, value);
177+
//}
178+
179+
@Override
180+
boolean isFile(File file) {
181+
return files.containsKey(file.getPath());
182+
}
183+
184+
@Override
185+
InputStream readStream(File file) throws FileNotFoundException {
186+
InputStream stream = files.get(file.getPath());
187+
if (stream == null) {
188+
throw new FileNotFoundException(file.getPath());
189+
}
190+
return stream;
191+
}
192+
}
193+
}

0 commit comments

Comments
 (0)