Skip to content

Commit 51762f2

Browse files
feat: add mtls support for NetHttpTransport (#1147)
* feat: support keystore in transport for mtls * fix format * update code * add tests * update test and doc * update names * create keystore from cert and key string * change certAndKey from string to inputstream * add mtls file * Update google-http-client/src/main/java/com/google/api/client/http/javanet/NetHttpTransport.java Co-authored-by: Jeff Ching <chingor@google.com> * Update google-http-client/src/main/java/com/google/api/client/http/javanet/NetHttpTransport.java Co-authored-by: Jeff Ching <chingor@google.com> * Update google-http-client/src/main/java/com/google/api/client/util/SslUtils.java Co-authored-by: Jeff Ching <chingor@google.com> * Update google-http-client/src/main/java/com/google/api/client/util/SslUtils.java Co-authored-by: Jeff Ching <chingor@google.com> * Update google-http-client/src/test/java/com/google/api/client/util/SecurityUtilsTest.java Co-authored-by: Jeff Ching <chingor@google.com> * Update google-http-client/src/main/java/com/google/api/client/util/SslUtils.java Co-authored-by: Jeff Ching <chingor@google.com> * update the code * fix name Co-authored-by: Jeff Ching <chingor@google.com>
1 parent b5754a4 commit 51762f2

File tree

9 files changed

+278
-6
lines changed

9 files changed

+278
-6
lines changed

google-http-client/src/main/java/com/google/api/client/http/HttpTransport.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,15 @@ public boolean supportsMethod(String method) throws IOException {
129129
return Arrays.binarySearch(SUPPORTED_METHODS, method) >= 0;
130130
}
131131

132+
/**
133+
* Returns whether the transport is mTLS.
134+
*
135+
* @return boolean indicating if the transport is mTLS.
136+
*/
137+
public boolean isMtls() {
138+
return false;
139+
}
140+
132141
/**
133142
* Builds a low level HTTP request for the given HTTP method.
134143
*

google-http-client/src/main/java/com/google/api/client/http/javanet/NetHttpTransport.java

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,16 @@ private static Proxy defaultProxy() {
8989
/** Host name verifier or {@code null} for the default. */
9090
private final HostnameVerifier hostnameVerifier;
9191

92+
/** Whether the transport is mTLS. Default value is {@code false}. */
93+
private final boolean isMtls;
94+
9295
/**
9396
* Constructor with the default behavior.
9497
*
9598
* <p>Instead use {@link Builder} to modify behavior.
9699
*/
97100
public NetHttpTransport() {
98-
this((ConnectionFactory) null, null, null);
101+
this((ConnectionFactory) null, null, null, false);
99102
}
100103

101104
/**
@@ -104,26 +107,33 @@ public NetHttpTransport() {
104107
* system properties</a>
105108
* @param sslSocketFactory SSL socket factory or {@code null} for the default
106109
* @param hostnameVerifier host name verifier or {@code null} for the default
110+
* @param isMtls Whether the transport is mTLS. Default value is {@code false}
107111
*/
108112
NetHttpTransport(
109-
Proxy proxy, SSLSocketFactory sslSocketFactory, HostnameVerifier hostnameVerifier) {
110-
this(new DefaultConnectionFactory(proxy), sslSocketFactory, hostnameVerifier);
113+
Proxy proxy,
114+
SSLSocketFactory sslSocketFactory,
115+
HostnameVerifier hostnameVerifier,
116+
boolean isMtls) {
117+
this(new DefaultConnectionFactory(proxy), sslSocketFactory, hostnameVerifier, isMtls);
111118
}
112119

113120
/**
114121
* @param connectionFactory factory to produce connections from {@link URL}s; if {@code null} then
115122
* {@link DefaultConnectionFactory} is used
116123
* @param sslSocketFactory SSL socket factory or {@code null} for the default
117124
* @param hostnameVerifier host name verifier or {@code null} for the default
125+
* @param isMtls Whether the transport is mTLS. Default value is {@code false}
118126
* @since 1.20
119127
*/
120128
NetHttpTransport(
121129
ConnectionFactory connectionFactory,
122130
SSLSocketFactory sslSocketFactory,
123-
HostnameVerifier hostnameVerifier) {
131+
HostnameVerifier hostnameVerifier,
132+
boolean isMtls) {
124133
this.connectionFactory = getConnectionFactory(connectionFactory);
125134
this.sslSocketFactory = sslSocketFactory;
126135
this.hostnameVerifier = hostnameVerifier;
136+
this.isMtls = isMtls;
127137
}
128138

129139
private ConnectionFactory getConnectionFactory(ConnectionFactory connectionFactory) {
@@ -141,6 +151,11 @@ public boolean supportsMethod(String method) {
141151
return Arrays.binarySearch(SUPPORTED_METHODS, method) >= 0;
142152
}
143153

154+
@Override
155+
public boolean isMtls() {
156+
return this.isMtls;
157+
}
158+
144159
@Override
145160
protected NetHttpRequest buildRequest(String method, String url) throws IOException {
146161
Preconditions.checkArgument(supportsMethod(method), "HTTP method %s not supported", method);
@@ -189,6 +204,9 @@ public static final class Builder {
189204
*/
190205
private ConnectionFactory connectionFactory;
191206

207+
/** Whether the transport is mTLS. Default value is {@code false}. */
208+
private boolean isMtls;
209+
192210
/**
193211
* Sets the HTTP proxy or {@code null} to use the proxy settings from <a
194212
* href="http://docs.oracle.com/javase/7/docs/api/java/net/doc-files/net-properties.html">system
@@ -275,6 +293,33 @@ public Builder trustCertificates(KeyStore trustStore) throws GeneralSecurityExce
275293
return setSslSocketFactory(sslContext.getSocketFactory());
276294
}
277295

296+
/**
297+
* Sets the SSL socket factory based on a root certificate trust store and a client certificate
298+
* key store. The client certificate key store will be used to establish mutual TLS.
299+
*
300+
* @param trustStore certificate trust store (use for example {@link SecurityUtils#loadKeyStore}
301+
* or {@link SecurityUtils#loadKeyStoreFromCertificates})
302+
* @param mtlsKeyStore key store for client certificate and key to establish mutual TLS. (use
303+
* for example {@link SecurityUtils#createMtlsKeyStore(InputStream)})
304+
* @param mtlsKeyStorePassword password for mtlsKeyStore parameter
305+
*/
306+
public Builder trustCertificates(
307+
KeyStore trustStore, KeyStore mtlsKeyStore, String mtlsKeyStorePassword)
308+
throws GeneralSecurityException {
309+
if (mtlsKeyStore != null && mtlsKeyStore.size() > 0) {
310+
this.isMtls = true;
311+
}
312+
SSLContext sslContext = SslUtils.getTlsSslContext();
313+
SslUtils.initSslContext(
314+
sslContext,
315+
trustStore,
316+
SslUtils.getPkixTrustManagerFactory(),
317+
mtlsKeyStore,
318+
mtlsKeyStorePassword,
319+
SslUtils.getDefaultKeyManagerFactory());
320+
return setSslSocketFactory(sslContext.getSocketFactory());
321+
}
322+
278323
/**
279324
* {@link Beta} <br>
280325
* Disables validating server SSL certificates by setting the SSL socket factory using {@link
@@ -319,8 +364,8 @@ public NetHttpTransport build() {
319364
setProxy(defaultProxy());
320365
}
321366
return this.proxy == null
322-
? new NetHttpTransport(connectionFactory, sslSocketFactory, hostnameVerifier)
323-
: new NetHttpTransport(this.proxy, sslSocketFactory, hostnameVerifier);
367+
? new NetHttpTransport(connectionFactory, sslSocketFactory, hostnameVerifier, isMtls)
368+
: new NetHttpTransport(this.proxy, sslSocketFactory, hostnameVerifier, isMtls);
324369
}
325370
}
326371
}

google-http-client/src/main/java/com/google/api/client/util/SecurityUtils.java

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.io.ByteArrayInputStream;
1818
import java.io.IOException;
1919
import java.io.InputStream;
20+
import java.io.InputStreamReader;
2021
import java.security.GeneralSecurityException;
2122
import java.security.InvalidKeyException;
2223
import java.security.KeyFactory;
@@ -31,6 +32,7 @@
3132
import java.security.cert.CertificateException;
3233
import java.security.cert.CertificateFactory;
3334
import java.security.cert.X509Certificate;
35+
import java.security.spec.PKCS8EncodedKeySpec;
3436
import java.util.List;
3537
import javax.net.ssl.X509TrustManager;
3638

@@ -258,5 +260,59 @@ public static void loadKeyStoreFromCertificates(
258260
}
259261
}
260262

263+
/**
264+
* Create a keystore for mutual TLS with the certificate and private key provided.
265+
*
266+
* @param certAndKey Certificate and private key input stream. The stream should contain one
267+
* certificate and one unencrypted private key. If there are multiple certificates, only the
268+
* first certificate will be used.
269+
* @return keystore for mutual TLS.
270+
*/
271+
public static KeyStore createMtlsKeyStore(InputStream certAndKey)
272+
throws GeneralSecurityException, IOException {
273+
KeyStore keystore = KeyStore.getInstance("JKS");
274+
keystore.load(null);
275+
276+
PemReader.Section certSection = null;
277+
PemReader.Section keySection = null;
278+
PemReader reader = new PemReader(new InputStreamReader(certAndKey));
279+
280+
while (certSection == null || keySection == null) {
281+
// Read the certificate and private key.
282+
PemReader.Section section = reader.readNextSection();
283+
if (section == null) {
284+
break;
285+
}
286+
287+
if (certSection == null && "CERTIFICATE".equals(section.getTitle())) {
288+
certSection = section;
289+
} else if ("PRIVATE KEY".equals(section.getTitle())) {
290+
keySection = section;
291+
}
292+
}
293+
294+
if (certSection == null) {
295+
throw new IllegalArgumentException("certificate is missing from certAndKey string");
296+
}
297+
if (keySection == null) {
298+
throw new IllegalArgumentException("private key is missing from certAndKey string");
299+
}
300+
301+
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
302+
X509Certificate cert =
303+
(X509Certificate)
304+
certFactory.generateCertificate(
305+
new ByteArrayInputStream(certSection.getBase64DecodedBytes()));
306+
307+
PKCS8EncodedKeySpec keySpecPKCS8 = new PKCS8EncodedKeySpec(keySection.getBase64DecodedBytes());
308+
PrivateKey key =
309+
KeyFactory.getInstance(cert.getPublicKey().getAlgorithm()).generatePrivate(keySpecPKCS8);
310+
311+
// Fit the certificate and private key into the keystore.
312+
keystore.setKeyEntry("alias", key, new char[] {}, new X509Certificate[] {cert});
313+
314+
return keystore;
315+
}
316+
261317
private SecurityUtils() {}
262318
}

google-http-client/src/main/java/com/google/api/client/util/SslUtils.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,36 @@ public static SSLContext initSslContext(
109109
return sslContext;
110110
}
111111

112+
/**
113+
* Initializes the SSL context to the trust managers supplied by the trust manager factory for the
114+
* given trust store, and to the key managers supplied by the key manager factory for the given
115+
* key store.
116+
*
117+
* @param sslContext SSL context (for example {@link SSLContext#getInstance})
118+
* @param trustStore key store for certificates to trust (for example {@link
119+
* SecurityUtils#getJavaKeyStore()})
120+
* @param trustManagerFactory trust manager factory (for example {@link
121+
* #getPkixTrustManagerFactory()})
122+
* @param mtlsKeyStore key store for client certificate and key to establish mutual TLS
123+
* @param mtlsKeyStorePassword password for mtlsKeyStore parameter
124+
* @param keyManagerFactory key manager factory (for example {@link
125+
* #getDefaultKeyManagerFactory()})
126+
*/
127+
public static SSLContext initSslContext(
128+
SSLContext sslContext,
129+
KeyStore trustStore,
130+
TrustManagerFactory trustManagerFactory,
131+
KeyStore mtlsKeyStore,
132+
String mtlsKeyStorePassword,
133+
KeyManagerFactory keyManagerFactory)
134+
throws GeneralSecurityException {
135+
trustManagerFactory.init(trustStore);
136+
keyManagerFactory.init(mtlsKeyStore, mtlsKeyStorePassword.toCharArray());
137+
sslContext.init(
138+
keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
139+
return sslContext;
140+
}
141+
112142
/**
113143
* {@link Beta} <br>
114144
* Returns an SSL context in which all X.509 certificates are trusted.

google-http-client/src/test/java/com/google/api/client/http/javanet/NetHttpTransportTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.io.InputStream;
2424
import java.net.HttpURLConnection;
2525
import java.net.URL;
26+
import java.security.KeyStore;
2627
import junit.framework.TestCase;
2728

2829
/**
@@ -36,6 +37,32 @@ public class NetHttpTransportTest extends TestCase {
3637
"GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE"
3738
};
3839

40+
public void testNotMtlsWithoutClientCert() throws Exception {
41+
KeyStore trustStore = KeyStore.getInstance("JKS");
42+
43+
NetHttpTransport transport =
44+
new NetHttpTransport.Builder().trustCertificates(trustStore).build();
45+
assertFalse(transport.isMtls());
46+
}
47+
48+
public void testIsMtlsWithClientCert() throws Exception {
49+
KeyStore trustStore = KeyStore.getInstance("JKS");
50+
KeyStore keyStore = KeyStore.getInstance("PKCS12");
51+
52+
// Load client certificate and private key from secret.p12 file.
53+
keyStore.load(
54+
this.getClass()
55+
.getClassLoader()
56+
.getResourceAsStream("com/google/api/client/util/secret.p12"),
57+
"notasecret".toCharArray());
58+
59+
NetHttpTransport transport =
60+
new NetHttpTransport.Builder()
61+
.trustCertificates(trustStore, keyStore, "notasecret")
62+
.build();
63+
assertTrue(transport.isMtls());
64+
}
65+
3966
public void testExecute_mock() throws Exception {
4067
for (String method : METHODS) {
4168
boolean isPutOrPost = method.equals("PUT") || method.equals("POST");

google-http-client/src/test/java/com/google/api/client/util/SecurityUtilsTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import com.google.api.client.testing.json.webtoken.TestCertificates;
1818
import com.google.api.client.testing.util.SecurityTestUtils;
1919
import java.io.ByteArrayInputStream;
20+
import java.io.InputStream;
21+
import java.security.KeyStore;
2022
import java.security.PrivateKey;
2123
import java.security.Signature;
2224
import java.security.cert.X509Certificate;
@@ -160,4 +162,47 @@ public void testVerifyX509() throws Exception {
160162
public void testVerifyX509WrongCa() throws Exception {
161163
assertNull(verifyX509(TestCertificates.BOGUS_CA_CERT));
162164
}
165+
166+
public void testCreateMtlsKeyStoreNoCert() throws Exception {
167+
final InputStream certMissing =
168+
getClass()
169+
.getClassLoader()
170+
.getResourceAsStream("com/google/api/client/util/privateKey.pem");
171+
172+
boolean thrown = false;
173+
try {
174+
SecurityUtils.createMtlsKeyStore(certMissing);
175+
fail("should have thrown");
176+
} catch (IllegalArgumentException e) {
177+
assertTrue(e.getMessage().contains("certificate is missing from certAndKey string"));
178+
thrown = true;
179+
}
180+
assertTrue("should have caught an IllegalArgumentException", thrown);
181+
}
182+
183+
public void testCreateMtlsKeyStoreNoPrivateKey() throws Exception {
184+
final InputStream privateKeyMissing =
185+
getClass().getClassLoader().getResourceAsStream("com/google/api/client/util/cert.pem");
186+
187+
boolean thrown = false;
188+
try {
189+
SecurityUtils.createMtlsKeyStore(privateKeyMissing);
190+
fail("should have thrown");
191+
} catch (IllegalArgumentException e) {
192+
assertTrue(e.getMessage().contains("private key is missing from certAndKey string"));
193+
thrown = true;
194+
}
195+
assertTrue("should have caught an IllegalArgumentException", thrown);
196+
}
197+
198+
public void testCreateMtlsKeyStoreSuccess() throws Exception {
199+
InputStream certAndKey =
200+
getClass()
201+
.getClassLoader()
202+
.getResourceAsStream("com/google/api/client/util/mtlsCertAndKey.pem");
203+
204+
KeyStore mtlsKeyStore = SecurityUtils.createMtlsKeyStore(certAndKey);
205+
206+
assertEquals(1, mtlsKeyStore.size());
207+
}
163208
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIICGzCCAYSgAwIBAgIIWrt6xtmHPs4wDQYJKoZIhvcNAQEFBQAwMzExMC8GA1UE
3+
AxMoMTAwOTEyMDcyNjg3OC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbTAeFw0x
4+
MjEyMDExNjEwNDRaFw0yMjExMjkxNjEwNDRaMDMxMTAvBgNVBAMTKDEwMDkxMjA3
5+
MjY4NzguYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20wgZ8wDQYJKoZIhvcNAQEB
6+
BQADgY0AMIGJAoGBAL1SdY8jTUVU7O4/XrZLYTw0ON1lV6MQRGajFDFCqD2Fd9tQ
7+
GLW8Iftx9wfXe1zuaehJSgLcyCxazfyJoN3RiONBihBqWY6d3lQKqkgsRTNZkdFJ
8+
Wdzl/6CxhK9sojh2p0r3tydtv9iwq5fuuWIvtODtT98EgphhncQAqkKoF3zVAgMB
9+
AAGjODA2MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQM
10+
MAoGCCsGAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4GBAD8XQEqzGePa9VrvtEGpf+R4
11+
fkxKbcYAzqYq202nKu0kfjhIYkYSBj6gi348YaxE64yu60TVl42l5HThmswUheW4
12+
uQIaq36JvwvsDP5Zoj5BgiNSnDAFQp+jJFBRUA5vooJKgKgMDf/r/DCOsbO6VJF1
13+
kWwa9n19NFiV0z3m6isj
14+
-----END CERTIFICATE-----
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIICGzCCAYSgAwIBAgIIWrt6xtmHPs4wDQYJKoZIhvcNAQEFBQAwMzExMC8GA1UE
3+
AxMoMTAwOTEyMDcyNjg3OC5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbTAeFw0x
4+
MjEyMDExNjEwNDRaFw0yMjExMjkxNjEwNDRaMDMxMTAvBgNVBAMTKDEwMDkxMjA3
5+
MjY4NzguYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20wgZ8wDQYJKoZIhvcNAQEB
6+
BQADgY0AMIGJAoGBAL1SdY8jTUVU7O4/XrZLYTw0ON1lV6MQRGajFDFCqD2Fd9tQ
7+
GLW8Iftx9wfXe1zuaehJSgLcyCxazfyJoN3RiONBihBqWY6d3lQKqkgsRTNZkdFJ
8+
Wdzl/6CxhK9sojh2p0r3tydtv9iwq5fuuWIvtODtT98EgphhncQAqkKoF3zVAgMB
9+
AAGjODA2MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQM
10+
MAoGCCsGAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4GBAD8XQEqzGePa9VrvtEGpf+R4
11+
fkxKbcYAzqYq202nKu0kfjhIYkYSBj6gi348YaxE64yu60TVl42l5HThmswUheW4
12+
uQIaq36JvwvsDP5Zoj5BgiNSnDAFQp+jJFBRUA5vooJKgKgMDf/r/DCOsbO6VJF1
13+
kWwa9n19NFiV0z3m6isj
14+
-----END CERTIFICATE-----
15+
-----BEGIN PRIVATE KEY-----
16+
MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAL1SdY8jTUVU7O4/
17+
XrZLYTw0ON1lV6MQRGajFDFCqD2Fd9tQGLW8Iftx9wfXe1zuaehJSgLcyCxazfyJ
18+
oN3RiONBihBqWY6d3lQKqkgsRTNZkdFJWdzl/6CxhK9sojh2p0r3tydtv9iwq5fu
19+
uWIvtODtT98EgphhncQAqkKoF3zVAgMBAAECgYB51B9cXe4yiGTzJ4pOKpHGySAy
20+
sC1F/IjXt2eeD3PuKv4m/hL4l7kScpLx0+NJuQ4j8U2UK/kQOdrGANapB1ZbMZAK
21+
/q0xmIUzdNIDiGSoTXGN2mEfdsEpQ/Xiv0lyhYBBPC/K4sYIpHccnhSRQUZlWLLY
22+
lE5cFNKC9b7226mNvQJBAPt0hfCNIN0kUYOA9jdLtx7CE4ySGMPf5KPBuzPd8ty1
23+
fxaFm9PB7B76VZQYmHcWy8rT5XjoLJHrmGW1ZvP+iDsCQQDAvnKoarPOGb5iJfkq
24+
RrA4flf1TOlf+1+uqIOJ94959jkkJeb0gv/TshDnm6/bWn+1kJylQaKygCizwPwB
25+
Z84vAkA0Duur4YvsPJijoQ9YY1SGCagCcjyuUKwFOxaGpmyhRPIKt56LOJqpzyno
26+
fy8ReKa4VyYq4eZYT249oFCwMwIBAkAROPNF2UL3x5UbcAkznd1hLujtIlI4IV4L
27+
XUNjsJtBap7we/KHJq11XRPlniO4lf2TW7iji5neGVWJulTKS1xBAkAerktk4Hsw
28+
ErUaUG1s/d+Sgc8e/KMeBElV+NxGhcWEeZtfHMn/6VOlbzY82JyvC9OKC80A5CAE
29+
VUV6b25kqrcu
30+
-----END PRIVATE KEY-----

0 commit comments

Comments
 (0)