import { conflictingCommunityAdvice, explicitlyDistrustedAuthority, expiredLeafCertificate, hostnameMismatch, unknownLocalAuthority, validPublicCertificate, } from "../fixtures/tls.js"; import { createCommunityAdvicePlugin, createFirefoxValidationPlugin, createUserDecisionPlugin, createVillageCommunityPlugin, } from "../plugins/demo-plugins.js"; import { createPolicyOverlayPlugin } from "../plugins/policy-overlay-plugin.js"; import { TrustPolicyOverlay, TrustRunner } from "../src/index.js"; import { chainRows, subjectName, 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", pendingDecision: undefined, communityEnabled: true, liveReport: undefined, }; const policyOverlay = new TrustPolicyOverlay(); const elements = { scenario: document.querySelector("#scenario"), probeForm: document.querySelector("#probe-form"), probeTarget: document.querySelector("#probe-target"), probeSubmit: document.querySelector("#probe-submit"), probeMessage: document.querySelector("#probe-message"), frameSeal: document.querySelector("#frame-seal"), community: document.querySelector("#community-enabled"), target: document.querySelector("#trust-target"), lifetime: document.querySelector("#trust-lifetime"), includeSubdomains: document.querySelector("#include-subdomains"), status: document.querySelector("#status"), identity: document.querySelector("#identity"), chain: document.querySelector("#chain"), journal: document.querySelector("#journal"), rules: document.querySelector("#rules"), 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.pendingDecision = undefined; renderTrustTargets(scenarios[state.scenario].facts); render(); }); elements.probeForm.addEventListener("submit", async (event) => { event.preventDefault(); elements.probeSubmit.disabled = true; elements.probeMessage.className = "probe-message"; elements.probeMessage.textContent = "Opening a read-only TLS connection…"; try { const response = await fetch("/api/probe", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ target: elements.probeTarget.value }), }); const report = await response.json(); if (!response.ok) throw new Error(report.error ?? `Probe failed (${response.status})`); state.liveReport = report; scenarios.live = { label: `Live: ${report.facts.hostname}:${report.facts.port}`, facts: report.facts, probePlugins: [configure(createProbeEvidencePlugin(report), "advisor")], }; let option = [...elements.scenario.options].find((item) => item.value === "live"); if (!option) { option = new Option(scenarios.live.label, "live"); elements.scenario.prepend(option); } else option.textContent = scenarios.live.label; state.scenario = "live"; elements.scenario.value = "live"; state.pendingDecision = undefined; renderTrustTargets(report.facts); elements.probeMessage.textContent = `Received ${report.facts.presentedChain.length} certificate(s).`; await render(); } catch (error) { elements.probeMessage.className = "probe-message error"; elements.probeMessage.textContent = error.message; } finally { elements.probeSubmit.disabled = false; } }); elements.target.addEventListener("change", () => { elements.includeSubdomains.disabled = !elements.target.value.startsWith("authority:"); if (elements.includeSubdomains.disabled) elements.includeSubdomains.checked = false; }); elements.community.addEventListener("change", () => { state.communityEnabled = elements.community.checked; render(); }); elements.trust.addEventListener("click", () => { state.pendingDecision = true; render(); }); elements.reject.addEventListener("click", () => { state.pendingDecision = false; render(); }); elements.clear.addEventListener("click", () => { state.pendingDecision = undefined; policyOverlay.clear(); render(); }); async function render() { const scenario = scenarios[state.scenario]; const facts = scenario.facts; const evidencePlugins = scenario.probePlugins ? [...scenario.probePlugins] : [configure(createFirefoxValidationPlugin(), "advisor")]; if (state.communityEnabled) { evidencePlugins.push( ...(scenario.plugins ?? [ configure(createVillageCommunityPlugin(), "advisor"), ]), ); } let decisionApplied = false; if (state.pendingDecision !== undefined) { policyOverlay.clear(); const userPlugin = createUserDecisionPlugin(state.pendingDecision, { target: elements.target.value.startsWith("authority:") ? "authority" : "certificate", authorityCertificateSha256: elements.target.value.startsWith("authority:") ? elements.target.value.slice("authority:".length) : undefined, lifetime: elements.lifetime.value, includeSubdomains: elements.includeSubdomains.checked, }); const decisionResult = await new TrustRunner({ plugins: [ ...evidencePlugins, configure(userPlugin, "decision-authority"), ], }).evaluate(facts); policyOverlay.remember(decisionResult.verdict, { pluginId: userPlugin.manifest.id, pluginName: userPlugin.manifest.name, }); state.pendingDecision = undefined; decisionApplied = true; } const plugins = [...evidencePlugins]; if (policyOverlay.match(facts)) { plugins.push( configure(createPolicyOverlayPlugin(policyOverlay), "decision-authority"), ); } const result = await new TrustRunner({ plugins }).evaluate(facts); const decidedByOverlay = result.journal.entries.some( (entry) => entry.pluginId === "org.browsec.local-policy-overlay" && entry.kind === "resolution", ); renderStatus(result, decisionApplied || decidedByOverlay); renderIdentity(result); renderChain(result); renderJournal(result); renderRules(policyOverlay.snapshot()); elements.clear.hidden = policyOverlay.snapshot().rules.length === 0; elements.frameSeal.textContent = state.scenario === "live" ? "TRUSTLAB · LIVE PROBE" : "TRUSTLAB · SYNTHETIC"; } function renderStatus(result, hasLocalDecision) { const copy = verdictCopy(result, hasLocalDecision); 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 renderRules(snapshot) { if (snapshot.history.length === 0) { elements.rules.replaceChildren(node("p", "No local trust rules.", "empty")); return; } const activeIds = new Set(snapshot.rules.map((rule) => rule.id)); elements.rules.replaceChildren( ...snapshot.history.map((rule) => { const article = document.createElement("article"); article.className = "journal-entry kind-resolution"; const status = activeIds.has(rule.id) ? "active" : "inactive"; article.append( node("span", status, "entry-kind"), node("h3", describeScope(rule.scope)), node( "p", `${rule.trusted ? "Trusted" : "Not trusted"} · ${describeLifetime(rule.lifetime)} · ${rule.sourcePluginName}`, ), node("code", rule.id), ); return article; }), ); } function describeScope(scope) { return scope.kind === "certificate-for-host" ? `Exact certificate ${shortFingerprint(scope.certificateSha256)} for ${scope.hostname}:${scope.port}` : `${scope.includeSubdomains ? "Authority for namespace" : "Authority for host"} ${scope.hostname} · ${shortFingerprint(scope.authorityCertificateSha256)}`; } function describeLifetime(lifetime) { const labels = { connection: "next connection", session: "browser session", persistent: "until revoked", until: `until ${lifetime.expiresAt}`, }; return labels[lifetime.kind]; } function renderIdentity(result) { elements.identity.replaceChildren( definition("Requested host", result.facts.hostname), definition("Port", String(result.facts.port)), definition("Conventional result", result.facts.validation), definition("TLS", result.facts.tls.version ?? "Unknown"), ); } function createProbeEvidencePlugin(report) { return { manifest: { manifestVersion: 1, trustApiVersion: "0.1", id: "org.browsec.live-probe", name: "Live TLS probe", version: "0.1.0", supportedModes: ["advisor"], capabilities: {}, }, hooks: { collectEvidence() { return { entries: [ ...(report.facts.validation === "success" ? [{ kind: "vote", code: "conventional-validation-succeeded", message: "The local OpenSSL validator accepted this certificate path.", data: { trusted: true }, }] : []), ...report.findings.map((finding) => ({ kind: finding.check === "trust-anchor" ? "warning" : "evidence", code: finding.code, message: finding.summary, data: { certificateSha256: finding.certificateSha256 }, })), ...report.caveats.map((message) => ({ kind: "warning", code: "probe-chain-source-limitation", message, })), ], }; }, }, }; } 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 }; } function renderTrustTargets(facts) { const current = elements.target.value; const options = [new Option("Exact certificate for this host", "certificate")]; const seen = new Set(); for (const certificate of facts.constructedChain) { if ( !certificate.isCa || !certificate.keyUsages.includes("keyCertSign") || seen.has(certificate.sha256) ) { continue; } seen.add(certificate.sha256); options.push( new Option( `Authority: ${subjectName(certificate.subject)} · ${shortFingerprint(certificate.sha256)}`, `authority:${certificate.sha256}`, ), ); } elements.target.replaceChildren(...options); if (options.some((option) => option.value === current)) elements.target.value = current; elements.includeSubdomains.disabled = !elements.target.value.startsWith("authority:"); } function shortFingerprint(fingerprint) { return fingerprint.length > 18 ? `${fingerprint.slice(0, 8)}…${fingerprint.slice(-8)}` : fingerprint; } renderTrustTargets(scenarios[state.scenario].facts); render();