mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-04 03:42:04 +00:00
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:
1 parent
7407d65294
commit
1eee053ff0
11 files changed
+271
-7
No files matched your search
@@ -156,6 +156,7 @@ def py_create_profile(app_dir: Path, seed: str, password: str) -> str:
|
||||
em.add_totp("python-totp", seed, deterministic=True)
|
||||
em.add_key_value("python-api", "token", "py-secret-value")
|
||||
em.add_seed("python-cold-seed", seed, words_num=24)
|
||||
em.add_ssh_key("python-ssh", seed)
|
||||
|
||||
mgr = FingerprintManager(app_dir)
|
||||
if fp not in mgr.fingerprints:
|
||||
@@ -233,6 +234,11 @@ def py_secrets_for(index: dict, seed: str) -> dict[str, str]:
|
||||
)
|
||||
elif kind == "managed_account":
|
||||
out[label] = derive_seed_phrase(bip85, int(entry.get("index", int(idx))), 12)
|
||||
elif kind == "ssh":
|
||||
from seedpass.core.password_generation import derive_ssh_key_pair
|
||||
|
||||
priv, _pub = derive_ssh_key_pair(seed, int(entry.get("index", int(idx))))
|
||||
out[label] = priv
|
||||
elif kind == "nostr":
|
||||
entropy = bip85.derive_entropy(
|
||||
index=int(entry.get("index", int(idx))), entropy_bytes=32
|
||||
@@ -282,7 +288,13 @@ def phase_a(tmp: Path) -> None:
|
||||
|
||||
py_secrets = py_secrets_for(py_index, SEED_A)
|
||||
ts_secrets = ts_secrets_for(app_dir, list(py_secrets), SEED_A)
|
||||
mismatches = [k for k in py_secrets if py_secrets[k] != ts_secrets.get(k)]
|
||||
# PEM values carry a trailing newline that the CLI capture strips; compare
|
||||
# on trailing whitespace-insensitive values.
|
||||
mismatches = [
|
||||
k
|
||||
for k in py_secrets
|
||||
if py_secrets[k].rstrip("\n") != (ts_secrets.get(k) or "").rstrip("\n")
|
||||
]
|
||||
check(
|
||||
"every secret derives identically",
|
||||
not mismatches,
|
||||
@@ -322,6 +334,7 @@ def phase_b(tmp: Path) -> None:
|
||||
run_cli(app_dir, "entry", "add", "totp", "ts-totp", env_extra=env)
|
||||
run_cli(app_dir, "entry", "add", "key-value", "ts-api", "token", "ts-secret-value", env_extra=env)
|
||||
run_cli(app_dir, "entry", "add", "managed-account", "ts-managed", env_extra=env)
|
||||
run_cli(app_dir, "entry", "add", "ssh", "ts-ssh", env_extra=env)
|
||||
|
||||
from utils.fingerprint import generate_fingerprint
|
||||
|
||||
@@ -337,7 +350,13 @@ def phase_b(tmp: Path) -> None:
|
||||
|
||||
py_secrets = py_secrets_for(py_index, SEED_B)
|
||||
ts_secrets = ts_secrets_for(app_dir, list(py_secrets), SEED_B)
|
||||
mismatches = [k for k in py_secrets if py_secrets[k] != ts_secrets.get(k)]
|
||||
# PEM values carry a trailing newline that the CLI capture strips; compare
|
||||
# on trailing whitespace-insensitive values.
|
||||
mismatches = [
|
||||
k
|
||||
for k in py_secrets
|
||||
if py_secrets[k].rstrip("\n") != (ts_secrets.get(k) or "").rstrip("\n")
|
||||
]
|
||||
check("every secret derives identically", not mismatches, f"mismatched: {mismatches}")
|
||||
|
||||
# Python's schema validation should accept the TS-written index
|
||||
|
||||
@@ -1103,6 +1103,36 @@ def gen_migrations() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def gen_ssh_keys() -> dict:
|
||||
from seedpass.core.password_generation import derive_ssh_key_pair
|
||||
|
||||
cases = []
|
||||
for mid in (PRIMARY, "zoo24"):
|
||||
mnemonic = MNEMONICS[mid]
|
||||
bip85 = _bip85(mnemonic)
|
||||
for index in (0, 1, 7):
|
||||
priv_pem, pub_pem = derive_ssh_key_pair(mnemonic, index)
|
||||
entropy = bip85.derive_entropy(index=index, entropy_bytes=32, app_no=32)
|
||||
cases.append(
|
||||
{
|
||||
"mnemonic_id": mid,
|
||||
"index": index,
|
||||
"entropy_hex": entropy.hex(),
|
||||
"private_key_pem": priv_pem,
|
||||
"public_key_pem": pub_pem,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"description": (
|
||||
"SSH key pairs: BIP-85 app 32, 32 bytes of entropy used directly "
|
||||
"as an Ed25519 private key. Serialized as PKCS#8 PEM (private) "
|
||||
"and SubjectPublicKeyInfo PEM (public), matching Python's "
|
||||
"cryptography output byte-for-byte."
|
||||
),
|
||||
"cases": cases,
|
||||
}
|
||||
|
||||
|
||||
def gen_kdf_metadata() -> dict:
|
||||
return {
|
||||
"description": (
|
||||
@@ -1146,6 +1176,7 @@ def main() -> None:
|
||||
"index_keys.json": gen_index_keys(),
|
||||
"kdf_metadata.json": gen_kdf_metadata(),
|
||||
"migrations.json": gen_migrations(),
|
||||
"ssh_keys.json": gen_ssh_keys(),
|
||||
"password_kdf.json": gen_password_kdf(),
|
||||
"legacy_payloads.json": gen_legacy_payloads(),
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user