Implement scoped authority trust overlay
This commit is contained in:
1 parent
1eeb8bac05
commit
d479c459fc
17 files changed
+712
-65
No files matched your search
+138
-15
@@ -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
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user