diff --git a/Cargo.lock b/Cargo.lock index cf7ce63..1bd480e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2207,7 +2207,7 @@ dependencies = [ [[package]] name = "signer" -version = "0.0.19" +version = "0.0.20" dependencies = [ "base64", "chacha20poly1305", diff --git a/Cargo.toml b/Cargo.toml index 67e91bd..838b184 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "signer" -version = "0.0.19" +version = "0.0.20" edition = "2021" license = "MIT" description = "Attended Nostr signing daemon — Rust port of n_signer" diff --git a/src/alg_cache.rs b/src/alg_cache.rs index d0f2bdd..ea27d9b 100644 --- a/src/alg_cache.rs +++ b/src/alg_cache.rs @@ -64,17 +64,7 @@ impl AlgorithmKeyCache { let phrase = mnemonic.phrase().ok_or(SignerError::MnemonicNotLoaded)?; - // Build the standard derivation path for this algorithm - let path = match alg { - CryptoAlg::Secp256k1 => format!("m/44'/1237'/{}'/0/0", index), - CryptoAlg::Ed25519 => format!("m/44'/102001'/{}'/0'/0'", index), - CryptoAlg::X25519 => format!("m/44'/102002'/{}'/0'/0'", index), - CryptoAlg::MlDsa65 => format!("m/44'/102003'/{}'/0'/0'", index), - CryptoAlg::SlhDsa128s => format!("m/44'/102004'/{}'/0'/0'", index), - CryptoAlg::MlKem768 => format!("m/44'/102005'/{}'/0'/0'", index), - CryptoAlg::Unknown => return Err(SignerError::InvalidInput), - }; - + let path = standard_path(alg, index)?; let entry = derive_alg_key(phrase, &path, alg, index)?; self.entries.push(entry); Ok(()) @@ -92,6 +82,22 @@ impl Default for AlgorithmKeyCache { } } +/// The standard derivation path for an algorithm at a given index. +/// +/// secp256k1 uses the NIP-06 path; ed25519/x25519/PQ use their +/// per-algorithm SLIP-44 coin types (102001'–102005'). +pub fn standard_path(alg: CryptoAlg, index: i32) -> Result { + match alg { + CryptoAlg::Secp256k1 => Ok(format!("m/44'/1237'/{}'/0/0", index)), + CryptoAlg::Ed25519 => Ok(format!("m/44'/102001'/{}'/0'/0'", index)), + CryptoAlg::X25519 => Ok(format!("m/44'/102002'/{}'/0'/0'", index)), + CryptoAlg::MlDsa65 => Ok(format!("m/44'/102003'/{}'/0'/0'", index)), + CryptoAlg::SlhDsa128s => Ok(format!("m/44'/102004'/{}'/0'/0'", index)), + CryptoAlg::MlKem768 => Ok(format!("m/44'/102005'/{}'/0'/0'", index)), + CryptoAlg::Unknown => Err(SignerError::InvalidInput), + } +} + /// Derive a single algorithm key entry. fn derive_alg_key( mnemonic_phrase: &str, diff --git a/src/lib.rs b/src/lib.rs index e48cba7..ae64509 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,4 +31,4 @@ pub mod error; pub use error::SignerError; /// Version string (matches C NSIGNER_VERSION). -pub const VERSION: &str = "v0.0.19"; +pub const VERSION: &str = "v0.0.20"; diff --git a/src/server.rs b/src/server.rs index 3d33164..5bb7bdc 100644 --- a/src/server.rs +++ b/src/server.rs @@ -316,21 +316,28 @@ impl ServerContext { // get_info is metadata — no key material if method == crate::enforcement::VERB_GET_INFO { let response = crate::dispatcher::handle_request(dispatcher, request); - let activity = format!("{} - -", caller.caller_id); + let activity = format!("{} {}()", caller.caller_id, method); return (response, activity); } // Algorithm-based verbs (bypass role table) — no authorization if crate::enforcement::is_algorithm_verb(&method) { let response = self.process_algorithm_verb(dispatcher, request, &selector_req); - let activity = format!("{} - -", caller.caller_id); + // Activity: caller method(algorithm,index) — the algorithm's + // standard derivation path identifies the key material. + let (alg_name, path) = extract_algorithm_and_path(request); + let activity = match (alg_name, path) { + (Some(a), Some(p)) => format!("{} {}({},{} {})", caller.caller_id, method, a, selector_req.index, p), + (Some(a), None) => format!("{} {}({})", caller.caller_id, method, a), + _ => format!("{} {}()", caller.caller_id, method), + }; return (response, activity); } // OTP verbs if method == crate::enforcement::VERB_ENCRYPT || method == crate::enforcement::VERB_DECRYPT { let response = crate::dispatcher::handle_request(dispatcher, request); - let activity = format!("{} - -", caller.caller_id); + let activity = format!("{} {}()", caller.caller_id, method); return (response, activity); } @@ -509,6 +516,40 @@ fn extract_method_and_selector(request: &str) -> Option<(String, SelectorRequest Some((method, sel)) } +/// Extract the algorithm name and its standard derivation path from an +/// algorithm-verb request's options (for the activity log). +/// +/// Returns `(algorithm_name, Some(path))` when the request carries a valid +/// algorithm; `(None, None)` otherwise. +fn extract_algorithm_and_path(request: &str) -> (Option, Option) { + let root: serde_json::Value = match serde_json::from_str(request) { + Ok(v) => v, + Err(_) => return (None, None), + }; + let alg_str = root + .get("params") + .and_then(|p| p.as_array()) + .and_then(|p| p.last()) + .and_then(|o| o.get("algorithm")) + .and_then(|v| v.as_str()); + let Some(alg_str) = alg_str else { + return (None, None); + }; + let alg = crate::pq_crypto::CryptoAlg::from_str(alg_str); + if alg == crate::pq_crypto::CryptoAlg::Unknown { + return (Some(alg_str.to_string()), None); + } + let index = root + .get("params") + .and_then(|p| p.as_array()) + .and_then(|p| p.last()) + .and_then(|o| o.get("index")) + .and_then(|v| v.as_i64()) + .unwrap_or(0) as i32; + let path = crate::alg_cache::standard_path(alg, index).ok(); + (Some(alg.as_str().to_string()), path) +} + /// Build an auth error response. fn make_auth_error(request: &str, code: i32, message: &str) -> String { let id = extract_id(request); @@ -551,3 +592,86 @@ fn extract_id(request: &str) -> String { }) .unwrap_or_else(|| "null".to_string()) } + +#[cfg(test)] +mod tests { + use super::*; + + /// Build the activity line for an algorithm-verb request, exactly as + /// `process_request` does. + fn activity_for(request: &str) -> String { + let (method, selector_req) = + extract_method_and_selector(request).expect("valid request"); + assert!(crate::enforcement::is_algorithm_verb(&method)); + let (alg_name, path) = extract_algorithm_and_path(request); + let caller_id = "uid:1000"; + match (alg_name, path) { + (Some(a), Some(p)) => { + format!("{} {}({},{} {})", caller_id, method, a, selector_req.index, p) + } + (Some(a), None) => format!("{} {}({})", caller_id, method, a), + _ => format!("{} {}()", caller_id, method), + } + } + + #[test] + fn test_activity_all_algorithms() { + // Every algorithm must produce a non-blank activity line with its + // curve name and standard derivation path. + let cases = [ + ("secp256k1", "m/44'/1237'/0'/0/0"), + ("ed25519", "m/44'/102001'/0'/0'/0'"), + ("x25519", "m/44'/102002'/0'/0'/0'"), + ("ml-dsa-65", "m/44'/102003'/0'/0'/0'"), + ("slh-dsa-128s", "m/44'/102004'/0'/0'/0'"), + ("ml-kem-768", "m/44'/102005'/0'/0'/0'"), + ]; + for (alg, expected_path) in cases { + let req = format!( + r#"{{"id":"1","method":"get_public_key","params":[{{"algorithm":"{}","index":0}}]}}"#, + alg + ); + let activity = activity_for(&req); + assert!( + activity.contains(alg), + "activity '{}' must contain algorithm '{}'", + activity, + alg + ); + assert!( + activity.contains(expected_path), + "activity '{}' must contain path '{}'", + activity, + expected_path + ); + assert!( + !activity.contains("- -"), + "activity '{}' must not contain the blank placeholder", + activity + ); + } + } + + #[test] + fn test_activity_index_substituted() { + let req = r#"{"id":"1","method":"sign","params":["00ff",{"algorithm":"ml-dsa-65","index":7}]}"#; + let activity = activity_for(req); + assert!(activity.contains("m/44'/102003'/7'/0'/0'"), "activity: {}", activity); + assert!(activity.contains("sign(ml-dsa-65,7"), "activity: {}", activity); + } + + #[test] + fn test_activity_unknown_algorithm() { + let req = r#"{"id":"1","method":"sign","params":["00ff",{"algorithm":"bogus","index":0}]}"#; + let activity = activity_for(req); + // Unknown algorithm: name echoed, no path (dispatcher will reject). + assert!(activity.contains("sign(bogus)"), "activity: {}", activity); + } + + #[test] + fn test_activity_missing_algorithm() { + let req = r#"{"id":"1","method":"sign","params":["00ff",{}]}"#; + let activity = activity_for(req); + assert!(activity.contains("sign()"), "activity: {}", activity); + } +}