implement auth, closed and close envelopes
This commit is contained in:
126
pkg/protocol/auth/nip42.go
Normal file
126
pkg/protocol/auth/nip42.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"lol.mleku.dev/chk"
|
||||
"lol.mleku.dev/errorf"
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
"next.orly.dev/pkg/encoders/kind"
|
||||
"next.orly.dev/pkg/encoders/tag"
|
||||
"next.orly.dev/pkg/utils"
|
||||
)
|
||||
|
||||
// GenerateChallenge creates a reasonable, 16-byte base64 challenge string
|
||||
func GenerateChallenge() (b []byte) {
|
||||
bb := make([]byte, 12)
|
||||
b = make([]byte, 16)
|
||||
_, _ = rand.Read(bb)
|
||||
base64.URLEncoding.Encode(b, bb)
|
||||
return
|
||||
}
|
||||
|
||||
// CreateUnsigned creates an event which should be sent via an "AUTH" command.
|
||||
// If the authentication succeeds, the user will be authenticated as a pubkey.
|
||||
func CreateUnsigned(pubkey, challenge []byte, relayURL string) (ev *event.E) {
|
||||
return &event.E{
|
||||
Pubkey: pubkey,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
Kind: kind.ClientAuthentication.K,
|
||||
Tags: tag.NewS(
|
||||
tag.New("relay", relayURL),
|
||||
tag.New("challenge", string(challenge)),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// helper function for ValidateAuthEvent.
|
||||
func parseURL(input string) (*url.URL, error) {
|
||||
return url.Parse(
|
||||
strings.ToLower(
|
||||
strings.TrimSuffix(input, "/"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
var (
|
||||
// ChallengeTag is the tag for the challenge in a NIP-42 auth event
|
||||
// (prevents relay attacks).
|
||||
ChallengeTag = []byte("challenge")
|
||||
// RelayTag is the relay tag for a NIP-42 auth event (prevents cross-server
|
||||
// attacks).
|
||||
RelayTag = []byte("relay")
|
||||
)
|
||||
|
||||
// Validate checks whether an event is a valid NIP-42 event for a given
|
||||
// challenge and relayURL. The result of the validation is encoded in the ok
|
||||
// bool.
|
||||
func Validate(evt *event.E, challenge []byte, relayURL string) (
|
||||
ok bool, err error,
|
||||
) {
|
||||
if evt.Kind != kind.ClientAuthentication.K {
|
||||
err = errorf.E(
|
||||
"event incorrect kind for auth: %d %s",
|
||||
evt.Kind, kind.GetString(evt.Kind),
|
||||
)
|
||||
return
|
||||
}
|
||||
if evt.Tags.GetFirst(ChallengeTag) == nil {
|
||||
err = errorf.E("challenge tag missing from auth response")
|
||||
return
|
||||
}
|
||||
if !utils.FastEqual(challenge, evt.Tags.GetFirst(ChallengeTag).Value()) {
|
||||
err = errorf.E("challenge tag incorrect from auth response")
|
||||
return
|
||||
}
|
||||
var expected, found *url.URL
|
||||
if expected, err = parseURL(relayURL); chk.D(err) {
|
||||
return
|
||||
}
|
||||
r := evt.Tags.
|
||||
GetFirst(RelayTag).Value()
|
||||
if len(r) == 0 {
|
||||
err = errorf.E("relay tag missing from auth response")
|
||||
return
|
||||
}
|
||||
if found, err = parseURL(string(r)); chk.D(err) {
|
||||
err = errorf.E("error parsing relay url: %s", err)
|
||||
return
|
||||
}
|
||||
if expected.Scheme != found.Scheme {
|
||||
err = errorf.E(
|
||||
"HTTP Scheme incorrect: expected '%s' got '%s",
|
||||
expected.Scheme, found.Scheme,
|
||||
)
|
||||
return
|
||||
}
|
||||
if expected.Host != found.Host {
|
||||
err = errorf.E(
|
||||
"HTTP Host incorrect: expected '%s' got '%s",
|
||||
expected.Host, found.Host,
|
||||
)
|
||||
return
|
||||
}
|
||||
if expected.Path != found.Path {
|
||||
err = errorf.E(
|
||||
"HTTP Path incorrect: expected '%s' got '%s",
|
||||
expected.Path, found.Path,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
ca := evt.CreatedAt
|
||||
if ca > now+10*60 || ca < now-10*60 {
|
||||
err = errorf.E(
|
||||
"auth event more than 10 minutes before or after current time",
|
||||
)
|
||||
return
|
||||
}
|
||||
// save for last, as it is the most expensive operation
|
||||
return evt.Verify()
|
||||
}
|
||||
34
pkg/protocol/auth/nip42_test.go
Normal file
34
pkg/protocol/auth/nip42_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"lol.mleku.dev/chk"
|
||||
"lol.mleku.dev/log"
|
||||
"next.orly.dev/pkg/crypto/p256k"
|
||||
)
|
||||
|
||||
func TestCreateUnsigned(t *testing.T) {
|
||||
var err error
|
||||
signer := new(p256k.Signer)
|
||||
if err = signer.Generate(); chk.E(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var ok bool
|
||||
const relayURL = "wss://example.com"
|
||||
for range 100 {
|
||||
challenge := GenerateChallenge()
|
||||
ev := CreateUnsigned(signer.Pub(), challenge, relayURL)
|
||||
if err = ev.Sign(signer); chk.E(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
log.I.S(ev)
|
||||
if ok, err = Validate(ev, challenge, relayURL); chk.E(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !ok {
|
||||
bb := ev.Marshal(nil)
|
||||
t.Fatalf("failed to validate auth event\n%s", bb)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user