v0.0.20 - Fixed blank activity log lines for algorithm-based verbs: all six algorithms now print method(algorithm,index path) with their standard derivation path; extracted standard_path() helper; added activity-format unit tests for all curves

This commit is contained in:
Laan Tungir
2026-08-22 07:10:30 -04:00
parent 03a977b774
commit b2f5d06570
5 changed files with 147 additions and 17 deletions
Generated
+1 -1
View File
@@ -2207,7 +2207,7 @@ dependencies = [
[[package]]
name = "signer"
version = "0.0.19"
version = "0.0.20"
dependencies = [
"base64",
"chacha20poly1305",
+1 -1
View File
@@ -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"
+17 -11
View File
@@ -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<String, SignerError> {
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,
+1 -1
View File
@@ -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";
+127 -3
View File
@@ -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<String>, Option<String>) {
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);
}
}