From 1eee053ff0102e4f0ca0340257b5ac99dc5e92e2 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 14 Aug 2026 16:22:27 -0400 Subject: [PATCH] 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 --- docs/typescript_port_compatibility_matrix.md | 4 +- js/packages/cli/src/program.ts | 28 +++++++ js/packages/cli/src/secrets.ts | 10 ++- js/packages/core/src/derive/ssh.ts | 76 +++++++++++++++++++ js/packages/core/src/index.ts | 6 ++ js/packages/core/test/ssh.test.ts | 38 ++++++++++ .../test-vectors/fixtures/manifest.json | 3 +- .../test-vectors/fixtures/ssh_keys.json | 48 ++++++++++++ js/packages/test-vectors/src/index.ts | 11 +++ scripts/cross_impl_check.py | 23 +++++- scripts/generate_ts_port_fixtures.py | 31 ++++++++ 11 files changed, 271 insertions(+), 7 deletions(-) create mode 100644 js/packages/core/src/derive/ssh.ts create mode 100644 js/packages/core/test/ssh.test.ts create mode 100644 js/packages/test-vectors/fixtures/ssh_keys.json diff --git a/docs/typescript_port_compatibility_matrix.md b/docs/typescript_port_compatibility_matrix.md index d3d8eb3d..2e3a8682 100644 --- a/docs/typescript_port_compatibility_matrix.md +++ b/docs/typescript_port_compatibility_matrix.md @@ -30,7 +30,7 @@ until a later milestone. | Password KDF (PBKDF2-SHA256) | P0 | green | `password_kdf.json` | NFKD+strip normalization incl. a unicode/whitespace case; 50k and 100k iterations; fingerprint-derived salt. | | Password KDF (Argon2id) | P0 | green | `password_kdf.json` | `@noble/hashes` argon2id (pure JS, audited family); default (t=2, m=64MiB, p=8) and light params verified. | | KDF metadata parsing | P0 | green | `kdf_metadata.json`, `legacy_payloads.json` | KdfConfig schema with Python-default fallbacks. | -| SSH key derivation | P1 | todo | — | Ed25519 from BIP-85 app 32; PEM serialization parity risk. | +| SSH key derivation | P1 | green | `ssh_keys.json` | Ed25519 from BIP-85 app 32; PKCS#8 and SubjectPublicKeyInfo PEM match Python `cryptography` byte-for-byte (fixed DER prefixes, no ASN.1 dependency). CLI: `entry add ssh`, reveal (private PEM), `entry ssh-public` (PEM or OpenSSH line — the OpenSSH format is a TS-only convenience Python does not emit). Cross-impl phases A/B include an ssh entry. | | PGP key derivation | P1/P2 | todo | — | Highest parity risk (PGPy serialization); import/roundtrip first. | | Nostr snapshot chunk/manifest model | P0 | green | `nostr_snapshot.json` | Kinds 30070/30071/30072; gzip chunking, chunk-hash verify, reassembly of Python-produced chunks, manifest JSON parse, manifest id HMAC (key_index chain). Compressed bytes are deliberately not byte-pinned (deflate encoders differ); cross-decompression verified both ways. | | Relay transport + sync flows | P0 | green | — (live mock relay) | RelayPool over platform WebSocket (multi-relay publish/fetch, dedupe, signature verification on ingest); publish/fetch snapshot with chunk-hash verify, delta publish/replay. End-to-end tested against an in-process NIP-01 relay incl. corrupted-chunk rejection, and at the CLI level: sync -> destroy local vault -> restore. jsdom skips transport tests (real-browser run: Milestone 6 CI). | @@ -51,7 +51,7 @@ until a later milestone. | Environment | Status | |---|---| -| Node 22 (vitest) | green — 162/162 core + 44 CLI | +| Node 22 (vitest) | green — 170/170 core + 44 CLI | | jsdom browser-like env | green — 148/148 core (3 transport tests Node-only) | | Real Chromium/Firefox (vitest browser mode) | todo — lands with Milestone 6 web app CI | diff --git a/js/packages/cli/src/program.ts b/js/packages/cli/src/program.ts index ab71a07c..7c74109f 100644 --- a/js/packages/cli/src/program.ts +++ b/js/packages/cli/src/program.ts @@ -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