From f9f0df478d018f23d63e87b3a306928912f3bd5a Mon Sep 17 00:00:00 2001 From: Toni Date: Wed, 22 Jul 2026 16:05:30 +0200 Subject: [PATCH] use crypto/hpke for MLKEM768X25519 --- LICENSE | 24 +++++++++++++ README.md | 9 +++++ client/main.go | 95 ++++++++++++++++++++++++++++---------------------- go.mod | 1 - go.sum | 2 -- server/main.go | 1 - 6 files changed, 86 insertions(+), 46 deletions(-) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..da82835 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +BSD 2-Clause License + +Copyright (c) 2026, Antoni Piasecki + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8191685 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# baalvpn + +A post-quantum 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 + +## 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 b5a4f3c..e1b4422 100644 --- a/client/main.go +++ b/client/main.go @@ -1,7 +1,7 @@ package main import ( - "crypto/sha256" + "crypto/hpke" "encoding/binary" "encoding/hex" "log" @@ -13,7 +13,6 @@ import ( "baalvpn/shared" - "filippo.io/mlkem768/xwing" "github.com/songgao/water" ) @@ -27,10 +26,10 @@ var ( serverAddr *net.UDPAddr iface *water.Interface conn *net.UDPConn - privKey *xwing.DecapsulationKey + privKey hpke.PrivateKey establishMutex sync.Mutex - peerPubKeys = shared.NewTMap[string, []byte]() + peerPubKeys = shared.NewTMap[string, hpke.PublicKey]() peerSessionKeys = shared.NewTMap[string, []byte]() peerEstablishAcks = shared.NewTMap[string, bool]() ) @@ -63,13 +62,14 @@ func main() { func register() { var err error - privKey, err = xwing.GenerateKey() + + privKey, err = hpke.MLKEM768X25519().GenerateKey() if err != nil { panic(err) } - pubKey := privKey.EncapsulationKey() + pubKey := privKey.PublicKey() - req := shared.BuildPkt(shared.REQ_REGISTER, pubKey) + req := shared.BuildPkt(shared.REQ_REGISTER, pubKey.Bytes()) if _, err := conn.WriteToUDP(req, serverAddr); err != nil { panic(err) } @@ -88,42 +88,47 @@ func getOrEstablishSessionKey(ip string) []byte { return key } - log.Println("requesting pubkey of " + ip) - req := shared.BuildPkt(shared.REQ_GET_PUBKEY, net.ParseIP(ip)) - if _, err := conn.WriteToUDP(req, serverAddr); err != nil { - panic(err) - } - + // TODO: this definitely shouldnt block the main thread for { - // TODO: eww + // request pubkey every 50ms time.Sleep(50 * time.Millisecond) - if peerPubKey, ok := peerPubKeys.GetOK(ip); ok { - log.Println("got pubkey of " + ip) - - ciphertext, sharedSecret, err := xwing.Encapsulate(peerPubKey) - if err != nil { - panic(err) - } - - reqData := append(net.ParseIP(ip), append(internalIP, ciphertext...)...) - req := shared.BuildPkt(shared.REQ_ESTABLISH, reqData) + peerPubKey, ok := peerPubKeys.GetOK(ip) + if !ok { + req := shared.BuildPkt(shared.REQ_GET_PUBKEY, net.ParseIP(ip)) if _, err := conn.WriteToUDP(req, serverAddr); err != nil { panic(err) } - - for { - // TODO: eww - time.Sleep(50 * time.Millisecond) - if peerEstablishAcks.Get(ip) { - peerEstablishAcks.Delete(ip) - break - } - } - - sessionKey := sha256.Sum256(sharedSecret) - peerSessionKeys.Set(ip, sessionKey[:]) - return sessionKey[:] + continue } + + log.Println("got pubkey of " + ip) + + ciphertext, sender, err := hpke.NewSender(peerPubKey, hpke.HKDFSHA256(), hpke.ExportOnly(), nil) + if err != nil { + panic(err) + } + sessionKey, err := sender.Export("baalvpn", 32) + if err != nil { + panic(err) + } + + reqData := append(net.ParseIP(ip), append(internalIP, ciphertext...)...) + req := shared.BuildPkt(shared.REQ_ESTABLISH, reqData) + if _, err := conn.WriteToUDP(req, serverAddr); err != nil { + panic(err) + } + + for { + // TODO: eww + time.Sleep(50 * time.Millisecond) + if peerEstablishAcks.Get(ip) { + peerEstablishAcks.Delete(ip) + break + } + } + + peerSessionKeys.Set(ip, sessionKey) + return sessionKey } } @@ -193,21 +198,27 @@ func handlePkt(pkt []byte) { } case shared.RESP_GET_PUBKEY: respIP := net.IP(pkt[4:20]).String() - pubKey := pkt[20:] + pubKey, err := hpke.MLKEM768X25519().NewPublicKey(pkt[20:]) + if err != nil { + panic(err) + } peerPubKeys.Set(respIP, pubKey) case shared.REQ_ESTABLISH: srcIP := net.IP(pkt[20:36]).String() ciphertext := pkt[36:] - sharedSecret, err := xwing.Decapsulate(privKey, ciphertext) + r, err := hpke.NewRecipient(ciphertext, privKey, hpke.HKDFSHA256(), hpke.ExportOnly(), nil) + if err != nil { + panic(err) + } + sessionKey, err := r.Export("baalvpn", 32) if err != nil { panic(err) } - sessionKey := sha256.Sum256(sharedSecret) - peerSessionKeys.Set(srcIP, sessionKey[:]) - log.Println("received session key from", srcIP) + peerSessionKeys.Set(srcIP, sessionKey) + reqData := append(net.ParseIP(srcIP), internalIP...) req := shared.BuildPkt(shared.RESP_ESTABLISH, reqData) if _, err := conn.WriteToUDP(req, serverAddr); err != nil { diff --git a/go.mod b/go.mod index a32655a..264ea1a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module baalvpn go 1.26.3 require ( - filippo.io/mlkem768 v0.0.0-20260214141301-2e7bebc7d88d github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 golang.org/x/crypto v0.54.0 ) diff --git a/go.sum b/go.sum index ce2f8ac..a4f2d2e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -filippo.io/mlkem768 v0.0.0-20260214141301-2e7bebc7d88d h1:YyLyABjdrdt2l/E6JAnku4BjhEDXhxQD2bPOnOvy8/M= -filippo.io/mlkem768 v0.0.0-20260214141301-2e7bebc7d88d/go.mod h1:ym4egWKLpazdho3bHx0xuQlCq02ttP+vhxxKO8LgO9c= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= diff --git a/server/main.go b/server/main.go index 937a373..e30f153 100644 --- a/server/main.go +++ b/server/main.go @@ -3,7 +3,6 @@ // TODO: somehow persist IPs // TODO: handle multiple peers behind one NAT // TODO: key rotation -// TODO: sha256 -> HKDF package main import (