1+ /*
2+ * Copyright 2025, Google Inc. All rights reserved.
3+ *
4+ * Redistribution and use in source and binary forms, with or without
5+ * modification, are permitted provided that the following conditions are
6+ * met:
7+ *
8+ * * Redistributions of source code must retain the above copyright
9+ * notice, this list of conditions and the following disclaimer.
10+ * * Redistributions in binary form must reproduce the above
11+ * copyright notice, this list of conditions and the following disclaimer
12+ * in the documentation and/or other materials provided with the
13+ * distribution.
14+ *
15+ * * Neither the name of Google Inc. nor the names of its
16+ * contributors may be used to endorse or promote products derived from
17+ * this software without specific prior written permission.
18+ *
19+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+ */
31+
32+ package com .google .auth .mtls ;
33+
34+ import com .google .api .client .json .JsonParser ;
35+ import com .google .api .client .json .gson .GsonFactory ;
36+ import com .google .api .client .util .SecurityUtils ;
37+ import com .google .common .annotations .VisibleForTesting ;
38+ import com .google .common .collect .ImmutableList ;
39+ import java .io .FileInputStream ;
40+ import java .io .FileNotFoundException ;
41+ import java .io .IOException ;
42+ import java .io .InputStream ;
43+ import java .security .GeneralSecurityException ;
44+ import java .security .KeyStore ;
45+ import java .util .List ;
46+
47+ /**
48+ * Provider class for mutual TLS. It is used to configure the mutual TLS in the transport with the
49+ * default client certificate on device.
50+ */
51+ public class SecureConnectProvider implements MtlsProvider {
52+ interface ProcessProvider {
53+ public Process createProcess (InputStream metadata ) throws IOException ;
54+ }
55+
56+ static class DefaultProcessProvider implements ProcessProvider {
57+ @ Override
58+ public Process createProcess (InputStream metadata ) throws IOException {
59+ if (metadata == null ) {
60+ return null ;
61+ }
62+ List <String > command = extractCertificateProviderCommand (metadata );
63+ return new ProcessBuilder (command ).start ();
64+ }
65+ }
66+
67+ private static final String DEFAULT_CONTEXT_AWARE_METADATA_PATH =
68+ System .getProperty ("user.home" ) + "/.secureConnect/context_aware_metadata.json" ;
69+
70+ private String metadataPath ;
71+ private ProcessProvider processProvider ;
72+
73+ @ VisibleForTesting
74+ SecureConnectProvider (ProcessProvider processProvider , String metadataPath ) {
75+ this .processProvider = processProvider ;
76+ this .metadataPath = metadataPath ;
77+ }
78+
79+ public SecureConnectProvider () {
80+ this (new DefaultProcessProvider (), DEFAULT_CONTEXT_AWARE_METADATA_PATH );
81+ }
82+
83+ /** The mutual TLS key store created with the default client certificate on device. */
84+ @ Override
85+ public KeyStore getKeyStore () throws IOException {
86+ try (InputStream stream = new FileInputStream (metadataPath )) {
87+ return getKeyStore (stream , processProvider );
88+ } catch (InterruptedException e ) {
89+ throw new IOException ("Interrupted executing certificate provider command" , e );
90+ } catch (GeneralSecurityException e ) {
91+ throw new CertificateSourceUnavailableException ("SecureConnect encountered GeneralSecurityException:" , e );
92+ } catch (FileNotFoundException exception ) {
93+ // If the metadata file doesn't exist, then there is no key store, so we will throw sentinel error
94+ throw new CertificateSourceUnavailableException ("SecureConnect metadata does not exist." );
95+ }
96+ }
97+
98+ @ VisibleForTesting
99+ static KeyStore getKeyStore (InputStream metadata , ProcessProvider processProvider )
100+ throws IOException , InterruptedException , GeneralSecurityException {
101+ Process process = processProvider .createProcess (metadata );
102+
103+ // Run the command and timeout after 1000 milliseconds.
104+ int exitCode = runCertificateProviderCommand (process , 1000 );
105+ if (exitCode != 0 ) {
106+ throw new IOException ("Cert provider command failed with exit code: " + exitCode );
107+ }
108+
109+ // Create mTLS key store with the input certificates from shell command.
110+ return SecurityUtils .createMtlsKeyStore (process .getInputStream ());
111+ }
112+
113+ @ VisibleForTesting
114+ static ImmutableList <String > extractCertificateProviderCommand (InputStream contextAwareMetadata )
115+ throws IOException {
116+ JsonParser parser = new GsonFactory ().createJsonParser (contextAwareMetadata );
117+ ContextAwareMetadataJson json = parser .parse (ContextAwareMetadataJson .class );
118+ return json .getCommands ();
119+ }
120+
121+ @ VisibleForTesting
122+ static int runCertificateProviderCommand (Process commandProcess , long timeoutMilliseconds )
123+ throws IOException , InterruptedException {
124+ long startTime = System .currentTimeMillis ();
125+ long remainTime = timeoutMilliseconds ;
126+
127+ // In the while loop, keep checking if the process is terminated every 100 milliseconds
128+ // until timeout is reached or process is terminated. In getKeyStore we set timeout to
129+ // 1000 milliseconds, so 100 millisecond is a good number for the sleep.
130+ while (remainTime > 0 ) {
131+ Thread .sleep (Math .min (remainTime + 1 , 100 ));
132+ remainTime -= System .currentTimeMillis () - startTime ;
133+
134+ try {
135+ return commandProcess .exitValue ();
136+ } catch (IllegalThreadStateException ignored ) {
137+ // exitValue throws IllegalThreadStateException if process has not yet terminated.
138+ // Once the process is terminated, exitValue no longer throws exception. Therefore
139+ // in the while loop, we use exitValue to check if process is terminated. See
140+ // https://docs.oracle.com/javase/7/docs/api/java/lang/Process.html#exitValue()
141+ // for more details.
142+ }
143+ }
144+
145+ commandProcess .destroy ();
146+ throw new IOException ("cert provider command timed out" );
147+ }
148+ }
0 commit comments