Implement scoped authority trust overlay

This commit is contained in:
sergeych committed 2026-08-16 19:42:53 +04:00
1 parent 1eeb8bac05
commit d479c459fc
17 files changed
+712 -65

No files matched your search

+138 -15
View File
@@ -12,8 +12,9 @@ import {
createUserDecisionPlugin,
createVillageCommunityPlugin,
} from "../plugins/demo-plugins.js";
import { TrustRunner } from "../src/index.js";
import { chainRows, verdictCopy } from "./model.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": {
@@ -58,17 +59,23 @@ const scenarios = {
const state = {
scenario: "unknown-local",
userDecision: undefined,
pendingDecision: undefined,
communityEnabled: true,
};
const policyOverlay = new TrustPolicyOverlay();
const elements = {
scenario: document.querySelector("#scenario"),
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"),
@@ -83,49 +90,92 @@ for (const [value, scenario] of Object.entries(scenarios)) {
elements.scenario.addEventListener("change", () => {
state.scenario = elements.scenario.value;
state.userDecision = undefined;
state.pendingDecision = undefined;
renderTrustTargets(scenarios[state.scenario].facts);
render();
});
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.userDecision = true;
state.pendingDecision = true;
render();
});
elements.reject.addEventListener("click", () => {
state.userDecision = false;
state.pendingDecision = false;
render();
});
elements.clear.addEventListener("click", () => {
state.userDecision = undefined;
state.pendingDecision = undefined;
policyOverlay.clear();
render();
});
async function render() {
const facts = scenarios[state.scenario].facts;
const plugins = [configure(createFirefoxValidationPlugin(), "advisor")];
const evidencePlugins = [configure(createFirefoxValidationPlugin(), "advisor")];
if (state.communityEnabled) {
plugins.push(
evidencePlugins.push(
...(scenarios[state.scenario].plugins ?? [
configure(createVillageCommunityPlugin(), "advisor"),
]),
);
}
const userPlugin = createUserDecisionPlugin(state.userDecision);
if (userPlugin) plugins.push(configure(userPlugin, "decision-authority"));
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);
renderStatus(result);
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);
elements.clear.hidden = state.userDecision === undefined;
renderRules(policyOverlay.snapshot());
elements.clear.hidden = policyOverlay.snapshot().rules.length === 0;
}
function renderStatus(result) {
const copy = verdictCopy(result, state.userDecision !== undefined);
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"),
@@ -134,6 +184,47 @@ function renderStatus(result) {
);
}
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),
@@ -203,4 +294,36 @@ 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();
+25 -1
View File
@@ -28,6 +28,23 @@
<span>Village community evidence</span>
</label>
<label for="trust-target">Decision target</label>
<select id="trust-target">
<option value="certificate">Exact certificate for this host</option>
</select>
<label for="trust-lifetime">Decision lifetime</label>
<select id="trust-lifetime">
<option value="connection">Next connection</option>
<option value="session" selected>Browser session</option>
<option value="persistent">Until revoked</option>
</select>
<label class="toggle">
<input id="include-subdomains" type="checkbox">
<span>Include subdomains for authority rules</span>
</label>
<p class="simulation-note">
This page evaluates fixtures only. It cannot alter browser trust.
</p>
@@ -59,6 +76,14 @@
</div>
<div id="journal" class="journal"></div>
</section>
<section class="panel">
<div class="section-heading">
<p class="eyebrow">Policy overlay</p>
<h2>Effective and consumed local rules</h2>
</div>
<div id="rules" class="journal"></div>
</section>
</section>
</main>
@@ -75,4 +100,3 @@
<script type="module" src="/ui/app.js"></script>
</body>
</html>