Copy the local key vault (src/, tests/) verbatim from alknet/crates/alknet-vault and create a standalone Cargo.toml (workspace-inherited fields inlined). Port the architecture docs (specs, ADRs 018-026, OQs 020-022) from alknet's nested multi-crate layout to a flat single-crate layout, fixing relative link paths. ADR and OQ numbers are preserved from alknet; a subsequent pass will renumber them to a per-project sequence (001, 002, ...) and rebrand alknet-vault -> alkvault (crate name, lib name, prose), updating the cross-references to non-vault alknet ADRs (003, 005, 008, 010, 014, 064) that are referenced in the copied docs but not copied over. Build, 108 tests, and clippy all pass clean.
390 lines
14 KiB
Rust
390 lines
14 KiB
Rust
//! Known-answer test vectors for BIP39, SLIP-0010, and AES-256-GCM.
|
|
//!
|
|
//! These tests verify that the cryptographic implementations produce correct
|
|
//! results against published reference vectors:
|
|
//!
|
|
//! - BIP39: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
|
|
//! - SLIP-0010: https://github.com/satoshilabs/slips/blob/master/slip-0010.md
|
|
//! - AES-256-GCM: NIST SP 800-38D
|
|
//!
|
|
//! ## SLIP-0010 Key Format Note
|
|
//!
|
|
//! The `ed25519-bip32` crate uses an extended key format (kL || kR || chain code)
|
|
//! internally. The private key bytes we extract are the first 32 bytes of the
|
|
//! extended key material, which differ from the raw SLIP-0010 test vector hex
|
|
//! because of the clamping that happens during extended key construction. Our
|
|
//! tests verify deterministic derivation and cross-consistency rather than
|
|
//! byte-for-byte matching against SLIP-0010 raw hex, since the crate's internal
|
|
//! representation handles clamping differently.
|
|
|
|
use alknet_vault::derivation::{derive_path_from_seed, PATHS};
|
|
use alknet_vault::mnemonic::{Language, Mnemonic};
|
|
use alknet_vault::protocol::KeyType;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// BIP39 Test Vectors
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// BIP39 test: known mnemonic with passphrase produces deterministic seed.
|
|
///
|
|
/// Uses the well-known "abandon...about" test vector from the BIP39 reference.
|
|
/// The seed is verified to be 64 bytes and deterministic.
|
|
#[test]
|
|
fn test_bip39_mnemonic_to_seed_with_passphrase() {
|
|
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
|
let mnemonic = Mnemonic::from_phrase(phrase, Language::English).unwrap();
|
|
|
|
// Seed with passphrase "TREZOR"
|
|
let seed_with_pass = mnemonic.to_seed(Some("TREZOR"));
|
|
|
|
// BIP39 seed must be 64 bytes
|
|
assert_eq!(
|
|
seed_with_pass.as_bytes().len(),
|
|
64,
|
|
"BIP39 seed must be 64 bytes"
|
|
);
|
|
|
|
// Deterministic: same mnemonic + same passphrase = same seed
|
|
let mnemonic2 = Mnemonic::from_phrase(phrase, Language::English).unwrap();
|
|
let seed2 = mnemonic2.to_seed(Some("TREZOR"));
|
|
assert_eq!(
|
|
seed_with_pass.as_bytes(),
|
|
seed2.as_bytes(),
|
|
"Same mnemonic + passphrase must produce same seed"
|
|
);
|
|
}
|
|
|
|
/// BIP39 test: known mnemonic with no passphrase (empty string).
|
|
#[test]
|
|
fn test_bip39_mnemonic_to_seed_no_passphrase() {
|
|
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
|
let mnemonic = Mnemonic::from_phrase(phrase, Language::English).unwrap();
|
|
let seed_no_pass = mnemonic.to_seed(None);
|
|
|
|
// Seed must be 64 bytes
|
|
assert_eq!(
|
|
seed_no_pass.as_bytes().len(),
|
|
64,
|
|
"BIP39 seed must be 64 bytes"
|
|
);
|
|
|
|
// Different passphrases produce different seeds
|
|
let mnemonic2 = Mnemonic::from_phrase(phrase, Language::English).unwrap();
|
|
let seed_with_pass = mnemonic2.to_seed(Some("TREZOR"));
|
|
assert_ne!(
|
|
seed_no_pass.as_bytes(),
|
|
seed_with_pass.as_bytes(),
|
|
"Seeds with different passphrases must differ"
|
|
);
|
|
}
|
|
|
|
/// BIP39 test: different mnemonics produce different seeds.
|
|
#[test]
|
|
fn test_bip39_different_mnemonics_different_seeds() {
|
|
// Use two different valid 24-word mnemonics
|
|
let mnemonic1 = Mnemonic::generate(24).unwrap();
|
|
let mnemonic2 = Mnemonic::generate(24).unwrap();
|
|
|
|
let seed1 = mnemonic1.to_seed(None);
|
|
let seed2 = mnemonic2.to_seed(None);
|
|
|
|
assert_ne!(
|
|
seed1.as_bytes(),
|
|
seed2.as_bytes(),
|
|
"Different mnemonics must produce different seeds"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SLIP-0010 Test Vectors (Ed25519)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// SLIP-0010 test: derive master key from a known seed.
|
|
///
|
|
/// Uses seed 0x000102...0f from SLIP-0010 Test Vector 1.
|
|
/// Verifies that derivation produces consistent, deterministic keys.
|
|
#[test]
|
|
fn test_slip0010_master_key_from_known_seed() {
|
|
// SLIP-0010 Test Vector 1 seed
|
|
let seed_hex = "000102030405060708090a0b0c0d0e0f";
|
|
let seed_bytes = hex::decode(seed_hex).unwrap();
|
|
|
|
// Derive the master key
|
|
let master = derive_path_from_seed(&seed_bytes, "m").unwrap();
|
|
|
|
// The master key must be 32 bytes for both private and public
|
|
assert_eq!(
|
|
master.private_key().len(),
|
|
32,
|
|
"Master private key must be 32 bytes"
|
|
);
|
|
assert_eq!(
|
|
master.public_key().len(),
|
|
32,
|
|
"Master public key must be 32 bytes"
|
|
);
|
|
|
|
// Derivation must be deterministic
|
|
let master2 = derive_path_from_seed(&seed_bytes, "m").unwrap();
|
|
assert_eq!(
|
|
master.private_key(),
|
|
master2.private_key(),
|
|
"Master key derivation must be deterministic"
|
|
);
|
|
assert_eq!(
|
|
master.public_key(),
|
|
master2.public_key(),
|
|
"Master public key derivation must be deterministic"
|
|
);
|
|
}
|
|
|
|
/// SLIP-0010 test: derive child key at m/0h from known seed.
|
|
///
|
|
/// Verifies that child derivation at the first level produces
|
|
/// deterministic results and differs from the master key.
|
|
#[test]
|
|
fn test_slip0010_child_key_m_0h() {
|
|
let seed_hex = "000102030405060708090a0b0c0d0e0f";
|
|
let seed_bytes = hex::decode(seed_hex).unwrap();
|
|
|
|
let child = derive_path_from_seed(&seed_bytes, "m/0'").unwrap();
|
|
|
|
// Must produce 32-byte keys
|
|
assert_eq!(child.private_key().len(), 32);
|
|
assert_eq!(child.public_key().len(), 32);
|
|
|
|
// Must differ from master key
|
|
let master = derive_path_from_seed(&seed_bytes, "m").unwrap();
|
|
assert_ne!(
|
|
child.private_key(),
|
|
master.private_key(),
|
|
"Child key must differ from master key"
|
|
);
|
|
|
|
// Must be deterministic
|
|
let child2 = derive_path_from_seed(&seed_bytes, "m/0'").unwrap();
|
|
assert_eq!(
|
|
child.private_key(),
|
|
child2.private_key(),
|
|
"Child key derivation must be deterministic"
|
|
);
|
|
}
|
|
|
|
/// SLIP-0010 test: derive child key at m/0h/1h/2h from known seed.
|
|
///
|
|
/// Verifies multi-level derivation produces deterministic results.
|
|
#[test]
|
|
fn test_slip0010_child_key_m_0h_1h_2h() {
|
|
let seed_hex = "000102030405060708090a0b0c0d0e0f";
|
|
let seed_bytes = hex::decode(seed_hex).unwrap();
|
|
|
|
let child = derive_path_from_seed(&seed_bytes, "m/0'/1'/2'").unwrap();
|
|
|
|
// Must produce 32-byte keys
|
|
assert_eq!(child.private_key().len(), 32);
|
|
assert_eq!(child.public_key().len(), 32);
|
|
|
|
// Must differ from shallower paths
|
|
let child_0 = derive_path_from_seed(&seed_bytes, "m/0'").unwrap();
|
|
let child_0_1 = derive_path_from_seed(&seed_bytes, "m/0'/1'").unwrap();
|
|
assert_ne!(
|
|
child.private_key(),
|
|
child_0.private_key(),
|
|
"Deeper path must differ from shallower"
|
|
);
|
|
assert_ne!(
|
|
child.private_key(),
|
|
child_0_1.private_key(),
|
|
"Each path must produce a unique key"
|
|
);
|
|
|
|
// Must be deterministic
|
|
let child2 = derive_path_from_seed(&seed_bytes, "m/0'/1'/2'").unwrap();
|
|
assert_eq!(child.private_key(), child2.private_key());
|
|
assert_eq!(child.public_key(), child2.public_key());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Cross-Consistency Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// End-to-end: mnemonic → seed → derived key at alknet identity path.
|
|
///
|
|
/// This test verifies that the full derivation stack produces consistent
|
|
/// results: given a known mnemonic, derive the seed, then derive the
|
|
/// identity key at m/74'/0'/0'/0'.
|
|
#[test]
|
|
fn test_cross_consistency_mnemonic_seed_derive_identity() {
|
|
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
|
let mnemonic = Mnemonic::from_phrase(phrase, Language::English).unwrap();
|
|
let seed = mnemonic.to_seed(None);
|
|
|
|
// Derive identity key at alknet path
|
|
let key = derive_path_from_seed(seed.as_bytes(), PATHS::IDENTITY).unwrap();
|
|
|
|
// Must be Ed25519 key length
|
|
assert_eq!(key.private_key().len(), 32, "Private key must be 32 bytes");
|
|
assert_eq!(key.public_key().len(), 32, "Public key must be 32 bytes");
|
|
|
|
// Must be deterministic: same mnemonic + same path = same key
|
|
let mnemonic2 = Mnemonic::from_phrase(phrase, Language::English).unwrap();
|
|
let seed2 = mnemonic2.to_seed(None);
|
|
let key2 = derive_path_from_seed(seed2.as_bytes(), PATHS::IDENTITY).unwrap();
|
|
|
|
assert_eq!(
|
|
key.private_key(),
|
|
key2.private_key(),
|
|
"Same seed + same path must produce same private key"
|
|
);
|
|
assert_eq!(
|
|
key.public_key(),
|
|
key2.public_key(),
|
|
"Same seed + same path must produce same public key"
|
|
);
|
|
}
|
|
|
|
/// Cross-consistency: different paths produce different keys from the same seed.
|
|
#[test]
|
|
fn test_cross_consistency_different_paths_different_keys() {
|
|
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
|
let mnemonic = Mnemonic::from_phrase(phrase, Language::English).unwrap();
|
|
let seed = mnemonic.to_seed(None);
|
|
|
|
let identity = derive_path_from_seed(seed.as_bytes(), PATHS::IDENTITY).unwrap();
|
|
let encryption = derive_path_from_seed(seed.as_bytes(), PATHS::ENCRYPTION).unwrap();
|
|
let ssh = derive_path_from_seed(seed.as_bytes(), PATHS::SSH_HOST).unwrap();
|
|
|
|
// All three must differ
|
|
assert_ne!(identity.private_key(), encryption.private_key());
|
|
assert_ne!(identity.private_key(), ssh.private_key());
|
|
assert_ne!(encryption.private_key(), ssh.private_key());
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AES-256-GCM Test Vectors
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// AES-256-GCM known-answer test using a known key and nonce.
|
|
///
|
|
/// Verifies that the `aes-gcm` crate produces correct results with a known
|
|
/// key, nonce, and plaintext. This is a sanity check for the primitive.
|
|
#[test]
|
|
fn test_aes256gcm_known_key_encrypt_decrypt() {
|
|
use aes_gcm::{
|
|
aead::{Aead, KeyInit},
|
|
Aes256Gcm, Nonce,
|
|
};
|
|
|
|
// Known 32-byte key
|
|
let key_bytes: [u8; 32] = [
|
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
|
|
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
|
|
0x1e, 0x1f,
|
|
];
|
|
|
|
let cipher = Aes256Gcm::new_from_slice(&key_bytes).unwrap();
|
|
|
|
// Known 12-byte nonce
|
|
let nonce_bytes: [u8; 12] = [
|
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
|
|
];
|
|
let nonce = Nonce::from_slice(&nonce_bytes);
|
|
|
|
let plaintext = b"hello, alknet vault!";
|
|
|
|
// Encrypt with known key and nonce
|
|
let ciphertext = cipher.encrypt(nonce, plaintext.as_ref()).unwrap();
|
|
|
|
// Decrypt with same key and nonce
|
|
let decrypted = cipher.decrypt(nonce, ciphertext.as_ref()).unwrap();
|
|
|
|
assert_eq!(
|
|
decrypted, plaintext,
|
|
"Decrypted plaintext must match original"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Alknet-specific regression tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Regression test: derive identity key at alknet path m/74'/0'/0'/0'
|
|
/// with a fixed seed, producing a known-answer result that we commit
|
|
/// as a regression test. If this test fails, the derivation algorithm
|
|
/// has changed.
|
|
#[test]
|
|
fn test_alknet_identity_path_regression() {
|
|
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
|
let mnemonic = Mnemonic::from_phrase(phrase, Language::English).unwrap();
|
|
let seed = mnemonic.to_seed(None);
|
|
|
|
let key = derive_path_from_seed(seed.as_bytes(), PATHS::IDENTITY).unwrap();
|
|
|
|
// Private and public keys must be 32 bytes
|
|
assert_eq!(key.private_key().len(), 32);
|
|
assert_eq!(key.public_key().len(), 32);
|
|
|
|
// The key must be non-zero
|
|
assert!(
|
|
key.private_key().iter().any(|&b| b != 0),
|
|
"Private key must not be all zeros"
|
|
);
|
|
assert!(
|
|
key.public_key().iter().any(|&b| b != 0),
|
|
"Public key must not be all zeros"
|
|
);
|
|
|
|
// Commit the expected hex values as a regression test.
|
|
// If these values change, the derivation has been altered.
|
|
let private_hex = hex::encode(key.private_key());
|
|
let public_hex = hex::encode(key.public_key());
|
|
|
|
// Derive again and verify determinism
|
|
let key2 = derive_path_from_seed(seed.as_bytes(), PATHS::IDENTITY).unwrap();
|
|
assert_eq!(hex::encode(key2.private_key()), private_hex);
|
|
assert_eq!(hex::encode(key2.public_key()), public_hex);
|
|
}
|
|
|
|
/// Regression test: derive encryption key at alknet path m/74'/2'/0'/0'
|
|
/// with a fixed seed, verifying determinism.
|
|
#[test]
|
|
fn test_alknet_encryption_path_regression() {
|
|
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
|
let mnemonic = Mnemonic::from_phrase(phrase, Language::English).unwrap();
|
|
let seed = mnemonic.to_seed(None);
|
|
|
|
let key = derive_path_from_seed(seed.as_bytes(), PATHS::ENCRYPTION).unwrap();
|
|
|
|
// Must be deterministic
|
|
let key2 = derive_path_from_seed(seed.as_bytes(), PATHS::ENCRYPTION).unwrap();
|
|
assert_eq!(key.private_key(), key2.private_key());
|
|
assert_eq!(key.public_key(), key2.public_key());
|
|
|
|
// Must differ from identity key
|
|
let identity = derive_path_from_seed(seed.as_bytes(), PATHS::IDENTITY).unwrap();
|
|
assert_ne!(key.private_key(), identity.private_key());
|
|
}
|
|
|
|
/// Verify that the VaultServiceHandle produces keys consistent with
|
|
/// direct derivation (integration test).
|
|
#[test]
|
|
fn test_service_derive_matches_direct_derivation() {
|
|
use alknet_vault::service::VaultServiceHandle;
|
|
|
|
let service = VaultServiceHandle::new();
|
|
let phrase = service.unlock_new(24).unwrap();
|
|
|
|
// Derive via service (which uses Mnemonic + Seed internally)
|
|
let service_key = service.derive_ed25519(PATHS::IDENTITY).unwrap();
|
|
|
|
// Derive directly from the same mnemonic
|
|
let mnemonic = Mnemonic::from_phrase(&phrase, Language::English).unwrap();
|
|
let seed = mnemonic.to_seed(None);
|
|
let direct_key = derive_path_from_seed(seed.as_bytes(), PATHS::IDENTITY).unwrap();
|
|
|
|
// Both methods must produce the same key
|
|
assert_eq!(service_key.key_type, KeyType::Ed25519);
|
|
assert_eq!(service_key.private_key, direct_key.private_key());
|
|
assert_eq!(service_key.public_key, direct_key.public_key());
|
|
}
|