Skip to content

Commit a892017

Browse files
authored
feat: Use standard TLS hostname validation for instances with DNS names. (#2125)
When the Cloud SQL Instance reports that it has a DNS Name, the connector will use standard TLS hostname validation when checking the server certificate. Now, the server's TLS certificate must contain a SAN record with the instance's DNS name. The ConnectSettings API added a field dns_names which contains all of the valid DNS names for an instance. See also GoogleCloudPlatform/cloud-sql-go-connector#954
1 parent 31917a4 commit a892017

File tree

8 files changed

+195
-23
lines changed

8 files changed

+195
-23
lines changed

core/src/main/java/com/google/cloud/sql/core/DefaultConnectionInfoRepository.java

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
2020
import com.google.api.services.sqladmin.SQLAdmin;
2121
import com.google.api.services.sqladmin.model.ConnectSettings;
22+
import com.google.api.services.sqladmin.model.DnsNameMapping;
2223
import com.google.api.services.sqladmin.model.GenerateEphemeralCertRequest;
2324
import com.google.api.services.sqladmin.model.GenerateEphemeralCertResponse;
2425
import com.google.api.services.sqladmin.model.IpMapping;
@@ -287,10 +288,31 @@ private InstanceMetadata fetchMetadata(CloudSqlInstanceName instanceName, AuthTy
287288
boolean pscEnabled =
288289
instanceMetadata.getPscEnabled() != null
289290
&& instanceMetadata.getPscEnabled().booleanValue();
290-
if (pscEnabled
291-
&& instanceMetadata.getDnsName() != null
292-
&& !instanceMetadata.getDnsName().isEmpty()) {
293-
ipAddrs.put(IpType.PSC, instanceMetadata.getDnsName());
291+
292+
if (pscEnabled) {
293+
// Search the dns_names field for the PSC DNS Name.
294+
String pscDnsName = null;
295+
if (instanceMetadata.getDnsNames() != null) {
296+
for (DnsNameMapping dnm : instanceMetadata.getDnsNames()) {
297+
if ("PRIVATE_SERVICE_CONNECT".equals(dnm.getConnectionType())
298+
&& "INSTANCE".equals(dnm.getDnsScope())) {
299+
pscDnsName = dnm.getName();
300+
break;
301+
}
302+
}
303+
}
304+
305+
// If the psc dns name was not found, use the legacy dns_name field
306+
if (pscDnsName == null
307+
&& instanceMetadata.getDnsName() != null
308+
&& !instanceMetadata.getDnsName().isEmpty()) {
309+
pscDnsName = instanceMetadata.getDnsName();
310+
}
311+
312+
// If the psc dns name was found, add it to the ipaddrs map.
313+
if (pscDnsName != null) {
314+
ipAddrs.put(IpType.PSC, pscDnsName);
315+
}
294316
}
295317

296318
// Verify the instance has at least one IP type assigned that can be used to connect.
@@ -301,6 +323,18 @@ private InstanceMetadata fetchMetadata(CloudSqlInstanceName instanceName, AuthTy
301323
+ "IP address.",
302324
instanceName.getConnectionName()));
303325
}
326+
327+
// Find a DNS name to use to validate the certificate from the dns_names field. Any
328+
// name in the list may be used to validate the server TLS certificate.
329+
// Fall back to legacy dns_name field if necessary.
330+
String serverName = null;
331+
if (instanceMetadata.getDnsNames() != null && !instanceMetadata.getDnsNames().isEmpty()) {
332+
serverName = instanceMetadata.getDnsNames().get(0).getName();
333+
}
334+
if (serverName == null) {
335+
serverName = instanceMetadata.getDnsName();
336+
}
337+
304338
// Update the Server CA certificate used to create the SSL connection with the instance.
305339
try {
306340
List<Certificate> instanceCaCertificates =
@@ -313,7 +347,7 @@ private InstanceMetadata fetchMetadata(CloudSqlInstanceName instanceName, AuthTy
313347
ipAddrs,
314348
instanceCaCertificates,
315349
isCasManagedCertificate(instanceMetadata.getServerCaMode()),
316-
instanceMetadata.getDnsName(),
350+
serverName,
317351
pscEnabled);
318352
} catch (CertificateException ex) {
319353
throw new RuntimeException(

core/src/main/java/com/google/cloud/sql/core/InstanceCheckingTrustManger.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,12 @@ private void checkCertificateChain(X509Certificate[] chain) throws CertificateEx
9696
throw new CertificateException("Subject is missing");
9797
}
9898

99-
if (instanceMetadata.isCasManagedCertificate() || instanceMetadata.isPscEnabled()) {
100-
checkSan(chain);
101-
} else {
99+
// If the instance metadata does not contain a domain name, use legacy CN validation
100+
if (Strings.isNullOrEmpty(instanceMetadata.getDnsName())) {
102101
checkCn(chain);
102+
} else {
103+
// If there is a DNS name, check the Subject Alternative Names.
104+
checkSan(chain);
103105
}
104106
}
105107

core/src/test/java/com/google/cloud/sql/core/CloudSqlCoreTestingBase.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
3131
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
3232
import com.google.api.services.sqladmin.model.ConnectSettings;
33+
import com.google.api.services.sqladmin.model.DnsNameMapping;
3334
import com.google.api.services.sqladmin.model.GenerateEphemeralCertResponse;
3435
import com.google.api.services.sqladmin.model.IpMapping;
3536
import com.google.api.services.sqladmin.model.SslCert;
@@ -42,6 +43,7 @@
4243
import java.time.Duration;
4344
import java.util.Base64;
4445
import java.util.Collections;
46+
import java.util.List;
4547
import java.util.concurrent.ConcurrentHashMap;
4648
import java.util.regex.Matcher;
4749
import java.util.regex.Pattern;
@@ -169,6 +171,18 @@ private String parseCertCnFromUrl(String url) {
169171

170172
MockHttpTransport fakeSuccessHttpTransport(
171173
String serverCert, Duration certDuration, String baseUrl, boolean cas, boolean psc) {
174+
return this.fakeSuccessHttpTransport(
175+
serverCert, certDuration, baseUrl, cas, psc, cas ? "db.example.com" : null, null);
176+
}
177+
178+
MockHttpTransport fakeSuccessHttpTransport(
179+
String serverCert,
180+
Duration certDuration,
181+
String baseUrl,
182+
boolean cas,
183+
boolean psc,
184+
String legacyDnsName,
185+
List<DnsNameMapping> dnsNames) {
172186
final JsonFactory jsonFactory = new GsonFactory();
173187
return new MockHttpTransport() {
174188
@Override
@@ -205,7 +219,8 @@ public LowLevelHttpResponse execute() throws IOException {
205219
.setDatabaseVersion("POSTGRES14")
206220
.setRegion("myRegion")
207221
.setPscEnabled(psc ? Boolean.TRUE : null)
208-
.setDnsName(cas || psc ? "db.example.com" : null)
222+
.setDnsName(legacyDnsName)
223+
.setDnsNames(dnsNames)
209224
.setServerCaMode(
210225
cas ? "GOOGLE_MANAGED_CAS_CA" : "GOOGLE_MANAGED_INTERNAL_CA");
211226
settings.setFactory(jsonFactory);

core/src/test/java/com/google/cloud/sql/core/ConnectorTest.java

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import com.google.api.client.http.BasicAuthentication;
2424
import com.google.api.client.http.HttpRequestInitializer;
25+
import com.google.api.services.sqladmin.model.DnsNameMapping;
2526
import com.google.cloud.sql.AuthType;
2627
import com.google.cloud.sql.ConnectorConfig;
2728
import com.google.cloud.sql.CredentialFactory;
@@ -193,6 +194,31 @@ public void create_successfulPrivateConnection_UsesInstanceName_EmptyDomainNameI
193194
assertThat(readLine(socket)).isEqualTo(SERVER_MESSAGE);
194195
}
195196

197+
@Test
198+
public void create_successfulPublicConnectionWithDomainNameLegacyDns()
199+
throws IOException, InterruptedException {
200+
FakeSslServer sslServer = new FakeSslServer();
201+
ConnectionConfig config =
202+
new ConnectionConfig.Builder()
203+
.withDomainName("db.example.com")
204+
.withIpTypes("PRIMARY")
205+
.build();
206+
207+
int port = sslServer.start(PUBLIC_IP);
208+
209+
Connector connector =
210+
newConnectorLegacyDnsField(
211+
config.getConnectorConfig(),
212+
port,
213+
"db.example.com",
214+
"myProject:myRegion:myInstance",
215+
false);
216+
217+
Socket socket = connector.connect(config, TEST_MAX_REFRESH_MS);
218+
219+
assertThat(readLine(socket)).isEqualTo(SERVER_MESSAGE);
220+
}
221+
196222
@Test
197223
public void create_successfulPublicConnectionWithDomainName()
198224
throws IOException, InterruptedException {
@@ -516,7 +542,14 @@ public void create_successfulDomainScopedConnection() throws IOException, Interr
516542
new CredentialFactoryProvider(new StubCredentialFactory("foo", null));
517543
ConnectionInfoRepositoryFactory factory =
518544
new StubConnectionInfoRepositoryFactory(
519-
fakeSuccessHttpTransport(TestKeys.getDomainServerCertPem(), Duration.ofSeconds(60)));
545+
fakeSuccessHttpTransport(
546+
TestKeys.getDomainServerCertPem(),
547+
Duration.ofSeconds(60),
548+
null,
549+
false,
550+
false,
551+
null,
552+
null));
520553

521554
int port = sslServer.start(PUBLIC_IP);
522555
ConnectionConfig config =
@@ -819,12 +852,48 @@ public HttpRequestInitializer create() {
819852
assertThrows(RuntimeException.class, () -> c.connect(config, TEST_MAX_REFRESH_MS));
820853
}
821854

855+
private Connector newConnectorLegacyDnsField(
856+
ConnectorConfig config, int port, String domainName, String instanceName, boolean cas) {
857+
ConnectionInfoRepositoryFactory factory =
858+
new StubConnectionInfoRepositoryFactory(
859+
fakeSuccessHttpTransport(
860+
TestKeys.getServerCertPem(),
861+
Duration.ofSeconds(0),
862+
null,
863+
cas,
864+
false,
865+
domainName,
866+
null));
867+
Connector connector =
868+
new Connector(
869+
config,
870+
factory,
871+
stubCredentialFactoryProvider.getInstanceCredentialFactory(config),
872+
defaultExecutor,
873+
clientKeyPair,
874+
10,
875+
TEST_MAX_REFRESH_MS,
876+
port,
877+
new DnsInstanceConnectionNameResolver(new MockDnsResolver(domainName, instanceName)));
878+
return connector;
879+
}
880+
822881
private Connector newConnector(
823882
ConnectorConfig config, int port, String domainName, String instanceName, boolean cas) {
824883
ConnectionInfoRepositoryFactory factory =
825884
new StubConnectionInfoRepositoryFactory(
826885
fakeSuccessHttpTransport(
827-
TestKeys.getServerCertPem(), Duration.ofSeconds(0), null, cas, false));
886+
TestKeys.getServerCertPem(),
887+
Duration.ofSeconds(0),
888+
null,
889+
cas,
890+
false,
891+
null,
892+
Collections.singletonList(
893+
new DnsNameMapping()
894+
.setName(domainName)
895+
.setConnectionType("PRIVATE_SERVICE_CONNECT")
896+
.setDnsScope("INSTANCE"))));
828897
Connector connector =
829898
new Connector(
830899
config,

core/src/test/java/com/google/cloud/sql/core/DefaultConnectionInfoRepositoryTest.java

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public void testFetchInstanceData_returnsIpAddresses()
5151
throws ExecutionException, InterruptedException, GeneralSecurityException,
5252
OperatorCreationException {
5353
MockAdminApi mockAdminApi =
54-
buildMockAdminApi(INSTANCE_CONNECTION_NAME, DATABASE_VERSION, DEFAULT_BASE_URL);
54+
buildMockAdminApi(INSTANCE_CONNECTION_NAME, DATABASE_VERSION, DEFAULT_BASE_URL, false);
5555
ConnectorConfig config = new ConnectorConfig.Builder().build();
5656
ConnectionInfoRepository repo =
5757
new StubConnectionInfoRepositoryFactory(mockAdminApi.getHttpTransport())
@@ -85,7 +85,45 @@ public void testFetchInstanceData_returnsPscForNonIpDatabase()
8585
null,
8686
DATABASE_VERSION,
8787
SAMPLE_PCS_DNS_NAME,
88-
DEFAULT_BASE_URL);
88+
DEFAULT_BASE_URL,
89+
false);
90+
mockAdminApi.addGenerateEphemeralCertResponse(
91+
INSTANCE_CONNECTION_NAME, Duration.ofHours(1), DEFAULT_BASE_URL);
92+
ConnectorConfig config = new ConnectorConfig.Builder().build();
93+
94+
ConnectionInfoRepository repo =
95+
new StubConnectionInfoRepositoryFactory(mockAdminApi.getHttpTransport())
96+
.create(new StubCredentialFactory().create(), config);
97+
98+
ConnectionInfo connectionInfo =
99+
repo.getConnectionInfo(
100+
new CloudSqlInstanceName(INSTANCE_CONNECTION_NAME),
101+
() -> Optional.empty(),
102+
AuthType.PASSWORD,
103+
newTestExecutor(),
104+
Futures.immediateFuture(mockAdminApi.getClientKeyPair()))
105+
.get();
106+
assertThat(connectionInfo.getSslContext()).isInstanceOf(SSLContext.class);
107+
108+
Map<IpType, String> ipAddrs = connectionInfo.getIpAddrs();
109+
assertThat(ipAddrs.get(IpType.PSC)).isEqualTo(SAMPLE_PCS_DNS_NAME);
110+
assertThat(ipAddrs.size()).isEqualTo(1);
111+
}
112+
113+
@Test
114+
public void testFetchInstanceData_legacyPscDns_returnsPscForNonIpDatabase()
115+
throws ExecutionException, InterruptedException, GeneralSecurityException,
116+
OperatorCreationException {
117+
118+
MockAdminApi mockAdminApi = new MockAdminApi();
119+
mockAdminApi.addConnectSettingsResponse(
120+
INSTANCE_CONNECTION_NAME,
121+
null,
122+
null,
123+
DATABASE_VERSION,
124+
SAMPLE_PCS_DNS_NAME,
125+
DEFAULT_BASE_URL,
126+
true);
89127
mockAdminApi.addGenerateEphemeralCertResponse(
90128
INSTANCE_CONNECTION_NAME, Duration.ofHours(1), DEFAULT_BASE_URL);
91129
ConnectorConfig config = new ConnectorConfig.Builder().build();
@@ -122,7 +160,8 @@ private ListeningScheduledExecutorService newTestExecutor() {
122160
public void testFetchInstanceData_throwsException_whenIamAuthnIsNotSupported()
123161
throws GeneralSecurityException, OperatorCreationException {
124162
MockAdminApi mockAdminApi =
125-
buildMockAdminApi(INSTANCE_CONNECTION_NAME, "SQLSERVER_2019_STANDARD", DEFAULT_BASE_URL);
163+
buildMockAdminApi(
164+
INSTANCE_CONNECTION_NAME, "SQLSERVER_2019_STANDARD", DEFAULT_BASE_URL, false);
126165
ConnectorConfig config = new ConnectorConfig.Builder().build();
127166
ConnectionInfoRepository repo =
128167
new StubConnectionInfoRepositoryFactory(mockAdminApi.getHttpTransport())
@@ -149,7 +188,7 @@ public void testFetchInstanceData_throwsException_whenIamAuthnIsNotSupported()
149188
public void testFetchInstanceData_throwsException_whenRequestsTimeout()
150189
throws GeneralSecurityException, OperatorCreationException {
151190
MockAdminApi mockAdminApi =
152-
buildMockAdminApi(INSTANCE_CONNECTION_NAME, DATABASE_VERSION, DEFAULT_BASE_URL);
191+
buildMockAdminApi(INSTANCE_CONNECTION_NAME, DATABASE_VERSION, DEFAULT_BASE_URL, false);
153192
ConnectorConfig config = new ConnectorConfig.Builder().build();
154193
ConnectionInfoRepository repo =
155194
new StubConnectionInfoRepositoryFactory(new BadConnectionFactory())
@@ -182,7 +221,7 @@ public void testSetAdminUrl_FetchInstanceData_returnsIpAddresses()
182221
String adminServicePath = "sqladmin/";
183222
String baseUrl = adminRootUrl + adminServicePath;
184223
MockAdminApi mockAdminApi =
185-
buildMockAdminApi(INSTANCE_CONNECTION_NAME, DATABASE_VERSION, baseUrl);
224+
buildMockAdminApi(INSTANCE_CONNECTION_NAME, DATABASE_VERSION, baseUrl, false);
186225
ConnectorConfig config =
187226
new ConnectorConfig.Builder()
188227
.withAdminRootUrl(adminRootUrl)
@@ -210,7 +249,7 @@ public void testSetAdminUrl_FetchInstanceData_returnsIpAddresses()
210249

211250
@SuppressWarnings("SameParameterValue")
212251
private MockAdminApi buildMockAdminApi(
213-
String instanceConnectionName, String databaseVersion, String baseUrl)
252+
String instanceConnectionName, String databaseVersion, String baseUrl, boolean legacyDnsName)
214253
throws GeneralSecurityException, OperatorCreationException {
215254
MockAdminApi mockAdminApi = new MockAdminApi();
216255
mockAdminApi.addConnectSettingsResponse(
@@ -219,7 +258,8 @@ private MockAdminApi buildMockAdminApi(
219258
SAMPLE_PRIVATE_IP,
220259
databaseVersion,
221260
SAMPLE_PCS_DNS_NAME,
222-
baseUrl);
261+
baseUrl,
262+
legacyDnsName);
223263
mockAdminApi.addGenerateEphemeralCertResponse(
224264
instanceConnectionName, Duration.ofHours(1), baseUrl);
225265
return mockAdminApi;

core/src/test/java/com/google/cloud/sql/core/MockAdminApi.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
2727
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
2828
import com.google.api.services.sqladmin.model.ConnectSettings;
29+
import com.google.api.services.sqladmin.model.DnsNameMapping;
2930
import com.google.api.services.sqladmin.model.GenerateEphemeralCertResponse;
3031
import com.google.api.services.sqladmin.model.IpMapping;
3132
import com.google.api.services.sqladmin.model.SslCert;
@@ -38,6 +39,7 @@
3839
import java.security.spec.InvalidKeySpecException;
3940
import java.time.Duration;
4041
import java.util.ArrayList;
42+
import java.util.Collections;
4143
import java.util.Date;
4244
import java.util.List;
4345
import java.util.concurrent.atomic.AtomicInteger;
@@ -83,7 +85,8 @@ public void addConnectSettingsResponse(
8385
String privateIp,
8486
String databaseVersion,
8587
String pscHostname,
86-
String baseUrl) {
88+
String baseUrl,
89+
boolean legacyPscDnsName) {
8790
CloudSqlInstanceName cloudSqlInstanceName = new CloudSqlInstanceName(instanceConnectionName);
8891

8992
ArrayList<IpMapping> ipMappings = new ArrayList<>();
@@ -103,10 +106,19 @@ public void addConnectSettingsResponse(
103106
.setServerCaCert(new SslCert().setCert(TestKeys.getServerCertPem()))
104107
.setDatabaseVersion(databaseVersion)
105108
.setPscEnabled(pscHostname != null)
106-
.setDnsName(pscHostname)
107109
.setPscEnabled(pscHostname != null)
108110
.setRegion(cloudSqlInstanceName.getRegionId());
109111
settings.setFactory(GsonFactory.getDefaultInstance());
112+
if (legacyPscDnsName) {
113+
settings.setDnsName(pscHostname);
114+
} else {
115+
settings.setDnsNames(
116+
Collections.singletonList(
117+
new DnsNameMapping()
118+
.setDnsScope("INSTANCE")
119+
.setConnectionType("PRIVATE_SERVICE_CONNECT")
120+
.setName(pscHostname)));
121+
}
110122

111123
connectSettingsRequests.add(
112124
new ConnectSettingsRequest(cloudSqlInstanceName, settings, baseUrl));

0 commit comments

Comments
 (0)