Generate client certificates and automatically do SASL EXTERNAL
This commit is contained in:
parent
3900e9dd81
commit
f8f692af05
@ -55,6 +55,7 @@
|
|||||||
"linkify-it": "2.2.0",
|
"linkify-it": "2.2.0",
|
||||||
"lodash": "4.17.15",
|
"lodash": "4.17.15",
|
||||||
"mime-types": "2.1.26",
|
"mime-types": "2.1.26",
|
||||||
|
"node-forge": "0.9.1",
|
||||||
"package-json": "6.5.0",
|
"package-json": "6.5.0",
|
||||||
"read": "1.0.7",
|
"read": "1.0.7",
|
||||||
"read-chunk": "3.2.0",
|
"read-chunk": "3.2.0",
|
||||||
|
@ -18,6 +18,7 @@ let storagePath;
|
|||||||
let packagesPath;
|
let packagesPath;
|
||||||
let fileUploadPath;
|
let fileUploadPath;
|
||||||
let userLogsPath;
|
let userLogsPath;
|
||||||
|
let clientCertificatesPath;
|
||||||
|
|
||||||
const Helper = {
|
const Helper = {
|
||||||
config: null,
|
config: null,
|
||||||
@ -31,6 +32,7 @@ const Helper = {
|
|||||||
getUsersPath,
|
getUsersPath,
|
||||||
getUserConfigPath,
|
getUserConfigPath,
|
||||||
getUserLogsPath,
|
getUserLogsPath,
|
||||||
|
getClientCertificatesPath,
|
||||||
setHome,
|
setHome,
|
||||||
getVersion,
|
getVersion,
|
||||||
getVersionCacheBust,
|
getVersionCacheBust,
|
||||||
@ -100,6 +102,7 @@ function setHome(newPath) {
|
|||||||
fileUploadPath = path.join(homePath, "uploads");
|
fileUploadPath = path.join(homePath, "uploads");
|
||||||
packagesPath = path.join(homePath, "packages");
|
packagesPath = path.join(homePath, "packages");
|
||||||
userLogsPath = path.join(homePath, "logs");
|
userLogsPath = path.join(homePath, "logs");
|
||||||
|
clientCertificatesPath = path.join(homePath, "certificates");
|
||||||
|
|
||||||
// Reload config from new home location
|
// Reload config from new home location
|
||||||
if (fs.existsSync(configPath)) {
|
if (fs.existsSync(configPath)) {
|
||||||
@ -185,6 +188,10 @@ function getUserLogsPath() {
|
|||||||
return userLogsPath;
|
return userLogsPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getClientCertificatesPath() {
|
||||||
|
return clientCertificatesPath;
|
||||||
|
}
|
||||||
|
|
||||||
function getStoragePath() {
|
function getStoragePath() {
|
||||||
return storagePath;
|
return storagePath;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ const Chan = require("./chan");
|
|||||||
const Msg = require("./msg");
|
const Msg = require("./msg");
|
||||||
const Helper = require("../helper");
|
const Helper = require("../helper");
|
||||||
const STSPolicies = require("../plugins/sts");
|
const STSPolicies = require("../plugins/sts");
|
||||||
|
const ClientCertificate = require("../plugins/clientCertificate");
|
||||||
|
|
||||||
module.exports = Network;
|
module.exports = Network;
|
||||||
|
|
||||||
@ -86,6 +87,10 @@ Network.prototype.validate = function (client) {
|
|||||||
this.port = this.tls ? 6697 : 6667;
|
this.port = this.tls ? 6697 : 6667;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.tls) {
|
||||||
|
ClientCertificate.remove(this.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
if (Helper.config.lockNetwork) {
|
if (Helper.config.lockNetwork) {
|
||||||
// This check is needed to prevent invalid user configurations
|
// This check is needed to prevent invalid user configurations
|
||||||
if (
|
if (
|
||||||
@ -182,6 +187,14 @@ Network.prototype.setIrcFrameworkOptions = function (client) {
|
|||||||
this.irc.options.tls = this.tls;
|
this.irc.options.tls = this.tls;
|
||||||
this.irc.options.rejectUnauthorized = this.rejectUnauthorized;
|
this.irc.options.rejectUnauthorized = this.rejectUnauthorized;
|
||||||
this.irc.options.webirc = this.createWebIrc(client);
|
this.irc.options.webirc = this.createWebIrc(client);
|
||||||
|
|
||||||
|
this.irc.options.client_certificate = this.tls ? ClientCertificate.get(this.uuid) : null;
|
||||||
|
|
||||||
|
if (this.irc.options.client_certificate && !this.irc.options.password) {
|
||||||
|
this.irc.options.sasl_mechanism = "EXTERNAL";
|
||||||
|
} else {
|
||||||
|
delete this.irc.options.sasl_mechanism;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Network.prototype.createWebIrc = function (client) {
|
Network.prototype.createWebIrc = function (client) {
|
||||||
|
134
src/plugins/clientCertificate.js
Normal file
134
src/plugins/clientCertificate.js
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const crypto = require("crypto");
|
||||||
|
const {md, pki} = require("node-forge");
|
||||||
|
const log = require("../log");
|
||||||
|
const Helper = require("../helper");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
get,
|
||||||
|
remove,
|
||||||
|
};
|
||||||
|
|
||||||
|
function get(uuid) {
|
||||||
|
if (Helper.config.public) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderPath = Helper.getClientCertificatesPath();
|
||||||
|
const paths = getPaths(folderPath, uuid);
|
||||||
|
|
||||||
|
if (!fs.existsSync(paths.privateKeyPath) || !fs.existsSync(paths.certificatePath)) {
|
||||||
|
return generateAndWrite(folderPath, paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
private_key: fs.readFileSync(paths.privateKeyPath, "utf-8"),
|
||||||
|
certificate: fs.readFileSync(paths.certificatePath, "utf-8"),
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
log.error("Unable to remove certificate", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(uuid) {
|
||||||
|
if (Helper.config.public) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paths = getPaths(Helper.getClientCertificatesPath(), uuid);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(paths.privateKeyPath)) {
|
||||||
|
fs.unlinkSync(paths.privateKeyPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(paths.certificatePath)) {
|
||||||
|
fs.unlinkSync(paths.certificatePath);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.error("Unable to remove certificate", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateAndWrite(folderPath, paths) {
|
||||||
|
const certificate = generate();
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(folderPath, {recursive: true});
|
||||||
|
|
||||||
|
fs.writeFileSync(paths.privateKeyPath, certificate.private_key, {
|
||||||
|
mode: 0o600,
|
||||||
|
});
|
||||||
|
fs.writeFileSync(paths.certificatePath, certificate.certificate, {
|
||||||
|
mode: 0o600,
|
||||||
|
});
|
||||||
|
|
||||||
|
return certificate;
|
||||||
|
} catch (e) {
|
||||||
|
log.error("Unable to write certificate", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate() {
|
||||||
|
const keys = pki.rsa.generateKeyPair(2048);
|
||||||
|
const cert = pki.createCertificate();
|
||||||
|
|
||||||
|
cert.publicKey = keys.publicKey;
|
||||||
|
cert.serialNumber = crypto.randomBytes(16).toString("hex").toUpperCase();
|
||||||
|
|
||||||
|
// Set notBefore a day earlier just in case the time between
|
||||||
|
// the client and server is not perfectly in sync
|
||||||
|
cert.validity.notBefore = new Date();
|
||||||
|
cert.validity.notBefore.setDate(cert.validity.notBefore.getDate() - 1);
|
||||||
|
|
||||||
|
// Set notAfter 100 years into the future just in case
|
||||||
|
// the server actually validates this field
|
||||||
|
cert.validity.notAfter = new Date();
|
||||||
|
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 100);
|
||||||
|
|
||||||
|
const attrs = [
|
||||||
|
{
|
||||||
|
name: "commonName",
|
||||||
|
value: "The Lounge IRC Client",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
cert.setSubject(attrs);
|
||||||
|
cert.setIssuer(attrs);
|
||||||
|
|
||||||
|
// Set extensions that indicate this is a client authentication certificate
|
||||||
|
cert.setExtensions([
|
||||||
|
{
|
||||||
|
name: "extKeyUsage",
|
||||||
|
clientAuth: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nsCertType",
|
||||||
|
client: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sign this certificate with a SHA256 signature
|
||||||
|
cert.sign(keys.privateKey, md.sha256.create());
|
||||||
|
|
||||||
|
const pem = {
|
||||||
|
private_key: pki.privateKeyToPem(keys.privateKey),
|
||||||
|
certificate: pki.certificateToPem(cert),
|
||||||
|
};
|
||||||
|
|
||||||
|
return pem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPaths(folderPath, uuid) {
|
||||||
|
return {
|
||||||
|
privateKeyPath: path.join(folderPath, `${uuid}.pem`),
|
||||||
|
certificatePath: path.join(folderPath, `${uuid}.crt`),
|
||||||
|
};
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const _ = require("lodash");
|
const _ = require("lodash");
|
||||||
|
const ClientCertificate = require("../clientCertificate");
|
||||||
|
|
||||||
exports.commands = ["quit"];
|
exports.commands = ["quit"];
|
||||||
exports.allowDisconnected = true;
|
exports.allowDisconnected = true;
|
||||||
@ -18,5 +19,7 @@ exports.input = function (network, chan, cmd, args) {
|
|||||||
const quitMessage = args[0] ? args.join(" ") : null;
|
const quitMessage = args[0] ? args.join(" ") : null;
|
||||||
network.quit(quitMessage);
|
network.quit(quitMessage);
|
||||||
|
|
||||||
|
ClientCertificate.remove(network.uuid);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
1
test/fixtures/.gitignore
vendored
1
test/fixtures/.gitignore
vendored
@ -1,6 +1,7 @@
|
|||||||
# Files that may be generated by tests
|
# Files that may be generated by tests
|
||||||
.thelounge/storage/
|
.thelounge/storage/
|
||||||
.thelounge/logs/
|
.thelounge/logs/
|
||||||
|
.thelounge/certificates/
|
||||||
|
|
||||||
# Fixtures contain fake packages, stored in a fake node_modules folder
|
# Fixtures contain fake packages, stored in a fake node_modules folder
|
||||||
!.thelounge/packages/node_modules/
|
!.thelounge/packages/node_modules/
|
||||||
|
53
test/plugins/clientCertificate.js
Normal file
53
test/plugins/clientCertificate.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const {expect} = require("chai");
|
||||||
|
const ClientCertificate = require("../../src/plugins/clientCertificate");
|
||||||
|
const Helper = require("../../src/helper");
|
||||||
|
|
||||||
|
describe("ClientCertificate", function () {
|
||||||
|
it("should not generate a client certificate in public mode", function () {
|
||||||
|
Helper.config.public = true;
|
||||||
|
|
||||||
|
const certificate = ClientCertificate.get("this-is-test-uuid");
|
||||||
|
expect(certificate).to.be.null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should generate a client certificate", function () {
|
||||||
|
Helper.config.public = false;
|
||||||
|
const certificate = ClientCertificate.get("this-is-test-uuid");
|
||||||
|
|
||||||
|
expect(certificate.certificate).to.match(/^-----BEGIN CERTIFICATE-----/);
|
||||||
|
expect(certificate.private_key).to.match(/^-----BEGIN RSA PRIVATE KEY-----/);
|
||||||
|
|
||||||
|
const certificate2 = ClientCertificate.get("this-is-test-uuid");
|
||||||
|
expect(certificate2.certificate).to.equal(certificate.certificate);
|
||||||
|
expect(certificate2.private_key).to.equal(certificate.private_key);
|
||||||
|
|
||||||
|
Helper.config.public = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove the client certificate files", function () {
|
||||||
|
Helper.config.public = false;
|
||||||
|
|
||||||
|
const privateKeyPath = path.join(
|
||||||
|
Helper.getClientCertificatesPath(),
|
||||||
|
`this-is-test-uuid.pem`
|
||||||
|
);
|
||||||
|
const certificatePath = path.join(
|
||||||
|
Helper.getClientCertificatesPath(),
|
||||||
|
`this-is-test-uuid.crt`
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fs.existsSync(privateKeyPath)).to.be.true;
|
||||||
|
expect(fs.existsSync(certificatePath)).to.be.true;
|
||||||
|
|
||||||
|
ClientCertificate.remove("this-is-test-uuid");
|
||||||
|
|
||||||
|
expect(fs.existsSync(privateKeyPath)).to.be.false;
|
||||||
|
expect(fs.existsSync(certificatePath)).to.be.false;
|
||||||
|
|
||||||
|
Helper.config.public = true;
|
||||||
|
});
|
||||||
|
});
|
@ -5853,6 +5853,11 @@ node-fetch@2.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.2.tgz#ab884e8e7e57e38a944753cec706f788d1768bb5"
|
||||||
integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=
|
integrity sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=
|
||||||
|
|
||||||
|
node-forge@0.9.1:
|
||||||
|
version "0.9.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5"
|
||||||
|
integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==
|
||||||
|
|
||||||
node-libs-browser@^2.2.1:
|
node-libs-browser@^2.2.1:
|
||||||
version "2.2.1"
|
version "2.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425"
|
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425"
|
||||||
|
Loading…
Reference in New Issue
Block a user