Files
signer/src/pq_crypto.rs
T

643 lines
26 KiB
Rust

//! Post-quantum crypto algorithm registry and operations.
//!
//! Port of `pq_crypto.c`. Provides the `CryptoAlg` enum, size constants,
//! and crypto operations for all six algorithms.
//!
//! PQ keygen uses the v2 FIPS seeded derivation scheme (see
//! `plans/pq_seeded_derivation_plan.md`): BIP-32 child bytes at the exact
//! seed length required by each algorithm feed the seeded keygen APIs
//! directly — no DRBG expansion. This matches the nostr_quantum_preparation
//! web app byte-for-byte (same mnemonic + path → same pubkeys).
// ── Algorithm Identifiers ────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CryptoAlg {
Secp256k1,
Ed25519,
X25519,
MlDsa65,
SlhDsa128s,
MlKem768,
Unknown,
}
impl CryptoAlg {
pub fn from_str(s: &str) -> Self {
match s {
"secp256k1" => Self::Secp256k1,
"ed25519" => Self::Ed25519,
"x25519" => Self::X25519,
"ml-dsa-65" => Self::MlDsa65,
"slh-dsa-128s" => Self::SlhDsa128s,
"ml-kem-768" => Self::MlKem768,
_ => Self::Unknown,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Secp256k1 => "secp256k1",
Self::Ed25519 => "ed25519",
Self::X25519 => "x25519",
Self::MlDsa65 => "ml-dsa-65",
Self::SlhDsa128s => "slh-dsa-128s",
Self::MlKem768 => "ml-kem-768",
Self::Unknown => "unknown",
}
}
}
// ── Key Sizes ────────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy)]
pub struct CryptoAlgSizes {
pub priv_key_len: usize,
pub pub_key_len: usize,
pub sig_len: usize, // 0 for KEM
pub ciphertext_len: usize, // 0 for signatures
pub shared_secret_len: usize, // 0 for signatures
}
impl CryptoAlg {
/// Sizes for each algorithm.
///
/// PQ private keys are stored in **seed form** (the preferred
/// serialization of the RustCrypto crates): ML-DSA-65 as the 32-byte ξ
/// seed, ML-KEM-768 as the 64-byte d ∥ z seed, SLH-DSA-128s as the
/// 64-byte sk serialization (sk.seed ∥ sk.prf ∥ pk). Public key,
/// signature, and ciphertext lengths are the standard FIPS sizes.
pub fn sizes(&self) -> Option<CryptoAlgSizes> {
match self {
Self::Secp256k1 => Some(CryptoAlgSizes {
priv_key_len: 32, pub_key_len: 32, sig_len: 64, ciphertext_len: 0, shared_secret_len: 0,
}),
Self::Ed25519 => Some(CryptoAlgSizes {
priv_key_len: 32, pub_key_len: 32, sig_len: 64, ciphertext_len: 0, shared_secret_len: 0,
}),
Self::X25519 => Some(CryptoAlgSizes {
priv_key_len: 32, pub_key_len: 32, sig_len: 0, ciphertext_len: 0, shared_secret_len: 32,
}),
Self::MlDsa65 => Some(CryptoAlgSizes {
priv_key_len: 32, pub_key_len: 1952, sig_len: 3309, ciphertext_len: 0, shared_secret_len: 0,
}),
Self::SlhDsa128s => Some(CryptoAlgSizes {
priv_key_len: 64, pub_key_len: 32, sig_len: 7856, ciphertext_len: 0, shared_secret_len: 0,
}),
Self::MlKem768 => Some(CryptoAlgSizes {
priv_key_len: 64, pub_key_len: 1184, sig_len: 0, ciphertext_len: 1088, shared_secret_len: 32,
}),
Self::Unknown => None,
}
}
}
// ── Crypto Operations ────────────────────────────────────────────────────────
/// BIP-32 path prefixes routed through BIP-32 derivation.
///
/// - `m/44'/1237'` — Nostr secp256k1 (NIP-06)
/// - `m/44'/102003'` … `m/44'/102007'` — PQ coin types (v2 seeded scheme;
/// 102003' ML-DSA-65, 102004' SLH-DSA-128s, 102005' ML-KEM-768,
/// 102006' ML-DSA-44 and 102007' Falcon-512 reserved)
///
/// Everything else — ed25519 (`102001'`) and x25519 (`102002'`) — uses
/// SLIP-0010, which is the correct derivation for those curves.
const BIP32_PATH_PREFIXES: &[&str] = &[
"m/44'/1237'",
"m/44'/102003'",
"m/44'/102004'",
"m/44'/102005'",
"m/44'/102006'",
"m/44'/102007'",
];
/// Derive a 32-byte seed from a mnemonic using a BIP-44 path.
///
/// For secp256k1 and PQ coin types: uses BIP-32 derivation.
/// For ed25519/x25519: uses SLIP-0010 (all-hardened).
///
/// Note: PQ keygen should use [`derive_pq_seed_from_path`] instead — it
/// produces the exact-length seed (32/48/64 bytes) required by the FIPS
/// seeded keygen APIs.
pub fn derive_seed_from_mnemonic(
mnemonic: &str,
path: &str,
) -> Result<[u8; 32], crate::SignerError> {
let seed = nips::nip006::mnemonic_to_seed(mnemonic, "");
// Parse the path
let path_indices = nips::nip006::parse_bip44_path(path)
.map_err(|_| crate::SignerError::KeyDerivationFailed)?;
// BIP-32 for secp256k1 (NIP-06) and PQ coin types (v2 seeded scheme);
// SLIP-0010 for ed25519/x25519 (correct for those curves).
if BIP32_PATH_PREFIXES.iter().any(|p| path.starts_with(p)) {
let (master_key, master_chain_code) = nips::nip006::bip32_master_key(&seed);
let (derived_key, _) = nips::nip006::bip32_derive_path(&master_key, &master_chain_code, &path_indices)
.map_err(|_| crate::SignerError::KeyDerivationFailed)?;
Ok(derived_key)
} else {
let (master_key, master_chain_code) = nips::nip006::slip10_master_key(&seed);
let (derived_key, _) = nips::nip006::slip10_derive_path(&master_key, &master_chain_code, &path_indices)
.map_err(|_| crate::SignerError::KeyDerivationFailed)?;
Ok(derived_key)
}
}
/// Derive a PQ keygen seed of `seed_len` bytes (32/48/64) from a BIP-32 path.
///
/// v2 seeded construction (matches nostr_quantum_preparation exactly):
/// - 32-byte seeds: the child private key at `path`.
/// - 48/64-byte seeds: the children at `path` and at the sibling path (last
/// level incremented by 1, hardened bit preserved) concatenated to 64
/// bytes, then truncated to the FIRST `seed_len` bytes.
///
/// The truncation rule is normative: taking the last 48 bytes or
/// concatenating in the opposite order produces different keys and breaks
/// seed-phrase recoverability.
pub fn derive_pq_seed_from_path(
mnemonic: &str,
path: &str,
seed_len: usize,
) -> Result<Vec<u8>, crate::SignerError> {
if !matches!(seed_len, 32 | 48 | 64) {
return Err(crate::SignerError::InvalidInput);
}
let bip39_seed = nips::nip006::mnemonic_to_seed(mnemonic, "");
let (master_key, master_chain_code) = nips::nip006::bip32_master_key(&bip39_seed);
let indices = nips::nip006::parse_bip44_path(path)
.map_err(|_| crate::SignerError::KeyDerivationFailed)?;
if indices.is_empty() {
return Err(crate::SignerError::KeyDerivationFailed);
}
let (child0, _) = nips::nip006::bip32_derive_path(&master_key, &master_chain_code, &indices)
.map_err(|_| crate::SignerError::KeyDerivationFailed)?;
if seed_len == 32 {
return Ok(child0.to_vec());
}
// Sibling path: last level + 1 (a plain u32 increment preserves the
// hardened bit: 0x80000000 + 1 = 0x80000001, i.e. hardened 1').
let mut sibling_indices = indices.clone();
let last = sibling_indices
.last_mut()
.ok_or(crate::SignerError::KeyDerivationFailed)?;
*last = last.wrapping_add(1);
let (child1, _) =
nips::nip006::bip32_derive_path(&master_key, &master_chain_code, &sibling_indices)
.map_err(|_| crate::SignerError::KeyDerivationFailed)?;
let mut combined = [0u8; 64];
combined[..32].copy_from_slice(&child0);
combined[32..].copy_from_slice(&child1);
Ok(combined[..seed_len].to_vec())
}
/// ed25519: derive keypair from a 32-byte seed.
pub fn ed25519_keygen_from_seed(seed: &[u8; 32]) -> ([u8; 32], [u8; 32]) {
use ed25519_dalek::{SigningKey, VerifyingKey};
let signing = SigningKey::from_bytes(seed);
let public: VerifyingKey = signing.verifying_key();
(*seed, public.to_bytes())
}
/// ed25519: sign a message. priv is 32-byte private key.
/// Returns 64-byte signature.
pub fn ed25519_sign(priv_key: &[u8; 32], msg: &[u8]) -> [u8; 64] {
use ed25519_dalek::{Signer, SigningKey};
let signing = SigningKey::from_bytes(priv_key);
let sig = signing.sign(msg);
sig.to_bytes()
}
/// ed25519: verify a signature. pub is 32-byte public key.
/// Returns true on valid, false on invalid.
pub fn ed25519_verify(pub_key: &[u8; 32], msg: &[u8], sig: &[u8; 64]) -> bool {
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
let verifying = match VerifyingKey::from_bytes(pub_key) {
Ok(k) => k,
Err(_) => return false,
};
let signature = Signature::from_bytes(sig);
verifying.verify(msg, &signature).is_ok()
}
/// x25519: derive keypair from a 32-byte seed.
pub fn x25519_keygen_from_seed(seed: &[u8; 32]) -> ([u8; 32], [u8; 32]) {
let secret = x25519_dalek::StaticSecret::from(*seed);
let public = x25519_dalek::PublicKey::from(&secret);
(*seed, public.to_bytes())
}
/// x25519: derive shared secret from our private key and peer's public key.
pub fn x25519_ecdh(our_priv: &[u8; 32], peer_pub: &[u8; 32]) -> [u8; 32] {
let secret = x25519_dalek::StaticSecret::from(*our_priv);
let public = x25519_dalek::PublicKey::from(*peer_pub);
secret.diffie_hellman(&public).to_bytes()
}
// ── secp256k1 ECDSA (not just Schnorr) ──────────────────────────────────────
/// secp256k1 ECDSA sign arbitrary bytes.
/// Hashes the message with SHA-256 before signing.
/// Returns 64-byte compact signature (r || s).
pub fn secp256k1_ecdsa_sign(priv_key: &[u8; 32], msg: &[u8]) -> Result<[u8; 64], crate::SignerError> {
use secp256k1::{Message, Secp256k1, SecretKey};
let secp = Secp256k1::new();
let sk = SecretKey::from_slice(priv_key).map_err(|_| crate::SignerError::CryptoFailed)?;
let hash = sha256(msg);
let msg = Message::from_digest_slice(&hash).map_err(|_| crate::SignerError::CryptoFailed)?;
let sig = secp.sign_ecdsa(&msg, &sk);
Ok(sig.serialize_compact())
}
/// secp256k1 ECDSA verify.
/// pub is 32-byte x-only pubkey (converted internally to compressed form).
/// sig is 64-byte compact (r || s).
pub fn secp256k1_ecdsa_verify(pub_key: &[u8; 32], msg: &[u8], sig: &[u8; 64]) -> bool {
use secp256k1::{Message, PublicKey, Secp256k1, ecdsa::Signature};
let secp = Secp256k1::new();
// x-only pubkey → compressed pubkey (prefix 0x02 for even)
let mut compressed = [0u8; 33];
compressed[0] = 0x02;
compressed[1..].copy_from_slice(pub_key);
let pk = match PublicKey::from_slice(&compressed) {
Ok(k) => k,
Err(_) => return false,
};
let hash = sha256(msg);
let msg = match Message::from_digest_slice(&hash) {
Ok(m) => m,
Err(_) => return false,
};
let signature = match Signature::from_compact(sig) {
Ok(s) => s,
Err(_) => return false,
};
// Also need the secret key to get the full public key for verification...
// Actually, we can verify with just the public key.
secp.verify_ecdsa(&msg, &signature, &pk).is_ok()
}
// ── PQ Crypto (FIPS seeded keygen — v2 scheme) ───────────────────────────────
//
// Keygen consumes the exact-length seed derived by `derive_pq_seed_from_path`
// (32 B ML-DSA, 48 B SLH-DSA, 64 B ML-KEM) via the RustCrypto seeded APIs.
// Private keys are stored in seed form (see `CryptoAlg::sizes`).
/// ML-DSA-65: generate keypair from a 32-byte seed (deterministic).
///
/// Returns (private_key = 32-byte ξ seed, public_key = 1952 bytes).
pub fn ml_dsa_65_keygen_from_seed(seed: &[u8; 32]) -> Result<(Vec<u8>, Vec<u8>), crate::SignerError> {
use ml_dsa::{MlDsa65, SigningKey, signature::Keypair};
let sk = SigningKey::<MlDsa65>::from_seed(seed.into());
let vk = sk.verifying_key();
Ok((sk.to_seed().to_vec(), vk.encode().to_vec()))
}
/// ML-DSA-65: sign a message. priv is the 32-byte ξ seed.
/// Returns the 3309-byte signature (deterministic FIPS 204 variant).
pub fn ml_dsa_65_sign(priv_key: &[u8], msg: &[u8]) -> Result<Vec<u8>, crate::SignerError> {
use ml_dsa::{MlDsa65, Seed, SigningKey, signature::Signer};
if priv_key.len() != 32 {
return Err(crate::SignerError::InvalidInput);
}
let seed: Seed = priv_key.try_into().map_err(|_| crate::SignerError::InvalidInput)?;
let sk = SigningKey::<MlDsa65>::from_seed(&seed);
let sig = sk.sign(msg);
Ok(sig.encode().to_vec())
}
/// ML-DSA-65: verify a signature. pub is the 1952-byte public key.
pub fn ml_dsa_65_verify(pub_key: &[u8], msg: &[u8], sig: &[u8]) -> bool {
use ml_dsa::{MlDsa65, Signature, VerifyingKey, signature::Verifier};
if pub_key.len() != 1952 {
return false;
}
let enc: ml_dsa::EncodedVerifyingKey<MlDsa65> =
match pub_key.try_into() {
Ok(e) => e,
Err(_) => return false,
};
let vk = VerifyingKey::<MlDsa65>::decode(&enc);
let signature = match Signature::<MlDsa65>::try_from(sig) {
Ok(s) => s,
Err(_) => return false,
};
vk.verify(msg, &signature).is_ok()
}
/// SLH-DSA-128s (SHA2 small): generate keypair from a 48-byte seed.
///
/// The seed splits as sk.seed(16) ∥ sk.prf(16) ∥ pk.seed(16) — matching
/// noble's `slh_dsa_sha2_128s.keygen(seed)` exactly.
/// Returns (private_key = 64-byte sk serialization, public_key = 32 bytes).
pub fn slh_dsa_128s_keygen_from_seed(seed: &[u8]) -> Result<(Vec<u8>, Vec<u8>), crate::SignerError> {
use slh_dsa::{Sha2_128s, SigningKey, signature::Keypair};
if seed.len() != 48 {
return Err(crate::SignerError::InvalidInput);
}
let sk = SigningKey::<Sha2_128s>::slh_keygen_internal(&seed[..16], &seed[16..32], &seed[32..48]);
let vk = sk.verifying_key();
Ok((sk.to_bytes().to_vec(), vk.to_bytes().to_vec()))
}
/// SLH-DSA-128s: sign a message. priv is the 64-byte sk serialization.
/// Returns the 7856-byte signature (deterministic: opt_rand = pk.seed).
pub fn slh_dsa_128s_sign(priv_key: &[u8], msg: &[u8]) -> Result<Vec<u8>, crate::SignerError> {
use slh_dsa::{Sha2_128s, SigningKey, signature::Signer};
let sk = SigningKey::<Sha2_128s>::try_from(priv_key)
.map_err(|_| crate::SignerError::InvalidInput)?;
let sig = sk.sign(msg);
Ok(sig.to_vec())
}
/// SLH-DSA-128s: verify a signature. pub is the 32-byte public key.
pub fn slh_dsa_128s_verify(pub_key: &[u8], msg: &[u8], sig: &[u8]) -> bool {
use slh_dsa::{Sha2_128s, Signature, VerifyingKey, signature::Verifier};
let vk = match VerifyingKey::<Sha2_128s>::try_from(pub_key) {
Ok(k) => k,
Err(_) => return false,
};
let signature = match Signature::<Sha2_128s>::try_from(sig) {
Ok(s) => s,
Err(_) => return false,
};
vk.verify(msg, &signature).is_ok()
}
/// ML-KEM-768: generate keypair from a 64-byte seed (deterministic).
///
/// The seed splits as d(32) ∥ z(32) — matching noble's `ml_kem768.keygen(seed)`.
/// Returns (private_key = 64-byte seed, public_key = 1184 bytes).
pub fn ml_kem_768_keygen_from_seed(seed: &[u8]) -> Result<(Vec<u8>, Vec<u8>), crate::SignerError> {
use ml_kem::ml_kem_768::DecapsulationKey;
use ml_kem::{KeyExport, Seed};
if seed.len() != 64 {
return Err(crate::SignerError::InvalidInput);
}
let seed: Seed = seed.try_into().map_err(|_| crate::SignerError::InvalidInput)?;
let dk = DecapsulationKey::from_seed(seed);
let ek = dk.encapsulation_key();
Ok((dk.to_seed().ok_or(crate::SignerError::CryptoFailed)?.to_vec(), ek.to_bytes().to_vec()))
}
/// ML-KEM-768: encapsulate. pub is the 1184-byte public key.
/// Returns (ciphertext[1088], shared_secret[32]). Uses OS randomness —
/// each encapsulation produces a different ciphertext, by design.
pub fn ml_kem_768_encaps(pub_key: &[u8]) -> Result<(Vec<u8>, [u8; 32]), crate::SignerError> {
use ml_kem::ml_kem_768::EncapsulationKey;
use ml_kem::kem::Encapsulate;
let ek = EncapsulationKey::new(
pub_key.try_into().map_err(|_| crate::SignerError::InvalidInput)?,
)
.map_err(|_| crate::SignerError::InvalidInput)?;
let (ct, ss) = ek.encapsulate();
Ok((ct.to_vec(), ss.into()))
}
/// ML-KEM-768: decapsulate. priv is the 64-byte seed, ct is the 1088-byte
/// ciphertext. Returns shared_secret[32].
pub fn ml_kem_768_decaps(priv_key: &[u8], ct: &[u8]) -> Result<[u8; 32], crate::SignerError> {
use ml_kem::ml_kem_768::DecapsulationKey;
use ml_kem::kem::Decapsulate;
use ml_kem::Seed;
if priv_key.len() != 64 {
return Err(crate::SignerError::InvalidInput);
}
let seed: Seed = priv_key.try_into().map_err(|_| crate::SignerError::InvalidInput)?;
let dk = DecapsulationKey::from_seed(seed);
let ct_arr = ct.try_into().map_err(|_| crate::SignerError::InvalidInput)?;
let ss = dk.decapsulate(&ct_arr);
Ok(ss.into())
}
// ── Helpers ─────────────────────────────────────────────────────────────────
/// SHA-256 hash (via nostr_core_lib_rust).
fn sha256(data: &[u8]) -> [u8; 32] {
nostr_core::crypto::sha256::sha256(data)
}
#[cfg(test)]
mod tests {
use super::*;
/// The fixed test mnemonic used by nostr_quantum_preparation's vectors.
const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
#[test]
fn test_alg_from_str() {
assert_eq!(CryptoAlg::from_str("secp256k1"), CryptoAlg::Secp256k1);
assert_eq!(CryptoAlg::from_str("ed25519"), CryptoAlg::Ed25519);
assert_eq!(CryptoAlg::from_str("ml-dsa-65"), CryptoAlg::MlDsa65);
assert_eq!(CryptoAlg::from_str("unknown"), CryptoAlg::Unknown);
}
#[test]
fn test_sizes() {
let s = CryptoAlg::Secp256k1.sizes().unwrap();
assert_eq!(s.priv_key_len, 32);
assert_eq!(s.pub_key_len, 32);
// PQ private keys are stored in seed form.
let s = CryptoAlg::MlDsa65.sizes().unwrap();
assert_eq!(s.priv_key_len, 32);
assert_eq!(s.pub_key_len, 1952);
assert_eq!(s.sig_len, 3309);
let s = CryptoAlg::SlhDsa128s.sizes().unwrap();
assert_eq!(s.priv_key_len, 64);
assert_eq!(s.pub_key_len, 32);
assert_eq!(s.sig_len, 7856);
let s = CryptoAlg::MlKem768.sizes().unwrap();
assert_eq!(s.priv_key_len, 64);
assert_eq!(s.pub_key_len, 1184);
assert_eq!(s.ciphertext_len, 1088);
assert_eq!(s.shared_secret_len, 32);
}
#[test]
fn test_ed25519_sign_verify() {
let seed = [0x42u8; 32];
let (priv_key, pub_key) = ed25519_keygen_from_seed(&seed);
let msg = b"hello world";
let sig = ed25519_sign(&priv_key, msg);
assert!(ed25519_verify(&pub_key, msg, &sig));
assert!(!ed25519_verify(&pub_key, b"wrong message", &sig));
}
#[test]
fn test_x25519_ecdh() {
let seed_a = [0x01u8; 32];
let seed_b = [0x02u8; 32];
let (priv_a, pub_a) = x25519_keygen_from_seed(&seed_a);
let (priv_b, pub_b) = x25519_keygen_from_seed(&seed_b);
let shared_a = x25519_ecdh(&priv_a, &pub_b);
let shared_b = x25519_ecdh(&priv_b, &pub_a);
assert_eq!(shared_a, shared_b);
}
// ── v2 seeded derivation ──────────────────────────────────────────
#[test]
fn test_pq_seed_lengths() {
// 32-byte seed: single child.
let s32 = derive_pq_seed_from_path(TEST_MNEMONIC, "m/44'/102003'/0'/0'/0'", 32).unwrap();
assert_eq!(s32.len(), 32);
// 48-byte seed: two children concatenated, first 48 of 64.
let s48 = derive_pq_seed_from_path(TEST_MNEMONIC, "m/44'/102004'/0'/0'/0'", 48).unwrap();
assert_eq!(s48.len(), 48);
// 64-byte seed: two children concatenated, all 64.
let s64 = derive_pq_seed_from_path(TEST_MNEMONIC, "m/44'/102005'/0'/0'/0'", 64).unwrap();
assert_eq!(s64.len(), 64);
// The 48-byte seed is a prefix of the 64-byte seed only when the
// paths share the same coin type — here they differ, so just check
// prefix consistency within the same coin type.
let s48b = derive_pq_seed_from_path(TEST_MNEMONIC, "m/44'/102005'/0'/0'/0'", 48).unwrap();
assert_eq!(&s64[..48], s48b.as_slice());
}
#[test]
fn test_pq_seed_determinism() {
let a = derive_pq_seed_from_path(TEST_MNEMONIC, "m/44'/102003'/0'/0'/0'", 32).unwrap();
let b = derive_pq_seed_from_path(TEST_MNEMONIC, "m/44'/102003'/0'/0'/0'", 32).unwrap();
assert_eq!(a, b);
// Different index → different seed.
let c = derive_pq_seed_from_path(TEST_MNEMONIC, "m/44'/102003'/0'/0'/1'", 32).unwrap();
assert_ne!(a, c);
}
#[test]
fn test_pq_seed_invalid_length() {
assert!(derive_pq_seed_from_path(TEST_MNEMONIC, "m/44'/102003'/0'/0'/0'", 33).is_err());
}
#[test]
fn test_pq_paths_use_bip32() {
// PQ coin types must route through BIP-32 (v2 scheme), not SLIP-0010.
// The 32-byte PQ seed equals the BIP-32 child at the same path.
let pq_seed =
derive_pq_seed_from_path(TEST_MNEMONIC, "m/44'/102003'/0'/0'/0'", 32).unwrap();
let bip32_seed =
derive_seed_from_mnemonic(TEST_MNEMONIC, "m/44'/102003'/0'/0'/0'").unwrap();
assert_eq!(pq_seed, bip32_seed.to_vec());
}
#[test]
fn test_ed25519_path_still_slip10() {
// ed25519 (102001') must remain SLIP-0010 — regression guard.
let seed = derive_seed_from_mnemonic(TEST_MNEMONIC, "m/44'/102001'/0'/0'/0'").unwrap();
let bip39_seed = nips::nip006::mnemonic_to_seed(TEST_MNEMONIC, "");
let (master_key, master_chain_code) = nips::nip006::slip10_master_key(&bip39_seed);
let path_indices = nips::nip006::parse_bip44_path("m/44'/102001'/0'/0'/0'").unwrap();
let (expected, _) =
nips::nip006::slip10_derive_path(&master_key, &master_chain_code, &path_indices)
.unwrap();
assert_eq!(seed, expected);
}
// ── PQ keygen / sign / verify / KEM roundtrips ────────────────────
#[test]
fn test_ml_dsa_65_roundtrip() {
let seed = [0x42u8; 32];
let (priv_key, pub_key) = ml_dsa_65_keygen_from_seed(&seed).unwrap();
assert_eq!(priv_key.len(), 32);
assert_eq!(pub_key.len(), 1952);
// Determinism: same seed → same keypair.
let (priv2, pub2) = ml_dsa_65_keygen_from_seed(&seed).unwrap();
assert_eq!(priv_key, priv2);
assert_eq!(pub_key, pub2);
let msg = b"hello world";
let sig = ml_dsa_65_sign(&priv_key, msg).unwrap();
assert_eq!(sig.len(), 3309);
assert!(ml_dsa_65_verify(&pub_key, msg, &sig));
assert!(!ml_dsa_65_verify(&pub_key, b"wrong message", &sig));
// Different seed → different key → verify fails.
let (_, pub_other) = ml_dsa_65_keygen_from_seed(&[0x99u8; 32]).unwrap();
assert!(!ml_dsa_65_verify(&pub_other, msg, &sig));
}
#[test]
fn test_slh_dsa_128s_roundtrip() {
let seed = [0x42u8; 48];
let (priv_key, pub_key) = slh_dsa_128s_keygen_from_seed(&seed).unwrap();
assert_eq!(priv_key.len(), 64);
assert_eq!(pub_key.len(), 32);
// Determinism.
let (priv2, pub2) = slh_dsa_128s_keygen_from_seed(&seed).unwrap();
assert_eq!(priv_key, priv2);
assert_eq!(pub_key, pub2);
let msg = b"hello world";
let sig = slh_dsa_128s_sign(&priv_key, msg).unwrap();
assert_eq!(sig.len(), 7856);
assert!(slh_dsa_128s_verify(&pub_key, msg, &sig));
assert!(!slh_dsa_128s_verify(&pub_key, b"wrong message", &sig));
// Deterministic signing: same key + msg → same signature
// (opt_rand defaults to pk.seed).
let sig2 = slh_dsa_128s_sign(&priv_key, msg).unwrap();
assert_eq!(sig, sig2);
}
#[test]
fn test_ml_kem_768_roundtrip() {
let seed = [0x42u8; 64];
let (priv_key, pub_key) = ml_kem_768_keygen_from_seed(&seed).unwrap();
assert_eq!(priv_key.len(), 64);
assert_eq!(pub_key.len(), 1184);
// Determinism.
let (priv2, pub2) = ml_kem_768_keygen_from_seed(&seed).unwrap();
assert_eq!(priv_key, priv2);
assert_eq!(pub_key, pub2);
// Encaps/decaps roundtrip.
let (ct, ss_send) = ml_kem_768_encaps(&pub_key).unwrap();
assert_eq!(ct.len(), 1088);
assert_eq!(ss_send.len(), 32);
let ss_recv = ml_kem_768_decaps(&priv_key, &ct).unwrap();
assert_eq!(ss_send, ss_recv);
// Encapsulation is randomized: two calls → different ciphertexts.
let (ct2, ss2) = ml_kem_768_encaps(&pub_key).unwrap();
assert_ne!(ct, ct2);
assert_ne!(ss_send.to_vec(), ss2.to_vec());
assert_eq!(ml_kem_768_decaps(&priv_key, &ct2).unwrap(), ss2);
}
#[test]
fn test_ml_kem_768_invalid_inputs() {
assert!(ml_kem_768_keygen_from_seed(&[0u8; 32]).is_err());
assert!(ml_kem_768_decaps(&[0u8; 64], &[0u8; 1087]).is_err());
assert!(ml_kem_768_encaps(&[0u8; 1183]).is_err());
}
}