Initial commit of auth handshake
parent
05a6a013bc
commit
3f1485e493
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue