import { execFileSync } from "node:child_process"; import { existsSync } from "node:fs"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { type ErzeugbarerSchluesseltyp, privateKeyLesen, publicKeyZeileLesen, schluesselpaarErzeugen, } from "@/lib/ssh/openssh"; import { ecdsaBlob, ed25519Blob, rsaBlob, skEcdsaBlob, skEd25519Blob, zeile } from "./hilfen"; /** * Gegenprobe mit dem echten OpenSSH (#006). `ssh-keygen` kommt nur hier vor, nie im * Produktivcode – dort waeren dafuer Private Keys als Dateien noetig. */ const SSH_KEYGEN = "/usr/bin/ssh-keygen"; const PASSPHRASE = "geheim und lang genug"; let verzeichnis: string; let zaehler = 0; function sshKeygen(...argumente: string[]): string { // stdin geschlossen: Fragt ssh-keygen doch einmal nach etwas, scheitert der Test, statt zu haengen. return execFileSync(SSH_KEYGEN, argumente, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); } async function dateiSchreiben(inhalt: string): Promise { const pfad = join(verzeichnis, `schluessel-${zaehler++}`); await writeFile(pfad, inhalt, { mode: 0o600 }); return pfad; } /** "256 SHA256:… kommentar (ED25519)" → "SHA256:…" */ function fingerprintAusAusgabe(ausgabe: string): string { return ausgabe.trim().split(/\s+/)[1]!; } beforeAll(async () => { verzeichnis = await mkdtemp(join(tmpdir(), "sshkv-test-")); }); afterAll(async () => { await rm(verzeichnis, { recursive: true, force: true }); }); describe.skipIf(!existsSync(SSH_KEYGEN))("Kompatibilität mit ssh-keygen", () => { describe.each([ ["ED25519", undefined], ["ED25519", PASSPHRASE], ["RSA4096", undefined], ["RSA4096", PASSPHRASE], ] as [ErzeugbarerSchluesseltyp, string | undefined][])( "selbst erzeugt: %s, Passphrase %s", (typ, passphrase) => { it("ssh-keygen -y liest den Private Key, -lf liefert denselben Fingerprint", async () => { const paar = await schluesselpaarErzeugen(typ, { kommentar: "sshkv:k_test pruefung", passphrase }); const pfad = await dateiSchreiben(paar.privateKeyOpenssh); const oeffentlich = sshKeygen("-y", "-P", passphrase ?? "", "-f", pfad).trim(); expect(oeffentlich).toBe(paar.publicKeyZeile); const pfadPublic = await dateiSchreiben(paar.publicKeyZeile + "\n"); expect(fingerprintAusAusgabe(sshKeygen("-lf", pfadPublic))).toBe(paar.fingerprint); // -lf auf den Private Key liest bei verschluesselten Dateien nur den Public-Teil. expect(fingerprintAusAusgabe(sshKeygen("-lf", pfad))).toBe(paar.fingerprint); }, 30_000); if (passphrase) { it("ssh-keygen lehnt eine falsche Passphrase ab", async () => { const paar = await schluesselpaarErzeugen(typ, { kommentar: "x", passphrase }); const pfad = await dateiSchreiben(paar.privateKeyOpenssh); expect(() => sshKeygen("-y", "-P", "falsch", "-f", pfad)).toThrow(); }, 30_000); } }, ); describe.each([ ["ed25519", [], ""], ["ed25519", [], PASSPHRASE], ["rsa", ["-b", "2048"], ""], ["rsa", ["-b", "2048"], PASSPHRASE], ] as [string, string[], string][])( "von ssh-keygen erzeugt: %s, Passphrase „%s“", (typ, extra, passphrase) => { it("wird gelesen, Public Key und Fingerprint stimmen", async () => { const pfad = join(verzeichnis, `fremd-${zaehler++}`); sshKeygen("-q", "-t", typ, ...extra, "-N", passphrase, "-C", "fremd@rechner", "-f", pfad); const privat = await readFile(pfad, "utf8"); const publicZeile = (await readFile(`${pfad}.pub`, "utf8")).trim(); const gelesen = privateKeyLesen(privat, passphrase || undefined); expect(gelesen.publicKeyZeile).toBe(publicZeile); expect(gelesen.kommentar).toBe("fremd@rechner"); expect(gelesen.verschluesselt).toBe(passphrase !== ""); expect(gelesen.fingerprint).toBe(fingerprintAusAusgabe(sshKeygen("-lf", `${pfad}.pub`))); expect(publicKeyZeileLesen(publicZeile).fingerprint).toBe(gelesen.fingerprint); if (passphrase) { expect(() => privateKeyLesen(privat, "falsch")).toThrow("Die Passphrase ist falsch."); } }, 30_000); }, ); it("liest von ssh-keygen erzeugte ECDSA-Public-Keys", async () => { for (const bits of ["256", "384", "521"]) { const pfad = join(verzeichnis, `ecdsa-${bits}`); sshKeygen("-q", "-t", "ecdsa", "-b", bits, "-N", "", "-C", "e", "-f", pfad); const publicZeile = await readFile(`${pfad}.pub`, "utf8"); const info = publicKeyZeileLesen(publicZeile); expect(info.typ).toBe(`ecdsa-sha2-nistp${bits}`); expect(info.fingerprint).toBe(fingerprintAusAusgabe(sshKeygen("-lf", `${pfad}.pub`))); } }); it.each([ ["ssh-ed25519", ed25519Blob], ["ssh-rsa", () => rsaBlob(2048)], ["ecdsa-sha2-nistp256", () => ecdsaBlob("nistp256")], ["sk-ssh-ed25519@openssh.com", skEd25519Blob], ["sk-ecdsa-sha2-nistp256@openssh.com", skEcdsaBlob], ] as const)("Fingerprint von %s stimmt mit ssh-keygen -lf überein", async (typ, blobBauen) => { const text = zeile(typ, blobBauen()); const pfad = await dateiSchreiben(text + "\n"); expect(publicKeyZeileLesen(text).fingerprint).toBe(fingerprintAusAusgabe(sshKeygen("-lf", pfad))); }); });