mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-04-01 00:18:12 +02:00
commit
7c1ab3e627
258
nip26/nip26.go
Normal file
258
nip26/nip26.go
Normal file
@ -0,0 +1,258 @@
|
||||
package nip26
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
)
|
||||
|
||||
type DelegationToken struct {
|
||||
delegator [32]byte
|
||||
token [64]byte
|
||||
kinds []int
|
||||
since *time.Time
|
||||
until *time.Time
|
||||
tag [4]string
|
||||
}
|
||||
|
||||
// Tag() returns the nostr formatted delegation tag for the DelegationToken d.
|
||||
func (d *DelegationToken) Tag() nostr.Tag {
|
||||
return nostr.Tag(d.tag[:])
|
||||
}
|
||||
|
||||
// Conditions() is the delegation conditions string as in NIP-26.
|
||||
func (d *DelegationToken) Conditions() (conditions string) {
|
||||
for _, k := range d.kinds {
|
||||
conditions += fmt.Sprintf("kind=%d&", k)
|
||||
}
|
||||
if d.since != nil {
|
||||
conditions += fmt.Sprintf("created_at>%d&", d.since.Unix())
|
||||
}
|
||||
if d.until != nil {
|
||||
conditions += fmt.Sprintf("created_at<%d&", d.until.Unix())
|
||||
}
|
||||
return strings.TrimSuffix(conditions, "&")
|
||||
}
|
||||
|
||||
// This error is returned by d.Parse(ev) if the event ev does not have a delegation token.
|
||||
var NoDelegationTag error = fmt.Errorf("No Delegation Tag")
|
||||
|
||||
// This error is returned by Import(t,delegatee_pk) if the token signature verification fails.
|
||||
var VerificationFailed error = fmt.Errorf("VerificationFailed")
|
||||
|
||||
// CheckDelegation reads the event and reports whether or not it is correctly delegated.
|
||||
// If there is a delegation tag, the delegation token signature will be checked according to NIP-26.
|
||||
// If there is no delegation tag, ok will be true and err will be nil.
|
||||
// For checking many events, it is advisable to use Parse to reduce the number of memory allocations.
|
||||
func CheckDelegation(ev *nostr.Event) (ok bool, err error) {
|
||||
d := new(DelegationToken)
|
||||
if ok, err := d.Parse(ev); ok == true && (err == nil || err == NoDelegationTag) {
|
||||
return true, nil
|
||||
} else {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// Import verifies that t is NIP-26 delegation token for the given delegatee.
|
||||
// The returned DelegationToken object can be used in DelegatedSign.
|
||||
// If the token signature verification fails, the error VerificationFailed will be returned.
|
||||
func Import(t nostr.Tag, delegatee_pk string) (d *DelegationToken, e error) {
|
||||
d = new(DelegationToken)
|
||||
if len(t) == 4 && t[0] == "delegation" {
|
||||
copy(d.tag[:], t)
|
||||
} else {
|
||||
return nil, fmt.Errorf("not a delegation tag")
|
||||
}
|
||||
if n, e := hex.Decode(d.delegator[:], []byte(d.tag[1])); n != 32 || e != nil {
|
||||
return nil, fmt.Errorf("invalid delegation tag")
|
||||
}
|
||||
if n, e := hex.Decode(d.token[:], []byte(d.tag[3])); n != 64 || e != nil {
|
||||
return nil, fmt.Errorf("invalid delegation tag")
|
||||
}
|
||||
if d.kinds, d.since, d.until, e = parseConditions(d.tag[2]); e != nil {
|
||||
return nil, fmt.Errorf("invalid conditions string")
|
||||
}
|
||||
|
||||
// compute the digest
|
||||
h := sha256.Sum256([]byte(fmt.Sprintf("nostr:delegation:%s:%s", delegatee_pk, d.tag[2])))
|
||||
|
||||
sig, err := schnorr.ParseSignature(d.token[:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error: %s", err.Error())
|
||||
}
|
||||
|
||||
pubkey, err := schnorr.ParsePubKey(d.delegator[:])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error: %s", err.Error())
|
||||
}
|
||||
if !sig.Verify(h[:], pubkey) {
|
||||
return nil, VerificationFailed
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Parse reads the event ev and stores the delegation token into d.
|
||||
// The ok value verifies the event is correctly delegated.
|
||||
// If there is no delegation token, then d will not be changed. In this case the error value will be `NoDelegationTag`, and ok will be set to true.
|
||||
// Parse does NOT verify the event was correctly signed. Use ev.CheckSignature() for this.
|
||||
// More efficient memory allocations versus CheckDelegation(ev) if many events need to be checked.
|
||||
func (d *DelegationToken) Parse(ev *nostr.Event) (ok bool, err error) {
|
||||
for _, t := range ev.Tags {
|
||||
if t[0] == "delegation" && len(t) == 4 {
|
||||
copy(d.tag[:], t)
|
||||
goto jump1
|
||||
}
|
||||
}
|
||||
// event has no delegation. set the token to nil and return.
|
||||
return true, NoDelegationTag
|
||||
|
||||
jump1:
|
||||
if n, e := hex.Decode(d.delegator[:], []byte(d.tag[1])); n != 32 || e != nil {
|
||||
return false, fmt.Errorf("invalid delegation tag")
|
||||
}
|
||||
if n, e := hex.Decode(d.token[:], []byte(d.tag[3])); n != 64 || e != nil {
|
||||
return false, fmt.Errorf("invalid delegation tag")
|
||||
}
|
||||
|
||||
if d.kinds, d.since, d.until, err = parseConditions(d.tag[2]); err != nil {
|
||||
return false, fmt.Errorf("invalid conditions string")
|
||||
}
|
||||
|
||||
if len(d.kinds) > 0 {
|
||||
for _, k := range d.kinds {
|
||||
if ev.Kind == k {
|
||||
goto jump2
|
||||
}
|
||||
}
|
||||
return false, fmt.Errorf("Event kind %d is not allowed in delegation conditions.", ev.Kind)
|
||||
}
|
||||
|
||||
jump2:
|
||||
if d.since != nil && ev.CreatedAt.Before(*d.since) {
|
||||
return false, fmt.Errorf("Event is created before delegation conditions allow.")
|
||||
}
|
||||
if d.until != nil && ev.CreatedAt.After(*d.until) {
|
||||
return false, fmt.Errorf("Event is created after delegation conditions allow.")
|
||||
}
|
||||
|
||||
// compute the digest
|
||||
h := sha256.Sum256([]byte(fmt.Sprintf("nostr:delegation:%s:%s", ev.PubKey, d.tag[2])))
|
||||
|
||||
sig, err := schnorr.ParseSignature(d.token[:])
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("Error: %s", err.Error())
|
||||
}
|
||||
|
||||
pubkey, err := schnorr.ParsePubKey(d.delegator[:])
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("Error: %s", err.Error())
|
||||
}
|
||||
|
||||
return sig.Verify(h[:], pubkey), nil
|
||||
}
|
||||
|
||||
// DelegatedSign performs a delegated signature on the event ev.
|
||||
// The delegation signature is not verified. If desired, the caller can ensure the delegation signature is correct by calling d.Parse(ev) or CheckDelegation(ev) afterwards.
|
||||
func DelegatedSign(ev *nostr.Event, d *DelegationToken, delegatee_sk string) error {
|
||||
for _, t := range ev.Tags {
|
||||
if t[0] == "delegation" {
|
||||
return fmt.Errorf("event already has delegation token")
|
||||
}
|
||||
}
|
||||
if d.since != nil && ev.CreatedAt.Before(*d.since) || d.until != nil && ev.CreatedAt.After(*d.until) {
|
||||
return fmt.Errorf("event created_at field is not compatible with delegation token time conditions.")
|
||||
}
|
||||
if len(d.kinds) > 0 {
|
||||
for _, k := range d.kinds {
|
||||
if ev.Kind == k {
|
||||
goto jump
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("event kind %d is not compatible with delegation token conditions.", ev.Kind)
|
||||
}
|
||||
jump:
|
||||
if pk, e := nostr.GetPublicKey(delegatee_sk); e != nil {
|
||||
return fmt.Errorf("invalid delegatee secret key.")
|
||||
} else {
|
||||
ev.PubKey = pk
|
||||
}
|
||||
ev.Tags = append(ev.Tags, d.Tag())
|
||||
return ev.Sign(delegatee_sk)
|
||||
}
|
||||
|
||||
// CreateToken creates a DelegationToken according to NIP-26.
|
||||
func CreateToken(delegator_sk string, delegatee_pk string, kinds []int, since *time.Time, until *time.Time) (d *DelegationToken, e error) {
|
||||
d = new(DelegationToken)
|
||||
s, e := hex.DecodeString(delegator_sk)
|
||||
if e != nil {
|
||||
return nil, fmt.Errorf("invalid delegator secret key")
|
||||
}
|
||||
|
||||
tee_pk, e := hex.DecodeString(delegatee_pk)
|
||||
if len(tee_pk) != 32 || e != nil {
|
||||
return nil, fmt.Errorf("invalid delegatee pubkey")
|
||||
}
|
||||
|
||||
// set delegator
|
||||
sk, tor_pk := btcec.PrivKeyFromBytes(s)
|
||||
copy(d.delegator[:], schnorr.SerializePubKey(tor_pk))
|
||||
|
||||
d.kinds = kinds
|
||||
d.since = since
|
||||
d.until = until
|
||||
|
||||
// generate tag
|
||||
d.tag[0] = "delegation"
|
||||
d.tag[1] = fmt.Sprintf("%x", d.delegator)
|
||||
d.tag[2] = d.Conditions()
|
||||
|
||||
h := sha256.Sum256([]byte(fmt.Sprintf("nostr:delegation:%x:%s", tee_pk, d.tag[2])))
|
||||
|
||||
if sig, err := schnorr.Sign(sk, h[:]); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
copy(d.token[:], sig.Serialize())
|
||||
}
|
||||
|
||||
d.tag[3] = fmt.Sprintf("%x", d.token)
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func parseConditions(conditions string) (kinds []int, since *time.Time, until *time.Time, err error) {
|
||||
kinds = make([]int, 0)
|
||||
for _, v := range strings.Split(conditions, "&") {
|
||||
switch {
|
||||
case strings.HasPrefix(v, "kind="):
|
||||
if i, e := strconv.ParseInt(strings.TrimPrefix(v, "kind="), 10, 64); e == nil {
|
||||
kinds = append(kinds, int(i))
|
||||
} else {
|
||||
return nil, nil, nil, fmt.Errorf("Invalid: %s!", v)
|
||||
}
|
||||
case strings.HasPrefix(v, "created_at>") && since == nil:
|
||||
if i, e := strconv.ParseInt(strings.TrimPrefix(v, "created_at>"), 10, 64); e == nil {
|
||||
t := time.Unix(i, 0)
|
||||
since = &t
|
||||
} else {
|
||||
return nil, nil, nil, fmt.Errorf("Invalid: %s", v)
|
||||
}
|
||||
case strings.HasPrefix(v, "created_at<") && until == nil:
|
||||
if i, e := strconv.ParseInt(strings.TrimPrefix(v, "created_at<"), 10, 64); e == nil {
|
||||
t := time.Unix(i, 0)
|
||||
until = &t
|
||||
} else {
|
||||
return nil, nil, nil, fmt.Errorf("Invalid: %s", v)
|
||||
}
|
||||
default:
|
||||
return nil, nil, nil, fmt.Errorf("Invalid: %s", v)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
50
nip26/nip26_test.go
Normal file
50
nip26/nip26_test.go
Normal file
@ -0,0 +1,50 @@
|
||||
package nip26
|
||||
|
||||
import (
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDelegateSign(t *testing.T) {
|
||||
since := time.Unix(1600000000, 0)
|
||||
until := time.Unix(1600000100, 0)
|
||||
delegator_secret_key, delegatee_secret_key := "3f0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459da", "e9142f724955c5854de36324dab0434f97b15ec6b33464d56ebe491e3f559d1b"
|
||||
delegatee_pubkey, _ := nostr.GetPublicKey(delegatee_secret_key)
|
||||
d1, err := CreateToken(delegator_secret_key, delegatee_pubkey, []int{1, 2, 3}, &since, &until)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
ev := &nostr.Event{}
|
||||
ev.CreatedAt = time.Unix(1600000050, 0)
|
||||
ev.Content = "hello world"
|
||||
ev.Kind = 1
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
d2, err := Import(d1.Tag(), delegatee_pubkey)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err = DelegatedSign(ev, d2, delegatee_secret_key); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if ok, err := CheckDelegation(ev); err != nil || ok == false {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tag := []string{"delegation", "9ea72be3fcfe38103195a41b67b6f96c14ed92d2091d6d9eb8166a5c27b0c35d", "kind=1&kind=2&kind=3&created_at>1600000000", "8432b8c86f789c2783ef3becb0fabf4def6031c6a615fa7a622f31329d80ed1b2a79ab753c0462f1440503c94e1829158a3a854a1d418ad256ae2cf8aa19fa9a"}
|
||||
d3, err := Import(tag, delegatee_pubkey)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
ev.Tags = nil
|
||||
if err = DelegatedSign(ev, d3, delegatee_secret_key); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if ok, err := d3.Parse(ev); err != nil || ok == false {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user