46
46
import com .google .auth .ApiKeyCredentials ;
47
47
import com .google .auth .Credentials ;
48
48
import com .google .auth .oauth2 .ComputeEngineCredentials ;
49
+ import com .google .auth .oauth2 .SecureSessionAgent ;
50
+ import com .google .auth .oauth2 .SecureSessionAgentConfig ;
49
51
import com .google .common .annotations .VisibleForTesting ;
50
52
import com .google .common .base .Preconditions ;
53
+ import com .google .common .base .Strings ;
51
54
import com .google .common .collect .ImmutableList ;
52
55
import com .google .common .collect .ImmutableMap ;
53
56
import com .google .common .io .Files ;
54
57
import io .grpc .CallCredentials ;
55
58
import io .grpc .ChannelCredentials ;
56
59
import io .grpc .Grpc ;
60
+ import io .grpc .InsecureChannelCredentials ;
57
61
import io .grpc .ManagedChannel ;
58
62
import io .grpc .ManagedChannelBuilder ;
59
63
import io .grpc .TlsChannelCredentials ;
60
64
import io .grpc .alts .GoogleDefaultChannelCredentials ;
61
65
import io .grpc .auth .MoreCallCredentials ;
62
66
import java .io .File ;
63
67
import java .io .IOException ;
68
+ import java .lang .reflect .Method ;
64
69
import java .nio .charset .StandardCharsets ;
65
70
import java .security .GeneralSecurityException ;
66
71
import java .security .KeyStore ;
@@ -99,6 +104,19 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP
99
104
@ VisibleForTesting
100
105
static final String DIRECT_PATH_ENV_ENABLE_XDS = "GOOGLE_CLOUD_ENABLE_DIRECT_PATH_XDS" ;
101
106
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
+
102
120
static final long DIRECT_PATH_KEEP_ALIVE_TIME_SECONDS = 3600 ;
103
121
static final long DIRECT_PATH_KEEP_ALIVE_TIMEOUT_SECONDS = 20 ;
104
122
static final String GCE_PRODUCTION_NAME_PRIOR_2016 = "Google" ;
@@ -107,6 +125,7 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP
107
125
private final int processorCount ;
108
126
private final Executor executor ;
109
127
private final HeaderProvider headerProvider ;
128
+ private final boolean useS2A ;
110
129
private final String endpoint ;
111
130
// TODO: remove. envProvider currently provides DirectPath environment variable, and is only used
112
131
// during initial rollout for DirectPath. This provider will be removed once the DirectPath
@@ -126,6 +145,7 @@ public final class InstantiatingGrpcChannelProvider implements TransportChannelP
126
145
@ Nullable private final Boolean allowNonDefaultServiceAccount ;
127
146
@ VisibleForTesting final ImmutableMap <String , ?> directPathServiceConfig ;
128
147
@ Nullable private final MtlsProvider mtlsProvider ;
148
+ @ Nullable private final SecureSessionAgent s2aConfigProvider ;
129
149
@ Nullable private final List <HardBoundTokenTypes > allowedHardBoundTokenTypes ;
130
150
@ VisibleForTesting final Map <String , String > headersWithDuplicatesRemoved = new HashMap <>();
131
151
@@ -153,9 +173,11 @@ private InstantiatingGrpcChannelProvider(Builder builder) {
153
173
this .processorCount = builder .processorCount ;
154
174
this .executor = builder .executor ;
155
175
this .headerProvider = builder .headerProvider ;
176
+ this .useS2A = builder .useS2A ;
156
177
this .endpoint = builder .endpoint ;
157
178
this .allowedHardBoundTokenTypes = builder .allowedHardBoundTokenTypes ;
158
179
this .mtlsProvider = builder .mtlsProvider ;
180
+ this .s2aConfigProvider = builder .s2aConfigProvider ;
159
181
this .envProvider = builder .envProvider ;
160
182
this .interceptorProvider = builder .interceptorProvider ;
161
183
this .maxInboundMessageSize = builder .maxInboundMessageSize ;
@@ -244,6 +266,17 @@ public TransportChannelProvider withEndpoint(String endpoint) {
244
266
return toBuilder ().setEndpoint (endpoint ).build ();
245
267
}
246
268
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
+
247
280
/** @deprecated Please modify pool settings via {@link #toBuilder()} */
248
281
@ Deprecated
249
282
@ Override
@@ -429,6 +462,136 @@ ChannelCredentials createMtlsChannelCredentials() throws IOException, GeneralSec
429
462
return null ;
430
463
}
431
464
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
+
432
595
private ManagedChannel createSingleChannel () throws IOException {
433
596
GrpcHeaderInterceptor headerInterceptor =
434
597
new GrpcHeaderInterceptor (headersWithDuplicatesRemoved );
@@ -468,14 +631,28 @@ private ManagedChannel createSingleChannel() throws IOException {
468
631
} else {
469
632
ChannelCredentials channelCredentials ;
470
633
try {
634
+ // Try and create credentials via DCA. See https://google.aip.dev/auth/4114.
471
635
channelCredentials = createMtlsChannelCredentials ();
472
636
} catch (GeneralSecurityException e ) {
473
637
throw new IOException (e );
474
638
}
475
639
if (channelCredentials != null ) {
640
+ // Create the channel using channel credentials created via DCA.
476
641
builder = Grpc .newChannelBuilder (endpoint , channelCredentials );
477
642
} 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
+ }
479
656
}
480
657
}
481
658
// google-c2p resolver requires service config lookup
@@ -623,7 +800,9 @@ public static final class Builder {
623
800
private Executor executor ;
624
801
private HeaderProvider headerProvider ;
625
802
private String endpoint ;
803
+ private boolean useS2A ;
626
804
private EnvironmentProvider envProvider ;
805
+ private SecureSessionAgent s2aConfigProvider = SecureSessionAgent .create ();
627
806
private MtlsProvider mtlsProvider = new MtlsProvider ();
628
807
@ Nullable private GrpcInterceptorProvider interceptorProvider ;
629
808
@ Nullable private Integer maxInboundMessageSize ;
@@ -652,6 +831,7 @@ private Builder(InstantiatingGrpcChannelProvider provider) {
652
831
this .executor = provider .executor ;
653
832
this .headerProvider = provider .headerProvider ;
654
833
this .endpoint = provider .endpoint ;
834
+ this .useS2A = provider .useS2A ;
655
835
this .envProvider = provider .envProvider ;
656
836
this .interceptorProvider = provider .interceptorProvider ;
657
837
this .maxInboundMessageSize = provider .maxInboundMessageSize ;
@@ -668,6 +848,7 @@ private Builder(InstantiatingGrpcChannelProvider provider) {
668
848
this .allowNonDefaultServiceAccount = provider .allowNonDefaultServiceAccount ;
669
849
this .directPathServiceConfig = provider .directPathServiceConfig ;
670
850
this .mtlsProvider = provider .mtlsProvider ;
851
+ this .s2aConfigProvider = provider .s2aConfigProvider ;
671
852
}
672
853
673
854
/**
@@ -720,6 +901,10 @@ public Builder setEndpoint(String endpoint) {
720
901
return this ;
721
902
}
722
903
904
+ Builder setUseS2A (boolean useS2A ) {
905
+ this .useS2A = useS2A ;
906
+ return this ;
907
+ }
723
908
/*
724
909
* Sets the allowed hard bound token types for this TransportChannelProvider.
725
910
*
@@ -739,6 +924,12 @@ Builder setMtlsProvider(MtlsProvider mtlsProvider) {
739
924
return this ;
740
925
}
741
926
927
+ @ VisibleForTesting
928
+ Builder setS2AConfigProvider (SecureSessionAgent s2aConfigProvider ) {
929
+ this .s2aConfigProvider = s2aConfigProvider ;
930
+ return this ;
931
+ }
932
+
742
933
/**
743
934
* Sets the GrpcInterceptorProvider for this TransportChannelProvider.
744
935
*
0 commit comments