fixing signer and partly implemented nip-98
This commit is contained in:
@@ -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
5
cmd/meow/main.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package main
|
||||
|
||||
func main() {
|
||||
|
||||
}
|
||||
64
cmd/nurl/main.go
Normal file
64
cmd/nurl/main.go
Normal 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
22
cmd/nurl/util.go
Normal 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
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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
53
httpauth/httpauth.go
Normal 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
22
httpauth/util.go
Normal 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
|
||||
)
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user