mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 22:55:24 +00:00
feat: add plugin packing, signing, pinned trust and tmxplug bundling
This commit is contained in:
1 parent
c6a44b7ff3
commit
2ee9b3592c
25 files changed
+1764
-56
No files matched your search
@@ -0,0 +1,110 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import crypto from "node:crypto";
|
||||
import {
|
||||
requireSignedPlugins,
|
||||
verifyPluginArtifact,
|
||||
type TrustedPluginKey,
|
||||
} from "../../plugins/trust.js";
|
||||
|
||||
function keyPair(): { key: TrustedPluginKey; privateKey: crypto.KeyObject } {
|
||||
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
||||
const raw = publicKey
|
||||
.export({ format: "der", type: "spki" })
|
||||
.subarray(12)
|
||||
.toString("base64");
|
||||
return { key: { id: "test", publicKey: raw, addedIn: "2.9.0" }, privateKey };
|
||||
}
|
||||
|
||||
function signed(buffer: Buffer, privateKey: crypto.KeyObject) {
|
||||
const digest = crypto.createHash("sha256").update(buffer).digest();
|
||||
return {
|
||||
sha256: digest.toString("hex"),
|
||||
signature: crypto.sign(null, digest, privateKey).toString("base64"),
|
||||
};
|
||||
}
|
||||
|
||||
describe("verifyPluginArtifact", () => {
|
||||
const artifact = Buffer.from("a plugin archive");
|
||||
|
||||
it("accepts a file signed by a pinned key", () => {
|
||||
const { key, privateKey } = keyPair();
|
||||
const { sha256, signature } = signed(artifact, privateKey);
|
||||
expect(verifyPluginArtifact(artifact, sha256, signature, [key])).toEqual({
|
||||
ok: true,
|
||||
keyId: "test",
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts a signature from any of several pinned keys", () => {
|
||||
const old = keyPair();
|
||||
const current = keyPair();
|
||||
const { sha256, signature } = signed(artifact, old.privateKey);
|
||||
const result = verifyPluginArtifact(artifact, sha256, signature, [
|
||||
current.key,
|
||||
{ ...old.key, id: "old" },
|
||||
]);
|
||||
expect(result).toEqual({ ok: true, keyId: "old" });
|
||||
});
|
||||
|
||||
it("refuses a tampered file", () => {
|
||||
const { key, privateKey } = keyPair();
|
||||
const { sha256, signature } = signed(artifact, privateKey);
|
||||
const tampered = Buffer.from("a plugin archivE");
|
||||
expect(verifyPluginArtifact(tampered, sha256, signature, [key])).toEqual({
|
||||
ok: false,
|
||||
reason: "sha256 does not match the file",
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses a tampered file even when the sha256 matches it", () => {
|
||||
const { key, privateKey } = keyPair();
|
||||
const { signature } = signed(artifact, privateKey);
|
||||
const tampered = Buffer.from("something else");
|
||||
const sha256 = crypto.createHash("sha256").update(tampered).digest("hex");
|
||||
expect(verifyPluginArtifact(tampered, sha256, signature, [key])).toEqual({
|
||||
ok: false,
|
||||
reason: "signature does not match a trusted key",
|
||||
});
|
||||
});
|
||||
|
||||
it("refuses a signature from a key that is not pinned", () => {
|
||||
const pinned = keyPair();
|
||||
const other = keyPair();
|
||||
const { sha256, signature } = signed(artifact, other.privateKey);
|
||||
expect(
|
||||
verifyPluginArtifact(artifact, sha256, signature, [pinned.key]).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("refuses garbage and an empty key list", () => {
|
||||
const { key, privateKey } = keyPair();
|
||||
const { sha256, signature } = signed(artifact, privateKey);
|
||||
expect(verifyPluginArtifact(artifact, sha256, "bm9wZQ==", [key]).ok).toBe(
|
||||
false,
|
||||
);
|
||||
expect(verifyPluginArtifact(artifact, "zz", signature, [key]).ok).toBe(
|
||||
false,
|
||||
);
|
||||
expect(verifyPluginArtifact(artifact, sha256, signature, []).ok).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("trusts no key by default until one is pinned", () => {
|
||||
const { privateKey } = keyPair();
|
||||
const { sha256, signature } = signed(artifact, privateKey);
|
||||
expect(verifyPluginArtifact(artifact, sha256, signature).ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("requireSignedPlugins", () => {
|
||||
it("is on only for the exact value true", () => {
|
||||
const before = process.env.TERMIX_REQUIRE_SIGNED_PLUGINS;
|
||||
process.env.TERMIX_REQUIRE_SIGNED_PLUGINS = "true";
|
||||
expect(requireSignedPlugins()).toBe(true);
|
||||
process.env.TERMIX_REQUIRE_SIGNED_PLUGINS = "1";
|
||||
expect(requireSignedPlugins()).toBe(false);
|
||||
if (before === undefined) delete process.env.TERMIX_REQUIRE_SIGNED_PLUGINS;
|
||||
else process.env.TERMIX_REQUIRE_SIGNED_PLUGINS = before;
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user