browsec/trustlab/test/policy-overlay.test.js

214 lines
6.6 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import {
expiredLeafCertificate,
unknownLocalAuthority,
} from "../fixtures/tls.js";
import { createUserDecisionPlugin } from "../plugins/demo-plugins.js";
import { createPolicyOverlayPlugin } from "../plugins/policy-overlay-plugin.js";
import { TrustPolicyOverlay, TrustRunner } from "../src/index.js";
test("authority decision becomes a scoped overlay rule used on retry", async () => {
const userPlugin = createUserDecisionPlugin(true, {
target: "authority",
lifetime: "session",
});
const first = await evaluateWith(userPlugin, "decision-authority");
assert.equal(first.verdict.scope.kind, "authority-for-host");
assert.equal(
first.verdict.scope.authorityCertificateSha256,
"ca-village-services",
);
const overlay = new TrustPolicyOverlay();
overlay.remember(first.verdict, {
pluginId: userPlugin.manifest.id,
pluginName: userPlugin.manifest.name,
});
const retry = await evaluateWith(
createPolicyOverlayPlugin(overlay),
"decision-authority",
);
assert.equal(retry.verdict.trusted, true);
assert.ok(
retry.journal.entries.some(
(entry) => entry.code === "local-policy-rule-matched",
),
);
assert.equal(overlay.snapshot().rules.length, 1);
});
test("authority decision binds the explicitly selected CA certificate", async () => {
const unknownAuthorityFacts = structuredClone(expiredLeafCertificate);
unknownAuthorityFacts.connectionId = "connection-selected-unknown-authority";
unknownAuthorityFacts.errors = ["unknown-issuer"];
unknownAuthorityFacts.failure = {
code: "unknown-issuer",
certificateSha256: "ca-village-public",
check: "trust-anchor",
summary: "The selected authority is not locally trusted.",
};
const plugin = createUserDecisionPlugin(true, {
target: "authority",
authorityCertificateSha256: "ca-village-public",
lifetime: "session",
});
const result = await new TrustRunner({
plugins: [{ plugin, mode: "decision-authority" }],
}).evaluate(unknownAuthorityFacts);
assert.equal(result.verdict.scope.kind, "authority-for-host");
assert.equal(
result.verdict.scope.authorityCertificateSha256,
"ca-village-public",
);
});
test("authority trust cannot override an expired leaf certificate", async () => {
const plugin = createUserDecisionPlugin(true, {
target: "authority",
authorityCertificateSha256: "ca-village-public",
lifetime: "session",
});
const result = await new TrustRunner({
plugins: [{ plugin, mode: "decision-authority" }],
}).evaluate(expiredLeafCertificate);
assert.equal(result.verdict.trusted, false);
assert.match(
result.journal.entries.at(-1).message,
/trust-anchor errors only/,
);
});
test("connection-lifetime rule is consumed by exactly one retry", async () => {
const userPlugin = createUserDecisionPlugin(true, {
target: "certificate",
lifetime: "connection",
});
const first = await evaluateWith(userPlugin, "decision-authority");
const overlay = new TrustPolicyOverlay();
overlay.remember(first.verdict, { pluginId: userPlugin.manifest.id });
const policyPlugin = createPolicyOverlayPlugin(overlay);
const accepted = await evaluateWith(policyPlugin, "decision-authority");
const rejected = await evaluateWith(policyPlugin, "decision-authority");
assert.equal(accepted.verdict.trusted, true);
assert.equal(rejected.verdict.trusted, false);
assert.equal(overlay.snapshot().rules.length, 0);
assert.equal(overlay.snapshot().history.length, 1);
});
test("local distrust takes precedence over a matching trust rule", () => {
const overlay = new TrustPolicyOverlay();
const scope = {
kind: "certificate-for-host",
hostname: unknownLocalAuthority.hostname,
port: unknownLocalAuthority.port,
certificateSha256: unknownLocalAuthority.presentedChain[0].sha256,
};
overlay.remember({
trusted: true,
scope,
lifetime: { kind: "session" },
reasonEntryIds: [],
overriddenErrors: [...unknownLocalAuthority.errors],
});
overlay.remember({
trusted: false,
scope,
lifetime: { kind: "session" },
reasonEntryIds: [],
overriddenErrors: [],
});
assert.equal(overlay.match(unknownLocalAuthority).trusted, false);
});
test("authority scope cannot target the leaf certificate", async () => {
const maliciousPlugin = {
manifest: manifest("test.non-ca-authority", ["decision-authority"]),
hooks: {
async onTlsFailure({ facts }) {
return {
trusted: true,
scope: {
kind: "authority-for-host",
hostname: facts.hostname,
port: "any",
authorityCertificateSha256: facts.presentedChain[0].sha256,
includeSubdomains: false,
},
lifetime: { kind: "session" },
reasonEntryIds: [],
overriddenErrors: [...facts.errors],
};
},
},
};
const result = await evaluateWith(maliciousPlugin, "decision-authority");
assert.equal(result.verdict.trusted, false);
assert.match(result.journal.entries.at(-1).message, /non-CA certificate/);
});
test("authority scope requires keyCertSign usage", async () => {
const brokenAuthorityFacts = structuredClone(unknownLocalAuthority);
for (const chain of [
brokenAuthorityFacts.presentedChain,
brokenAuthorityFacts.constructedChain,
]) {
const authority = chain.find(
(certificate) => certificate.sha256 === "ca-village-services",
);
authority.keyUsages = ["crlSign"];
}
const plugin = {
manifest: manifest("test.non-signing-authority", ["decision-authority"]),
hooks: {
async onTlsFailure({ facts }) {
return {
trusted: true,
scope: {
kind: "authority-for-host",
hostname: facts.hostname,
port: "any",
authorityCertificateSha256: "ca-village-services",
includeSubdomains: false,
},
lifetime: { kind: "session" },
reasonEntryIds: [],
overriddenErrors: [...facts.errors],
};
},
},
};
const result = await new TrustRunner({
plugins: [{ plugin, mode: "decision-authority" }],
}).evaluate(brokenAuthorityFacts);
assert.equal(result.verdict.trusted, false);
assert.match(result.journal.entries.at(-1).message, /without keyCertSign/);
});
function evaluateWith(plugin, mode) {
return new TrustRunner({ plugins: [{ plugin, mode }] }).evaluate(
unknownLocalAuthority,
);
}
function manifest(id, supportedModes) {
return {
manifestVersion: 1,
trustApiVersion: "0.1",
id,
name: id,
version: "0.0.0",
supportedModes,
capabilities: {},
};
}