Skip to content

Commit 5a757ad

Browse files
committed
Fully test the certfp code
1 parent 23994cd commit 5a757ad

File tree

4 files changed

+125
-4
lines changed

4 files changed

+125
-4
lines changed

spec/e2e/authentication.spec.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { TestIrcServer } from "matrix-org-irc";
2+
import { IrcBridgeE2ETest } from "../util/e2e-test";
3+
import { describe, it } from "@jest/globals";
4+
import { delay } from "../../src/promiseutil";
5+
import { exec } from "node:child_process";
6+
import { getKeyPairFromString } from "../../src/bridge/AdminRoomHandler";
7+
import { randomUUID } from "node:crypto";
8+
9+
async function generateCertificatePair() {
10+
return new Promise<ReturnType<typeof getKeyPairFromString>>((resolve, reject) => {
11+
exec(
12+
'openssl req -nodes -newkey rsa:2048 -keyout - -x509 -days 3 -out -' +
13+
' -subj "/C=US/ST=Utah/L=Lehi/O=Your Company, Inc./OU=IT/CN=yourdomain.com"', {
14+
timeout: 5000,
15+
},
16+
(err, stdout) => {
17+
if (err) {
18+
reject(err);
19+
return;
20+
}
21+
resolve(getKeyPairFromString(stdout));
22+
});
23+
})
24+
}
25+
26+
27+
async function expectMsg(msgSet: string[], expected: string, timeoutMs = 5000) {
28+
let waitTime = 0;
29+
do {
30+
waitTime += 200;
31+
await delay(200);
32+
if (waitTime > timeoutMs) {
33+
throw Error(`Timeout waiting for "${expected}, instead got\n\t${msgSet.join('\n\t')}"`);
34+
}
35+
} while (!msgSet.includes(expected))
36+
}
37+
38+
const PASSWORD = randomUUID();
39+
40+
/**
41+
* Note, this test assumes the IRCD we're testing against has services enabled
42+
* and certfp support. This isn't terribly standard, but we test with ergo which
43+
* has all this supported.
44+
*/
45+
describe('Authentication tests', () => {
46+
let testEnv: IrcBridgeE2ETest;
47+
let certPair: ReturnType<typeof getKeyPairFromString>;
48+
beforeEach(async () => {
49+
certPair = await generateCertificatePair();
50+
testEnv = await IrcBridgeE2ETest.createTestEnv({
51+
matrixLocalparts: [TestIrcServer.generateUniqueNick("alice")],
52+
ircNicks: ["bob_authtest"],
53+
traceToFile: true,
54+
});
55+
await testEnv.setUp();
56+
});
57+
afterEach(() => {
58+
return testEnv?.tearDown();
59+
});
60+
it('should be able to add a client certificate with the !certfp command', async () => {
61+
const { homeserver, ircBridge } = testEnv
62+
const aliceUserId = homeserver.users[0].userId;
63+
const alice = homeserver.users[0].client;
64+
const { bob_authtest: bob } = testEnv.ircTest.clients;
65+
const nickServMsgs: string[] = [];
66+
const adminRoomPromise = await testEnv.createAdminRoomHelper(alice);
67+
const channel = TestIrcServer.generateUniqueChannel('authtest');
68+
bob.on('notice', (from, _to, notice) => {
69+
if (from === 'NickServ') {
70+
nickServMsgs.push(notice);
71+
}
72+
});
73+
await bob.say('NickServ', `REGISTER ${PASSWORD}}`);
74+
await expectMsg(nickServMsgs, 'Account created');
75+
await expectMsg(nickServMsgs, `You're now logged in as ${bob.nick}`);
76+
bob.say('NickServ', `CERT ADD ${certPair.cert.fingerprint256}`);
77+
await expectMsg(nickServMsgs, 'Certificate fingerprint successfully added');
78+
79+
const adminRoomId = adminRoomPromise;
80+
const responseOne = alice.waitForRoomEvent({ eventType: 'm.room.message', sender: ircBridge.appServiceUserId });
81+
await alice.sendText(adminRoomId, '!certfp');
82+
expect((await responseOne).data.content.body).toEqual(
83+
"Please enter your certificate and private key (without formatting) for localhost. Say 'cancel' to cancel."
84+
);
85+
const responseTwo = alice.waitForRoomEvent({ eventType: 'm.room.message', sender: ircBridge.appServiceUserId });
86+
await alice.sendText(adminRoomId,
87+
certPair.cert.toString()+"\n"+certPair.privateKey.export({type: "pkcs8", format: "pem"})
88+
);
89+
expect((await responseTwo).data.content.body).toEqual(
90+
'Successfully stored certificate for localhost. Use !reconnect to use this cert.'
91+
);
92+
93+
await testEnv.joinChannelHelper(alice, adminRoomId, channel);
94+
const bridgedClient = await ircBridge.getBridgedClientsForUserId(aliceUserId)[0];
95+
const aliceIrcClient = await bridgedClient.waitForConnected().then(() => bridgedClient.assertConnected());
96+
// Slight gut wrenching to get the fingerprint out.
97+
const getCertResponse = await new Promise((resolve, reject) => {
98+
const timeout = setTimeout(() => reject(new Error('Timed out getting cert response')), 5000);
99+
aliceIrcClient.on('raw', (msg) => {
100+
console.log(msg);
101+
if (msg.rawCommand === '276') {
102+
clearTimeout(timeout);
103+
resolve(msg);
104+
}
105+
});
106+
})
107+
bridgedClient.whois(bridgedClient.nick);
108+
console.log(await getCertResponse);
109+
});
110+
});

spec/util/e2e-test.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ dns.setDefaultResultOrder('ipv4first');
1818

1919
const WAIT_EVENT_TIMEOUT = 10000;
2020

21-
const DEFAULT_PORT = parseInt(process.env.IRC_TEST_PORT ?? '6667', 10);
21+
const IRC_SECURE = process.env.IRC_SECURE === "false";
22+
const DEFAULT_PORT = parseInt(process.env.IRC_TEST_PORT ?? IRC_SECURE ? "6697" : "6667", 10);
2223
const DEFAULT_ADDRESS = process.env.IRC_TEST_ADDRESS ?? "127.0.0.1";
2324
const IRCBRIDGE_TEST_REDIS_URL = process.env.IRCBRIDGE_TEST_REDIS_URL;
2425

@@ -182,7 +183,11 @@ export class IrcBridgeE2ETest {
182183

183184
const workerID = parseInt(process.env.JEST_WORKER_ID ?? '0');
184185
const { matrixLocalparts, config } = opts;
185-
const ircTest = new TestIrcServer();
186+
const ircTest = new TestIrcServer(undefined, undefined, {
187+
secure: IRC_SECURE,
188+
port: DEFAULT_PORT,
189+
selfSigned: true,
190+
});
186191
const [postgresDb, homeserver] = await Promise.all([
187192
this.createDatabase(),
188193
createHS(["ircbridge_bot", ...matrixLocalparts || []], workerID),
@@ -241,6 +246,9 @@ export class IrcBridgeE2ETest {
241246
port: DEFAULT_PORT,
242247
additionalAddresses: [DEFAULT_ADDRESS],
243248
onlyAdditionalAddresses: true,
249+
sasl: true,
250+
ssl: IRC_SECURE,
251+
sslselfsign: IRC_SECURE,
244252
matrixClients: {
245253
userTemplate: "@irc_$NICK",
246254
displayName: "$NICK",
@@ -290,7 +298,8 @@ export class IrcBridgeE2ETest {
290298
debugApi: {
291299
enabled: false,
292300
port: 0,
293-
}
301+
},
302+
passwordEncryptionKeyPath: './spec/support/passkey.pem',
294303
},
295304
...config,
296305
...(redisUri && { connectionPool: {

src/datastore/postgres/PgDataStore.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,6 @@ export class PgDataStore implements DataStore, ProvisioningStore {
543543
log.warn(`Failed to decrypt TLS key for ${userId} ${domain}`, ex);
544544
}
545545
}
546-
console.log(config);
547546
return new IrcClientConfig(userId, domain, config);
548547
}
549548

src/irc/ConnectionInstance.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,9 @@ export class ConnectionInstance {
422422
saslType = "EXTERNAL";
423423
}
424424

425+
log.debug(saslType ? `Connecting using ${saslType} auth` : 'Connecting without authentication');
426+
console.log(secure);
427+
425428
const connectionOpts: IrcClientOpts = {
426429
userName: opts.username,
427430
realName: opts.realname,

0 commit comments

Comments
 (0)