Files
qinglong/packages/ql3-local-admin/test/decisionIssuerKeyring.test.cjs
T

149 lines
5.3 KiB
JavaScript

const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
const {
LegacyCrontabDecisionIssuerKeyringConflictError,
LegacyCrontabDecisionIssuerKeyringFileProvider,
LegacyCrontabDecisionIssuerKeyringUnavailableError,
MAX_LEGACY_CRONTAB_DECISION_ISSUER_KEYS,
provisionLegacyCrontabDecisionIssuerKeyring,
rotateLegacyCrontabDecisionIssuerKeyring,
} = require('../dist/legacy-adoption/legacyCrontabDecisionIssuerKeyring');
function fixture(t) {
const root = fs.mkdtempSync(
path.join(os.tmpdir(), 'ql3-adoption-issuer-keyring-'),
);
fs.chmodSync(root, 0o700);
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
return { root, filePath: path.join(root, 'issuer.keyring') };
}
test('provisions one private dedicated key and rotates with exact replay fences', async (t) => {
const { filePath } = fixture(t);
const initial = await provisionLegacyCrontabDecisionIssuerKeyring(filePath);
assert.equal(initial.schemaVersion, 1);
assert.equal(
initial.kind,
'qinglong3-legacy-crontab-decision-issuer-keyring-summary',
);
assert.equal(initial.keyCount, 1);
assert.match(initial.activeKeyId, /^qladk-/);
assert.match(initial.keyringDigest, /^[0-9a-f]{64}$/);
assert.equal(fs.statSync(filePath).mode & 0o777, 0o600);
const provider = new LegacyCrontabDecisionIssuerKeyringFileProvider(filePath);
const active = await provider.active();
assert.equal(active.keyId, initial.activeKeyId);
assert.equal(active.key.byteLength, 32);
const rotated = await rotateLegacyCrontabDecisionIssuerKeyring({
filePath,
expectedActiveKeyId: initial.activeKeyId,
expectedKeyringDigest: initial.keyringDigest,
});
assert.equal(rotated.keyCount, 2);
assert.notEqual(rotated.activeKeyId, initial.activeKeyId);
assert.notEqual(rotated.keyringDigest, initial.keyringDigest);
assert.equal((await provider.active()).keyId, rotated.activeKeyId);
assert.equal(
(await provider.resolve(initial.activeKeyId)).keyId,
active.keyId,
);
await assert.rejects(
rotateLegacyCrontabDecisionIssuerKeyring({
filePath,
expectedActiveKeyId: initial.activeKeyId,
expectedKeyringDigest: initial.keyringDigest,
}),
LegacyCrontabDecisionIssuerKeyringConflictError,
);
});
test('never replaces an existing keyring and leaves another rotation lock intact', async (t) => {
const { filePath } = fixture(t);
const initial = await provisionLegacyCrontabDecisionIssuerKeyring(filePath);
const before = fs.readFileSync(filePath);
await assert.rejects(
provisionLegacyCrontabDecisionIssuerKeyring(filePath),
LegacyCrontabDecisionIssuerKeyringConflictError,
);
assert.deepEqual(fs.readFileSync(filePath), before);
const lockPath = `${filePath}.lock`;
fs.writeFileSync(lockPath, '', { mode: 0o600, flag: 'wx' });
await assert.rejects(
rotateLegacyCrontabDecisionIssuerKeyring({
filePath,
expectedActiveKeyId: initial.activeKeyId,
expectedKeyringDigest: initial.keyringDigest,
}),
LegacyCrontabDecisionIssuerKeyringUnavailableError,
);
assert.equal(fs.existsSync(lockPath), true);
});
test('fails closed for broad files, tampering, symlinks and parent replacement', async (t) => {
const { root, filePath } = fixture(t);
await provisionLegacyCrontabDecisionIssuerKeyring(filePath);
const provider = new LegacyCrontabDecisionIssuerKeyringFileProvider(filePath);
fs.chmodSync(filePath, 0o644);
await assert.rejects(
provider.active(),
LegacyCrontabDecisionIssuerKeyringUnavailableError,
);
fs.chmodSync(filePath, 0o600);
const manifest = JSON.parse(fs.readFileSync(filePath, 'utf8'));
manifest.keys[manifest.activeKeyId] = 'not-canonical-key-material';
fs.writeFileSync(filePath, `${JSON.stringify(manifest)}\n`, { mode: 0o600 });
await assert.rejects(
provider.active(),
LegacyCrontabDecisionIssuerKeyringUnavailableError,
);
const other = path.join(root, 'other.keyring');
await provisionLegacyCrontabDecisionIssuerKeyring(other);
const link = path.join(root, 'link.keyring');
fs.symlinkSync(other, link);
await assert.rejects(
new LegacyCrontabDecisionIssuerKeyringFileProvider(link).active(),
LegacyCrontabDecisionIssuerKeyringUnavailableError,
);
const moved = `${root}-moved`;
fs.renameSync(root, moved);
fs.mkdirSync(root, { mode: 0o700 });
t.after(() => fs.rmSync(moved, { recursive: true, force: true }));
await assert.rejects(
provider.active(),
LegacyCrontabDecisionIssuerKeyringUnavailableError,
);
});
test('retains at most eight verification keys', async (t) => {
const { filePath } = fixture(t);
let summary = await provisionLegacyCrontabDecisionIssuerKeyring(filePath);
while (summary.keyCount < MAX_LEGACY_CRONTAB_DECISION_ISSUER_KEYS) {
summary = await rotateLegacyCrontabDecisionIssuerKeyring({
filePath,
expectedActiveKeyId: summary.activeKeyId,
expectedKeyringDigest: summary.keyringDigest,
});
}
assert.equal(summary.keyCount, MAX_LEGACY_CRONTAB_DECISION_ISSUER_KEYS);
await assert.rejects(
rotateLegacyCrontabDecisionIssuerKeyring({
filePath,
expectedActiveKeyId: summary.activeKeyId,
expectedKeyringDigest: summary.keyringDigest,
}),
LegacyCrontabDecisionIssuerKeyringUnavailableError,
);
});