207 lines
6.0 KiB
JavaScript
207 lines
6.0 KiB
JavaScript
import {
|
|
conflictingCommunityAdvice,
|
|
explicitlyDistrustedAuthority,
|
|
expiredLeafCertificate,
|
|
hostnameMismatch,
|
|
unknownLocalAuthority,
|
|
validPublicCertificate,
|
|
} from "../fixtures/tls.js";
|
|
import {
|
|
createCommunityAdvicePlugin,
|
|
createFirefoxValidationPlugin,
|
|
createUserDecisionPlugin,
|
|
createVillageCommunityPlugin,
|
|
} from "../plugins/demo-plugins.js";
|
|
import { TrustRunner } from "../src/index.js";
|
|
import { chainRows, verdictCopy } from "./model.js";
|
|
|
|
const scenarios = {
|
|
"unknown-local": {
|
|
label: "Unknown village authority",
|
|
facts: unknownLocalAuthority,
|
|
},
|
|
"valid-public": {
|
|
label: "Valid conventional path",
|
|
facts: validPublicCertificate,
|
|
},
|
|
"expired-leaf": {
|
|
label: "Expired server certificate",
|
|
facts: expiredLeafCertificate,
|
|
},
|
|
"hostname-mismatch": {
|
|
label: "Hostname mismatch",
|
|
facts: hostnameMismatch,
|
|
},
|
|
"distrusted-authority": {
|
|
label: "Explicitly distrusted authority",
|
|
facts: explicitlyDistrustedAuthority,
|
|
},
|
|
"conflicting-advice": {
|
|
label: "Conflicting community advice",
|
|
facts: conflictingCommunityAdvice,
|
|
plugins: [
|
|
configure(createCommunityAdvicePlugin({
|
|
id: "community.archivists",
|
|
name: "Regional archivists",
|
|
trusted: true,
|
|
message: "The archivists recognize this exact certificate and recommend trust.",
|
|
}), "advisor"),
|
|
configure(createCommunityAdvicePlugin({
|
|
id: "community.network-watch",
|
|
name: "Independent network watch",
|
|
trusted: false,
|
|
message: "The network observers report an unexpected certificate change.",
|
|
}), "advisor"),
|
|
],
|
|
},
|
|
};
|
|
|
|
const state = {
|
|
scenario: "unknown-local",
|
|
userDecision: undefined,
|
|
communityEnabled: true,
|
|
};
|
|
|
|
const elements = {
|
|
scenario: document.querySelector("#scenario"),
|
|
community: document.querySelector("#community-enabled"),
|
|
status: document.querySelector("#status"),
|
|
identity: document.querySelector("#identity"),
|
|
chain: document.querySelector("#chain"),
|
|
journal: document.querySelector("#journal"),
|
|
trust: document.querySelector("#trust"),
|
|
reject: document.querySelector("#reject"),
|
|
clear: document.querySelector("#clear-decision"),
|
|
};
|
|
|
|
for (const [value, scenario] of Object.entries(scenarios)) {
|
|
const option = document.createElement("option");
|
|
option.value = value;
|
|
option.textContent = scenario.label;
|
|
elements.scenario.append(option);
|
|
}
|
|
|
|
elements.scenario.addEventListener("change", () => {
|
|
state.scenario = elements.scenario.value;
|
|
state.userDecision = undefined;
|
|
render();
|
|
});
|
|
elements.community.addEventListener("change", () => {
|
|
state.communityEnabled = elements.community.checked;
|
|
render();
|
|
});
|
|
elements.trust.addEventListener("click", () => {
|
|
state.userDecision = true;
|
|
render();
|
|
});
|
|
elements.reject.addEventListener("click", () => {
|
|
state.userDecision = false;
|
|
render();
|
|
});
|
|
elements.clear.addEventListener("click", () => {
|
|
state.userDecision = undefined;
|
|
render();
|
|
});
|
|
|
|
async function render() {
|
|
const facts = scenarios[state.scenario].facts;
|
|
const plugins = [configure(createFirefoxValidationPlugin(), "advisor")];
|
|
if (state.communityEnabled) {
|
|
plugins.push(
|
|
...(scenarios[state.scenario].plugins ?? [
|
|
configure(createVillageCommunityPlugin(), "advisor"),
|
|
]),
|
|
);
|
|
}
|
|
const userPlugin = createUserDecisionPlugin(state.userDecision);
|
|
if (userPlugin) plugins.push(configure(userPlugin, "decision-authority"));
|
|
|
|
const result = await new TrustRunner({ plugins }).evaluate(facts);
|
|
renderStatus(result);
|
|
renderIdentity(result);
|
|
renderChain(result);
|
|
renderJournal(result);
|
|
elements.clear.hidden = state.userDecision === undefined;
|
|
}
|
|
|
|
function renderStatus(result) {
|
|
const copy = verdictCopy(result, state.userDecision !== undefined);
|
|
elements.status.dataset.state = result.verdict.trusted ? "trusted" : "not-trusted";
|
|
elements.status.replaceChildren(
|
|
node("p", copy.eyebrow, "eyebrow"),
|
|
node("h1", copy.title),
|
|
node("p", copy.detail, "status-detail"),
|
|
);
|
|
}
|
|
|
|
function renderIdentity(result) {
|
|
elements.identity.replaceChildren(
|
|
definition("Requested host", result.facts.hostname),
|
|
definition("Port", String(result.facts.port)),
|
|
definition("Firefox result", result.facts.validation),
|
|
definition("TLS", result.facts.tls.version ?? "Unknown"),
|
|
);
|
|
}
|
|
|
|
function renderChain(result) {
|
|
elements.chain.replaceChildren(
|
|
...chainRows(result.facts).map((certificate, index, all) => {
|
|
const item = document.createElement("li");
|
|
item.className = certificate.failed ? "certificate failed" : "certificate";
|
|
item.append(
|
|
node("span", certificate.role, "certificate-role"),
|
|
node("strong", certificate.name),
|
|
node("code", certificate.sha256 ?? "No fingerprint"),
|
|
node("span", certificate.edge, "edge-label"),
|
|
);
|
|
item.setAttribute("aria-label", `${certificate.role}: ${certificate.name}. ${certificate.edge}`);
|
|
if (index < all.length - 1) item.dataset.linked = "true";
|
|
return item;
|
|
}),
|
|
);
|
|
}
|
|
|
|
function renderJournal(result) {
|
|
if (result.journal.entries.length === 0) {
|
|
elements.journal.replaceChildren(node("p", "No plugin findings.", "empty"));
|
|
return;
|
|
}
|
|
|
|
elements.journal.replaceChildren(
|
|
...result.journal.entries.map((entry) => {
|
|
const article = document.createElement("article");
|
|
article.className = `journal-entry kind-${entry.kind}`;
|
|
article.append(
|
|
node("span", entry.kind, "entry-kind"),
|
|
node("h3", entry.pluginName),
|
|
node("p", entry.message ?? formatCode(entry.code)),
|
|
node("code", entry.code ?? entry.id),
|
|
);
|
|
return article;
|
|
}),
|
|
);
|
|
}
|
|
|
|
function definition(term, value) {
|
|
const wrapper = document.createElement("div");
|
|
wrapper.append(node("dt", term), node("dd", value));
|
|
return wrapper;
|
|
}
|
|
|
|
function node(tag, text, className) {
|
|
const element = document.createElement(tag);
|
|
element.textContent = text;
|
|
if (className) element.className = className;
|
|
return element;
|
|
}
|
|
|
|
function formatCode(value = "") {
|
|
return value.replaceAll("-", " ");
|
|
}
|
|
|
|
function configure(plugin, mode) {
|
|
return { plugin, mode };
|
|
}
|
|
|
|
render();
|