157 lines
4.5 KiB
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)
|
|
}
|
|
}
|
|
}
|