Initial commit of auth handshake

live
noah metz 2024-04-03 18:52:04 -06:00
parent 05a6a013bc
commit 3f1485e493
8 changed files with 388 additions and 0 deletions

@ -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
}

@ -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)
}
}

@ -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)
}
}

@ -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
)

@ -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=

@ -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
}

@ -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")
}
}

@ -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
}