Skip to content

Pr 13169 fixes#13873

Draft
macastelaz wants to merge 10 commits into
googleapis:agentic-identities-bound-tokenfrom
macastelaz:pr-13169-fixes
Draft

Pr 13169 fixes#13873
macastelaz wants to merge 10 commits into
googleapis:agentic-identities-bound-tokenfrom
macastelaz:pr-13169-fixes

Conversation

@macastelaz

Copy link
Copy Markdown
Contributor

No description provided.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces Agent Identity token binding support for Cloud Run. It adds AgentIdentityUtils to resolve, load, and verify certificates and private keys, and updates ComputeEngineCredentials to request bound tokens via POST requests when a valid certificate chain is present. The review feedback suggests a cohesive improvement to implement a single-read pattern for certificate files. By reading the certificate chain once, caching it in CertInfo, and passing it to parseCertificate and getBoundTokenPayload, the implementation can avoid redundant disk I/O and prevent potential race conditions during certificate rotation.

Comment on lines +232 to +278
static CertInfo loadAndVerifyCredentials(String certPath, String keyPath) throws IOException {
X509Certificate cert = null;
PrivateKey privateKey = null;

if (!Strings.isNullOrEmpty(certPath)
&& !Strings.isNullOrEmpty(keyPath)
&& !certPath.equals(keyPath)
&& Files.exists(Paths.get(keyPath))) {
// Separate files, verify match with retry
int retries = 0;
boolean matched = false;
while (retries < 3) {
try {
cert = parseCertificate(certPath);
privateKey = readPrivateKey(keyPath, cert.getPublicKey().getAlgorithm());

if (verifyKeyPair(cert, privateKey)) {
matched = true;
break;
}
LOGGER.warn("Cert and key mismatch, retrying...");
} catch (Exception e) {
LOGGER.warn("Failed to read or verify cert/key, retrying...", e);
}

retries++;
if (retries < 3) {
try {
timeService.sleep(100); // 0.1 seconds backoff
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while waiting for cert/key match.", e);
}
}
}

if (!matched) {
throw new IOException(
"Agent Identity certificate and private key mismatch or read failure after 3 retries.");
}
} else if (!Strings.isNullOrEmpty(certPath)) {
// Bundle or only cert available
cert = parseCertificate(certPath);
}

return new CertInfo(cert, certPath);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Update loadAndVerifyCredentials to read the certificate chain once using readCertificateChain(certPath) and pass the content to parseCertificate and the updated CertInfo constructor. This eliminates double disk reads and prevents race conditions where the certificate file might be modified or deleted between verification and token payload generation.

  static CertInfo loadAndVerifyCredentials(String certPath, String keyPath) throws IOException {
    X509Certificate cert = null;
    PrivateKey privateKey = null;
    String certContent = null;

    if (!Strings.isNullOrEmpty(certPath)
        && !Strings.isNullOrEmpty(keyPath)
        && !certPath.equals(keyPath)
        && Files.exists(Paths.get(keyPath))) {
      // Separate files, verify match with retry
      int retries = 0;
      boolean matched = false;
      while (retries < 3) {
        try {
          certContent = readCertificateChain(certPath);
          cert = parseCertificate(certContent);
          privateKey = readPrivateKey(keyPath, cert.getPublicKey().getAlgorithm());

          if (verifyKeyPair(cert, privateKey)) {
            matched = true;
            break;
          }
          LOGGER.warn("Cert and key mismatch, retrying...");
        } catch (Exception e) {
          LOGGER.warn("Failed to read or verify cert/key, retrying...", e);
        }

        retries++;
        if (retries < 3) {
          try {
            timeService.sleep(100); // 0.1 seconds backoff
          } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IOException("Interrupted while waiting for cert/key match.", e);
          }
        }
      }

      if (!matched) {
        throw new IOException(
            "Agent Identity certificate and private key mismatch or read failure after 3 retries.");
      }
    } else if (!Strings.isNullOrEmpty(certPath)) {
      // Bundle or only cert available
      certContent = readCertificateChain(certPath);
      cert = parseCertificate(certContent);
    }

    return new CertInfo(cert, certPath, certContent);
  }

Comment on lines +465 to +470
static String getBoundTokenPayload() throws IOException {
CertInfo info = getAgentIdentityCertInfo();
if (info != null && shouldRequestBoundToken(info.certificate)) {
return readCertificateChain(info.path);
}
return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Update getBoundTokenPayload to return the cached certificateChain from CertInfo directly instead of reading the file from disk again. This prevents a race condition during certificate rotation and avoids redundant disk I/O.

Suggested change
static String getBoundTokenPayload() throws IOException {
CertInfo info = getAgentIdentityCertInfo();
if (info != null && shouldRequestBoundToken(info.certificate)) {
return readCertificateChain(info.path);
}
return null;
static String getBoundTokenPayload() throws IOException {
CertInfo info = getAgentIdentityCertInfo();
if (info != null && shouldRequestBoundToken(info.certificate)) {
return info.certificateChain;
}
return null;

Comment on lines +134 to +142
static class CertInfo {
final X509Certificate certificate;
final String path;

CertInfo(X509Certificate certificate, String path) {
this.certificate = certificate;
this.path = path;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To avoid double disk I/O and potential race conditions during certificate rotation, we should store the raw certificate chain content as a String in CertInfo when it is first read. This allows getBoundTokenPayload to return the cached chain directly without reading the file from disk a second time.

  static class CertInfo {
    final X509Certificate certificate;
    final String path;
    final String certificateChain;

    CertInfo(X509Certificate certificate, String path, String certificateChain) {
      this.certificate = certificate;
      this.path = path;
      this.certificateChain = certificateChain;
    }
  }

Comment on lines +503 to +510
private static X509Certificate parseCertificate(String certPath) throws IOException {
try (InputStream stream = new FileInputStream(certPath)) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate) cf.generateCertificate(stream);
} catch (GeneralSecurityException e) {
throw new IOException(
"Failed to parse Agent Identity certificate for bound token request.", e);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update parseCertificate to accept the pre-read certificate content as a String instead of a file path. This allows parsing the certificate from memory, supporting the single-read pattern.

Suggested change
private static X509Certificate parseCertificate(String certPath) throws IOException {
try (InputStream stream = new FileInputStream(certPath)) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate) cf.generateCertificate(stream);
} catch (GeneralSecurityException e) {
throw new IOException(
"Failed to parse Agent Identity certificate for bound token request.", e);
}
private static X509Certificate parseCertificate(String certContent) throws IOException {
try (InputStream stream = new java.io.ByteArrayInputStream(certContent.getBytes(StandardCharsets.UTF_8))) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate) cf.generateCertificate(stream);
} catch (GeneralSecurityException e) {
throw new IOException(
"Failed to parse Agent Identity certificate for bound token request.", e);
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants