igneum/relay/test/guard.test.mjs

113 lines
6.7 KiB
JavaScript

// node --test relay/test/guard.test.mjs (no dependencies, no network)
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { authVia, runCanon, keygen, signRun, verifyRun, machineTag, sameTag, checkRun, wantsReboot, feedLimit, retentionCutoff, machineForSecret, secretHash, newNonce, newSecret, FEED_LIMIT_MAX, RETENTION_DAYS, POST_ALLOWED, DROP_KINDS, mayRead } from '../lib/guard.mjs';
const T = 'ABCDEFGHIJKLMNOPQRST'; // the token shape: 20 characters
const K = 'relaykeyrelaykeyrelaykeyrelaykeyrelaykeyrelayke'; // 48
const I = 'intakekeyintakekeyintakekeyinta'; // 32
const env = { RELAY_TOKEN: T, RELAY_KEY: K, LOG_INTAKE_KEY: I };
test('authVia: the header alone is the token tier (X24); the path token still works for the phone page', () => {
assert.equal(authVia({ headers: { 'x-relay-token': T } }, env), 'token');
assert.equal(authVia({ query: { token: T }, headers: {} }, env), 'token');
assert.equal(authVia({ headers: { 'x-relay-token': T.slice(0, 19) + 'x' } }, env), null);
assert.equal(authVia({ headers: {} }, env), null);
});
test('authVia: three tiers; the intake key is its own tier and RELAY_INTAKE_COMPAT=0 closes it (X23)', () => {
assert.equal(authVia({ headers: { 'x-igneum-key': K } }, env), 'key');
assert.equal(authVia({ headers: { 'x-igneum-key': I } }, env), 'intake');
assert.equal(authVia({ headers: { 'x-igneum-key': I } }, { ...env, LOG_INTAKE_KEY: '', LOG_INTAKE_KEY_NEXT: I }), 'intake');
assert.equal(authVia({ headers: { 'x-igneum-key': I } }, { ...env, RELAY_INTAKE_COMPAT: '0' }), null);
assert.equal(authVia({ headers: { 'x-igneum-key': 'wrongwrongwrongwrongwrongwrongwr' } }, env), null);
});
test('tiers: what each may post and read', () => {
assert.equal(POST_ALLOWED.token.has('task'), true);
assert.equal(POST_ALLOWED.key.has('task'), false);
assert.equal(POST_ALLOWED.key.has('secret'), false);
assert.deepEqual([...POST_ALLOWED.intake].sort(), ['drop', 'upload']);
assert.equal(DROP_KINDS.intake.has('result'), false);
assert.equal(DROP_KINDS.key.has('run'), false);
assert.equal(DROP_KINDS.token, null);
assert.deepEqual(['token', 'key', 'intake', null].map(mayRead), [true, true, false, false]);
});
test('runCanon: deterministic, names the machine, the nonce, the flags and the body hash', () => {
const a = runCanon({ to: 'PC1', nonce: 'a'.repeat(32), body: 'Write-Host hi', flags: { elevated: true } });
const b = runCanon({ to: 'PC1', nonce: 'a'.repeat(32), body: 'Write-Host hi', flags: { elevated: 1 } });
assert.equal(a, b);
assert.match(a, /^igneum-relay-run\/1\nto=PC1\nnonce=a{32}\nelevated=1\nreboot_continue=0\nreboot=0\nbody_sha256=[0-9a-f]{64}\n$/);
assert.notEqual(a, runCanon({ to: 'PC2', nonce: 'a'.repeat(32), body: 'Write-Host hi', flags: { elevated: true } }));
assert.notEqual(a, runCanon({ to: 'PC1', nonce: 'a'.repeat(32), body: 'Write-Host hi ', flags: { elevated: true } }));
assert.notEqual(a, runCanon({ to: 'PC1', nonce: 'a'.repeat(32), body: 'Write-Host hi', flags: {} }));
});
test('Ed25519: a good signature verifies; a changed byte, another key or a malformed signature does not', () => {
const { seed, pub } = keygen();
const other = keygen();
const canon = runCanon({ to: 'PC1', nonce: newNonce(), body: 'x' });
const sig = signRun(canon, seed);
assert.equal(sig.length, 128);
assert.equal(verifyRun(canon, sig, pub), true);
assert.equal(verifyRun(canon + ' ', sig, pub), false);
assert.equal(verifyRun(canon, sig, other.pub), false);
assert.equal(verifyRun(canon, sig.slice(0, 127) + (sig.endsWith('0') ? '1' : '0'), pub), false);
assert.equal(verifyRun(canon, 'nothex', pub), false);
assert.equal(verifyRun(canon, sig, 'nothex'), false);
});
test('machineTag: HMAC with the machine secret; sameTag compares in constant time and refuses malformed tags', () => {
const s = newSecret();
const canon = runCanon({ to: 'PC1', nonce: newNonce(), body: 'x' });
const t = machineTag(s, canon);
assert.equal(t.length, 64);
assert.equal(sameTag(t, machineTag(s, canon)), true);
assert.equal(sameTag(t, machineTag(newSecret(), canon)), false);
assert.equal(sameTag(t, machineTag(s, canon + 'x')), false);
assert.equal(sameTag(t, 'short'), false);
});
test('checkRun: a run without the signature, the tag or the nonce is refused; a complete one passes (X23)', () => {
const { seed, pub } = keygen();
const nonce = newNonce();
const body = 'Write-Host hi';
const flags = { elevated: true, nonce, mac: machineTag(newSecret(), runCanon({ to: 'PC1', nonce, body, flags: { elevated: true } })) };
flags.sig = signRun(runCanon({ to: 'PC1', nonce, body, flags }), seed);
assert.equal(checkRun({ to: 'PC1', body, flags }, pub), null);
assert.match(checkRun({ to: 'PC1', body, flags: {} }, pub), /nonce/);
assert.match(checkRun({ to: 'PC1', body, flags: { nonce } }, pub), /mac/);
assert.match(checkRun({ to: 'PC1', body, flags: { nonce, mac: flags.mac } }, pub), /sig/);
assert.match(checkRun({ to: 'PC1', body, flags }, ''), /RELAY_RUN_PUB/);
assert.match(checkRun({ to: 'PC1', body: body + ' ', flags }, pub), /does not verify/);
assert.match(checkRun({ to: 'PC2', body, flags }, pub), /does not verify/);
assert.match(checkRun({ to: 'PC1', body, flags: { ...flags, elevated: false } }, pub), /does not verify/);
assert.match(checkRun({ to: 'all', body, flags }, pub), /one named machine/);
});
test('wantsReboot: the marker on its own line only (X28)', () => {
assert.equal(wantsReboot('features enabled\nRELAY-REBOOT\n'), true);
assert.equal(wantsReboot('RELAY-REBOOT'), true);
assert.equal(wantsReboot('a\r\nRELAY-REBOOT\r\nb'), true);
assert.equal(wantsReboot('the script prints RELAY-REBOOT when it wants a restart\n'), false);
assert.equal(wantsReboot('RELAY-REBOOT-NOT\n'), false);
assert.equal(wantsReboot(''), false);
});
test('feedLimit and retention (X26)', () => {
assert.equal(feedLimit({}), 50);
assert.equal(feedLimit({ limit: '500' }), FEED_LIMIT_MAX);
assert.equal(feedLimit({ limit: '0' }), 50);
assert.equal(feedLimit({ limit: '7' }), 7);
assert.equal(RETENTION_DAYS, 30);
assert.equal(retentionCutoff(Date.UTC(2026, 9, 5, 22, 0, 0)), '2026-09-05T22:00:00.000Z');
});
test('machineForSecret: the stored sha256 names the machine; a wrong or malformed secret names nothing (X27)', () => {
const s = newSecret();
const rows = [{ name: 'PC1', secret_hash: secretHash(s) }, { name: 'PC2', secret_hash: secretHash(newSecret()) }, { name: 'Mac', secret_hash: null }];
assert.deepEqual(machineForSecret(s, rows), { name: 'PC1' });
assert.deepEqual(machineForSecret(newSecret(), rows), { error: 'unknown machine secret' });
assert.deepEqual(machineForSecret('short', rows), { error: 'x-machine-secret must be 64 hex' });
});