hot fixes to utc-6

This commit is contained in:
Dorian
2026-03-12 12:56:59 +00:00
parent 6fee6befed
commit f05198ea09
26 changed files with 1123 additions and 76 deletions
+2 -2
View File
@@ -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"
+12 -1
View File
@@ -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,
})
+28 -5
View File
@@ -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,
}))
}
+22 -3
View File
@@ -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> {
+36
View File
@@ -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
+16 -1
View File
@@ -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,
})
}
+17 -1
View File
@@ -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(),
});
+1
View File
@@ -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;