Files
realy/cmd/nostrjwt/main.go

157 lines
4.5 KiB
Go

// Package main is a tool for generating new JWT key pairs and a kind 13004 JWT
// delegation event that allows authentication against a pubkey while using
// non-nostr-native tools such as cURL and Postman and minimalistic HTTP browser
// implementations as found in some e-book readers.
package main
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/base64"
"fmt"
"os"
realy_lol "realy.lol"
"realy.lol/bech32encoding"
"realy.lol/event"
"realy.lol/hex"
"realy.lol/httpauth"
"realy.lol/kind"
"realy.lol/lol"
"realy.lol/p256k"
"realy.lol/tag"
"realy.lol/tags"
"realy.lol/timestamp"
)
const (
jwtIssuerEnv = "NOSTR_PUBLIC_KEY"
secEnv = "NOSTR_SECRET_KEY"
jwtSecEnv = "NOSTR_JWT_SECRET"
)
var userAgent = fmt.Sprintf("nostrjwt/%s", realy_lol.Version)
func fail(format string, a ...any) {
_, _ = fmt.Fprintf(os.Stderr, format+"\n", a...)
os.Exit(1)
}
func main() {
lol.SetLogLevel("trace")
// log.I.S(os.Args)
if len(os.Args) < 2 || os.Args[1] == "help" {
fmt.Printf(`nostrjwt usage:
nostrjwt gen
generate a JWT secret and a nostr event for kind %d that creates a
binding between the JWT pubkey and the nostr npub for authentication,
as an alternative to nip-98 on devices that cannot do BIP-340 signatures
the secret key data should be stored in %s environment
variable for later use
the pubkey in the nostr event is required for generating a token
nostrjwt bearer <request URL> [<optional expiry in 0h0m0s format for JWT token>]
request URL must match the one that will be in the HTTP Request this bearer
token must refer to
nostr pubkey must be registered with the relay as associated with the JWT
secret signing the token, it should be stored in %s
environment variable
using the JWT secret, found in the %s environment variable,
generate a signed JWT header in standard format as used by curl to add
to make GET and POST requests to a nostr HTTP JWT savvy relay to read or
publish events
expiry sets an amount of time after the current moment that the token
will expire
`, kind.JWTBinding.K, jwtSecEnv, jwtIssuerEnv, jwtSecEnv)
os.Exit(0)
}
var err error
if len(os.Args) >= 2 {
switch os.Args[1] {
case "gen":
// check environment for secret key
var skb []byte
nsex := os.Getenv(secEnv)
if len(nsex) == 0 {
fail("no key found in environment variable %s", secEnv)
}
if skb, err = bech32encoding.NsecToBytes([]byte(nsex)); chk.E(err) {
fail("failed to decode nsec: '%s'", err.Error())
}
sign := &p256k.Signer{}
if err = sign.InitSec(skb); chk.E(err) {
fail("failed to init signer: '%s'", err.Error())
}
pub := hex.Enc(sign.Pub())
// generate a new JWT key pair
var x509sec, x509pub, pemSec, pemPub []byte
if x509sec, x509pub, pemSec, pemPub, _, _, err = httpauth.GenerateJWTKeys(); chk.E(err) {
fail(err.Error())
}
fmt.Printf("%s\n%s\n", pemSec, pemPub)
fmt.Printf("export %s=%s\n", jwtSecEnv, x509sec)
fmt.Printf("export %s=%s\n\n", jwtIssuerEnv, pub)
var ev event.T
httpauth.MakeJWTEvent(string(x509pub))
ev.Tags = tags.New(tag.New([]byte("J"), x509pub, []byte("ES256")))
ev.CreatedAt = timestamp.Now()
ev.Kind = kind.JWTBinding
if err = ev.Sign(sign); chk.E(err) {
fail(err.Error())
}
fmt.Printf("%s\n", ev.Serialize())
case "bearer":
// check args
if len(os.Args) < 4 {
fail("missing required positional arguments, got '%s' require 'bearer <request URL> <nostr pubkey>'",
os.Args[1:])
}
// jwt secret key must be found in NOSTR_JWT_SECRET
var jskb []byte
jwtSec := os.Getenv(jwtSecEnv)
if len(jwtSec) == 0 {
fail("no key found in environment variable %s", jwtSecEnv)
}
jwtIss := os.Getenv(jwtIssuerEnv)
if len(jwtIss) == 0 {
fail("no pubkey found in environment variable %s", jwtIssuerEnv)
}
if jskb, err = base64.URLEncoding.DecodeString(jwtSec); chk.E(err) {
fail(err.Error())
}
var sec *ecdsa.PrivateKey
if sec, err = x509.ParseECPrivateKey(jskb); chk.E(err) {
fail(err.Error())
}
var tok []byte
// log.I.S(os.Args)
// generate claim
if len(os.Args) == 3 {
if tok, err = httpauth.GenerateJWTClaims(os.Args[2], jwtIss); chk.E(err) {
fail(err.Error())
}
} else if len(os.Args) == 4 {
if tok, err = httpauth.GenerateJWTClaims(os.Args[2], jwtIss, os.Args[3]); chk.E(err) {
fail(err.Error())
}
}
var signed string
if signed, err = httpauth.SignJWTtoken(tok, sec); chk.E(err) {
fail(err.Error())
}
fmt.Printf("Bearer %s\n", signed)
}
}
}