feat: fix NIP-07 signing to use node Nostr key, add test script

Added node.nostr-sign RPC that uses the node-level Nostr key (matching
getPublicKey), fixing pubkey mismatch where identity.nostr-sign used a
different key. Updated appLauncher to call node.nostr-sign. Added
nostr_sign_hash() to nostr_discovery.rs. Created test-nip07.sh with
11 automated checks (INSTALL-02).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-13 03:18:45 +00:00
co-authored by Claude Opus 4.6
parent d80cfb0d8d
commit 1ac6034457
7 changed files with 242 additions and 15 deletions
+59 -12
View File
@@ -317,26 +317,73 @@ impl RpcHandler {
}))
}
/// Sign a Nostr event hash with an identity's Nostr key.
/// Sign a Nostr event with an identity's Nostr key.
///
/// Accepts either:
/// - `event_hash` (hex) + `id` — sign a pre-computed hash
/// - `event` (full event object) — compute NIP-01 hash, fill pubkey, sign
/// If `id` is omitted, uses the default identity.
pub(super) async fn handle_identity_nostr_sign(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value> {
let params = params.unwrap_or_default();
let id = params
.get("id")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: id"))?;
let event_hash = params
.get("event_hash")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: event_hash"))?;
let manager = IdentityManager::new(&self.config.data_dir).await?;
let signature = manager.nostr_sign(id, event_hash).await?;
let (records, _) = manager.list().await?;
// Resolve identity: prefer explicit id, then default, then any with Nostr key
let id = if let Some(id) = params.get("id").and_then(|v| v.as_str()) {
id.to_string()
} else {
// Prefer an identity with a Nostr key
records.iter()
.find(|r| r.nostr_pubkey.is_some())
.map(|r| r.id.clone())
.ok_or_else(|| anyhow::anyhow!("No identity with Nostr key found"))?
};
let identity = records.iter().find(|r| r.id == id)
.ok_or_else(|| anyhow::anyhow!("Identity not found: {}", id))?;
let pubkey_hex = identity.nostr_pubkey.clone()
.ok_or_else(|| anyhow::anyhow!("Identity has no Nostr key"))?;
if let Some(event_hash) = params.get("event_hash").and_then(|v| v.as_str()) {
// Direct hash signing
let signature = manager.nostr_sign(&id, event_hash).await?;
return Ok(serde_json::json!({ "signature": signature }));
}
// Full event signing: compute NIP-01 event hash
let event = params.get("event")
.ok_or_else(|| anyhow::anyhow!("Missing 'event' or 'event_hash' parameter"))?;
let kind = event.get("kind").and_then(|v| v.as_u64()).unwrap_or(1);
let content = event.get("content").and_then(|v| v.as_str()).unwrap_or("");
let created_at = event.get("created_at").and_then(|v| v.as_u64())
.unwrap_or_else(|| std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs());
let tags = event.get("tags").cloned().unwrap_or_else(|| serde_json::json!([]));
// NIP-01 serialization: [0, pubkey, created_at, kind, tags, content]
let serialized = serde_json::json!([0, pubkey_hex, created_at, kind, tags, content]);
let serialized_str = serde_json::to_string(&serialized)?;
// SHA-256 hash
use sha2::{Sha256, Digest};
let hash = Sha256::digest(serialized_str.as_bytes());
let event_hash_hex = hex::encode(hash);
let signature = manager.nostr_sign(&id, &event_hash_hex).await?;
// Return the complete signed event
Ok(serde_json::json!({
"signature": signature,
"id": event_hash_hex,
"pubkey": pubkey_hex,
"created_at": created_at,
"kind": kind,
"tags": tags,
"content": content,
"sig": signature,
}))
}