Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
261 lines
7.9 KiB
TypeScript
261 lines
7.9 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import crypto from "node:crypto";
|
|
|
|
import type { KnownHost } from "./models";
|
|
import {
|
|
fingerprintFromPublicKey,
|
|
normalizeKnownHost,
|
|
normalizeKnownHosts,
|
|
upsertKnownHost,
|
|
} from "./knownHosts";
|
|
|
|
const knownHost = (overrides: Partial<KnownHost> = {}): KnownHost => ({
|
|
id: "kh-existing",
|
|
hostname: "10.2.0.32",
|
|
port: 22,
|
|
keyType: "ssh-ed25519",
|
|
publicKey: "ssh-ed25519 old-key",
|
|
fingerprint: "old-fingerprint",
|
|
discoveredAt: 100,
|
|
...overrides,
|
|
});
|
|
|
|
test("upsertKnownHost updates an existing host key instead of appending a duplicate", () => {
|
|
const existing = knownHost({ convertedToHostId: "host-1" });
|
|
const incoming = knownHost({
|
|
id: "kh-new",
|
|
publicKey: "ssh-ed25519 new-key",
|
|
fingerprint: "new-fingerprint",
|
|
discoveredAt: 200,
|
|
});
|
|
|
|
const result = upsertKnownHost([existing], incoming);
|
|
|
|
assert.equal(result.length, 1);
|
|
assert.deepEqual(result[0], {
|
|
...existing,
|
|
publicKey: "ssh-ed25519 new-key",
|
|
fingerprint: "new-fingerprint",
|
|
lastSeen: 200,
|
|
});
|
|
});
|
|
|
|
test("upsertKnownHost updates by id even when the incoming key type is unknown", () => {
|
|
const existing = knownHost({
|
|
id: "kh-1",
|
|
keyType: "ssh-ed25519",
|
|
publicKey: "SHA256:old-key",
|
|
fingerprint: "old-fingerprint",
|
|
discoveredAt: 100,
|
|
});
|
|
const incoming = knownHost({
|
|
id: "kh-1",
|
|
keyType: "unknown",
|
|
publicKey: undefined,
|
|
fingerprint: "new-fingerprint",
|
|
discoveredAt: 200,
|
|
});
|
|
|
|
const result = upsertKnownHost([existing], incoming);
|
|
|
|
assert.equal(result.length, 1);
|
|
assert.equal(result[0].id, "kh-1");
|
|
assert.equal(result[0].keyType, "unknown");
|
|
assert.equal(result[0].fingerprint, "new-fingerprint");
|
|
assert.equal(result[0].lastSeen, 200);
|
|
});
|
|
|
|
test("upsertKnownHost prefers the matching id over an earlier selector match", () => {
|
|
const duplicate = knownHost({
|
|
id: "kh-duplicate",
|
|
fingerprint: "duplicate-fingerprint",
|
|
discoveredAt: 50,
|
|
});
|
|
const target = knownHost({
|
|
id: "kh-target",
|
|
fingerprint: "target-fingerprint",
|
|
discoveredAt: 100,
|
|
});
|
|
const incoming = knownHost({
|
|
id: "kh-target",
|
|
fingerprint: "new-fingerprint",
|
|
discoveredAt: 200,
|
|
});
|
|
|
|
const result = upsertKnownHost([duplicate, target], incoming);
|
|
|
|
assert.equal(result.length, 2);
|
|
assert.equal(result[0].fingerprint, "duplicate-fingerprint");
|
|
assert.equal(result[1].id, "kh-target");
|
|
assert.equal(result[1].fingerprint, "new-fingerprint");
|
|
});
|
|
|
|
test("upsertKnownHost appends genuinely new host keys", () => {
|
|
const existing = knownHost();
|
|
const incoming = knownHost({
|
|
id: "kh-other",
|
|
hostname: "10.2.0.33",
|
|
fingerprint: "other-fingerprint",
|
|
});
|
|
|
|
const result = upsertKnownHost([existing], incoming);
|
|
|
|
assert.deepEqual(result, [existing, incoming]);
|
|
});
|
|
|
|
// --- Fingerprint derivation -------------------------------------------------
|
|
|
|
const makeRawPublicKey = (keyType: string, body = "trusted imported host key") => {
|
|
const type = Buffer.from(keyType);
|
|
const length = Buffer.alloc(4);
|
|
length.writeUInt32BE(type.length, 0);
|
|
return Buffer.concat([length, type, Buffer.from(body)]);
|
|
};
|
|
|
|
test("fingerprintFromPublicKey matches Node's SHA-256 over a base64-decoded OpenSSH line", () => {
|
|
const rawKey = makeRawPublicKey("ssh-ed25519");
|
|
const base64Body = rawKey.toString("base64");
|
|
const expected = crypto.createHash("sha256").update(rawKey).digest("base64").replace(/=+$/g, "");
|
|
|
|
assert.equal(fingerprintFromPublicKey(`ssh-ed25519 ${base64Body}`), expected);
|
|
assert.equal(
|
|
fingerprintFromPublicKey(`ssh-ed25519 ${base64Body} comment-tail`),
|
|
expected,
|
|
"trailing comment is ignored",
|
|
);
|
|
});
|
|
|
|
test("fingerprintFromPublicKey strips a SHA256: prefix and trailing padding", () => {
|
|
assert.equal(fingerprintFromPublicKey("SHA256:abc123=="), "abc123");
|
|
assert.equal(fingerprintFromPublicKey("sha256:abc123"), "abc123");
|
|
});
|
|
|
|
test("fingerprintFromPublicKey returns empty string on missing input", () => {
|
|
assert.equal(fingerprintFromPublicKey(undefined), "");
|
|
assert.equal(fingerprintFromPublicKey(null), "");
|
|
assert.equal(fingerprintFromPublicKey(""), "");
|
|
});
|
|
|
|
// --- Migration --------------------------------------------------------------
|
|
|
|
test("normalizeKnownHost backfills fingerprint when only publicKey is stored", () => {
|
|
const rawKey = makeRawPublicKey("ssh-ed25519");
|
|
const base64Body = rawKey.toString("base64");
|
|
const expected = crypto.createHash("sha256").update(rawKey).digest("base64").replace(/=+$/g, "");
|
|
|
|
const stored: KnownHost = {
|
|
id: "kh-1",
|
|
hostname: "vps-1.example.com",
|
|
port: 22,
|
|
keyType: "ssh-ed25519",
|
|
publicKey: `ssh-ed25519 ${base64Body}`,
|
|
discoveredAt: 1,
|
|
};
|
|
|
|
const migrated = normalizeKnownHost(stored);
|
|
assert.notEqual(migrated, stored, "should return a new object when fingerprint is added");
|
|
assert.equal(migrated.fingerprint, expected);
|
|
assert.equal(migrated.keyType, "ssh-ed25519");
|
|
});
|
|
|
|
test("normalizeKnownHost backfills keyType from an OpenSSH-format publicKey", () => {
|
|
const rawKey = makeRawPublicKey("ssh-rsa");
|
|
const base64Body = rawKey.toString("base64");
|
|
|
|
const stored: KnownHost = {
|
|
id: "kh-1",
|
|
hostname: "vps-1.example.com",
|
|
port: 22,
|
|
keyType: "",
|
|
publicKey: `ssh-rsa ${base64Body}`,
|
|
discoveredAt: 1,
|
|
};
|
|
|
|
const migrated = normalizeKnownHost(stored);
|
|
assert.equal(migrated.keyType, "ssh-rsa");
|
|
});
|
|
|
|
test("normalizeKnownHost returns the same reference when nothing needs backfilling", () => {
|
|
const rawKey = makeRawPublicKey("ssh-ed25519");
|
|
const fp = crypto.createHash("sha256").update(rawKey).digest("base64").replace(/=+$/g, "");
|
|
|
|
const stored: KnownHost = {
|
|
id: "kh-1",
|
|
hostname: "vps-1.example.com",
|
|
port: 22,
|
|
keyType: "ssh-ed25519",
|
|
publicKey: `ssh-ed25519 ${rawKey.toString("base64")}`,
|
|
fingerprint: fp,
|
|
discoveredAt: 1,
|
|
};
|
|
|
|
assert.equal(normalizeKnownHost(stored), stored);
|
|
});
|
|
|
|
test("normalizeKnownHost is a no-op when publicKey is opaque and nothing else is known", () => {
|
|
const stored: KnownHost = {
|
|
id: "kh-1",
|
|
hostname: "vps-1.example.com",
|
|
port: 22,
|
|
keyType: "unknown",
|
|
publicKey: "SHA256:already-just-a-fingerprint",
|
|
discoveredAt: 1,
|
|
};
|
|
|
|
const migrated = normalizeKnownHost(stored);
|
|
// The SHA256: prefix becomes the fingerprint; keyType stays as "unknown" since
|
|
// we cannot recover it from a bare fingerprint.
|
|
assert.equal(migrated.fingerprint, "already-just-a-fingerprint");
|
|
assert.equal(migrated.keyType, "unknown");
|
|
});
|
|
|
|
test("normalizeKnownHosts returns the same array reference when nothing needs migration", () => {
|
|
const rawKey = makeRawPublicKey("ssh-ed25519");
|
|
const fp = crypto.createHash("sha256").update(rawKey).digest("base64").replace(/=+$/g, "");
|
|
|
|
const list: KnownHost[] = [{
|
|
id: "kh-1",
|
|
hostname: "vps-1.example.com",
|
|
port: 22,
|
|
keyType: "ssh-ed25519",
|
|
publicKey: `ssh-ed25519 ${rawKey.toString("base64")}`,
|
|
fingerprint: fp,
|
|
discoveredAt: 1,
|
|
}];
|
|
|
|
assert.equal(normalizeKnownHosts(list), list);
|
|
});
|
|
|
|
test("normalizeKnownHosts migrates each entry that needs backfilling", () => {
|
|
const rawKeyA = makeRawPublicKey("ssh-ed25519", "host-a-key");
|
|
const rawKeyB = makeRawPublicKey("ssh-rsa", "host-b-key");
|
|
const fpA = crypto.createHash("sha256").update(rawKeyA).digest("base64").replace(/=+$/g, "");
|
|
const fpB = crypto.createHash("sha256").update(rawKeyB).digest("base64").replace(/=+$/g, "");
|
|
|
|
const list: KnownHost[] = [
|
|
{
|
|
id: "kh-1",
|
|
hostname: "vps-1.example.com",
|
|
port: 22,
|
|
keyType: "ssh-ed25519",
|
|
publicKey: `ssh-ed25519 ${rawKeyA.toString("base64")}`,
|
|
discoveredAt: 1,
|
|
},
|
|
{
|
|
id: "kh-2",
|
|
hostname: "vps-2.example.com",
|
|
port: 22,
|
|
keyType: "",
|
|
publicKey: `ssh-rsa ${rawKeyB.toString("base64")}`,
|
|
discoveredAt: 2,
|
|
},
|
|
];
|
|
|
|
const migrated = normalizeKnownHosts(list);
|
|
assert.notEqual(migrated, list);
|
|
assert.equal(migrated[0].fingerprint, fpA);
|
|
assert.equal(migrated[1].fingerprint, fpB);
|
|
assert.equal(migrated[1].keyType, "ssh-rsa");
|
|
});
|