162 lines
5.4 KiB
Rust
162 lines
5.4 KiB
Rust
|
|
// spec, раздел "Ключи → Мнемоника и seed". End-to-end recovery flow per [C-4]:
|
|||
|
|
// от entropy через mnemonic + master_seed + per-role HKDF до **terminal observable
|
|||
|
|
// outputs**: account_id (32B), node_id (32B), а также все 6 keypair частей
|
|||
|
|
// (account pk/sk, node pk/sk, mlkem pk/sk).
|
|||
|
|
//
|
|||
|
|
// Идемпотентность: повторный прогон ВСЕЙ цепочки на той же entropy производит
|
|||
|
|
// byte-identical terminal IDs и keypair bytes — это гарантия recovery flow
|
|||
|
|
// (восстановление identity на новом устройстве из 24 слов).
|
|||
|
|
|
|||
|
|
use mt_codec::domain;
|
|||
|
|
use mt_crypto::{
|
|||
|
|
keypair_from_seed, keypair_from_seed_mlkem, sha256_raw, MlkemPublicKey, MlkemSecretKey,
|
|||
|
|
PublicKey, SecretKey, SuiteId,
|
|||
|
|
};
|
|||
|
|
use mt_mnemonic::{
|
|||
|
|
entropy_to_mnemonic, mldsa_seed_for_role, mlkem_seed_for_role, mnemonic_to_master_seed,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Terminal observable IDs (per spec):
|
|||
|
|
//
|
|||
|
|
// account_id = SHA-256("mt-account" || suite_id_bytes (LE u16) || pk_acc)
|
|||
|
|
// node_id = SHA-256("mt-node" || pk_node)
|
|||
|
|
|
|||
|
|
fn 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 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 Identity {
|
|||
|
|
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_identity(entropy: &[u8; 32]) -> Identity {
|
|||
|
|
// 1-2: entropy → mnemonic
|
|||
|
|
let mnemonic = entropy_to_mnemonic(entropy);
|
|||
|
|
// 3: mnemonic → master_seed (PBKDF2-HMAC-SHA-256, 2^20 iter)
|
|||
|
|
let master_seed = mnemonic_to_master_seed(&mnemonic).expect("valid mnemonic");
|
|||
|
|
|
|||
|
|
// 4-5: account keypair
|
|||
|
|
let acc_seed = mldsa_seed_for_role(&master_seed, domain::ACCOUNT_KEY);
|
|||
|
|
let (pk_acc, sk_acc) = keypair_from_seed(&acc_seed).expect("account keygen");
|
|||
|
|
// 6: account_id terminal
|
|||
|
|
let aid = account_id(&pk_acc);
|
|||
|
|
|
|||
|
|
// 7-8: node keypair
|
|||
|
|
let node_seed = mldsa_seed_for_role(&master_seed, domain::NODE_KEY);
|
|||
|
|
let (pk_node, sk_node) = keypair_from_seed(&node_seed).expect("node keygen");
|
|||
|
|
// 9: node_id terminal
|
|||
|
|
let nid = node_id(&pk_node);
|
|||
|
|
|
|||
|
|
// 10-11: app encryption keypair (ML-KEM-768)
|
|||
|
|
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("mlkem keygen");
|
|||
|
|
|
|||
|
|
Identity {
|
|||
|
|
pk_acc,
|
|||
|
|
sk_acc,
|
|||
|
|
pk_node,
|
|||
|
|
sk_node,
|
|||
|
|
pk_mlkem,
|
|||
|
|
sk_mlkem,
|
|||
|
|
account_id: aid,
|
|||
|
|
node_id: nid,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn e2e_recovery_terminal_observable_byte_exact() {
|
|||
|
|
// Step 1: entropy = [0xAB; 32] — фиксированный test input
|
|||
|
|
let entropy = [0xABu8; 32];
|
|||
|
|
|
|||
|
|
// Steps 2-11: первое derivation
|
|||
|
|
let id1 = derive_identity(&entropy);
|
|||
|
|
|
|||
|
|
// Step 12: ПОВТОРИТЬ шаги 2-11 → второе derivation
|
|||
|
|
let id2 = derive_identity(&entropy);
|
|||
|
|
|
|||
|
|
// Steps 13-14: terminal IDs byte-exact
|
|||
|
|
assert_eq!(
|
|||
|
|
id1.account_id, id2.account_id,
|
|||
|
|
"account_id terminal mismatch"
|
|||
|
|
);
|
|||
|
|
assert_eq!(id1.node_id, id2.node_id, "node_id terminal mismatch");
|
|||
|
|
|
|||
|
|
// Step 15: все 6 key parts byte-exact
|
|||
|
|
assert_eq!(
|
|||
|
|
id1.pk_acc.as_bytes(),
|
|||
|
|
id2.pk_acc.as_bytes(),
|
|||
|
|
"account pk mismatch"
|
|||
|
|
);
|
|||
|
|
assert_eq!(
|
|||
|
|
id1.sk_acc.as_bytes(),
|
|||
|
|
id2.sk_acc.as_bytes(),
|
|||
|
|
"account sk mismatch"
|
|||
|
|
);
|
|||
|
|
assert_eq!(
|
|||
|
|
id1.pk_node.as_bytes(),
|
|||
|
|
id2.pk_node.as_bytes(),
|
|||
|
|
"node pk mismatch"
|
|||
|
|
);
|
|||
|
|
assert_eq!(
|
|||
|
|
id1.sk_node.as_bytes(),
|
|||
|
|
id2.sk_node.as_bytes(),
|
|||
|
|
"node sk mismatch"
|
|||
|
|
);
|
|||
|
|
assert_eq!(
|
|||
|
|
id1.pk_mlkem.as_bytes(),
|
|||
|
|
id2.pk_mlkem.as_bytes(),
|
|||
|
|
"mlkem pk mismatch"
|
|||
|
|
);
|
|||
|
|
assert_eq!(
|
|||
|
|
id1.sk_mlkem.as_bytes(),
|
|||
|
|
id2.sk_mlkem.as_bytes(),
|
|||
|
|
"mlkem sk mismatch"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn e2e_recovery_distinct_entropies_produce_distinct_terminals() {
|
|||
|
|
let id1 = derive_identity(&[0x00u8; 32]);
|
|||
|
|
let id2 = derive_identity(&[0xFFu8; 32]);
|
|||
|
|
assert_ne!(id1.account_id, id2.account_id);
|
|||
|
|
assert_ne!(id1.node_id, id2.node_id);
|
|||
|
|
assert_ne!(id1.pk_acc.as_bytes(), id2.pk_acc.as_bytes());
|
|||
|
|
assert_ne!(id1.pk_node.as_bytes(), id2.pk_node.as_bytes());
|
|||
|
|
assert_ne!(id1.pk_mlkem.as_bytes(), id2.pk_mlkem.as_bytes());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn e2e_recovery_account_node_keys_differ_from_same_master() {
|
|||
|
|
// Domain separation per HKDF info: ACCOUNT_KEY vs NODE_KEY → разные ML-DSA seeds
|
|||
|
|
// → разные pk → разные account_id / node_id даже на одной мнемонике.
|
|||
|
|
let id = derive_identity(&[0x42u8; 32]);
|
|||
|
|
assert_ne!(
|
|||
|
|
id.pk_acc.as_bytes(),
|
|||
|
|
id.pk_node.as_bytes(),
|
|||
|
|
"account и node pk должны различаться (HKDF domain separation)"
|
|||
|
|
);
|
|||
|
|
assert_ne!(
|
|||
|
|
id.account_id, id.node_id,
|
|||
|
|
"account_id и node_id должны различаться"
|
|||
|
|
);
|
|||
|
|
}
|