Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions host.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ type attempt struct {
connectionID uint64
// outcome is set once a game outcome has been reported.
outcome atomic.Bool
// key is the admission key of the login until its identity is verified.
key atomic.Pointer[admission.Key]
}

func newHost(addr string, maxSessions int, log *slog.Logger) (*host, error) {
Expand Down Expand Up @@ -292,6 +294,7 @@ func (h *host) establish(a *attempt, adm nethernet.Admission, password string) {
h.changed()

<-conn.Context().Done()
a.key.Store(nil)
h.mu.Lock()
delete(h.sessions, a.connectionID)
delete(h.tuples, a.tuple)
Expand Down Expand Up @@ -358,14 +361,19 @@ func (h *host) gameOutcome(addr net.Addr, stage, reason string) {
h.mu.Lock()
a, ok := h.conns[na.ConnectionID]
h.mu.Unlock()
if ok && !a.outcome.Swap(true) {
if !ok {
return
}
a.key.Store(nil)
if !a.outcome.Swap(true) {
h.emit(a, stage, reason, nil)
}
}

func (h *host) fail(a *attempt, reason string, err error) {
h.log.Debug("admission failed", "reason", reason, "addr", a.tuple, "error", err)
h.emit(a, "ticket.failed", reason, nil)
a.key.Store(nil)
h.mu.Lock()
h.pending--
delete(h.tuples, a.tuple)
Expand All @@ -382,16 +390,22 @@ func (h *host) emit(a *attempt, stage, reason string, remote *netip.AddrPort) {

// verifier returns a one-shot check of the login identity key against the admission.
func (h *host) verifier(a *attempt, key admission.Key, claims admission.Claims) func(*ecdsa.PublicKey) error {
deadline := time.Now().Add(time.Until(claims.ExpiresAt))
ttl := time.Until(claims.ExpiresAt)
deadline := time.Now().Add(ttl)
a.key.Store(&key)
time.AfterFunc(ttl, func() { a.key.Store(nil) })
var used atomic.Bool
return func(pub *ecdsa.PublicKey) error {
if used.Swap(true) {
return errors.New("nxs: admission identity already verified")
}
k := a.key.Swap(nil)
var err error
if time.Now().After(deadline) {
if k == nil {
err = errors.New("nxs: admission no longer pending")
} else if time.Now().After(deadline) {
err = errors.New("nxs: admission expired before login")
} else if !key.VerifyBinding(h.audience, pub, claims.IdentityBinding) {
} else if !k.VerifyBinding(h.audience, pub, claims.IdentityBinding) {
err = errors.New("nxs: login identity does not match admission")
}
if err != nil && h.gameOutcomes && !a.outcome.Swap(true) {
Expand Down
61 changes: 61 additions & 0 deletions host_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package nxs

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"testing"
"time"

"github.com/df-mc/go-nethernet"
"github.com/df-mc/go-nxs/admission"
)

func TestVerifierReleasesKey(t *testing.T) {
h := &host{log: testLog(), report: func(event) {}, conns: make(map[uint64]*attempt), gameOutcomes: true,
audience: admission.Audience("00000000000000000000000000000000")}
key := admission.Key{ID: "K001", Secret: "fake-provider-secret-at-least-32-bytes"}
identity, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
binding, _ := key.Binding(h.audience, &identity.PublicKey)
claims := admission.Claims{ExpiresAt: time.Now().Add(time.Minute), IdentityBinding: binding}

a := &attempt{}
verify := h.verifier(a, key, claims)
if err := verify(&identity.PublicKey); err != nil {
t.Fatalf("verify: %v", err)
}
if a.key.Load() != nil {
t.Fatal("a verified login should release its key")
}
if verify(&identity.PublicKey) == nil {
t.Fatal("verify should be one-shot")
}

a = &attempt{}
other, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if h.verifier(a, key, claims)(&other.PublicKey) == nil || a.key.Load() != nil {
t.Fatal("a mismatched login should be rejected and release its key")
}

a = &attempt{}
verify = h.verifier(a, key, admission.Claims{ExpiresAt: time.Now().Add(100 * time.Millisecond), IdentityBinding: binding})
waitFor(t, func() bool { return a.key.Load() == nil })
if verify(&identity.PublicKey) == nil {
t.Fatal("an expired login should be rejected")
}

a = &attempt{}
verify = h.verifier(a, key, claims)
h.fail(a, "closed", nil)
if a.key.Load() != nil || verify(&identity.PublicKey) == nil {
t.Fatal("a failed admission should release its key")
}

a = &attempt{connectionID: 1}
verify = h.verifier(a, key, claims)
h.conns[1] = a
h.gameOutcome(&nethernet.Addr{ConnectionID: 1}, "ticket.game_rejected", "banned")
if a.key.Load() != nil || verify(&identity.PublicKey) == nil {
t.Fatal("a rejected login should release its key")
}
}
Loading