643 lines
26 KiB
Rust
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());
|
|
}
|
|
}
|