105 lines
3.4 KiB
Rust
105 lines
3.4 KiB
Rust
//! Cross-implementation PQ keygen conformance.
|
|
//!
|
|
//! Validates the v2 seeded derivation against the pinned vectors from
|
|
//! nostr_quantum_preparation (`test/vectors/seed-to-pubkeys.v2.json`):
|
|
//! the same fixed mnemonic must produce the same PQ public keys in the
|
|
//! web app (noble) and here (RustCrypto crates).
|
|
//!
|
|
//! The test is skipped (passes with a note) when the vector file does not
|
|
//! exist yet — coordinate generation with the web app project
|
|
//! (`test/vectors/generate-vectors.mjs`).
|
|
|
|
use signer::pq_crypto;
|
|
|
|
const VECTOR_PATH: &str = concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/../nostr_quantum_preparation/test/vectors/seed-to-pubkeys.v2.json"
|
|
);
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct Vector {
|
|
mnemonic: String,
|
|
#[serde(rename = "derivedPublicKeys")]
|
|
derived_public_keys: DerivedPublicKeys,
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct DerivedPublicKeys {
|
|
#[serde(rename = "ml-dsa-65")]
|
|
ml_dsa_65: KeyEntry,
|
|
#[serde(rename = "slh-dsa-128s")]
|
|
slh_dsa_128s: KeyEntry,
|
|
#[serde(rename = "ml-kem-768")]
|
|
ml_kem_768: KeyEntry,
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct KeyEntry {
|
|
#[serde(rename = "derivationPath")]
|
|
derivation_path: String,
|
|
#[serde(rename = "publicKeyHex")]
|
|
public_key_hex: String,
|
|
}
|
|
|
|
/// Extract the base path (strip the trailing leaf index and any sibling
|
|
/// notation like "+ m/44'/102004'/0'/0'/1'").
|
|
fn base_path(full: &str) -> String {
|
|
full.split(" + ").next().unwrap_or(full).to_string()
|
|
}
|
|
|
|
#[test]
|
|
fn pq_pubkeys_match_web_app_v2_vectors() {
|
|
let data = match std::fs::read_to_string(VECTOR_PATH) {
|
|
Ok(d) => d,
|
|
Err(_) => {
|
|
eprintln!("SKIP: {VECTOR_PATH} not found — generate it in nostr_quantum_preparation");
|
|
return;
|
|
}
|
|
};
|
|
let vector: Vector = serde_json::from_str(&data)
|
|
.expect("valid v2 vector JSON");
|
|
|
|
// ML-DSA-65: 32-byte seed from one child.
|
|
let seed = pq_crypto::derive_pq_seed_from_path(
|
|
&vector.mnemonic,
|
|
&base_path(&vector.derived_public_keys.ml_dsa_65.derivation_path),
|
|
32,
|
|
)
|
|
.unwrap();
|
|
let seed_arr: [u8; 32] = seed.as_slice().try_into().unwrap();
|
|
let (_, pub_key) = pq_crypto::ml_dsa_65_keygen_from_seed(&seed_arr).unwrap();
|
|
assert_eq!(
|
|
hex::encode(&pub_key),
|
|
vector.derived_public_keys.ml_dsa_65.public_key_hex,
|
|
"ml-dsa-65 pubkey must match the web app vector"
|
|
);
|
|
|
|
// SLH-DSA-128s: 48-byte seed from two children (first 48 of 64).
|
|
let seed = pq_crypto::derive_pq_seed_from_path(
|
|
&vector.mnemonic,
|
|
&base_path(&vector.derived_public_keys.slh_dsa_128s.derivation_path),
|
|
48,
|
|
)
|
|
.unwrap();
|
|
let (_, pub_key) = pq_crypto::slh_dsa_128s_keygen_from_seed(&seed).unwrap();
|
|
assert_eq!(
|
|
hex::encode(&pub_key),
|
|
vector.derived_public_keys.slh_dsa_128s.public_key_hex,
|
|
"slh-dsa-128s pubkey must match the web app vector"
|
|
);
|
|
|
|
// ML-KEM-768: 64-byte seed from two children.
|
|
let seed = pq_crypto::derive_pq_seed_from_path(
|
|
&vector.mnemonic,
|
|
&base_path(&vector.derived_public_keys.ml_kem_768.derivation_path),
|
|
64,
|
|
)
|
|
.unwrap();
|
|
let (_, pub_key) = pq_crypto::ml_kem_768_keygen_from_seed(&seed).unwrap();
|
|
assert_eq!(
|
|
hex::encode(&pub_key),
|
|
vector.derived_public_keys.ml_kem_768.public_key_hex,
|
|
"ml-kem-768 pubkey must match the web app vector"
|
|
);
|
|
}
|