201 lines
5.8 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: [
createCommunityAdvicePlugin({
id: "community.archivists",
name: "Regional archivists",
trusted: true,
message: "The archivists recognize this exact certificate and recommend trust.",
}),
createCommunityAdvicePlugin({
id: "community.network-watch",
name: "Independent network watch",
trusted: false,
message: "The network observers report an unexpected certificate change.",
}),
],
},
};
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 = [createFirefoxValidationPlugin()];
if (state.communityEnabled) {
plugins.push(
...(scenarios[state.scenario].plugins ?? [createVillageCommunityPlugin()]),
);
}
const userPlugin = createUserDecisionPlugin(state.userDecision);
if (userPlugin) plugins.push(userPlugin);
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("-", " ");
}
render();