Skip to main content

nula_core/nips/
nip26.rs

1//! [NIP-26] Delegated Event Signing.
2//!
3//! NIP-26 lets a *delegator* keypair authorise a *delegatee* keypair to
4//! publish events on its behalf, scoped by a `kind=` / `created_at<` /
5//! `created_at>` condition string. The delegation lives entirely
6//! inside one extra `delegation` tag on each event the delegatee
7//! publishes; the event itself is still signed by the delegatee's
8//! key, and clients that want to honour the delegation verify the
9//! token against the delegator's pubkey.
10//!
11//! ```jsonc
12//! [
13//!   "delegation",
14//!   "<delegator-pubkey-hex>",
15//!   "<conditions-query-string>",
16//!   "<delegation-token-hex>"
17//! ]
18//! ```
19//!
20//! The token is a 64-byte BIP-340 Schnorr signature of `SHA-256` of
21//! the ASCII string
22//! `nostr:delegation:<delegatee-pubkey-hex>:<conditions>`.
23//!
24//! # Status
25//!
26//! NIP-26 carries an `unrecommended` warning in the spec: relays and
27//! clients have largely moved on to NIP-46 remote signers as the
28//! preferred way to keep the root key cold. We still ship a complete
29//! implementation so existing on-relay corpora remain decodable and
30//! so callers can migrate off NIP-26 at their own pace.
31//!
32//! # Authoring & verifying
33//!
34//! ```
35//! use nula_core::Keys;
36//! use nula_core::event::Kind;
37//! use nula_core::nips::nip26::{Conditions, sign_delegation, verify_delegation};
38//!
39//! let delegator = Keys::generate().unwrap();
40//! let delegatee = Keys::generate().unwrap();
41//! let conditions = Conditions::new().allow_kind(Kind::TEXT_NOTE);
42//!
43//! let token = sign_delegation(&delegator, delegatee.public_key(), &conditions);
44//! assert!(verify_delegation(
45//!     delegator.public_key(),
46//!     delegatee.public_key(),
47//!     &conditions,
48//!     &token,
49//! ));
50//! ```
51//!
52//! [NIP-26]: https://github.com/nostr-protocol/nips/blob/master/26.md
53
54use std::fmt;
55
56use secp256k1::schnorr::Signature;
57use sha2::{Digest, Sha256};
58use thiserror::Error;
59
60use crate::event::{Event, Kind, Tag, TagKind, Tags};
61use crate::key::{Keys, PublicKey};
62use crate::types::Timestamp;
63
64/// Tag head for the delegation tag.
65pub const DELEGATION_TAG_KEY: &str = "delegation";
66
67/// One condition in the NIP-26 query string.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
69#[non_exhaustive]
70pub enum Condition {
71    /// `kind=<u16>` — the delegatee may sign only this kind.
72    Kind(Kind),
73    /// `created_at>=<ts>` (rendered as `created_at><ts>` per NIP-26)
74    /// — the event's `created_at` MUST be strictly after `ts`.
75    CreatedAfter(Timestamp),
76    /// `created_at<<ts>` — the event's `created_at` MUST be strictly
77    /// before `ts`.
78    CreatedBefore(Timestamp),
79}
80
81impl fmt::Display for Condition {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match self {
84            Self::Kind(k) => write!(f, "kind={}", k.as_u16()),
85            Self::CreatedAfter(ts) => write!(f, "created_at>{}", ts.as_secs()),
86            Self::CreatedBefore(ts) => write!(f, "created_at<{}", ts.as_secs()),
87        }
88    }
89}
90
91/// A list of [`Condition`]s.
92///
93/// Order is preserved through `parse` / `render` so a parsed-then-
94/// rendered string is byte-identical to its input — required to keep
95/// the delegation token verifiable.
96#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
97pub struct Conditions {
98    items: Vec<Condition>,
99}
100
101impl Conditions {
102    /// Construct an empty (unconditional) list.
103    #[must_use]
104    pub const fn new() -> Self {
105        Self { items: Vec::new() }
106    }
107
108    /// Append a `kind=` condition.
109    #[must_use]
110    pub fn allow_kind(mut self, kind: Kind) -> Self {
111        self.items.push(Condition::Kind(kind));
112        self
113    }
114
115    /// Append a `created_at>` condition (the event's `created_at`
116    /// must be strictly after `ts`).
117    #[must_use]
118    pub fn after(mut self, ts: Timestamp) -> Self {
119        self.items.push(Condition::CreatedAfter(ts));
120        self
121    }
122
123    /// Append a `created_at<` condition (the event's `created_at`
124    /// must be strictly before `ts`).
125    #[must_use]
126    pub fn before(mut self, ts: Timestamp) -> Self {
127        self.items.push(Condition::CreatedBefore(ts));
128        self
129    }
130
131    /// Iterate the inner conditions in declaration order.
132    pub fn iter(&self) -> impl Iterator<Item = &Condition> {
133        self.items.iter()
134    }
135
136    /// `true` when no condition is set.
137    #[must_use]
138    pub const fn is_empty(&self) -> bool {
139        self.items.is_empty()
140    }
141
142    /// Render the canonical wire form (`kind=1&created_at>123`).
143    ///
144    /// An empty [`Conditions`] renders to `""`. Order matches the
145    /// order in which conditions were appended.
146    #[must_use]
147    pub fn render(&self) -> String {
148        let mut out = String::new();
149        for (i, c) in self.items.iter().enumerate() {
150            if i > 0 {
151                out.push('&');
152            }
153            out.push_str(&c.to_string());
154        }
155        out
156    }
157
158    /// Parse a NIP-26 conditions query string.
159    ///
160    /// # Errors
161    ///
162    /// Returns [`ConditionsError`] for any malformed clause — empty
163    /// segment, unsupported field, missing operator, non-numeric
164    /// value, etc.
165    pub fn parse(s: &str) -> Result<Self, ConditionsError> {
166        if s.is_empty() {
167            return Ok(Self::new());
168        }
169        let mut items = Vec::with_capacity(s.matches('&').count() + 1);
170        for raw in s.split('&') {
171            items.push(parse_one(raw)?);
172        }
173        Ok(Self { items })
174    }
175
176    /// `true` when an event with the given `(kind, created_at)`
177    /// satisfies *every* condition in the list. An empty
178    /// [`Conditions`] always matches.
179    #[must_use]
180    pub fn matches(&self, kind: Kind, created_at: Timestamp) -> bool {
181        self.items.iter().all(|c| match c {
182            Condition::Kind(k) => *k == kind,
183            Condition::CreatedAfter(ts) => created_at.as_secs() > ts.as_secs(),
184            Condition::CreatedBefore(ts) => created_at.as_secs() < ts.as_secs(),
185        })
186    }
187}
188
189fn parse_one(raw: &str) -> Result<Condition, ConditionsError> {
190    if let Some(rest) = raw.strip_prefix("kind=") {
191        let n: u16 = rest
192            .parse()
193            .map_err(|_| ConditionsError::InvalidValue(raw.to_owned()))?;
194        return Ok(Condition::Kind(Kind::new(n)));
195    }
196    if let Some(rest) = raw.strip_prefix("created_at>") {
197        let n: u64 = rest
198            .parse()
199            .map_err(|_| ConditionsError::InvalidValue(raw.to_owned()))?;
200        return Ok(Condition::CreatedAfter(Timestamp::from_secs(n)));
201    }
202    if let Some(rest) = raw.strip_prefix("created_at<") {
203        let n: u64 = rest
204            .parse()
205            .map_err(|_| ConditionsError::InvalidValue(raw.to_owned()))?;
206        return Ok(Condition::CreatedBefore(Timestamp::from_secs(n)));
207    }
208    Err(ConditionsError::UnsupportedClause(raw.to_owned()))
209}
210
211/// Errors produced by [`Conditions::parse`].
212#[derive(Debug, Clone, PartialEq, Eq, Error)]
213#[non_exhaustive]
214pub enum ConditionsError {
215    /// A clause did not match any of the supported NIP-26 fields.
216    #[error("unsupported delegation clause `{0}`")]
217    UnsupportedClause(String),
218    /// A clause's value could not be parsed as `u16` / `u64`.
219    #[error("invalid value in delegation clause `{0}`")]
220    InvalidValue(String),
221}
222
223/// Compute the canonical delegation message that gets hashed and
224/// signed.
225///
226/// Public for callers that need to integrate with an external signer
227/// (NIP-07 browser extension, hardware wallet, NIP-46 bunker) where
228/// the signing happens out-of-process.
229#[must_use]
230pub fn delegation_message(delegatee: &PublicKey, conditions: &Conditions) -> String {
231    format!(
232        "nostr:delegation:{}:{}",
233        delegatee.to_hex(),
234        conditions.render()
235    )
236}
237
238/// 32-byte hash that the delegator signs to mint a delegation token.
239#[must_use]
240pub fn delegation_hash(delegatee: &PublicKey, conditions: &Conditions) -> [u8; 32] {
241    let msg = delegation_message(delegatee, conditions);
242    let digest = Sha256::digest(msg.as_bytes());
243    digest.into()
244}
245
246/// 64-byte BIP-340 Schnorr signature on the delegation hash.
247pub type DelegationToken = Signature;
248
249/// Sign a delegation as the *delegator*.
250#[must_use]
251pub fn sign_delegation(
252    delegator: &Keys,
253    delegatee: &PublicKey,
254    conditions: &Conditions,
255) -> DelegationToken {
256    let h = delegation_hash(delegatee, conditions);
257    delegator.sign_schnorr(&h)
258}
259
260/// Verify a delegation token.
261#[must_use]
262pub fn verify_delegation(
263    delegator: &PublicKey,
264    delegatee: &PublicKey,
265    conditions: &Conditions,
266    token: &DelegationToken,
267) -> bool {
268    let h = delegation_hash(delegatee, conditions);
269    delegator.verify_schnorr(&h, token)
270}
271
272impl Tag {
273    /// Build a NIP-26 `delegation` tag.
274    ///
275    /// Wire form: `["delegation", <delegator-hex>, <conditions>, <token-hex>]`.
276    #[must_use]
277    pub fn delegation(
278        delegator: PublicKey,
279        conditions: &Conditions,
280        token: &DelegationToken,
281    ) -> Self {
282        Self::with(
283            &TagKind::Custom(DELEGATION_TAG_KEY.to_owned()),
284            [delegator.to_hex(), conditions.render(), token.to_string()],
285        )
286    }
287}
288
289/// A parsed `delegation` tag.
290#[derive(Debug, Clone, PartialEq, Eq)]
291pub struct Delegation {
292    /// Delegator's public key.
293    pub delegator: PublicKey,
294    /// Conditions on the delegated authority.
295    pub conditions: Conditions,
296    /// 64-byte Schnorr token signed by `delegator`.
297    pub token: DelegationToken,
298}
299
300/// Errors produced when reading a `delegation` tag off the wire.
301#[derive(Debug, Error)]
302#[non_exhaustive]
303pub enum DelegationError {
304    /// The `delegation` tag was missing or malformed.
305    #[error("event has no well-formed `delegation` tag")]
306    Missing,
307    /// The delegator pubkey hex was malformed.
308    #[error(transparent)]
309    Pubkey(#[from] crate::key::PublicKeyError),
310    /// The conditions string did not parse.
311    #[error(transparent)]
312    Conditions(#[from] ConditionsError),
313    /// The token hex did not decode into a 64-byte Schnorr signature.
314    #[error("invalid delegation token: {0}")]
315    Token(String),
316    /// The token signature did not verify against the delegator key.
317    #[error("delegation token does not verify against the declared delegator")]
318    InvalidSignature,
319    /// The event's `(kind, created_at)` does not satisfy the
320    /// declared conditions.
321    #[error("event does not satisfy the delegation conditions")]
322    ConditionsViolated,
323}
324
325/// Extract the `delegation` tag from an [`Event`] or [`Tags`] list,
326/// if present and well-formed.
327///
328/// This does **not** verify the cryptographic token; use
329/// [`verify_event_delegation`] for that.
330///
331/// # Errors
332///
333/// - [`DelegationError::Missing`] when no `delegation` tag is present
334///   or it is shorter than the four wire-form values.
335/// - [`DelegationError::Pubkey`] / [`DelegationError::Conditions`] /
336///   [`DelegationError::Token`] for individually malformed pieces.
337pub fn parse_delegation(tags: &Tags) -> Result<Delegation, DelegationError> {
338    for tag in tags {
339        if !is_delegation_tag(&tag.kind()) {
340            continue;
341        }
342        let values = tag.values();
343        let (Some(d), Some(c), Some(t)) = (values.get(1), values.get(2), values.get(3)) else {
344            return Err(DelegationError::Missing);
345        };
346        let delegator = PublicKey::parse(d)?;
347        let conditions = Conditions::parse(c)?;
348        let token = t
349            .parse::<Signature>()
350            .map_err(|e| DelegationError::Token(e.to_string()))?;
351        return Ok(Delegation {
352            delegator,
353            conditions,
354            token,
355        });
356    }
357    Err(DelegationError::Missing)
358}
359
360fn is_delegation_tag(kind: &TagKind) -> bool {
361    matches!(kind, TagKind::Custom(s) if s == DELEGATION_TAG_KEY)
362}
363
364/// End-to-end verifier: an event is *delegation-valid* iff
365///
366/// 1. it carries a well-formed `delegation` tag,
367/// 2. the embedded token verifies as a Schnorr signature by
368///    `delegator` over `nostr:delegation:<event.pubkey>:<conditions>`,
369/// 3. the event's `(kind, created_at)` satisfies the conditions.
370///
371/// The event's own NIP-01 signature is **not** re-checked here:
372/// callers should call [`Event::verify`] independently. Splitting the
373/// two checks keeps the cost composable when verifying a batch.
374///
375/// # Errors
376///
377/// Returns the corresponding [`DelegationError`] on the first failed
378/// step.
379pub fn verify_event_delegation(event: &Event) -> Result<Delegation, DelegationError> {
380    let delegation = parse_delegation(&event.tags)?;
381    if !verify_delegation(
382        &delegation.delegator,
383        &event.pubkey,
384        &delegation.conditions,
385        &delegation.token,
386    ) {
387        return Err(DelegationError::InvalidSignature);
388    }
389    if !delegation.conditions.matches(event.kind, event.created_at) {
390        return Err(DelegationError::ConditionsViolated);
391    }
392    Ok(delegation)
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use crate::EventBuilder;
399    use crate::event::Kind;
400
401    fn fixture_keys(byte: u8) -> Keys {
402        let hex: String = format!("{byte:064x}");
403        Keys::parse(&hex).unwrap()
404    }
405
406    #[test]
407    fn conditions_render_and_parse_round_trip() {
408        let c = Conditions::new()
409            .allow_kind(Kind::TEXT_NOTE)
410            .after(Timestamp::from_secs(1_700_000_000))
411            .before(Timestamp::from_secs(1_800_000_000));
412        let rendered = c.render();
413        assert_eq!(
414            rendered,
415            "kind=1&created_at>1700000000&created_at<1800000000"
416        );
417        assert_eq!(Conditions::parse(&rendered).unwrap(), c);
418    }
419
420    #[test]
421    fn empty_conditions_render_to_empty_string() {
422        let empty = Conditions::new();
423        assert_eq!(empty.render(), "");
424        assert_eq!(Conditions::parse("").unwrap(), empty);
425        // An empty conditions list always matches.
426        assert!(empty.matches(Kind::TEXT_NOTE, Timestamp::from_secs(1)));
427    }
428
429    #[test]
430    fn parse_rejects_malformed_clauses() {
431        assert!(matches!(
432            Conditions::parse("foobar=1"),
433            Err(ConditionsError::UnsupportedClause(s)) if s == "foobar=1"
434        ));
435        assert!(matches!(
436            Conditions::parse("kind=abc"),
437            Err(ConditionsError::InvalidValue(s)) if s == "kind=abc"
438        ));
439    }
440
441    #[test]
442    fn matches_enforces_strict_inequalities() {
443        let c = Conditions::new()
444            .after(Timestamp::from_secs(100))
445            .before(Timestamp::from_secs(200));
446        assert!(c.matches(Kind::TEXT_NOTE, Timestamp::from_secs(150)));
447        // Boundary: NIP-26 uses strict `<` / `>`.
448        assert!(!c.matches(Kind::TEXT_NOTE, Timestamp::from_secs(100)));
449        assert!(!c.matches(Kind::TEXT_NOTE, Timestamp::from_secs(200)));
450    }
451
452    #[test]
453    fn delegation_token_verifies_with_correct_inputs() {
454        let delegator = fixture_keys(1);
455        let delegatee = fixture_keys(2);
456        let conditions = Conditions::new().allow_kind(Kind::TEXT_NOTE);
457
458        let token = sign_delegation(&delegator, delegatee.public_key(), &conditions);
459        assert!(verify_delegation(
460            delegator.public_key(),
461            delegatee.public_key(),
462            &conditions,
463            &token,
464        ));
465    }
466
467    #[test]
468    fn delegation_token_fails_when_delegatee_changes() {
469        let delegator = fixture_keys(1);
470        let delegatee_a = fixture_keys(2);
471        let delegatee_b = fixture_keys(3);
472        let conditions = Conditions::new().allow_kind(Kind::TEXT_NOTE);
473
474        let token = sign_delegation(&delegator, delegatee_a.public_key(), &conditions);
475        assert!(!verify_delegation(
476            delegator.public_key(),
477            delegatee_b.public_key(),
478            &conditions,
479            &token,
480        ));
481    }
482
483    #[test]
484    fn delegation_token_fails_when_conditions_change() {
485        let delegator = fixture_keys(1);
486        let delegatee = fixture_keys(2);
487        let signed_conditions = Conditions::new().allow_kind(Kind::TEXT_NOTE);
488        let mutated_conditions = Conditions::new().allow_kind(Kind::REACTION);
489
490        let token = sign_delegation(&delegator, delegatee.public_key(), &signed_conditions);
491        assert!(!verify_delegation(
492            delegator.public_key(),
493            delegatee.public_key(),
494            &mutated_conditions,
495            &token,
496        ));
497    }
498
499    #[test]
500    fn parse_delegation_round_trips_through_a_built_event() {
501        let delegator = fixture_keys(1);
502        let delegatee = fixture_keys(2);
503        let conditions = Conditions::new().allow_kind(Kind::TEXT_NOTE);
504        let token = sign_delegation(&delegator, delegatee.public_key(), &conditions);
505
506        let event = EventBuilder::text_note("delegated note")
507            .tag(Tag::delegation(
508                *delegator.public_key(),
509                &conditions,
510                &token,
511            ))
512            .sign_with_keys(&delegatee)
513            .unwrap();
514
515        let parsed = parse_delegation(&event.tags).unwrap();
516        assert_eq!(&parsed.delegator, delegator.public_key());
517        assert_eq!(parsed.conditions, conditions);
518
519        let verified = verify_event_delegation(&event).expect("delegation must verify");
520        assert_eq!(&verified.delegator, delegator.public_key());
521    }
522
523    #[test]
524    fn verify_event_delegation_rejects_violated_conditions() {
525        let delegator = fixture_keys(1);
526        let delegatee = fixture_keys(2);
527        // Allow only TEXT_NOTE — but build a REACTION event.
528        let conditions = Conditions::new().allow_kind(Kind::TEXT_NOTE);
529        let token = sign_delegation(&delegator, delegatee.public_key(), &conditions);
530
531        let event = EventBuilder::new(Kind::REACTION, "+")
532            .tag(Tag::delegation(
533                *delegator.public_key(),
534                &conditions,
535                &token,
536            ))
537            .sign_with_keys(&delegatee)
538            .unwrap();
539
540        assert!(matches!(
541            verify_event_delegation(&event),
542            Err(DelegationError::ConditionsViolated)
543        ));
544    }
545
546    #[test]
547    fn verify_event_delegation_detects_token_tampering() {
548        let delegator = fixture_keys(1);
549        let delegatee = fixture_keys(2);
550        let real = Conditions::new().allow_kind(Kind::TEXT_NOTE);
551        let real_token = sign_delegation(&delegator, delegatee.public_key(), &real);
552
553        // Build the event with mutated conditions but the token from
554        // the original conditions.
555        let mutated = Conditions::new().allow_kind(Kind::REACTION);
556        let event = EventBuilder::new(Kind::REACTION, "+")
557            .tag(Tag::delegation(
558                *delegator.public_key(),
559                &mutated,
560                &real_token,
561            ))
562            .sign_with_keys(&delegatee)
563            .unwrap();
564
565        assert!(matches!(
566            verify_event_delegation(&event),
567            Err(DelegationError::InvalidSignature)
568        ));
569    }
570}