Add explainable certificate path construction

This commit is contained in:
sergeych committed 2026-08-16 23:58:03 +04:00
1 parent 2d02590214
commit f0dbc19807
12 files changed
+319 -2

No files matched your search

+45
View File
@@ -85,6 +85,8 @@ const elements = {
status: document.querySelector("#status"),
identity: document.querySelector("#identity"),
chain: document.querySelector("#chain"),
pathPanel: document.querySelector("#path-panel"),
paths: document.querySelector("#paths"),
journal: document.querySelector("#journal"),
rules: document.querySelector("#rules"),
trust: document.querySelector("#trust"),
@@ -154,6 +156,7 @@ elements.importBundle.addEventListener("change", async () => {
facts: bundle.evidence.facts,
findings: bundle.evidence.findings,
caveats: bundle.evidence.caveats,
pathAnalysis: bundle.evidence.pathAnalysis,
};
policyOverlay.clear();
activateReport(report, "offline");
@@ -246,6 +249,7 @@ async function render() {
renderStatus(result, decisionApplied || decidedByOverlay);
renderIdentity(result);
renderChain(result);
renderPathAnalysis(state.scenario === "live" ? state.liveReport?.pathAnalysis : undefined, facts);
renderJournal(result);
renderRules(policyOverlay.snapshot());
elements.clear.hidden = policyOverlay.snapshot().rules.length === 0;
@@ -255,6 +259,47 @@ async function render() {
elements.exportBundle.hidden = state.scenario !== "live";
}
function renderPathAnalysis(analysis, facts) {
elements.pathPanel.hidden = !analysis;
if (!analysis) return;
const certificates = new Map(
[...facts.presentedChain, ...facts.constructedChain].map((certificate) => [certificate.sha256, certificate]),
);
const pathCards = analysis.paths.map((path, index) => {
const article = document.createElement("article");
article.className = `path-card status-${path.status}`;
article.append(
node("span", path.status, "entry-kind"),
node("h3", `Candidate path ${index + 1}`),
node("p", path.certificateSha256
.map((fingerprint) => subjectName(certificates.get(fingerprint)?.subject))
.join(" → ")),
node("code", [formatCode(path.terminalReason), path.structuralTerminalReason && path.structuralTerminalReason !== path.terminalReason ? `ends at: ${formatCode(path.structuralTerminalReason)}` : undefined, path.trustSource].filter(Boolean).join(" · ")),
);
if (path.validationFailures?.length) {
article.append(node("p", `Validation: ${path.validationFailures.map((failure) => formatCode(failure.code)).join("; ")}`));
}
return article;
});
const rejected = analysis.edges.filter((edge) =>
!edge.accepted && (edge.issuerNameMatches || edge.authorityKeyMatches === true || edge.signatureValid),
);
if (rejected.length) {
const heading = node("h3", "Rejected issuer edges", "path-subheading");
pathCards.push(heading, ...rejected.map((edge) => {
const article = document.createElement("article");
article.className = "path-card status-rejected";
article.append(
node("span", "rejected", "entry-kind"),
node("h3", `${subjectName(certificates.get(edge.childSha256)?.subject)} → ${subjectName(certificates.get(edge.issuerSha256)?.subject)}`),
node("p", edge.reasons.map(formatCode).join("; ")),
);
return article;
}));
}
elements.paths.replaceChildren(...pathCards);
}
function activateReport(report, source) {
state.liveReport = report;
state.source = source;
+8
View File
@@ -86,6 +86,14 @@
<ol id="chain" class="chain"></ol>
</section>
<section id="path-panel" class="panel" hidden>
<div class="section-heading">
<p class="eyebrow">Candidate construction</p>
<h2>Every route to trust—and where it ends</h2>
</div>
<div id="paths" class="path-analysis"></div>
</section>
<section class="panel">
<div class="section-heading">
<p class="eyebrow">Append-only journal</p>
+10
View File
@@ -296,6 +296,16 @@ select {
.entry-kind { color: var(--cyan); font: 700 0.62rem ui-monospace, monospace; text-transform: uppercase; }
.empty { color: var(--ink-muted); }
.path-analysis { display: grid; gap: 0.65rem; }
.path-card { display: grid; grid-template-columns: 5rem 1fr auto; gap: 0.2rem 0.8rem; padding: 0.9rem 1rem; border-left: 3px solid var(--amber); background: #0c161b; }
.path-card.status-trusted { border-color: var(--green); }
.path-card.status-untrusted, .path-card.status-rejected { border-color: var(--red); }
.path-card h3, .path-card p { margin: 0; }
.path-card h3 { font-size: 0.86rem; }
.path-card p { grid-column: 2 / -1; color: var(--ink-muted); font-size: 0.78rem; overflow-wrap: anywhere; }
.path-card code { color: #78919b; font-size: 0.68rem; }
.path-subheading { margin: 1rem 0 0.2rem; font-size: 0.85rem; }
.decision-bar {
position: fixed;
z-index: 20;