Pr 13169 fixes#13873
Conversation
1. POST request to MDS with cert-chain 2. Cert-key matching 3. Included logic to consider the user's choice by looking at GOOGLE_API_USE_CLIENT_CERTIFICATE env variable 4. Bound ID tokens.
…etry logic. Nit fixes.
There was a problem hiding this comment.
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.
| 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); | ||
| } |
There was a problem hiding this comment.
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);
}| static String getBoundTokenPayload() throws IOException { | ||
| CertInfo info = getAgentIdentityCertInfo(); | ||
| if (info != null && shouldRequestBoundToken(info.certificate)) { | ||
| return readCertificateChain(info.path); | ||
| } | ||
| return null; |
There was a problem hiding this comment.
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.
| 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; |
| static class CertInfo { | ||
| final X509Certificate certificate; | ||
| final String path; | ||
|
|
||
| CertInfo(X509Certificate certificate, String path) { | ||
| this.certificate = certificate; | ||
| this.path = path; | ||
| } | ||
| } |
There was a problem hiding this comment.
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;
}
}| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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); | |
| } | |
| } |
No description provided.