use k256::schnorr::SigningKey;
use sha2::{Digest, Sha256};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Nip26Error {
#[error("invalid delegator secret key")]
InvalidDelegatorKey,
#[error("invalid pubkey hex: {0}")]
InvalidPubkey(String),
#[error("invalid condition: {0}")]
InvalidCondition(String),
#[error("invalid delegation signature")]
InvalidSignature,
#[error("malformed delegation tag")]
MalformedTag,
#[error("wrong delegation tag name")]
WrongTagName,
#[error("delegation conditions not satisfied: {0}")]
ConditionsNotSatisfied(String),
#[error("signing failed: {0}")]
SigningFailed(String),
#[error("hex decode error: {0}")]
HexError(String),
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Conditions {
pub kinds: Vec<u64>,
pub created_after: Option<u64>,
pub created_before: Option<u64>,
}
impl Conditions {
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Result<Self, Nip26Error> {
let mut c = Conditions::default();
if s.is_empty() {
return Ok(c);
}
for part in s.split('&') {
if part.is_empty() {
continue;
}
if let Some(kind_str) = part.strip_prefix("kind=") {
let k: u64 = kind_str.parse().map_err(|_| {
Nip26Error::InvalidCondition(format!("invalid kind: {kind_str}"))
})?;
c.kinds.push(k);
} else if let Some(ts_str) = part.strip_prefix("created_at>") {
let t: u64 = ts_str.parse().map_err(|_| {
Nip26Error::InvalidCondition(format!("invalid timestamp: {ts_str}"))
})?;
c.created_after = Some(t);
} else if let Some(ts_str) = part.strip_prefix("created_at<") {
let t: u64 = ts_str.parse().map_err(|_| {
Nip26Error::InvalidCondition(format!("invalid timestamp: {ts_str}"))
})?;
c.created_before = Some(t);
} else {
return Err(Nip26Error::InvalidCondition(format!(
"unknown condition: {part}"
)));
}
}
Ok(c)
}
#[allow(clippy::inherent_to_string)]
pub fn to_string(&self) -> String {
let mut parts = Vec::new();
for k in &self.kinds {
parts.push(format!("kind={k}"));
}
if let Some(ts) = self.created_after {
parts.push(format!("created_at>{ts}"));
}
if let Some(ts) = self.created_before {
parts.push(format!("created_at<{ts}"));
}
parts.join("&")
}
pub fn permits(&self, kind: u64, created_at: u64) -> bool {
if !self.kinds.is_empty() && !self.kinds.contains(&kind) {
return false;
}
if let Some(after) = self.created_after {
if !(after + 1..).contains(&created_at) {
return false;
}
}
if let Some(before) = self.created_before {
if !(..before).contains(&created_at) {
return false;
}
}
true
}
}
pub struct DelegationToken {
pub delegator_pubkey: String,
pub delegatee_pubkey: String,
pub conditions_str: String,
pub conditions: Conditions,
pub sig: String,
}
impl DelegationToken {
pub fn create(
delegator_sk: &[u8; 32],
delegatee_pubkey: &str,
conditions: &Conditions,
) -> Result<Self, Nip26Error> {
let signing_key =
SigningKey::from_bytes(delegator_sk).map_err(|_| Nip26Error::InvalidDelegatorKey)?;
let delegator_pubkey = hex::encode(signing_key.verifying_key().to_bytes());
let conditions_str = conditions.to_string();
let hash = delegation_token_hash(delegatee_pubkey, &conditions_str);
let mut aux = [0u8; 32];
let _ = getrandom::getrandom(&mut aux);
let signature = signing_key
.sign_raw(&hash, &aux)
.map_err(|e| Nip26Error::SigningFailed(e.to_string()))?;
Ok(DelegationToken {
delegator_pubkey,
delegatee_pubkey: delegatee_pubkey.to_string(),
conditions_str,
conditions: conditions.clone(),
sig: hex::encode(signature.to_bytes()),
})
}
pub fn verify(&self) -> Result<(), Nip26Error> {
let pk_bytes = decode_hex32(&self.delegator_pubkey)?;
let verifying_key = k256::schnorr::VerifyingKey::from_bytes(&pk_bytes)
.map_err(|e| Nip26Error::InvalidPubkey(e.to_string()))?;
let hash = delegation_token_hash(&self.delegatee_pubkey, &self.conditions_str);
let sig_bytes = hex::decode(&self.sig).map_err(|e| Nip26Error::HexError(e.to_string()))?;
let sig = k256::schnorr::Signature::try_from(sig_bytes.as_slice())
.map_err(|_| Nip26Error::InvalidSignature)?;
verifying_key
.verify_raw(&hash, &sig)
.map_err(|_| Nip26Error::InvalidSignature)
}
pub fn from_tag(tag: &DelegationTag) -> Result<Self, Nip26Error> {
if tag.0.len() < 4 {
return Err(Nip26Error::MalformedTag);
}
if tag.0[0] != "delegation" {
return Err(Nip26Error::WrongTagName);
}
let delegator_pubkey = tag.0[1].clone();
let conditions_str = tag.0[2].clone();
let sig = tag.0[3].clone();
decode_hex32(&delegator_pubkey)?;
let conditions = Conditions::from_str(&conditions_str)?;
let delegatee_pubkey = tag.0.get(4).cloned().unwrap_or_default();
Ok(DelegationToken {
delegator_pubkey,
delegatee_pubkey,
conditions_str,
conditions,
sig,
})
}
}
pub struct DelegationTag(pub Vec<String>);
impl DelegationTag {
pub fn from_token(token: &DelegationToken) -> Self {
DelegationTag(vec![
"delegation".to_string(),
token.delegator_pubkey.clone(),
token.conditions_str.clone(),
token.sig.clone(),
token.delegatee_pubkey.clone(),
])
}
}
pub fn validate_delegation_tag(
tag: &DelegationTag,
author_pubkey: &str,
kind: u64,
created_at: u64,
) -> Result<(), Nip26Error> {
let mut token = DelegationToken::from_tag(tag)?;
token.delegatee_pubkey = author_pubkey.to_string();
token.verify()?;
if !token.conditions.permits(kind, created_at) {
return Err(Nip26Error::ConditionsNotSatisfied(format!(
"kind={kind} created_at={created_at} not permitted by '{}'",
token.conditions_str
)));
}
Ok(())
}
fn delegation_token_hash(delegatee_pubkey: &str, conditions_str: &str) -> [u8; 32] {
let message = format!("nostr:delegation:{delegatee_pubkey}:{conditions_str}");
Sha256::digest(message.as_bytes()).into()
}
fn decode_hex32(hex_str: &str) -> Result<[u8; 32], Nip26Error> {
let bytes = hex::decode(hex_str).map_err(|e| Nip26Error::HexError(e.to_string()))?;
if bytes.len() != 32 {
return Err(Nip26Error::InvalidPubkey(format!(
"expected 32 bytes, got {}",
bytes.len()
)));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(arr)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keys::generate_keypair;
fn make_keypair() -> ([u8; 32], String) {
let kp = generate_keypair().unwrap();
let sk = *kp.secret.as_bytes();
let pk = kp.public.to_hex();
(sk, pk)
}
#[test]
fn conditions_parse_empty() {
let c = Conditions::from_str("").unwrap();
assert_eq!(c, Conditions::default());
}
#[test]
fn conditions_parse_multi_kind() {
let c = Conditions::from_str("kind=1&kind=6").unwrap();
assert_eq!(c.kinds, vec![1u64, 6u64]);
}
#[test]
fn conditions_strict_boundary() {
let c = Conditions::from_str("created_at>100&created_at<200").unwrap();
assert!(!c.permits(1, 100));
assert!(c.permits(1, 101));
assert!(!c.permits(1, 200));
assert!(c.permits(1, 199));
}
#[test]
fn conditions_unknown_key_error() {
assert!(Conditions::from_str("author=abc").is_err());
}
#[test]
fn delegation_roundtrip() {
let (delegator_sk, _) = make_keypair();
let (_, delegatee_pk) = make_keypair();
let conditions = Conditions::from_str("kind=1").unwrap();
let token = DelegationToken::create(&delegator_sk, &delegatee_pk, &conditions).unwrap();
assert_eq!(token.sig.len(), 128);
token.verify().unwrap();
}
#[test]
fn validate_valid_delegation() {
let (delegator_sk, _) = make_keypair();
let (_, delegatee_pk) = make_keypair();
let conditions = Conditions::from_str("kind=1&created_at>0&created_at<9999999999").unwrap();
let token = DelegationToken::create(&delegator_sk, &delegatee_pk, &conditions).unwrap();
let tag = DelegationTag::from_token(&token);
validate_delegation_tag(&tag, &delegatee_pk, 1, 1_000_000_000).unwrap();
}
#[test]
fn validate_wrong_kind_fails() {
let (delegator_sk, _) = make_keypair();
let (_, delegatee_pk) = make_keypair();
let conditions = Conditions::from_str("kind=1").unwrap();
let token = DelegationToken::create(&delegator_sk, &delegatee_pk, &conditions).unwrap();
let tag = DelegationTag::from_token(&token);
let err = validate_delegation_tag(&tag, &delegatee_pk, 4, 0).unwrap_err();
assert!(matches!(err, Nip26Error::ConditionsNotSatisfied(_)));
}
}