diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java new file mode 100644 index 000000000000..45513f2a697a --- /dev/null +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AgentIdentityUtils.java @@ -0,0 +1,572 @@ +/* + * Copyright 2026, Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.auth.oauth2; + +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonObjectParser; +import com.google.api.core.InternalApi; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.cert.CertificateFactory; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Utility class for Agent Identity token binding in Cloud Run. */ +@InternalApi +public final class AgentIdentityUtils { + + private static final Logger LOGGER = LoggerFactory.getLogger(AgentIdentityUtils.class); + + // Environment variables + static final String GOOGLE_API_CERTIFICATE_CONFIG = "GOOGLE_API_CERTIFICATE_CONFIG"; + static final String GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES = + "GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES"; + + private static final List AGENT_IDENTITY_SPIFFE_PATTERNS = + ImmutableList.of( + Pattern.compile("^agents\\.global\\.org-\\d+\\.system\\.id\\.goog$"), + Pattern.compile("^agents\\.global\\.proj-\\d+\\.system\\.id\\.goog$")); + + private static final int SAN_URI_TYPE = 6; + private static final String SPIFFE_SCHEME_PREFIX = "spiffe://"; + + private static String wellKnownDir = "/var/run/secrets/workload-spiffe-credentials/"; + + @VisibleForTesting + static void setWellKnownDir(String dir) { + wellKnownDir = dir; + } + + // Polling configuration + private static final int FAST_POLL_CYCLES = 50; + private static final long FAST_POLL_INTERVAL_MS = 100; // 0.1 seconds + private static final long SLOW_POLL_INTERVAL_MS = 500; // 0.5 seconds + private static final long TOTAL_TIMEOUT_MS = 30000; // 30 seconds + private static final List POLLING_INTERVALS; + + static { + List intervals = new ArrayList<>(); + for (int i = 0; i < FAST_POLL_CYCLES; i++) { + intervals.add(FAST_POLL_INTERVAL_MS); + } + long remainingTime = TOTAL_TIMEOUT_MS - (FAST_POLL_CYCLES * FAST_POLL_INTERVAL_MS); + int slowPollCycles = (int) (remainingTime / SLOW_POLL_INTERVAL_MS); + for (int i = 0; i < slowPollCycles; i++) { + intervals.add(SLOW_POLL_INTERVAL_MS); + } + POLLING_INTERVALS = Collections.unmodifiableList(intervals); + } + + public interface EnvReader { + String getEnv(String name); + } + + private static EnvReader envReader = System::getenv; + + @VisibleForTesting + interface TimeService { + long currentTimeMillis(); + + void sleep(long millis) throws InterruptedException; + } + + private static TimeService timeService = + new TimeService() { + @Override + public long currentTimeMillis() { + return System.currentTimeMillis(); + } + + @Override + public void sleep(long millis) throws InterruptedException { + Thread.sleep(millis); + } + }; + + private AgentIdentityUtils() {} + + static class CertInfo { + final X509Certificate certificate; + final String path; + + CertInfo(X509Certificate certificate, String path) { + this.certificate = certificate; + this.path = path; + } + } + + static class ResolvedCertAndKeyPaths { + final String certPath; + final String keyPath; + + ResolvedCertAndKeyPaths(String certPath, String keyPath) { + this.certPath = certPath; + this.keyPath = keyPath; + } + } + + /** + * Retrieves the certificate and path for the Agent Identity. + * + *

This method attempts to load the certificate and private key for the agent identity. It + * first checks the location specified by the {@code GOOGLE_API_CERTIFICATE_CONFIG} environment + * variable. If not set, it falls back to well-known default locations. + * + *

To handle transient race conditions during certificate rotation on disk, this method employs + * a retry mechanism with backoff when reading the configuration and certificate files. + * + * @return A {@link CertInfo} object containing the loaded certificate and its path, or {@code + * null} if the agent identity features are disabled, opted out, or if no valid credentials + * could be loaded. + * @throws IOException If an I/O error occurs while reading the files, or if the key-pair + * verification fails after retries. + */ + static CertInfo getAgentIdentityCertInfo() throws IOException { + if (isOptedOut()) { + return null; + } + String certConfigPath = envReader.getEnv(GOOGLE_API_CERTIFICATE_CONFIG); + boolean configExists = + !Strings.isNullOrEmpty(certConfigPath) && Files.exists(Paths.get(certConfigPath)); + + ResolvedCertAndKeyPaths paths = resolveCertAndKeyPaths(certConfigPath); + boolean certsPresent = !Strings.isNullOrEmpty(paths.certPath); + + if (!shouldEnableMtls(certsPresent, configExists)) { + return null; + } + + return loadAndVerifyCredentials(paths.certPath, paths.keyPath); + } + + /** + * Resolves the paths for the certificate and private key based on the config path or well-known + * locations. + */ + static ResolvedCertAndKeyPaths resolveCertAndKeyPaths(String certConfigPath) throws IOException { + String certPath = null; + String keyPath = null; + + if (!Strings.isNullOrEmpty(certConfigPath)) { + // Read cert path from config file. We use retry with backoff to handle transient race + // conditions where the config file might be being updated by a rotation process. + certPath = getCertificatePathWithRetry(certConfigPath); + keyPath = extractKeyPathFromConfig(certConfigPath); + } else { + // Fallback to well-known locations. We use retry with backoff here as well to handle + // race conditions during file replacement by a rotation process. + certPath = getWellKnownCertificatePathWithRetry(); + if (certPath != null) { + if (certPath.endsWith("credentialbundle.pem")) { + keyPath = certPath; // Bundle contains both + } else if (certPath.endsWith("certificates.pem")) { + keyPath = Paths.get(wellKnownDir, "private_key.pem").toString(); + } + } + } + return new ResolvedCertAndKeyPaths(certPath, keyPath); + } + + /** + * Loads the certificate and private key, and verifies that they match if they are separate files. + */ + 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); + } + + /** + * Checks if the user has opted out of token sharing by setting the environment variable to true. + */ + private static boolean isOptedOut() { + String optOut = envReader.getEnv(GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES); + return "true".equalsIgnoreCase(optOut); + } + + /** + * Reads the certificate path from the config file with retry logic to handle rotation race + * conditions. + */ + private static String getCertificatePathWithRetry(String certConfigPath) throws IOException { + boolean warned = false; + for (long sleepInterval : POLLING_INTERVALS) { + try { + if (Files.exists(Paths.get(certConfigPath))) { + String certPath = extractCertPathFromConfig(certConfigPath); + if (!Strings.isNullOrEmpty(certPath) && Files.exists(Paths.get(certPath))) { + return certPath; + } + } + } catch (IOException e) { + // Fall through to retry + } + if (!warned) { + Slf4jUtils.log( + LOGGER, + org.slf4j.event.Level.WARN, + Collections.emptyMap(), + String.format( + "Certificate config file not found at %s (from %s environment variable). Retrying for up to %d seconds.", + certConfigPath, GOOGLE_API_CERTIFICATE_CONFIG, TOTAL_TIMEOUT_MS / 1000)); + warned = true; + } + try { + timeService.sleep(sleepInterval); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException( + "Interrupted while waiting for Agent Identity certificate files for bound token request.", + e); + } + } + throw new IOException( + "Unable to find Agent Identity certificate config or file for bound token request after multiple retries. Token binding protection is failing. You can turn off this protection by setting " + + GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES + + " to false to fall back to unbound tokens."); + } + + /** Searches for certificates at well-known locations with retry logic. */ + private static String getWellKnownCertificatePathWithRetry() throws IOException { + String bundlePath = Paths.get(wellKnownDir, "credentialbundle.pem").toString(); + String certOnlyPath = Paths.get(wellKnownDir, "certificates.pem").toString(); + + boolean warned = false; + for (long sleepInterval : POLLING_INTERVALS) { + try { + if (Files.exists(Paths.get(bundlePath))) { + return bundlePath; + } + if (Files.exists(Paths.get(certOnlyPath))) { + return certOnlyPath; + } + } catch (Exception e) { + // Fall through to retry + } + if (!warned) { + Slf4jUtils.log( + LOGGER, + org.slf4j.event.Level.WARN, + Collections.emptyMap(), + String.format( + "Well-known certificate file not found at %s. Retrying for up to %d seconds.", + wellKnownDir, TOTAL_TIMEOUT_MS / 1000)); + warned = true; + } + try { + timeService.sleep(sleepInterval); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while waiting for well-known certificate files.", e); + } + } + throw new IOException( + "Unable to find well-known certificate file for bound token request after multiple retries."); + } + + /** Reads the full certificate chain from the specified path as a string. */ + static String readCertificateChain(String certPath) throws IOException { + return new String(Files.readAllBytes(Paths.get(certPath)), StandardCharsets.UTF_8); + } + + /** + * Verifies that the private key corresponds to the public key in the certificate by performing a + * test signature and verification. + */ + static boolean verifyKeyPair(X509Certificate cert, PrivateKey privateKey) { + try { + byte[] data = "verification-data".getBytes(StandardCharsets.UTF_8); + + String keyAlgorithm = cert.getPublicKey().getAlgorithm(); + String sigAlg; + if ("RSA".equals(keyAlgorithm)) { + sigAlg = "SHA256withRSA"; + } else if ("EC".equals(keyAlgorithm)) { + sigAlg = "SHA256withECDSA"; + } else { + throw new IllegalArgumentException("Unsupported key algorithm: " + keyAlgorithm); + } + + Signature signer = Signature.getInstance(sigAlg); + signer.initSign(privateKey); + signer.update(data); + byte[] signature = signer.sign(); + + Signature verifier = Signature.getInstance(sigAlg); + verifier.initVerify(cert.getPublicKey()); + verifier.update(data); + + return verifier.verify(signature); + } catch (Exception e) { + LOGGER.warn("Key pair verification failed", e); + return false; + } + } + + /** Reads the private key from the specified path using PKCS8 format. */ + static PrivateKey readPrivateKey(String keyPath, String algorithm) throws IOException { + String keyPem = new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8); + OAuth2Utils.Pkcs8Algorithm pkcs8Alg = + "EC".equals(algorithm) ? OAuth2Utils.Pkcs8Algorithm.EC : OAuth2Utils.Pkcs8Algorithm.RSA; + return OAuth2Utils.privateKeyFromPkcs8(keyPem, pkcs8Alg); + } + + /** + * Determines if mTLS should be enabled based on environment variables and certificate presence. + */ + static boolean shouldEnableMtls(boolean certsPresent, boolean configExists) throws IOException { + String useClientCert = envReader.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE"); + + // Case 1: Explicitly enabled via environment variable + if ("true".equalsIgnoreCase(useClientCert)) { + if (certsPresent) { + // Certs are available, enable mTLS + return true; + } + if (configExists) { + // Config exists but files are missing - fail fast + throw new IOException( + "Certificate intent established via config, but cert files are missing."); + } + // Neither exist, do not enable + return false; + } + // Case 2: Explicitly disabled via environment variable + else if ("false".equalsIgnoreCase(useClientCert)) { + if (certsPresent) { + // Warn that we are ignoring present certs because it was explicitly disabled + Slf4jUtils.log( + LOGGER, + org.slf4j.event.Level.WARN, + Collections.emptyMap(), + "Token binding protection is disabled because mTLS was explicitly disabled via GOOGLE_API_USE_CLIENT_CERTIFICATE."); + return false; + } + return false; + } + // Case 3: Environment variable is unset + else { + if (certsPresent) { + // Infer mTLS is enabled because certs are present + return true; + } + if (configExists) { + // Config exists but files are missing - fail fast + throw new IOException( + "Certificate intent inferred via config, but cert files are missing."); + } + // Neither cert-config nor certsexist, do not enable + return false; + } + } + + /** Retrieves the bound token payload (certificate chain) if applicable. */ + static String getBoundTokenPayload() throws IOException { + CertInfo info = getAgentIdentityCertInfo(); + if (info != null && shouldRequestBoundToken(info.certificate)) { + return readCertificateChain(info.path); + } + return null; + } + + @SuppressWarnings("unchecked") + /** Extracts the certificate path from the JSON configuration file. */ + private static String extractCertPathFromConfig(String certConfigPath) throws IOException { + try (InputStream stream = new FileInputStream(certConfigPath)) { + JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); + GenericJson config = parser.parseAndClose(stream, StandardCharsets.UTF_8, GenericJson.class); + Map certConfigs = (Map) config.get("cert_configs"); + if (certConfigs != null) { + Map workload = (Map) certConfigs.get("workload"); + if (workload != null) { + return (String) workload.get("cert_path"); + } + } + } + return null; + } + + @SuppressWarnings("unchecked") + /** Extracts the private key path from the JSON configuration file. */ + private static String extractKeyPathFromConfig(String certConfigPath) throws IOException { + try (InputStream stream = new FileInputStream(certConfigPath)) { + JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY); + GenericJson config = parser.parseAndClose(stream, StandardCharsets.UTF_8, GenericJson.class); + Map certConfigs = (Map) config.get("cert_configs"); + if (certConfigs != null) { + Map workload = (Map) certConfigs.get("workload"); + if (workload != null) { + return (String) workload.get("key_path"); + } + } + } + return null; + } + + /** Parses the X509 certificate from the specified path. */ + 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); + } + } + + /** + * Determines if a bound token should be requested by checking if any of the certificate's Subject + * Alternative Names (SANs) match allowed SPIFFE patterns. + */ + static boolean shouldRequestBoundToken(X509Certificate cert) { + try { + Collection> sans = cert.getSubjectAlternativeNames(); + if (sans == null) { + return false; + } + // Iterate through all Subject Alternative Names + for (List san : sans) { + // Check if the SAN entry is a URI (type 6) + if (san.size() >= 2 + && san.get(0) instanceof Integer + && (Integer) san.get(0) == SAN_URI_TYPE) { + Object value = san.get(1); + if (value instanceof String) { + String uri = (String) value; + // Check if the URI starts with "spiffe://" + if (uri.startsWith(SPIFFE_SCHEME_PREFIX)) { + String withoutScheme = uri.substring(SPIFFE_SCHEME_PREFIX.length()); + int slashIndex = withoutScheme.indexOf('/'); + // Extract the trust domain (part before the first slash) + String trustDomain = + (slashIndex == -1) ? withoutScheme : withoutScheme.substring(0, slashIndex); + // Match the trust domain against allowed agent patterns + for (Pattern pattern : AGENT_IDENTITY_SPIFFE_PATTERNS) { + if (pattern.matcher(trustDomain).matches()) { + return true; + } + } + } + } + } + } + } catch (CertificateParsingException e) { + LOGGER.warn("Failed to parse Subject Alternative Names from certificate", e); + } + return false; + } + + @VisibleForTesting + public static void setEnvReader(EnvReader reader) { + envReader = reader; + } + + @VisibleForTesting + static void setTimeService(TimeService service) { + timeService = service; + } + + @VisibleForTesting + static void resetTimeService() { + timeService = + new TimeService() { + @Override + public long currentTimeMillis() { + return System.currentTimeMillis(); + } + + @Override + public void sleep(long millis) throws InterruptedException { + Thread.sleep(millis); + } + }; + } +} diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java index ad5fb8e7dcf3..f833cc297a23 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java @@ -61,6 +61,7 @@ import java.io.ObjectInputStream; import java.net.SocketTimeoutException; import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -317,7 +318,7 @@ public String getUniverseDomain() throws IOException { private String getUniverseDomainFromMetadata() throws IOException { HttpResponse response = - getMetadataResponse(getUniverseDomainUrl(), RequestType.UNTRACKED, false); + getMetadataResponse(getUniverseDomainUrl(), "GET", null, RequestType.UNTRACKED, false); int statusCode = response.getStatusCode(); if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) { return Credentials.GOOGLE_DEFAULT_UNIVERSE; @@ -376,7 +377,8 @@ public String getProjectId() { private String getProjectIdFromMetadata() { try { - HttpResponse response = getMetadataResponse(getProjectIdUrl(), RequestType.UNTRACKED, false); + HttpResponse response = + getMetadataResponse(getProjectIdUrl(), "GET", null, RequestType.UNTRACKED, false); int statusCode = response.getStatusCode(); if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) { LoggingUtils.log( @@ -418,8 +420,21 @@ private String getProjectIdFromMetadata() { /** Refresh the access token by getting it from the GCE metadata server */ @Override public AccessToken refreshAccessToken() throws IOException { - HttpResponse response = - getMetadataResponse(createTokenUrlWithScopes(), RequestType.ACCESS_TOKEN_REQUEST, true); + String tokenUrl = createTokenUrlWithScopes(); + + String boundTokenPayload = AgentIdentityUtils.getBoundTokenPayload(); + HttpResponse response; + + if (boundTokenPayload != null) { + java.util.Map payload = + Collections.singletonMap("certificate_chain", boundTokenPayload); + String jsonString = OAuth2Utils.JSON_FACTORY.toString(payload); + + response = + getMetadataResponse(tokenUrl, "POST", jsonString, RequestType.ACCESS_TOKEN_REQUEST, true); + } else { + response = getMetadataResponse(tokenUrl, "GET", null, RequestType.ACCESS_TOKEN_REQUEST, true); + } int statusCode = response.getStatusCode(); if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) { throw new IOException( @@ -487,8 +502,22 @@ public IdToken idTokenWithAudience(String targetAudience, List payload = + Collections.singletonMap("certificate_chain", boundTokenPayload); + String jsonString = OAuth2Utils.JSON_FACTORY.toString(payload); + + response = + getMetadataResponse( + documentUrl.toString(), "POST", jsonString, RequestType.ID_TOKEN_REQUEST, true); + } else { + response = + getMetadataResponse( + documentUrl.toString(), "GET", null, RequestType.ID_TOKEN_REQUEST, true); + } int statusCode = response.getStatusCode(); if (statusCode == HttpStatusCodes.STATUS_CODE_NOT_FOUND) { throw new IOException( @@ -517,10 +546,26 @@ public IdToken idTokenWithAudience(String targetAudience, List spiffeEntry = Arrays.asList(6, uri); + Collection> sans = Collections.singletonList(spiffeEntry); + when(mockCert.getSubjectAlternativeNames()).thenReturn(sans); + return mockCert; + } + + @Test + public void getAgentIdentityCertificate_optedOut_returnsNullImmediately() throws IOException { + envProvider.setEnv("GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES", "true"); + envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", "/non/existent/path"); + assertNull(AgentIdentityUtils.getAgentIdentityCertInfo()); + } + + @Test + public void getAgentIdentityCertificate_noConfigEnvVar_returnsNull() throws IOException { + AgentIdentityUtils.setTimeService(new FakeTimeService()); + assertThrows(IOException.class, AgentIdentityUtils::getAgentIdentityCertInfo); + } + + @Test + public void getAgentIdentityCertificate_happyPath_loadsCertificate() throws IOException { + URL certUrl = getClass().getClassLoader().getResource("x509_leaf_certificate.pem"); + assertNotNull(certUrl, "Test resource x509_leaf_certificate.pem not found"); + String certPath = new File(certUrl.getFile()).getAbsolutePath(); + File configFile = tempDir.resolve("config.json").toFile(); + String configJson = + "{" + + " \"cert_configs\": {" + + " \"workload\": {" + + " \"cert_path\": \"" + + certPath.replace("\\", "\\\\") + + "\"" + + " }" + + " }" + + "}"; + try (FileOutputStream fos = new FileOutputStream(configFile)) { + fos.write(configJson.getBytes(StandardCharsets.UTF_8)); + } + envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", configFile.getAbsolutePath()); + AgentIdentityUtils.CertInfo info = AgentIdentityUtils.getAgentIdentityCertInfo(); + assertNotNull(info); + assertTrue(info.certificate.getIssuerDN().getName().contains("unit-tests")); + } + + @Test + public void getAgentIdentityCertificate_timeout_throwsIOException() { + envProvider.setEnv( + "GOOGLE_API_CERTIFICATE_CONFIG", + tempDir.resolve("missing.json").toAbsolutePath().toString()); + AgentIdentityUtils.setTimeService(new FakeTimeService()); + IOException e = assertThrows(IOException.class, AgentIdentityUtils::getAgentIdentityCertInfo); + assertTrue( + e.getMessage() + .contains( + "Unable to find Agent Identity certificate config or file for bound token request after multiple retries.")); + } + + @Test + public void shouldEnableMtls_true_certsPresent_returnsTrue() throws IOException { + envProvider.setEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "true"); + assertTrue(AgentIdentityUtils.shouldEnableMtls(true, true)); + } + + @Test + public void shouldEnableMtls_true_certsMissing_throwsIOException() { + envProvider.setEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "true"); + assertThrows(IOException.class, () -> AgentIdentityUtils.shouldEnableMtls(false, true)); + } + + @Test + public void shouldEnableMtls_false_certsPresent_returnsFalse() throws IOException { + envProvider.setEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false"); + assertFalse(AgentIdentityUtils.shouldEnableMtls(true, true)); + } + + @Test + public void shouldEnableMtls_unset_certsPresent_returnsTrue() throws IOException { + envProvider.setEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE", null); + assertTrue(AgentIdentityUtils.shouldEnableMtls(true, true)); + } + + @Test + public void getAgentIdentityCertInfo_fallbackPath_loadsCertificate() throws IOException { + AgentIdentityUtils.setWellKnownDir(tempDir.toAbsolutePath().toString() + "/"); + + URL certUrl = getClass().getClassLoader().getResource("x509_leaf_certificate.pem"); + assertNotNull(certUrl, "Test resource x509_leaf_certificate.pem not found"); + String certPath = new File(certUrl.getFile()).getAbsolutePath(); + + Files.copy(Paths.get(certPath), tempDir.resolve("certificates.pem")); + + envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", null); + + AgentIdentityUtils.CertInfo info = AgentIdentityUtils.getAgentIdentityCertInfo(); + assertNotNull(info); + assertEquals(tempDir.resolve("certificates.pem").toAbsolutePath().toString(), info.path); + } + + @Test + public void verifyKeyPair_match_returnsTrue() throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair kp = kpg.generateKeyPair(); + + X509Certificate mockCert = mock(X509Certificate.class); + when(mockCert.getPublicKey()).thenReturn(kp.getPublic()); + + assertTrue(AgentIdentityUtils.verifyKeyPair(mockCert, kp.getPrivate())); + } + + @Test + public void verifyKeyPair_mismatch_returnsFalse() throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair kp1 = kpg.generateKeyPair(); + KeyPair kp2 = kpg.generateKeyPair(); + + X509Certificate mockCert = mock(X509Certificate.class); + when(mockCert.getPublicKey()).thenReturn(kp1.getPublic()); + + assertFalse(AgentIdentityUtils.verifyKeyPair(mockCert, kp2.getPrivate())); + } + + @Test + public void getAgentIdentityCertInfo_mismatch_throwsIOExceptionAfterRetries() throws Exception { + URL certUrl = getClass().getClassLoader().getResource("x509_leaf_certificate.pem"); + assertNotNull(certUrl, "Test resource x509_leaf_certificate.pem not found"); + String certPath = new File(certUrl.getFile()).getAbsolutePath(); + + // Generate a random key that won't match the cert + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + KeyPair kp = kpg.generateKeyPair(); + + File keyFile = tempDir.resolve("private_key.pem").toFile(); + String keyPem = + "-----BEGIN PRIVATE KEY-----\n" + + java.util.Base64.getMimeEncoder().encodeToString(kp.getPrivate().getEncoded()) + + "\n-----END PRIVATE KEY-----"; + try (FileOutputStream fos = new FileOutputStream(keyFile)) { + fos.write(keyPem.getBytes(StandardCharsets.UTF_8)); + } + + File configFile = tempDir.resolve("config.json").toFile(); + String configJson = + "{" + + " \"cert_configs\": {" + + " \"workload\": {" + + " \"cert_path\": \"" + + certPath.replace("\\", "\\\\") + + "\"," + + " \"key_path\": \"" + + keyFile.getAbsolutePath().replace("\\", "\\\\") + + "\"" + + " }" + + " }" + + "}"; + try (FileOutputStream fos = new FileOutputStream(configFile)) { + fos.write(configJson.getBytes(StandardCharsets.UTF_8)); + } + + envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", configFile.getAbsolutePath()); + + FakeTimeService fakeTime = new FakeTimeService(); + AgentIdentityUtils.setTimeService(fakeTime); + + IOException e = assertThrows(IOException.class, AgentIdentityUtils::getAgentIdentityCertInfo); + assertTrue( + e.getMessage() + .contains( + "Agent Identity certificate and private key mismatch or read failure after 3 retries.")); + assertEquals(200, fakeTime.currentTimeMillis()); // 2 retries * 100ms + } + + private static class FakeTimeService implements AgentIdentityUtils.TimeService { + private final AtomicLong currentTime = new AtomicLong(0); + + @Override + public long currentTimeMillis() { + return currentTime.get(); + } + + @Override + public void sleep(long millis) throws InterruptedException { + currentTime.addAndGet(millis); + } + } + + private static class TestEnvironmentProvider { + private final java.util.Map env = new java.util.HashMap<>(); + + void setEnv(String key, String value) { + env.put(key, value); + } + + String getEnv(String key) { + return env.get(key); + } + } +} diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java index 82240171d9af..af49109b6b04 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java @@ -60,16 +60,23 @@ import com.google.auth.oauth2.DefaultCredentialsProviderTest.MockRequestCountingTransportFactory; import java.io.IOException; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayDeque; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Queue; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** Test case for {@link ComputeEngineCredentials}. */ @@ -77,6 +84,41 @@ class ComputeEngineCredentialsTest extends BaseSerializationTest { private static final URI CALL_URI = URI.create("http://googleapis.com/testapi/v1/foo"); + private TestEnvironmentProvider envProvider; + private Path tempDir; + + @BeforeEach + void setUp() throws IOException { + envProvider = new TestEnvironmentProvider(); + // Inject our test environment reader into AgentIdentityUtils + AgentIdentityUtils.setEnvReader(envProvider::getEnv); + tempDir = Files.createTempDirectory("compute_engine_creds_test"); + + // Speed up polling in tests by using a fake time service that advances time immediately + final AtomicLong currentTime = new AtomicLong(0); + AgentIdentityUtils.setTimeService( + new AgentIdentityUtils.TimeService() { + @Override + public long currentTimeMillis() { + return currentTime.get(); + } + + @Override + public void sleep(long millis) { + currentTime.addAndGet(millis); + } + }); + // Opt out of bound tokens by default in tests to avoid polling delays + envProvider.setEnv("GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES", "true"); + } + + @AfterEach + void tearDown() { + // Reset the mocks + AgentIdentityUtils.resetTimeService(); + AgentIdentityUtils.setEnvReader(System::getenv); + } + private static final String TOKEN_URL = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"; @@ -1177,6 +1219,117 @@ void getProjectId_explicitSet_noMDsCall() { assertEquals(0, transportFactory.transport.getRequestCount()); } + @Test + void refreshAccessToken_agentConfigMissingFile_throws() throws IOException { + envProvider.setEnv("GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES", "false"); + envProvider.setEnv( + AgentIdentityUtils.GOOGLE_API_CERTIFICATE_CONFIG, + tempDir.resolve("missing_config.json").toAbsolutePath().toString()); + final AtomicLong currentTime = new AtomicLong(0); + AgentIdentityUtils.setTimeService( + new AgentIdentityUtils.TimeService() { + @Override + public long currentTimeMillis() { + return currentTime.get(); + } + + @Override + public void sleep(long millis) { + currentTime.addAndGet(millis); + } + }); + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + IOException e = assertThrows(IOException.class, credentials::refreshAccessToken); + assertTrue( + e.getMessage() + .contains( + "Unable to find Agent Identity certificate config or file for bound token request after multiple retries.")); + } + + private void setupCertAndKeyConfig() throws IOException { + java.nio.file.Path certSource = + java.nio.file.Paths.get("testresources/agent/agent_spiffe_cert.pem"); + java.nio.file.Path certTarget = tempDir.resolve("certificates.pem"); + Files.copy(certSource, certTarget, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + + java.nio.file.Path keySource = + java.nio.file.Paths.get("testresources/agent/agent_spiffe_key.pem"); + java.nio.file.Path keyTarget = tempDir.resolve("private_key.pem"); + Files.copy(keySource, keyTarget, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + + Path configPath = tempDir.resolve("config.json"); + Map workload = new HashMap<>(); + workload.put("cert_path", certTarget.toAbsolutePath().toString()); + workload.put("key_path", keyTarget.toAbsolutePath().toString()); + + Map certConfigs = new HashMap<>(); + certConfigs.put("workload", workload); + + Map config = new HashMap<>(); + config.put("cert_configs", certConfigs); + + String configContent = OAuth2Utils.JSON_FACTORY.toString(config); + Files.write(configPath, configContent.getBytes(StandardCharsets.UTF_8)); + envProvider.setEnv("GOOGLE_API_CERTIFICATE_CONFIG", configPath.toAbsolutePath().toString()); + } + + @Test + void refreshAccessToken_withValidCertAndKey_requestsBoundToken() throws IOException { + setupCertAndKeyConfig(); + envProvider.setEnv( + "GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES", "false"); // Enable bound token + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); + transportFactory.transport.setAccessToken("default", ACCESS_TOKEN); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + AccessToken token = credentials.refreshAccessToken(); + + assertNotNull(token); + com.google.api.client.testing.http.MockLowLevelHttpRequest request = + transportFactory.transport.getRequest(); + assertEquals("POST", transportFactory.transport.getRequestMethod()); + String body = request.getContentAsString(); + assertTrue(body.contains("certificate_chain")); + } + + @Test + void idTokenWithAudience_withValidCertAndKey_requestsBoundToken() throws IOException { + setupCertAndKeyConfig(); + envProvider.setEnv( + "GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES", "false"); // Enable bound token + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); + transportFactory.transport.setIdToken(STANDARD_ID_TOKEN); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + IdToken token = credentials.idTokenWithAudience("https://foo.bar", null); + + assertNotNull(token); + com.google.api.client.testing.http.MockLowLevelHttpRequest request = + transportFactory.transport.getRequest(); + assertEquals("POST", transportFactory.transport.getRequestMethod()); + String body = request.getContentAsString(); + assertTrue(body.contains("certificate_chain")); + } + + private static class TestEnvironmentProvider { + private final Map env = new HashMap<>(); + + void setEnv(String key, String value) { + env.put(key, value); + } + + String getEnv(String key) { + return env.get(key); + } + } + static class MockMetadataServerTransportFactory implements HttpTransportFactory { MockMetadataServerTransport transport = diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java index d0447871b01a..292679942ace 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/DefaultCredentialsProviderTest.java @@ -67,11 +67,31 @@ import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** Test case for {@link DefaultCredentialsProvider}. */ class DefaultCredentialsProviderTest { + @BeforeEach + void setUp() { + // Isolate tests and opt out of bound tokens by default to avoid polling delays + AgentIdentityUtils.setEnvReader( + name -> { + if ("GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES".equals(name)) { + return "true"; // Triggers isOptedOut() = true + } + return null; + }); + } + + @AfterEach + void tearDown() { + // Reset to default behavior. + AgentIdentityUtils.setEnvReader(System::getenv); + } + private static final String USER_CLIENT_SECRET = "jakuaL9YyieakhECKL2SwZcu"; private static final String USER_CLIENT_ID = "ya29.1.AADtN_UtlxN3PuGAxrN2XQnZTVRvDyVWnYq4I6dws"; private static final String GCLOUDSDK_CLIENT_ID = diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdTokenCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdTokenCredentialsTest.java index e3dcec4b520c..614a87e7d063 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdTokenCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdTokenCredentialsTest.java @@ -34,11 +34,42 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** Test case for {@link IdTokenCredentials}. */ class IdTokenCredentialsTest extends BaseSerializationTest { + private static class TestEnvironmentProvider { + private final Map env = new HashMap<>(); + + void setEnv(String key, String value) { + env.put(key, value); + } + + String getEnv(String key) { + return env.get(key); + } + } + + private TestEnvironmentProvider envProvider; + + @BeforeEach + void setUp() { + envProvider = new TestEnvironmentProvider(); + AgentIdentityUtils.setEnvReader(envProvider::getEnv); + // Opt out by default to avoid polling delays or file reads + envProvider.setEnv("GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES", "true"); + } + + @AfterEach + void tearDown() { + AgentIdentityUtils.setEnvReader(System::getenv); + } + @Test void hashCode_equals() throws IOException { ComputeEngineCredentialsTest.MockMetadataServerTransportFactory transportFactory = diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/LoggingTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/LoggingTest.java index 524a312ce0c1..8a68dd6eebd5 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/LoggingTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/LoggingTest.java @@ -65,7 +65,9 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -78,6 +80,23 @@ */ class LoggingTest { + @BeforeEach + void setUp() { + // Opt out of bound tokens by default to avoid polling delays + AgentIdentityUtils.setEnvReader( + name -> { + if ("GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES".equals(name)) { + return "true"; + } + return null; + }); + } + + @AfterEach + void tearDown() { + AgentIdentityUtils.setEnvReader(System::getenv); + } + private TestAppender setupTestLogger(Class clazz) { TestAppender testAppender = new TestAppender(); testAppender.start(); diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java index 1b218b73ef45..e927b0a28f9f 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java @@ -71,6 +71,7 @@ public class MockMetadataServerTransport extends MockHttpTransport { private boolean emptyContent; private MockLowLevelHttpRequest request; + private String requestMethod; public MockMetadataServerTransport() {} @@ -123,8 +124,13 @@ public MockLowLevelHttpRequest getRequest() { return request; } + public String getRequestMethod() { + return requestMethod; + } + @Override public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + this.requestMethod = method; if (url.startsWith(ComputeEngineCredentials.getTokenServerEncodedUrl())) { this.request = getMockRequestForTokenEndpoint(url); return this.request; diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/functional/FTComputeEngineCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/functional/FTComputeEngineCredentialsTest.java index 1a27d3cdc98a..52c23c1f14a1 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/functional/FTComputeEngineCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/functional/FTComputeEngineCredentialsTest.java @@ -39,18 +39,50 @@ import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.json.webtoken.JsonWebSignature; import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.AgentIdentityUtils; import com.google.auth.oauth2.ComputeEngineCredentials; import com.google.auth.oauth2.GoogleCredentials; import com.google.auth.oauth2.IdToken; import com.google.auth.oauth2.IdTokenCredentials; import com.google.auth.oauth2.IdTokenProvider; import com.google.auth.oauth2.OAuth2Utils; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; final class FTComputeEngineCredentialsTest { private final String computeUrl = "https://compute.googleapis.com/compute/v1/projects/gcloud-devel/zones/us-central1-a/instances"; + private static class TestEnvironmentProvider { + private final Map env = new HashMap<>(); + + void setEnv(String key, String value) { + env.put(key, value); + } + + String getEnv(String key) { + return env.get(key); + } + } + + private TestEnvironmentProvider envProvider; + + @BeforeEach + void setUp() { + envProvider = new TestEnvironmentProvider(); + AgentIdentityUtils.setEnvReader(envProvider::getEnv); + // Opt out by default to avoid polling delays or file reads + envProvider.setEnv("GOOGLE_API_PREVENT_TOKEN_SHARING_FOR_GCP_SERVICES", "true"); + } + + @AfterEach + void tearDown() { + AgentIdentityUtils.setEnvReader(System::getenv); + } + @Test void RefreshCredentials() throws Exception { final ComputeEngineCredentials credentials = ComputeEngineCredentials.create(); diff --git a/google-auth-library-java/oauth2_http/testresources/agent/agent_spiffe_cert.pem b/google-auth-library-java/oauth2_http/testresources/agent/agent_spiffe_cert.pem new file mode 100644 index 000000000000..885ecec9b6cf --- /dev/null +++ b/google-auth-library-java/oauth2_http/testresources/agent/agent_spiffe_cert.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUb5t9VPgT3uTrj+z47BfU8xF6sFowDQYJKoZIhvcNAQEL +BQAwIjEgMB4GA1UEAwwXVGVzdCBTUElGRkUgQ2VydGlmaWNhdGUwHhcNMjYwNTEx +MjMyODU3WhcNMzYwNTA4MjMyODU3WjAiMSAwHgYDVQQDDBdUZXN0IFNQSUZGRSBD +ZXJ0aWZpY2F0ZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJlPtSAh +hYqci8YJaPlgZWUbup4q/TGuaLdR/zWpFCevAQW7/hq29LT8WsoyuirSlbJg67io +PPuNp98e614L/88jP3wMeSuBSXGMyeExrMQ35ZbrnmD268Fl/jEZXPujmeFWmRoy +miKSkNvYLvkN0fiooyFYZdNG0/f6zT+dwJqoOYgeBl9XXDf5QjnQbhQXFQJIbxfQ +TTaGFtEXXwKCs9cd5EvwbiDuaM+F2O7txkaD4r+77AjsXWUFkBBbSUzs2b3ujZot +ZqwNCeeGML/G+5TQj3UExTtDzYpjvncEuqdomFraqXy6DmyELhyChugsz5rGx5Vt +aP3RvDkpLwUxjWcCAwEAAaOBnzCBnDAdBgNVHQ4EFgQUoS1OUx9a1hBHt0tHAefV +7QQ3JYswHwYDVR0jBBgwFoAUoS1OUx9a1hBHt0tHAefV7QQ3JYswDwYDVR0TAQH/ +BAUwAwEB/zBJBgNVHREEQjBAhj5zcGlmZmU6Ly9hZ2VudHMuZ2xvYmFsLnByb2ot +MTIzNDUuc3lzdGVtLmlkLmdvb2cvdGVzdC13b3JrbG9hZDANBgkqhkiG9w0BAQsF +AAOCAQEAfCrNuLFIlpvtDpBKD8lxj2vF4/6fbLhl/5YGGFf2uydaWLr2hTmLFrer +fzGf04hPD5UJTms0ZlvHTvapm1IikxKhkmp8GOlMvKWl/EwIDaJyJ8EaYCpkrTNs +pLR/ujgeSKnIHgm2Oql7HZxB1T25teFcLIIMd0zs69h/Sxejw+OTKnwb7san1qSy +uNvXVDPNFrGyjvBXAVyjyvs3Adz/A+GYhyaP31s+3qGUUR4/axv0M8pUVmK6D8lU +5TkO7smVELt51xaq77Nvv1r5FasJkF5CrnqwAjq1QfVJpDsuTCEriUXguxaXFbzM +Qfw7TEXp3xPVinhkDjlRfiGg6hIvNw== +-----END CERTIFICATE----- diff --git a/google-auth-library-java/oauth2_http/testresources/agent/agent_spiffe_key.pem b/google-auth-library-java/oauth2_http/testresources/agent/agent_spiffe_key.pem new file mode 100644 index 000000000000..b136c31e3db7 --- /dev/null +++ b/google-auth-library-java/oauth2_http/testresources/agent/agent_spiffe_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCZT7UgIYWKnIvG +CWj5YGVlG7qeKv0xrmi3Uf81qRQnrwEFu/4atvS0/FrKMroq0pWyYOu4qDz7jaff +HuteC//PIz98DHkrgUlxjMnhMazEN+WW655g9uvBZf4xGVz7o5nhVpkaMpoikpDb +2C75DdH4qKMhWGXTRtP3+s0/ncCaqDmIHgZfV1w3+UI50G4UFxUCSG8X0E02hhbR +F18CgrPXHeRL8G4g7mjPhdju7cZGg+K/u+wI7F1lBZAQW0lM7Nm97o2aLWasDQnn +hjC/xvuU0I91BMU7Q82KY753BLqnaJha2ql8ug5shC4cgoboLM+axseVbWj90bw5 +KS8FMY1nAgMBAAECggEAA/g2Eq6bhEYr/lHL2u0kIvT2JSNGBAb/WIPLs/cWE2le +GujOGjrGskSSEaGa0JuiqAg6m9p+hCFPIyFShQOSUsNkYOqUkDJPe8+vG62xsTDx +uMsoAijsKMlI+bqtsPXm+MuWHw4Hww5nDsvI4Sz6iB6bS4E8JB2cEaDEtx/do7aq +5XHJYoy0kO/l87n1UlIq8Pi6yW36pQKKEK+Dm5q+QmgJIeNLFp0hb5r7/fLFENTD +CZO0GvMdmtTkTwyPB1mvFjP6mNNAZcO6j9eaWNCxFhPH+MNWtF+694wgPOQIFKL3 +n4FPmWUS7vGJjNI55Ff2tTHrbWcMuf4RwXbVIG+v1QKBgQDPnNEs2TRrMglFtT6J +wDbICK8AQbBAPFjtZUGxNnsCOdiprZXshZJ7Yq8lokDlsU+5l0YTzRj2v1IcEL49 +oy8WeQfbcOZTGgf+Qo50ZiLhziCqNTlxiXMp3Kg5h4cjxUz2tsF2Nmzn75411uPa +Q/SKHCzfz4uUtHJla8tHENuzdQKBgQC9CwOleHwdS5D1jzBpb/+rvc0vcyYeihZe +v2ao2ItEZZpDyZoou3HiuTe8b9jpg4iS1PtBe6ck/99dBA3CSCF6VCS+++fc+HOz +IyPZjfg8qQ/88XMIEJy8eAa2OFT/EwK3AeZvIuXVumUHI2R8AfkVB0OfjSVZ7hnI +p1ndQ09t6wKBgHDiQVHzX+8RK719SN25Z4/oOM8Y6G5k4a1iqw9iIgwZy9amjagn +EHiKNdVunX7GpCSzPeUyVWqEqG6eI/J7sfS0JjOI9ZMlykbThYWAq2K/oz8o5Wz4 +YWfXlJiDOlWWx7w1rodKHHkX7pwzlXxuCp61pyiiPrDCVJkUvViMsAipAoGAQHda +FfqhcKgNVgAvhTVBXgLKzwyYij+S41qoGppF29w+IDHG1W8epi99d1A5C2DkmRXy +XOFbHX34YNL6Ei/g4sOBCHQFHNDJO+SW3CDS73TD1AFOtghcOtU/jLJnIdkMyvXl +7C5dbGY0/5stMDDIDUi94dITU7ijqE6RkafblWMCgYEAxwFH2JJn8HyU30PVrIFh +AsviZ3BYrCbYFlkZpelCHzHc5imyR9euLwXHIPS0vTE5V8GzkPEgW3DIOFMmBwe2 +8M/jGi/RQCnD7Hh0C+Y0vtUyF8z88lvN5ac6lU9UpGVBBkd8HEsq5ukL3PlnW4B0 +uRGJOa6FU01LIEMoCL5LP28= +-----END PRIVATE KEY-----