182 lines
5.0 KiB
JavaScript
182 lines
5.0 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
createBuiltinFinalHandler,
|
|
DecisionJournal,
|
|
TrustRunner,
|
|
} from "../src/index.js";
|
|
import {
|
|
unknownLocalAuthority,
|
|
validPublicCertificate,
|
|
} from "../fixtures/tls.js";
|
|
|
|
test("built-in handler rejects an unresolved certificate failure", async () => {
|
|
const result = await new TrustRunner().evaluate(unknownLocalAuthority);
|
|
assert.equal(result.verdict.trusted, false);
|
|
assert.equal(result.journal.entries.length, 0);
|
|
});
|
|
|
|
test("built-in handler preserves an ordinary successful validation", async () => {
|
|
const result = await new TrustRunner().evaluate(validPublicCertificate);
|
|
assert.equal(result.verdict.trusted, true);
|
|
});
|
|
|
|
test("a decision plugin can trust a narrowly scoped connection", async () => {
|
|
const plugin = pluginWith({
|
|
decide({ facts }) {
|
|
return {
|
|
trusted: true,
|
|
scope: { hostname: facts.hostname, port: facts.port },
|
|
};
|
|
},
|
|
});
|
|
|
|
const result = await new TrustRunner({ plugins: [plugin] }).evaluate(
|
|
unknownLocalAuthority,
|
|
);
|
|
assert.equal(result.verdict.trusted, true);
|
|
assert.equal(result.journal.entries.at(-1).kind, "resolution");
|
|
assert.equal(result.journal.entries.at(-1).pluginId, plugin.manifest.id);
|
|
});
|
|
|
|
test("evidence collection is independent but appended in plugin order", async () => {
|
|
const slowFirst = pluginWith({
|
|
id: "test.first",
|
|
async collectEvidence() {
|
|
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
return { entries: [{ kind: "evidence", code: "first" }] };
|
|
},
|
|
});
|
|
const fastSecond = pluginWith({
|
|
id: "test.second",
|
|
collectEvidence() {
|
|
return { entries: [{ kind: "warning", code: "second" }] };
|
|
},
|
|
});
|
|
|
|
const result = await new TrustRunner({ plugins: [slowFirst, fastSecond] }).evaluate(
|
|
validPublicCertificate,
|
|
);
|
|
assert.deepEqual(
|
|
result.journal.entries.map((entry) => entry.code),
|
|
["first", "second"],
|
|
);
|
|
});
|
|
|
|
test("plugin timeout is recorded and falls through safely", async () => {
|
|
const plugin = pluginWith({
|
|
async decide() {
|
|
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
return undefined;
|
|
},
|
|
});
|
|
|
|
const result = await new TrustRunner({ plugins: [plugin], timeoutMs: 5 }).evaluate(
|
|
unknownLocalAuthority,
|
|
);
|
|
assert.equal(result.verdict.trusted, false);
|
|
assert.equal(result.journal.entries.at(-1).code, "plugin-timeout");
|
|
});
|
|
|
|
test("a verdict cannot escape the active hostname and port", async () => {
|
|
const plugin = pluginWith({
|
|
decide() {
|
|
return {
|
|
trusted: true,
|
|
scope: { hostname: "different.test", port: 443 },
|
|
};
|
|
},
|
|
});
|
|
|
|
const result = await new TrustRunner({ plugins: [plugin] }).evaluate(
|
|
unknownLocalAuthority,
|
|
);
|
|
assert.equal(result.verdict.trusted, false);
|
|
assert.match(result.journal.entries.at(-1).message, /scope does not match/);
|
|
});
|
|
|
|
test("observer and advisor plugins cannot issue terminal verdicts", async () => {
|
|
for (const role of ["observer", "advisor"]) {
|
|
const plugin = pluginWith({
|
|
role,
|
|
decide({ facts }) {
|
|
return {
|
|
trusted: true,
|
|
scope: { hostname: facts.hostname, port: facts.port },
|
|
};
|
|
},
|
|
});
|
|
const result = await new TrustRunner({ plugins: [plugin] }).evaluate(
|
|
unknownLocalAuthority,
|
|
);
|
|
assert.equal(result.verdict.trusted, false);
|
|
assert.match(result.journal.entries.at(-1).message, /cannot issue/);
|
|
}
|
|
});
|
|
|
|
test("veto authority can reject but cannot trust", async () => {
|
|
const plugin = pluginWith({
|
|
role: "veto-authority",
|
|
decide({ facts }) {
|
|
return {
|
|
trusted: true,
|
|
scope: { hostname: facts.hostname, port: facts.port },
|
|
};
|
|
},
|
|
});
|
|
const result = await new TrustRunner({ plugins: [plugin] }).evaluate(
|
|
unknownLocalAuthority,
|
|
);
|
|
assert.equal(result.verdict.trusted, false);
|
|
assert.match(result.journal.entries.at(-1).message, /cannot issue a trusted/);
|
|
});
|
|
|
|
test("journal snapshots and entries are immutable", () => {
|
|
const journal = new DecisionJournal();
|
|
journal.append(
|
|
{ id: "test.plugin", name: "Test plugin" },
|
|
[{ kind: "evidence", data: { nested: true } }],
|
|
);
|
|
const snapshot = journal.snapshot();
|
|
|
|
assert.throws(() => snapshot.entries.push({}), TypeError);
|
|
assert.throws(() => {
|
|
snapshot.entries[0].data.nested = false;
|
|
}, TypeError);
|
|
});
|
|
|
|
test("only Browsec's immutable handler can occupy the final position", () => {
|
|
assert.throws(
|
|
() =>
|
|
new TrustRunner({
|
|
finalHandler: {
|
|
manifest: {
|
|
id: "test.impostor",
|
|
name: "Impostor",
|
|
role: "decision-authority",
|
|
},
|
|
finalize() {},
|
|
},
|
|
}),
|
|
/must be Browsec's built-in handler/,
|
|
);
|
|
|
|
assert.doesNotThrow(() =>
|
|
new TrustRunner({ finalHandler: createBuiltinFinalHandler() }),
|
|
);
|
|
});
|
|
|
|
function pluginWith({
|
|
id = "test.plugin",
|
|
role = "decision-authority",
|
|
collectEvidence,
|
|
decide,
|
|
}) {
|
|
return {
|
|
manifest: { id, name: id, role },
|
|
collectEvidence,
|
|
decide,
|
|
};
|
|
}
|