Files
SeedPass/js/packages/core/test/ssh.test.ts
T
thePR0M3TH3ANandClaude Fable 5 1eee053ff0 parity: SSH key derivation (Ed25519, PEM byte-for-byte)
- core/derive/ssh.ts: BIP-85 app 32 entropy used directly as the Ed25519
  private key, serialized as PKCS#8 and SubjectPublicKeyInfo PEM. Both DER
  shapes are fixed for Ed25519, so they are emitted from literal prefixes
  rather than adding an ASN.1 dependency to the crypto path.
- Output matches Python's `cryptography` byte-for-byte across two seeds and
  three indices, including PEM line wrapping.
- CLI: `entry add ssh`, reveal (private key PEM), and `entry ssh-public`
  which prints the public key without touching secret material. The
  OpenSSH authorized_keys line is a TS-only convenience -- Python emits
  only PEM, which ssh(1) cannot consume directly -- and is marked as such
  in the compatibility matrix.
- Cross-impl phases A and B now include an ssh entry in both directions.

One harness fix: PEM values carry a trailing newline that the CLI capture
strips, so the cross-impl comparison normalizes trailing whitespace. The
unit tests still compare exactly.

170 core + 46 CLI tests green; 16/16 cross-implementation checks green.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-14 16:22:27 -04:00

39 lines
1.4 KiB
TypeScript

/** SSH key derivation parity: PEM output must match Python byte-for-byte. */
import { describe, expect, it } from "vitest";
import { sshKeyCases, mnemonics } from "@seedpass/test-vectors";
import {
deriveSshKeyPair,
deriveSshEntropy,
sshPublicKeyOpenSsh,
Bip85,
bytesToHex,
} from "@seedpass/core";
describe("SSH key derivation", () => {
it.each(sshKeyCases)("$mnemonic_id index $index", (c) => {
const mnemonic = mnemonics[c.mnemonic_id]!;
const pair = deriveSshKeyPair(mnemonic, c.index);
expect(bytesToHex(pair.privateKey)).toBe(c.entropy_hex);
expect(pair.privateKeyPem).toBe(c.private_key_pem);
expect(pair.publicKeyPem).toBe(c.public_key_pem);
});
it("uses BIP-85 app 32 for its entropy", () => {
const c = sshKeyCases[0]!;
const bip85 = Bip85.fromMnemonic(mnemonics[c.mnemonic_id]!);
expect(bytesToHex(deriveSshEntropy(bip85, c.index))).toBe(c.entropy_hex);
});
it("emits an OpenSSH public key line (TS-only convenience)", () => {
const pair = deriveSshKeyPair(mnemonics["abandon12"]!, 0);
const line = sshPublicKeyOpenSsh(pair.publicKey, "seedpass");
expect(line).toMatch(/^ssh-ed25519 [A-Za-z0-9+/=]+ seedpass$/);
// The blob embeds the same 32-byte key the PEM carries
const blob = Buffer.from(line.split(" ")[1]!, "base64");
expect(blob.subarray(blob.length - 32).toString("hex")).toBe(
bytesToHex(pair.publicKey),
);
});
});