4848import java .security .cert .CertificateFactory ;
4949import java .security .cert .CertificateParsingException ;
5050import java .security .cert .X509Certificate ;
51- import java .util .Collection ;
52- import java .util .List ;
53- import java .util .Map ;
51+ import java .util .*;
5452import java .util .logging .Level ;
5553import java .util .logging .Logger ;
5654import java .util .regex .Pattern ;
@@ -67,15 +65,34 @@ final class AgentIdentityUtils {
6765 ImmutableList .of (
6866 Pattern .compile ("^agents\\ .global\\ .org-\\ d+\\ .system\\ .id\\ .goog$" ),
6967 Pattern .compile ("^agents\\ .global\\ .proj-\\ d+\\ .system\\ .id\\ .goog$" ));
68+ private static final int SAN_URI_TYPE = 6 ;
69+ private static final String SPIFFE_SCHEME_PREFIX = "spiffe://" ;
7070
7171 // Polling configuration
72- private static final long TOTAL_TIMEOUT_MS = 30000 ; // 30 seconds
73- private static final long FAST_POLL_DURATION_MS = 5000 ; // 5 seconds
72+ private static final int FAST_POLL_CYCLES = 50 ;
7473 private static final long FAST_POLL_INTERVAL_MS = 100 ; // 0.1 seconds
7574 private static final long SLOW_POLL_INTERVAL_MS = 500 ; // 0.5 seconds
75+ private static final long TOTAL_TIMEOUT_MS = 30000 ; // 30 seconds
76+ private static final List <Long > POLLING_INTERVALS ;
7677
77- private static final int SAN_URI_TYPE = 6 ;
78- private static final String SPIFFE_SCHEME_PREFIX = "spiffe://" ;
78+ // Pre-calculates the sequence of polling intervals
79+ static {
80+ List <Long > intervals = new ArrayList <>();
81+
82+ for (int i = 0 ; i < FAST_POLL_CYCLES ; i ++) {
83+ intervals .add (FAST_POLL_INTERVAL_MS );
84+ }
85+
86+ long remainingTime = TOTAL_TIMEOUT_MS - (FAST_POLL_CYCLES * FAST_POLL_INTERVAL_MS );
87+ // Integer division is sufficient here as we want full cycles
88+ int slowPollCycles = (int ) (remainingTime / SLOW_POLL_INTERVAL_MS );
89+
90+ for (int i = 0 ; i < slowPollCycles ; i ++) {
91+ intervals .add (SLOW_POLL_INTERVAL_MS );
92+ }
93+
94+ POLLING_INTERVALS = Collections .unmodifiableList (intervals );
95+ }
7996
8097 // Interface to allow mocking System.getenv for tests without exposing it publicly.
8198 interface EnvReader {
@@ -111,7 +128,8 @@ public void sleep(long millis) throws InterruptedException {
111128 private AgentIdentityUtils () {}
112129
113130 /**
114- * Gets the Agent Identity certificate if available and enabled.
131+ * Gets the Agent Identity certificate if certificate is available and agent token sharing is not
132+ * disabled.
115133 *
116134 * @return The X509Certificate if found and Agent Identities are enabled, null otherwise.
117135 * @throws IOException If there is an error reading the certificate file after retries.
@@ -130,39 +148,46 @@ static X509Certificate getAgentIdentityCertificate() throws IOException {
130148 return parseCertificate (certPath );
131149 }
132150
133- /** Checks if the user has opted out of Agent Token sharing. */
151+ /**
152+ * Checks if Agent Identity token sharing is disabled via an environment variable.
153+ *
154+ * @return {@code true} if the {@link #GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES}
155+ * variable is set to {@code "false"}, otherwise returns {@code false}.
156+ */
134157 private static boolean isOptedOut () {
135158 String optOut = envReader .getEnv (GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES );
136159 return optOut != null && "false" .equalsIgnoreCase (optOut );
137160 }
138161
139- /** Polls for the certificate config file and the certificate file it references. */
162+ /**
163+ * Polls for the certificate config file and the certificate file it references, and returns the
164+ * certificate's path.
165+ *
166+ * <p>This method will retry for a total of {@link #TOTAL_TIMEOUT_MS} milliseconds before failing.
167+ *
168+ * @param certConfigPath The path to the certificate configuration JSON file.
169+ * @return The path to the certificate file extracted from the config.
170+ * @throws IOException If the files cannot be found after the timeout, or if the thread is
171+ * interrupted while waiting.
172+ */
140173 private static String getCertificatePathWithRetry (String certConfigPath ) throws IOException {
141- long startTime = timeService .currentTimeMillis ();
142174 boolean warned = false ;
143175
144- while (true ) {
176+ // Deterministic polling loop based on pre-calculated intervals.
177+ for (long sleepInterval : POLLING_INTERVALS ) {
145178 try {
146179 if (Files .exists (Paths .get (certConfigPath ))) {
147180 String certPath = extractCertPathFromConfig (certConfigPath );
148181 if (!Strings .isNullOrEmpty (certPath ) && Files .exists (Paths .get (certPath ))) {
149182 return certPath ;
150183 }
151184 }
152- } catch (Exception e ) {
153- // Ignore exceptions during polling and retry
154- LOGGER .log (Level .FINE , "Error while polling for certificate files" );
155- }
156-
157- long elapsedTime = timeService .currentTimeMillis () - startTime ;
158- if (elapsedTime >= TOTAL_TIMEOUT_MS ) {
159- throw new IOException (
160- "Certificate config or certificate file not found after multiple retries. "
161- + "Token binding protection is failing. You can turn off this protection by setting "
162- + GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES
163- + " to false to fall back to unbound tokens." );
185+ } catch (IOException e ) {
186+ // Do not log here to prevent noise in the logs per iteration.
187+ // Fall through to the sleep logic to retry.
164188 }
165189
190+ // If we are here, we failed to find the certificate, log a warning only once.
166191 if (!warned ) {
167192 LOGGER .warning (
168193 String .format (
@@ -172,17 +197,32 @@ private static String getCertificatePathWithRetry(String certConfigPath) throws
172197 warned = true ;
173198 }
174199
200+ // Sleep before the next attempt.
175201 try {
176- long sleepTime =
177- elapsedTime < FAST_POLL_DURATION_MS ? FAST_POLL_INTERVAL_MS : SLOW_POLL_INTERVAL_MS ;
178- timeService .sleep (sleepTime );
202+ timeService .sleep (sleepInterval );
179203 } catch (InterruptedException e ) {
180204 Thread .currentThread ().interrupt ();
181- throw new IOException ("Interrupted while waiting for certificate files" , e );
205+ throw new IOException (
206+ "Interrupted while waiting for Agent Identity certificate files for bound token request." ,
207+ e );
182208 }
183209 }
210+
211+ // If the loop completes without returning, we have timed out.
212+ throw new IOException (
213+ "Unable to find Agent Identity certificate config or file for bound token request after multiple retries. "
214+ + "Token binding protection is failing. You can turn off this protection by setting "
215+ + GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES
216+ + " to false to fall back to unbound tokens." );
184217 }
185218
219+ /**
220+ * Parses the certificate configuration JSON file and extracts the path to the certificate.
221+ *
222+ * @param certConfigPath The path to the certificate configuration JSON file.
223+ * @return The certificate file path, or {@code null} if not found in the config.
224+ * @throws IOException If the configuration file cannot be read.
225+ */
186226 @ SuppressWarnings ("unchecked" )
187227 private static String extractCertPathFromConfig (String certConfigPath ) throws IOException {
188228 try (InputStream stream = new FileInputStream (certConfigPath )) {
@@ -199,12 +239,20 @@ private static String extractCertPathFromConfig(String certConfigPath) throws IO
199239 return null ;
200240 }
201241
242+ /**
243+ * Parses an X.509 certificate from the given file path.
244+ *
245+ * @param certPath The path to the certificate file.
246+ * @return The parsed {@link X509Certificate}.
247+ * @throws IOException If the certificate file cannot be read or parsed.
248+ */
202249 private static X509Certificate parseCertificate (String certPath ) throws IOException {
203250 try (InputStream stream = new FileInputStream (certPath )) {
204251 CertificateFactory cf = CertificateFactory .getInstance ("X.509" );
205252 return (X509Certificate ) cf .generateCertificate (stream );
206253 } catch (GeneralSecurityException e ) {
207- throw new IOException ("Failed to parse certificate" , e );
254+ throw new IOException (
255+ "Failed to parse Agent Identity certificate for bound token request." , e );
208256 }
209257 }
210258
@@ -255,7 +303,7 @@ static String calculateCertificateFingerprint(X509Certificate cert) throws IOExc
255303 byte [] digest = md .digest ();
256304 return BaseEncoding .base64Url ().omitPadding ().encode (digest );
257305 } catch (GeneralSecurityException e ) {
258- throw new IOException ("Failed to calculate certificate fingerprint" , e );
306+ throw new IOException ("Failed to calculate fingerprint for Agent Identity certificate. " , e );
259307 }
260308 }
261309
0 commit comments