Skip to content

Commit

Permalink
Fully test the certfp code
Browse files Browse the repository at this point in the history
  • Loading branch information
Half-Shot committed Aug 10, 2023
1 parent b56bb00 commit 90b8c42
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 4 deletions.
110 changes: 110 additions & 0 deletions spec/e2e/authentication.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { TestIrcServer } from "matrix-org-irc";
import { IrcBridgeE2ETest } from "../util/e2e-test";
import { describe, it } from "@jest/globals";
import { delay } from "../../src/promiseutil";
import { exec } from "node:child_process";
import { getKeyPairFromString } from "../../src/bridge/AdminRoomHandler";
import { randomUUID } from "node:crypto";

async function generateCertificatePair() {
return new Promise<ReturnType<typeof getKeyPairFromString>>((resolve, reject) => {
exec(
'openssl req -nodes -newkey rsa:2048 -keyout - -x509 -days 3 -out -' +
' -subj "/C=US/ST=Utah/L=Lehi/O=Your Company, Inc./OU=IT/CN=yourdomain.com"', {
timeout: 5000,
},
(err, stdout) => {
if (err) {
reject(err);
return;
}
resolve(getKeyPairFromString(stdout));
});
})
}


async function expectMsg(msgSet: string[], expected: string, timeoutMs = 5000) {
let waitTime = 0;
do {
waitTime += 200;
await delay(200);
if (waitTime > timeoutMs) {
throw Error(`Timeout waiting for "${expected}, instead got\n\t${msgSet.join('\n\t')}"`);
}
} while (!msgSet.includes(expected))
}

const PASSWORD = randomUUID();

/**
* Note, this test assumes the IRCD we're testing against has services enabled
* and certfp support. This isn't terribly standard, but we test with ergo which
* has all this supported.
*/
describe('Authentication tests', () => {
let testEnv: IrcBridgeE2ETest;
let certPair: ReturnType<typeof getKeyPairFromString>;
beforeEach(async () => {
certPair = await generateCertificatePair();
testEnv = await IrcBridgeE2ETest.createTestEnv({
matrixLocalparts: [TestIrcServer.generateUniqueNick("alice")],
ircNicks: ["bob_authtest"],
traceToFile: true,
});
await testEnv.setUp();
});
afterEach(() => {
return testEnv?.tearDown();
});
it('should be able to add a client certificate with the !certfp command', async () => {
const { homeserver, ircBridge } = testEnv
const aliceUserId = homeserver.users[0].userId;
const alice = homeserver.users[0].client;
const { bob_authtest: bob } = testEnv.ircTest.clients;
const nickServMsgs: string[] = [];
const adminRoomPromise = await testEnv.createAdminRoomHelper(alice);
const channel = TestIrcServer.generateUniqueChannel('authtest');
bob.on('notice', (from, _to, notice) => {
if (from === 'NickServ') {
nickServMsgs.push(notice);
}
});
await bob.say('NickServ', `REGISTER ${PASSWORD}}`);
await expectMsg(nickServMsgs, 'Account created');
await expectMsg(nickServMsgs, `You're now logged in as ${bob.nick}`);
bob.say('NickServ', `CERT ADD ${certPair.cert.fingerprint256}`);
await expectMsg(nickServMsgs, 'Certificate fingerprint successfully added');

const adminRoomId = adminRoomPromise;
const responseOne = alice.waitForRoomEvent({ eventType: 'm.room.message', sender: ircBridge.appServiceUserId });
await alice.sendText(adminRoomId, '!certfp');
expect((await responseOne).data.content.body).toEqual(
"Please enter your certificate and private key (without formatting) for localhost. Say 'cancel' to cancel."
);
const responseTwo = alice.waitForRoomEvent({ eventType: 'm.room.message', sender: ircBridge.appServiceUserId });
await alice.sendText(adminRoomId,
certPair.cert.toString()+"\n"+certPair.privateKey.export({type: "pkcs8", format: "pem"})
);
expect((await responseTwo).data.content.body).toEqual(
'Successfully stored certificate for localhost. Use !reconnect to use this cert.'
);

await testEnv.joinChannelHelper(alice, adminRoomId, channel);
const bridgedClient = await ircBridge.getBridgedClientsForUserId(aliceUserId)[0];
const aliceIrcClient = await bridgedClient.waitForConnected().then(() => bridgedClient.assertConnected());
// Slight gut wrenching to get the fingerprint out.
const getCertResponse = await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Timed out getting cert response')), 5000);
aliceIrcClient.on('raw', (msg) => {
console.log(msg);
if (msg.rawCommand === '276') {
clearTimeout(timeout);
resolve(msg);
}
});
})
bridgedClient.whois(bridgedClient.nick);
console.log(await getCertResponse);
});
});
15 changes: 12 additions & 3 deletions spec/util/e2e-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ dns.setDefaultResultOrder('ipv4first');

const WAIT_EVENT_TIMEOUT = 10000;

const DEFAULT_PORT = parseInt(process.env.IRC_TEST_PORT ?? '6667', 10);
const IRC_SECURE = process.env.IRC_SECURE === "false";
const DEFAULT_PORT = parseInt(process.env.IRC_TEST_PORT ?? IRC_SECURE ? "6697" : "6667", 10);
const DEFAULT_ADDRESS = process.env.IRC_TEST_ADDRESS ?? "127.0.0.1";
const IRCBRIDGE_TEST_REDIS_URL = process.env.IRCBRIDGE_TEST_REDIS_URL;

Expand Down Expand Up @@ -182,7 +183,11 @@ export class IrcBridgeE2ETest {

const workerID = parseInt(process.env.JEST_WORKER_ID ?? '0');
const { matrixLocalparts, config } = opts;
const ircTest = new TestIrcServer();
const ircTest = new TestIrcServer(undefined, undefined, {
secure: IRC_SECURE,
port: DEFAULT_PORT,
selfSigned: true,
});
const [postgresDb, homeserver] = await Promise.all([
this.createDatabase(),
createHS(["ircbridge_bot", ...matrixLocalparts || []], workerID),
Expand Down Expand Up @@ -241,6 +246,9 @@ export class IrcBridgeE2ETest {
port: DEFAULT_PORT,
additionalAddresses: [DEFAULT_ADDRESS],
onlyAdditionalAddresses: true,
sasl: true,
ssl: IRC_SECURE,
sslselfsign: IRC_SECURE,
matrixClients: {
userTemplate: "@irc_$NICK",
displayName: "$NICK",
Expand Down Expand Up @@ -290,7 +298,8 @@ export class IrcBridgeE2ETest {
debugApi: {
enabled: false,
port: 0,
}
},
passwordEncryptionKeyPath: './spec/support/passkey.pem',
},
...config,
...(redisUri && { connectionPool: {
Expand Down
1 change: 0 additions & 1 deletion src/datastore/postgres/PgDataStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,6 @@ export class PgDataStore implements DataStore, ProvisioningStore {
log.warn(`Failed to decrypt TLS key for ${userId} ${domain}`, ex);
}
}
console.log(config);
return new IrcClientConfig(userId, domain, config);
}

Expand Down
3 changes: 3 additions & 0 deletions src/irc/ConnectionInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,9 @@ export class ConnectionInstance {
saslType = "EXTERNAL";
}

log.debug(saslType ? `Connecting using ${saslType} auth` : 'Connecting without authentication');
console.log(secure);

const connectionOpts: IrcClientOpts = {
userName: opts.username,
realName: opts.realname,
Expand Down

0 comments on commit 90b8c42

Please sign in to comment.