Skip to content

Commit 51dd5e0

Browse files
committed
feat: Update TLS validation to use both SAN and CN fields.
1 parent fd0b3da commit 51dd5e0

2 files changed

Lines changed: 154 additions & 20 deletions

File tree

src/socket.ts

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,35 +30,61 @@ interface SocketOptions {
3030
serverName: string;
3131
}
3232

33+
/**
34+
* Manages hostname validation of the server certificate.
35+
* @param instanceInfo
36+
* @param instanceDnsName
37+
* @param serverName
38+
*/
3339
export function validateCertificate(
3440
instanceInfo: InstanceConnectionInfo,
3541
instanceDnsName: string,
3642
serverName: string
3743
) {
3844
return (hostname: string, cert: tls.PeerCertificate): Error | undefined => {
45+
if (!cert) {
46+
return new CloudSQLConnectorError({
47+
message: 'Certificate missing',
48+
code: 'ENOSQLADMINVERIFYCERT',
49+
});
50+
}
51+
3952
if (!instanceDnsName) {
40-
// Legacy CA Mode
41-
if (!cert || !cert.subject) {
42-
return new CloudSQLConnectorError({
43-
message: 'No certificate to verify',
44-
code: 'ENOSQLADMINVERIFYCERT',
45-
});
46-
}
47-
const expectedCN = `${instanceInfo.projectId}:${instanceInfo.instanceId}`;
48-
if (cert.subject.CN !== expectedCN) {
49-
return new CloudSQLConnectorError({
50-
message: `Certificate had CN ${cert.subject.CN}, expected ${expectedCN}`,
51-
code: 'EBADSQLADMINVERIFYCERT',
52-
});
53-
}
54-
return undefined;
53+
return checkCn(instanceInfo, cert);
5554
} else {
56-
// Standard TLS Verify Full hostname verification using SAN
57-
return tls.checkServerIdentity(serverName, cert);
55+
const err = tls.checkServerIdentity(serverName, cert);
56+
if (err) {
57+
const cnErr = checkCn(instanceInfo, cert);
58+
if (cnErr) {
59+
return err;
60+
}
61+
}
5862
}
63+
64+
return undefined;
5965
};
6066
}
6167

68+
function checkCn(
69+
instanceInfo: InstanceConnectionInfo,
70+
cert: tls.PeerCertificate
71+
) {
72+
const expectedCN = `${instanceInfo.projectId}:${instanceInfo.instanceId}`;
73+
if (!cert.subject || !cert.subject.CN) {
74+
return new CloudSQLConnectorError({
75+
message: `Certificate missing CN, expected ${expectedCN}`,
76+
code: 'ENOSQLADMINVERIFYCERT',
77+
});
78+
}
79+
if (cert.subject.CN && cert.subject.CN !== expectedCN) {
80+
return new CloudSQLConnectorError({
81+
message: `Certificate had CN ${cert.subject.CN}, expected ${expectedCN}`,
82+
code: 'EBADSQLADMINVERIFYCERT',
83+
});
84+
}
85+
return undefined;
86+
}
87+
6288
export function getSocket({
6389
ephemeralCert,
6490
host,

test/socket.ts

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import t from 'tap';
1717
import {getSocket, validateCertificate} from '../src/socket';
1818
import {CA_CERT, CLIENT_CERT, CLIENT_KEY} from './fixtures/certs';
1919
import {setupTLSServer} from './fixtures/setup-tls-server';
20+
import {parseInstanceConnectionName} from '../src/parse-instance-connection-name';
2021

2122
t.test('getSocket', async t => {
2223
// the mock tls server for this test will use a custom port in order
@@ -72,7 +73,9 @@ t.test('validateCertificate no cert', async t => {
7273
null,
7374
'abcde.12345.us-central1.sql.goog'
7475
)('hostname', {} as tls.PeerCertificate),
75-
{code: 'ENOSQLADMINVERIFYCERT'},
76+
{
77+
code: 'ENOSQLADMINVERIFYCERT',
78+
},
7679
'should return a missing cert to verify error'
7780
);
7881
});
@@ -94,8 +97,6 @@ t.test('validateCertificate mismatch', async t => {
9497
'abcde.12345.us-central1.sql.goog'
9598
)('hostname', cert),
9699
{
97-
message:
98-
'Certificate had CN other-project:other-instance, expected my-project:my-instance',
99100
code: 'EBADSQLADMINVERIFYCERT',
100101
},
101102
'should return a missing cert to verify error'
@@ -143,3 +144,110 @@ t.test('validateCertificate valid CAS CA', async t => {
143144
'DNS name matches SAN in cert'
144145
);
145146
});
147+
148+
t.test('validateCertificate cases', async t => {
149+
const tcs = [
150+
{
151+
desc: 'cn match',
152+
icn: 'myProject:myRegion:myInstance',
153+
cn: 'myProject:myInstance',
154+
valid: true,
155+
},
156+
{
157+
desc: 'cn no match',
158+
icn: 'myProject:myRegion:badInstance',
159+
cn: 'myProject:myInstance',
160+
valid: false,
161+
},
162+
{
163+
desc: 'cn empty',
164+
icn: 'myProject:myRegion:myInstance',
165+
san: 'db.example.com',
166+
valid: false,
167+
},
168+
{
169+
desc: 'san match',
170+
serverName: 'db.example.com',
171+
icn: 'myProject:myRegion:myInstance',
172+
san: 'db.example.com',
173+
valid: true,
174+
},
175+
{
176+
desc: 'san no match',
177+
serverName: 'bad.example.com',
178+
icn: 'myProject:myRegion:myInstance',
179+
san: 'db.example.com',
180+
valid: false,
181+
},
182+
{
183+
desc: 'san empty match',
184+
serverName: 'empty.example.com',
185+
icn: 'myProject:myRegion:myInstance',
186+
cn: '',
187+
valid: false,
188+
},
189+
{
190+
desc: 'san match with cn present',
191+
serverName: 'db.example.com',
192+
icn: 'myProject:myRegion:myInstance',
193+
san: 'db.example.com',
194+
cn: 'myProject:myInstance',
195+
valid: true,
196+
},
197+
{
198+
desc: 'san no match fallback to cn',
199+
serverName: 'db.example.com',
200+
icn: 'myProject:myRegion:myInstance',
201+
san: 'other.example.com',
202+
cn: 'myProject:myInstance',
203+
valid: true,
204+
},
205+
{
206+
desc: 'san empty match fallback to cn',
207+
serverName: 'db.example.com',
208+
icn: 'myProject:myRegion:myInstance',
209+
cn: 'myProject:myInstance',
210+
valid: true,
211+
},
212+
{
213+
desc: 'san no match fallback to cn and fail',
214+
serverName: 'db.example.com',
215+
icn: 'myProject:myRegion:badInstance',
216+
san: 'other.example.com',
217+
cn: 'myProject:myInstance',
218+
valid: false,
219+
},
220+
];
221+
222+
await Promise.all(
223+
tcs.map(tc =>
224+
t.test(tc.desc + ' cas', async t => {
225+
testThings(t, tc);
226+
})
227+
)
228+
);
229+
});
230+
231+
function testThings(t, tc) {
232+
const cert = {
233+
subject: {CN: tc.cn},
234+
subjectaltname: tc.san ? 'DNS:' + tc.san : undefined,
235+
} as tls.PeerCertificate;
236+
237+
const instanceInfo = parseInstanceConnectionName(tc.icn);
238+
instanceInfo.domainName = tc.san;
239+
240+
const err = validateCertificate(
241+
instanceInfo,
242+
tc.san,
243+
tc.serverName
244+
)(tc.serverName, cert);
245+
246+
if (tc.valid) {
247+
t.match(err, undefined, 'want no error');
248+
} else {
249+
if (!err) {
250+
t.fail('want error, got no error');
251+
}
252+
}
253+
}

0 commit comments

Comments
 (0)