1use rand::RngCore;
36use secp256k1::{Keypair, Secp256k1, SecretKey, XOnlyPublicKey, schnorr::Signature};
37use sha2::{Digest, Sha256};
38
39use crate::identity::{CertError, sign_did_cert, verify_payload_sig};
40use crate::signing::{b64decode, b64encode};
41
42pub const NOSTR_BINDING_DOMAIN: &str = "wire-nostr-binding-v1";
45
46#[derive(Debug, PartialEq, Eq)]
50pub enum NostrKeyError {
51 Secp,
53 BadEncoding,
55 IdentitySig(CertError),
57 PossessionSig,
59}
60
61impl std::fmt::Display for NostrKeyError {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 match self {
64 NostrKeyError::Secp => write!(f, "malformed secp256k1 key/signature"),
65 NostrKeyError::BadEncoding => write!(f, "malformed binding field encoding"),
66 NostrKeyError::IdentitySig(e) => write!(f, "identity cross-signature: {e}"),
67 NostrKeyError::PossessionSig => write!(f, "secp256k1 possession proof failed"),
68 }
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct NostrBinding {
75 pub pubkey: String,
77 pub ed_sig: String,
79 pub schnorr_sig: String,
82}
83
84pub fn generate_transport_key() -> ([u8; 32], [u8; 32]) {
87 let secp = Secp256k1::new();
88 let mut rng = rand::thread_rng();
92 loop {
93 let mut seed = [0u8; 32];
94 rng.fill_bytes(&mut seed);
95 if let Ok(sk) = SecretKey::from_byte_array(seed) {
96 let kp = Keypair::from_secret_key(&secp, &sk);
97 return (sk.secret_bytes(), kp.x_only_public_key().0.serialize());
98 }
99 }
100}
101
102pub fn xonly_from_secret(secret: &[u8; 32]) -> Result<[u8; 32], NostrKeyError> {
104 let secp = Secp256k1::new();
105 let kp = Keypair::from_seckey_byte_array(&secp, *secret).map_err(|_| NostrKeyError::Secp)?;
106 Ok(kp.x_only_public_key().0.serialize())
107}
108
109pub fn binding_payload(session_did: &str, nostr_xonly: &[u8; 32]) -> String {
113 format!(
114 "{NOSTR_BINDING_DOMAIN}|{session_did}|{}",
115 hex::encode(nostr_xonly)
116 )
117}
118
119fn binding_digest(session_did: &str, nostr_xonly: &[u8; 32]) -> [u8; 32] {
122 let mut h = Sha256::new();
123 h.update(binding_payload(session_did, nostr_xonly).as_bytes());
124 let d = h.finalize();
125 let mut out = [0u8; 32];
126 out.copy_from_slice(&d);
127 out
128}
129
130pub fn build_binding(
133 session_ed_sk: &[u8],
134 nostr_secp_sk: &[u8; 32],
135 session_did: &str,
136) -> Result<NostrBinding, NostrKeyError> {
137 let secp = Secp256k1::new();
138 let kp =
139 Keypair::from_seckey_byte_array(&secp, *nostr_secp_sk).map_err(|_| NostrKeyError::Secp)?;
140 let xonly = kp.x_only_public_key().0.serialize();
141
142 let payload = binding_payload(session_did, &xonly);
144 let ed_sig = sign_did_cert(session_ed_sk, &payload).map_err(NostrKeyError::IdentitySig)?;
145
146 let digest = binding_digest(session_did, &xonly);
148 let sig = schnorr_sign_digest(nostr_secp_sk, &digest)?;
149
150 Ok(NostrBinding {
151 pubkey: b64encode(&xonly),
152 ed_sig,
153 schnorr_sig: b64encode(&sig),
154 })
155}
156
157pub fn schnorr_sign_digest(
162 secp_sk: &[u8; 32],
163 digest: &[u8; 32],
164) -> Result<[u8; 64], NostrKeyError> {
165 let secp = Secp256k1::new();
166 let kp = Keypair::from_seckey_byte_array(&secp, *secp_sk).map_err(|_| NostrKeyError::Secp)?;
167 Ok(*secp.sign_schnorr_no_aux_rand(digest, &kp).as_ref())
168}
169
170pub fn schnorr_verify_digest(
174 xonly: &[u8; 32],
175 digest: &[u8; 32],
176 sig: &[u8; 64],
177) -> Result<(), NostrKeyError> {
178 let secp = Secp256k1::new();
179 let pk = XOnlyPublicKey::from_byte_array(*xonly).map_err(|_| NostrKeyError::Secp)?;
180 let sig = Signature::from_byte_array(*sig);
181 secp.verify_schnorr(&sig, digest, &pk)
182 .map_err(|_| NostrKeyError::PossessionSig)
183}
184
185pub fn verify_binding(
189 session_ed_pubkey: &[u8],
190 pubkey_b64: &str,
191 ed_sig_b64: &str,
192 schnorr_sig_b64: &str,
193 session_did: &str,
194) -> Result<[u8; 32], NostrKeyError> {
195 let xonly_bytes = b64decode(pubkey_b64).map_err(|_| NostrKeyError::BadEncoding)?;
196 if xonly_bytes.len() != 32 {
197 return Err(NostrKeyError::BadEncoding);
198 }
199 let mut xonly_arr = [0u8; 32];
200 xonly_arr.copy_from_slice(&xonly_bytes);
201
202 let payload = binding_payload(session_did, &xonly_arr);
204 verify_payload_sig(session_ed_pubkey, ed_sig_b64, &payload)
205 .map_err(NostrKeyError::IdentitySig)?;
206
207 let sig_bytes = b64decode(schnorr_sig_b64).map_err(|_| NostrKeyError::BadEncoding)?;
209 let sig_arr: [u8; 64] = sig_bytes
210 .as_slice()
211 .try_into()
212 .map_err(|_| NostrKeyError::BadEncoding)?;
213 let digest = binding_digest(session_did, &xonly_arr);
214 schnorr_verify_digest(&xonly_arr, &digest, &sig_arr)?;
215
216 Ok(xonly_arr)
217}
218
219pub fn card_nostr_binding(
224 card: &serde_json::Value,
225 session_ed_pubkey: &[u8],
226) -> Result<Option<[u8; 32]>, NostrKeyError> {
227 let Some(b) = card.get("nostr_pubkey") else {
228 return Ok(None);
229 };
230 let session_did = card.get("did").and_then(|v| v.as_str()).unwrap_or_default();
231 let pubkey = b.get("pubkey").and_then(|v| v.as_str());
232 let ed_sig = b.get("ed_sig").and_then(|v| v.as_str());
233 let schnorr_sig = b.get("schnorr_sig").and_then(|v| v.as_str());
234 let (Some(pubkey), Some(ed_sig), Some(schnorr_sig)) = (pubkey, ed_sig, schnorr_sig) else {
235 return Err(NostrKeyError::BadEncoding);
236 };
237 verify_binding(session_ed_pubkey, pubkey, ed_sig, schnorr_sig, session_did).map(Some)
238}
239
240pub fn with_nostr_binding_if_keyed(
247 mut card: crate::agent_card::AgentCard,
248) -> anyhow::Result<crate::agent_card::AgentCard> {
249 let Ok(nostr_sk) = crate::config::read_nostr_key() else {
250 return Ok(card); };
252 let Ok(session_sk) = crate::config::read_private_key() else {
253 return Ok(card);
254 };
255 let session_did = card
256 .get("did")
257 .and_then(|v| v.as_str())
258 .unwrap_or_default()
259 .to_string();
260 if session_did.is_empty() {
261 return Ok(card);
262 }
263 match build_binding(&session_sk, &nostr_sk, &session_did) {
264 Ok(b) => {
265 if let Some(obj) = card.as_object_mut() {
266 obj.insert(
267 "nostr_pubkey".into(),
268 serde_json::json!({
269 "pubkey": b.pubkey,
270 "ed_sig": b.ed_sig,
271 "schnorr_sig": b.schnorr_sig,
272 }),
273 );
274 }
275 Ok(card)
276 }
277 Err(e) => {
278 eprintln!("wire: nostr binding skipped (build failed: {e})");
279 Ok(card)
280 }
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use crate::signing::generate_keypair;
288
289 #[test]
290 fn roundtrip_binding_verifies() {
291 let (ed_sk, ed_pk) = generate_keypair();
292 let (nostr_sk, nostr_xonly) = generate_transport_key();
293 let did = "did:wire:slate-lotus-88232017";
294 let b = build_binding(&ed_sk, &nostr_sk, did).unwrap();
295 assert_eq!(b64decode(&b.pubkey).unwrap(), nostr_xonly.to_vec());
297 assert_eq!(
299 verify_binding(&ed_pk, &b.pubkey, &b.ed_sig, &b.schnorr_sig, did),
300 Ok(nostr_xonly)
301 );
302 }
303
304 #[test]
305 fn wrong_identity_key_rejected() {
306 let (ed_sk, _ed_pk) = generate_keypair();
307 let (_other_sk, other_pk) = generate_keypair();
308 let (nostr_sk, _x) = generate_transport_key();
309 let did = "did:wire:x-1";
310 let b = build_binding(&ed_sk, &nostr_sk, did).unwrap();
311 assert!(matches!(
313 verify_binding(&other_pk, &b.pubkey, &b.ed_sig, &b.schnorr_sig, did),
314 Err(NostrKeyError::IdentitySig(_))
315 ));
316 }
317
318 #[test]
322 fn npub_squat_without_possession_rejected() {
323 let (ed_sk, ed_pk) = generate_keypair();
324 let (_victim_sk, victim_xonly) = generate_transport_key();
325 let did = "did:wire:squatter-9";
326 let payload = binding_payload(did, &victim_xonly);
329 let ed_sig = sign_did_cert(&ed_sk, &payload).unwrap();
330 let (attacker_sk, _ax) = generate_transport_key();
331 let secp = Secp256k1::new();
332 let kp = Keypair::from_seckey_byte_array(&secp, attacker_sk).unwrap();
333 let digest = binding_digest(did, &victim_xonly);
334 let bad_sig = secp.sign_schnorr_no_aux_rand(&digest, &kp);
335 assert_eq!(
336 verify_binding(
337 &ed_pk,
338 &b64encode(&victim_xonly),
339 &ed_sig,
340 &b64encode(bad_sig.as_ref()),
341 did
342 ),
343 Err(NostrKeyError::PossessionSig)
344 );
345 }
346
347 #[test]
348 fn tampered_did_rejected() {
349 let (ed_sk, ed_pk) = generate_keypair();
350 let (nostr_sk, _x) = generate_transport_key();
351 let b = build_binding(&ed_sk, &nostr_sk, "did:wire:real-1").unwrap();
352 assert!(
355 verify_binding(
356 &ed_pk,
357 &b.pubkey,
358 &b.ed_sig,
359 &b.schnorr_sig,
360 "did:wire:other-2"
361 )
362 .is_err()
363 );
364 }
365
366 #[test]
367 fn malformed_fields_rejected() {
368 let (_sk, pk) = generate_keypair();
369 assert_eq!(
370 verify_binding(&pk, "!!notb64", "x", "y", "did:wire:z"),
371 Err(NostrKeyError::BadEncoding)
372 );
373 assert_eq!(
374 verify_binding(&pk, &b64encode(b"short"), "x", "y", "did:wire:z"),
375 Err(NostrKeyError::BadEncoding)
376 );
377 }
378
379 #[test]
380 fn xonly_from_secret_matches_generation() {
381 let (sk, xonly) = generate_transport_key();
382 assert_eq!(xonly_from_secret(&sk).unwrap(), xonly);
383 }
384
385 #[test]
386 fn card_nostr_binding_reads_and_verifies() {
387 let (ed_sk, ed_pk) = generate_keypair();
388 let (nostr_sk, nostr_xonly) = generate_transport_key();
389 let did = "did:wire:reader-1";
390 let b = build_binding(&ed_sk, &nostr_sk, did).unwrap();
391 let card = serde_json::json!({
392 "did": did,
393 "nostr_pubkey": {
394 "pubkey": b.pubkey,
395 "ed_sig": b.ed_sig,
396 "schnorr_sig": b.schnorr_sig,
397 }
398 });
399 assert_eq!(card_nostr_binding(&card, &ed_pk), Ok(Some(nostr_xonly)));
400 let plain = serde_json::json!({"did": did});
402 assert_eq!(card_nostr_binding(&plain, &ed_pk), Ok(None));
403 }
404}