browsec/trustlab/test/runner.test.js

252 lines
7.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.verdict.lifetime.kind, "connection");
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 failure hook can trust an exact certificate for a host", async () => {
const configured = configuredPlugin({
async onTlsFailure({ facts }) {
return verdictFor(facts, true);
},
});
const result = await new TrustRunner({ plugins: [configured] }).evaluate(
unknownLocalAuthority,
);
assert.equal(result.verdict.trusted, true);
assert.equal(result.verdict.scope.kind, "certificate-for-host");
assert.equal(result.journal.entries.at(-1).kind, "resolution");
assert.equal(
result.journal.entries.at(-1).pluginId,
configured.plugin.manifest.id,
);
});
test("evidence collection is independent but appended in plugin order", async () => {
const slowFirst = configuredPlugin({
id: "test.first",
mode: "observer",
async collectEvidence() {
await new Promise((resolve) => setTimeout(resolve, 5));
return { entries: [{ kind: "evidence", code: "first" }] };
},
});
const fastSecond = configuredPlugin({
id: "test.second",
mode: "observer",
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("asynchronous failure hook timeout is recorded and falls through safely", async () => {
const configured = configuredPlugin({
async onTlsFailure() {
await new Promise((resolve) => setTimeout(resolve, 30));
return undefined;
},
});
const result = await new TrustRunner({
plugins: [configured],
timeoutMs: 5,
}).evaluate(unknownLocalAuthority);
assert.equal(result.verdict.trusted, false);
assert.equal(result.journal.entries.at(-1).code, "plugin-timeout");
});
test("successful-path hook must return synchronously", async () => {
const configured = configuredPlugin({
async onBeforeTlsAccept() {
return undefined;
},
});
const result = await new TrustRunner({ plugins: [configured] }).evaluate(
validPublicCertificate,
);
assert.equal(result.verdict.trusted, true);
assert.match(result.journal.entries.at(-1).message, /must return synchronously/);
});
test("a verdict cannot escape the active hostname, port, or certificate", async () => {
const configured = configuredPlugin({
async onTlsFailure({ facts }) {
return {
...verdictFor(facts, true),
scope: {
...exactScope(facts),
hostname: "different.test",
},
};
},
});
const result = await new TrustRunner({ plugins: [configured] }).evaluate(
unknownLocalAuthority,
);
assert.equal(result.verdict.trusted, false);
assert.match(result.journal.entries.at(-1).message, /scope does not match/);
});
test("observer and advisor modes cannot issue terminal verdicts", async () => {
for (const mode of ["observer", "advisor"]) {
const configured = configuredPlugin({
mode,
async onTlsFailure({ facts }) {
return verdictFor(facts, true);
},
});
const result = await new TrustRunner({ plugins: [configured] }).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 configured = configuredPlugin({
mode: "veto-authority",
async onTlsFailure({ facts }) {
return verdictFor(facts, true);
},
});
const result = await new TrustRunner({ plugins: [configured] }).evaluate(
unknownLocalAuthority,
);
assert.equal(result.verdict.trusted, false);
assert.match(result.journal.entries.at(-1).message, /cannot issue a trusted/);
});
test("authority scope must identify a certificate in the active chain", async () => {
const configured = configuredPlugin({
async onTlsFailure({ facts }) {
return {
trusted: true,
scope: {
kind: "authority-for-host",
hostname: facts.hostname,
port: facts.port,
authorityCertificateSha256: "not-in-this-chain",
includeSubdomains: false,
},
lifetime: { kind: "session" },
reasonEntryIds: [],
overriddenErrors: [...facts.errors],
};
},
});
const result = await new TrustRunner({ plugins: [configured] }).evaluate(
unknownLocalAuthority,
);
assert.equal(result.verdict.trusted, false);
assert.match(result.journal.entries.at(-1).message, /does not match the certificate chain/);
});
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: pluginManifest("test.impostor", ["decision-authority"]),
finalize() {},
},
}),
/must be Browsec's built-in handler/,
);
assert.doesNotThrow(() =>
new TrustRunner({ finalHandler: createBuiltinFinalHandler() }),
);
});
function configuredPlugin({
id = "test.plugin",
mode = "decision-authority",
collectEvidence,
onBeforeTlsAccept,
onTlsFailure,
}) {
return {
mode,
plugin: {
manifest: pluginManifest(id, [mode]),
hooks: { collectEvidence, onBeforeTlsAccept, onTlsFailure },
},
};
}
function pluginManifest(id, supportedModes) {
return {
manifestVersion: 1,
trustApiVersion: "0.1",
id,
name: id,
version: "0.0.0",
supportedModes,
capabilities: {},
};
}
function verdictFor(facts, trusted) {
return {
trusted,
scope: exactScope(facts),
lifetime: { kind: "connection" },
reasonEntryIds: [],
overriddenErrors: trusted ? [...facts.errors] : [],
};
}
function exactScope(facts) {
return {
kind: "certificate-for-host",
hostname: facts.hostname,
port: facts.port,
certificateSha256: facts.presentedChain[0].sha256,
};
}