mirror of
https://github.com/thelounge/thelounge
synced 2024-11-25 13:30:21 +00:00
Generate client certificates and automatically do SASL EXTERNAL
This commit is contained in:
parent
3900e9dd81
commit
f8f692af05
8 changed files with 217 additions and 0 deletions
|
@ -55,6 +55,7 @@
|
|||
"linkify-it": "2.2.0",
|
||||
"lodash": "4.17.15",
|
||||
"mime-types": "2.1.26",
|
||||
"node-forge": "0.9.1",
|
||||
"package-json": "6.5.0",
|
||||
"read": "1.0.7",
|
||||
"read-chunk": "3.2.0",
|
||||
|
|
|
@ -18,6 +18,7 @@ let storagePath;
|
|||
let packagesPath;
|
||||
let fileUploadPath;
|
||||
let userLogsPath;
|
||||
let clientCertificatesPath;
|
||||
|
||||
const Helper = {
|
||||
config: null,
|
||||
|
@ -31,6 +32,7 @@ const Helper = {
|
|||
getUsersPath,
|
||||
getUserConfigPath,
|
||||
getUserLogsPath,
|
||||
getClientCertificatesPath,
|
||||
setHome,
|
||||
getVersion,
|
||||
getVersionCacheBust,
|
||||
|
@ -100,6 +102,7 @@ function setHome(newPath) {
|
|||
fileUploadPath = path.join(homePath, "uploads");
|
||||
packagesPath = path.join(homePath, "packages");
|
||||
userLogsPath = path.join(homePath, "logs");
|
||||
clientCertificatesPath = path.join(homePath, "certificates");
|
||||
|
||||
// Reload config from new home location
|
||||
if (fs.existsSync(configPath)) {
|
||||
|
@ -185,6 +188,10 @@ function getUserLogsPath() {
|
|||
return userLogsPath;
|
||||
}
|
||||
|
||||
function getClientCertificatesPath() {
|
||||
return clientCertificatesPath;
|
||||
}
|
||||
|
||||
function getStoragePath() {
|
||||
return storagePath;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ const Chan = require("./chan");
|
|||
const Msg = require("./msg");
|
||||
const Helper = require("../helper");
|
||||
const STSPolicies = require("../plugins/sts");
|
||||
const ClientCertificate = require("../plugins/clientCertificate");
|
||||
|
||||
module.exports = Network;
|
||||
|
||||
|
@ -86,6 +87,10 @@ Network.prototype.validate = function (client) {
|
|||
this.port = this.tls ? 6697 : 6667;
|
||||
}
|
||||
|
||||
if (!this.tls) {
|
||||
ClientCertificate.remove(this.uuid);
|
||||
}
|
||||
|
||||
if (Helper.config.lockNetwork) {
|
||||
// This check is needed to prevent invalid user configurations
|
||||
if (
|
||||
|
@ -182,6 +187,14 @@ Network.prototype.setIrcFrameworkOptions = function (client) {
|
|||
this.irc.options.tls = this.tls;
|
||||
this.irc.options.rejectUnauthorized = this.rejectUnauthorized;
|
||||
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) {
|
||||
|
|
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";
|
||||
|
||||
const _ = require("lodash");
|
||||
const ClientCertificate = require("../clientCertificate");
|
||||
|
||||
exports.commands = ["quit"];
|
||||
exports.allowDisconnected = true;
|
||||
|
@ -18,5 +19,7 @@ exports.input = function (network, chan, cmd, args) {
|
|||
const quitMessage = args[0] ? args.join(" ") : null;
|
||||
network.quit(quitMessage);
|
||||
|
||||
ClientCertificate.remove(network.uuid);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
|
1
test/fixtures/.gitignore
vendored
1
test/fixtures/.gitignore
vendored
|
@ -1,6 +1,7 @@
|
|||
# Files that may be generated by tests
|
||||
.thelounge/storage/
|
||||
.thelounge/logs/
|
||||
.thelounge/certificates/
|
||||
|
||||
# Fixtures contain fake packages, stored in a fake node_modules folder
|
||||
!.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"
|
||||
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:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425"
|
||||
|
|
Loading…
Reference in a new issue