fix(ssh): nur ed25519-Schlüssel speichern, die ssh2 lesen kann

ssh2.utils.generateKeyPairSync liefert in etwa 4 von 1000 Fällen einen privaten Schlüssel, an dem der eigene Parser scheitert. Ein so erzeugter Server-Schlüssel wäre dauerhaft unbrauchbar gewesen; in den Tests war es die Ursache des gelegentlich roten SSH-Tests.

Refs #014

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 16:39:54 +02:00
co-authored by Claude Opus 5.5
parent 543fc1c8b4
commit 8dca30e254
7 changed files with 52 additions and 15 deletions
+3 -4
View File
@@ -1,11 +1,10 @@
import { utils } from "ssh2";
import { afterEach, describe, expect, it } from "vitest";
import { ProxmoxClient } from "@/engine/proxmox/client";
import { erkunden } from "@/engine/proxmox/erkundung";
import { sshAusfuehren, SshFehler } from "@/engine/ssh/verbindung";
import { MOCK_TOKEN_GEHEIMNIS, MOCK_TOKEN_ID, ProxmoxMock } from "../proxmox-mock/mock";
import { szenarioEinrichten, type Szenario } from "../proxmox-mock/szenario";
import { SshMock } from "../ssh-mock";
import { SshMock, testPaar } from "../ssh-mock";
const aufraeumen: Array<() => Promise<void>> = [];
afterEach(async () => {
@@ -102,7 +101,7 @@ describe("Verbindungstest (Erkundung)", () => {
});
describe("SSH", () => {
const paar = utils.generateKeyPairSync("ed25519");
const paar = testPaar();
async function sshServer() {
const ssh = await new SshMock(paar.public).starten();
@@ -147,7 +146,7 @@ describe("SSH", () => {
it("meldet einen nicht hinterlegten Schlüssel als Anmeldefehler mit Hinweis", async () => {
const ssh = await sshServer();
const fremd = utils.generateKeyPairSync("ed25519");
const fremd = testPaar();
const fehler = (await sshAusfuehren(
{
host: "127.0.0.1",
+13
View File
@@ -0,0 +1,13 @@
import { utils } from "ssh2";
import { describe, expect, it } from "vitest";
import { ed25519Erzeugen } from "@/engine/ssh/schluessel";
describe("ed25519Erzeugen", () => {
it("liefert nur Schlüssel, die ssh2 selbst lesen kann (roh scheitern etwa 4 von 1000)", () => {
for (let i = 0; i < 1500; i++) {
const paar = ed25519Erzeugen("test");
expect(utils.parseKey(paar.privat)).not.toBeInstanceOf(Error);
expect(paar.oeffentlich).toMatch(/^ssh-ed25519 \S+ test$/);
}
});
});
+8 -1
View File
@@ -1,5 +1,6 @@
import type { AddressInfo } from "node:net";
import { Server, utils } from "ssh2";
import { ed25519Erzeugen } from "@/engine/ssh/schluessel";
import { hostSchluesselFingerabdruck } from "@/engine/ssh/verbindung";
/**
@@ -25,7 +26,7 @@ export class SshMock {
async starten(): Promise<this> {
// ssh2 liest ed25519 nur im OpenSSH-Format, nicht als PKCS8.
const hostPem = utils.generateKeyPairSync("ed25519").private;
const hostPem = ed25519Erzeugen().privat;
const host = utils.parseKey(hostPem);
if (host instanceof Error) throw host;
this.hostSchluessel = hostSchluesselFingerabdruck((Array.isArray(host) ? host[0]! : host).getPublicSSH());
@@ -64,3 +65,9 @@ export class SshMock {
await new Promise<void>((erfuellen) => this.server?.close(() => erfuellen()) ?? erfuellen());
}
}
/** Schlüsselpaar für Tests im Format von ssh2 (`public`/`private`) – immer lesbar. */
export function testPaar(): { public: string; private: string } {
const paar = ed25519Erzeugen();
return { public: paar.oeffentlich, private: paar.privat };
}
+2 -3
View File
@@ -1,16 +1,15 @@
import { utils } from "ssh2";
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { POST as hostSchluesselPost } from "@/app/api/v1/server/[name]/ssh/hostschluessel/route";
import { POST as testPost } from "@/app/api/v1/server/[name]/test/route";
import { verschluesseln } from "@/lib/geheimnisse";
import { MOCK_TOKEN_GEHEIMNIS, MOCK_TOKEN_ID, ProxmoxMock } from "./proxmox-mock/mock";
import { szenarioEinrichten } from "./proxmox-mock/szenario";
import { SshMock } from "./ssh-mock";
import { SshMock, testPaar } from "./ssh-mock";
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
import { angemeldet, anfrage, kontext } from "./helpers/anfragen";
const mock = new ProxmoxMock();
const paar = utils.generateKeyPairSync("ed25519");
const paar = testPaar();
let ssh: SshMock;
describe.runIf(datenbankVerfuegbar)("Verbindungstest über die API", () => {