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]>
This commit is contained in:
thePR0M3TH3ANandClaude Fable 5 committed 2026-08-14 16:22:27 -04:00
1 parent 7407d65294
commit 1eee053ff0
11 files changed
+271 -7

No files matched your search

+28
View File
@@ -50,6 +50,9 @@ import {
addSeedEntry,
addManagedAccountEntry,
addNostrKeyEntry,
addSshKeyEntry,
deriveSshKeyPair,
sshPublicKeyOpenSsh,
} from "@seedpass/core";
import { readFile, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
@@ -395,6 +398,31 @@ export function buildProgram(io: ProgramIo = defaultIo): Command {
await finishAdd(vault, id);
});
commonAddOptions(
add.command("ssh <label>").description("derived Ed25519 SSH key entry"),
).action(async (label: string, o: { notes?: string; tags?: string[]; archived?: boolean }) => {
const vault = await openFromOptions(program.opts());
const id = addSshKeyEntry(vault.index, label, commonOpts(o));
await finishAdd(vault, id);
});
entry
.command("ssh-public <refOrQuery>")
.description("print an SSH entry's public key (not secret material)")
.option("--format <fmt>", "pem or openssh", "openssh")
.option("--comment <text>", "comment for the openssh format", "")
.action(async (refOrQuery: string, o: { format: string; comment: string }) => {
const vault = await openFromOptions(program.opts());
const hit = resolveEntry(vault.index, refOrQuery);
if (hit.entry.kind !== "ssh") throw new Error(`${hit.ref} is not an ssh entry`);
const pair = deriveSshKeyPair(vault.mnemonic, hit.entry.index);
io.out(
o.format === "pem"
? pair.publicKeyPem.trimEnd()
: sshPublicKeyOpenSsh(pair.publicKey, o.comment || hit.entry.label),
);
});
entry
.command("modify <refOrQuery>")
.description("update entry fields (kind-checked); prints the new metadata")