Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 6 additions & 91 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ redis = { version = "1.0", features = ["tokio-comp", "connection-manager"
deadpool-redis = { version = "0.23", features = ["rt_tokio_1"] }

# Nostr
nostr = { version = "0.36" }
nostr = { version = "0.44", features = ["nip44", "nip98"] }

# Serialization
serde = { version = "1", features = ["derive"] }
Expand Down
6 changes: 4 additions & 2 deletions crates/git-credential-nostr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use std::io::{self, BufRead, Write};

use base64::Engine as _;
use nostr::nips::nip98::{HttpData, HttpMethod};
use nostr::{EventBuilder, Keys, UncheckedUrl};
use nostr::types::Url;
use nostr::{EventBuilder, Keys};
use zeroize::Zeroize;

// ── helpers ──────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -204,7 +205,8 @@ pub fn run() -> i32 {
};
raw_key.zeroize();

let http_data = HttpData::new(UncheckedUrl::from(url.as_str()), method);
let parsed_url = Url::parse(&url).unwrap_or_else(|e| panic!("invalid URL {url:?}: {e}"));
let http_data = HttpData::new(parsed_url, method);
let event = match EventBuilder::http_auth(http_data).sign_with_keys(&keys) {
Ok(e) => e,
Err(e) => {
Expand Down
40 changes: 26 additions & 14 deletions crates/git-sign-nostr/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ use std::time::{SystemTime, UNIX_EPOCH};

use base64::Engine as _;
use chrono::DateTime;
use nostr::bitcoin::hashes::sha256::Hash as Sha256Hash;
use nostr::bitcoin::hashes::{Hash, HashEngine};
use nostr::bitcoin::secp256k1::schnorr::Signature;
use nostr::bitcoin::secp256k1::{Keypair, Message, XOnlyPublicKey};
use nostr::hashes::sha256::Hash as Sha256Hash;
use nostr::hashes::{Hash, HashEngine};
use nostr::secp256k1::schnorr::Signature;
use nostr::secp256k1::{Keypair, Message};
use nostr::{FromBech32, PublicKey, SecretKey, SECP256K1};
use zeroize::Zeroize;

Expand Down Expand Up @@ -957,7 +957,7 @@ fn do_sign(key_id: &str, status: &mut StatusWriter) -> Result<(), Error> {

// Parse directly into SecretKey — avoids nostr::Keys which stores
// non-zeroizable copies of the secret material internally.
let mut secret_key = match SecretKey::parse(&*raw_key) {
let mut secret_key = match SecretKey::parse(&raw_key) {
Ok(k) => k,
Err(e) => {
raw_key.zeroize();
Expand All @@ -970,7 +970,7 @@ fn do_sign(key_id: &str, status: &mut StatusWriter) -> Result<(), Error> {
// Drop secret_key immediately after creating the keypair so it doesn't
// linger on the stack through the rest of the function.
// Wrapped in KeypairGuard so non_secure_erase() runs on ALL exit paths.
let keypair = KeypairGuard::new(Keypair::from_secret_key(&SECP256K1, &secret_key));
let keypair = KeypairGuard::new(Keypair::from_secret_key(SECP256K1, &secret_key));
// Explicitly zero the SecretKey stack slot before dropping. nostr::SecretKey's
// Drop calls inner.non_secure_erase(), but that operates on the moved value.
// This write_bytes targets our local copy to minimize residual secret material.
Expand Down Expand Up @@ -1236,8 +1236,14 @@ fn do_verify(sig_file: &str, status: &mut StatusWriter) -> Result<(), Error> {
})?;

// Verify BIP-340 signature
let xonly: &XOnlyPublicKey = &pk;
if SECP256K1.verify_schnorr(&sig, &message, xonly).is_err() {
let xonly = pk.xonly().map_err(|_| {
write_errsig(status, Some(&envelope.pk));
Error::VerifyFailed {
pk: Some(envelope.pk.clone()),
msg: "invalid public key xonly conversion".to_string(),
}
})?;
if SECP256K1.verify_schnorr(&sig, &message, &xonly).is_err() {
status.write_line("NEWSIG");
status.write_line(&format!("BADSIG {} {}", envelope.pk, envelope.pk));
return Err(Error::VerifyFailed {
Expand Down Expand Up @@ -1543,8 +1549,14 @@ fn verify_oa(agent_pk_hex: &str, oa: &(String, String, String)) -> bool {
}
};

let xonly: &XOnlyPublicKey = &owner_pk;
if SECP256K1.verify_schnorr(&sig, &message, xonly).is_err() {
let xonly = match owner_pk.xonly() {
Ok(x) => x,
Err(_) => {
eprintln!("warning: oa owner pubkey conversion to xonly failed");
return false;
}
};
if SECP256K1.verify_schnorr(&sig, &message, &xonly).is_err() {
eprintln!("warning: NIP-OA owner attestation signature verification failed");
return false;
}
Expand Down Expand Up @@ -2026,7 +2038,7 @@ Initial commit"

/// Helper: sign a payload and return the armored signature
fn sign_payload(secret_hex: &str, payload: &[u8], t: u64) -> String {
let keypair = Keypair::from_seckey_str(&SECP256K1, secret_hex).unwrap();
let keypair = Keypair::from_seckey_str(SECP256K1, secret_hex).unwrap();
let (xonly, _) = keypair.x_only_public_key();
let pk_hex = hex::encode(xonly.serialize());
let hash = compute_signing_hash(t, None, payload);
Expand Down Expand Up @@ -2059,9 +2071,9 @@ Initial commit"
let message = Message::from_digest(hash);
let sig_bytes = hex::decode(&envelope.sig).map_err(|_| "bad sig hex")?;
let sig = Signature::from_slice(&sig_bytes).map_err(|_| "bad sig")?;
let xonly: &XOnlyPublicKey = &pk;
let xonly = pk.xonly().map_err(|_| "xonly conversion failed")?;
SECP256K1
.verify_schnorr(&sig, &message, xonly)
.verify_schnorr(&sig, &message, &xonly)
.map_err(|_| "signature verification failed")?;
Ok(envelope)
}
Expand Down Expand Up @@ -2107,7 +2119,7 @@ Initial commit"
fn test_verify_rejects_non_canonical_json() {
// Build a valid signature but with extra whitespace in JSON
let secret = "0000000000000000000000000000000000000000000000000000000000000003";
let keypair = Keypair::from_seckey_str(&SECP256K1, secret).unwrap();
let keypair = Keypair::from_seckey_str(SECP256K1, secret).unwrap();
let (xonly, _) = keypair.x_only_public_key();
let pk_hex = hex::encode(xonly.serialize());
let payload = test_payload();
Expand Down
4 changes: 2 additions & 2 deletions crates/sprout-acp/src/engram_fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ async fn fetch_core_body(
let filter = nostr::Filter::new()
.kind(nostr::Kind::Custom(KIND_AGENT_ENGRAM as u16))
.author(agent_keys.public_key())
.custom_tag(nostr::SingleLetterTag::lowercase(nostr::Alphabet::D), [d])
.custom_tag(
.custom_tags(nostr::SingleLetterTag::lowercase(nostr::Alphabet::D), [d])
.custom_tags(
nostr::SingleLetterTag::lowercase(nostr::Alphabet::P),
[owner.to_hex()],
)
Expand Down
12 changes: 7 additions & 5 deletions crates/sprout-acp/src/filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ impl FilterContext {
author: event.pubkey.to_hex(),
kind: event.kind.as_u16() as u32,
channel_id: channel_id.to_string(),
timestamp: event.created_at.as_u64(),
timestamp: event.created_at.as_secs(),
}
}
}
Expand Down Expand Up @@ -485,16 +485,18 @@ mod tests {
/// Build a minimal test event with the given kind and content.
fn make_event(kind: u32, content: &str) -> nostr::Event {
let keys = Keys::generate();
EventBuilder::new(Kind::Custom(kind as u16), content, [])
EventBuilder::new(Kind::Custom(kind as u16), content)
.tags([])
.sign_with_keys(&keys)
.unwrap()
}

/// Build a test event with an explicit `p` tag.
fn make_event_with_p_tag(kind: u32, content: &str, p_hex: &str) -> nostr::Event {
let keys = Keys::generate();
let p_tag = Tag::parse(&["p", p_hex]).expect("tag parse");
EventBuilder::new(Kind::Custom(kind as u16), content, [p_tag])
let p_tag = Tag::parse(["p", p_hex]).expect("tag parse");
EventBuilder::new(Kind::Custom(kind as u16), content)
.tags([p_tag])
.sign_with_keys(&keys)
.unwrap()
}
Expand Down Expand Up @@ -535,7 +537,7 @@ mod tests {
assert_eq!(ctx.author, event.pubkey.to_hex());
assert_eq!(ctx.kind, 9);
assert_eq!(ctx.channel_id, channel_id.to_string());
assert_eq!(ctx.timestamp, event.created_at.as_u64());
assert_eq!(ctx.timestamp, event.created_at.as_secs());
}

// ── evaluate_filter ───────────────────────────────────────────────────────
Expand Down
7 changes: 4 additions & 3 deletions crates/sprout-acp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ async fn publish_presence(
use nostr::{EventBuilder, Kind};
use sprout_core::kind::KIND_PRESENCE_UPDATE;

let event = EventBuilder::new(Kind::Custom(KIND_PRESENCE_UPDATE as u16), status, [])
let event = EventBuilder::new(Kind::Custom(KIND_PRESENCE_UPDATE as u16), status)
.tags([])
.sign_with_keys(keys)
.map_err(|e| relay::RelayError::Http(format!("presence sign error: {e}")))?;
publisher.publish_event(event).await?;
Expand Down Expand Up @@ -506,7 +507,7 @@ fn handle_relay_observer_control_event(

// Freshness: reject stale/replayed frames outside ±5 minute window.
let now = chrono::Utc::now().timestamp();
let event_ts = event.created_at.as_u64() as i64;
let event_ts = event.created_at.as_secs() as i64;
if (event_ts - now).unsigned_abs() > OBSERVER_CONTROL_FRESHNESS_SECS as u64 {
tracing::warn!(
event_ts,
Expand Down Expand Up @@ -1351,7 +1352,7 @@ async fn tokio_main() -> Result<()> {
|| kind_u32 == KIND_MEMBER_REMOVED_NOTIFICATION
{
let ch = sprout_event.channel_id;
let ts = sprout_event.event.created_at.as_u64();
let ts = sprout_event.event.created_at.as_secs();
let eid = sprout_event.event.id.to_hex();

// Two-layer membership dedup:
Expand Down
Loading
Loading