use rand::RngCore;
use secp256k1::{Keypair, Secp256k1, SecretKey, XOnlyPublicKey, schnorr::Signature};
use sha2::{Digest, Sha256};
use crate::identity::{CertError, sign_did_cert, verify_payload_sig};
use crate::signing::{b64decode, b64encode};
pub const NOSTR_BINDING_DOMAIN: &str = "wire-nostr-binding-v1";
#[derive(Debug, PartialEq, Eq)]
pub enum NostrKeyError {
Secp,
BadEncoding,
IdentitySig(CertError),
PossessionSig,
}
impl std::fmt::Display for NostrKeyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NostrKeyError::Secp => write!(f, "malformed secp256k1 key/signature"),
NostrKeyError::BadEncoding => write!(f, "malformed binding field encoding"),
NostrKeyError::IdentitySig(e) => write!(f, "identity cross-signature: {e}"),
NostrKeyError::PossessionSig => write!(f, "secp256k1 possession proof failed"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NostrBinding {
pub pubkey: String,
pub ed_sig: String,
pub schnorr_sig: String,
}
pub fn generate_transport_key() -> ([u8; 32], [u8; 32]) {
let secp = Secp256k1::new();
let mut rng = rand::thread_rng();
loop {
let mut seed = [0u8; 32];
rng.fill_bytes(&mut seed);
if let Ok(sk) = SecretKey::from_byte_array(seed) {
let kp = Keypair::from_secret_key(&secp, &sk);
return (sk.secret_bytes(), kp.x_only_public_key().0.serialize());
}
}
}
pub fn xonly_from_secret(secret: &[u8; 32]) -> Result<[u8; 32], NostrKeyError> {
let secp = Secp256k1::new();
let kp = Keypair::from_seckey_byte_array(&secp, *secret).map_err(|_| NostrKeyError::Secp)?;
Ok(kp.x_only_public_key().0.serialize())
}
pub fn binding_payload(session_did: &str, nostr_xonly: &[u8; 32]) -> String {
format!(
"{NOSTR_BINDING_DOMAIN}|{session_did}|{}",
hex::encode(nostr_xonly)
)
}
fn binding_digest(session_did: &str, nostr_xonly: &[u8; 32]) -> [u8; 32] {
let mut h = Sha256::new();
h.update(binding_payload(session_did, nostr_xonly).as_bytes());
let d = h.finalize();
let mut out = [0u8; 32];
out.copy_from_slice(&d);
out
}
pub fn build_binding(
session_ed_sk: &[u8],
nostr_secp_sk: &[u8; 32],
session_did: &str,
) -> Result<NostrBinding, NostrKeyError> {
let secp = Secp256k1::new();
let kp =
Keypair::from_seckey_byte_array(&secp, *nostr_secp_sk).map_err(|_| NostrKeyError::Secp)?;
let xonly = kp.x_only_public_key().0.serialize();
let payload = binding_payload(session_did, &xonly);
let ed_sig = sign_did_cert(session_ed_sk, &payload).map_err(NostrKeyError::IdentitySig)?;
let digest = binding_digest(session_did, &xonly);
let sig = schnorr_sign_digest(nostr_secp_sk, &digest)?;
Ok(NostrBinding {
pubkey: b64encode(&xonly),
ed_sig,
schnorr_sig: b64encode(&sig),
})
}
pub fn schnorr_sign_digest(
secp_sk: &[u8; 32],
digest: &[u8; 32],
) -> Result<[u8; 64], NostrKeyError> {
let secp = Secp256k1::new();
let kp = Keypair::from_seckey_byte_array(&secp, *secp_sk).map_err(|_| NostrKeyError::Secp)?;
Ok(*secp.sign_schnorr_no_aux_rand(digest, &kp).as_ref())
}
pub fn schnorr_verify_digest(
xonly: &[u8; 32],
digest: &[u8; 32],
sig: &[u8; 64],
) -> Result<(), NostrKeyError> {
let secp = Secp256k1::new();
let pk = XOnlyPublicKey::from_byte_array(*xonly).map_err(|_| NostrKeyError::Secp)?;
let sig = Signature::from_byte_array(*sig);
secp.verify_schnorr(&sig, digest, &pk)
.map_err(|_| NostrKeyError::PossessionSig)
}
pub fn verify_binding(
session_ed_pubkey: &[u8],
pubkey_b64: &str,
ed_sig_b64: &str,
schnorr_sig_b64: &str,
session_did: &str,
) -> Result<[u8; 32], NostrKeyError> {
let xonly_bytes = b64decode(pubkey_b64).map_err(|_| NostrKeyError::BadEncoding)?;
if xonly_bytes.len() != 32 {
return Err(NostrKeyError::BadEncoding);
}
let mut xonly_arr = [0u8; 32];
xonly_arr.copy_from_slice(&xonly_bytes);
let payload = binding_payload(session_did, &xonly_arr);
verify_payload_sig(session_ed_pubkey, ed_sig_b64, &payload)
.map_err(NostrKeyError::IdentitySig)?;
let sig_bytes = b64decode(schnorr_sig_b64).map_err(|_| NostrKeyError::BadEncoding)?;
let sig_arr: [u8; 64] = sig_bytes
.as_slice()
.try_into()
.map_err(|_| NostrKeyError::BadEncoding)?;
let digest = binding_digest(session_did, &xonly_arr);
schnorr_verify_digest(&xonly_arr, &digest, &sig_arr)?;
Ok(xonly_arr)
}
pub fn card_nostr_binding(
card: &serde_json::Value,
session_ed_pubkey: &[u8],
) -> Result<Option<[u8; 32]>, NostrKeyError> {
let Some(b) = card.get("nostr_pubkey") else {
return Ok(None);
};
let session_did = card.get("did").and_then(|v| v.as_str()).unwrap_or_default();
let pubkey = b.get("pubkey").and_then(|v| v.as_str());
let ed_sig = b.get("ed_sig").and_then(|v| v.as_str());
let schnorr_sig = b.get("schnorr_sig").and_then(|v| v.as_str());
let (Some(pubkey), Some(ed_sig), Some(schnorr_sig)) = (pubkey, ed_sig, schnorr_sig) else {
return Err(NostrKeyError::BadEncoding);
};
verify_binding(session_ed_pubkey, pubkey, ed_sig, schnorr_sig, session_did).map(Some)
}
pub fn with_nostr_binding_if_keyed(
mut card: crate::agent_card::AgentCard,
) -> anyhow::Result<crate::agent_card::AgentCard> {
let Ok(nostr_sk) = crate::config::read_nostr_key() else {
return Ok(card); };
let Ok(session_sk) = crate::config::read_private_key() else {
return Ok(card);
};
let session_did = card
.get("did")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
if session_did.is_empty() {
return Ok(card);
}
match build_binding(&session_sk, &nostr_sk, &session_did) {
Ok(b) => {
if let Some(obj) = card.as_object_mut() {
obj.insert(
"nostr_pubkey".into(),
serde_json::json!({
"pubkey": b.pubkey,
"ed_sig": b.ed_sig,
"schnorr_sig": b.schnorr_sig,
}),
);
}
Ok(card)
}
Err(e) => {
eprintln!("wire: nostr binding skipped (build failed: {e})");
Ok(card)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::signing::generate_keypair;
#[test]
fn roundtrip_binding_verifies() {
let (ed_sk, ed_pk) = generate_keypair();
let (nostr_sk, nostr_xonly) = generate_transport_key();
let did = "did:wire:slate-lotus-88232017";
let b = build_binding(&ed_sk, &nostr_sk, did).unwrap();
assert_eq!(b64decode(&b.pubkey).unwrap(), nostr_xonly.to_vec());
assert_eq!(
verify_binding(&ed_pk, &b.pubkey, &b.ed_sig, &b.schnorr_sig, did),
Ok(nostr_xonly)
);
}
#[test]
fn wrong_identity_key_rejected() {
let (ed_sk, _ed_pk) = generate_keypair();
let (_other_sk, other_pk) = generate_keypair();
let (nostr_sk, _x) = generate_transport_key();
let did = "did:wire:x-1";
let b = build_binding(&ed_sk, &nostr_sk, did).unwrap();
assert!(matches!(
verify_binding(&other_pk, &b.pubkey, &b.ed_sig, &b.schnorr_sig, did),
Err(NostrKeyError::IdentitySig(_))
));
}
#[test]
fn npub_squat_without_possession_rejected() {
let (ed_sk, ed_pk) = generate_keypair();
let (_victim_sk, victim_xonly) = generate_transport_key();
let did = "did:wire:squatter-9";
let payload = binding_payload(did, &victim_xonly);
let ed_sig = sign_did_cert(&ed_sk, &payload).unwrap();
let (attacker_sk, _ax) = generate_transport_key();
let secp = Secp256k1::new();
let kp = Keypair::from_seckey_byte_array(&secp, attacker_sk).unwrap();
let digest = binding_digest(did, &victim_xonly);
let bad_sig = secp.sign_schnorr_no_aux_rand(&digest, &kp);
assert_eq!(
verify_binding(
&ed_pk,
&b64encode(&victim_xonly),
&ed_sig,
&b64encode(bad_sig.as_ref()),
did
),
Err(NostrKeyError::PossessionSig)
);
}
#[test]
fn tampered_did_rejected() {
let (ed_sk, ed_pk) = generate_keypair();
let (nostr_sk, _x) = generate_transport_key();
let b = build_binding(&ed_sk, &nostr_sk, "did:wire:real-1").unwrap();
assert!(
verify_binding(
&ed_pk,
&b.pubkey,
&b.ed_sig,
&b.schnorr_sig,
"did:wire:other-2"
)
.is_err()
);
}
#[test]
fn malformed_fields_rejected() {
let (_sk, pk) = generate_keypair();
assert_eq!(
verify_binding(&pk, "!!notb64", "x", "y", "did:wire:z"),
Err(NostrKeyError::BadEncoding)
);
assert_eq!(
verify_binding(&pk, &b64encode(b"short"), "x", "y", "did:wire:z"),
Err(NostrKeyError::BadEncoding)
);
}
#[test]
fn xonly_from_secret_matches_generation() {
let (sk, xonly) = generate_transport_key();
assert_eq!(xonly_from_secret(&sk).unwrap(), xonly);
}
#[test]
fn card_nostr_binding_reads_and_verifies() {
let (ed_sk, ed_pk) = generate_keypair();
let (nostr_sk, nostr_xonly) = generate_transport_key();
let did = "did:wire:reader-1";
let b = build_binding(&ed_sk, &nostr_sk, did).unwrap();
let card = serde_json::json!({
"did": did,
"nostr_pubkey": {
"pubkey": b.pubkey,
"ed_sig": b.ed_sig,
"schnorr_sig": b.schnorr_sig,
}
});
assert_eq!(card_nostr_binding(&card, &ed_pk), Ok(Some(nostr_xonly)));
let plain = serde_json::json!({"did": did});
assert_eq!(card_nostr_binding(&plain, &ed_pk), Ok(None));
}
}