5757
5858/** Internal utility class for handling Agent Identity certificate-bound access tokens. */
5959final class AgentIdentityUtils {
60- private static final Logger LOGGER = Logger .getLogger (AgentIdentityUtils .class .getName ());
61-
62- static final String GOOGLE_API_CERTIFICATE_CONFIG = "GOOGLE_API_CERTIFICATE_CONFIG" ;
63- static final String GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES =
64- "GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES" ;
65-
66- private static final List <Pattern > AGENT_IDENTITY_SPIFFE_PATTERNS =
67- ImmutableList .of (
68- Pattern .compile ("^agents\\ .global\\ .org-\\ d+\\ .system\\ .id\\ .goog$" ),
69- Pattern .compile ("^agents\\ .global\\ .proj-\\ d+\\ .system\\ .id\\ .goog$" ));
70-
71- // Polling configuration
72- static long TOTAL_TIMEOUT_MS = 30000 ; // 30 seconds
73- static long FAST_POLL_DURATION_MS = 5000 ; // 5 seconds
74- static long FAST_POLL_INTERVAL_MS = 100 ; // 0.1 seconds
75- static long SLOW_POLL_INTERVAL_MS = 500 ; // 0.5 seconds
76-
77- private static final int SAN_URI_TYPE = 6 ;
78- private static final String SPIFFE_SCHEME_PREFIX = "spiffe://" ;
79-
80- // Interface to allow mocking System.getenv for tests without exposing it publicly.
81- interface EnvReader {
82- String getEnv (String name );
83- }
84-
85- private static EnvReader envReader = System ::getenv ;
86-
87- private AgentIdentityUtils () {}
88-
89- /**
90- * Gets the Agent Identity certificate if available and enabled.
91- *
92- * @return The X509Certificate if found and Agent Identities are enabled, null otherwise.
93- * @throws IOException If there is an error reading the certificate file after retries.
94- */
95- static X509Certificate getAgentIdentityCertificate () throws IOException {
96- if (isOptedOut ()) {
97- return null ;
98- }
99-
100- String certConfigPath = envReader .getEnv (GOOGLE_API_CERTIFICATE_CONFIG );
101- if (Strings .isNullOrEmpty (certConfigPath )) {
102- return null ;
103- }
104-
105- String certPath = getCertificatePathWithRetry (certConfigPath );
106- return parseCertificate (certPath );
60+ private static final Logger LOGGER = Logger .getLogger (AgentIdentityUtils .class .getName ());
61+
62+ static final String GOOGLE_API_CERTIFICATE_CONFIG = "GOOGLE_API_CERTIFICATE_CONFIG" ;
63+ static final String GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES =
64+ "GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES" ;
65+
66+ private static final List <Pattern > AGENT_IDENTITY_SPIFFE_PATTERNS =
67+ ImmutableList .of (
68+ Pattern .compile ("^agents\\ .global\\ .org-\\ d+\\ .system\\ .id\\ .goog$" ),
69+ Pattern .compile ("^agents\\ .global\\ .proj-\\ d+\\ .system\\ .id\\ .goog$" ));
70+
71+ // Polling configuration
72+ static long TOTAL_TIMEOUT_MS = 30000 ; // 30 seconds
73+ static long FAST_POLL_DURATION_MS = 5000 ; // 5 seconds
74+ static long FAST_POLL_INTERVAL_MS = 100 ; // 0.1 seconds
75+ static long SLOW_POLL_INTERVAL_MS = 500 ; // 0.5 seconds
76+
77+ private static final int SAN_URI_TYPE = 6 ;
78+ private static final String SPIFFE_SCHEME_PREFIX = "spiffe://" ;
79+
80+ // Interface to allow mocking System.getenv for tests without exposing it publicly.
81+ interface EnvReader {
82+ String getEnv (String name );
83+ }
84+
85+ private static EnvReader envReader = System ::getenv ;
86+
87+ private AgentIdentityUtils () {}
88+
89+ /**
90+ * Gets the Agent Identity certificate if available and enabled.
91+ *
92+ * @return The X509Certificate if found and Agent Identities are enabled, null otherwise.
93+ * @throws IOException If there is an error reading the certificate file after retries.
94+ */
95+ static X509Certificate getAgentIdentityCertificate () throws IOException {
96+ if (isOptedOut ()) {
97+ return null ;
10798 }
10899
109- /** Checks if the user has opted out of Agent Token sharing. */
110- private static boolean isOptedOut () {
111- String optOut = envReader .getEnv (GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES );
112- return optOut != null && "false" .equalsIgnoreCase (optOut );
100+ String certConfigPath = envReader .getEnv (GOOGLE_API_CERTIFICATE_CONFIG );
101+ if (Strings .isNullOrEmpty (certConfigPath )) {
102+ return null ;
113103 }
114104
115- /** Polls for the certificate config file and the certificate file it references. */
116- private static String getCertificatePathWithRetry (String certConfigPath ) throws IOException {
117- long startTime = System .currentTimeMillis ();
118- boolean warned = false ;
119-
120- while (true ) {
121- try {
122- if (Files .exists (Paths .get (certConfigPath ))) {
123- String certPath = extractCertPathFromConfig (certConfigPath );
124- if (!Strings .isNullOrEmpty (certPath ) && Files .exists (Paths .get (certPath ))) {
125- return certPath ;
126- }
127- }
128- } catch (Exception e ) {
129- // Ignore exceptions during polling and retry
130- LOGGER .log (Level .FINE , "Error while polling for certificate files" , e );
131- }
132-
133- long elapsedTime = System .currentTimeMillis () - startTime ;
134- if (elapsedTime >= TOTAL_TIMEOUT_MS ) {
135- throw new IOException (
136- "Certificate config or certificate file not found after multiple retries. "
137- + "Token binding protection is failing. You can turn off this protection by setting "
138- + GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES
139- + " to false to fall back to unbound tokens." );
140- }
141-
142- if (!warned ) {
143- LOGGER .warning (
144- String .format (
145- "Certificate config file not found at %s (from %s environment variable). "
146- + "Retrying for up to %d seconds." ,
147- certConfigPath ,
148- GOOGLE_API_CERTIFICATE_CONFIG ,
149- TOTAL_TIMEOUT_MS / 1000 ));
150- warned = true ;
151- }
152-
153- try {
154- long sleepTime =
155- elapsedTime < FAST_POLL_DURATION_MS ? FAST_POLL_INTERVAL_MS : SLOW_POLL_INTERVAL_MS ;
156- Thread .sleep (sleepTime );
157- } catch (InterruptedException e ) {
158- Thread .currentThread ().interrupt ();
159- throw new IOException ("Interrupted while waiting for certificate files" , e );
160- }
105+ String certPath = getCertificatePathWithRetry (certConfigPath );
106+ return parseCertificate (certPath );
107+ }
108+
109+ /** Checks if the user has opted out of Agent Token sharing. */
110+ private static boolean isOptedOut () {
111+ String optOut = envReader .getEnv (GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES );
112+ return optOut != null && "false" .equalsIgnoreCase (optOut );
113+ }
114+
115+ /** Polls for the certificate config file and the certificate file it references. */
116+ private static String getCertificatePathWithRetry (String certConfigPath ) throws IOException {
117+ long startTime = System .currentTimeMillis ();
118+ boolean warned = false ;
119+
120+ while (true ) {
121+ try {
122+ if (Files .exists (Paths .get (certConfigPath ))) {
123+ String certPath = extractCertPathFromConfig (certConfigPath );
124+ if (!Strings .isNullOrEmpty (certPath ) && Files .exists (Paths .get (certPath ))) {
125+ return certPath ;
126+ }
161127 }
128+ } catch (Exception e ) {
129+ // Ignore exceptions during polling and retry
130+ LOGGER .log (Level .FINE , "Error while polling for certificate files" , e );
131+ }
132+
133+ long elapsedTime = System .currentTimeMillis () - startTime ;
134+ if (elapsedTime >= TOTAL_TIMEOUT_MS ) {
135+ throw new IOException (
136+ "Certificate config or certificate file not found after multiple retries. "
137+ + "Token binding protection is failing. You can turn off this protection by setting "
138+ + GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES
139+ + " to false to fall back to unbound tokens." );
140+ }
141+
142+ if (!warned ) {
143+ LOGGER .warning (
144+ String .format (
145+ "Certificate config file not found at %s (from %s environment variable). "
146+ + "Retrying for up to %d seconds." ,
147+ certConfigPath , GOOGLE_API_CERTIFICATE_CONFIG , TOTAL_TIMEOUT_MS / 1000 ));
148+ warned = true ;
149+ }
150+
151+ try {
152+ long sleepTime =
153+ elapsedTime < FAST_POLL_DURATION_MS ? FAST_POLL_INTERVAL_MS : SLOW_POLL_INTERVAL_MS ;
154+ Thread .sleep (sleepTime );
155+ } catch (InterruptedException e ) {
156+ Thread .currentThread ().interrupt ();
157+ throw new IOException ("Interrupted while waiting for certificate files" , e );
158+ }
162159 }
163-
164- @ SuppressWarnings ("unchecked" )
165- private static String extractCertPathFromConfig (String certConfigPath ) throws IOException {
166- try (InputStream stream = new FileInputStream (certConfigPath )) {
167- JsonObjectParser parser = new JsonObjectParser (OAuth2Utils .JSON_FACTORY );
168- GenericJson config =
169- parser .parseAndClose (stream , StandardCharsets .UTF_8 , GenericJson .class );
170- Map <String , Object > certConfigs = (Map <String , Object >) config .get ("cert_configs" );
171- if (certConfigs != null ) {
172- Map <String , Object > workload = (Map <String , Object >) certConfigs .get ("workload" );
173- if (workload != null ) {
174- return (String ) workload .get ("cert_path" );
175- }
176- }
160+ }
161+
162+ @ SuppressWarnings ("unchecked" )
163+ private static String extractCertPathFromConfig (String certConfigPath ) throws IOException {
164+ try (InputStream stream = new FileInputStream (certConfigPath )) {
165+ JsonObjectParser parser = new JsonObjectParser (OAuth2Utils .JSON_FACTORY );
166+ GenericJson config = parser .parseAndClose (stream , StandardCharsets .UTF_8 , GenericJson .class );
167+ Map <String , Object > certConfigs = (Map <String , Object >) config .get ("cert_configs" );
168+ if (certConfigs != null ) {
169+ Map <String , Object > workload = (Map <String , Object >) certConfigs .get ("workload" );
170+ if (workload != null ) {
171+ return (String ) workload .get ("cert_path" );
177172 }
178- return null ;
173+ }
179174 }
180-
181- private static X509Certificate parseCertificate (String certPath ) throws IOException {
182- try (InputStream stream = new FileInputStream (certPath )) {
183- CertificateFactory cf = CertificateFactory .getInstance ("X.509" );
184- return (X509Certificate ) cf .generateCertificate (stream );
185- } catch (GeneralSecurityException e ) {
186- throw new IOException ("Failed to parse certificate" , e );
187- }
175+ return null ;
176+ }
177+
178+ private static X509Certificate parseCertificate (String certPath ) throws IOException {
179+ try (InputStream stream = new FileInputStream (certPath )) {
180+ CertificateFactory cf = CertificateFactory .getInstance ("X.509" );
181+ return (X509Certificate ) cf .generateCertificate (stream );
182+ } catch (GeneralSecurityException e ) {
183+ throw new IOException ("Failed to parse certificate" , e );
188184 }
185+ }
189186
190- /** Checks if the certificate belongs to an Agent Identity by inspecting SANs. */
191- static boolean shouldRequestBoundToken (X509Certificate cert ) {
192- try {
193- Collection <List <?>> sans = cert .getSubjectAlternativeNames ();
194- if (sans == null ) {
195- return false ;
196- }
197- for (List <?> san : sans ) {
198- // SAN entry is a list where first element is the type (Integer) and second is value (mostly
199- // String)
200- if (san .size () >= 2
201- && san .get (0 ) instanceof Integer
202- && (Integer ) san .get (0 ) == SAN_URI_TYPE ) {
203- Object value = san .get (1 );
204- if (value instanceof String ) {
205- String uri = (String ) value ;
206- if (uri .startsWith (SPIFFE_SCHEME_PREFIX )) {
207- // Extract trust domain: spiffe://<trust_domain>/...
208- String withoutScheme = uri .substring (SPIFFE_SCHEME_PREFIX .length ());
209- int slashIndex = withoutScheme .indexOf ('/' );
210- String trustDomain =
211- (slashIndex == -1 ) ? withoutScheme : withoutScheme .substring (0 , slashIndex );
212-
213- for (Pattern pattern : AGENT_IDENTITY_SPIFFE_PATTERNS ) {
214- if (pattern .matcher (trustDomain ).matches ()) {
215- return true ;
216- }
217- }
218- }
219- }
187+ /** Checks if the certificate belongs to an Agent Identity by inspecting SANs. */
188+ static boolean shouldRequestBoundToken (X509Certificate cert ) {
189+ try {
190+ Collection <List <?>> sans = cert .getSubjectAlternativeNames ();
191+ if (sans == null ) {
192+ return false ;
193+ }
194+ for (List <?> san : sans ) {
195+ // SAN entry is a list where first element is the type (Integer) and second is value (mostly
196+ // String)
197+ if (san .size () >= 2
198+ && san .get (0 ) instanceof Integer
199+ && (Integer ) san .get (0 ) == SAN_URI_TYPE ) {
200+ Object value = san .get (1 );
201+ if (value instanceof String ) {
202+ String uri = (String ) value ;
203+ if (uri .startsWith (SPIFFE_SCHEME_PREFIX )) {
204+ // Extract trust domain: spiffe://<trust_domain>/...
205+ String withoutScheme = uri .substring (SPIFFE_SCHEME_PREFIX .length ());
206+ int slashIndex = withoutScheme .indexOf ('/' );
207+ String trustDomain =
208+ (slashIndex == -1 ) ? withoutScheme : withoutScheme .substring (0 , slashIndex );
209+
210+ for (Pattern pattern : AGENT_IDENTITY_SPIFFE_PATTERNS ) {
211+ if (pattern .matcher (trustDomain ).matches ()) {
212+ return true ;
220213 }
214+ }
221215 }
222- } catch (CertificateParsingException e ) {
223- LOGGER .log (Level .WARNING , "Failed to parse Subject Alternative Names from certificate" , e );
216+ }
224217 }
225- return false ;
218+ }
219+ } catch (CertificateParsingException e ) {
220+ LOGGER .log (Level .WARNING , "Failed to parse Subject Alternative Names from certificate" , e );
226221 }
227-
228- /** Calculates the SHA-256 fingerprint of the certificate, Base64Url encoded without padding. */
229- static String calculateCertificateFingerprint (X509Certificate cert ) throws IOException {
230- try {
231- MessageDigest md = MessageDigest .getInstance ("SHA-256" );
232- byte [] der = cert .getEncoded ();
233- md .update (der );
234- byte [] digest = md .digest ();
235- return BaseEncoding .base64Url ().omitPadding ().encode (digest );
236- } catch (GeneralSecurityException e ) {
237- throw new IOException ("Failed to calculate certificate fingerprint" , e );
238- }
222+ return false ;
223+ }
224+
225+ /** Calculates the SHA-256 fingerprint of the certificate, Base64Url encoded without padding. */
226+ static String calculateCertificateFingerprint (X509Certificate cert ) throws IOException {
227+ try {
228+ MessageDigest md = MessageDigest .getInstance ("SHA-256" );
229+ byte [] der = cert .getEncoded ();
230+ md .update (der );
231+ byte [] digest = md .digest ();
232+ return BaseEncoding .base64Url ().omitPadding ().encode (digest );
233+ } catch (GeneralSecurityException e ) {
234+ throw new IOException ("Failed to calculate certificate fingerprint" , e );
239235 }
236+ }
240237
241- @ VisibleForTesting
242- static void setEnvReader (EnvReader reader ) {
243- envReader = reader ;
244- }
245- }
238+ @ VisibleForTesting
239+ static void setEnvReader (EnvReader reader ) {
240+ envReader = reader ;
241+ }
242+ }
0 commit comments