672 lines
25 KiB
Rust
672 lines
25 KiB
Rust
use std::env;
|
||
use std::process::ExitCode;
|
||
use std::time::Instant;
|
||
|
||
use mt_codec::domain;
|
||
use mt_crypto::{
|
||
hash, keypair, keypair_from_seed, sha256_raw, sign, verify, Hash32, PublicKey, SecretKey,
|
||
Signature, KEYPAIR_SEED_SIZE, PUBLIC_KEY_SIZE, SECRET_KEY_SIZE, SIGNATURE_SIZE,
|
||
};
|
||
use mt_examples::{
|
||
hex_full, print_field, print_kv, print_note, print_section, print_subsection, print_warn,
|
||
xxd_dump,
|
||
};
|
||
use mt_merkle::{empty_internal, internal_hash, leaf_hash, TREE_DEPTH};
|
||
|
||
// Debug fingerprint через domain-separated hash. Label «fingerprint (debug)»
|
||
// явно указывает что это NOT raw SHA-256 — используется mt-fingerprint-debug
|
||
// domain для избежания confusion с consensus hashes.
|
||
fn fingerprint(bytes: &[u8]) -> String {
|
||
let h: Hash32 = hash(b"mt-fingerprint-debug", &[bytes]);
|
||
let short: Vec<u8> = h[..8].to_vec();
|
||
hex_full(&short)
|
||
}
|
||
|
||
fn debug_hash(bytes: &[u8]) -> String {
|
||
let h: Hash32 = hash(b"mt-fingerprint-debug", &[bytes]);
|
||
hex_full(&h)
|
||
}
|
||
|
||
// Raw SHA-256 без domain separation — для FIPS 180-4 conformance checks
|
||
// и для сравнения с standalone `shasum -a 256` output.
|
||
fn raw_sha256(bytes: &[u8]) -> String {
|
||
hex_full(&sha256_raw(bytes))
|
||
}
|
||
|
||
// Dump SK только если явно включён через env var M1_DUMP_SK=1.
|
||
// По умолчанию — redacted fingerprint only. Prevents accidental leak через
|
||
// cargo run --release --example m1_crypto > shared.log.
|
||
fn dump_sk_enabled() -> bool {
|
||
std::env::var("M1_DUMP_SK").ok().as_deref() == Some("1")
|
||
}
|
||
|
||
// cmd_keypair_deterministic заменяет старый print_recovery_disclosure (soft note):
|
||
// реальный test of deterministic KeyGen — два прогона keypair_from_seed(seed) с
|
||
// тем же seed → byte-exact equality, с разным seed → различные. Это actual
|
||
// демонстрация recovery flow на уровне primitive (не только note про mnemonic crate).
|
||
|
||
fn print_pk(pk: &PublicKey) {
|
||
print_subsection("PUBLIC KEY — передаётся в сеть, попадает в AccountTable/NodeTable");
|
||
print_kv("size", format!("{PUBLIC_KEY_SIZE} bytes"));
|
||
print_kv(
|
||
"fingerprint (8B, domain=mt-fingerprint-debug)",
|
||
fingerprint(pk.as_bytes()),
|
||
);
|
||
print_kv(
|
||
"debug_hash (domain=mt-fingerprint-debug)",
|
||
debug_hash(pk.as_bytes()),
|
||
);
|
||
print_kv("sha256_raw (no domain)", raw_sha256(pk.as_bytes()));
|
||
println!(" hex_full:");
|
||
println!(" {}", hex_full(pk.as_bytes()));
|
||
println!(" xxd dump:");
|
||
for line in xxd_dump(pk.as_bytes()).lines() {
|
||
println!(" {line}");
|
||
}
|
||
}
|
||
|
||
fn print_sk(sk: &SecretKey) {
|
||
print_subsection("SECRET KEY — НЕ покидает узел, хранится только локально");
|
||
print_kv("size", format!("{SECRET_KEY_SIZE} bytes"));
|
||
print_kv(
|
||
"fingerprint (8B, domain=mt-fingerprint-debug)",
|
||
fingerprint(sk.as_bytes()),
|
||
);
|
||
|
||
if !dump_sk_enabled() {
|
||
print_warn(
|
||
"SK bytes REDACTED — set M1_DUMP_SK=1 to show (dev-only, unsafe для shared output)",
|
||
);
|
||
return;
|
||
}
|
||
|
||
print_kv(
|
||
"debug_hash (domain=mt-fingerprint-debug)",
|
||
debug_hash(sk.as_bytes()),
|
||
);
|
||
print_kv("sha256_raw (no domain)", raw_sha256(sk.as_bytes()));
|
||
println!(" hex_full:");
|
||
println!(" {}", hex_full(sk.as_bytes()));
|
||
println!(" xxd dump:");
|
||
for line in xxd_dump(sk.as_bytes()).lines() {
|
||
println!(" {line}");
|
||
}
|
||
print_warn("SK отображён только потому что M1_DUMP_SK=1 установлен");
|
||
print_warn("В production SK никогда не логируется, не сериализуется в открытом виде");
|
||
}
|
||
|
||
fn print_sig(sig: &Signature) {
|
||
print_subsection("SIGNATURE — ML-DSA-65 deterministic");
|
||
print_kv("size", format!("{SIGNATURE_SIZE} bytes"));
|
||
print_kv(
|
||
"fingerprint (8B, domain=mt-fingerprint-debug)",
|
||
fingerprint(sig.as_bytes()),
|
||
);
|
||
print_kv(
|
||
"debug_hash (domain=mt-fingerprint-debug)",
|
||
debug_hash(sig.as_bytes()),
|
||
);
|
||
print_kv("sha256_raw (no domain)", raw_sha256(sig.as_bytes()));
|
||
println!(" hex_full:");
|
||
println!(" {}", hex_full(sig.as_bytes()));
|
||
println!(" xxd dump:");
|
||
for line in xxd_dump(sig.as_bytes()).lines() {
|
||
println!(" {line}");
|
||
}
|
||
}
|
||
|
||
fn cmd_keypair_random(n: usize) -> bool {
|
||
print_section(&format!(
|
||
"KEYPAIR RANDOM — ML-DSA-65 (OS rng entropy) × {n} (sanity check primitive)"
|
||
));
|
||
print_subsection("Алгоритм");
|
||
print_kv(
|
||
"scheme",
|
||
"ML-DSA-65 (NIST FIPS 204 finalized 2024-08, lattice-based, [I-1] compliant)",
|
||
);
|
||
print_kv(
|
||
"library",
|
||
"OpenSSL 3.5.5 LTS via own thin C FFI wrapper (mt-crypto-native)",
|
||
);
|
||
print_kv(
|
||
"entropy source",
|
||
"time+pid+stack-addr → SHA-256 (test/tool only; production через keypair_from_seed из HKDF)",
|
||
);
|
||
print_kv("pk size", format!("{PUBLIC_KEY_SIZE} bytes"));
|
||
print_kv("sk size", format!("{SECRET_KEY_SIZE} bytes"));
|
||
print_kv("sig size", format!("{SIGNATURE_SIZE} bytes"));
|
||
|
||
let mut pks: Vec<PublicKey> = Vec::with_capacity(n);
|
||
let mut sks: Vec<Vec<u8>> = Vec::with_capacity(n);
|
||
let mut all_verify_ok = true;
|
||
|
||
for i in 0..n {
|
||
print_section(&format!("KEYPAIR #{} / {}", i + 1, n));
|
||
let t0 = Instant::now();
|
||
let (pk, sk) = keypair();
|
||
let elapsed = t0.elapsed();
|
||
print_kv(
|
||
"generation time",
|
||
format!("{:.2}ms", elapsed.as_secs_f64() * 1000.0),
|
||
);
|
||
|
||
print_pk(&pk);
|
||
print_sk(&sk);
|
||
|
||
let msg = format!("keypair#{i} self-test");
|
||
let sig = sign(&sk, msg.as_bytes()).expect("sign self-test");
|
||
let ok = verify(&pk, msg.as_bytes(), &sig);
|
||
print_subsection("Self-test sign+verify");
|
||
print_kv("test message", format!("{:?} ({} bytes)", msg, msg.len()));
|
||
print_kv("verify(pk,msg,sig)", format!("{ok}"));
|
||
if !ok {
|
||
all_verify_ok = false;
|
||
}
|
||
|
||
pks.push(pk);
|
||
sks.push(sk.as_bytes().to_vec());
|
||
}
|
||
|
||
print_section("UNIQUENESS INVARIANT");
|
||
let mut pks_distinct = true;
|
||
let mut sks_distinct = true;
|
||
for i in 0..pks.len() {
|
||
for j in (i + 1)..pks.len() {
|
||
if pks[i].as_bytes() == pks[j].as_bytes() {
|
||
pks_distinct = false;
|
||
print_warn(&format!("pk[{i}] == pk[{j}] !!!"));
|
||
}
|
||
if sks[i] == sks[j] {
|
||
sks_distinct = false;
|
||
print_warn(&format!("sk[{i}] == sk[{j}] !!!"));
|
||
}
|
||
}
|
||
}
|
||
print_kv("all_public_keys_distinct", format!("{pks_distinct}"));
|
||
print_kv("all_secret_keys_distinct", format!("{sks_distinct}"));
|
||
|
||
print_section("RECOVERY MECHANISM POINTER");
|
||
print_note("Production identity flow — через m1_mnemonic (deterministic from 24 слов)");
|
||
print_note("Эта команда — RANDOM keypair sanity check ML-DSA-65 primitive (test/dev only)");
|
||
print_note(
|
||
"Для deterministic recovery test: subcommand `keypair` (без --random) либо `m1_mnemonic`",
|
||
);
|
||
|
||
let pass = all_verify_ok && pks_distinct && sks_distinct;
|
||
println!(
|
||
"\n[result] KEYPAIR-RANDOM: {}",
|
||
if pass { "PASS" } else { "FAIL" }
|
||
);
|
||
pass
|
||
}
|
||
|
||
fn cmd_keypair_deterministic() -> bool {
|
||
print_section("KEYPAIR DETERMINISTIC — ML-DSA-65.KeyGen(seed) byte-exact recovery test");
|
||
|
||
print_subsection("Алгоритм");
|
||
print_kv(
|
||
"scheme",
|
||
"ML-DSA-65 (NIST FIPS 204 finalized 2024-08, FIPS 204 §3.1 ξ ∈ B32)",
|
||
);
|
||
print_kv(
|
||
"seed source",
|
||
"fixed test value [0x42; 32] (production: HKDF-Expand из master_seed)",
|
||
);
|
||
print_kv("pk size", format!("{PUBLIC_KEY_SIZE} bytes"));
|
||
print_kv("sk size", format!("{SECRET_KEY_SIZE} bytes"));
|
||
|
||
let seed = [0x42u8; KEYPAIR_SEED_SIZE];
|
||
print_subsection("DERIVATION TRACE — два независимых прогона keypair_from_seed(seed)");
|
||
print_kv("seed hex", hex_full(&seed));
|
||
|
||
let (pk_a, sk_a) = keypair_from_seed(&seed).expect("keygen run A");
|
||
let (pk_b, sk_b) = keypair_from_seed(&seed).expect("keygen run B");
|
||
|
||
let pk_match = pk_a.as_bytes() == pk_b.as_bytes();
|
||
let sk_match = sk_a.as_bytes() == sk_b.as_bytes();
|
||
|
||
print_kv(
|
||
"Run A pk fingerprint (8B)",
|
||
hex_full(&sha256_raw(pk_a.as_bytes())[..8]),
|
||
);
|
||
print_kv(
|
||
"Run B pk fingerprint (8B)",
|
||
hex_full(&sha256_raw(pk_b.as_bytes())[..8]),
|
||
);
|
||
print_kv(
|
||
"Run A sk fingerprint (8B)",
|
||
hex_full(&sha256_raw(sk_a.as_bytes())[..8]),
|
||
);
|
||
print_kv(
|
||
"Run B sk fingerprint (8B)",
|
||
hex_full(&sha256_raw(sk_b.as_bytes())[..8]),
|
||
);
|
||
|
||
print_subsection("BYTE-EXACT EQUALITY ASSERTIONS");
|
||
print_kv("pk_a == pk_b", if pk_match { "OK ✓" } else { "FAIL ✗" });
|
||
print_kv("sk_a == sk_b", if sk_match { "OK ✓" } else { "FAIL ✗" });
|
||
|
||
print_subsection("DIFFERENT SEED → DIFFERENT KEYPAIR (sanity)");
|
||
let seed2 = [0x43u8; KEYPAIR_SEED_SIZE];
|
||
let (pk_c, _) = keypair_from_seed(&seed2).expect("keygen seed2");
|
||
let pk_differs = pk_a.as_bytes() != pk_c.as_bytes();
|
||
print_kv("seed2 hex", hex_full(&seed2));
|
||
print_kv(
|
||
"pk_a != pk_c (different seed)",
|
||
if pk_differs { "OK ✓" } else { "FAIL ✗" },
|
||
);
|
||
|
||
print_subsection("FINGERPRINTS — для cross-impl byte-exact verification");
|
||
print_kv("pk sha256", hex_full(&sha256_raw(pk_a.as_bytes())));
|
||
print_kv("sk sha256", hex_full(&sha256_raw(sk_a.as_bytes())));
|
||
|
||
print_section("RECOVERY MECHANISM");
|
||
print_note(
|
||
"Эта команда — actual byte-exact test, не описание. Тот же seed → тот же keypair всегда.",
|
||
);
|
||
print_note(
|
||
"В production seed выводится из 24-слов мнемоники через mt-mnemonic (HKDF-Expand role-keyed).",
|
||
);
|
||
print_note("Полный recovery flow до terminal IDs — m1_mnemonic recovery-fingerprint");
|
||
|
||
let pass = pk_match && sk_match && pk_differs;
|
||
println!(
|
||
"\n[result] KEYPAIR-DETERMINISTIC: {}",
|
||
if pass { "PASS" } else { "FAIL" }
|
||
);
|
||
pass
|
||
}
|
||
|
||
fn cmd_sign(msg: &str) -> bool {
|
||
print_section(&format!(
|
||
"SIGN + VERIFY — ML-DSA-65 deterministic signature on message {msg:?}"
|
||
));
|
||
|
||
print_subsection("Message preparation");
|
||
let msg_bytes = msg.as_bytes();
|
||
print_kv("len", format!("{} bytes", msg_bytes.len()));
|
||
print_kv("ascii", msg.to_string());
|
||
print_kv("hex", hex_full(msg_bytes));
|
||
|
||
let (pk, sk) = keypair();
|
||
print_pk(&pk);
|
||
print_sk(&sk);
|
||
|
||
print_subsection("Sign call trace");
|
||
print_kv(
|
||
"function",
|
||
format!("mt_crypto::sign(sk, msg) → Signature[{SIGNATURE_SIZE}B]"),
|
||
);
|
||
print_kv(
|
||
"internal",
|
||
"OpenSSL EVP_DigestSign + OSSL_SIGNATURE_PARAM_DETERMINISTIC=1 (FIPS 204 Algorithm 2, deterministic variant)",
|
||
);
|
||
let t0 = Instant::now();
|
||
let sig = sign(&sk, msg_bytes).expect("sign happy path");
|
||
let sign_ms = t0.elapsed().as_secs_f64() * 1000.0;
|
||
print_kv("sign latency", format!("{sign_ms:.2}ms"));
|
||
print_sig(&sig);
|
||
|
||
print_subsection("Verify call trace (happy path)");
|
||
let t1 = Instant::now();
|
||
let ok = verify(&pk, msg_bytes, &sig);
|
||
let verify_ms = t1.elapsed().as_secs_f64() * 1000.0;
|
||
print_kv("verify latency", format!("{verify_ms:.2}ms"));
|
||
print_kv("result", format!("{ok}"));
|
||
|
||
print_section("ADVERSARIAL TESTS");
|
||
|
||
print_subsection("1. Mutation at 4 positions must all reject");
|
||
let positions = [0usize, 166, 333, 665];
|
||
let mut mut_all_rejected = true;
|
||
for &pos in &positions {
|
||
let mut bad = *sig.as_bytes();
|
||
bad[pos] ^= 0xFF;
|
||
let bad_sig = Signature::from_array(bad);
|
||
let rejected = !verify(&pk, msg_bytes, &bad_sig);
|
||
print_kv(
|
||
&format!("mutate byte {pos}"),
|
||
format!(
|
||
"sig[{pos}]=0x{:02x} → 0x{:02x}, verify={}",
|
||
sig.as_bytes()[pos],
|
||
bad[pos],
|
||
if rejected {
|
||
"REJECTED ✓"
|
||
} else {
|
||
"ACCEPTED ✗"
|
||
}
|
||
),
|
||
);
|
||
if !rejected {
|
||
mut_all_rejected = false;
|
||
}
|
||
}
|
||
|
||
print_subsection("2. Mutated message must be rejected");
|
||
let mut bad_msg = msg_bytes.to_vec();
|
||
if !bad_msg.is_empty() {
|
||
bad_msg[0] ^= 0x01;
|
||
} else {
|
||
bad_msg.push(0x01);
|
||
}
|
||
let msg_rejected = !verify(&pk, &bad_msg, &sig);
|
||
print_kv("orig msg hex", hex_full(msg_bytes));
|
||
print_kv("bad msg hex", hex_full(&bad_msg));
|
||
print_kv(
|
||
"verify(pk, bad_msg, sig)",
|
||
if msg_rejected {
|
||
"REJECTED ✓"
|
||
} else {
|
||
"ACCEPTED ✗"
|
||
},
|
||
);
|
||
|
||
print_subsection("3. Cross-key: verify with wrong pubkey must fail");
|
||
let (other_pk, _) = keypair();
|
||
let cross_rejected = !verify(&other_pk, msg_bytes, &sig);
|
||
print_kv("other_pk fingerprint", fingerprint(other_pk.as_bytes()));
|
||
print_kv(
|
||
"verify(other_pk, msg, sig)",
|
||
if cross_rejected {
|
||
"REJECTED ✓"
|
||
} else {
|
||
"ACCEPTED ✗"
|
||
},
|
||
);
|
||
|
||
print_subsection(
|
||
"4. Determinism: same (sk, msg) twice → identical σ (FIPS 204 §3.7, RND = 0x00 × 32)",
|
||
);
|
||
let sig2 = sign(&sk, msg_bytes).expect("sign sample 2");
|
||
let identical = sig.as_bytes() == sig2.as_bytes();
|
||
let sig2_verifies = verify(&pk, msg_bytes, &sig2);
|
||
print_kv("sig1 fingerprint", fingerprint(sig.as_bytes()));
|
||
print_kv("sig2 fingerprint", fingerprint(sig2.as_bytes()));
|
||
print_kv("sig1 == sig2", format!("{identical}"));
|
||
print_kv("verify(pk, msg, sig2)", format!("{sig2_verifies}"));
|
||
|
||
let pass =
|
||
ok && mut_all_rejected && msg_rejected && cross_rejected && identical && sig2_verifies;
|
||
println!("\n[result] SIGN: {}", if pass { "PASS" } else { "FAIL" });
|
||
pass
|
||
}
|
||
|
||
fn cmd_hash() -> bool {
|
||
print_section("HASH — SHA-256 (FIPS 180-4) + domain-separated composition");
|
||
|
||
print_subsection("1. FIPS 180-4 §B.1 vector: SHA-256(\"abc\")");
|
||
let input = b"abc";
|
||
print_kv("input ascii", "abc");
|
||
print_kv("input hex", hex_full(input));
|
||
print_kv("input len", format!("{} bytes", input.len()));
|
||
let got = sha256_raw(input);
|
||
let expected_hex = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
|
||
print_kv("mt_crypto::sha256_raw(b\"abc\")", hex_full(&got));
|
||
print_kv("FIPS 180-4 expected", expected_hex.to_string());
|
||
let fips_ok = hex_full(&got) == expected_hex;
|
||
print_kv("byte-exact match", format!("{fips_ok}"));
|
||
print_note("NOTE: mt_crypto::hash(domain, parts) использует NUL separator и self-delimiting");
|
||
print_note("formula SHA-256(domain || 0x00 || parts) — не raw SHA-256. Для FIPS compatibility");
|
||
print_note("используется отдельная функция sha256_raw() без domain separation.");
|
||
|
||
print_subsection("2. Empty-parts collapse: hash(d,[]) == hash(d,[b\"\"])");
|
||
let a = hash(domain::OP, &[]);
|
||
let b = hash(domain::OP, &[&[] as &[u8]]);
|
||
print_kv("hash(OP, [])", hex_full(&a));
|
||
print_kv("hash(OP, [b\"\"])", hex_full(&b));
|
||
let collapse_ok = a == b;
|
||
print_kv("equal", format!("{collapse_ok}"));
|
||
|
||
print_subsection("3. Part concatenation: hash(d,[a,b,c]) == hash(d,[a||b||c])");
|
||
let h_parts = hash(domain::OP, &[b"aa", b"bb", b"cc"]);
|
||
let h_concat = hash(domain::OP, &[b"aabbcc"]);
|
||
print_kv("hash(OP, [aa,bb,cc])", hex_full(&h_parts));
|
||
print_kv("hash(OP, [aabbcc])", hex_full(&h_concat));
|
||
let concat_ok = h_parts == h_concat;
|
||
print_kv("equal", format!("{concat_ok}"));
|
||
|
||
print_subsection("4. Domain separation — все R2 class domains c input=x");
|
||
let x: &[u8] = b"x";
|
||
let class_domains: &[(&str, &[u8])] = &[
|
||
("mt-op", domain::OP),
|
||
("mt-nodereg", domain::NODEREG),
|
||
("mt-proposal", domain::PROPOSAL),
|
||
("mt-bundle", domain::BUNDLE),
|
||
("mt-vdf-reveal", domain::VDF_REVEAL),
|
||
];
|
||
let mut outputs: Vec<Hash32> = Vec::new();
|
||
for (name, d) in class_domains {
|
||
let h = hash(d, &[x]);
|
||
print_kv(&format!("{name:<16} input=x"), hex_full(&h));
|
||
outputs.push(h);
|
||
}
|
||
let mut class_distinct = true;
|
||
for i in 0..outputs.len() {
|
||
for j in (i + 1)..outputs.len() {
|
||
if outputs[i] == outputs[j] {
|
||
class_distinct = false;
|
||
print_warn(&format!(
|
||
"collision: {} == {}",
|
||
class_domains[i].0, class_domains[j].0
|
||
));
|
||
}
|
||
}
|
||
}
|
||
print_kv("all_class_domains_distinct", format!("{class_distinct}"));
|
||
|
||
print_subsection("5. Domain separator registry — полный список 32 штук");
|
||
let all_domains: &[(&str, &[u8])] = &[
|
||
("mt-op", domain::OP),
|
||
("mt-nodereg", domain::NODEREG),
|
||
("mt-proposal", domain::PROPOSAL),
|
||
("mt-bundle", domain::BUNDLE),
|
||
("mt-vdf-reveal", domain::VDF_REVEAL),
|
||
("mt-account", domain::ACCOUNT),
|
||
("mt-candidate-vdf-init", domain::CANDIDATE_VDF_INIT),
|
||
("mt-merkle-leaf", domain::MERKLE_LEAF),
|
||
("mt-merkle-node", domain::MERKLE_NODE),
|
||
("mt-state-root", domain::STATE_ROOT),
|
||
("mt-timechain", domain::TIMECHAIN),
|
||
("mt-lottery", domain::LOTTERY),
|
||
("mt-bc-aggregate", domain::BC_AGGREGATE),
|
||
("mt-bc-aggregate-empty", domain::BC_AGGREGATE_EMPTY),
|
||
("mt-selection", domain::SELECTION),
|
||
("mt-nodereg-sort", domain::NODEREG_SORT),
|
||
("mt-confirmation", domain::CONFIRMATION),
|
||
("mt-app", domain::APP),
|
||
("mt-node", domain::NODE),
|
||
("mt-genesis", domain::GENESIS),
|
||
("mt-seed", domain::SEED),
|
||
("mt-account-key", domain::ACCOUNT_KEY),
|
||
("mt-node-key", domain::NODE_KEY),
|
||
("mt-content-chunk", domain::CONTENT_CHUNK),
|
||
("mt-content-manifest", domain::CONTENT_MANIFEST),
|
||
("mt-profile", domain::PROFILE),
|
||
("mt-encryption-key", domain::ENCRYPTION_KEY),
|
||
("mt-app-encryption-key", domain::APP_ENCRYPTION_KEY),
|
||
("mt-prekeys", domain::PREKEYS),
|
||
("mt-tunnel-online", domain::TUNNEL_ONLINE),
|
||
("mt-tunnel-mesh", domain::TUNNEL_MESH),
|
||
("mt-bootstrap-pow", domain::BOOTSTRAP_POW),
|
||
("mt-recovery-fingerprint", domain::RECOVERY_FINGERPRINT),
|
||
];
|
||
print_kv("registry size", format!("{}", all_domains.len()));
|
||
for (name, bytes) in all_domains {
|
||
print_field(
|
||
name,
|
||
&format!("{} bytes, hex={}", bytes.len(), hex_full(bytes)),
|
||
);
|
||
}
|
||
|
||
let pass = fips_ok && collapse_ok && concat_ok && class_distinct;
|
||
println!("\n[result] HASH: {}", if pass { "PASS" } else { "FAIL" });
|
||
pass
|
||
}
|
||
|
||
fn cmd_merkle() -> bool {
|
||
print_section("MERKLE — Sparse Merkle Tree depth=256, empty_internal table");
|
||
|
||
print_subsection("Определения");
|
||
print_kv("TREE_DEPTH", format!("{TREE_DEPTH}"));
|
||
print_kv("EMPTY_LEAF", hex_full(&empty_internal(0)));
|
||
print_kv(
|
||
"leaf_hash(x)",
|
||
"= SHA-256(\"mt-merkle-leaf\" || x)".to_string(),
|
||
);
|
||
print_kv(
|
||
"internal_hash(l,r)",
|
||
"= SHA-256(\"mt-merkle-node\" || l || r)".to_string(),
|
||
);
|
||
print_kv(
|
||
"empty_internal(k)",
|
||
"= level 0: [0;32]; level k≥1: internal_hash(empty_internal(k-1), empty_internal(k-1))"
|
||
.to_string(),
|
||
);
|
||
|
||
print_subsection("Derivation trace — первые 3 уровня");
|
||
let lvl0 = empty_internal(0);
|
||
let lvl1 = empty_internal(1);
|
||
let lvl2 = empty_internal(2);
|
||
println!(" level 0 = EMPTY_LEAF");
|
||
println!(" = {}", hex_full(&lvl0));
|
||
println!();
|
||
println!(" level 1 = internal_hash(level_0, level_0)");
|
||
println!(" = SHA-256(\"mt-merkle-node\" || level_0 || level_0)");
|
||
println!(" input domain (14B): {}", hex_full(domain::MERKLE_NODE));
|
||
println!(
|
||
" input data (64B): {}",
|
||
hex_full(&[&lvl0[..], &lvl0[..]].concat())
|
||
);
|
||
println!(" output (32B): {}", hex_full(&lvl1));
|
||
println!();
|
||
println!(" level 2 = internal_hash(level_1, level_1)");
|
||
println!(" input domain (14B): {}", hex_full(domain::MERKLE_NODE));
|
||
println!(
|
||
" input data (64B): {}",
|
||
hex_full(&[&lvl1[..], &lvl1[..]].concat())
|
||
);
|
||
println!(" output (32B): {}", hex_full(&lvl2));
|
||
|
||
print_subsection("Полная таблица empty_internal(0..=256)");
|
||
for k in 0..=TREE_DEPTH {
|
||
let v = empty_internal(k);
|
||
println!(" level {k:>3}: {}", hex_full(&v));
|
||
}
|
||
|
||
print_subsection("Invariants");
|
||
let lvl256 = empty_internal(TREE_DEPTH);
|
||
let level0_zero = lvl0 == [0u8; 32];
|
||
let mut monotonic_all_differ = true;
|
||
for k in 0..TREE_DEPTH {
|
||
if empty_internal(k) == empty_internal(k + 1) {
|
||
monotonic_all_differ = false;
|
||
print_warn(&format!("level {k} == level {}", k + 1));
|
||
}
|
||
}
|
||
let determinism = empty_internal(TREE_DEPTH) == lvl256;
|
||
print_kv("level_0_is_all_zero", format!("{level0_zero}"));
|
||
print_kv("all_consecutive_differ", format!("{monotonic_all_differ}"));
|
||
print_kv("determinism (re-query equal)", format!("{determinism}"));
|
||
|
||
print_subsection("Sanity: leaf_hash(\"hello\") определён и отличается от EMPTY_LEAF");
|
||
let leaf = leaf_hash(b"hello");
|
||
let internal = internal_hash(&lvl0, &lvl0);
|
||
print_kv("leaf_hash(b\"hello\")", hex_full(&leaf));
|
||
print_kv("internal_hash(zeros,zeros)", hex_full(&internal));
|
||
print_kv(
|
||
"leaf_hash == internal_hash(z,z)",
|
||
format!("{}", leaf == internal),
|
||
);
|
||
print_kv(
|
||
"leaf_hash == empty_internal(1)",
|
||
format!("{}", leaf == lvl1),
|
||
);
|
||
let leaf_vs_empty = leaf != lvl0;
|
||
print_kv("leaf_hash != EMPTY_LEAF", format!("{leaf_vs_empty}"));
|
||
|
||
let pass = level0_zero && monotonic_all_differ && determinism && leaf_vs_empty;
|
||
println!("\n[result] MERKLE: {}", if pass { "PASS" } else { "FAIL" });
|
||
pass
|
||
}
|
||
|
||
fn cmd_all() -> bool {
|
||
print_section("M1 CRYPTO — FULL USER JOURNEY");
|
||
print_note(
|
||
"Прогоняется полный путь: deterministic keypair test + 3 random keypairs + sign + hash + merkle",
|
||
);
|
||
|
||
let a = cmd_keypair_deterministic();
|
||
let b = cmd_keypair_random(3);
|
||
let c = cmd_sign("Montana M1 crypto shakedown — production journey");
|
||
let d = cmd_hash();
|
||
let e = cmd_merkle();
|
||
|
||
print_section("SUMMARY");
|
||
print_kv("keypair-deterministic", if a { "PASS" } else { "FAIL" });
|
||
print_kv("keypair-random", if b { "PASS" } else { "FAIL" });
|
||
print_kv("sign", if c { "PASS" } else { "FAIL" });
|
||
print_kv("hash", if d { "PASS" } else { "FAIL" });
|
||
print_kv("merkle-empty", if e { "PASS" } else { "FAIL" });
|
||
|
||
let pass = a && b && c && d && e;
|
||
println!(
|
||
"\n[result] ALL SCENARIOS: {}",
|
||
if pass { "PASS" } else { "FAIL" }
|
||
);
|
||
pass
|
||
}
|
||
|
||
fn bool_to_exit(pass: bool) -> ExitCode {
|
||
if pass {
|
||
ExitCode::SUCCESS
|
||
} else {
|
||
ExitCode::FAILURE
|
||
}
|
||
}
|
||
|
||
fn usage() {
|
||
eprintln!("M1 CRYPTO — ML-DSA-65 + SHA-256 + Sparse Merkle shakedown binary");
|
||
eprintln!();
|
||
eprintln!("usage: m1_crypto <subcommand> [args]");
|
||
eprintln!();
|
||
eprintln!(" keypair Deterministic keypair test (recovery sanity, default)");
|
||
eprintln!(" keypair-random [N] RANDOM N keypairs (default 1) + uniqueness check");
|
||
eprintln!(" sign [MSG] Keypair → sign(MSG) → verify + 4 adversarial теста");
|
||
eprintln!(" hash FIPS 180-4 vector + domain separation + registry полный");
|
||
eprintln!(" merkle-empty Sparse Merkle: все 257 levels + derivation trace");
|
||
eprintln!(" all Прогнать все подкоманды подряд (user journey)");
|
||
eprintln!();
|
||
eprintln!("Всё печатает байты в hex + xxd dump. Exit code 0=PASS, 1=FAIL.");
|
||
}
|
||
|
||
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 {
|
||
"keypair" => cmd_keypair_deterministic(),
|
||
"keypair-random" => {
|
||
let n: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(1);
|
||
cmd_keypair_random(n)
|
||
},
|
||
"sign" => {
|
||
let default_msg = String::from("Montana M1 crypto shakedown");
|
||
let msg = args.get(2).unwrap_or(&default_msg);
|
||
cmd_sign(msg)
|
||
},
|
||
"hash" => cmd_hash(),
|
||
"merkle-empty" => cmd_merkle(),
|
||
"all" => cmd_all(),
|
||
_ => {
|
||
usage();
|
||
return ExitCode::FAILURE;
|
||
},
|
||
};
|
||
bool_to_exit(pass)
|
||
}
|