4646import com .google .auth .ApiKeyCredentials ;
4747import com .google .auth .Credentials ;
4848import com .google .auth .oauth2 .ComputeEngineCredentials ;
49+ import com .google .auth .oauth2 .SecureSessionAgent ;
50+ import com .google .auth .oauth2 .SecureSessionAgentConfig ;
4951import com .google .common .annotations .VisibleForTesting ;
5052import com .google .common .base .Preconditions ;
53+ import com .google .common .base .Strings ;
5154import com .google .common .collect .ImmutableList ;
5255import com .google .common .collect .ImmutableMap ;
5356import com .google .common .io .Files ;
5457import io .grpc .CallCredentials ;
5558import io .grpc .ChannelCredentials ;
5659import io .grpc .Grpc ;
60+ import io .grpc .InsecureChannelCredentials ;
5761import io .grpc .ManagedChannel ;
5862import io .grpc .ManagedChannelBuilder ;
5963import io .grpc .TlsChannelCredentials ;
6064import io .grpc .alts .GoogleDefaultChannelCredentials ;
6165import io .grpc .auth .MoreCallCredentials ;
66+ import io .grpc .s2a .S2AChannelCredentials ;
6267import java .io .File ;
6368import java .io .IOException ;
6469import java .nio .charset .StandardCharsets ;
@@ -99,6 +104,15 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP
99104 @ VisibleForTesting
100105 static final String DIRECT_PATH_ENV_ENABLE_XDS = "GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS" ;
101106
107+ // The public portion of the mTLS MDS root certificate is stored for performing
108+ // cert verification when establishing an mTLS connection with the MDS. See
109+ // https://cloud.google.com/compute/docs/metadata/overview#https-mds-root-certs
110+ private static final String MTLS_MDS_ROOT_PATH = "/run/google-mds-mtls/root.crt" ;
111+ // The mTLS MDS credentials are formatted as the concatenation of a PEM-encoded certificate chain
112+ // followed by a PEM-encoded private key. See
113+ // https://cloud.google.com/compute/docs/metadata/overview#https-mds-client-certs
114+ private static final String MTLS_MDS_CERT_CHAIN_AND_KEY_PATH = "/run/google-mds-mtls/client.key" ;
115+
102116 static final long DIRECT_PATH_KEEP_ALIVE_TIME_SECONDS = 3600 ;
103117 static final long DIRECT_PATH_KEEP_ALIVE_TIMEOUT_SECONDS = 20 ;
104118 static final String GCE_PRODUCTION_NAME_PRIOR_2016 = "Google" ;
@@ -107,6 +121,7 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP
107121 private final int processorCount ;
108122 private final Executor executor ;
109123 private final HeaderProvider headerProvider ;
124+ private final boolean useS2A ;
110125 private final String endpoint ;
111126 // TODO: remove. envProvider currently provides DirectPath environment variable, and is only used
112127 // during initial rollout for DirectPath. This provider will be removed once the DirectPath
@@ -126,6 +141,7 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP
126141 @ Nullable private final Boolean allowNonDefaultServiceAccount ;
127142 @ VisibleForTesting final ImmutableMap <String , ?> directPathServiceConfig ;
128143 @ Nullable private final MtlsProvider mtlsProvider ;
144+ @ Nullable private final SecureSessionAgent s2aConfigProvider ;
129145 @ VisibleForTesting final Map <String , String > headersWithDuplicatesRemoved = new HashMap <>();
130146
131147 @ Nullable
@@ -136,7 +152,9 @@ private InstantiatingGrpcChannelProvider(Builder builder) {
136152 this .executor = builder .executor ;
137153 this .headerProvider = builder .headerProvider ;
138154 this .endpoint = builder .endpoint ;
155+ this .useS2A = builder .useS2A ;
139156 this .mtlsProvider = builder .mtlsProvider ;
157+ this .s2aConfigProvider = builder .s2aConfigProvider ;
140158 this .envProvider = builder .envProvider ;
141159 this .interceptorProvider = builder .interceptorProvider ;
142160 this .maxInboundMessageSize = builder .maxInboundMessageSize ;
@@ -225,6 +243,17 @@ public TransportChannelProvider withEndpoint(String endpoint) {
225243 return toBuilder ().setEndpoint (endpoint ).build ();
226244 }
227245
246+ /**
247+ * Specify whether or not to use S2A.
248+ *
249+ * @param useS2A
250+ * @return A new {@link InstantiatingGrpcChannelProvider} with useS2A set.
251+ */
252+ @ Override
253+ public TransportChannelProvider withUseS2A (boolean useS2A ) {
254+ return toBuilder ().setUseS2A (useS2A ).build ();
255+ }
256+
228257 /** @deprecated Please modify pool settings via {@link #toBuilder()} */
229258 @ Deprecated
230259 @ Override
@@ -410,6 +439,101 @@ ChannelCredentials createMtlsChannelCredentials() throws IOException, GeneralSec
410439 return null ;
411440 }
412441
442+ /**
443+ * This method creates {@link TlsChannelCredentials} to be used by the client to establish an mTLS
444+ * connection to S2A. Returns null if any of {@param trustBundle}, {@param privateKey} or {@param
445+ * certChain} are missing.
446+ *
447+ * @param trustBundle the trust bundle to be used to establish the client -> S2A mTLS connection
448+ * @param privateKey the client's private key to be used to establish the client -> S2A mtls
449+ * connection
450+ * @param certChain the client's cert chain to be used to establish the client -> S2A mtls
451+ * connection
452+ * @return {@link ChannelCredentials} to use to create an mtls connection between client and S2A
453+ * @throws IOException on error
454+ */
455+ @ VisibleForTesting
456+ ChannelCredentials createMtlsToS2AChannelCredentials (
457+ File trustBundle , File privateKey , File certChain ) throws IOException {
458+ if (trustBundle == null || privateKey == null || certChain == null ) {
459+ return null ;
460+ }
461+ return TlsChannelCredentials .newBuilder ()
462+ .keyManager (privateKey , certChain )
463+ .trustManager (trustBundle )
464+ .build ();
465+ }
466+
467+ /**
468+ * This method creates {@link ChannelCredentials} to be used by client to establish a plaintext
469+ * connection to S2A. if {@param plaintextAddress} is not present, returns null.
470+ *
471+ * @param plaintextAddress the address to reach S2A which accepts plaintext connections
472+ * @return {@link ChannelCredentials} to use to create a plaintext connection between client and
473+ * S2A
474+ */
475+ ChannelCredentials createPlaintextToS2AChannelCredentials (String plaintextAddress ) {
476+ if (Strings .isNullOrEmpty (plaintextAddress )) {
477+ return null ;
478+ }
479+ return S2AChannelCredentials .newBuilder (plaintextAddress , InsecureChannelCredentials .create ())
480+ .build ();
481+ }
482+
483+ /**
484+ * This method creates gRPC {@link ChannelCredentials} configured to use S2A to estbalish a mTLS
485+ * connection. First, the address of S2A is discovered by using the {@link S2A} utility to learn
486+ * the {@code mtlsAddress} to reach S2A and the {@code plaintextAddress} to reach S2A. Prefer to
487+ * use the {@code mtlsAddress} address to reach S2A if it is non-empty and the MTLS-MDS
488+ * credentials can successfully be discovered and used to create {@link TlsChannelCredentials}. If
489+ * there is any failure using mTLS-to-S2A, fallback to using a plaintext connection to S2A using
490+ * the {@code plaintextAddress}. If {@code plaintextAddress} is not available, this function
491+ * returns null; in this case S2A will not be used, and a TLS connection to the service will be
492+ * established.
493+ *
494+ * @return {@link ChannelCredentials} configured to use S2A to create mTLS connection to
495+ * mtlsEndpoint.
496+ */
497+ ChannelCredentials createS2ASecuredChannelCredentials () {
498+ SecureSessionAgentConfig config = s2aConfigProvider .getConfig ();
499+ String plaintextAddress = config .getPlaintextAddress ();
500+ String mtlsAddress = config .getMtlsAddress ();
501+ if (Strings .isNullOrEmpty (mtlsAddress )) {
502+ // Fallback to plaintext connection to S2A.
503+ LOG .log (
504+ Level .INFO ,
505+ "Cannot establish an mTLS connection to S2A because autoconfig endpoint did not return a mtls address to reach S2A." );
506+ return createPlaintextToS2AChannelCredentials (plaintextAddress );
507+ }
508+ // Currently, MTLS to MDS is only available on GCE. See:
509+ // https://cloud.google.com/compute/docs/metadata/overview#https-mds
510+ // Try to load MTLS-MDS creds.
511+ File rootFile = new File (MTLS_MDS_ROOT_PATH );
512+ File certKeyFile = new File (MTLS_MDS_CERT_CHAIN_AND_KEY_PATH );
513+ if (rootFile .isFile () && certKeyFile .isFile ()) {
514+ // Try to connect to S2A using mTLS.
515+ ChannelCredentials mtlsToS2AChannelCredentials = null ;
516+ try {
517+ mtlsToS2AChannelCredentials =
518+ createMtlsToS2AChannelCredentials (rootFile , certKeyFile , certKeyFile );
519+ } catch (IOException ignore ) {
520+ // Fallback to plaintext-to-S2A connection on error.
521+ LOG .log (
522+ Level .WARNING ,
523+ "Cannot establish an mTLS connection to S2A due to error creating MTLS to MDS TlsChannelCredentials credentials, falling back to plaintext connection to S2A: "
524+ + ignore .getMessage ());
525+ return createPlaintextToS2AChannelCredentials (plaintextAddress );
526+ }
527+ return S2AChannelCredentials .newBuilder (mtlsAddress , mtlsToS2AChannelCredentials ).build ();
528+ } else {
529+ // Fallback to plaintext-to-S2A connection if MTLS-MDS creds do not exist.
530+ LOG .log (
531+ Level .INFO ,
532+ "Cannot establish an mTLS connection to S2A because MTLS to MDS credentials do not exist on filesystem, falling back to plaintext connection to S2A" );
533+ return createPlaintextToS2AChannelCredentials (plaintextAddress );
534+ }
535+ }
536+
413537 private ManagedChannel createSingleChannel () throws IOException {
414538 GrpcHeaderInterceptor headerInterceptor =
415539 new GrpcHeaderInterceptor (headersWithDuplicatesRemoved );
@@ -447,16 +571,31 @@ private ManagedChannel createSingleChannel() throws IOException {
447571 builder .keepAliveTime (DIRECT_PATH_KEEP_ALIVE_TIME_SECONDS , TimeUnit .SECONDS );
448572 builder .keepAliveTimeout (DIRECT_PATH_KEEP_ALIVE_TIMEOUT_SECONDS , TimeUnit .SECONDS );
449573 } else {
574+ // Try and create credentials via DCA. See https://google.aip.dev/auth/4114.
450575 ChannelCredentials channelCredentials ;
451576 try {
452577 channelCredentials = createMtlsChannelCredentials ();
453578 } catch (GeneralSecurityException e ) {
454579 throw new IOException (e );
455580 }
456581 if (channelCredentials != null ) {
582+ // Create the channel using channel credentials created via DCA.
457583 builder = Grpc .newChannelBuilder (endpoint , channelCredentials );
458584 } else {
459- builder = ManagedChannelBuilder .forAddress (serviceAddress , port );
585+ // Could not create channel credentials via DCA. In accordance with
586+ // https://google.aip.dev/auth/4115, if credentials not available through
587+ // DCA, try mTLS with credentials held by the S2A (Secure Session Agent).
588+ if (useS2A ) {
589+ channelCredentials = createS2ASecuredChannelCredentials ();
590+ }
591+ if (channelCredentials != null ) {
592+ // Create the channel using S2A-secured channel credentials.
593+ // {@code endpoint} is set to mtlsEndpoint in {@link EndpointContext} when useS2A is true.
594+ builder = Grpc .newChannelBuilder (endpoint , channelCredentials );
595+ } else {
596+ // Use default if we cannot initialize channel credentials via DCA or S2A.
597+ builder = ManagedChannelBuilder .forAddress (serviceAddress , port );
598+ }
460599 }
461600 }
462601 // google-c2p resolver requires service config lookup
@@ -604,7 +743,9 @@ public static final class Builder {
604743 private Executor executor ;
605744 private HeaderProvider headerProvider ;
606745 private String endpoint ;
746+ private boolean useS2A ;
607747 private EnvironmentProvider envProvider ;
748+ private SecureSessionAgent s2aConfigProvider = SecureSessionAgent .create ();
608749 private MtlsProvider mtlsProvider = new MtlsProvider ();
609750 @ Nullable private GrpcInterceptorProvider interceptorProvider ;
610751 @ Nullable private Integer maxInboundMessageSize ;
@@ -632,6 +773,7 @@ private Builder(InstantiatingGrpcChannelProvider provider) {
632773 this .executor = provider .executor ;
633774 this .headerProvider = provider .headerProvider ;
634775 this .endpoint = provider .endpoint ;
776+ this .useS2A = provider .useS2A ;
635777 this .envProvider = provider .envProvider ;
636778 this .interceptorProvider = provider .interceptorProvider ;
637779 this .maxInboundMessageSize = provider .maxInboundMessageSize ;
@@ -648,6 +790,7 @@ private Builder(InstantiatingGrpcChannelProvider provider) {
648790 this .allowNonDefaultServiceAccount = provider .allowNonDefaultServiceAccount ;
649791 this .directPathServiceConfig = provider .directPathServiceConfig ;
650792 this .mtlsProvider = provider .mtlsProvider ;
793+ this .s2aConfigProvider = provider .s2aConfigProvider ;
651794 }
652795
653796 /**
@@ -700,12 +843,23 @@ public Builder setEndpoint(String endpoint) {
700843 return this ;
701844 }
702845
846+ Builder setUseS2A (boolean useS2A ) {
847+ this .useS2A = useS2A ;
848+ return this ;
849+ }
850+
703851 @ VisibleForTesting
704852 Builder setMtlsProvider (MtlsProvider mtlsProvider ) {
705853 this .mtlsProvider = mtlsProvider ;
706854 return this ;
707855 }
708856
857+ @ VisibleForTesting
858+ Builder setS2AConfigProvider (SecureSessionAgent s2aConfigProvider ) {
859+ this .s2aConfigProvider = s2aConfigProvider ;
860+ return this ;
861+ }
862+
709863 /**
710864 * Sets the GrpcInterceptorProvider for this TransportChannelProvider.
711865 *
0 commit comments