Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,15 @@
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.core.InternalApi;
import com.google.auth.http.HttpTransportFactory;
import java.io.Serializable;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.util.Enumeration;
import java.util.Objects;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

/**
* An HttpTransportFactory that creates {@link NetHttpTransport} instances configured for mTLS
Expand All @@ -49,8 +54,25 @@
*/
@NullMarked
@InternalApi
public class MtlsHttpTransportFactory implements HttpTransportFactory {
private final KeyStore mtlsKeyStore;
public class MtlsHttpTransportFactory implements HttpTransportFactory, Serializable {
private static final long serialVersionUID = 1L;
private final transient @Nullable KeyStore mtlsKeyStore;
private final transient boolean hasKeyStore;

/**
* Default no-arg constructor creating an instance without a KeyStore ({@code hasKeyStore() ==
* false}). This constructor is invoked reflectively by {@code
* ExternalAccountCredentials.readObject()} during deserialization (since {@code transportFactory}
* is transient on {@code ExternalAccountCredentials}), after which {@code
* IdentityPoolCredentials.readObject()} reconstructs {@code X509Provider} from the serialized
* certificate configuration and replaces the transport factory with {@link
* #MtlsHttpTransportFactory(KeyStore)}. Not intended for direct use; callers configuring mTLS
* should use {@link #MtlsHttpTransportFactory(KeyStore)}.
*/
public MtlsHttpTransportFactory() {
this.mtlsKeyStore = null;
this.hasKeyStore = false;
}

/**
* Constructs a factory for mTLS transports.
Expand All @@ -61,6 +83,46 @@
*/
public MtlsHttpTransportFactory(KeyStore mtlsKeyStore) {
this.mtlsKeyStore = Objects.requireNonNull(mtlsKeyStore, "mtlsKeyStore cannot be null");
this.hasKeyStore = checkHasKeyStore(this.mtlsKeyStore);
}

/**
* Returns whether this factory was constructed with a non-null {@link KeyStore} containing client
* certificates for mTLS. A factory created via the no-arg constructor (e.g. during
* deserialization), with an empty KeyStore, or with a KeyStore containing only trusted CA
* certificates (without a private key entry and certificate chain) will return {@code false}.
*/
public boolean hasKeyStore() {

Check warning on line 95 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java

View workflow job for this annotation

GitHub Actions / bom-content-test

Check warning on line 95 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java

View workflow job for this annotation

GitHub Actions / BomContentAssertionsTest (Test for assertion logic in BomContentTest)

return this.hasKeyStore;
}

/** Returns the {@link KeyStore} used by this factory, or {@code null} if none was configured. */
public @Nullable KeyStore getKeyStore() {

Check warning on line 100 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java

View workflow job for this annotation

GitHub Actions / bom-content-test

Check warning on line 100 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsHttpTransportFactory.java

View workflow job for this annotation

GitHub Actions / BomContentAssertionsTest (Test for assertion logic in BomContentTest)

return this.mtlsKeyStore;
}

private static boolean checkHasKeyStore(@Nullable KeyStore keyStore) {
if (keyStore == null) {
return false;
}
try {
Enumeration<String> aliases = keyStore.aliases();
if (aliases == null) {
return false;
}
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
if (keyStore.isKeyEntry(alias)) {
Certificate[] chain = keyStore.getCertificateChain(alias);
if (chain != null && chain.length > 0) {
return true;
}
}
}
return false;
} catch (KeyStoreException e) {
return false;
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ public class AwsCredentials extends ExternalAccountCredentials {

@Override
public AccessToken refreshAccessToken() throws IOException {
ImpersonatedCredentials impersonated = getImpersonatedCredentials();
if (impersonated != null) {
return impersonated.refreshAccessToken();
}

StsTokenExchangeRequest.Builder stsTokenExchangeRequest =
StsTokenExchangeRequest.newBuilder(retrieveSubjectToken(), getSubjectTokenType())
.setAudience(getAudience());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,20 @@
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Enumeration;
import java.util.List;
import java.util.regex.Matcher;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

/**
* Provider for retrieving the subject tokens for {@link IdentityPoolCredentials} by reading an
Expand All @@ -63,6 +67,9 @@
public class CertificateIdentityPoolSubjectTokenSupplier
implements IdentityPoolSubjectTokenSupplier {

// Matches the computed value in released versions so previously serialized instances still load.
private static final long serialVersionUID = -7726329500407809830L;

private final IdentityPoolCredentialSource credentialSource;

CertificateIdentityPoolSubjectTokenSupplier(IdentityPoolCredentialSource credentialSource) {
Expand Down Expand Up @@ -92,6 +99,34 @@ private static String loadAndEncodeLeafCertificate(String path) throws IOExcepti
}
}

private static @Nullable String extractAndEncodeLeafCertificate(@Nullable KeyStore keyStore) {
if (keyStore == null) {
return null;
}
try {
Enumeration<String> aliases = keyStore.aliases();
if (aliases == null) {
return null;
}
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
if (keyStore.isKeyEntry(alias)) {
Certificate[] chain = keyStore.getCertificateChain(alias);
if (chain != null && chain.length > 0 && chain[0] instanceof X509Certificate) {
return encodeCert((X509Certificate) chain[0]);
}
Certificate cert = keyStore.getCertificate(alias);
if (cert instanceof X509Certificate) {
return encodeCert((X509Certificate) cert);
}
}
}
return null;
} catch (KeyStoreException | CertificateEncodingException e) {
return null;
}
}

@VisibleForTesting
static X509Certificate parseCertificate(byte[] certData) throws CertificateException {
if (certData == null || certData.length == 0) {
Expand Down Expand Up @@ -131,14 +166,27 @@ private static String encodeCert(X509Certificate certificate)
*/
@Override
public String getSubjectToken(ExternalAccountSupplierContext context) throws IOException {
String leafCertPath = credentialSource.getCredentialLocation();
return getSubjectToken(context, null);
}

/**
* Retrieves the X509 subject token, extracting the leaf certificate directly from {@code
* pinnedKeyStore} when provided so that the TLS client certificate and {@code subject_token} are
* pinned to the exact same certificate snapshot.
*/
String getSubjectToken(ExternalAccountSupplierContext context, @Nullable KeyStore pinnedKeyStore)
throws IOException {
String trustChainPath = null;
if (credentialSource.getCertificateConfig() != null) {
trustChainPath = credentialSource.getCertificateConfig().getTrustChainPath();
}

// Load and encode the leaf certificate.
String encodedLeafCert = loadAndEncodeLeafCertificate(leafCertPath);
// Extract the leaf certificate from the pinned KeyStore if present; otherwise read from disk.
String encodedLeafCert = extractAndEncodeLeafCertificate(pinnedKeyStore);
if (encodedLeafCert == null) {
String leafCertPath = credentialSource.getCredentialLocation();
encodedLeafCert = loadAndEncodeLeafCertificate(leafCertPath);
}

// Initialize the certificate chain for the subject token. The Security Token Service (STS)
// requires that the leaf certificate (the one used for authenticating this workload) must be
Expand Down Expand Up @@ -238,7 +286,7 @@ private void populateCertChainFromTrustChain(
* @throws CertificateException If an error occurs while parsing a certificate.
*/
@VisibleForTesting
static List<X509Certificate> readTrustChain(String trustChainPath)
static List<X509Certificate> readTrustChain(@Nullable String trustChainPath)
throws IOException, CertificateException {
List<X509Certificate> certificateTrustChain = new ArrayList<>();

Expand Down
Loading
Loading