Merge branch 'phase/009-host-key-pruefung'
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { hostKeyEntscheiden } from "@/server/services/hostkeys";
|
||||
import { vorgangAusfuehren } from "@/worker/ausfuehren";
|
||||
import { masterKeyVergessen } from "@/worker/geheimnisse";
|
||||
import { HANDLER } from "@/worker/handler";
|
||||
import { hostVerbinden } from "@/worker/ssh/host";
|
||||
import { HostKeyGeaendertFehler, befehlAusfuehren } from "@/worker/ssh/verbindung";
|
||||
import { datenbankLeeren } from "../hilfen/db";
|
||||
import { hostKeyFingerprint, hostKeysTauschen, rootPasswort, zielA, zielBereit } from "./hilfen/ziele";
|
||||
|
||||
/** Host-Key-Pruefung gegen ein echtes sshd (#009). */
|
||||
|
||||
const AKTEUR = { id: "test", name: "Test" };
|
||||
let verzeichnis: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
await zielBereit(zielA);
|
||||
verzeichnis = mkdtempSync(path.join(tmpdir(), "sshkv-master-"));
|
||||
const datei = path.join(verzeichnis, "master.key");
|
||||
writeFileSync(datei, randomBytes(32), { mode: 0o600 });
|
||||
process.env["SSHKV_MASTER_KEY_FILE"] = datei;
|
||||
masterKeyVergessen();
|
||||
});
|
||||
|
||||
afterAll(() => rmSync(verzeichnis, { recursive: true, force: true }));
|
||||
|
||||
beforeEach(async () => {
|
||||
await datenbankLeeren();
|
||||
});
|
||||
|
||||
async function hostAnlegen() {
|
||||
return prisma.host.create({
|
||||
data: { name: "ziel-a", adresse: zielA.host, port: zielA.port, anmeldeart: "ROOT" },
|
||||
});
|
||||
}
|
||||
|
||||
async function abrufen(hostId: string) {
|
||||
const vorgang = await prisma.vorgang.create({ data: { art: "HOSTKEY_ABRUFEN", hostId } });
|
||||
await vorgangAusfuehren(vorgang.id, HANDLER);
|
||||
return prisma.vorgang.findUniqueOrThrow({ where: { id: vorgang.id } });
|
||||
}
|
||||
|
||||
const passwort = () => ({ benutzer: "root", passwort: rootPasswort() });
|
||||
|
||||
describe("Host-Key (TOFU)", () => {
|
||||
it("ruft den Key ab, ohne ihm zu vertrauen", async () => {
|
||||
const host = await hostAnlegen();
|
||||
const vorgang = await abrufen(host.id);
|
||||
expect(vorgang.status).toBe("ERLEDIGT");
|
||||
const keys = await prisma.hostSchluessel.findMany({ where: { hostId: host.id } });
|
||||
expect(keys).toHaveLength(1);
|
||||
expect(keys[0]).toMatchObject({ status: "AUSSTEHEND", algorithmus: "ssh-ed25519" });
|
||||
expect(keys[0]?.fingerprint).toBe(await hostKeyFingerprint(zielA));
|
||||
// Ohne Bestaetigung keine Verbindung.
|
||||
await expect(hostVerbinden(host, passwort())).rejects.toThrow(/noch kein Host-Key bestätigt/);
|
||||
});
|
||||
|
||||
it("verbindet nach Bestaetigung und verweigert nach einem Host-Key-Wechsel", async () => {
|
||||
const host = await hostAnlegen();
|
||||
await abrufen(host.id);
|
||||
const [key] = await prisma.hostSchluessel.findMany({ where: { hostId: host.id } });
|
||||
await hostKeyEntscheiden({ hostSchluesselId: key!.id, vertrauen: true }, AKTEUR);
|
||||
|
||||
const verbindung = await hostVerbinden(host, passwort());
|
||||
expect((await befehlAusfuehren(verbindung, "id -un")).stdout.trim()).toBe("root");
|
||||
verbindung.end();
|
||||
|
||||
await hostKeysTauschen(zielA);
|
||||
await expect(hostVerbinden(host, passwort())).rejects.toBeInstanceOf(HostKeyGeaendertFehler);
|
||||
|
||||
const abweichung = await prisma.abweichung.findFirst({
|
||||
where: { hostId: host.id, art: "HOSTKEY_GEAENDERT" },
|
||||
});
|
||||
expect(abweichung?.behobenAm).toBeNull();
|
||||
const neu = await prisma.hostSchluessel.findFirst({ where: { hostId: host.id, status: "AUSSTEHEND" } });
|
||||
expect(neu?.fingerprint).toBe(await hostKeyFingerprint(zielA));
|
||||
expect((await prisma.host.findUniqueOrThrow({ where: { id: host.id } })).driftZustand).toBe("DRIFT");
|
||||
|
||||
// Ersetzen nur ausdruecklich.
|
||||
await expect(hostKeyEntscheiden({ hostSchluesselId: neu!.id, vertrauen: true }, AKTEUR)).rejects.toThrow(
|
||||
/ausdrücklich/,
|
||||
);
|
||||
await hostKeyEntscheiden({ hostSchluesselId: neu!.id, vertrauen: true, ersetzen: true }, AKTEUR);
|
||||
const wieder = await hostVerbinden(host, passwort());
|
||||
wieder.end();
|
||||
expect(
|
||||
await prisma.abweichung.count({
|
||||
where: { hostId: host.id, art: "HOSTKEY_GEAENDERT", behobenAm: null },
|
||||
}),
|
||||
).toBe(0);
|
||||
const aktionen = (await prisma.protokoll.findMany()).map((e) => e.aktion);
|
||||
expect(aktionen).toEqual(
|
||||
expect.arrayContaining([
|
||||
"hostkey.abgerufen",
|
||||
"hostkey.bestaetigt",
|
||||
"hostkey.abweichung",
|
||||
"hostkey.ersetzt",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("meldet einen nicht erreichbaren Host verstaendlich", async () => {
|
||||
const host = await prisma.host.create({ data: { name: "weg", adresse: "127.0.0.1", port: 1 } });
|
||||
const vorgang = await abrufen(host.id);
|
||||
expect(vorgang.status).toBe("FEHLGESCHLAGEN");
|
||||
expect(vorgang.fehler).toMatch(/lehnt die Verbindung ab/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user