Files
mariadb-connector-nodejs/test/integration/test-ssl.js

693 lines
23 KiB
JavaScript

// SPDX-License-Identifier: LGPL-2.1-or-later
// Copyright (c) 2015-2025 MariaDB Corporation Ab
'use strict';
const base = require('../base.js');
const { assert } = require('chai');
const fs = require('fs');
const Conf = require('../conf');
const tls = require('tls');
const { isMaxscale, isMaxscaleMinVersion, getHostSuffix } = require('../base');
const crypto = require('crypto');
const errors = require('../../lib/misc/errors');
describe('ssl', function () {
let ca = Conf.baseConfig.ssl && Conf.baseConfig.ssl.ca ? Conf.baseConfig.ssl.ca : null;
let clientKey = null;
let clientCert = null;
let clientKeystore = null;
let sslEnable = false;
let sslPort = Conf.baseConfig.port;
before(async () => {
if (process.env.TEST_MAXSCALE_TLS_PORT) sslPort = parseInt(process.env.TEST_MAXSCALE_TLS_PORT);
if (
tls.DEFAULT_MIN_VERSION === 'TLSv1.2' &&
((process.platform === 'win32' && shareConn.info.isMariaDB() && !shareConn.info.hasMinVersion(10, 4, 0)) ||
(!shareConn.info.isMariaDB() && !shareConn.info.hasMinVersion(8, 0, 0)))
) {
//TLSv1.2 is supported on windows only since MariaDB 10.4
//TLSv1.2 is supported in MySQL only since 8.0 (unix/windows)
//so if testing with Node.js 12, force possible TLS1.1
if (!shareConn.info.isMariaDB() && !shareConn.info.hasMinVersion(5, 7, 0)) {
//MySQL 5.5 and MySQL 5.6 needs TLSv1
tls.DEFAULT_MIN_VERSION = 'TLSv1';
} else {
tls.DEFAULT_MIN_VERSION = 'TLSv1.1';
}
}
let serverCaFile = Conf.baseConfig.ssl && Conf.baseConfig.ssl.ca ? null : process.env.TEST_DB_SERVER_CERT;
let clientKeyFileName = process.env.TEST_DB_CLIENT_KEY;
let clientCertFileName = process.env.TEST_DB_CLIENT_CERT;
let clientKeystoreFileName = process.env.TEST_DB_CLIENT_PKCS;
if (!serverCaFile && (Conf.baseConfig.host === 'localhost' || Conf.baseConfig.host === 'mariadb.example.com')) {
try {
if (fs.existsSync('../../ssl')) {
serverCaFile = '../../ssl/server.crt';
clientKeyFileName = '../../ssl/client.key';
clientCertFileName = '../../ssl/client.crt';
clientKeystoreFileName = '../../ssl/fullclient-keystore.p12';
}
} catch (err) {
console.error(err);
}
}
if (serverCaFile) ca = [fs.readFileSync(serverCaFile, 'utf8')];
if (clientKeyFileName) clientKey = [fs.readFileSync(clientKeyFileName, 'utf8')];
if (clientCertFileName) clientCert = [fs.readFileSync(clientCertFileName, 'utf8')];
if (clientKeystoreFileName) clientKeystore = [fs.readFileSync(clientKeystoreFileName)];
await shareConn.query("DROP USER IF EXISTS 'sslTestUser'" + getHostSuffix());
await shareConn.query("DROP USER IF EXISTS 'X509testUser'" + getHostSuffix());
await shareConn.query("DROP USER IF EXISTS 'nosslTestUser'" + getHostSuffix());
await shareConn.query("CREATE USER 'sslTestUser'" + getHostSuffix() + " IDENTIFIED BY 'ytoKS@led5' REQUIRE SSL");
await shareConn.query("CREATE USER 'nosslTestUser'" + getHostSuffix() + " IDENTIFIED BY 'ytoKS@led5'");
await shareConn.query("GRANT SELECT ON *.* TO 'sslTestUser'" + getHostSuffix());
await shareConn.query("GRANT SELECT ON *.* TO 'nosslTestUser'" + getHostSuffix());
await shareConn.query(
"CREATE USER 'X509testUser'" + getHostSuffix() + " IDENTIFIED BY 'éà@d684SQpl¨^' REQUIRE X509"
);
await shareConn.query("GRANT SELECT ON *.* TO 'X509testUser'" + getHostSuffix());
await shareConn.query('FLUSH PRIVILEGES');
const rows = await shareConn.query("SHOW VARIABLES LIKE 'have_ssl'");
if (rows.length === 0 || rows[0].Value === 'YES') {
sslEnable = true;
}
});
it('error when server ssl is disable', async function () {
if (sslEnable) {
this.skip();
return;
}
try {
await base.createConnection({
ssl: { rejectUnauthorized: false },
port: sslPort
});
throw new Error('Must have thrown an exception !');
} catch (err) {
assert.equal(err.errno, 45023);
assert.equal(err.code, 'ER_SERVER_SSL_DISABLED');
}
});
it('signed certificate error', async function () {
if (!sslEnable) this.skip();
let conn = null;
// skip for ephemeral, since will succeed
if (shareConn.info.isMariaDB() && shareConn.info.hasMinVersion(11, 4, 0) && !shareConn.info.hasMinVersion(23, 0, 0))
this.skip();
if (isMaxscale() && isMaxscaleMinVersion(25, 8, 0)) this.skip();
try {
conn = await base.createConnection({
user: 'sslTestUser',
password: 'ytoKS@led5',
ssl: true,
port: sslPort
});
await validConnection(conn);
throw new Error('Must have thrown an exception !');
} catch (err) {
assert(
err.message.includes('Self signed certificate') ||
err.message.includes('self-signed certificate') ||
err.message.includes('unable to get local issuer certificate') ||
err.message.includes('unable to verify the first certificate'),
err.message
);
} finally {
if (conn != null) conn.end();
}
});
it('signed certificate error with ephemeral', async function () {
if (!sslEnable) this.skip();
let isMaxscaleEphemeral = false;
if (isMaxscale() && isMaxscaleMinVersion(25, 8, 0)) {
// MaxScale implements this in the 25.08 release
isMaxscaleEphemeral = true;
} else if (
!shareConn.info.isMariaDB() ||
!shareConn.info.hasMinVersion(11, 4, 0) ||
shareConn.info.hasMinVersion(23, 0, 0) ||
(isMaxscale() && !isMaxscaleMinVersion(25, 8, 0))
)
this.skip();
let conn = null;
try {
conn = await base.createConnection({
user: 'sslTestUser',
password: 'ytoKS@led5',
ssl: true,
port: sslPort
});
await validConnection(conn);
// if not ephemeral certificate must throw error
if (
!isMaxscaleEphemeral &&
!shareConn.info.isMariaDB() &&
(!shareConn.info.hasMinVersion(11, 4, 0) || shareConn.info.hasMinVersion(23, 0, 0))
) {
throw new Error('Must have thrown an exception !');
}
} finally {
if (conn != null) conn.end();
}
});
it('signed certificate forcing', async function () {
if (!sslEnable) this.skip();
const conn = await base.createConnection({ ssl: { rejectUnauthorized: false }, port: sslPort });
await validConnection(conn);
await conn.end();
});
it('self signed certificate server before ephemeral', async function () {
if (isMaxscale()) this.skip();
if (!sslEnable) this.skip();
// test will work either because server certificate chain is trusted (not don in tests)
// or using mariadb ephemeral certificate validation
if (
!shareConn.info.isMariaDB() ||
(shareConn.info.hasMinVersion(11, 4, 0) && !shareConn.info.hasMinVersion(23, 0, 0))
)
this.skip();
try {
await base.createConnection({ ssl: true, port: sslPort });
throw new Error('must have thrown error');
} catch (e) {
assert.equal(e.errno, errors.ER_SELF_SIGNED);
}
});
it('self signed certificate forcing no password', async function () {
if (isMaxscale()) this.skip();
if (!sslEnable) this.skip();
// test will work either because server certificate chain is trusted (not done in tests)
// or using mariadb ephemeral certificate validation
if (shareConn.info.isMariaDB() && shareConn.info.hasMinVersion(11, 4, 0) && !shareConn.info.hasMinVersion(23, 0, 0))
this.skip();
if (Conf.baseConfig.password) this.skip();
try {
await base.createConnection({ ssl: true, port: sslPort });
throw new Error('must have thrown error');
} catch (e) {
assert.equal(e.errno, errors.ER_SELF_SIGNED);
}
});
it('self signed certificate forcing with password ssl:true', async function () {
if (isMaxscale()) this.skip();
if (!sslEnable) this.skip();
// test will work either because server certificate chain is trusted (not don in tests)
// or using mariadb ephemeral certificate validation
if (
!shareConn.info.isMariaDB() ||
!shareConn.info.hasMinVersion(11, 4, 0) ||
shareConn.info.hasMinVersion(23, 0, 0)
)
this.skip();
if (!Conf.baseConfig.password) this.skip();
const conn = await base.createConnection({
user: 'sslTestUser',
password: 'ytoKS@led5',
ssl: true,
port: sslPort
});
await validConnection(conn);
await conn.end();
});
it('self signed certificate forcing with password ssl: {rejectUnauthorized: true}', async function () {
if (isMaxscale()) this.skip();
if (!sslEnable) this.skip();
// test will work either because server certificate chain is trusted (not done in tests)
// or using mariadb ephemeral certificate validation
if (
!shareConn.info.isMariaDB() ||
!shareConn.info.hasMinVersion(11, 4, 0) ||
shareConn.info.hasMinVersion(23, 0, 0)
)
this.skip();
if (!Conf.baseConfig.password) this.skip();
const conn = await base.createConnection({
user: 'sslTestUser',
password: 'ytoKS@led5',
ssl: { rejectUnauthorized: true },
port: sslPort
});
await validConnection(conn);
await conn.end();
});
it('ensure connection use SSL ', async function () {
if (isMaxscale()) this.skip();
if (!sslEnable) this.skip();
if (!base.utf8Collation()) this.skip();
const conn = await base.createConnection({
user: 'sslTestUser',
password: 'ytoKS@led5',
ssl: { rejectUnauthorized: false, checkServerIdentity: () => {} },
port: sslPort
});
await validConnection(conn);
conn.end();
});
it('ensure connection use NOT SSL ', async function () {
if (isMaxscale()) this.skip();
if (!sslEnable) this.skip();
if (!base.utf8Collation()) this.skip();
const conn = await base.createConnection({
user: 'nosslTestUser',
password: 'ytoKS@led5',
allowPublicKeyRetrieval: true
});
await validConnection(conn);
conn.end();
});
it('SSLv3 disable', async function () {
if (!sslEnable) this.skip();
try {
await base.createConnection({
ssl: {
rejectUnauthorized: false,
secureProtocol: 'SSLv3_client_method'
},
port: sslPort
});
throw new Error('Must have thrown an exception !');
} catch (err) {
assert(err.message.includes('SSLv3 methods disabled'));
}
});
it('SSLv2 disable', async function () {
if (!sslEnable) this.skip();
try {
await base.createConnection({
ssl: { rejectUnauthorized: false, secureProtocol: 'SSLv2_method' },
port: sslPort
});
throw new Error('Must have thrown an exception !');
} catch (err) {
assert(err.message.includes('SSLv2 methods disabled'));
}
});
it('TLSv1 working', async function () {
if (
!sslEnable ||
(shareConn.info.isMariaDB() && shareConn.info.hasMinVersion(10, 3, 0)) ||
(!shareConn.info.isMariaDB() &&
(shareConn.info.hasMinVersion(8, 0, 0) || shareConn.info.hasMinVersion(5, 7, 43))) ||
shareConn.info.serverVersion.raw.includes('focal')
) {
this.skip();
return;
}
const conn = await base.createConnection({
ssl: { rejectUnauthorized: false, secureProtocol: 'TLSv1_method' },
port: sslPort
});
checkProtocol(conn, 'TLSv1');
conn.end();
});
it('TLSv1.1 working', async function () {
if (
!sslEnable ||
(shareConn.info.isMariaDB() && shareConn.info.hasMinVersion(10, 3, 0)) ||
(!shareConn.info.isMariaDB() &&
(shareConn.info.hasMinVersion(8, 0, 0) || shareConn.info.hasMinVersion(5, 7, 43))) ||
shareConn.info.serverVersion.raw.includes('focal')
) {
this.skip();
return;
}
const conn = await base.createConnection({
ssl: { rejectUnauthorized: false, secureProtocol: 'TLSv1_1_method' },
port: sslPort
});
checkProtocol(conn, 'TLSv1.1');
conn.end();
});
it('TLSv1.1 with permit cipher', async function () {
if (
!sslEnable ||
(shareConn.info.isMariaDB() && shareConn.info.hasMinVersion(10, 4, 0)) ||
(!shareConn.info.isMariaDB() &&
(shareConn.info.hasMinVersion(8, 0, 0) || shareConn.info.hasMinVersion(5, 7, 43))) ||
shareConn.info.serverVersion.raw.includes('focal')
) {
this.skip();
return;
}
const conn = await base.createConnection({
ssl: {
rejectUnauthorized: false,
secureProtocol: 'TLSv1_1_method',
ciphers:
'DHE-RSA-AES256-SHA:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256'
},
port: sslPort
});
checkProtocol(conn, 'TLSv1.1');
conn.end();
});
it('TLSv1.1 no common cipher', async function () {
if (!sslEnable) this.skip();
if (
!shareConn.info.isMariaDB() &&
(!shareConn.info.hasMinVersion(5, 7, 10) || shareConn.info.hasMinVersion(8, 0, 0))
) {
this.skip();
return;
}
try {
await base.createConnection({
ssl: {
rejectUnauthorized: false,
secureProtocol: 'TLSv1_1_method',
ciphers: 'ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256'
},
port: sslPort
});
throw new Error('Must have thrown an exception !');
} catch (err) {
if (err.code !== 'ERR_SSL_NO_PROTOCOLS_AVAILABLE') {
assert(err.message.includes('no ciphers available'), err);
}
}
});
it('TLSv1.1 wrong cipher', async function () {
if (!sslEnable) this.skip();
if (
!shareConn.info.isMariaDB() &&
(!shareConn.info.hasMinVersion(5, 7, 10) || shareConn.info.hasMinVersion(8, 0, 0))
) {
this.skip();
return;
}
try {
await base.createConnection({
ssl: {
rejectUnauthorized: false,
secureProtocol: 'TLSv1_1_method',
ciphers: 'ECDHE-ECDSA-AES256-STRANGE'
},
port: sslPort
});
throw new Error('Must have thrown an exception !');
} catch (err) {
assert(err.message.includes('no ciphers available') || err.message.includes('no cipher match'));
}
});
it('TLSv1.2 working', async function () {
if (!sslEnable) this.skip();
//MariaDB server doesn't permit TLSv1.2 on windows
//MySQL community version doesn't support TLSv1.2
const isWin = process.platform === 'win32';
if (isWin || !shareConn.info.isMariaDB()) this.skip();
const conn = await base.createConnection({
ssl: { rejectUnauthorized: false, secureProtocol: 'TLSv1_2_method' },
port: sslPort
});
checkProtocol(conn, 'TLSv1.2');
await conn.end();
});
it('TLSv1.2 with cipher working', async function () {
if (isMaxscale()) this.skip();
if (!sslEnable) this.skip();
//MariaDB server doesn't permit TLSv1.2 on windows
//MySQL community version doesn't support TLSv1.2
const isWin = process.platform === 'win32';
if (!shareConn.info.isMariaDB() || (isWin && !shareConn.info.hasMinVersion(10, 4, 2))) {
this.skip();
}
const conn = await base.createConnection({
ssl: {
rejectUnauthorized: false,
secureProtocol: 'TLSv1_2_method',
secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
ciphers:
'DHE-RSA-AES256-SHA:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256'
},
port: sslPort
});
checkProtocol(conn, 'TLSv1.2');
await validConnection(conn);
conn.end();
});
it('CA provided ignoring name verification', async function () {
if (!sslEnable) this.skip();
if (!ca) this.skip();
if (!shareConn.info.isMariaDB() && !shareConn.info.hasMinVersion(5, 7, 10)) this.skip();
if (Conf.baseConfig.host !== 'localhost') this.skip();
let conn = await base.createConnection({
ssl: {
ca: ca,
checkServerIdentity: (servername, cert) => {}
},
port: sslPort
});
await validConnection(conn);
conn.end();
let success = false;
try {
conn = await base.createConnection({
ssl: {
ca: ca,
checkServerIdentity: (servername, cert) => {
throw new Error('test identity');
}
},
port: sslPort
});
await validConnection(conn);
conn.end();
success = true;
} catch (e) {
// eat
}
if (success && (!shareConn.info.isMariaDB() || !shareConn.info.hasMinVersion(11, 4, 0))) {
throw new Error('Must have thrown an exception, since server identity must not have been verified !');
}
});
it('CA name verification error', async function () {
if (!sslEnable) this.skip();
if (!ca) this.skip();
if (!shareConn.info.isMariaDB() && !shareConn.info.hasMinVersion(5, 7, 10)) this.skip();
if (Conf.baseConfig.host !== 'localhost') this.skip();
try {
await base.createConnection({ host: '127.0.0.1', ssl: { ca: ca } });
throw new Error('Must have thrown an exception !');
} catch (err) {
assert(
err.message.includes("Hostname/IP doesn't match certificate's altnames") ||
err.message.includes("Hostname/IP does not match certificate's altnames"),
'error was : ' + err.message
);
assert(err.message.includes("IP: 127.0.0.1 is not in the cert's list"), 'error was : ' + err.message);
}
});
it('CA provided with matching cn', async function () {
if (Conf.baseConfig.host !== 'localhost' && Conf.baseConfig.host !== 'mariadb.example.com') this.skip();
if (!sslEnable) this.skip();
if (!ca) this.skip();
if (!shareConn.info.isMariaDB() && !shareConn.info.hasMinVersion(5, 7, 10)) this.skip();
const conn = await base.createConnection({ host: 'mariadb.example.com', ssl: { ca: ca }, port: sslPort });
const isWin = process.platform === 'win32';
let expectedProtocol = ['TLSv1.2', 'TLSv1.3'];
if (shareConn.info.isMariaDB()) {
if (isWin && !shareConn.info.hasMinVersion(10, 4, 0)) {
expectedProtocol = 'TLSv1.1';
}
} else if (!shareConn.info.hasMinVersion(5, 7, 28)) {
expectedProtocol = 'TLSv1.1';
}
checkProtocol(conn, expectedProtocol);
await validConnection(conn);
await conn.end();
});
it('Mutual authentication without providing client certificate', async function () {
if (isMaxscale()) this.skip();
if (!sslEnable) this.skip();
if (!ca) this.skip();
let conn = null;
try {
conn = await base.createConnection({
user: 'X509testUser',
password: 'éà@d684SQpl¨^',
host: '192.168.3.19',
ssl: { ca: ca },
port: sslPort
});
} catch (err) {
// skip
}
if (conn) {
await validConnection(conn);
conn.end();
throw new Error('Must have thrown an exception !');
}
});
it('Mutual authentication providing client certificate', async function () {
if (isMaxscale()) this.skip();
if (!sslEnable) this.skip();
if (!ca || !clientKey || !clientCert) this.skip();
if (!base.utf8Collation()) this.skip();
const conn = await base.createConnection({
user: 'X509testUser',
password: 'éà@d684SQpl¨^',
host: 'mariadb.example.com',
ssl: {
ca: ca,
cert: clientCert,
key: clientKey
},
port: sslPort
});
await validConnection(conn);
conn.end();
});
it('Mutual authentication providing client keystore', async function () {
if (isMaxscale()) this.skip();
if (!sslEnable) this.skip();
if (!ca || !clientKeystore) this.skip();
if (!base.utf8Collation()) this.skip();
const ver = process.version.substring(1).split('.');
//on node.js 17+ client keystore won't be supported until installing openssl 3.0
if (parseInt(ver[0]) >= 17) this.skip();
const conn = await base.createConnection({
user: 'X509testUser',
password: 'éà@d684SQpl¨^',
host: 'mariadb.example.com',
ssl: {
ca: ca,
pfx: clientKeystore,
passphrase: 'kspass'
},
port: sslPort
});
await validConnection(conn);
conn.end();
});
it('ssl change user', async function () {
if (isMaxscale()) this.skip();
if (!shareConn.info.isMariaDB()) this.skip();
if (!sslEnable) this.skip();
let currUser;
const conn = await base.createConnection({
ssl: { rejectUnauthorized: false },
port: sslPort
});
conn.query('DROP USER ChangeUser' + getHostSuffix()).catch((err) => {});
conn.query('FLUSH PRIVILEGES');
conn.query('CREATE USER ChangeUser' + getHostSuffix() + " IDENTIFIED BY 'mySupPassw@rd2'");
conn.query('GRANT SELECT ON *.* TO ChangeUser' + getHostSuffix() + ' with grant option');
await conn.query('FLUSH PRIVILEGES');
let res = await conn.query('SELECT CURRENT_USER');
currUser = res[0]['CURRENT_USER'];
await conn.changeUser({
user: 'ChangeUser',
password: 'mySupPassw@rd2',
connectAttributes: { par1: 'bouh', par2: 'bla' }
});
res = await conn.query('SELECT CURRENT_USER');
const user = res[0]['CURRENT_USER'];
assert.equal(user, 'ChangeUser' + getHostSuffix().replaceAll("'", ''));
assert(user !== currUser);
conn.query('DROP USER ChangeUser' + getHostSuffix());
conn.end();
});
it('ssl dialog authentication plugin', async function () {
if (!process.env.TEST_PAM_USER) this.skip();
if (isMaxscale()) this.skip();
if (!shareConn.info.isMariaDB()) this.skip();
if (!sslEnable) this.skip();
this.timeout(10000);
try {
await shareConn.query("INSTALL PLUGIN pam SONAME 'auth_pam'");
} catch (error) {}
try {
await shareConn.query("DROP USER IF EXISTS '" + process.env.TEST_PAM_USER + "'" + getHostSuffix());
} catch (error) {}
await shareConn.query(
"CREATE USER '" + process.env.TEST_PAM_USER + "'" + getHostSuffix() + " IDENTIFIED VIA pam USING 'mariadb'"
);
await shareConn.query(
"GRANT SELECT ON *.* TO '" + process.env.TEST_PAM_USER + "'" + getHostSuffix() + ' IDENTIFIED VIA pam'
);
await shareConn.query('FLUSH PRIVILEGES');
const conn = await base.createConnection({
user: process.env.TEST_PAM_USER,
password: process.env.TEST_PAM_PWD,
ssl: { rejectUnauthorized: false },
port: sslPort
});
await conn.end();
});
});
function checkProtocol(conn, protocol) {
const ver = process.version.substring(1).split('.');
const currentProtocol = conn.__tests.getSocket().getProtocol();
if (ver[0] > 5 || (ver[0] === '5' && ver[1] === '7')) {
if (Array.isArray(protocol)) {
for (let i = 0; i < protocol.length; i++) {
if (currentProtocol === protocol[i]) return;
}
//throw error
assert.equal(currentProtocol, protocol);
return;
}
assert.equal(currentProtocol, protocol);
}
}
async function validConnection(conn) {
let rows = await conn.query("SELECT 'a' t");
assert.deepEqual(rows, [{ t: 'a' }]);
}