667 lines
24 KiB
Rust
667 lines
24 KiB
Rust
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)
|
||
}
|