hot fixes to utc-6
This commit is contained in:
@@ -62,8 +62,8 @@ serde_yaml = "0.9"
|
||||
# Uses rustls-tls for cross-compilation (no OpenSSL dependency)
|
||||
reqwest = { version = "0.11", default-features = false, features = ["json", "socks", "rustls-tls"] }
|
||||
|
||||
# Nostr (node discovery)
|
||||
nostr-sdk = "0.44"
|
||||
# Nostr (node discovery + NIP-44 encrypted peer handshake)
|
||||
nostr-sdk = { version = "0.44", features = ["nip44"] }
|
||||
|
||||
# Backup encryption (DID identity export) + TOTP 2FA encryption
|
||||
argon2 = "0.5"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::RpcHandler;
|
||||
use crate::{nostr_handshake, peers};
|
||||
use anyhow::Result;
|
||||
use nostr_sdk::FromBech32;
|
||||
|
||||
impl RpcHandler {
|
||||
/// Discover nodes (presence-only — returns Nostr pubkeys + DIDs, no onion addresses).
|
||||
@@ -22,10 +23,19 @@ impl RpcHandler {
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let recipient = params
|
||||
// Accept either hex pubkey or npub1... bech32 format
|
||||
let recipient_raw = params
|
||||
.get("recipient_nostr_pubkey")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing recipient_nostr_pubkey"))?;
|
||||
let recipient = if recipient_raw.starts_with("npub1") {
|
||||
nostr_sdk::PublicKey::from_bech32(recipient_raw)
|
||||
.map_err(|e| anyhow::anyhow!("Invalid npub: {}", e))?
|
||||
.to_hex()
|
||||
} else {
|
||||
recipient_raw.to_string()
|
||||
};
|
||||
let recipient = recipient.as_str();
|
||||
|
||||
let (data, _) = self.state_manager.get_snapshot().await;
|
||||
let our_onion = data
|
||||
@@ -124,6 +134,7 @@ impl RpcHandler {
|
||||
.map(|hs| {
|
||||
serde_json::json!({
|
||||
"from_nostr_pubkey": hs.from_nostr_pubkey,
|
||||
"from_nostr_npub": hs.from_nostr_npub,
|
||||
"message": hs.message,
|
||||
"timestamp": hs.timestamp,
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use super::RpcHandler;
|
||||
use crate::identity_manager::{IdentityManager, IdentityPurpose};
|
||||
use anyhow::{Context, Result};
|
||||
use nostr_sdk::ToBech32;
|
||||
|
||||
impl RpcHandler {
|
||||
/// List all identities with their default status.
|
||||
@@ -25,6 +26,8 @@ impl RpcHandler {
|
||||
"did": id.did,
|
||||
"created_at": id.created_at,
|
||||
"is_default": is_default,
|
||||
"nostr_pubkey": id.nostr_pubkey,
|
||||
"nostr_npub": id.nostr_npub,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -65,6 +68,8 @@ impl RpcHandler {
|
||||
"pubkey": record.pubkey_hex,
|
||||
"did": record.did,
|
||||
"created_at": record.created_at,
|
||||
"nostr_pubkey": record.nostr_pubkey,
|
||||
"nostr_npub": record.nostr_npub,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -92,6 +97,8 @@ impl RpcHandler {
|
||||
"did": record.did,
|
||||
"created_at": record.created_at,
|
||||
"is_default": is_default,
|
||||
"nostr_pubkey": record.nostr_pubkey,
|
||||
"nostr_npub": record.nostr_npub,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -189,17 +196,27 @@ impl RpcHandler {
|
||||
let params = params.unwrap_or_default();
|
||||
|
||||
// If a DID is provided, resolve it; otherwise use the node's DID
|
||||
let is_local = params.get("did").and_then(|v| v.as_str()).is_none();
|
||||
let pubkey_hex = if let Some(did) = params.get("did").and_then(|v| v.as_str()) {
|
||||
// Extract pubkey from did:key format
|
||||
let pubkey_bytes = crate::identity::pubkey_bytes_from_did_key(did)?;
|
||||
hex::encode(pubkey_bytes)
|
||||
} else {
|
||||
// Use node's own pubkey
|
||||
let (data, _) = self.state_manager.get_snapshot().await;
|
||||
data.server_info.pubkey.clone()
|
||||
};
|
||||
|
||||
let document = crate::identity::did_document_from_pubkey_hex(&pubkey_hex)?;
|
||||
// For local node, include Nostr secp256k1 key in DID Document (paired identity)
|
||||
let document = if is_local {
|
||||
let identity_dir = self.config.data_dir.join("identity");
|
||||
match crate::nostr_discovery::get_nostr_pubkey(&identity_dir).await {
|
||||
Ok(nostr_pubkey) => {
|
||||
crate::identity::did_document_with_nostr(&pubkey_hex, &nostr_pubkey)?
|
||||
}
|
||||
Err(_) => crate::identity::did_document_from_pubkey_hex(&pubkey_hex)?,
|
||||
}
|
||||
} else {
|
||||
crate::identity::did_document_from_pubkey_hex(&pubkey_hex)?
|
||||
};
|
||||
Ok(document)
|
||||
}
|
||||
|
||||
@@ -287,10 +304,16 @@ impl RpcHandler {
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: id"))?;
|
||||
|
||||
let manager = IdentityManager::new(&self.config.data_dir).await?;
|
||||
let pubkey = manager.create_nostr_key(id).await?;
|
||||
let pubkey_hex = manager.create_nostr_key(id).await?;
|
||||
|
||||
// Derive npub (bech32 NIP-19) from hex
|
||||
let npub = nostr_sdk::PublicKey::from_hex(&pubkey_hex)
|
||||
.ok()
|
||||
.and_then(|pk| pk.to_bech32().ok());
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"nostr_pubkey": pubkey,
|
||||
"nostr_pubkey": pubkey_hex,
|
||||
"nostr_npub": npub,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,25 @@ use super::RpcHandler;
|
||||
use crate::{backup, identity, nostr_discovery};
|
||||
use crate::container::docker_packages;
|
||||
use anyhow::Result;
|
||||
use nostr_sdk::ToBech32;
|
||||
|
||||
impl RpcHandler {
|
||||
pub(super) async fn handle_node_did(&self) -> Result<serde_json::Value> {
|
||||
let (data, _) = self.state_manager.get_snapshot().await;
|
||||
let did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
|
||||
Ok(serde_json::json!({ "did": did, "pubkey": data.server_info.pubkey }))
|
||||
let identity_dir = self.config.data_dir.join("identity");
|
||||
let nostr_pubkey = nostr_discovery::get_nostr_pubkey(&identity_dir).await.ok();
|
||||
let nostr_npub = nostr_pubkey.as_ref().and_then(|hex| {
|
||||
nostr_sdk::PublicKey::from_hex(hex)
|
||||
.ok()
|
||||
.and_then(|pk| pk.to_bech32().ok())
|
||||
});
|
||||
Ok(serde_json::json!({
|
||||
"did": did,
|
||||
"pubkey": data.server_info.pubkey,
|
||||
"nostr_pubkey": nostr_pubkey,
|
||||
"nostr_npub": nostr_npub,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Sign a challenge to prove control of the node DID (proof-of-control for onboarding).
|
||||
@@ -91,8 +104,14 @@ impl RpcHandler {
|
||||
|
||||
pub(super) async fn handle_node_nostr_pubkey(&self) -> Result<serde_json::Value> {
|
||||
let identity_dir = self.config.data_dir.join("identity");
|
||||
let pubkey = nostr_discovery::get_nostr_pubkey(&identity_dir).await?;
|
||||
Ok(serde_json::json!({ "nostr_pubkey": pubkey }))
|
||||
let pubkey_hex = nostr_discovery::get_nostr_pubkey(&identity_dir).await?;
|
||||
let npub = nostr_sdk::PublicKey::from_hex(&pubkey_hex)
|
||||
.ok()
|
||||
.and_then(|pk| pk.to_bech32().ok());
|
||||
Ok(serde_json::json!({
|
||||
"nostr_pubkey": pubkey_hex,
|
||||
"nostr_npub": npub,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) async fn handle_node_nostr_verify_revoked(&self) -> Result<serde_json::Value> {
|
||||
|
||||
@@ -181,6 +181,42 @@ pub fn did_document_from_pubkey_hex(pubkey_hex: &str) -> Result<serde_json::Valu
|
||||
}))
|
||||
}
|
||||
|
||||
/// Generate a DID Document that includes both the Ed25519 key and a Nostr secp256k1 key.
|
||||
/// The Nostr key is added as an additional verification method, formally pairing
|
||||
/// the two identities so a user can use either protocol.
|
||||
pub fn did_document_with_nostr(
|
||||
pubkey_hex: &str,
|
||||
nostr_pubkey_hex: &str,
|
||||
) -> Result<serde_json::Value> {
|
||||
let mut doc = did_document_from_pubkey_hex(pubkey_hex)?;
|
||||
let did = did_key_from_pubkey_hex(pubkey_hex)?;
|
||||
let nostr_key_id = format!("{}#key-nostr-1", did);
|
||||
|
||||
// Add EcdsaSecp256k1VerificationKey2019 context
|
||||
if let Some(contexts) = doc["@context"].as_array_mut() {
|
||||
contexts.push(serde_json::json!(
|
||||
"https://w3id.org/security/suites/secp256k1-2019/v1"
|
||||
));
|
||||
}
|
||||
|
||||
// Add Nostr secp256k1 key to verificationMethod array
|
||||
if let Some(vms) = doc["verificationMethod"].as_array_mut() {
|
||||
vms.push(serde_json::json!({
|
||||
"id": nostr_key_id,
|
||||
"type": "EcdsaSecp256k1VerificationKey2019",
|
||||
"controller": did,
|
||||
"publicKeyHex": nostr_pubkey_hex
|
||||
}));
|
||||
}
|
||||
|
||||
// Add to authentication (Nostr key can also authenticate)
|
||||
if let Some(auth) = doc["authentication"].as_array_mut() {
|
||||
auth.push(serde_json::json!(nostr_key_id));
|
||||
}
|
||||
|
||||
Ok(doc)
|
||||
}
|
||||
|
||||
/// Extract the raw 32-byte Ed25519 public key from a did:key string.
|
||||
pub fn pubkey_bytes_from_did_key(did: &str) -> Result<[u8; 32]> {
|
||||
let multibase_str = did
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::path::{Path, PathBuf};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::identity::did_key_from_pubkey_hex;
|
||||
use nostr_sdk::ToBech32;
|
||||
|
||||
const IDENTITIES_DIR: &str = "identities";
|
||||
const DEFAULT_MARKER: &str = ".default";
|
||||
@@ -40,7 +41,10 @@ pub struct IdentityRecord {
|
||||
pub pubkey_hex: String,
|
||||
pub did: String,
|
||||
pub created_at: String,
|
||||
/// Nostr secp256k1 public key in hex format
|
||||
pub nostr_pubkey: Option<String>,
|
||||
/// Nostr public key in bech32 npub format (NIP-19)
|
||||
pub nostr_npub: Option<String>,
|
||||
}
|
||||
|
||||
/// On-disk format for identity storage (includes secret key bytes).
|
||||
@@ -149,6 +153,7 @@ impl IdentityManager {
|
||||
did,
|
||||
created_at,
|
||||
nostr_pubkey: None,
|
||||
nostr_npub: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -248,6 +253,7 @@ impl IdentityManager {
|
||||
let keys = nostr_sdk::Keys::generate();
|
||||
let secret_hex = keys.secret_key().display_secret().to_string();
|
||||
let pubkey_hex = keys.public_key().to_hex();
|
||||
let npub = keys.public_key().to_bech32().unwrap_or_default();
|
||||
|
||||
file.nostr_secret_hex = Some(secret_hex);
|
||||
file.nostr_pubkey_hex = Some(pubkey_hex.clone());
|
||||
@@ -255,7 +261,7 @@ impl IdentityManager {
|
||||
let json = serde_json::to_string_pretty(&file).context("Failed to serialize identity")?;
|
||||
fs::write(&file_path, json.as_bytes()).await.context("Failed to write identity file")?;
|
||||
|
||||
tracing::info!("Created Nostr key for identity {}", id);
|
||||
tracing::info!("Created Nostr key for identity {} (npub: {})", id, &npub[..20.min(npub.len())]);
|
||||
Ok(pubkey_hex)
|
||||
}
|
||||
|
||||
@@ -317,6 +323,14 @@ impl IdentityManager {
|
||||
.context("Failed to read identity file")?;
|
||||
let file: IdentityFile = serde_json::from_slice(&data)
|
||||
.context("Failed to parse identity file")?;
|
||||
|
||||
// Derive npub (bech32) from hex pubkey if available
|
||||
let nostr_npub = file.nostr_pubkey_hex.as_ref().and_then(|hex| {
|
||||
nostr_sdk::PublicKey::from_hex(hex)
|
||||
.ok()
|
||||
.and_then(|pk| pk.to_bech32().ok())
|
||||
});
|
||||
|
||||
Ok(IdentityRecord {
|
||||
id: file.id,
|
||||
name: file.name,
|
||||
@@ -325,6 +339,7 @@ impl IdentityManager {
|
||||
did: file.did,
|
||||
created_at: file.created_at,
|
||||
nostr_pubkey: file.nostr_pubkey_hex,
|
||||
nostr_npub,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,10 @@ pub enum HandshakeMessage {
|
||||
/// Result of polling for incoming handshake messages
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct IncomingHandshake {
|
||||
/// Sender's Nostr public key in hex
|
||||
pub from_nostr_pubkey: String,
|
||||
/// Sender's Nostr public key in bech32 npub format (NIP-19)
|
||||
pub from_nostr_npub: String,
|
||||
pub message: HandshakeMessage,
|
||||
pub timestamp: String,
|
||||
}
|
||||
@@ -103,11 +106,13 @@ pub async fn publish_presence(
|
||||
.ok_or_else(|| anyhow::anyhow!("No Nostr keys — generate them first"))?;
|
||||
|
||||
let nostr_pubkey = keys.public_key().to_hex();
|
||||
let nostr_npub = keys.public_key().to_bech32().unwrap_or_default();
|
||||
let client = build_client(keys, tor_proxy)?;
|
||||
|
||||
let content = serde_json::json!({
|
||||
"did": did,
|
||||
"nostr_pubkey": nostr_pubkey,
|
||||
"nostr_npub": nostr_npub,
|
||||
"version": version,
|
||||
// No onion address — exchanged only via encrypted DM
|
||||
})
|
||||
@@ -131,13 +136,16 @@ pub async fn publish_presence(
|
||||
/// Returns Nostr pubkeys and DIDs of discoverable nodes.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DiscoverableNode {
|
||||
/// Nostr secp256k1 public key in hex
|
||||
pub nostr_pubkey: String,
|
||||
/// Nostr public key in bech32 npub format (NIP-19)
|
||||
pub nostr_npub: String,
|
||||
pub did: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
pub async fn discover_nodes(
|
||||
identity_dir: &Path,
|
||||
_identity_dir: &Path,
|
||||
relays: &[String],
|
||||
tor_proxy: Option<&str>,
|
||||
) -> Result<Vec<DiscoverableNode>> {
|
||||
@@ -188,8 +196,14 @@ pub async fn discover_nodes(
|
||||
}
|
||||
|
||||
if !nostr_pubkey.is_empty() {
|
||||
// Derive npub (bech32 NIP-19) from hex
|
||||
let nostr_npub = nostr_sdk::PublicKey::from_hex(&nostr_pubkey)
|
||||
.ok()
|
||||
.and_then(|pk| pk.to_bech32().ok())
|
||||
.unwrap_or_default();
|
||||
nodes.push(DiscoverableNode {
|
||||
nostr_pubkey,
|
||||
nostr_npub,
|
||||
did,
|
||||
version,
|
||||
});
|
||||
@@ -374,8 +388,10 @@ pub async fn poll_handshakes(
|
||||
|
||||
// Try to parse as HandshakeMessage
|
||||
if let Ok(msg) = serde_json::from_str::<HandshakeMessage>(&plaintext) {
|
||||
let from_npub = event.pubkey.to_bech32().unwrap_or_default();
|
||||
handshakes.push(IncomingHandshake {
|
||||
from_nostr_pubkey: event.pubkey.to_hex(),
|
||||
from_nostr_npub: from_npub,
|
||||
message: msg,
|
||||
timestamp: event.created_at.to_human_datetime(),
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::identity::{self, NodeIdentity};
|
||||
use crate::monitoring::MetricsStore;
|
||||
use crate::node_message;
|
||||
use crate::nostr_discovery;
|
||||
use crate::nostr_handshake;
|
||||
use crate::peers;
|
||||
use crate::state::StateManager;
|
||||
use anyhow::Result;
|
||||
|
||||
Reference in New Issue
Block a user