montana/Монтана-Протокол/Код/crates/mt-examples/examples/m1_mnemonic.rs

667 lines
24 KiB
Rust
Raw Permalink Normal View History

use std::env;
use std::process::ExitCode;
use mt_codec::domain;
use mt_crypto::{
keypair_from_seed, keypair_from_seed_mlkem, sha256_raw, MlkemPublicKey, MlkemSecretKey,
PublicKey, SecretKey, SuiteId,
};
use mt_examples::{hex_full, print_field, print_kv, print_note, print_section, print_subsection};
use mt_mnemonic::{
entropy_to_mnemonic, mldsa_seed_for_role, mlkem_seed_for_role, mnemonic_to_master_seed,
wordlist, MnemonicError, WORDLIST_FINGERPRINT, WORDLIST_SIZE,
};
fn hex_from(input: &str) -> Option<Vec<u8>> {
let clean: String = input.chars().filter(|c| !c.is_whitespace()).collect();
if clean.len() % 2 != 0 {
return None;
}
(0..clean.len())
.step_by(2)
.map(|i| u8::from_str_radix(&clean[i..i + 2], 16).ok())
.collect()
}
fn to_array_32(v: &[u8]) -> Option<[u8; 32]> {
if v.len() != 32 {
return None;
}
let mut out = [0u8; 32];
out.copy_from_slice(v);
Some(out)
}
fn print_master_seed(master: &[u8; 64]) {
print_subsection("MASTER SEED — 64 байта, результат PBKDF2");
print_kv("fingerprint (8B)", hex_full(&master[..8]));
println!(" hex_full:");
println!(" {}", hex_full(master));
}
fn print_entropy(entropy: &[u8; 32]) {
print_subsection("ENTROPY — 32 байта");
print_kv("hex", hex_full(entropy));
let checksum = sha256_raw(entropy)[0];
print_kv(
"SHA-256(entropy)[0] checksum byte",
format!("0x{checksum:02x}"),
);
}
fn print_mnemonic(mnemonic: &str) {
print_subsection("MNEMONIC — 24 слова");
let words: Vec<&str> = mnemonic.split(' ').collect();
print_kv("word count", words.len().to_string());
print_kv("length bytes", mnemonic.len().to_string());
for (i, w) in words.iter().enumerate() {
println!(" [{:>2}] {w}", i + 1);
}
}
fn print_wordlist_fingerprint_check() -> bool {
print_subsection("WORDLIST FINGERPRINT CHECK");
let wl = wordlist();
print_kv("wordlist size", format!("{} слов", wl.len()));
print_kv(
"binding fingerprint (spec)",
hex_full(&WORDLIST_FINGERPRINT),
);
print_kv("first word", wl[0].to_string());
print_kv("last word", wl[WORDLIST_SIZE - 1].to_string());
print_note("fingerprint check выполнен внутри wordlist() при первом вызове — PASS");
true
}
// Terminal observable IDs (per spec):
// account_id = SHA-256("mt-account" || suite_id_LE_bytes(2) || pk_acc)
// node_id = SHA-256("mt-node" || pk_node)
fn compute_account_id(pk: &PublicKey) -> [u8; 32] {
let mut buf = Vec::with_capacity(domain::ACCOUNT.len() + 2 + pk.as_bytes().len());
buf.extend_from_slice(domain::ACCOUNT);
let suite_id = (SuiteId::Mldsa65 as u16).to_le_bytes();
buf.extend_from_slice(&suite_id);
buf.extend_from_slice(pk.as_bytes());
sha256_raw(&buf)
}
fn compute_node_id(pk: &PublicKey) -> [u8; 32] {
let mut buf = Vec::with_capacity(domain::NODE.len() + pk.as_bytes().len());
buf.extend_from_slice(domain::NODE);
buf.extend_from_slice(pk.as_bytes());
sha256_raw(&buf)
}
struct DerivedIdentity {
pk_acc: PublicKey,
sk_acc: SecretKey,
pk_node: PublicKey,
sk_node: SecretKey,
pk_mlkem: MlkemPublicKey,
sk_mlkem: MlkemSecretKey,
account_id: [u8; 32],
node_id: [u8; 32],
}
fn derive_full_identity(entropy: &[u8; 32]) -> Result<DerivedIdentity, MnemonicError> {
let mnemonic = entropy_to_mnemonic(entropy);
let master_seed = mnemonic_to_master_seed(&mnemonic)?;
let acc_seed = mldsa_seed_for_role(&master_seed, domain::ACCOUNT_KEY);
let (pk_acc, sk_acc) =
keypair_from_seed(&acc_seed).expect("HKDF-derived seed cannot fail ML-DSA KeyGen");
let account_id = compute_account_id(&pk_acc);
let node_seed = mldsa_seed_for_role(&master_seed, domain::NODE_KEY);
let (pk_node, sk_node) =
keypair_from_seed(&node_seed).expect("HKDF-derived seed cannot fail ML-DSA KeyGen");
let node_id = compute_node_id(&pk_node);
let mlkem_seed = mlkem_seed_for_role(&master_seed, domain::APP_ENCRYPTION_KEY);
let (pk_mlkem, sk_mlkem) =
keypair_from_seed_mlkem(&mlkem_seed).expect("HKDF-derived seed cannot fail ML-KEM KeyGen");
Ok(DerivedIdentity {
pk_acc,
sk_acc,
pk_node,
sk_node,
pk_mlkem,
sk_mlkem,
account_id,
node_id,
})
}
// Recovery fingerprint per [C-4] terminal observable identity:
// SHA-256("mt-recovery-fingerprint" ||
// pk_acc || sk_acc || pk_node || sk_node ||
// pk_mlkem || sk_mlkem || account_id || node_id)
fn compute_recovery_fingerprint(id: &DerivedIdentity) -> [u8; 32] {
let mut buf = Vec::with_capacity(
domain::RECOVERY_FINGERPRINT.len()
+ id.pk_acc.as_bytes().len()
+ id.sk_acc.as_bytes().len()
+ id.pk_node.as_bytes().len()
+ id.sk_node.as_bytes().len()
+ id.pk_mlkem.as_bytes().len()
+ id.sk_mlkem.as_bytes().len()
+ 32
+ 32,
);
buf.extend_from_slice(domain::RECOVERY_FINGERPRINT);
buf.extend_from_slice(id.pk_acc.as_bytes());
buf.extend_from_slice(id.sk_acc.as_bytes());
buf.extend_from_slice(id.pk_node.as_bytes());
buf.extend_from_slice(id.sk_node.as_bytes());
buf.extend_from_slice(id.pk_mlkem.as_bytes());
buf.extend_from_slice(id.sk_mlkem.as_bytes());
buf.extend_from_slice(&id.account_id);
buf.extend_from_slice(&id.node_id);
sha256_raw(&buf)
}
fn parse_entropy_arg(entropy_hex: Option<&str>) -> Option<[u8; 32]> {
if let Some(h) = entropy_hex {
let bytes = hex_from(h)?;
to_array_32(&bytes)
} else {
Some([0u8; 32])
}
}
fn cmd_seeds(entropy_hex: Option<&str>) -> bool {
print_section("SEEDS DERIVATION FROM ENTROPY (промежуточные значения, не keypair)");
print_wordlist_fingerprint_check();
let entropy = match parse_entropy_arg(entropy_hex) {
Some(e) => e,
None => {
eprintln!("invalid entropy hex (must be 32 bytes / 64 hex chars)");
return false;
},
};
if entropy_hex.is_none() {
print_note("entropy не указан — использую [0x00; 32] (M-1 Vector 1)");
}
print_entropy(&entropy);
let mnemonic = entropy_to_mnemonic(&entropy);
print_mnemonic(&mnemonic);
let master_seed = match mnemonic_to_master_seed(&mnemonic) {
Ok(m) => m,
Err(e) => {
eprintln!("mnemonic_to_master_seed failed: {e}");
return false;
},
};
print_master_seed(&master_seed);
print_section("PER-ROLE KEY SEEDS — HKDF-Expand (НЕ сами keypair, только seed material)");
print_subsection("ACCOUNT KEYPAIR SEED — 32 байта (ML-DSA-65)");
let mldsa_account = mldsa_seed_for_role(&master_seed, domain::ACCOUNT_KEY);
print_kv("info (domain separator)", "\"mt-account-key\"");
print_kv("HKDF-Expand L", "32");
println!(" hex_full:");
println!(" {}", hex_full(&mldsa_account));
print_subsection("NODE KEYPAIR SEED — 32 байта (ML-DSA-65)");
let mldsa_node = mldsa_seed_for_role(&master_seed, domain::NODE_KEY);
print_kv("info (domain separator)", "\"mt-node-key\"");
print_kv("HKDF-Expand L", "32");
println!(" hex_full:");
println!(" {}", hex_full(&mldsa_node));
print_subsection("APP ENCRYPTION KEYPAIR SEED — 64 байта (ML-KEM-768)");
let mlkem_app = mlkem_seed_for_role(&master_seed, domain::APP_ENCRYPTION_KEY);
print_kv("info (domain separator)", "\"mt-app-encryption-key\"");
print_kv("HKDF-Expand L", "64");
println!(" hex_full:");
println!(" {}", hex_full(&mlkem_app));
print_section("CONSEQUENCES");
print_note(
"Идентичная entropy → идентичная мнемоника → идентичный master_seed → идентичные seeds",
);
print_note("Это промежуточные значения. Полные keypair (pk/sk) → subcommand `keypair`");
print_note(
"Полная цепочка до terminal IDs (account_id, node_id, recovery fingerprint) → `recovery-fingerprint`",
);
println!("\n[result] SEEDS: PASS");
true
}
fn print_pk_brief(label: &str, bytes: &[u8]) {
print_subsection(label);
print_kv("size", format!("{} bytes", bytes.len()));
print_kv("sha256", hex_full(&sha256_raw(bytes)));
print_kv("first 32 bytes", hex_full(&bytes[..32]));
}
fn print_sk_brief(label: &str, bytes: &[u8]) {
print_subsection(label);
print_kv("size", format!("{} bytes", bytes.len()));
print_kv("sha256 (binding fingerprint)", hex_full(&sha256_raw(bytes)));
print_note("SK bytes redacted — production identity, не выводится");
}
fn cmd_keypair(entropy_hex: Option<&str>) -> bool {
print_section("KEYPAIR DERIVATION FROM ENTROPY → terminal observable identity");
print_wordlist_fingerprint_check();
let entropy = match parse_entropy_arg(entropy_hex) {
Some(e) => e,
None => {
eprintln!("invalid entropy hex (must be 32 bytes / 64 hex chars)");
return false;
},
};
if entropy_hex.is_none() {
print_note("entropy не указан — использую [0x00; 32] (M-1 Vector 1)");
}
print_entropy(&entropy);
let id = match derive_full_identity(&entropy) {
Ok(i) => i,
Err(e) => {
eprintln!("derive failed: {e}");
return false;
},
};
print_section("ML-DSA-65 KEYPAIRS (terminal — это что попадает в state сети)");
print_pk_brief("ACCOUNT PUBLIC KEY (1952 байт)", id.pk_acc.as_bytes());
print_sk_brief("ACCOUNT SECRET KEY (4032 байт)", id.sk_acc.as_bytes());
print_pk_brief("NODE PUBLIC KEY (1952 байт)", id.pk_node.as_bytes());
print_sk_brief("NODE SECRET KEY (4032 байт)", id.sk_node.as_bytes());
print_section("ML-KEM-768 KEYPAIR (для encryption на клиентском уровне)");
print_pk_brief(
"APP ENCRYPTION PUBLIC KEY (1184 байт)",
id.pk_mlkem.as_bytes(),
);
print_sk_brief(
"APP ENCRYPTION SECRET KEY (2400 байт)",
id.sk_mlkem.as_bytes(),
);
print_section("TERMINAL OBSERVABLE IDS — это что network видит");
print_subsection("ACCOUNT_ID = SHA-256(\"mt-account\" || suite_id_LE(2B) || pk_acc)");
print_kv("hex", hex_full(&id.account_id));
print_subsection("NODE_ID = SHA-256(\"mt-node\" || pk_node)");
print_kv("hex", hex_full(&id.node_id));
print_section("CONSEQUENCES");
print_note("Те же entropy → те же account_id и node_id (deterministic recovery flow)");
print_note(
"Потеря устройства: ввод 24 слов на новом устройстве восстанавливает все 6 key parts + IDs",
);
print_note(
"Verification на двух устройствах — через `recovery-fingerprint` (одна 64-char hex)",
);
println!("\n[result] KEYPAIR: PASS");
true
}
fn cmd_recovery_fingerprint(entropy_hex: Option<&str>) -> bool {
print_section("RECOVERY FINGERPRINT — single 64-char hex для two-device manual validation");
print_wordlist_fingerprint_check();
let entropy = match parse_entropy_arg(entropy_hex) {
Some(e) => e,
None => {
eprintln!("invalid entropy hex (must be 32 bytes / 64 hex chars)");
return false;
},
};
if entropy_hex.is_none() {
print_note("entropy не указан — использую [0x00; 32] (M-1 Vector 1)");
}
print_entropy(&entropy);
let id = match derive_full_identity(&entropy) {
Ok(i) => i,
Err(e) => {
eprintln!("derive failed: {e}");
return false;
},
};
let fp = compute_recovery_fingerprint(&id);
print_section("FINGERPRINT");
print_subsection(
"SHA-256(\"mt-recovery-fingerprint\" || pk_acc || sk_acc || pk_node || sk_node || pk_mlkem || sk_mlkem || account_id || node_id)",
);
print_kv("hex (64 chars)", hex_full(&fp));
print_section("USAGE — two-device manual validation");
print_note("Запустить на устройстве A с теми же 24 словами, на устройстве B — те же 24 слова");
print_note(
"Сравнить две 64-char hex visually — побайтное равенство ⇔ recovery flow byte-identical",
);
print_note("Расхождение даже одного hex-символа = либо разные мнемоники, либо bug в recovery");
print_subsection("Component fingerprints (для подробного debug)");
print_kv("account_id", hex_full(&id.account_id));
print_kv("node_id", hex_full(&id.node_id));
print_kv(
"sha256(pk_acc)",
hex_full(&sha256_raw(id.pk_acc.as_bytes())),
);
print_kv(
"sha256(sk_acc)",
hex_full(&sha256_raw(id.sk_acc.as_bytes())),
);
print_kv(
"sha256(pk_node)",
hex_full(&sha256_raw(id.pk_node.as_bytes())),
);
print_kv(
"sha256(sk_node)",
hex_full(&sha256_raw(id.sk_node.as_bytes())),
);
print_kv(
"sha256(pk_mlkem)",
hex_full(&sha256_raw(id.pk_mlkem.as_bytes())),
);
print_kv(
"sha256(sk_mlkem)",
hex_full(&sha256_raw(id.sk_mlkem.as_bytes())),
);
println!("\n[result] RECOVERY-FINGERPRINT: PASS");
true
}
fn cmd_mnemonic(mnemonic: &str) -> bool {
print_section("MNEMONIC → MASTER SEED TRACE");
print_wordlist_fingerprint_check();
print_mnemonic(mnemonic);
print_subsection("PARSE + CHECKSUM VERIFY");
match mnemonic_to_master_seed(mnemonic) {
Ok(master_seed) => {
print_kv("parse + checksum", "OK");
print_master_seed(&master_seed);
print_subsection("PER-ROLE DERIVATIONS (промежуточные seeds)");
let acc = mldsa_seed_for_role(&master_seed, domain::ACCOUNT_KEY);
let node = mldsa_seed_for_role(&master_seed, domain::NODE_KEY);
let app = mlkem_seed_for_role(&master_seed, domain::APP_ENCRYPTION_KEY);
print_kv("mldsa_seed(account)", hex_full(&acc));
print_kv("mldsa_seed(node)", hex_full(&node));
print_kv("mlkem_seed(app)", hex_full(&app));
println!("\n[result] MNEMONIC: PASS");
true
},
Err(MnemonicError::WordCount(n)) => {
eprintln!("parse failed: expected 24 words, got {n}");
println!("\n[result] MNEMONIC: FAIL (word count)");
false
},
Err(MnemonicError::UnknownWord(pos)) => {
eprintln!("parse failed: word at position {pos} is not in Montana wordlist");
println!("\n[result] MNEMONIC: FAIL (unknown word)");
false
},
Err(MnemonicError::ChecksumMismatch) => {
eprintln!("parse failed: mnemonic checksum does not match SHA-256(entropy)[0]");
println!("\n[result] MNEMONIC: FAIL (checksum mismatch)");
false
},
}
}
fn cmd_vectors() -> bool {
print_section("6 BINDING TEST VECTORS — byte-exact verification vs spec");
print_wordlist_fingerprint_check();
let mut all_pass = true;
// === M-1 Vector 1 ===
print_section("M-1 Vector 1 — entropy = [0x00; 32]");
let entropy_1 = [0u8; 32];
let mnemonic_1 = entropy_to_mnemonic(&entropy_1);
let master_1 = mnemonic_to_master_seed(&mnemonic_1).expect("valid");
print_entropy(&entropy_1);
print_kv("last word (expected)", "art (index 102)");
print_mnemonic(&mnemonic_1);
print_master_seed(&master_1);
let expected_1 = concat!(
"38a1421ac3ce191fbdc46b1cca266a9d72d22320fb38bda6a3df90a1ead664a7",
"8951703197be882ace38e0f557a492a8e9ff5e3c02290a8eecf5939468708edb",
);
let ok_1 = hex_full(&master_1) == expected_1;
print_kv(
"byte-exact vs spec binding",
if ok_1 { "OK ✓" } else { "FAIL ✗" },
);
all_pass &= ok_1;
// === M-1 Vector 2 ===
print_section("M-1 Vector 2 — entropy = [0xFF; 32]");
let entropy_2 = [0xFFu8; 32];
let mnemonic_2 = entropy_to_mnemonic(&entropy_2);
let master_2 = mnemonic_to_master_seed(&mnemonic_2).expect("valid");
print_entropy(&entropy_2);
print_kv("last word (expected)", "vote");
print_mnemonic(&mnemonic_2);
print_master_seed(&master_2);
let expected_2 = concat!(
"a5925c51583447a0abe43b65dbc591f3780a91c7d44c6b333975a211096039f3",
"d1d0ca9e125aa4e756f0a35b0006378ac69450e8254e32f16409a350f3ca9104",
);
let ok_2 = hex_full(&master_2) == expected_2;
print_kv(
"byte-exact vs spec binding",
if ok_2 { "OK ✓" } else { "FAIL ✗" },
);
all_pass &= ok_2;
// === M-1 Vector 3 ===
print_section("M-1 Vector 3 — entropy = SHA-256(\"Montana test vector 3\")");
let entropy_3_hash = sha256_raw(b"Montana test vector 3");
let mut entropy_3 = [0u8; 32];
entropy_3.copy_from_slice(&entropy_3_hash);
let mnemonic_3 = entropy_to_mnemonic(&entropy_3);
let master_3 = mnemonic_to_master_seed(&mnemonic_3).expect("valid");
print_entropy(&entropy_3);
print_mnemonic(&mnemonic_3);
print_master_seed(&master_3);
let expected_3 = concat!(
"da13e259eb58c79a650c312efe79d2ef42861ad114206ec48cb4b1eb5dcf0c22",
"75b074ef8b02fbc2123032090ff004d7cc546d2bbf34c4e10ec3c6fb092f9a47",
);
let ok_3 = hex_full(&master_3) == expected_3;
print_kv(
"byte-exact vs spec binding",
if ok_3 { "OK ✓" } else { "FAIL ✗" },
);
all_pass &= ok_3;
// === Per-role vectors — используют master_1 из Vector 1 ===
print_section("Per-role Derivation Vectors — master_seed из M-1 Vector 1");
print_subsection("Derivation Vector 1 — mldsa_seed(account)");
let deriv_1 = mldsa_seed_for_role(&master_1, domain::ACCOUNT_KEY);
print_field("info", "\"mt-account-key\" (14 байт)");
print_field("L", "32");
println!(" hex: {}", hex_full(&deriv_1));
let exp_deriv_1 = "08ce5c19768c679fda24c0d3360e57ce03d00c94c175e59f50e9c77894c20818";
let ok_d1 = hex_full(&deriv_1) == exp_deriv_1;
print_kv("byte-exact", if ok_d1 { "OK ✓" } else { "FAIL ✗" });
all_pass &= ok_d1;
print_subsection("Derivation Vector 2 — mldsa_seed(node)");
let deriv_2 = mldsa_seed_for_role(&master_1, domain::NODE_KEY);
print_field("info", "\"mt-node-key\" (11 байт)");
print_field("L", "32");
println!(" hex: {}", hex_full(&deriv_2));
let exp_deriv_2 = "efe527d96de2cb82b3ee2e8ad24b4aca71014e37896b0c025a376335ad456acc";
let ok_d2 = hex_full(&deriv_2) == exp_deriv_2;
print_kv("byte-exact", if ok_d2 { "OK ✓" } else { "FAIL ✗" });
all_pass &= ok_d2;
print_subsection("Derivation Vector 3 — mlkem_seed(app-encryption)");
let deriv_3 = mlkem_seed_for_role(&master_1, domain::APP_ENCRYPTION_KEY);
print_field("info", "\"mt-app-encryption-key\" (21 байт)");
print_field("L", "64");
println!(" hex: {}", hex_full(&deriv_3));
let exp_deriv_3 = concat!(
"3eb9bcd201a1d5e671c9d23a929589a26ceb53338cd0684b5d77314a14601b03",
"9f3e2ae7e5e0be8acd47b4b928c3e73b5d875b9fc7089b22bc1d59e9dc31077e",
);
let ok_d3 = hex_full(&deriv_3) == exp_deriv_3;
print_kv("byte-exact", if ok_d3 { "OK ✓" } else { "FAIL ✗" });
all_pass &= ok_d3;
println!(
"\n[result] VECTORS: {}",
if all_pass { "PASS (6/6)" } else { "FAIL" }
);
all_pass
}
fn cmd_roundtrip(entropy_hex: Option<&str>) -> bool {
print_section("ROUNDTRIP — entropy → mnemonic → master_seed");
print_wordlist_fingerprint_check();
let entropy: [u8; 32] = if let Some(h) = entropy_hex {
let Some(bytes) = hex_from(h) else {
eprintln!("invalid hex");
return false;
};
let Some(arr) = to_array_32(&bytes) else {
eprintln!("entropy must be 32 bytes");
return false;
};
arr
} else {
print_note("entropy не указан — использую SHA-256(\"roundtrip\")");
let h = sha256_raw(b"roundtrip");
let mut arr = [0u8; 32];
arr.copy_from_slice(&h);
arr
};
print_entropy(&entropy);
let mnemonic = entropy_to_mnemonic(&entropy);
print_mnemonic(&mnemonic);
let master_first = mnemonic_to_master_seed(&mnemonic).expect("valid");
print_master_seed(&master_first);
// Вторая derivation — должна дать тот же результат
let master_second = mnemonic_to_master_seed(&mnemonic).expect("valid");
let matches = master_first == master_second;
print_subsection("IDEMPOTENCY");
print_kv("second derivation equals first", format!("{matches}"));
println!(
"\n[result] ROUNDTRIP: {}",
if matches { "PASS" } else { "FAIL" }
);
matches
}
fn cmd_all() -> bool {
print_section("M1 MNEMONIC — FULL USER JOURNEY");
print_note(
"Прогоняется полный путь: fingerprint check → seeds → keypair (terminal) → recovery-fingerprint → mnemonic parse → vectors → roundtrip",
);
let a = cmd_seeds(None);
let b = cmd_keypair(None);
let c = cmd_recovery_fingerprint(None);
let d = cmd_mnemonic(
"abandon abandon abandon abandon abandon abandon abandon abandon \
abandon abandon abandon abandon abandon abandon abandon abandon \
abandon abandon abandon abandon abandon abandon abandon art",
);
let e = cmd_vectors();
let f = cmd_roundtrip(None);
print_section("SUMMARY");
print_kv("seeds", if a { "PASS" } else { "FAIL" });
print_kv("keypair (terminal)", if b { "PASS" } else { "FAIL" });
print_kv("recovery-fingerprint", if c { "PASS" } else { "FAIL" });
print_kv("mnemonic", if d { "PASS" } else { "FAIL" });
print_kv("vectors (6 binding)", if e { "PASS" } else { "FAIL" });
print_kv("roundtrip", if f { "PASS" } else { "FAIL" });
let pass = a && b && c && d && e && f;
println!("\n[result] ALL: {}", if pass { "PASS" } else { "FAIL" });
pass
}
fn usage() {
eprintln!(
"M1 MNEMONIC — Montana 24-слов recovery + per-role keypair derivation + terminal IDs"
);
eprintln!();
eprintln!("usage: m1_mnemonic <subcommand> [args]");
eprintln!();
eprintln!(" seeds [ENTROPY_HEX] Промежуточные seed material (HKDF outputs)");
eprintln!(" keypair [ENTROPY_HEX] Полные ML-DSA + ML-KEM keypairs + terminal IDs");
eprintln!(" recovery-fingerprint [ENTROPY_HEX] Single 64-char hex для two-device validation");
eprintln!(" mnemonic \"STRING\" Parse mnemonic, derive master_seed + seeds");
eprintln!(" vectors Прогон 6 binding test vectors (см. спеку)");
eprintln!(" roundtrip [ENTROPY_HEX] entropy → mnemonic → master_seed idempotency");
eprintln!(" all Full user journey");
eprintln!();
eprintln!("Exit 0 = PASS, 1 = FAIL.");
}
fn bool_to_exit(pass: bool) -> ExitCode {
if pass {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
}
fn main() -> ExitCode {
let args: Vec<String> = env::args().collect();
let sub = match args.get(1) {
Some(s) => s.as_str(),
None => {
usage();
return ExitCode::FAILURE;
},
};
let pass = match sub {
"seeds" => cmd_seeds(args.get(2).map(|s| s.as_str())),
"keypair" => cmd_keypair(args.get(2).map(|s| s.as_str())),
"recovery-fingerprint" => cmd_recovery_fingerprint(args.get(2).map(|s| s.as_str())),
"mnemonic" => {
let Some(m) = args.get(2) else {
eprintln!("mnemonic subcommand requires string argument");
return ExitCode::FAILURE;
};
cmd_mnemonic(m)
},
"vectors" => cmd_vectors(),
"roundtrip" => cmd_roundtrip(args.get(2).map(|s| s.as_str())),
"all" => cmd_all(),
_ => {
usage();
return ExitCode::FAILURE;
},
};
bool_to_exit(pass)
}