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