Add synchronized DER view and investigation bundles

This commit is contained in:
sergeych committed 2026-08-16 23:42:04 +04:00
1 parent 216afb6c18
commit f5e4ece6e7
10 files changed
+372 -31

No files matched your search

@@ -0,0 +1,65 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import test from "node:test";
import { createInvestigationBundle, parseInvestigationBundle } from "../src/investigation-bundle.js";
import { exploreCertificateDer } from "../src/der-explorer.js";
test("investigation bundles round-trip original DER and normalized evidence", async () => {
const derBytes = bytes("30023000");
const der = exploreCertificateDer(derBytes);
const certificate = {
subject: "CN=Offline Test",
sha256: sha256(derBytes),
isCa: false,
keyUsages: [],
selfSigned: false,
derBase64: Buffer.from(derBytes).toString("base64"),
der,
};
const report = {
observedAt: "2026-08-16T00:00:00.000Z",
facts: {
connectionId: "offline-test",
hostname: "offline.test",
port: 443,
validation: "failure",
errors: ["unknown-issuer"],
failure: { code: "unknown-issuer", check: "trust-anchor", summary: "Unknown", certificateSha256: certificate.sha256 },
presentedChain: [certificate],
constructedChain: [certificate],
tls: {},
},
findings: [],
caveats: [],
};
const bundle = createInvestigationBundle(report);
const parsed = await parseInvestigationBundle(JSON.stringify(bundle));
assert.equal(parsed.evidence.facts.hostname, "offline.test");
assert.equal(parsed.evidence.facts.presentedChain[0].derBase64, certificate.derBase64);
assert.equal(parsed.privacy.containsPrivateKeys, false);
});
test("bundle import rejects altered DER-derived authority facts and secret-bearing declarations", async () => {
const derBytes = bytes("30023000");
const der = exploreCertificateDer(derBytes);
const base = {
format: "org.browsec.investigation",
version: 1,
privacy: { containsPrivateKeys: false, containsTlsSessionSecrets: false },
evidence: {
facts: {
connectionId: "tampered", hostname: "offline.test", port: 443, validation: "failure", errors: [],
presentedChain: [{ subject: "x", sha256: sha256(derBytes), isCa: true, keyUsages: [], selfSigned: false, derBase64: Buffer.from(derBytes).toString("base64"), der }],
constructedChain: [], tls: {},
},
findings: [], caveats: [],
},
};
await assert.rejects(parseInvestigationBundle(JSON.stringify(base)), /CA assertion/);
base.privacy.containsPrivateKeys = true;
await assert.rejects(parseInvestigationBundle(JSON.stringify(base)), /no-secrets declaration/);
});
function bytes(hex) { return Uint8Array.from(hex.match(/../g).map((pair) => Number.parseInt(pair, 16))); }
function sha256(value) { return createHash("sha256").update(value).digest("hex"); }
+16 -1
View File
@@ -15,7 +15,7 @@ import {
createVillageCommunityPlugin,
} from "../plugins/demo-plugins.js";
import { TrustRunner } from "../src/index.js";
import { chainRows, subjectName, verdictCopy } from "../ui/model.js";
import { chainRows, flattenDerTree, smallestDerNodeAt, subjectName, verdictCopy } from "../ui/model.js";
test("UI model identifies the failed end of an unknown-authority chain", () => {
const rows = chainRows(unknownLocalAuthority);
@@ -46,6 +46,19 @@ test("certificate display names prefer the common name", () => {
assert.equal(subjectName("O=Nameless"), "O=Nameless");
});
test("DER byte selection resolves to the narrowest ASN.1 node", () => {
const tree = {
type: "SEQUENCE", offset: 0, endOffset: 12, children: [
{ type: "INTEGER", offset: 2, endOffset: 5, children: [] },
{ type: "OCTET STRING", offset: 5, endOffset: 12, children: [] },
],
};
assert.deepEqual(flattenDerTree(tree).map((item) => item.depth), [0, 1, 1]);
assert.equal(smallestDerNodeAt(tree, 3).type, "INTEGER");
assert.equal(smallestDerNodeAt(tree, 8).type, "OCTET STRING");
assert.equal(smallestDerNodeAt(tree, 12), undefined);
});
test("UI plugins produce attributed evidence and a local Boolean verdict", async () => {
const plugins = [
configure(createFirefoxValidationPlugin(), "advisor"),
@@ -98,6 +111,8 @@ test("security surface contains immutable-frame and simulation labels", async ()
assert.match(html, /TRUSTLAB · SYNTHETIC/);
assert.match(html, /cannot alter browser or system trust/);
assert.match(html, /Live TLS target/);
assert.match(html, /Export investigation/);
assert.match(html, /Import investigation/);
assert.match(html, /Decision target/);
assert.match(html, /Effective and consumed local rules/);
});