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 ;
6266import java .io .File ;
6367import java .io .IOException ;
68+ import java .lang .reflect .Method ;
6469import java .nio .charset .StandardCharsets ;
6570import java .security .GeneralSecurityException ;
6671import java .security .KeyStore ;
@@ -99,6 +104,19 @@ 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+ // {@link <a
110+ // href="https://cloud.google.com/compute/docs/metadata/overview#https-mds-root-certs">this</a>
111+ // for more information.}
112+ private static final String MTLS_MDS_ROOT_PATH = "/run/google-mds-mtls/root.crt" ;
113+ // The mTLS MDS credentials are formatted as the concatenation of a PEM-encoded certificate chain
114+ // followed by a PEM-encoded private key. See
115+ // {@link <a
116+ // href="https://cloud.google.com/compute/docs/metadata/overview#https-mds-client-certs">this</a>
117+ // for more information.}
118+ private static final String MTLS_MDS_CERT_CHAIN_AND_KEY_PATH = "/run/google-mds-mtls/client.key" ;
119+
102120 static final long DIRECT_PATH_KEEP_ALIVE_TIME_SECONDS = 3600 ;
103121 static final long DIRECT_PATH_KEEP_ALIVE_TIMEOUT_SECONDS = 20 ;
104122 static final String GCE_PRODUCTION_NAME_PRIOR_2016 = "Google" ;
@@ -107,6 +125,7 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP
107125 private final int processorCount ;
108126 private final Executor executor ;
109127 private final HeaderProvider headerProvider ;
128+ private final boolean useS2A ;
110129 private final String endpoint ;
111130 // TODO: remove. envProvider currently provides DirectPath environment variable, and is only used
112131 // during initial rollout for DirectPath. This provider will be removed once the DirectPath
@@ -126,6 +145,7 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP
126145 @ Nullable private final Boolean allowNonDefaultServiceAccount ;
127146 @ VisibleForTesting final ImmutableMap <String , ?> directPathServiceConfig ;
128147 @ Nullable private final MtlsProvider mtlsProvider ;
148+ @ Nullable private final SecureSessionAgent s2aConfigProvider ;
129149 @ Nullable private final List <HardBoundTokenTypes > allowedHardBoundTokenTypes ;
130150 @ VisibleForTesting final Map <String , String > headersWithDuplicatesRemoved = new HashMap <>();
131151
@@ -153,9 +173,11 @@ private InstantiatingGrpcChannelProvider(Builder builder) {
153173 this .processorCount = builder .processorCount ;
154174 this .executor = builder .executor ;
155175 this .headerProvider = builder .headerProvider ;
176+ this .useS2A = builder .useS2A ;
156177 this .endpoint = builder .endpoint ;
157178 this .allowedHardBoundTokenTypes = builder .allowedHardBoundTokenTypes ;
158179 this .mtlsProvider = builder .mtlsProvider ;
180+ this .s2aConfigProvider = builder .s2aConfigProvider ;
159181 this .envProvider = builder .envProvider ;
160182 this .interceptorProvider = builder .interceptorProvider ;
161183 this .maxInboundMessageSize = builder .maxInboundMessageSize ;
@@ -244,6 +266,17 @@ public TransportChannelProvider withEndpoint(String endpoint) {
244266 return toBuilder ().setEndpoint (endpoint ).build ();
245267 }
246268
269+ /**
270+ * Specify whether or not to use S2A.
271+ *
272+ * @param useS2A
273+ * @return A new {@link InstantiatingGrpcChannelProvider} with useS2A set.
274+ */
275+ @ Override
276+ public TransportChannelProvider withUseS2A (boolean useS2A ) {
277+ return toBuilder ().setUseS2A (useS2A ).build ();
278+ }
279+
247280 /** @deprecated Please modify pool settings via {@link #toBuilder()} */
248281 @ Deprecated
249282 @ Override
@@ -429,6 +462,136 @@ ChannelCredentials createMtlsChannelCredentials() throws IOException, GeneralSec
429462 return null ;
430463 }
431464
465+ /**
466+ * Create the S2A-Secured Channel credentials. Load the API using reflection. Once the S2A API is
467+ * stable in gRPC-Java, all callers of this method can simply use the S2A APIs directly and this
468+ * method can be deleted.
469+ *
470+ * @param s2aAddress the address of the S2A server used to secure the connection.
471+ * @param s2aChannelCredentials the credentials to be used when connecting to the S2A.
472+ * @return {@code ChannelCredentials} instance.
473+ */
474+ ChannelCredentials buildS2AChannelCredentials (
475+ String s2aAddress , ChannelCredentials s2aChannelCredentials ) {
476+ try {
477+ // Load the S2A API.
478+ Class <?> s2aChannelCreds = Class .forName ("io.grpc.s2a.S2AChannelCredentials" );
479+ Class <?> s2aChannelCredsBuilder = Class .forName ("io.grpc.s2a.S2AChannelCredentials$Builder" );
480+
481+ // Load and invoke the S2A API methods.
482+ Class <?>[] partypes = new Class [2 ];
483+ partypes [0 ] = String .class ;
484+ partypes [1 ] = ChannelCredentials .class ;
485+ Method newBuilder = s2aChannelCreds .getMethod ("newBuilder" , partypes );
486+ Object arglist [] = new Object [2 ];
487+ arglist [0 ] = s2aAddress ;
488+ arglist [1 ] = s2aChannelCredentials ;
489+ Object retObjBuilder = newBuilder .invoke (null , arglist );
490+ Method build = s2aChannelCredsBuilder .getMethod ("build" , null );
491+ Object retObjCreds = build .invoke (retObjBuilder , null );
492+ return (ChannelCredentials ) retObjCreds ;
493+ } catch (Throwable t ) {
494+ LOG .log (
495+ Level .WARNING ,
496+ "Falling back to default (TLS without S2A) because S2A APIs cannot be used: "
497+ + t .getMessage ());
498+ return null ;
499+ }
500+ }
501+
502+ /**
503+ * This method creates {@link TlsChannelCredentials} to be used by the client to establish an mTLS
504+ * connection to S2A. Returns null if any of {@param trustBundle}, {@param privateKey} or {@param
505+ * certChain} are missing.
506+ *
507+ * @param trustBundle the trust bundle to be used to establish the client -> S2A mTLS connection
508+ * @param privateKey the client's private key to be used to establish the client -> S2A mtls
509+ * connection
510+ * @param certChain the client's cert chain to be used to establish the client -> S2A mtls
511+ * connection
512+ * @return {@link ChannelCredentials} to use to create an mtls connection between client and S2A
513+ * @throws IOException on error
514+ */
515+ @ VisibleForTesting
516+ ChannelCredentials createMtlsToS2AChannelCredentials (
517+ File trustBundle , File privateKey , File certChain ) throws IOException {
518+ if (trustBundle == null || privateKey == null || certChain == null ) {
519+ return null ;
520+ }
521+ return TlsChannelCredentials .newBuilder ()
522+ .keyManager (privateKey , certChain )
523+ .trustManager (trustBundle )
524+ .build ();
525+ }
526+
527+ /**
528+ * This method creates {@link ChannelCredentials} to be used by client to establish a plaintext
529+ * connection to S2A. if {@param plaintextAddress} is not present, returns null.
530+ *
531+ * @param plaintextAddress the address to reach S2A which accepts plaintext connections
532+ * @return {@link ChannelCredentials} to use to create a plaintext connection between client and
533+ * S2A
534+ */
535+ ChannelCredentials createPlaintextToS2AChannelCredentials (String plaintextAddress ) {
536+ if (Strings .isNullOrEmpty (plaintextAddress )) {
537+ return null ;
538+ }
539+ return buildS2AChannelCredentials (plaintextAddress , InsecureChannelCredentials .create ());
540+ }
541+
542+ /**
543+ * This method creates gRPC {@link ChannelCredentials} configured to use S2A to estbalish a mTLS
544+ * connection. First, the address of S2A is discovered by using the {@link S2A} utility to learn
545+ * the {@code mtlsAddress} to reach S2A and the {@code plaintextAddress} to reach S2A. Prefer to
546+ * use the {@code mtlsAddress} address to reach S2A if it is non-empty and the MTLS-MDS
547+ * credentials can successfully be discovered and used to create {@link TlsChannelCredentials}. If
548+ * there is any failure using mTLS-to-S2A, fallback to using a plaintext connection to S2A using
549+ * the {@code plaintextAddress}. If {@code plaintextAddress} is not available, this function
550+ * returns null; in this case S2A will not be used, and a TLS connection to the service will be
551+ * established.
552+ *
553+ * @return {@link ChannelCredentials} configured to use S2A to create mTLS connection.
554+ */
555+ ChannelCredentials createS2ASecuredChannelCredentials () {
556+ SecureSessionAgentConfig config = s2aConfigProvider .getConfig ();
557+ String plaintextAddress = config .getPlaintextAddress ();
558+ String mtlsAddress = config .getMtlsAddress ();
559+ if (Strings .isNullOrEmpty (mtlsAddress )) {
560+ // Fallback to plaintext connection to S2A.
561+ LOG .log (
562+ Level .INFO ,
563+ "Cannot establish an mTLS connection to S2A because autoconfig endpoint did not return a mtls address to reach S2A." );
564+ return createPlaintextToS2AChannelCredentials (plaintextAddress );
565+ }
566+ // Currently, MTLS to MDS is only available on GCE. See:
567+ // https://cloud.google.com/compute/docs/metadata/overview#https-mds
568+ // Try to load MTLS-MDS creds.
569+ File rootFile = new File (MTLS_MDS_ROOT_PATH );
570+ File certKeyFile = new File (MTLS_MDS_CERT_CHAIN_AND_KEY_PATH );
571+ if (rootFile .isFile () && certKeyFile .isFile ()) {
572+ // Try to connect to S2A using mTLS.
573+ ChannelCredentials mtlsToS2AChannelCredentials = null ;
574+ try {
575+ mtlsToS2AChannelCredentials =
576+ createMtlsToS2AChannelCredentials (rootFile , certKeyFile , certKeyFile );
577+ } catch (IOException ignore ) {
578+ // Fallback to plaintext-to-S2A connection on error.
579+ LOG .log (
580+ Level .WARNING ,
581+ "Cannot establish an mTLS connection to S2A due to error creating MTLS to MDS TlsChannelCredentials credentials, falling back to plaintext connection to S2A: "
582+ + ignore .getMessage ());
583+ return createPlaintextToS2AChannelCredentials (plaintextAddress );
584+ }
585+ return buildS2AChannelCredentials (mtlsAddress , mtlsToS2AChannelCredentials );
586+ } else {
587+ // Fallback to plaintext-to-S2A connection if MTLS-MDS creds do not exist.
588+ LOG .log (
589+ Level .INFO ,
590+ "Cannot establish an mTLS connection to S2A because MTLS to MDS credentials do not exist on filesystem, falling back to plaintext connection to S2A" );
591+ return createPlaintextToS2AChannelCredentials (plaintextAddress );
592+ }
593+ }
594+
432595 private ManagedChannel createSingleChannel () throws IOException {
433596 GrpcHeaderInterceptor headerInterceptor =
434597 new GrpcHeaderInterceptor (headersWithDuplicatesRemoved );
@@ -468,14 +631,28 @@ private ManagedChannel createSingleChannel() throws IOException {
468631 } else {
469632 ChannelCredentials channelCredentials ;
470633 try {
634+ // Try and create credentials via DCA. See https://google.aip.dev/auth/4114.
471635 channelCredentials = createMtlsChannelCredentials ();
472636 } catch (GeneralSecurityException e ) {
473637 throw new IOException (e );
474638 }
475639 if (channelCredentials != null ) {
640+ // Create the channel using channel credentials created via DCA.
476641 builder = Grpc .newChannelBuilder (endpoint , channelCredentials );
477642 } else {
478- builder = ManagedChannelBuilder .forAddress (serviceAddress , port );
643+ // Could not create channel credentials via DCA. In accordance with
644+ // https://google.aip.dev/auth/4115, if credentials not available through
645+ // DCA, try mTLS with credentials held by the S2A (Secure Session Agent).
646+ if (useS2A ) {
647+ channelCredentials = createS2ASecuredChannelCredentials ();
648+ }
649+ if (channelCredentials != null ) {
650+ // Create the channel using S2A-secured channel credentials.
651+ builder = Grpc .newChannelBuilder (endpoint , channelCredentials );
652+ } else {
653+ // Use default if we cannot initialize channel credentials via DCA or S2A.
654+ builder = ManagedChannelBuilder .forAddress (serviceAddress , port );
655+ }
479656 }
480657 }
481658 // google-c2p resolver requires service config lookup
@@ -623,7 +800,9 @@ public static final class Builder {
623800 private Executor executor ;
624801 private HeaderProvider headerProvider ;
625802 private String endpoint ;
803+ private boolean useS2A ;
626804 private EnvironmentProvider envProvider ;
805+ private SecureSessionAgent s2aConfigProvider = SecureSessionAgent .create ();
627806 private MtlsProvider mtlsProvider = new MtlsProvider ();
628807 @ Nullable private GrpcInterceptorProvider interceptorProvider ;
629808 @ Nullable private Integer maxInboundMessageSize ;
@@ -652,6 +831,7 @@ private Builder(InstantiatingGrpcChannelProvider provider) {
652831 this .executor = provider .executor ;
653832 this .headerProvider = provider .headerProvider ;
654833 this .endpoint = provider .endpoint ;
834+ this .useS2A = provider .useS2A ;
655835 this .envProvider = provider .envProvider ;
656836 this .interceptorProvider = provider .interceptorProvider ;
657837 this .maxInboundMessageSize = provider .maxInboundMessageSize ;
@@ -668,6 +848,7 @@ private Builder(InstantiatingGrpcChannelProvider provider) {
668848 this .allowNonDefaultServiceAccount = provider .allowNonDefaultServiceAccount ;
669849 this .directPathServiceConfig = provider .directPathServiceConfig ;
670850 this .mtlsProvider = provider .mtlsProvider ;
851+ this .s2aConfigProvider = provider .s2aConfigProvider ;
671852 }
672853
673854 /**
@@ -720,6 +901,10 @@ public Builder setEndpoint(String endpoint) {
720901 return this ;
721902 }
722903
904+ Builder setUseS2A (boolean useS2A ) {
905+ this .useS2A = useS2A ;
906+ return this ;
907+ }
723908 /*
724909 * Sets the allowed hard bound token types for this TransportChannelProvider.
725910 *
@@ -739,6 +924,12 @@ Builder setMtlsProvider(MtlsProvider mtlsProvider) {
739924 return this ;
740925 }
741926
927+ @ VisibleForTesting
928+ Builder setS2AConfigProvider (SecureSessionAgent s2aConfigProvider ) {
929+ this .s2aConfigProvider = s2aConfigProvider ;
930+ return this ;
931+ }
932+
742933 /**
743934 * Sets the GrpcInterceptorProvider for this TransportChannelProvider.
744935 *
0 commit comments