Files
signer/src/dispatcher.rs
T

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\""));
}
}