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:
Generated
+1
-1
@@ -2207,7 +2207,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "signer"
|
||||
version = "0.0.19"
|
||||
version = "0.0.20"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"chacha20poly1305",
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user