mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
129 lines
6.3 KiB
TypeScript
129 lines
6.3 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*/
|
|
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { createHash, generateKeyPairSync, sign, type KeyObject } from 'node:crypto';
|
|
import { rmSync } from 'node:fs';
|
|
import { canonicalJson } from '@/lib/extension-package';
|
|
import { checkBundleSignature, clearKeyringCache, keyIdOf, OFFICIAL_DIRECTORY, rootKeysFor } from '../directory-keys';
|
|
|
|
// The newest keyring issue date per directory is remembered in the state dir.
|
|
const stateDir = await vi.hoisted(async () => {
|
|
const { mkdtempSync } = await import('node:fs');
|
|
const { tmpdir } = await import('node:os');
|
|
const { join } = await import('node:path');
|
|
const dir = mkdtempSync(join(tmpdir(), 'bulwark-directory-keys-'));
|
|
process.env.ADMIN_STATE_DIR = dir;
|
|
return dir;
|
|
});
|
|
afterAll(() => rmSync(stateDir, { recursive: true, force: true }));
|
|
|
|
function keypair() {
|
|
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
|
|
const spki = publicKey.export({ type: 'spki', format: 'der' }) as Buffer;
|
|
const raw = spki.subarray(spki.length - 32);
|
|
return { privateKey, raw: raw.toString('base64'), keyId: keyIdOf(raw) };
|
|
}
|
|
|
|
const sig = (key: KeyObject, text: string) => sign(null, Buffer.from(text, 'utf8'), key).toString('base64');
|
|
|
|
const DIR = 'https://directory.test';
|
|
const root = keypair();
|
|
const online = keypair();
|
|
const bytes = new TextEncoder().encode('zip bytes');
|
|
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
|
|
function keyring(status: 'active' | 'revoked' = 'active', signer = root) {
|
|
const body = {
|
|
_type: 'https://extensions.bulwarkmail.org/keyring/v1',
|
|
rootKeyId: signer.keyId,
|
|
issuedAt: '2026-10-14T00:00:00.000Z',
|
|
keys: [{ keyId: online.keyId, publicKey: online.raw, status, notBefore: '2026-10-14T00:00:00.000Z', notAfter: null }],
|
|
};
|
|
return { ...body, signature: sig(signer.privateKey, canonicalJson(body)) };
|
|
}
|
|
|
|
function signedHeaders(over: Record<string, unknown> = {}, key = online) {
|
|
const statement = canonicalJson({
|
|
_type: 'https://extensions.bulwarkmail.org/release/v1', slug: 'jitsi-meet', version: '1.2.0', kind: 'plugin',
|
|
bundle: { sha256, size: bytes.byteLength }, source: { host: 'github.com', repo: 'o/r', commit: 'c'.repeat(40), subpath: '' },
|
|
approvedAt: '2026-10-14T00:00:00.000Z', ...over,
|
|
});
|
|
return new Headers({
|
|
'x-bundle-statement': Buffer.from(statement, 'utf8').toString('base64'),
|
|
'x-bundle-signature': sig(key.privateKey, statement),
|
|
'x-bundle-signature-key': key.keyId,
|
|
});
|
|
}
|
|
|
|
const fetchKeyring = (doc: unknown): typeof fetch => (async () => Response.json(doc)) as typeof fetch;
|
|
|
|
const check = (headers: Headers, opts: { ring?: unknown; configured?: string | null; dir?: string; version?: string; data?: Uint8Array } = {}) =>
|
|
checkBundleSignature({
|
|
directoryUrl: opts.dir ?? DIR,
|
|
configuredRootKeys: opts.configured === undefined ? root.raw : opts.configured,
|
|
slug: 'jitsi-meet', version: opts.version ?? '1.2.0', kind: 'plugin',
|
|
bytes: opts.data ?? bytes, headers, fetchImpl: fetchKeyring(opts.ring ?? keyring()),
|
|
});
|
|
|
|
beforeEach(() => clearKeyringCache());
|
|
|
|
describe('checkBundleSignature', () => {
|
|
it('accepts a bundle signed by an online key from a root-signed keyring', async () => {
|
|
expect(await check(signedHeaders())).toMatchObject({ status: 'verified', keyId: online.keyId, commit: 'c'.repeat(40) });
|
|
});
|
|
|
|
it('refuses a bundle whose bytes changed', async () => {
|
|
expect(await check(signedHeaders(), { data: new TextEncoder().encode('zip bytez') })).toMatchObject({ status: 'invalid' });
|
|
});
|
|
|
|
it('refuses a valid signature for another extension or version', async () => {
|
|
expect(await check(signedHeaders({ slug: 'other' }))).toMatchObject({ status: 'invalid', reason: expect.stringContaining('not jitsi-meet') });
|
|
expect(await check(signedHeaders(), { version: '1.3.0' })).toMatchObject({ status: 'invalid' });
|
|
});
|
|
|
|
it('refuses a key the keyring revoked, or one it does not list', async () => {
|
|
expect(await check(signedHeaders(), { ring: keyring('revoked') })).toMatchObject({ status: 'invalid', reason: expect.stringContaining('revoked') });
|
|
expect(await check(signedHeaders({}, keypair()))).toMatchObject({ status: 'invalid', reason: expect.stringContaining('unknown key') });
|
|
});
|
|
|
|
it('refuses a keyring signed by a root key it does not trust', async () => {
|
|
expect(await check(signedHeaders(), { ring: keyring('active', keypair()) })).toMatchObject({ status: 'invalid', reason: expect.stringContaining('trusted root') });
|
|
});
|
|
|
|
it('refuses an unsigned bundle when root keys are known', async () => {
|
|
expect(await check(new Headers())).toMatchObject({ status: 'invalid', reason: expect.stringContaining('not signed') });
|
|
});
|
|
|
|
it('lets an unsigned bundle through from a directory with no known root keys', async () => {
|
|
expect(await check(new Headers(), { configured: '' })).toEqual({ status: 'unsigned' });
|
|
});
|
|
|
|
it('always knows the official directory root key', () => {
|
|
expect(Object.keys(rootKeysFor(OFFICIAL_DIRECTORY, null))).toHaveLength(1);
|
|
expect(rootKeysFor('https://self-hosted.example', null)).toEqual({});
|
|
});
|
|
|
|
it('refuses a keyring older than the newest one it has seen from that directory', async () => {
|
|
// A root-signed keyring stays valid on its own, so a compromised
|
|
// directory host could replay one in which a since-revoked key is still
|
|
// active and sign bundles with that leaked key.
|
|
const ringAt = (issuedAt: string) => {
|
|
const body = {
|
|
_type: 'https://extensions.bulwarkmail.org/keyring/v1',
|
|
rootKeyId: root.keyId,
|
|
issuedAt,
|
|
keys: [{ keyId: online.keyId, publicKey: online.raw, status: 'active', notBefore: issuedAt, notAfter: null }],
|
|
};
|
|
return { ...body, signature: sig(root.privateKey, canonicalJson(body)) };
|
|
};
|
|
const dir = 'https://replay.directory.test';
|
|
expect(await check(signedHeaders(), { dir, ring: ringAt('2026-10-20T00:00:00.000Z') })).toMatchObject({ status: 'verified' });
|
|
clearKeyringCache();
|
|
expect(await check(signedHeaders(), { dir, ring: ringAt('2026-10-14T00:00:00.000Z') }))
|
|
.toMatchObject({ status: 'invalid', reason: expect.stringContaining('older than the one seen before') });
|
|
clearKeyringCache();
|
|
expect(await check(signedHeaders(), { dir, ring: ringAt('2026-10-21T00:00:00.000Z') })).toMatchObject({ status: 'verified' });
|
|
});
|
|
});
|