fixing signer and partly implemented nip-98

This commit is contained in:
2025-01-29 09:30:35 -01:06
parent 31c6e08cd1
commit 4b5fcc4f34
12 changed files with 278 additions and 64 deletions

View File

@@ -49,7 +49,16 @@ func PublicKeyToNpub(pk *secp256k1.PublicKey) (encoded by, err er) {
// NsecToSecretKey decodes a nostr secret key (nsec) and returns the secp256k1
// secret key.
func NsecToSecretKey(encoded by) (sk *secp256k1.SecretKey, err er) {
var b5, b8, hrp by
var b8 by
if b8, err = NsecToBytes(encoded); chk.E(err) {
return
}
sk = secp256k1.SecKeyFromBytes(b8)
return
}
func NsecToBytes(encoded by) (sk by, err er) {
var b5, hrp by
if hrp, b5, err = bech32.Decode(encoded); chk.E(err) {
return
}
@@ -58,10 +67,26 @@ func NsecToSecretKey(encoded by) (sk *secp256k1.SecretKey, err er) {
hrp, SecHRP)
return
}
if b8, err = ConvertFromBech32(b5); chk.E(err) {
if sk, err = ConvertFromBech32(b5); chk.E(err) {
return
}
sk = secp256k1.SecKeyFromBytes(b8)
return
}
func NpubToBytes(encoded by) (pk by, err er) {
var b5, hrp by
if hrp, b5, err = bech32.Decode(encoded); chk.E(err) {
return
}
if !equals(hrp, PubHRP) {
err = log.E.Err("wrong human readable part, got '%s' want '%s'",
hrp, SecHRP)
return
}
if pk, err = ConvertFromBech32(b5); chk.E(err) {
return
}
pk = pk[:schnorr.PubKeyBytesLen]
return
}
@@ -82,7 +107,7 @@ func NpubToPublicKey(encoded by) (pk *secp256k1.PublicKey, err er) {
return
}
return schnorr.ParsePubKey(b8[:32])
return schnorr.ParsePubKey(b8[:schnorr.PubKeyBytesLen])
}
// HexToPublicKey decodes a string that should be a 64 character long hex

5
cmd/meow/main.go Normal file
View File

@@ -0,0 +1,5 @@
package main
func main() {
}

64
cmd/nurl/main.go Normal file
View File

@@ -0,0 +1,64 @@
package main
import (
"os"
"fmt"
"realy.lol/bech32encoding"
"realy.lol/p256k"
"realy.lol/httpauth"
"net/http"
"strings"
"io"
)
const secEnv = "NOSTR_SECRET_KEY"
func fail(format string, a ...any) {
_, _ = fmt.Fprintf(os.Stderr, format+"\n", a...)
os.Exit(1)
}
func main() {
if len(os.Args) < 3 {
fail(`error: nurl requires minimum 2 args: <get/post> <url>
singing nsec (in bech32 format) is expected to be found in %s environment variable.
`, secEnv)
}
meth := strings.ToLower(os.Args[1])
ur := os.Args[2]
switch meth {
case "get", "post":
default:
fail("first parameter must be either 'get' or 'post', got '%s'", meth)
}
var sk []byte
var err error
if sk, err = bech32encoding.NsecToBytes([]byte(os.Getenv(secEnv))); chk.E(err) {
fail("failed to decode nsec: '%s'", err.Error())
}
sign := &p256k.Signer{}
if err = sign.InitSec(sk); chk.E(err) {
fail("failed to init signer: '%s'", err.Error())
}
var read io.ReadCloser
// if we are uploading data
if len(os.Args) > 3 && meth == "post" {
switch os.Args[3] {
// as is common, `-` means "read data from stdin"
case "-":
read = os.Stdin
default:
// otherwise assume it is a file and fail if it isn't
if read, err = os.OpenFile(os.Args[3], os.O_RDONLY, 0600); chk.E(err) {
fail("failed to open file for reading")
}
}
}
var r *http.Request
if r, err = httpauth.MakeRequest(ur, meth, sign, read); chk.E(err) {
fail("failed to create nostr authed http request: %s", err.Error())
}
_ = r
}

22
cmd/nurl/util.go Normal file
View File

@@ -0,0 +1,22 @@
package main
import (
"bytes"
"realy.lol/context"
"realy.lol/lol"
)
type (
bo = bool
by = []byte
st = string
er = error
no = int
cx = context.T
)
var (
log, chk, errorf = lol.Main.Log, lol.Main.Check, lol.Main.Errorf
equals = bytes.Equal
)

View File

@@ -20,6 +20,10 @@ import (
"realy.lol/realy/config"
"realy.lol/units"
"realy.lol/realy/options"
"strings"
"realy.lol/bech32encoding"
"realy.lol/p256k"
"realy.lol/signer"
)
func main() {
@@ -71,14 +75,31 @@ func main() {
r := &app.Relay{C: cfg, Store: storage}
go app.MonitorResources(c)
var server *realy.Server
admins := strings.Split(cfg.AdminNpubs, ",")
var administrators []signer.I
for _, admin := range admins {
if len(admin) < 1 {
continue
}
var pk by
if pk, err = bech32encoding.NpubToBytes(by(admin)); chk.E(err) {
return
}
log.I.S(pk)
sign := &p256k.Signer{}
if err = sign.InitPub(pk); chk.E(err) {
return
}
administrators = append(administrators, sign)
log.I.F("administrator pubkey: %0x", sign.Pub())
}
serverParams := &realy.ServerParams{
Ctx: c,
Cancel: cancel,
Rl: r,
DbPath: cfg.DataDir,
MaxLimit: ratel.DefaultMaxLimit,
AdminUser: cfg.AdminUser,
AdminPass: cfg.AdminPass,
Admins: administrators,
PublicReadable: cfg.PublicReadable,
}
var opts []options.O

View File

@@ -5,16 +5,18 @@ import (
k1 "realy.lol/ec/secp256k1"
"realy.lol/p256k"
"realy.lol/signer"
"realy.lol/timestamp"
)
// Sign the event using the signer.I. Uses github.com/bitcoin-core/secp256k1 if available for much faster
// signatures.
// Sign the event using the signer.I. Uses github.com/bitcoin-core/secp256k1 if
// available for much faster signatures.
func (ev *T) Sign(keys signer.I) (err er) {
ev.PubKey = keys.Pub()
ev.CreatedAt = timestamp.Now()
ev.ID = ev.GetIDBytes()
if ev.Sig, err = keys.Sign(ev.ID); chk.E(err) {
return
}
ev.PubKey = keys.Pub()
return
}

53
httpauth/httpauth.go Normal file
View File

@@ -0,0 +1,53 @@
package httpauth
import (
"realy.lol/event"
"realy.lol/tags"
"realy.lol/tag"
"realy.lol/kind"
"strings"
"net/http"
"realy.lol/signer"
"net/url"
"encoding/base64"
"io"
)
func MakeEvent(u, method st) (ev *event.T) {
ev = &event.T{
Kind: kind.HTTPAuth,
Tags: tags.New(tag.New("u", u), tag.New("method", strings.ToUpper(method))),
}
return
}
func MakeRequest(ur, meth st,
sign signer.I, payload io.ReadCloser) (r *http.Request, err er) {
if _, err = url.Parse(ur); chk.E(err) {
return
}
method := strings.ToUpper(meth)
ev := MakeEvent(ur, method)
if err = ev.Sign(sign); chk.E(err) {
return
}
log.I.F("%s", ev.Serialize())
b64 := base64.RawURLEncoding.EncodeToString(ev.Serialize())
log.I.F("%s", b64)
var req *http.Request
if req, err = http.NewRequest(method, ur, nil); chk.E(err) {
return
}
req.Header.Add("Authorization", "Nostr "+b64)
switch method {
case "POST":
// add the reader for the data
req.Body = payload
case "GET":
default:
err = errorf.E("unsupported http method: %s", method)
return
}
return
}

22
httpauth/util.go Normal file
View File

@@ -0,0 +1,22 @@
package httpauth
import (
"bytes"
"realy.lol/context"
"realy.lol/lol"
)
type (
bo = bool
by = []byte
st = string
er = error
no = int
cx = context.T
)
var (
log, chk, errorf = lol.Main.Log, lol.Main.Check, lol.Main.Errorf
equals = bytes.Equal
)

View File

@@ -26,8 +26,7 @@ type C struct {
DataDir st `env:"REALY_DATA_DIR" usage:"storage location for the ratel event store"`
Listen st `env:"REALY_LISTEN" default:"0.0.0.0" usage:"network listen address"`
Port no `env:"REALY_PORT" default:"3334" usage:"port to listen on"`
AdminUser st `env:"REALY_ADMIN_USER" default:"admin" usage:"admin user"`
AdminPass st `env:"REALY_ADMIN_PASS" usage:"admin password"`
AdminNpubs st `env:"REALY_ADMIN_NPUBS" usage:"comma separated lists of bech32 format pubkeys of authorised administrators for the http admin endpoints"`
LogLevel st `env:"REALY_LOG_LEVEL" default:"info" usage:"debug level: fatal error warn info debug trace"`
DbLogLevel st `env:"REALY_DB_LOG_LEVEL" default:"info" usage:"debug level: fatal error warn info debug trace"`
AuthRequired bo `env:"REALY_AUTH_REQUIRED" default:"false" usage:"requires auth for all access"`

View File

@@ -1,7 +1,6 @@
package realy
import (
"crypto/subtle"
"fmt"
"io"
"net/http"
@@ -10,38 +9,42 @@ import (
"realy.lol/cmd/realy/app"
"realy.lol/context"
"realy.lol/hex"
"realy.lol/sha256"
)
// func (s *Server) auth(r *http.Request) (authed bo) {
// if s.adminUser == "" || s.adminPass == "" {
// // disallow this if it hasn't been configured, the default values are empty.
// return
// }
// username, password, ok := r.BasicAuth()
// if ok {
// usernameHash := sha256.Sum256(by(username))
// passwordHash := sha256.Sum256(by(password))
// expectedUsernameHash := sha256.Sum256(by(s.adminUser))
// expectedPasswordHash := sha256.Sum256(by(s.adminPass))
// usernameMatch := subtle.ConstantTimeCompare(usernameHash[:],
// expectedUsernameHash[:]) == 1
// passwordMatch := subtle.ConstantTimeCompare(passwordHash[:],
// expectedPasswordHash[:]) == 1
// if usernameMatch && passwordMatch {
// return true
// }
// }
// return
// }
func (s *Server) auth(r *http.Request) (authed bo) {
if s.adminUser == "" || s.adminPass == "" {
// disallow this if it hasn't been configured, the default values are empty.
return
}
username, password, ok := r.BasicAuth()
if ok {
usernameHash := sha256.Sum256(by(username))
passwordHash := sha256.Sum256(by(password))
expectedUsernameHash := sha256.Sum256(by(s.adminUser))
expectedPasswordHash := sha256.Sum256(by(s.adminPass))
usernameMatch := subtle.ConstantTimeCompare(usernameHash[:],
expectedUsernameHash[:]) == 1
passwordMatch := subtle.ConstantTimeCompare(passwordHash[:],
expectedPasswordHash[:]) == 1
if usernameMatch && passwordMatch {
return true
}
}
return
}
func (s *Server) unauthorized(w http.ResponseWriter) {
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
fmt.Fprintf(w, "you may have not configured your admin username/password")
fmt.Fprintf(w, "your npub is not welcome here")
}
func (s *Server) handleAdmin(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleHTTP(w http.ResponseWriter, r *http.Request) {
switch {
case strings.HasPrefix(r.URL.Path, "/export"):
if ok := s.auth(r); !ok {

View File

@@ -18,33 +18,34 @@ import (
"realy.lol/realy/listeners"
"realy.lol/realy/options"
"realy.lol/relay"
"realy.lol/signer"
)
type Server struct {
Ctx cx
Cancel context.F
options *options.T
relay relay.I
clientsMu sync.Mutex
clients map[*websocket.Conn]struct{}
Addr st
serveMux *http.ServeMux
httpServer *http.Server
authRequired bo
publicReadable bo
maxLimit no
adminUser, adminPass st
listeners *listeners.T
Ctx cx
Cancel context.F
options *options.T
relay relay.I
clientsMu sync.Mutex
clients map[*websocket.Conn]struct{}
Addr st
serveMux *http.ServeMux
httpServer *http.Server
authRequired bo
publicReadable bo
maxLimit no
admins []signer.I
listeners *listeners.T
}
type ServerParams struct {
Ctx cx
Cancel context.F
Rl relay.I
DbPath st
MaxLimit no
AdminUser, AdminPass st
PublicReadable bo
Ctx cx
Cancel context.F
Rl relay.I
DbPath st
MaxLimit no
Admins []signer.I
PublicReadable bo
}
func NewServer(sp *ServerParams, opts ...options.O) (*Server, er) {
@@ -66,8 +67,7 @@ func NewServer(sp *ServerParams, opts ...options.O) (*Server, er) {
authRequired: authRequired,
publicReadable: sp.PublicReadable,
maxLimit: sp.MaxLimit,
adminUser: sp.AdminUser,
adminPass: sp.AdminPass,
admins: sp.Admins,
listeners: listeners.New(),
}
if storage := sp.Rl.Storage(); storage != nil {
@@ -94,7 +94,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} else if r.Header.Get("Accept") == "application/nostr+json" {
s.handleRelayInfo(w, r)
} else {
s.handleAdmin(w, r)
s.handleHTTP(w, r)
}
}

View File

@@ -7,25 +7,23 @@ type I interface {
// InitSec initialises the secret (signing) key from the raw bytes, and also
// derives the public key because it can.
InitSec(sec by) (err er)
// InitPub initializes the public (verification) key from raw bytes.
// InitPub initializes the public (verification) key from raw bytes, this is
// expected to be an x-only 32 byte pubkey.
InitPub(pub by) (err er)
// Sec returns the secret key bytes.
Sec() by
// Pub returns the public key bytes (x-only schnorr pubkey).
Pub() by
// ECPub returns the public key bytes (33 byte ecdsa pubkey). The first byte is always 2 due
// to ECDH and X-only keys.
ECPub() by
// Sign creates a signature using the stored secret key.
Sign(msg by) (sig by, err er)
// Verify checks a message hash and signature match the stored public key.
Verify(msg, sig by) (valid bo, err er)
// Zero wipes the secret key to prevent memory leaks.
Zero()
// ECDH returns a shared secret derived using Elliptic Curve Diffie Hellman on the I
// secret and provided pubkey.
// ECDH returns a shared secret derived using Elliptic Curve Diffie-Hellman on
// the I secret and provided pubkey.
ECDH(pub by) (secret by, err er)
// Negate flips the the secret key to change between odd and even compressed public key.
// Negate flips the secret key to change between odd and even compressed public key.
Negate()
}