diff --git a/README.md b/README.md index 960a844..50cfb7b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ 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 +* **Post-quantum security:** Uses [HPKE](https://datatracker.ietf.org/doc/html/rfc9180/)([XWingMLKEM768X25519](https://datatracker.ietf.org/doc/html/draft-connolly-cfrg-xwing-kem/), [HKDF-SHA-256](https://en.wikipedia.org/wiki/HKDF)) + [XChaCha20-Poly1305](https://en.wikipedia.org/wiki/ChaCha20-Poly1305#XChaCha20-Poly1305_%E2%80%93_extended_nonce_variant) for encryption and [ML-DSA-44](https://nvlpubs.nist.gov/nistpubs/fips/nist.fips.204.pdf) for signatures. ## Protocol @@ -11,3 +11,4 @@ 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 +* [`github.com/cloudflare/circl`](https://github.com/cloudflare/circl) - used for ML-DSA, hopefully [not for long](https://github.com/golang/go/issues/77626) diff --git a/client/main.go b/client/main.go index e54ec9a..2f6526e 100644 --- a/client/main.go +++ b/client/main.go @@ -15,13 +15,15 @@ import ( "baalvpn/shared" + "github.com/cloudflare/circl/sign/mldsa/mldsa44" "github.com/songgao/water" ) // TODO: parse some sort of config+key file const ( - SERVER_IP = "172.20.12.47" - IFACE_NAME = "baalvpn" + SERVER_IP = "172.20.12.47" + IFACE_NAME = "baalvpn" + SERVER_PUBKEY = "b137ffedfaf45cb4dc5339373cdc935fbcc7597dde71b86112d282c2bda4c252e6676490acd3f3fd71a7ab7ce2edbe69521a966fa1e91056c8cd9f98443d49505c4a3663b90c322da0942e7511f9c600e0ebbeac071ba3763f623bfd2c2092123c0b2807518d70b790e2612b1b2daa32aa8fa14be8aef9072771e7c4e8c21b64173a504f92378804dcef63430b5c2d4a504d95a766b23ff04e804baf36b515d8c4116e9ffe5fa9afb1c2d005936b3e7222de604e8bae04fd5c56520d8e9dc428e6f6107fd735fcf54a7a324d8236d831b89b4fcda426d701add2b1f4122ba29021d263b74eea6ce1a546daab62b31b12c5b2f14a57ed1160af8630e1d2fa551e7609022a240ad487c76ef1c5b3632c7ca9d5c3648e8b0eaab2256ff5245b0dc25decd9271126b4834b13ddb365e75ad4411e73058460499c9a5522182007378aca990da89f87c90df379112586c90f42a2fe7ec1ab119fb95641abe61f7a04f95f2bd2a849cf01f561bcd4d5f4c57fff30e34e317bdbcba57126b068ded038a7947b6fd8501f2362fba7a695e628647ff28123b8a75df59da7b7b6a8064f6781fd70e62e86208942dc2fbe3a08999bc6b7e493dcf247d3679804c21ac0e64afd906d50b433a858ba6b55cfbb6668c5a4a7814345659756560a4e5678201a2cde4ca3c101520e0d1a0b06af5de5a596793e7bd68cdc45d2234e79b646fbb305096c193f053c6f158a0b1d2d590f43ba9236bb77000e1de75b2eb9693185bd82ac414db63450ea4808752f14946b8609d4b54c909b6854c8b2e822897e2497671102f41366808cd962802a128a551d6c56cccafb133c12294c14d97f02bc808c4a6777f1f602fe248d6d7ec6e850431d593a0510b3e5f75e19cafd3317b9951a12870d3ffdb6f4d0449ff63cce9a2acfd7eafc47befdd506b8b781133e4e4d364b816a072fa05b350368a62a61e4f63377897f579e21c67520c0edf949033210d7785a2435de37a0d609661b71d442bb11f8f9322f2df8dc52662de48299c8e3d82ab75124cb91e03f2d776ae33a6aed94d2eefa7a20d1fb9932c2511aa48304f703316e6397373418c1496b8be06cc97018af7bc43bf94cf5012c7486e251bfff3e275e759f7e1ca15afe8c1934c2a151d8cdd8c028fc67d3c6a22514d1aea6b907f130eda14860c559dee0dd88202ba2e00f5848777c2767bf96817c6b195c2556b4ce223397649c35f311de79c8687e3eec956a054bb36eb3024e520ec6d6455b12d44434ec4c41f7bcdd5133dc884de56ab77a8b1f50a613a5fbf90de87d30469dbc6d3c79b87e9a04a986bd00aa213e9cf270a37b50be919dcb999776f7928a7b43d700eeb6c92eaf901047607cb1d05839374aaa4eb49e8e7e4a69dc70a9b5b006284d72afa9a8f6a96473db5aff3c2f2ae2377a80570d2cd052cb2302d66c5115277673c9634117748c0ddf08315b17229a2ddaf238feb46270278de8d45b0c603a07b890d0f443f96c97a66bb2c783f43807e466e90aab185bcfc9e2036d1d745402c05913ee5bf380a99e5912612b10f39fa0dadefd2e2c47c67e4c118ea2d0579d35793fc2151bcbc40ebaa4e42b86231da9e952238d55327c8f9b96aec5bb1c4b283a6c67d0e0e715e754d8df84740920c002060781ac61662645ae3c92b4d79c28e7cc5e5103ff3b760e9d1feabed95b606275a57e1a1dc91a88505b41d1a4df434891d8e3f8531820a6397a5815c17dfde6e115061898f13611ecede2a1db94d2900359015e675244493f08228f95e36d9c004c0cb0405c2caed5cb31bd88800a6bdac7b53de67b36c2ea92ddd8496bafb0c304f458b28ef51217" ) var ( @@ -76,18 +78,18 @@ func register() { // TODO: all this probably should be retried until we get RESP_REGISTER - log.Println("requesting register challenge") + log.Println("requesting authSecret") 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") + log.Println("received authSecret, registering...") + mac := hmac.New(sha256.New, authSecret) mac.Write(pubKey.Bytes()) send(shared.BuildPkt(shared.REQ_REGISTER, mac.Sum(nil), pubKey.Bytes())) @@ -136,12 +138,20 @@ func handleIncomingPkt(pkt []byte) { switch pktType { case shared.RESP_GET_CHALLENGE: - challengeCh <- pkt[4:] + if !verifyPacket(pkt) { + log.Println("failed to verify signature") + return + } + challengeCh <- pkt[2424:] case shared.RESP_REGISTER: - internalIP = net.IP(pkt[4:20]) + if !verifyPacket(pkt) { + log.Println("failed to verify signature") + return + } + internalIP = net.IP(pkt[2424:2440]) var err error - multicastKey, err = hpke.Open(privKey, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte("baalvpn-register"), pkt[20:]) + multicastKey, err = hpke.Open(privKey, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte("baalvpn-register"), pkt[2440:]) if err != nil { panic(err) } @@ -149,7 +159,7 @@ func handleIncomingPkt(pkt []byte) { log.Println("got assigned IP by the server:", internalIP.String()) setupInterface() go relayPackets() - case shared.ENC_PKT: + case shared.UNICAST_PKT: srcIP := net.IP(pkt[52:68]).String() sessionKey := getOrEstablishSessionKey(srcIP) @@ -173,8 +183,12 @@ func handleIncomingPkt(pkt []byte) { log.Println(err) } case shared.RESP_GET_PUBKEY: - respIP := net.IP(pkt[4:20]).String() - pubKey, err := hpke.MLKEM768X25519().NewPublicKey(pkt[20:]) + if !verifyPacket(pkt) { + log.Println("failed to verify signature") + return + } + respIP := net.IP(pkt[2424:2440]).String() + pubKey, err := hpke.MLKEM768X25519().NewPublicKey(pkt[2440:]) if err != nil { panic(err) } @@ -307,7 +321,7 @@ func relayPackets() { panic(err) } - send(buildAuthPkt(shared.ENC_PKT, net.ParseIP(destIP), internalIP, encryptedPkt)) + send(buildAuthPkt(shared.UNICAST_PKT, net.ParseIP(destIP), internalIP, encryptedPkt)) } } } @@ -324,3 +338,16 @@ func send(req []byte) { panic(err) } } + +func verifyPacket(pkt []byte) bool { + pubKeyBytes, err := hex.DecodeString(SERVER_PUBKEY) + if err != nil { + panic(err) + } + var pubKey mldsa44.PublicKey + if err := pubKey.UnmarshalBinary(pubKeyBytes); err != nil { + panic(err) + } + + return mldsa44.Verify(&pubKey, pkt[2424:], nil, pkt[4:2424]) +} diff --git a/go.mod b/go.mod index 264ea1a..469971c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module baalvpn go 1.26.3 require ( + github.com/cloudflare/circl v1.6.4 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 a4f2d2e..68ab802 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U= +github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY= 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 9eee5fc..32006bd 100644 --- a/server/main.go +++ b/server/main.go @@ -1,6 +1,5 @@ // TODO: slice bounds checking -// TODO: authentication -// TODO: somehow persist IPs +// TODO: somehow persist internal IPs // TODO: key rotation // TODO: dont trust claimed IP at all // TODO: replay attacks are still a thing @@ -13,11 +12,14 @@ import ( "crypto/sha256" "encoding/binary" "encoding/hex" + "fmt" "log" "net" "os" "baalvpn/shared" + + "github.com/cloudflare/circl/sign/mldsa/mldsa44" ) type Peer struct { @@ -27,16 +29,29 @@ type Peer struct { AuthSecret []byte } +const SERVER_PRIVKEY = "b137ffedfaf45cb4dc5339373cdc935fbcc7597dde71b86112d282c2bda4c252c95edb48c5bd92159c992dd601b3294e59fda2b605c176232b760e545f28d3cd8a6ba045d4ad630481521182c5aab0d045f68f12de2a6cdbaea9245576b715cf1329c19e90b009b33708da77f4836cbe0b722ee325e1fe01c5b12f62a86802621829329024600243115b8680c0929108a1285428240017710046819c46264a4825c8b425d818248b2411111204e28481a3248e40c671081988020280d93800180688e4a864e28030c9080c4c800158a46524c390db0426d3b22424c845d11810109211e4c270a348415234610a304d89202590a480c4006c91240e12352021b131014540db320c2104116342648c86681c8744d924089cc845e24002544231021109d48069431290490048e13466031421511068d980404c32909b404112826c8b16061a132e19295054280cd1c08d92444109188de34046db42441323460c3741e4144080b62154184618b744019520210860d1a86820a11014c04004320803c14184b86c0297852185200ac1508ca25194c69182b0715934260018450a115008b52d1009619a922c200430c89491c312520a312488a00d8b16521c13904434411b434504830110120410c2444c284210b22003338de0228e01306448b60844060c19294618830000832400a780a1469240422440a864d1184dd9c084cb108a08363090464e049364899670a3b2400c018820282988b81018b7891c39465a84700c134811b48dda464981c20103044cc30045d9320de44880e226822239214c840c04274213c8251b08910205202116299b380400138424046808352219411091306a528080a22686c1444ed3002ecb18621a1410daa8205290315928661a37911b188889002014242a419268204189591641d1b664d128054c2664030645cab26484a0690a478c19b5812282695132409c344610b448ccc80518a545224489a48229d0a80d214249c4a66dda8228d4022d9b0441c820119a88215b1029944484d1b29110c90d14125013c4818bc228921070514070c922698a2890014620db98702334020b468d21014912a4901325519906095c3460dcb0911c37418a422dc3224a2211698110401282810b4670a3060c61a64843427053404ca2a68048486440246153966c90b86d1bb50863a029d0384964422d10258c59a285c8c89193860823b748032185d0b810a428524ca64c1a29490284815b008c0ca969c1a6409c36b1659c04f7e6f1cc28e6944fafdf06980608ec937a242945a9949704a47ae87cb6aa3b6d85d65f3f9cbcd301505fe0b19607d67f82ad539580c667927fe53c13af2e1a5177fd6f6f8a30829b363f586b92e5e9979c122393a0415e5b29fcf9da93196c10cad9c5038dd227961bc42f3f663d3647f9b811ff9ccaaf17d0a59b1fed877448412c254e3e7936023d4ef9b6b489080eaa04ca14e90e6947ff1d6320fc37acfa3d44b2710fda267e824513a056d7f5666bfac613008b1fba4f2fa12316b16ef3258ff9df5e1301835e76537318626d1d995637892b81d75ab7ed59ac0fd94af24bae94f457c88645b7de7f87faff893bda4bc55a54205b34eea275d02503eb3275b200f6b5fc2cdd82a2f46027e29b6a070c0dbe008933943861ed5e4566e7d1757084ef614f58b4237211dda05ef06bacf6ddc67d8f293fc874cf8eaaba7f4cea273e23fddfcbd40ee1ea7cf2bba94b35cded5682db8e451f5596082775055200da0c4de55834aa5e330e8d797d0d882243cca9eaa04452cbfdc50ebf5bdbacc1eee49c679eac90d13867b91e95a15a9a822ce61691f8a7aecd438d0d701534332688a7047734cc5c9ed538f7f8476c1fb8cdb44eb6db972815dc60464eb1931e1b8102838fb83b6a26458381001a70a61a7f68dd0ddbb077c5a86b48badac51271b01d5efbf019053bf6911ae8bf18fd77e70a2d0288d87af5a6035d52d3328262d72b11e1a597911ad7ec2349c106b25fd0207c31bd6f261104c1ec81f662031def52edce9eafb5cd30a4a77d1d3f4c030dcbcd13a110e1934c91323d2e8bcf57a4bd0f5f86fdd6f322af7791f65fa1af298ef6069863057847758c9285a214c0d264070a870c24a8d0a859d704a89cd0bd448c2175540259d92bae09ccf8e1ef5ba4f2ab8e409ab2027f17103ebe734d11984c8c3d4f337036cb3066413bcd488426ab17128829e88a73171d7f507e3405ba2b5b5bba6334cd51bc8861eff8443dad08670ff91bf45e918c832bfae923daae2c5425f032372257ba12d1b70d4eb38e96707270b5b3b5dff36c4f39526adfcfd96ec806828f431230d4637147e5a20ed646c6e5d81b759db34bde3e7fd39e2b16b774c5d8aa960df7cac49ba1b456ee9f3a0c8a177a6aea0f5d639e9efda6976da073b87b5e02d1a63da02d3b2e5710d4f2b76c7f01c747f328b3ccee5daf30a1a780ecd8896d376082468f1ec08b46a1533a6681c05b02d4a66327728d9014a3f81431f49b3aa82936d8224827606ef7faf12e4975832abd63513a02ffd72499cb33d305dcf1fe37006f156b04a02c3973c5e014f4733c1539f84921e7e1da5ee5d5cca7eaf2462bb4fdea2c91affea2eedfd96b6e706e718d1ff5b4a42b04a8fd20eade15c604bacd169508f3b06143e27e8bf817bf5f1fbd56a8a900ea45eb0c0c62447f9f964ae7b63d354270d02d58fa45b1f461fdceb7fa6bae89798ff7358de44adb58ca19aa989a34b3bb165c1b39054a7e2c1ec67d2fe7f80fc929f70116c41463e85b0e666709652a635eb6b2f1fc021e2e2903631a699b05015e905c68bcfc66dad19e87cb1c4dfb513d2bc473e72410cb3028cfb80295adde6aaa86737bea8b2ad135941d3ef2e17f0de620bd80e3d2aab1272bb7a44f86bcc5ea234910ab3cc15a300c48019d5035f6caf06b9602cc238fb1061a77067464ff0c4f0fed3d8abfccaed26f6e3945e6ec8c31dc92e9f04d1af749705af3612c73ed33e2f93f53eef4c29d48fd66cdb92cfadb6167ede617e71beff15cd231fefb95dfe9affcad1e67086474bfc49a3d3bdc34dd2017a3fc4d45bb2324cd31df6086efe84bc58891ce54bb24ca390297d0600ffc9e112c22860b58a32c21e5961538e30f835b2b0043579441877d858d1d44e1dc9902438bc3a1af68a52de945d7b857587bf37f85992f67e2388380bc3e3387262f9f1dde51491f7fee5202eba070bf82caa395ffeed4eb435073cdd52f40f48259e3c274588820359f4275dd0a097e5faae86aa29c399953b0e20261e3360f7a539d4eb60ccf2c677441e1215ec3fc0777ba613464ec48ea33238606d15e9c544d60d87aa39bd0b80f24a27fea01872eb18a0b38b374d609a14b057a8c6abba736e16ab9fb8a941df040883f9e0270a40e483664168f6c6f57071d06215b081313a221e75818ad80bdf59c84cf59f75af3026c190782b9a386e3c46479561ab413569fc1ca198f39b2b77fb9f21fd9de72998e5183f3006ac0ad36cdf3568aef4346608dc6a05f5cb7fed731cc731796704ed5c009e8fa7749625f8bdd78c28332104bcb9f8b73b786ef36472557f2231ca4ae" + var ( - 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]() + + peers = shared.NewTMap[string, *Peer]() + peersByInternal = shared.NewTMap[string, *Peer]() + peersByPubKey = shared.NewTMap[string, *Peer]() ) func main() { + if len(os.Args) > 1 && os.Args[1] == "keygen" { + pubKey, privKey, err := mldsa44.GenerateKey(rand.Reader) + if err != nil { + panic(err) + } + fmt.Println("pubKey: " + hex.EncodeToString(pubKey.Bytes())) + fmt.Println("privKey: " + hex.EncodeToString(privKey.Bytes())) + return + } + if os.Geteuid() != 0 { panic("root permissions needed") } @@ -119,7 +134,7 @@ func handleReq(req []byte, addr *net.UDPAddr) { panic(err) } - sendTo(addr, shared.BuildPkt(shared.RESP_GET_CHALLENGE, ciphertext)) + sendTo(addr, buildSignedPkt(shared.RESP_GET_CHALLENGE, ciphertext)) case shared.REQ_REGISTER: auth := req[4:36] @@ -170,8 +185,8 @@ func handleReq(req []byte, addr *net.UDPAddr) { panic(err) } - sendTo(addr, shared.BuildPkt(shared.RESP_REGISTER, net.ParseIP(internalIP), ciphertext)) - case shared.ENC_PKT: + sendTo(addr, buildSignedPkt(shared.RESP_REGISTER, net.ParseIP(internalIP), ciphertext)) + case shared.UNICAST_PKT: peer := peers.Get(addr.String()) if peer == nil { log.Println("ENC_PKT from unregistered peer:", addr.String()) @@ -222,7 +237,7 @@ func handleReq(req []byte, addr *net.UDPAddr) { if err != nil { panic(err) } - sendTo(addr, shared.BuildPkt(shared.RESP_GET_PUBKEY, net.ParseIP(peerIP), pubKey)) + sendTo(addr, buildSignedPkt(shared.RESP_GET_PUBKEY, net.ParseIP(peerIP), pubKey)) } case shared.REQ_ESTABLISH, shared.RESP_ESTABLISH: peer := peers.Get(addr.String()) @@ -256,6 +271,26 @@ func handleReq(req []byte, addr *net.UDPAddr) { } } +func buildSignedPkt(pktType uint16, parts ...[]byte) []byte { + pkt := shared.BuildPkt(pktType, parts...) + + privKeyBytes, err := hex.DecodeString(SERVER_PRIVKEY) + if err != nil { + panic(err) + } + var privKey mldsa44.PrivateKey + if err := privKey.UnmarshalBinary(privKeyBytes); err != nil { + panic(err) + } + + sig, err := privKey.Sign(rand.Reader, pkt[4:], nil) + if err != nil { + panic(err) + } + + return append(pkt[:4], append(sig, pkt[4:]...)...) +} + func broadcast(pkt []byte, except string) { peers.ForEach(func(k string, v *Peer) { if v.InternalIP == except { diff --git a/shared/proto.go b/shared/proto.go index f952b8e..f6165ef 100644 --- a/shared/proto.go +++ b/shared/proto.go @@ -4,44 +4,53 @@ 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 +// pubKey - public key generated by the client +// privKey - private key generated by the client +// serverPubKey - pre-shared public key used for authenticating the server +// serverPrivKey - long-term private key used for authenticating the server +// sessionKey - per-peer symmetrical key derived via HPKE for encrypting unicast packets +// multicastKey - key shared between everyone for encrypting multicast packets +// authSecret - secret assigned to a client for authenticating subsequent requests +// SYM = XChaCha20‑Poly1305 with nonce prepended +// HMAC = HMAC-SHA-256 +// hpke = HPKE(MLKEM768X25519, ChaCha20Poly1305, HKDF-SHA-256) +// ds = ML-DSA + +// packet format +// [ version - 2 bytes ] [ type - 2 bytes ] [ payload - see below ] 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 ] + // (server -> client) provides an encrypted authSecret + // [ ds.Sign(serverPrivKey, rest) - 2420 bytes ] [ HPKE(pubKey, authSecret) - 1168 bytes ] RESP_GET_CHALLENGE // (client -> server) requests an IP // [ HMAC(authSecret, pubKey) - 32 bytes ] [ pubKey - 1216 bytes ] REQ_REGISTER - // (server -> client) returns the assigned IP and pubKey-encrypted multicastKey - // [ ip - 16 bytes ] [ ciphertext - 1168 bytes ] + // (server -> client) returns the assigned IP and encrypted multicastKey + // [ ds.Sign(serverPrivKey, rest) - 2420 bytes ] [ ip - 16 bytes ] [ hpke.Seal(pubKey, multicastKey) - 1168 bytes ] RESP_REGISTER - // (client -> server -> client2) relays an encrypted packet to a specified peer - // [ HMAC(authSecret, rest) - 32 bytes ] [ destIP - 16 bytes ] [ srcIP - 16 bytes ] [ encryptedPkt ] - ENC_PKT - // (client -> server -> *) broadcasts an encrypted packet - // [ HMAC(authSecret, rest) - 32 bytes ] [ encryptedPkt ] - BROADCAST_PKT // (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 ] + // [ ds.Sign(serverPrivKey, rest) - 2420 bytes ] [ ip - 16 bytes ] [ pubKey - 1216 bytes ] RESP_GET_PUBKEY // (client -> server -> client2) establishes a sessionKey with another peer - // [ HMAC(authSecret, rest) - 32 bytes ] [ destIP - 16 bytes ] [ srcIP - 16 bytes ] [ ciphertext - 1120 bytes ] + // [ HMAC(authSecret, rest) - 32 bytes ] [ destIP - 16 bytes ] [ srcIP - 16 bytes ] [ hpke.Encap(pubKey) - 1120 bytes ] REQ_ESTABLISH // (client2 -> server -> client) acknowledges the sessionKey was established // [ HMAC(authSecret, rest) - 32 bytes ] [ destIP - 16 bytes ] [ srcIP - 16 bytes ] RESP_ESTABLISH + // (client -> server -> client2) relays an encrypted packet to a specified peer + // [ HMAC(authSecret, rest) - 32 bytes ] [ destIP - 16 bytes ] [ srcIP - 16 bytes ] [ SYM(sessionKey, pkt) ] + UNICAST_PKT + // (client -> server -> *) broadcasts an encrypted packet + // [ HMAC(authSecret, rest) - 32 bytes ] [ SYM(multicastKey, pkt) ] + BROADCAST_PKT ) func BuildPkt(pktType uint16, parts ...[]byte) []byte {