diff --git a/.gitignore b/.gitignore index 8c3970a..7a187b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ Justfile +/test diff --git a/README.md b/README.md index f09ea27..960a844 100644 --- a/README.md +++ b/README.md @@ -5,5 +5,9 @@ A hub-and-spoke VPN for IPv6 ## Features * **Post-quantum security:** Uses [XWingMLKEM768X25519](https://datatracker.ietf.org/doc/html/draft-connolly-cfrg-xwing-kem/) + [XChaCha20-Poly1305](https://en.wikipedia.org/wiki/ChaCha20-Poly1305#XChaCha20-Poly1305_%E2%80%93_extended_nonce_variant) + [HKDF-SHA-256](https://en.wikipedia.org/wiki/HKDF) for end-to-end encryption +## Protocol + +See [proto.go](https://git.ton1.dev/toni/baalvpn/src/branch/main/shared/proto.go) + ## Third-party dependencies * [`github.com/songgao/water`](https://github.com/songgao/water) - cross-platform wrapper around TUN interfaces diff --git a/client/main.go b/client/main.go index 68d0e9d..e54ec9a 100644 --- a/client/main.go +++ b/client/main.go @@ -1,7 +1,9 @@ package main import ( + "crypto/hmac" "crypto/hpke" + "crypto/sha256" "encoding/binary" "encoding/hex" "log" @@ -26,9 +28,11 @@ var ( iface *water.Interface conn *net.UDPConn serverAddr *net.UDPAddr - internalIP net.IP = nil + internalIP net.IP privKey hpke.PrivateKey multicastKey []byte + challengeCh = make(chan []byte, 1) + authSecret []byte establishMutex sync.Mutex peerPubKeys = shared.NewTMap[string, hpke.PublicKey]() @@ -70,8 +74,23 @@ func register() { } pubKey := privKey.PublicKey() - // TODO: this probably should be repeated until we get RESP_REGISTER - send(shared.BuildPkt(shared.REQ_REGISTER, pubKey.Bytes())) + // TODO: all this probably should be retried until we get RESP_REGISTER + + log.Println("requesting register challenge") + send(shared.BuildPkt(shared.REQ_GET_CHALLENGE, pubKey.Bytes())) + + challenge := <-challengeCh + log.Println("got register challenge") + + authSecret, err = hpke.Open(privKey, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte("baalvpn-challenge"), challenge) + if err != nil { + panic(err) + } + + log.Println("registering") + mac := hmac.New(sha256.New, authSecret) + mac.Write(pubKey.Bytes()) + send(shared.BuildPkt(shared.REQ_REGISTER, mac.Sum(nil), pubKey.Bytes())) } func receivePackets() { @@ -116,11 +135,13 @@ func handleIncomingPkt(pkt []byte) { pktType := binary.LittleEndian.Uint16(pkt[2:]) switch pktType { + case shared.RESP_GET_CHALLENGE: + challengeCh <- pkt[4:] case shared.RESP_REGISTER: internalIP = net.IP(pkt[4:20]) var err error - multicastKey, err = hpke.Open(privKey, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte("baalvpn"), pkt[20:]) + multicastKey, err = hpke.Open(privKey, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte("baalvpn-register"), pkt[20:]) if err != nil { panic(err) } @@ -129,11 +150,11 @@ func handleIncomingPkt(pkt []byte) { setupInterface() go relayPackets() case shared.ENC_PKT: - srcIP := net.IP(pkt[20:36]).String() + srcIP := net.IP(pkt[52:68]).String() sessionKey := getOrEstablishSessionKey(srcIP) - decryptedPkt, err := DecryptSym(sessionKey, pkt[36:]) + decryptedPkt, err := DecryptSym(sessionKey, pkt[68:]) if err != nil { panic(err) } @@ -143,7 +164,7 @@ func handleIncomingPkt(pkt []byte) { log.Println(err) } case shared.BROADCAST_PKT: - decryptedPkt, err := DecryptSym(multicastKey, pkt[4:]) + decryptedPkt, err := DecryptSym(multicastKey, pkt[36:]) if err != nil { panic(err) } @@ -159,14 +180,14 @@ func handleIncomingPkt(pkt []byte) { } peerPubKeys.Set(respIP, pubKey) case shared.REQ_ESTABLISH: - srcIP := net.IP(pkt[20:36]).String() - ciphertext := pkt[36:] + srcIP := net.IP(pkt[52:68]).String() + ciphertext := pkt[68:] r, err := hpke.NewRecipient(ciphertext, privKey, hpke.HKDFSHA256(), hpke.ExportOnly(), nil) if err != nil { panic(err) } - sessionKey, err := r.Export("baalvpn", 32) + sessionKey, err := r.Export("baalvpn-establish", 32) if err != nil { panic(err) } @@ -174,10 +195,9 @@ func handleIncomingPkt(pkt []byte) { log.Println("received session key from", srcIP) peerSessionKeys.Set(srcIP, sessionKey) - reqData := append(net.ParseIP(srcIP), internalIP...) - send(shared.BuildPkt(shared.RESP_ESTABLISH, reqData)) + send(buildAuthPkt(shared.RESP_ESTABLISH, net.ParseIP(srcIP), internalIP)) case shared.RESP_ESTABLISH: - srcIP := net.IP(pkt[20:36]).String() + srcIP := net.IP(pkt[52:68]).String() log.Println("established session key with", srcIP) peerEstablishAcks.Set(srcIP, true) default: @@ -215,13 +235,12 @@ func getOrEstablishSessionKey(ip string) []byte { if err != nil { panic(err) } - sessionKey, err := sender.Export("baalvpn", 32) + sessionKey, err := sender.Export("baalvpn-establish", 32) if err != nil { panic(err) } - reqData := append(net.ParseIP(ip), append(internalIP, ciphertext...)...) - send(shared.BuildPkt(shared.REQ_ESTABLISH, reqData)) + send(buildAuthPkt(shared.REQ_ESTABLISH, net.ParseIP(ip), internalIP, ciphertext)) for { // TODO: eww @@ -279,7 +298,7 @@ func relayPackets() { panic(err) } - send(shared.BuildPkt(shared.BROADCAST_PKT, encryptedPkt)) + send(buildAuthPkt(shared.BROADCAST_PKT, encryptedPkt)) } else { destSessionKey := getOrEstablishSessionKey(destIP) @@ -288,12 +307,18 @@ func relayPackets() { panic(err) } - reqBody := append(net.ParseIP(destIP), append(internalIP, encryptedPkt...)...) - send(shared.BuildPkt(shared.ENC_PKT, reqBody)) + send(buildAuthPkt(shared.ENC_PKT, net.ParseIP(destIP), internalIP, encryptedPkt)) } } } +func buildAuthPkt(pktType uint16, parts ...[]byte) []byte { + pkt := shared.BuildPkt(pktType, parts...) + mac := hmac.New(sha256.New, authSecret) + mac.Write(pkt[4:]) + return append(pkt[:4], append(mac.Sum(nil), pkt[4:]...)...) +} + func send(req []byte) { if _, err := conn.WriteToUDP(req, serverAddr); err != nil { panic(err) diff --git a/server/main.go b/server/main.go index 70a9055..9eee5fc 100644 --- a/server/main.go +++ b/server/main.go @@ -3,11 +3,14 @@ // TODO: somehow persist IPs // TODO: key rotation // TODO: dont trust claimed IP at all +// TODO: replay attacks are still a thing package main import ( + "crypto/hmac" "crypto/hpke" "crypto/rand" + "crypto/sha256" "encoding/binary" "encoding/hex" "log" @@ -21,14 +24,16 @@ type Peer struct { RealAddr *net.UDPAddr InternalIP string HexPublicKey string + AuthSecret []byte } var ( - peers = shared.NewTMap[string, *Peer]() - peersByInternal = shared.NewTMap[string, *Peer]() - peersByPubKey = shared.NewTMap[string, *Peer]() - conn *net.UDPConn - multicastKey = make([]byte, 32) + peers = shared.NewTMap[string, *Peer]() + peersByInternal = shared.NewTMap[string, *Peer]() + peersByPubKey = shared.NewTMap[string, *Peer]() + conn *net.UDPConn + multicastKey = make([]byte, 32) + authSecretsByPubKey = shared.NewTMap[string, []byte]() ) func main() { @@ -93,7 +98,7 @@ func handleReq(req []byte, addr *net.UDPAddr) { pktType := binary.LittleEndian.Uint16(req[2:]) switch pktType { - case shared.REQ_REGISTER: + case shared.REQ_GET_CHALLENGE: pubKey, err := hpke.MLKEM768X25519().NewPublicKey(req[4:]) if err != nil { log.Println("invalid pubkey") @@ -102,10 +107,48 @@ func handleReq(req []byte, addr *net.UDPAddr) { hexPubKey := hex.EncodeToString(pubKey.Bytes()) + authSecret := make([]byte, 32) + if _, err = rand.Read(authSecret); err != nil { + panic(err) + } + + authSecretsByPubKey.Set(hexPubKey, authSecret) + + ciphertext, err := hpke.Seal(pubKey, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte("baalvpn-challenge"), authSecret) + if err != nil { + panic(err) + } + + sendTo(addr, shared.BuildPkt(shared.RESP_GET_CHALLENGE, ciphertext)) + case shared.REQ_REGISTER: + auth := req[4:36] + + pubKey, err := hpke.MLKEM768X25519().NewPublicKey(req[36:]) + if err != nil { + log.Println("invalid pubkey") + return + } + + hexPubKey := hex.EncodeToString(pubKey.Bytes()) + + authSecret, ok := authSecretsByPubKey.GetOK(hexPubKey) + if !ok { + log.Println("unknown pubkey tried to REGISTER") + return + } + defer authSecretsByPubKey.Delete(hexPubKey) + + mac := hmac.New(sha256.New, authSecret) + mac.Write(pubKey.Bytes()) + if !hmac.Equal(mac.Sum(nil), auth) { + log.Println("failed to authenticate REGISTER") + return + } + var internalIP string - // TODO: authenticating only by PUBLIC key is obviously a bad idea if peer, ok := peersByPubKey.GetOK(hexPubKey); ok { internalIP = peer.InternalIP + peer.AuthSecret = authSecret peers.Delete(peer.RealAddr.String()) peer.RealAddr = addr peers.Set(addr.String(), peer) @@ -115,28 +158,35 @@ func handleReq(req []byte, addr *net.UDPAddr) { RealAddr: addr, InternalIP: internalIP, HexPublicKey: hexPubKey, + AuthSecret: authSecret, } peers.Set(addr.String(), peer) peersByInternal.Set(internalIP, peer) peersByPubKey.Set(hexPubKey, peer) } - ciphertext, err := hpke.Seal(pubKey, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte("baalvpn"), multicastKey) + ciphertext, err := hpke.Seal(pubKey, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte("baalvpn-register"), multicastKey) if err != nil { panic(err) } - resp := shared.BuildPkt(shared.RESP_REGISTER, append(net.ParseIP(internalIP), ciphertext...)) - sendTo(addr, resp) + sendTo(addr, shared.BuildPkt(shared.RESP_REGISTER, net.ParseIP(internalIP), ciphertext)) case shared.ENC_PKT: peer := peers.Get(addr.String()) if peer == nil { - log.Println("data from unregistered peer:", addr.String()) + log.Println("ENC_PKT from unregistered peer:", addr.String()) return } - destIP := net.IP(req[4:20]).String() - srcIP := net.IP(req[20:36]).String() + mac := hmac.New(sha256.New, peer.AuthSecret) + mac.Write(req[36:]) + if !hmac.Equal(mac.Sum(nil), req[4:36]) { + log.Println("failed to authenticate ENC_PKT") + return + } + + destIP := net.IP(req[36:52]).String() + srcIP := net.IP(req[52:68]).String() if srcIP != peer.InternalIP { log.Println("rejected spoofed srcIP in ENC_PKT") return @@ -151,7 +201,14 @@ func handleReq(req []byte, addr *net.UDPAddr) { case shared.BROADCAST_PKT: peer := peers.Get(addr.String()) if peer == nil { - log.Println("data from unregistered peer:", addr.String()) + log.Println("BROADCAST_PKT from unregistered peer:", addr.String()) + return + } + + mac := hmac.New(sha256.New, peer.AuthSecret) + mac.Write(req[36:]) + if !hmac.Equal(mac.Sum(nil), req[4:36]) { + log.Println("failed to authenticate BROADCAST_PKT") return } @@ -165,23 +222,29 @@ func handleReq(req []byte, addr *net.UDPAddr) { if err != nil { panic(err) } - resp := shared.BuildPkt(shared.RESP_GET_PUBKEY, append(net.ParseIP(peerIP), pubKey...)) - sendTo(addr, resp) + sendTo(addr, shared.BuildPkt(shared.RESP_GET_PUBKEY, net.ParseIP(peerIP), pubKey)) } case shared.REQ_ESTABLISH, shared.RESP_ESTABLISH: peer := peers.Get(addr.String()) if peer == nil { - log.Println("unregistered peer tried to ESTABLISH") + log.Println("ESTABLISH from unregistered peer:", addr.String()) return } - srcIP := net.IP(req[20:36]).String() + mac := hmac.New(sha256.New, peer.AuthSecret) + mac.Write(req[36:]) + if !hmac.Equal(mac.Sum(nil), req[4:36]) { + log.Println("failed to authenticate ESTABLISH") + return + } + + srcIP := net.IP(req[52:68]).String() if srcIP != peer.InternalIP { log.Println("rejected spoofed srcIP in ESTABLISH") return } - dstIP := net.IP(req[4:20]).String() + dstIP := net.IP(req[36:52]).String() dstPeer := peersByInternal.Get(dstIP) if dstPeer == nil { log.Println("tried to ESTABLISH with an unknown peer") diff --git a/shared/proto.go b/shared/proto.go index 226856f..f952b8e 100644 --- a/shared/proto.go +++ b/shared/proto.go @@ -4,38 +4,58 @@ import "encoding/binary" const PROTO_VERSION uint16 = 1 +// pubKey - public key generated by the client +// privKey - private key generated by the client +// sessionKey - key established between two peers +// multicastKey - key shared between everyone for encrypting multicast pkts +// authSecret - secret given during registration for authenticating subsequent requests + const ( _ uint16 = iota + // (client -> server) requests a challenge to prove the ownership of the privKey + // [ pubKey - 1216 bytes ] + REQ_GET_CHALLENGE + // (server -> client) provides a pubKey-encrypted authSecret + // [ ciphertext - 1168 bytes ] + RESP_GET_CHALLENGE // (client -> server) requests an IP - // [ pubkey - 1216 bytes ] + // [ HMAC(authSecret, pubKey) - 32 bytes ] [ pubKey - 1216 bytes ] REQ_REGISTER - // (server -> client) returns the assigned IP and encapsulated multicast key + // (server -> client) returns the assigned IP and pubKey-encrypted multicastKey // [ ip - 16 bytes ] [ ciphertext - 1168 bytes ] RESP_REGISTER // (client -> server -> client2) relays an encrypted packet to a specified peer - // [ destIP - 16 bytes ] [ srcIP - 16 bytes ] [ encrypted pkt ] + // [ HMAC(authSecret, rest) - 32 bytes ] [ destIP - 16 bytes ] [ srcIP - 16 bytes ] [ encryptedPkt ] ENC_PKT // (client -> server -> *) broadcasts an encrypted packet - // [ encrypted pkt ] + // [ HMAC(authSecret, rest) - 32 bytes ] [ encryptedPkt ] BROADCAST_PKT - // (client -> server) requests peer's pubkey from the server for encapsulation + // (client -> server) requests peer's pubKey from the server for encapsulation // [ ip - 16 bytes ] REQ_GET_PUBKEY - // (server -> client) provides requested pubkey - // [ ip - 16 bytes ] [ pubkey - 1216 bytes ] + // (server -> client) provides requested pubKey + // [ ip - 16 bytes ] [ pubKey - 1216 bytes ] RESP_GET_PUBKEY - // (client -> server -> client2) establishes a session key with another peer - // [ destIP - 16 bytes ] [ srcIP - 16 bytes ] [ ciphertext - 1120 bytes ] + // (client -> server -> client2) establishes a sessionKey with another peer + // [ HMAC(authSecret, rest) - 32 bytes ] [ destIP - 16 bytes ] [ srcIP - 16 bytes ] [ ciphertext - 1120 bytes ] REQ_ESTABLISH - // (client2 -> server -> client) acknowledges the session key was established - // [ destIP - 16 bytes ] [ srcIP - 16 bytes ] + // (client2 -> server -> client) acknowledges the sessionKey was established + // [ HMAC(authSecret, rest) - 32 bytes ] [ destIP - 16 bytes ] [ srcIP - 16 bytes ] RESP_ESTABLISH ) -func BuildPkt(pktType uint16, data []byte) []byte { - out := make([]byte, 4+len(data)) +func BuildPkt(pktType uint16, parts ...[]byte) []byte { + var total int + for _, p := range parts { + total += len(p) + } + + out := make([]byte, 4, 4+total) binary.LittleEndian.PutUint16(out[0:], PROTO_VERSION) binary.LittleEndian.PutUint16(out[2:], pktType) - copy(out[4:], data) + + for _, p := range parts { + out = append(out, p...) + } return out }