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();