765 lines
28 KiB
Rust
765 lines
28 KiB
Rust
//! Dispatcher — JSON-RPC 2.0 request routing.
|
|
//!
|
|
//! Port of `dispatcher.c`. Routes verbs to the appropriate handler
|
|
//! (algorithm-based, nostr, OTP, or metadata).
|
|
|
|
use crate::alg_cache::AlgorithmKeyCache;
|
|
use crate::enforcement;
|
|
use crate::error::RpcError;
|
|
use crate::key_store::KeyStore;
|
|
use crate::mnemonic::MnemonicState;
|
|
use crate::pq_crypto::CryptoAlg;
|
|
use crate::role_table::RoleTable;
|
|
use crate::selector::{selector_resolve, SelectorRequest};
|
|
use crate::SignerError;
|
|
|
|
use serde_json::{json, Value};
|
|
|
|
use base64::Engine;
|
|
|
|
/// Dispatcher context — holds references to shared state.
|
|
pub struct DispatcherContext<'a> {
|
|
pub role_table: &'a mut RoleTable,
|
|
pub mnemonic: &'a MnemonicState,
|
|
pub key_store: &'a mut KeyStore,
|
|
pub alg_key_cache: &'a mut AlgorithmKeyCache,
|
|
}
|
|
|
|
/// Process a JSON-RPC request string and produce a JSON-RPC response string.
|
|
///
|
|
/// Response format on success: `{"id":"...","result":"..."}`
|
|
/// Response format on error: `{"id":"...","error":{"code":N,"message":"..."}}`
|
|
pub fn handle_request(ctx: &mut DispatcherContext, json_request: &str) -> String {
|
|
let root: Value = match serde_json::from_str(json_request) {
|
|
Ok(v) => v,
|
|
Err(_) => return make_error_response("null", RpcError::PARSE_ERROR),
|
|
};
|
|
|
|
let id = root
|
|
.get("id")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("null")
|
|
.to_string();
|
|
|
|
let method = match root.get("method").and_then(|v| v.as_str()) {
|
|
Some(m) => m,
|
|
None => return make_error_response(&id, RpcError::INVALID_REQUEST),
|
|
};
|
|
|
|
let params = match root.get("params") {
|
|
Some(p) if p.is_array() => p.as_array().unwrap(),
|
|
_ => return make_error_response(&id, RpcError::INVALID_REQUEST),
|
|
};
|
|
|
|
// Extract options (last param if it's an object)
|
|
let options = params
|
|
.last()
|
|
.and_then(|v| if v.is_object() { Some(v) } else { None });
|
|
|
|
// ── Route ──────────────────────────────────────────────────────────
|
|
|
|
// Algorithm-based verbs (bypass role table)
|
|
if enforcement::is_algorithm_verb(method) {
|
|
return handle_algorithm_verb(ctx, &id, method, params, options);
|
|
}
|
|
|
|
// get_info (metadata; no key material)
|
|
if method == enforcement::VERB_GET_INFO {
|
|
return handle_get_info(&id);
|
|
}
|
|
|
|
// OTP verbs (encrypt/decrypt with algorithm:"otp")
|
|
if method == enforcement::VERB_ENCRYPT || method == enforcement::VERB_DECRYPT {
|
|
if let Some(opts) = options {
|
|
if opts.get("algorithm").and_then(|v| v.as_str()) == Some("otp") {
|
|
return handle_otp_verb(&id, method, params);
|
|
}
|
|
}
|
|
// Non-OTP encrypt/decrypt with other algorithms
|
|
return make_error_response(&id, RpcError::ALGORITHM_NOT_SUPPORTED);
|
|
}
|
|
|
|
// Nostr verbs (role-based)
|
|
if is_nostr_verb(method) {
|
|
return handle_nostr_verb(ctx, &id, method, params, options);
|
|
}
|
|
|
|
make_error_response(&id, RpcError::METHOD_NOT_FOUND)
|
|
}
|
|
|
|
// ── Algorithm Verb Handler ───────────────────────────────────────────────────
|
|
|
|
fn handle_algorithm_verb(
|
|
ctx: &mut DispatcherContext,
|
|
id: &str,
|
|
method: &str,
|
|
params: &[Value],
|
|
options: Option<&Value>,
|
|
) -> String {
|
|
let alg = match options.and_then(|o| o.get("algorithm")).and_then(|v| v.as_str()) {
|
|
Some(s) => CryptoAlg::from_str(s),
|
|
None => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
|
|
if alg == CryptoAlg::Unknown {
|
|
return make_error_response(
|
|
id,
|
|
RpcError {
|
|
code: -32602,
|
|
message: "missing_or_invalid_algorithm",
|
|
},
|
|
);
|
|
}
|
|
|
|
let index = options
|
|
.and_then(|o| o.get("index"))
|
|
.and_then(|v| v.as_i64())
|
|
.unwrap_or(0) as i32;
|
|
|
|
// Enforcement: check verb+algorithm validity
|
|
if let Err(_) = enforcement::enforce_verb_algorithm(method, alg) {
|
|
return make_error_response(id, RpcError::ALGORITHM_NOT_SUPPORTED);
|
|
}
|
|
|
|
// Mnemonic must be loaded
|
|
if !ctx.mnemonic.is_loaded() {
|
|
return make_error_response(id, RpcError::MNEMONIC_NOT_LOADED);
|
|
}
|
|
|
|
// Derive the key on demand
|
|
if let Err(_) = ctx.alg_key_cache.derive(ctx.mnemonic, alg, index) {
|
|
return make_error_response(
|
|
id,
|
|
RpcError {
|
|
code: -32602,
|
|
message: "key_derivation_failed",
|
|
},
|
|
);
|
|
}
|
|
|
|
let key_entry = match ctx.alg_key_cache.get(alg, index) {
|
|
Some(e) => e,
|
|
None => {
|
|
return make_error_response(
|
|
id,
|
|
RpcError {
|
|
code: -32602,
|
|
message: "key_derivation_failed",
|
|
},
|
|
)
|
|
}
|
|
};
|
|
|
|
let alg_name = alg.as_str();
|
|
let key_id = &key_entry.key_id;
|
|
|
|
// ── Dispatch by verb ──────────────────────────────────────────────
|
|
|
|
match method {
|
|
enforcement::VERB_GET_PUBLIC_KEY => {
|
|
let result = json!({
|
|
"algorithm": alg_name,
|
|
"public_key": key_entry.pubkey_hex,
|
|
"key_id": key_id,
|
|
});
|
|
make_success_response(id, &result.to_string())
|
|
}
|
|
|
|
enforcement::VERB_SIGN => {
|
|
let msg_hex = match params.first().and_then(|v| v.as_str()) {
|
|
Some(s) => s,
|
|
None => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
let msg_bytes = match hex::decode(msg_hex) {
|
|
Ok(b) => b,
|
|
Err(_) => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
if msg_bytes.is_empty() {
|
|
return make_error_response(id, RpcError::INVALID_PARAMS);
|
|
}
|
|
|
|
let priv_slice = key_entry.private_key.as_slice();
|
|
let sig = sign_with_alg(alg, &priv_slice[..32].try_into().unwrap(), &msg_bytes);
|
|
match sig {
|
|
Ok(s) => {
|
|
let sig_hex = hex::encode(&s);
|
|
let result = json!({
|
|
"algorithm": alg_name,
|
|
"key_id": key_id,
|
|
"signature": sig_hex,
|
|
});
|
|
make_success_response(id, &result.to_string())
|
|
}
|
|
Err(_) => make_error_response(
|
|
id,
|
|
RpcError {
|
|
code: -32602,
|
|
message: "signing_failed",
|
|
},
|
|
),
|
|
}
|
|
}
|
|
|
|
enforcement::VERB_VERIFY => {
|
|
let msg_hex = match params.first().and_then(|v| v.as_str()) {
|
|
Some(s) => s,
|
|
None => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
let sig_hex = match params.get(1).and_then(|v| v.as_str()) {
|
|
Some(s) => s,
|
|
None => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
let msg_bytes = match hex::decode(msg_hex) {
|
|
Ok(b) => b,
|
|
Err(_) => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
let sig_bytes = match hex::decode(sig_hex) {
|
|
Ok(b) => b,
|
|
Err(_) => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
|
|
let pub_slice = key_entry.public_key.as_slice();
|
|
let valid = verify_with_alg(alg, pub_slice, &msg_bytes, &sig_bytes);
|
|
let result = json!({
|
|
"valid": valid,
|
|
"algorithm": alg_name,
|
|
});
|
|
make_success_response(id, &result.to_string())
|
|
}
|
|
|
|
enforcement::VERB_DERIVE_SHARED => {
|
|
if alg != CryptoAlg::X25519 {
|
|
return make_error_response(id, RpcError::ALGORITHM_NOT_SUPPORTED);
|
|
}
|
|
let peer_hex = match params.first().and_then(|v| v.as_str()) {
|
|
Some(s) => s,
|
|
None => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
let peer_bytes = match hex::decode(peer_hex) {
|
|
Ok(b) if b.len() == 32 => b,
|
|
_ => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
|
|
let priv_slice = key_entry.private_key.as_slice();
|
|
let mut priv_arr = [0u8; 32];
|
|
priv_arr.copy_from_slice(&priv_slice[..32]);
|
|
let peer_arr: [u8; 32] = peer_bytes[..32].try_into().unwrap();
|
|
let shared = crate::pq_crypto::x25519_ecdh(&priv_arr, &peer_arr);
|
|
let shared_hex = hex::encode(&shared);
|
|
let result = json!({
|
|
"shared_secret": shared_hex,
|
|
"algorithm": "x25519",
|
|
});
|
|
make_success_response(id, &result.to_string())
|
|
}
|
|
|
|
enforcement::VERB_DERIVE => {
|
|
// HMAC-SHA256(privkey, data) — index is required
|
|
let data_str = match params.first().and_then(|v| v.as_str()) {
|
|
Some(s) => s,
|
|
None => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
// index is required for derive (no default)
|
|
let has_index = options
|
|
.and_then(|o| o.get("index"))
|
|
.and_then(|v| v.as_i64())
|
|
.is_some();
|
|
if !has_index {
|
|
return make_error_response(
|
|
id,
|
|
RpcError {
|
|
code: -32602,
|
|
message: "missing_index",
|
|
},
|
|
);
|
|
}
|
|
|
|
let priv_slice = key_entry.private_key.as_slice();
|
|
let mac = nostr_core::crypto::hmac::hmac_sha256(priv_slice, data_str.as_bytes());
|
|
let mac_hex = hex::encode(&mac);
|
|
let result = json!({
|
|
"algorithm": alg_name,
|
|
"key_id": key_id,
|
|
"digest": mac_hex,
|
|
});
|
|
make_success_response(id, &result.to_string())
|
|
}
|
|
|
|
enforcement::VERB_ENCAPSULATE => {
|
|
// ML-KEM-768 only — TODO: Phase 13
|
|
make_error_response(id, RpcError::NOT_YET_IMPLEMENTED)
|
|
}
|
|
|
|
enforcement::VERB_DECAPSULATE => {
|
|
// ML-KEM-768 only — TODO: Phase 13
|
|
make_error_response(id, RpcError::NOT_YET_IMPLEMENTED)
|
|
}
|
|
|
|
_ => make_error_response(id, RpcError::METHOD_NOT_FOUND),
|
|
}
|
|
}
|
|
|
|
// ── Nostr Verb Handler ───────────────────────────────────────────────────────
|
|
|
|
fn handle_nostr_verb(
|
|
ctx: &mut DispatcherContext,
|
|
id: &str,
|
|
method: &str,
|
|
params: &[Value],
|
|
options: Option<&Value>,
|
|
) -> String {
|
|
// Parse selector from options
|
|
let mut sel = SelectorRequest::new();
|
|
if let Some(opts) = options {
|
|
if let Some(role) = opts.get("role").and_then(|v| v.as_str()) {
|
|
sel.has_role = true;
|
|
sel.role_name = role.to_string();
|
|
}
|
|
if let Some(path) = opts.get("role_path").and_then(|v| v.as_str()) {
|
|
sel.has_role_path = true;
|
|
sel.role_path = path.to_string();
|
|
}
|
|
}
|
|
|
|
// Resolve selector
|
|
let role_index = match selector_resolve(&sel, ctx.role_table) {
|
|
Ok(i) => i,
|
|
Err(e) => {
|
|
return make_error_response(
|
|
id,
|
|
match e {
|
|
crate::selector::SelectorError::Ambiguous => RpcError::AMBIGUOUS_ROLE_SELECTOR,
|
|
crate::selector::SelectorError::NotFound => RpcError::UNKNOWN_ROLE,
|
|
crate::selector::SelectorError::NoDefault => RpcError::NO_DEFAULT_ROLE,
|
|
crate::selector::SelectorError::PathMismatch => RpcError::PATH_NOT_ALLOWED,
|
|
crate::selector::SelectorError::RoleRequired => RpcError::ROLE_REQUIRED,
|
|
crate::selector::SelectorError::PathRequired => RpcError::PATH_REQUIRED,
|
|
_ => RpcError::INVALID_PARAMS,
|
|
},
|
|
)
|
|
}
|
|
};
|
|
|
|
// Enforce verb+role
|
|
let role = &ctx.role_table.entries[role_index];
|
|
if let Err(_) = enforcement::enforce_verb_role(method, role) {
|
|
return make_error_response(
|
|
id,
|
|
RpcError {
|
|
code: 1004,
|
|
message: "purpose_mismatch",
|
|
},
|
|
);
|
|
}
|
|
|
|
// Mnemonic must be loaded
|
|
if !ctx.mnemonic.is_loaded() {
|
|
return make_error_response(id, RpcError::MNEMONIC_NOT_LOADED);
|
|
}
|
|
|
|
// Ensure key is derived. For variable-path roles, use the concrete
|
|
// path supplied by the client; for fixed-path roles, use the stored
|
|
// template.
|
|
if !role.derived {
|
|
let has_variable = role.has_variable_path();
|
|
let result = if has_variable && sel.has_role_path {
|
|
ctx.key_store
|
|
.derive_one_with_path(ctx.role_table, ctx.mnemonic, role_index, &sel.role_path)
|
|
} else {
|
|
ctx.key_store
|
|
.derive_one(ctx.role_table, ctx.mnemonic, role_index)
|
|
};
|
|
if let Err(_) = result {
|
|
return make_error_response(
|
|
id,
|
|
RpcError {
|
|
code: -32602,
|
|
message: "key_derivation_failed",
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
// Dispatch by verb
|
|
match method {
|
|
enforcement::VERB_NOSTR_GET_PUBLIC_KEY => {
|
|
let pub_hex = ctx.key_store.get_pubkey_hex(role_index).unwrap_or("");
|
|
// Check for structured format option
|
|
let want_structured = options
|
|
.and_then(|o| o.get("format"))
|
|
.and_then(|v| v.as_str())
|
|
== Some("structured");
|
|
|
|
if want_structured {
|
|
let key_id = if pub_hex.len() >= 16 {
|
|
&pub_hex[..16]
|
|
} else {
|
|
&pub_hex
|
|
};
|
|
let result = json!({
|
|
"algorithm": "secp256k1",
|
|
"public_key": pub_hex,
|
|
"key_id": key_id,
|
|
});
|
|
make_success_response(id, &result.to_string())
|
|
} else {
|
|
// Plain hex string
|
|
make_success_response(id, &format!("\"{}\"", pub_hex))
|
|
}
|
|
}
|
|
|
|
enforcement::VERB_NOSTR_SIGN_EVENT => {
|
|
let event_json = match params.first().and_then(|v| v.as_str()) {
|
|
Some(s) => s,
|
|
None => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
match ctx.key_store.sign_event(role_index, event_json) {
|
|
Ok(signed) => make_success_response(id, &format!("\"{}\"", signed)),
|
|
Err(_) => make_error_response(id, RpcError::INVALID_PARAMS),
|
|
}
|
|
}
|
|
|
|
enforcement::VERB_NOSTR_MINE_EVENT => {
|
|
// TODO: Phase 10 — NIP-13 mining
|
|
make_error_response(id, RpcError::NOT_YET_IMPLEMENTED)
|
|
}
|
|
|
|
enforcement::VERB_NOSTR_NIP44_ENCRYPT => {
|
|
let peer_hex = match params.first().and_then(|v| v.as_str()) {
|
|
Some(s) => s,
|
|
None => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
let plaintext = match params.get(1).and_then(|v| v.as_str()) {
|
|
Some(s) => s,
|
|
None => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
match ctx.key_store.nip44_encrypt(role_index, peer_hex, plaintext) {
|
|
Ok(ct) => {
|
|
let ct_b64 = base64::engine::general_purpose::STANDARD.encode(&ct);
|
|
make_success_response(id, &format!("\"{}\"", ct_b64))
|
|
}
|
|
Err(_) => make_error_response(id, RpcError::INVALID_PARAMS),
|
|
}
|
|
}
|
|
|
|
enforcement::VERB_NOSTR_NIP44_DECRYPT => {
|
|
let peer_hex = match params.first().and_then(|v| v.as_str()) {
|
|
Some(s) => s,
|
|
None => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
let ciphertext_b64 = match params.get(1).and_then(|v| v.as_str()) {
|
|
Some(s) => s,
|
|
None => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
let ct = match base64::engine::general_purpose::STANDARD.decode(ciphertext_b64) {
|
|
Ok(b) => b,
|
|
Err(_) => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
match ctx.key_store.nip44_decrypt(role_index, peer_hex, &ct) {
|
|
Ok(pt) => {
|
|
let pt_b64 = base64::engine::general_purpose::STANDARD.encode(&pt);
|
|
make_success_response(id, &format!("\"{}\"", pt_b64))
|
|
}
|
|
Err(_) => make_error_response(id, RpcError::INVALID_PARAMS),
|
|
}
|
|
}
|
|
|
|
enforcement::VERB_NOSTR_NIP04_ENCRYPT => {
|
|
let peer_hex = match params.first().and_then(|v| v.as_str()) {
|
|
Some(s) => s,
|
|
None => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
let plaintext = match params.get(1).and_then(|v| v.as_str()) {
|
|
Some(s) => s,
|
|
None => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
match ctx.key_store.nip04_encrypt(role_index, peer_hex, plaintext) {
|
|
Ok(ct) => make_success_response(id, &format!("\"{}\"", ct)),
|
|
Err(_) => make_error_response(id, RpcError::INVALID_PARAMS),
|
|
}
|
|
}
|
|
|
|
enforcement::VERB_NOSTR_NIP04_DECRYPT => {
|
|
let peer_hex = match params.first().and_then(|v| v.as_str()) {
|
|
Some(s) => s,
|
|
None => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
let ciphertext = match params.get(1).and_then(|v| v.as_str()) {
|
|
Some(s) => s,
|
|
None => return make_error_response(id, RpcError::INVALID_PARAMS),
|
|
};
|
|
match ctx.key_store.nip04_decrypt(role_index, peer_hex, ciphertext) {
|
|
Ok(pt) => make_success_response(id, &format!("\"{}\"", pt)),
|
|
Err(_) => make_error_response(id, RpcError::INVALID_PARAMS),
|
|
}
|
|
}
|
|
|
|
_ => make_error_response(id, RpcError::METHOD_NOT_FOUND),
|
|
}
|
|
}
|
|
|
|
// ── OTP Verb Handler ─────────────────────────────────────────────────────────
|
|
|
|
fn handle_otp_verb(id: &str, method: &str, _params: &[Value]) -> String {
|
|
// TODO: Phase 12 — OTP pad encrypt/decrypt
|
|
let _ = method;
|
|
make_error_response(
|
|
id,
|
|
RpcError {
|
|
code: -32601,
|
|
message: "otp_pad_not_bound",
|
|
},
|
|
)
|
|
}
|
|
|
|
// ── get_info ────────────────────────────────────────────────────────────────
|
|
|
|
fn handle_get_info(id: &str) -> String {
|
|
let info = json!({
|
|
"name": "n_signer",
|
|
"implementation": "host",
|
|
"version": crate::VERSION,
|
|
"api": "json-rpc-2.0",
|
|
"verbs": [
|
|
"get_info", "get_public_key", "sign", "verify",
|
|
"encapsulate", "decapsulate", "derive_shared_secret", "derive",
|
|
"encrypt", "decrypt",
|
|
"nostr_get_public_key", "nostr_sign_event", "nostr_mine_event",
|
|
"nostr_nip04_encrypt", "nostr_nip04_decrypt",
|
|
"nostr_nip44_encrypt", "nostr_nip44_decrypt",
|
|
],
|
|
"algorithms": [
|
|
"secp256k1", "ed25519", "x25519",
|
|
"ml-dsa-65", "slh-dsa-128s", "ml-kem-768", "otp",
|
|
],
|
|
});
|
|
make_success_response(id, &info.to_string())
|
|
}
|
|
|
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
fn is_nostr_verb(verb: &str) -> bool {
|
|
matches!(
|
|
verb,
|
|
enforcement::VERB_NOSTR_GET_PUBLIC_KEY
|
|
| enforcement::VERB_NOSTR_SIGN_EVENT
|
|
| enforcement::VERB_NOSTR_MINE_EVENT
|
|
| enforcement::VERB_NOSTR_NIP44_ENCRYPT
|
|
| enforcement::VERB_NOSTR_NIP44_DECRYPT
|
|
| enforcement::VERB_NOSTR_NIP04_ENCRYPT
|
|
| enforcement::VERB_NOSTR_NIP04_DECRYPT
|
|
)
|
|
}
|
|
|
|
fn sign_with_alg(alg: CryptoAlg, priv_key: &[u8; 32], msg: &[u8]) -> Result<Vec<u8>, SignerError> {
|
|
match alg {
|
|
CryptoAlg::Secp256k1 => {
|
|
// Check for scheme option (schnorr default, ecdsa alternative)
|
|
// For now, default to schnorr
|
|
let sk = nostr_core::types::SecretKey::from_bytes(*priv_key);
|
|
let digest = nostr_core::crypto::sha256::sha256(msg);
|
|
let sig = nostr_core::crypto::keys::schnorr_sign(&sk, &digest)?;
|
|
Ok(sig.as_bytes().to_vec())
|
|
}
|
|
CryptoAlg::Ed25519 => {
|
|
let sig = crate::pq_crypto::ed25519_sign(priv_key, msg);
|
|
Ok(sig.to_vec())
|
|
}
|
|
CryptoAlg::MlDsa65 => crate::pq_crypto::ml_dsa_65_sign(priv_key, msg),
|
|
CryptoAlg::SlhDsa128s => crate::pq_crypto::slh_dsa_128s_sign(priv_key, msg),
|
|
_ => Err(SignerError::CryptoFailed),
|
|
}
|
|
}
|
|
|
|
fn verify_with_alg(alg: CryptoAlg, pub_key: &[u8], msg: &[u8], sig: &[u8]) -> bool {
|
|
match alg {
|
|
CryptoAlg::Secp256k1 => {
|
|
if pub_key.len() != 32 || sig.len() != 64 {
|
|
return false;
|
|
}
|
|
// Use schnorr verify (default)
|
|
let pub_arr: [u8; 32] = pub_key[..32].try_into().unwrap();
|
|
let sig_arr: [u8; 64] = sig[..64].try_into().unwrap();
|
|
let digest = nostr_core::crypto::sha256::sha256(msg);
|
|
let pk = nostr_core::types::PublicKey::from_bytes(pub_arr);
|
|
let signature = nostr_core::types::Signature::from_bytes(sig_arr);
|
|
nostr_core::crypto::keys::schnorr_verify(&pk, &digest, &signature).unwrap_or(false)
|
|
}
|
|
CryptoAlg::Ed25519 => {
|
|
if pub_key.len() != 32 || sig.len() != 64 {
|
|
return false;
|
|
}
|
|
let pub_arr: [u8; 32] = pub_key[..32].try_into().unwrap();
|
|
let sig_arr: [u8; 64] = sig[..64].try_into().unwrap();
|
|
crate::pq_crypto::ed25519_verify(&pub_arr, msg, &sig_arr)
|
|
}
|
|
CryptoAlg::MlDsa65 => crate::pq_crypto::ml_dsa_65_verify(pub_key, msg, sig),
|
|
CryptoAlg::SlhDsa128s => crate::pq_crypto::slh_dsa_128s_verify(pub_key, msg, sig),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn make_error_response(id: &str, err: RpcError) -> String {
|
|
format!(
|
|
r#"{{"id":"{}","error":{}}}"#,
|
|
id,
|
|
err.to_json()
|
|
)
|
|
}
|
|
|
|
fn make_success_response(id: &str, result: &str) -> String {
|
|
// result is already a JSON value string — wrap it as a JSON string
|
|
format!(r#"{{"id":"{}","result":{}}}"#, id, result)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::alg_cache::AlgorithmKeyCache;
|
|
use crate::key_store::KeyStore;
|
|
use crate::mnemonic::MnemonicState;
|
|
use crate::role_table::*;
|
|
|
|
fn make_ctx<'a>(
|
|
role_table: &'a mut RoleTable,
|
|
mnemonic: &'a MnemonicState,
|
|
key_store: &'a mut KeyStore,
|
|
alg_cache: &'a mut AlgorithmKeyCache,
|
|
) -> DispatcherContext<'a> {
|
|
DispatcherContext {
|
|
role_table,
|
|
mnemonic,
|
|
key_store,
|
|
alg_key_cache: alg_cache,
|
|
}
|
|
}
|
|
|
|
fn setup() -> (
|
|
RoleTable,
|
|
MnemonicState,
|
|
KeyStore,
|
|
AlgorithmKeyCache,
|
|
) {
|
|
let mut mnemonic = MnemonicState::new();
|
|
mnemonic
|
|
.load("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about")
|
|
.unwrap();
|
|
|
|
let mut table = RoleTable::new();
|
|
table
|
|
.register_role_path(
|
|
"main",
|
|
"m/44'/1237'/0'/0/0",
|
|
RolePurpose::Nostr,
|
|
RoleCurve::Secp256k1,
|
|
-1, -1, -1, &[],
|
|
)
|
|
.unwrap();
|
|
|
|
let mut store = KeyStore::new();
|
|
store.derive_all(&mut table, &mnemonic).unwrap();
|
|
|
|
(table, mnemonic, store, AlgorithmKeyCache::new())
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_info() {
|
|
let (mut table, mnemonic, mut store, mut cache) = setup();
|
|
let mut ctx = make_ctx(&mut table, &mnemonic, &mut store, &mut cache);
|
|
|
|
let req = r#"{"id":"1","method":"get_info","params":[]}"#;
|
|
let resp = handle_request(&mut ctx, req);
|
|
assert!(resp.contains("\"result\""));
|
|
assert!(resp.contains("n_signer"));
|
|
assert!(resp.contains("json-rpc-2.0"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_nostr_get_public_key() {
|
|
let (mut table, mnemonic, mut store, mut cache) = setup();
|
|
let mut ctx = make_ctx(&mut table, &mnemonic, &mut store, &mut cache);
|
|
|
|
let req = r#"{"id":"2","method":"nostr_get_public_key","params":[],"params":[{},{"role":"main","role_path":"m/44'/1237'/0'/0/0"}]}"#;
|
|
// Use proper format
|
|
let req = r#"{"id":"2","method":"nostr_get_public_key","params":[{"role":"main","role_path":"m/44'/1237'/0'/0/0"}]}"#;
|
|
let resp = handle_request(&mut ctx, req);
|
|
assert!(resp.contains("\"result\""));
|
|
assert!(!resp.contains("\"error\""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_unknown_method() {
|
|
let (mut table, mnemonic, mut store, mut cache) = setup();
|
|
let mut ctx = make_ctx(&mut table, &mnemonic, &mut store, &mut cache);
|
|
|
|
let req = r#"{"id":"3","method":"nonexistent_method","params":[]}"#;
|
|
let resp = handle_request(&mut ctx, req);
|
|
assert!(resp.contains("\"error\""));
|
|
assert!(resp.contains("-32601"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_error() {
|
|
let (mut table, mnemonic, mut store, mut cache) = setup();
|
|
let mut ctx = make_ctx(&mut table, &mnemonic, &mut store, &mut cache);
|
|
|
|
let req = r#"not valid json"#;
|
|
let resp = handle_request(&mut ctx, req);
|
|
assert!(resp.contains("\"error\""));
|
|
assert!(resp.contains("-32700"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_ed25519_get_public_key() {
|
|
let (mut table, mnemonic, mut store, mut cache) = setup();
|
|
let mut ctx = make_ctx(&mut table, &mnemonic, &mut store, &mut cache);
|
|
|
|
let req = r#"{"id":"4","method":"get_public_key","params":[{"algorithm":"ed25519","index":0}]}"#;
|
|
let resp = handle_request(&mut ctx, req);
|
|
assert!(resp.contains("\"result\""));
|
|
assert!(resp.contains("ed25519"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_ed25519_sign_verify() {
|
|
let (mut table, mnemonic, mut store, mut cache) = setup();
|
|
let mut ctx = make_ctx(&mut table, &mnemonic, &mut store, &mut cache);
|
|
|
|
let msg_hex = hex::encode(b"hello world");
|
|
let sign_req = format!(
|
|
r#"{{"id":"5","method":"sign","params":["{}"],{{"algorithm":"ed25519","index":0}}}}"#,
|
|
msg_hex
|
|
);
|
|
// Fix JSON format
|
|
let sign_req = format!(
|
|
r#"{{"id":"5","method":"sign","params":["{}",{{"algorithm":"ed25519","index":0}}]}}"#,
|
|
msg_hex
|
|
);
|
|
let resp = handle_request(&mut ctx, &sign_req);
|
|
assert!(resp.contains("\"result\""), "sign response: {}", resp);
|
|
assert!(resp.contains("signature"));
|
|
|
|
// Extract signature from response
|
|
let resp_json: Value = serde_json::from_str(&resp).unwrap();
|
|
let sig_hex = resp_json["result"]["signature"].as_str().unwrap();
|
|
|
|
// Verify
|
|
let verify_req = format!(
|
|
r#"{{"id":"6","method":"verify","params":["{}","{}",{{"algorithm":"ed25519","index":0}}]}}"#,
|
|
msg_hex, sig_hex
|
|
);
|
|
let resp = handle_request(&mut ctx, &verify_req);
|
|
assert!(resp.contains("\"valid\":true"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_missing_algorithm() {
|
|
let (mut table, mnemonic, mut store, mut cache) = setup();
|
|
let mut ctx = make_ctx(&mut table, &mnemonic, &mut store, &mut cache);
|
|
|
|
let req = r#"{"id":"7","method":"get_public_key","params":[{}]}"#;
|
|
let resp = handle_request(&mut ctx, req);
|
|
assert!(resp.contains("\"error\""));
|
|
}
|
|
}
|