From 36f1304334770f0adae56da4f6dba6c0d5362fdc Mon Sep 17 00:00:00 2001 From: "Jonathan Hess (he/him)" <103529393+hessjcg@users.noreply.github.com> Date: Fri, 10 Jan 2025 10:23:33 -0700 Subject: [PATCH] feat: Support Private CA for server certificates. (#408) Support TLS validation on instances configured with a customer-managed private CA. Includes integration test. --- .github/workflows/tests.yml | 4 ++++ package-lock.json | 2 -- src/socket.ts | 33 ++++++++++++++++++--------------- system-test/pg-connect.cjs | 26 ++++++++++++++++++++++++++ system-test/pg-connect.mjs | 26 ++++++++++++++++++++++++++ system-test/pg-connect.ts | 26 ++++++++++++++++++++++++++ 6 files changed, 100 insertions(+), 17 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 44af4e02..aff7bbf4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -161,6 +161,8 @@ jobs: POSTGRES_DB:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_DB POSTGRES_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_CONNECTION_NAME POSTGRES_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_PASS + POSTGRES_CUSTOMER_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_CONNECTION_NAME + POSTGRES_CUSTOMER_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_PASS SQLSERVER_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_CONNECTION_NAME SQLSERVER_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_USER SQLSERVER_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_PASS @@ -188,6 +190,8 @@ jobs: POSTGRES_DB: "${{ steps.secrets.outputs.POSTGRES_DB }}" POSTGRES_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CAS_CONNECTION_NAME }}" POSTGRES_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CAS_PASS }}" + POSTGRES_CUSTOMER_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_CONNECTION_NAME }}" + POSTGRES_CUSTOMER_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_PASS }}" SQLSERVER_CONNECTION_NAME: "${{ steps.secrets.outputs.SQLSERVER_CONNECTION_NAME }}" SQLSERVER_USER: "${{ steps.secrets.outputs.SQLSERVER_USER }}" SQLSERVER_PASS: "${{ steps.secrets.outputs.SQLSERVER_PASS }}" diff --git a/package-lock.json b/package-lock.json index 12ea2ae6..f59a6f2e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "@sequelize/core": "^7.0.0-alpha.29", "@types/node": "^22.0.0", "@types/pg": "^8.10.1", - "@types/semver": "^7.5.0", "@types/tap": "^18.0.0", "@types/tedious": "^4.0.9", "@typescript-eslint/eslint-plugin": "^7.0.0", @@ -29,7 +28,6 @@ "mysql2": "^3.2.0", "nock": "^13.3.0", "pg": "^8.10.0", - "semver": "^7.5.1", "tap": "^21.0.0", "tedious": "^16.1.0", "typeorm": "^0.3.19", diff --git a/src/socket.ts b/src/socket.ts index 6c757ad4..a3c4e1cf 100644 --- a/src/socket.ts +++ b/src/socket.ts @@ -36,23 +36,26 @@ export function validateCertificate( dnsName: string ) { return (hostname: string, cert: tls.PeerCertificate): Error | undefined => { - if (serverCaMode === 'GOOGLE_MANAGED_CAS_CA') { + if (!serverCaMode || serverCaMode === 'GOOGLE_MANAGED_INTERNAL_CA') { + // Legacy CA Mode + if (!cert || !cert.subject) { + return new CloudSQLConnectorError({ + message: 'No certificate to verify', + code: 'ENOSQLADMINVERIFYCERT', + }); + } + const expectedCN = `${instanceInfo.projectId}:${instanceInfo.instanceId}`; + if (cert.subject.CN !== expectedCN) { + return new CloudSQLConnectorError({ + message: `Certificate had CN ${cert.subject.CN}, expected ${expectedCN}`, + code: 'EBADSQLADMINVERIFYCERT', + }); + } + return undefined; + } else { + // Standard TLS Verify Full hostname verification using SAN return tls.checkServerIdentity(dnsName, cert); } - if (!cert || !cert.subject) { - return new CloudSQLConnectorError({ - message: 'No certificate to verify', - code: 'ENOSQLADMINVERIFYCERT', - }); - } - const expectedCN = `${instanceInfo.projectId}:${instanceInfo.instanceId}`; - if (cert.subject.CN !== expectedCN) { - return new CloudSQLConnectorError({ - message: `Certificate had CN ${cert.subject.CN}, expected ${expectedCN}`, - code: 'EBADSQLADMINVERIFYCERT', - }); - } - return undefined; }; } diff --git a/system-test/pg-connect.cjs b/system-test/pg-connect.cjs index 601b07ce..87781314 100644 --- a/system-test/pg-connect.cjs +++ b/system-test/pg-connect.cjs @@ -93,3 +93,29 @@ t.test( connector.close(); } ); + +t.test( + 'open connection to Customer Private CAS-based CA instance and retrieves standard pg tables', + async t => { + const connector = new Connector(); + const clientOpts = await connector.getOptions({ + instanceConnectionName: String( + process.env.POSTGRES_CUSTOMER_CAS_CONNECTION_NAME + ), + }); + const client = new Client({ + ...clientOpts, + user: String(process.env.POSTGRES_USER), + password: String(process.env.POSTGRES_CUSTOMER_CAS_PASS), + database: String(process.env.POSTGRES_DB), + }); + client.connect(); + const { + rows: [result], + } = await client.query('SELECT NOW();'); + const returnedDate = result['now']; + t.ok(returnedDate.getTime(), 'should have valid returned date object'); + await client.end(); + connector.close(); + } +); diff --git a/system-test/pg-connect.mjs b/system-test/pg-connect.mjs index 4b7fec05..7116a340 100644 --- a/system-test/pg-connect.mjs +++ b/system-test/pg-connect.mjs @@ -93,3 +93,29 @@ t.test( connector.close(); } ); + +t.test( + 'open connection to Customer Private CAS-based CA instance and retrieves standard pg tables', + async t => { + const connector = new Connector(); + const clientOpts = await connector.getOptions({ + instanceConnectionName: String( + process.env.POSTGRES_CUSTOMER_CAS_CONNECTION_NAME + ), + }); + const client = new Client({ + ...clientOpts, + user: String(process.env.POSTGRES_USER), + password: String(process.env.POSTGRES_CUSTOMER_CAS_PASS), + database: String(process.env.POSTGRES_DB), + }); + client.connect(); + const { + rows: [result], + } = await client.query('SELECT NOW();'); + const returnedDate = result['now']; + t.ok(returnedDate.getTime(), 'should have valid returned date object'); + await client.end(); + connector.close(); + } +); diff --git a/system-test/pg-connect.ts b/system-test/pg-connect.ts index de10d0b2..ff2385f8 100644 --- a/system-test/pg-connect.ts +++ b/system-test/pg-connect.ts @@ -93,3 +93,29 @@ t.test( connector.close(); } ); + +t.test( + 'open connection to Customer Private CAS-based CA instance and retrieves standard pg tables', + async t => { + const connector = new Connector(); + const clientOpts = await connector.getOptions({ + instanceConnectionName: String( + process.env.POSTGRES_CUSTOMER_CAS_CONNECTION_NAME + ), + }); + const client = new Client({ + ...clientOpts, + user: String(process.env.POSTGRES_USER), + password: String(process.env.POSTGRES_CUSTOMER_CAS_PASS), + database: String(process.env.POSTGRES_DB), + }); + client.connect(); + const { + rows: [result], + } = await client.query('SELECT NOW();'); + const returnedDate = result['now']; + t.ok(returnedDate.getTime(), 'should have valid returned date object'); + await client.end(); + connector.close(); + } +);