From 3f1485e493a5654b6cb92fb01395a1979a12b26f Mon Sep 17 00:00:00 2001 From: Noah Metz Date: Wed, 3 Apr 2024 18:52:04 -0600 Subject: [PATCH] Initial commit of auth handshake --- client.go | 34 ++++++++++ cmd/client/main.go | 29 +++++++++ cmd/server/main.go | 28 ++++++++ go.mod | 8 +++ go.sum | 4 ++ packet.go | 78 +++++++++++++++++++++++ packet_test.go | 51 +++++++++++++++ server.go | 156 +++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 388 insertions(+) create mode 100644 client.go create mode 100644 cmd/client/main.go create mode 100644 cmd/server/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 packet.go create mode 100644 packet_test.go create mode 100644 server.go diff --git a/client.go b/client.go new file mode 100644 index 0000000..1a53223 --- /dev/null +++ b/client.go @@ -0,0 +1,34 @@ +package pnyx + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/sha512" +) + +type Client struct { + Key ed25519.PrivateKey +} + +func KeyID(key ed25519.PublicKey) ClientID { + hash := sha512.Sum512([]byte(key)) + return (ClientID)((hash)[0:16]) +} + +func(client Client) ID() ClientID { + return KeyID(client.Key.Public().(ed25519.PublicKey)) +} + +func NewClient(key ed25519.PrivateKey) (Client, error) { + if key == nil { + var err error + _, key, err = ed25519.GenerateKey(rand.Reader) + if err != nil { + return Client{}, err + } + } + + return Client{ + Key: key, + }, nil +} diff --git a/cmd/client/main.go b/cmd/client/main.go new file mode 100644 index 0000000..b8f753a --- /dev/null +++ b/cmd/client/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "net" + "os" + "time" +) + +func main() { + address, err := net.ResolveUDPAddr("udp", os.Args[1]) + if err != nil { + panic(err) + } + + connection, err := net.DialUDP("udp", nil, address) + if err != nil { + panic(err) + } + + for true { + written, err := connection.Write([]byte(os.Args[2])) + if written != len(os.Args[2]) { + panic(written) + } else if err != nil { + panic(err) + } + time.Sleep(time.Second) + } +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..112254f --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "os" + "os/signal" + "syscall" + + "git.metznet.ca/MetzNet/pnyx" +) + + +func main() { + os_sigs := make(chan os.Signal, 1) + signal.Notify(os_sigs, syscall.SIGINT, syscall.SIGINT) + + server := pnyx.NewServer() + + err := server.Start(os.Args[1]) + if err != nil { + panic(err) + } + + <-os_sigs + err = server.Stop() + if err != nil { + panic(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fdf2b6a --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module git.metznet.ca/MetzNet/pnyx + +go 1.21.5 + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4aae644 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/packet.go b/packet.go new file mode 100644 index 0000000..5081dc6 --- /dev/null +++ b/packet.go @@ -0,0 +1,78 @@ +package pnyx + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/sha512" + "fmt" + + "filippo.io/edwards25519" +) + +func NewSessionOpen(key ed25519.PrivateKey) ([]byte, ed25519.PrivateKey, error) { + if key == nil { + return nil, nil, fmt.Errorf("Cannot create a SESSION_OPEN packet without a key") + } + + public, private, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, fmt.Errorf("Failed to generate ecdh key: %w", err) + } + + packet := make([]byte, SESSION_OPEN_LENGTH) + cur := 0 + + copy(packet[cur:], []byte(key.Public().(ed25519.PublicKey))) + cur += PUBKEY_LENGTH + + copy(packet[cur:], []byte(public)) + cur += PUBKEY_LENGTH + + signature := ed25519.Sign(key, packet[:cur]) + copy(packet[cur:], signature) + cur += SIGNATURE_LENGTH + + return packet, private, nil +} + +func ParseSessionOpen(session_open []byte) (ed25519.PublicKey, ed25519.PublicKey, error) { + if len(session_open) != SESSION_OPEN_LENGTH { + return nil, nil, fmt.Errorf("Bad SESSION_OPEN length: %d/%d", len(session_open), SESSION_OPEN_LENGTH) + } + + cur := 0 + + client_pubkey := (ed25519.PublicKey)(session_open[cur:cur+PUBKEY_LENGTH]) + cur += PUBKEY_LENGTH + + client_ecdh := (ed25519.PublicKey)(session_open[cur:cur+PUBKEY_LENGTH]) + cur += PUBKEY_LENGTH + + digest := session_open[:cur] + signature := session_open[cur:cur+SIGNATURE_LENGTH] + cur += SIGNATURE_LENGTH + + if ed25519.Verify(client_pubkey, digest, signature) == false { + return nil, nil, fmt.Errorf("SESSION_OPEN signature verification failed") + } + + return client_pubkey, client_ecdh, nil +} + +func ECDH(public ed25519.PublicKey, private ed25519.PrivateKey) ([]byte, error) { + public_point, err := (&edwards25519.Point{}).SetBytes(public) + if err != nil { + return nil, err + } + + + h := sha512.Sum512(private.Seed()) + private_scalar, err := (&edwards25519.Scalar{}).SetBytesWithClamping(h[:32]) + if err != nil { + return nil, err + } + + shared_point := public_point.ScalarMult(private_scalar, public_point) + + return shared_point.BytesMontgomery(), nil +} diff --git a/packet_test.go b/packet_test.go new file mode 100644 index 0000000..1fdb6d1 --- /dev/null +++ b/packet_test.go @@ -0,0 +1,51 @@ +package pnyx + +import ( + "crypto/ed25519" + "crypto/rand" + "slices" + "testing" +) + +func fatalErr(t *testing.T, err error) { + if err != nil { + t.Fatal(err) + } +} + +func TestSessionOpen(t *testing.T) { + client_pubkey, client_key, err := ed25519.GenerateKey(rand.Reader) + fatalErr(t, err) + + session_open, client_ecdh, err := NewSessionOpen(client_key) + fatalErr(t, err) + + client_pubkey_parsed, client_ecdh_parsed, err := ParseSessionOpen(session_open) + fatalErr(t, err) + + if slices.Compare(client_pubkey, client_pubkey_parsed) != 0 { + t.Fatalf("Client Pubkey %x does not match parsed %x", client_pubkey, client_pubkey_parsed) + } + + if slices.Compare(client_ecdh.Public().(ed25519.PublicKey), client_ecdh_parsed) != 0 { + t.Fatalf("Client Pubkey %x does not match parsed %x", client_pubkey, client_pubkey_parsed) + } +} + +func TestECDH(t *testing.T) { + client_public, client_private, err := ed25519.GenerateKey(rand.Reader) + fatalErr(t, err) + + server_public, server_private, err := ed25519.GenerateKey(rand.Reader) + fatalErr(t, err) + + server_secret, err := ECDH(client_public, server_private) + fatalErr(t, err) + + client_secret, err := ECDH(server_public, client_private) + fatalErr(t, err) + + if slices.Compare(server_secret, client_secret) != 0 { + t.Fatalf("Server and Client secrets do not match") + } +} diff --git a/server.go b/server.go new file mode 100644 index 0000000..81c7d22 --- /dev/null +++ b/server.go @@ -0,0 +1,156 @@ +package pnyx + +import ( + "encoding/binary" + "errors" + "fmt" + "net" + "os" + "sync/atomic" + + "github.com/google/uuid" +) + +const ( + SERVER_UDP_BUFFER_SIZE = 2048 + + PUBKEY_LENGTH = 32 + ECDH_LENGTH = 32 + SIGNATURE_LENGTH = 64 + SESSION_OPEN_LENGTH = PUBKEY_LENGTH + ECDH_LENGTH + SIGNATURE_LENGTH +) + +type PacketType uint16 +const ( + SESSION_OPEN PacketType = iota + SESSION_AUTHENTICATE + SESSION_CONNECT + SESSION_CLOSE + SESSION_CLOSED + SESSION_DATA +) + +/* +Session Flow: +1. Client send a SESSION_OPEN packet to the server with first half for ECDH and it's public key +2. Server responds with an SESSION_AUTHENTICATE packet with the server's ECDH half and public key +aside - at this point if the client and server both hold the private parts of their public keys, + they both hold the same ECDH secret which they put in a KDF to generate a symmetric session key +aside - at this point the server creates the session in memory, but there is no return address associated + with it yet +3. Client sends a SESSION_CONNECT packet to the server with the session ID in cleartext and the + return address hashed with the key(to prove the return address has not been modified without the key) +4. Server adds the return address to the session info, and maps the address to the session for future packets +5. Server sends the HELLO packet to the client encrypted by the session key + +If a client disconnects at any point and gets a new return address: +1. Client sends a SESSION_CONNECT packet to the server from the new socket +2. Server removes the old return address, and fills/maps the new return address + +If a client wants to gracefully disconnect and notify the server to close the session: +1. Client sends a SESSION_CLOSE +2. Server responds with SESSION_CLOSED + +Session Packets: +1. SESSION_OPEN + Payload is CLIENT_PUBKEY + ECDH_HALF + SIGNATURE +3. SESSION_CONNECT +4. SESSION_CLOSE +5. SESSION_CLOSED +*/ + +type SessionID uuid.UUID +type ClientID uuid.UUID + +type Connection struct { + state string + session SessionID +} + +type Session struct { + state string + client ClientID +} + +type Server struct { + active atomic.Bool + connection *net.UDPConn + stopped chan error + + connections map[string]SessionID + sessions map[SessionID]Session +} + +func NewServer() *Server { + server := &Server{ + connection: nil, + active: atomic.Bool{}, + stopped: make(chan error, 0), + } + server.active.Store(false) + return server +} + +func (server *Server) Log(format string, fields ...interface{}) { + fmt.Fprint(os.Stderr, fmt.Sprintf(format, fields...) + "\n") +} + +func(server *Server) Stop() error { + was_active := server.active.CompareAndSwap(true, false) + if was_active { + err := server.connection.Close() + if err != nil { + return err + } + return <-server.stopped + } else { + return fmt.Errorf("Called stop func on stopped server") + } +} + +func(server *Server) run() { + server.Log("Started server on %s", server.connection.LocalAddr()) + + var buf [SERVER_UDP_BUFFER_SIZE]byte + for true { + read, addr, err := server.connection.ReadFromUDP(buf[:]) + if err == nil { + var packet_type PacketType = PacketType(binary.BigEndian.Uint16(buf[0:2])) + switch packet_type { + default: + server.Log("Unhandled packet type 0x%04x from %s: %+v", packet_type, addr, buf[:read]) + } + } else if errors.Is(err, net.ErrClosed) { + server.Log("UDP_CLOSE: %s", server.connection.LocalAddr()) + break + } else { + server.Log("UDP_READ_ERROR: %s", err) + } + } + + server.Log("Shut down server on %s", server.connection.LocalAddr()) + server.stopped <- nil +} + +func(server *Server) Start(listen string) error { + was_inactive := server.active.CompareAndSwap(false, true) + if was_inactive == false { + return fmt.Errorf("Server already active") + } + + address, err := net.ResolveUDPAddr("udp", listen) + if err != nil { + server.active.Store(false) + return err + } + + server.connection, err = net.ListenUDP("udp", address) + if err != nil { + server.active.Store(false) + return fmt.Errorf("Failed to create udp server: %w", err) + } + + go server.run() + + return nil +}