feat: OpenSSH-Schluessel erzeugen, schreiben und lesen (#006)
Private Keys sollen nie als Datei auf dem sshkv-Server liegen, auch nicht kurz. ssh-keygen braucht aber genau das. Deshalb erzeugt Node crypto die Schluessel (ed25519, RSA-4096), und ein eigener Serializer schreibt das Format openssh-key-v1 – unverschluesselt oder mit aes256-ctr und bcrypt_pbkdf (16 Runden, 16 Byte Salt), Checkint und Padding wie OpenSSH. Der Parser liest auch von ssh-keygen erzeugte Dateien, prueft, dass Private und Public Key zusammengehoeren, und meldet fehlende oder falsche Passphrasen ueber eigene Codes. Fehlermeldungen sind feste Saetze ohne Eingabe darin, damit kein Schluesselmaterial in Logs geraet. RSA unter 2048 Bit wird beim Import abgelehnt; wer nur beobachtet, kann die Grenze ausschalten. Die Tests pruefen beide Richtungen gegen das echte ssh-keygen (-y, -lf). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { generateKeyPairSync } from "node:crypto";
|
||||
|
||||
/** Kleine Wire-Format-Helfer, um gezielt kaputte oder seltene Blobs zu bauen (#006). */
|
||||
export function sshString(inhalt: Buffer | string): Buffer {
|
||||
const daten = typeof inhalt === "string" ? Buffer.from(inhalt) : inhalt;
|
||||
const laenge = Buffer.alloc(4);
|
||||
laenge.writeUInt32BE(daten.length);
|
||||
return Buffer.concat([laenge, daten]);
|
||||
}
|
||||
|
||||
export function mpint(betrag: Buffer): Buffer {
|
||||
return sshString((betrag[0]! & 0x80) !== 0 ? Buffer.concat([Buffer.from([0]), betrag]) : betrag);
|
||||
}
|
||||
|
||||
export function zeile(typ: string, blob: Buffer, kommentar = "test"): string {
|
||||
return `${typ} ${blob.toString("base64")} ${kommentar}`;
|
||||
}
|
||||
|
||||
export function rsaBlob(bits: number): Buffer {
|
||||
const { publicKey } = generateKeyPairSync("rsa", { modulusLength: bits });
|
||||
const jwk = publicKey.export({ format: "jwk" });
|
||||
return Buffer.concat([
|
||||
sshString("ssh-rsa"),
|
||||
mpint(Buffer.from(jwk.e!, "base64url")),
|
||||
mpint(Buffer.from(jwk.n!, "base64url")),
|
||||
]);
|
||||
}
|
||||
|
||||
const KURVEN = { nistp256: "P-256", nistp384: "P-384", nistp521: "P-521" } as const;
|
||||
|
||||
export function ecdsaPunkt(kurve: keyof typeof KURVEN): Buffer {
|
||||
const { publicKey } = generateKeyPairSync("ec", { namedCurve: KURVEN[kurve] });
|
||||
const jwk = publicKey.export({ format: "jwk" });
|
||||
return Buffer.concat([
|
||||
Buffer.from([4]),
|
||||
Buffer.from(jwk.x!, "base64url"),
|
||||
Buffer.from(jwk.y!, "base64url"),
|
||||
]);
|
||||
}
|
||||
|
||||
export function ecdsaBlob(kurve: keyof typeof KURVEN): Buffer {
|
||||
return Buffer.concat([sshString(`ecdsa-sha2-${kurve}`), sshString(kurve), sshString(ecdsaPunkt(kurve))]);
|
||||
}
|
||||
|
||||
export function ed25519Roh(): Buffer {
|
||||
const { publicKey } = generateKeyPairSync("ed25519");
|
||||
return Buffer.from(publicKey.export({ format: "jwk" }).x!, "base64url");
|
||||
}
|
||||
|
||||
export function ed25519Blob(): Buffer {
|
||||
return Buffer.concat([sshString("ssh-ed25519"), sshString(ed25519Roh())]);
|
||||
}
|
||||
|
||||
export function skEd25519Blob(): Buffer {
|
||||
return Buffer.concat([sshString("sk-ssh-ed25519@openssh.com"), sshString(ed25519Roh()), sshString("ssh:")]);
|
||||
}
|
||||
|
||||
export function skEcdsaBlob(): Buffer {
|
||||
return Buffer.concat([
|
||||
sshString("sk-ecdsa-sha2-nistp256@openssh.com"),
|
||||
sshString("nistp256"),
|
||||
sshString(ecdsaPunkt("nistp256")),
|
||||
sshString("ssh:"),
|
||||
]);
|
||||
}
|
||||
|
||||
/** Base64-Rumpf eines PEM-Textes, um Fehlermeldungen auf Schluesselmaterial zu pruefen. */
|
||||
export function pemRumpf(pem: string): string {
|
||||
return pem
|
||||
.split("\n")
|
||||
.filter((z) => z && !z.startsWith("-----"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
/** Ob `text` irgendein 16-Zeichen-Stueck aus `material` enthaelt. */
|
||||
export function enthaeltMaterial(text: string, material: string): boolean {
|
||||
for (let i = 0; i + 16 <= material.length; i += 4) {
|
||||
if (text.includes(material.slice(i, i + 16))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
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<string> {
|
||||
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)));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,227 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
bitsVonRsa,
|
||||
type ErzeugtesSchluesselpaar,
|
||||
fingerprintSha256,
|
||||
privateKeyLesen,
|
||||
publicKeyZeileLesen,
|
||||
SchluesselFormatFehler,
|
||||
schluesselpaarErzeugen,
|
||||
} from "@/lib/ssh/openssh";
|
||||
import {
|
||||
ecdsaBlob,
|
||||
ed25519Blob,
|
||||
enthaeltMaterial,
|
||||
pemRumpf,
|
||||
rsaBlob,
|
||||
skEcdsaBlob,
|
||||
skEd25519Blob,
|
||||
sshString,
|
||||
zeile,
|
||||
} from "./hilfen";
|
||||
|
||||
const PASSPHRASE = "korrekt Pferd Batterie Heftklammer";
|
||||
|
||||
function fehlerVon(aufruf: () => unknown): SchluesselFormatFehler {
|
||||
try {
|
||||
aufruf();
|
||||
} catch (fehler) {
|
||||
expect(fehler).toBeInstanceOf(SchluesselFormatFehler);
|
||||
return fehler as SchluesselFormatFehler;
|
||||
}
|
||||
throw new Error("Es wurde ein Fehler erwartet.");
|
||||
}
|
||||
|
||||
// RSA-4096 kostet je Erzeugung bis zu einige Sekunden; einmal je Variante reicht.
|
||||
const paare: Record<string, ErzeugtesSchluesselpaar> = {};
|
||||
|
||||
beforeAll(async () => {
|
||||
const [ed, edPass, rsa, rsaPass] = await Promise.all([
|
||||
schluesselpaarErzeugen("ED25519", { kommentar: "sshkv:k_1 backup" }),
|
||||
schluesselpaarErzeugen("ED25519", { kommentar: "mit passphrase", passphrase: PASSPHRASE }),
|
||||
schluesselpaarErzeugen("RSA4096", { kommentar: "rsa ohne" }),
|
||||
schluesselpaarErzeugen("RSA4096", { kommentar: "rsa mit", passphrase: PASSPHRASE }),
|
||||
]);
|
||||
Object.assign(paare, { ed, edPass, rsa, rsaPass });
|
||||
}, 60_000);
|
||||
|
||||
describe("schluesselpaarErzeugen und privateKeyLesen", () => {
|
||||
it.each([
|
||||
["ed", "ED25519", 256, false],
|
||||
["edPass", "ED25519", 256, true],
|
||||
["rsa", "RSA", 4096, false],
|
||||
["rsaPass", "RSA", 4096, true],
|
||||
] as const)("Rundreise %s", (name, typ, bits, mitPassphrase) => {
|
||||
const paar = paare[name]!;
|
||||
expect(paar.privateKeyOpenssh.startsWith("-----BEGIN OPENSSH PRIVATE KEY-----\n")).toBe(true);
|
||||
expect(paar.privateKeyOpenssh.endsWith("-----END OPENSSH PRIVATE KEY-----\n")).toBe(true);
|
||||
expect(paar.fingerprint).toMatch(/^SHA256:[A-Za-z0-9+/]{43}$/);
|
||||
|
||||
const gelesen = privateKeyLesen(paar.privateKeyOpenssh, mitPassphrase ? PASSPHRASE : undefined);
|
||||
expect(gelesen).toMatchObject({
|
||||
typ,
|
||||
bits,
|
||||
publicKeyZeile: paar.publicKeyZeile,
|
||||
fingerprint: paar.fingerprint,
|
||||
verschluesselt: mitPassphrase,
|
||||
});
|
||||
|
||||
const oeffentlich = publicKeyZeileLesen(paar.publicKeyZeile);
|
||||
expect(oeffentlich.fingerprint).toBe(paar.fingerprint);
|
||||
expect(oeffentlich.kommentar).toBe(gelesen.kommentar);
|
||||
});
|
||||
|
||||
it("bricht die Base64-Zeilen wie OpenSSH nach 70 Zeichen um", () => {
|
||||
const zeilen = paare["rsa"]!.privateKeyOpenssh.trim().split("\n").slice(1, -1);
|
||||
expect(zeilen.slice(0, -1).every((z) => z.length === 70)).toBe(true);
|
||||
});
|
||||
|
||||
it("verschluesselt mit aes256-ctr und bcrypt", () => {
|
||||
const roh = Buffer.from(pemRumpf(paare["edPass"]!.privateKeyOpenssh), "base64").toString("latin1");
|
||||
expect(roh).toContain("aes256-ctr");
|
||||
expect(roh).toContain("bcrypt");
|
||||
});
|
||||
|
||||
it("erzeugt jedes Mal einen anderen Schluessel", async () => {
|
||||
const a = await schluesselpaarErzeugen("ED25519", { kommentar: "a" });
|
||||
expect(a.fingerprint).not.toBe(paare["ed"]!.fingerprint);
|
||||
});
|
||||
|
||||
it("verträgt CRLF und Leerraum um den Private Key", () => {
|
||||
const text = "\n " + paare["ed"]!.privateKeyOpenssh.replace(/\n/g, "\r\n") + " \n";
|
||||
expect(privateKeyLesen(text).fingerprint).toBe(paare["ed"]!.fingerprint);
|
||||
});
|
||||
|
||||
it("lehnt Steuerzeichen im Kommentar ab", async () => {
|
||||
await expect(schluesselpaarErzeugen("ED25519", { kommentar: "a\nssh-rsa AAAA" })).rejects.toBeInstanceOf(
|
||||
SchluesselFormatFehler,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("privateKeyLesen – Fehlerfälle", () => {
|
||||
it("meldet eine fehlende Passphrase", () => {
|
||||
expect(fehlerVon(() => privateKeyLesen(paare["edPass"]!.privateKeyOpenssh)).code).toBe(
|
||||
"passphrase_fehlt",
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["edPass", "rsaPass"])("meldet eine falsche Passphrase (%s)", (name) => {
|
||||
const fehler = fehlerVon(() => privateKeyLesen(paare[name]!.privateKeyOpenssh, "falsch"));
|
||||
expect(fehler.code).toBe("passphrase_falsch");
|
||||
expect(fehler.message).toBe("Die Passphrase ist falsch.");
|
||||
});
|
||||
|
||||
it("erkennt einen abgeschnittenen Key", () => {
|
||||
const pem = paare["rsa"]!.privateKeyOpenssh;
|
||||
const zeilen = pem.trim().split("\n");
|
||||
const halb = [...zeilen.slice(0, zeilen.length / 2), zeilen.at(-1)!].join("\n");
|
||||
expect(fehlerVon(() => privateKeyLesen(halb)).code).toBe("ungueltig");
|
||||
expect(fehlerVon(() => privateKeyLesen(pem.slice(0, pem.length / 2))).code).toBe("ungueltig");
|
||||
});
|
||||
|
||||
it("erkennt ein vertauschtes Byte im unverschlüsselten Teil", () => {
|
||||
const roh = Buffer.from(pemRumpf(paare["ed"]!.privateKeyOpenssh), "base64");
|
||||
roh[roh.length - 60]! ^= 0x01;
|
||||
const pem = `-----BEGIN OPENSSH PRIVATE KEY-----\n${roh.toString("base64")}\n-----END OPENSSH PRIVATE KEY-----\n`;
|
||||
expect(fehlerVon(() => privateKeyLesen(pem)).code).toBe("ungueltig");
|
||||
});
|
||||
|
||||
it("weist PEM-Formate außer OpenSSH mit eigener Meldung ab", () => {
|
||||
const fehler = fehlerVon(() =>
|
||||
privateKeyLesen("-----BEGIN RSA PRIVATE KEY-----\nAAAA\n-----END RSA PRIVATE KEY-----"),
|
||||
);
|
||||
expect(fehler.code).toBe("nicht_unterstuetzt");
|
||||
});
|
||||
|
||||
it("weist Unsinn ab", () => {
|
||||
expect(fehlerVon(() => privateKeyLesen("hallo")).code).toBe("ungueltig");
|
||||
});
|
||||
|
||||
it("enthält in keiner Fehlermeldung Schlüsselmaterial oder die Passphrase", () => {
|
||||
const pem = paare["rsaPass"]!.privateKeyOpenssh;
|
||||
const rumpf = pemRumpf(pem);
|
||||
const meldungen = [
|
||||
fehlerVon(() => privateKeyLesen(pem, "falsch")).message,
|
||||
fehlerVon(() => privateKeyLesen(pem)).message,
|
||||
fehlerVon(() => privateKeyLesen(pem.slice(0, 400))).message,
|
||||
fehlerVon(() => privateKeyLesen(pem.replace("A", "!"))).message,
|
||||
fehlerVon(() => privateKeyLesen(pem.replace("OPENSSH", "RSA"))).message,
|
||||
];
|
||||
for (const meldung of meldungen) {
|
||||
expect(enthaeltMaterial(meldung, rumpf)).toBe(false);
|
||||
expect(meldung).not.toContain(PASSPHRASE);
|
||||
expect(meldung).not.toContain("-----BEGIN");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("publicKeyZeileLesen", () => {
|
||||
it.each([
|
||||
["ssh-ed25519", ed25519Blob, 256],
|
||||
["ssh-rsa", () => rsaBlob(2048), 2048],
|
||||
["ecdsa-sha2-nistp256", () => ecdsaBlob("nistp256"), 256],
|
||||
["ecdsa-sha2-nistp384", () => ecdsaBlob("nistp384"), 384],
|
||||
["ecdsa-sha2-nistp521", () => ecdsaBlob("nistp521"), 521],
|
||||
["sk-ssh-ed25519@openssh.com", skEd25519Blob, 256],
|
||||
["sk-ecdsa-sha2-nistp256@openssh.com", skEcdsaBlob, 256],
|
||||
] as const)("liest %s", (typ, blobBauen, bits) => {
|
||||
const blob = blobBauen();
|
||||
const info = publicKeyZeileLesen(zeile(typ, blob, "jemand@rechner mit leerzeichen"));
|
||||
expect(info).toMatchObject({ typ, kommentar: "jemand@rechner mit leerzeichen", bits });
|
||||
expect(info.blob.equals(blob)).toBe(true);
|
||||
});
|
||||
|
||||
it("akzeptiert Zeilen ohne Kommentar", () => {
|
||||
expect(publicKeyZeileLesen(`ssh-ed25519 ${ed25519Blob().toString("base64")}`).kommentar).toBe("");
|
||||
});
|
||||
|
||||
it("lehnt einen Blob ab, dessen Typ nicht zur Zeile passt", () => {
|
||||
expect(fehlerVon(() => publicKeyZeileLesen(zeile("ssh-rsa", ed25519Blob()))).code).toBe("ungueltig");
|
||||
expect(
|
||||
fehlerVon(() => publicKeyZeileLesen(zeile("ecdsa-sha2-nistp384", ecdsaBlob("nistp256")))).code,
|
||||
).toBe("ungueltig");
|
||||
});
|
||||
|
||||
it("lehnt RSA unter 2048 Bit ab, außer der Aufrufer erlaubt es", () => {
|
||||
const kurz = zeile("ssh-rsa", rsaBlob(1024));
|
||||
const fehler = fehlerVon(() => publicKeyZeileLesen(kurz));
|
||||
expect(fehler.code).toBe("zu_schwach");
|
||||
expect(fehler.message).toContain("2048");
|
||||
expect(publicKeyZeileLesen(kurz, { rsaMindestBits: 0 }).bits).toBe(1024);
|
||||
});
|
||||
|
||||
it("lehnt unbekannte Typen, kaputtes Base64 und angehängte Bytes ab", () => {
|
||||
expect(fehlerVon(() => publicKeyZeileLesen("ssh-dss AAAAB3NzaC1kc3M= x")).code).toBe(
|
||||
"nicht_unterstuetzt",
|
||||
);
|
||||
expect(fehlerVon(() => publicKeyZeileLesen("ssh-ed25519 AAA*AAA x")).code).toBe("ungueltig");
|
||||
expect(fehlerVon(() => publicKeyZeileLesen("ssh-ed25519")).code).toBe("ungueltig");
|
||||
const mitRest = Buffer.concat([ed25519Blob(), sshString("x")]);
|
||||
expect(fehlerVon(() => publicKeyZeileLesen(zeile("ssh-ed25519", mitRest))).code).toBe("ungueltig");
|
||||
const abgeschnitten = ed25519Blob().subarray(0, 30);
|
||||
expect(fehlerVon(() => publicKeyZeileLesen(zeile("ssh-ed25519", abgeschnitten))).code).toBe("ungueltig");
|
||||
});
|
||||
|
||||
it("lehnt Steuerzeichen im Kommentar ab", () => {
|
||||
expect(fehlerVon(() => publicKeyZeileLesen(zeile("ssh-ed25519", ed25519Blob(), "a\u0000b"))).code).toBe(
|
||||
"ungueltig",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fingerprintSha256 und bitsVonRsa", () => {
|
||||
it("ist SHA-256 über den Blob, Base64 ohne Padding", () => {
|
||||
const blob = ed25519Blob();
|
||||
const erwartet = createHash("sha256").update(blob).digest("base64").replace(/=+$/, "");
|
||||
expect(fingerprintSha256(blob)).toBe(`SHA256:${erwartet}`);
|
||||
expect(fingerprintSha256(blob)).not.toContain("=");
|
||||
});
|
||||
|
||||
it("zählt die Bits des RSA-Moduls", () => {
|
||||
expect(bitsVonRsa(rsaBlob(2048))).toBe(2048);
|
||||
expect(bitsVonRsa(rsaBlob(3072))).toBe(3072);
|
||||
expect(() => bitsVonRsa(ed25519Blob())).toThrow(SchluesselFormatFehler);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user