From 6f24325487b7fec589d6f191b2081b2476b2ce5a Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Sun, 26 Apr 2015 19:19:54 +0300 Subject: [PATCH] Replace handshake NULLs with an IDtag Each handshake message contains so called IDtag: XTEA encrypted first 64 bits of transmitted message with client's identity as a key. To determine if it is handshake message we check all possible client identities as a key. Now handshake messages became indistinguishable from the random. Signed-off-by: Sergey Matveev --- TODO | 1 - cmd/govpn-client/main.go | 10 +++- cmd/govpn-server/main.go | 25 ++++++++-- doc/govpn.texi | 32 +++++++------ doc/handshake.txt | 2 +- handshake.go | 100 +++++++++++++++------------------------ identify.go | 23 +++++++-- 7 files changed, 105 insertions(+), 88 deletions(-) diff --git a/TODO b/TODO index 90dd1f3..63d6ef3 100644 --- a/TODO +++ b/TODO @@ -4,4 +4,3 @@ human memorized passphrases to be used * Randomize ports usage * Fragmentize packets, noise the traffic -* Indistinguishable from noise handshake packets diff --git a/cmd/govpn-client/main.go b/cmd/govpn-client/main.go index 8b621c8..119a03d 100644 --- a/cmd/govpn-client/main.go +++ b/cmd/govpn-client/main.go @@ -52,6 +52,7 @@ func main() { govpn.Noncediff = *nonceDiff id := govpn.IDDecode(*IDRaw) + govpn.PeersInitDummy(id) key := govpn.KeyRead(*keyPath) if id == nil { panic("ID is not specified") @@ -120,13 +121,18 @@ MainCycle: } udpPktData = udpBuf[:udpPkt.Size] - if govpn.IsValidHandshakePkt(udpPktData) { + if peer == nil { if udpPkt.Addr.String() != remote.String() { udpReady <- struct{}{} log.Println("Unknown handshake message") continue } - if p := handshake.Client(conn, key, udpPktData); p != nil { + if govpn.IDsCache.Find(udpPktData) == nil { + log.Println("Invalid identity in handshake packet") + udpReady <- struct{}{} + continue + } + if p := handshake.Client(id, conn, key, udpPktData); p != nil { log.Println("Handshake completed") if firstUpCall { go govpn.ScriptCall(*upPath, *ifaceName) diff --git a/cmd/govpn-server/main.go b/cmd/govpn-server/main.go index b129f2d..3d952ea 100644 --- a/cmd/govpn-server/main.go +++ b/cmd/govpn-server/main.go @@ -114,6 +114,8 @@ func main() { var udpPkt *govpn.UDPPkt var udpPktData []byte var ethEvent EthEvent + var peerId *govpn.PeerId + var handshakeProcessForce bool ethSink := make(chan EthEvent) log.Println(govpn.VersionGet()) @@ -187,13 +189,21 @@ MainCycle: } udpPktData = udpBuf[:udpPkt.Size] addr = udpPkt.Addr.String() - if govpn.IsValidHandshakePkt(udpPktData) { + handshakeProcessForce = false + HandshakeProcess: + if _, exists = peers[addr]; handshakeProcessForce || !exists { + peerId = govpn.IDsCache.Find(udpPktData) + if peerId == nil { + log.Println("Unknown identity from", addr) + udpReady <- struct{}{} + continue + } state, exists = states[addr] if !exists { state = govpn.HandshakeNew(udpPkt.Addr) states[addr] = state } - peer = state.Server(conn, udpPktData) + peer = state.Server(peerId, conn, udpPktData) if peer != nil { log.Println("Peer handshake finished", peer) if _, exists = peers[addr]; exists { @@ -216,7 +226,9 @@ MainCycle: }() } } - udpReady <- struct{}{} + if !handshakeProcessForce { + udpReady <- struct{}{} + } continue } peerState, exists = peers[addr] @@ -224,7 +236,12 @@ MainCycle: udpReady <- struct{}{} continue } - peerState.peer.UDPProcess(udpPktData, peerState.tap, udpReady) + // If it fails during processing, then try to work with it + // as with handshake packet + if !peerState.peer.UDPProcess(udpPktData, peerState.tap, udpReady) { + handshakeProcessForce = true + goto HandshakeProcess + } } } } diff --git a/doc/govpn.texi b/doc/govpn.texi index 21656a0..3f78209 100644 --- a/doc/govpn.texi +++ b/doc/govpn.texi @@ -330,7 +330,7 @@ DH-EKE powered by @url{http://cr.yp.to/ecdh.html, Curve25519} @item Packet overhead 24 bytes per packet @item Handshake overhead -4 UDP (2 from client, 2 from server) packets, 240 bytes total payload +4 UDP (2 from client, 2 from server) packets, 200 bytes total payload @end table @menu @@ -346,7 +346,8 @@ ENCn(SERIAL) + ENC(KEY, ENCn(SERIAL), DATA) + AUTH(ENCn(SERIAL) + ENC(KEY, ENCn(SERIAL), DATA)) @end verbatim -Each transport message is indistinguishable from pseudo random noise. +All transport and handshake messages are indistinguishable from +pseudo random noise. @code{SERIAL} is message's serial number. Odds are reserved for client(→server) messages, evens for server(→client) messages. @@ -384,13 +385,19 @@ rearranged UDP packets. @verbatiminclude handshake.utxt +Each handshake message ends with so called @code{IDtag}: it is an XTEA +encrypted first 64 bits of each message with client's identity as a key. +It is used to transmit identity and to mark packet as handshake message. +Server can determine used identity by trying all possible known to him +keys. It consumes resources, but XTEA is rather fast algorithm and +handshake messages checking is seldom enough event. + @enumerate @item client generates @code{CPubKey}, random 64bit @code{R} that is used as a -nonce for encryption, and an encrypted @code{R} with XTEA, where the key -equals to client's identity +nonce for encryption @item -@verb{|R + enc(PSK, R, CPubKey) + xtea(ID, R) + NULL + NULLs -> Server|} [65 bytes] +@verb{|R + enc(PSK, R, CPubKey) + IDtag -> Server|} [48 bytes] @item server remembers clients address, decrypt @code{CPubKey}, generates @code{SPrivKey}/@code{SPubKey}, computes common shared key @code{K} @@ -398,19 +405,19 @@ server remembers clients address, decrypt @code{CPubKey}, generates number @code{RS} and 256bit random @code{SS}. PSK-encryption uses incremented @code{R} (from previous message) for nonce @item -@verb{|enc(PSK, R+1, SPubKey) + enc(K, R, RS + SS) + NULLs -> Client|} [88 bytes] +@verb{|enc(PSK, R+1, SPubKey) + enc(K, R, RS + SS) + IDtag -> Client|} [80 bytes] @item client decrypt @code{SPubKey}, computes @code{K}, decrypts @code{RS}, @code{SS} with key @code{K}, remembers @code{SS}, generates 64bit random number @code{RC} and 256bit random @code{SC}, @item -@verb{|enc(K, R+1, RS + RC + SC) + NULLs -> Server|} [64 bytes] +@verb{|enc(K, R+1, RS + RC + SC) + IDtag -> Server|} [56 bytes] @item server decrypt @code{RS}, @code{RC}, @code{SC} with key @code{K}, compares @code{RS} with it's own one send before, computes final main encryption key @code{S = SS XOR SC} @item -@verb{|ENC(K, 0, RC) + NULLs -> Client|} [24 bytes] +@verb{|ENC(K, 0, RC) + IDtag -> Client|} [16 bytes] @item server switches to the new client @item @@ -418,12 +425,9 @@ client decrypts @code{RC} and compares with it's own generated one, computes final main encryption key @code{S} @end enumerate -Where PSK is 256bit pre-shared key, @code{NULLs} are 16 null-bytes. -@code{R*} are required for handshake randomization and two-way -authentication. K key is used only during handshake. @code{NULLs} are -required to differentiate common transport protocol messages from -handshake ones. DH public keys can be trivially derived from private -ones. +Where PSK is 256bit pre-shared key. @code{R*} are required for handshake +randomization and two-way authentication. K key is used only during +handshake. DH public keys can be trivially derived from private ones. @node Reporting bugs @unnumbered Reporting bugs diff --git a/doc/handshake.txt b/doc/handshake.txt index d946833..b36d9f3 100644 --- a/doc/handshake.txt +++ b/doc/handshake.txt @@ -4,7 +4,7 @@ participant Server Client -> Client : R=rand(64bit) Client -> Client : CPrivKey=rand(256bit) -Client -> Server : R, enc(PSK, R, CPubKey), xtea(ID, R) +Client -> Server : R, enc(PSK, R, CPubKey) Server -> Server : SPrivKey=rand(256bit) Server -> Server : K=DH(SPrivKey, CPubKey) Server -> Server : RS=rand(64bit) diff --git a/handshake.go b/handshake.go index 44f9a29..86e6083 100644 --- a/handshake.go +++ b/handshake.go @@ -28,7 +28,6 @@ import ( "time" "golang.org/x/crypto/curve25519" - "golang.org/x/crypto/poly1305" "golang.org/x/crypto/salsa20" "golang.org/x/crypto/salsa20/salsa" "golang.org/x/crypto/xtea" @@ -55,25 +54,17 @@ func keyFromSecrets(server, client []byte) *[KeySize]byte { return k } -// Check if it is valid handshake-related message. -// Minimal size and last 16 zero bytes. -func IsValidHandshakePkt(pkt []byte) bool { - if len(pkt) < 24 { - return false - } - for i := len(pkt) - poly1305.TagSize; i < len(pkt); i++ { - if pkt[i] != '\x00' { - return false - } - } - return true -} - // Zero handshake's memory state func (h *Handshake) Zero() { - sliceZero(h.rNonce[:]) - sliceZero(h.dhPriv[:]) - sliceZero(h.key[:]) + if h.rNonce != nil { + sliceZero(h.rNonce[:]) + } + if h.dhPriv != nil { + sliceZero(h.dhPriv[:]) + } + if h.key != nil { + sliceZero(h.key[:]) + } if h.rServer != nil { sliceZero(h.rServer[:]) } @@ -119,6 +110,17 @@ func HandshakeNew(addr *net.UDPAddr) *Handshake { return &state } +// Generate ID tag from client identification and data. +func idTag(id *PeerId, data []byte) []byte { + ciph, err := xtea.NewCipher(id[:]) + if err != nil { + panic(err) + } + enc := make([]byte, xtea.BlockSize) + ciph.Encrypt(enc, data[:xtea.BlockSize]) + return enc +} + // Start handshake's procedure from the client. // It is the entry point for starting the handshake procedure. // You have to specify outgoing conn address, remote's addr address, @@ -137,19 +139,8 @@ func HandshakeStart(conn *net.UDPConn, addr *net.UDPAddr, id *PeerId, key *[32]b } enc := make([]byte, 32) salsa20.XORKeyStream(enc, dhPub[:], state.rNonce[:], key) - - ciph, err := xtea.NewCipher(id[:]) - if err != nil { - panic(err) - } - rEnc := make([]byte, xtea.BlockSize) - ciph.Encrypt(rEnc, state.rNonce[:]) - data := append(state.rNonce[:], enc...) - data = append(data, rEnc...) - data = append(data, '\x00') - data = append(data, make([]byte, poly1305.TagSize)...) - + data = append(data, idTag(id, state.rNonce[:])...) if _, err := conn.WriteTo(data, addr); err != nil { panic(err) } @@ -158,24 +149,14 @@ func HandshakeStart(conn *net.UDPConn, addr *net.UDPAddr, id *PeerId, key *[32]b // Process handshake message on the server side. // This function is intended to be called on server's side. -// Our outgoing conn connection and received data are required. +// Client identity, our outgoing conn connection and +// received data are required. // If this is the final handshake message, then new Peer object // will be created and used as a transport. If no mutually // authenticated Peer is ready, then return nil. -func (h *Handshake) Server(conn *net.UDPConn, data []byte) *Peer { - switch len(data) { - case 65: // R + ENC(PSK, dh_client_pub) + xtea(ID, R) + NULL + NULLs - if h.rNonce != nil { - log.Println("Invalid handshake stage from", h.addr) - return nil - } - - // Try to determine client's ID - id := IDsCache.Find(data[:8], data[8+32:8+32+8]) - if id == nil { - log.Println("Unknown identity from", h.addr) - return nil - } +func (h *Handshake) Server(id *PeerId, conn *net.UDPConn, data []byte) *Peer { + // R + ENC(PSK, dh_client_pub) + IDtag + if len(data) == 48 && h.rNonce == nil { key := KeyRead(path.Join(PeersPath, id.String(), "key")) h.Id = *id @@ -210,17 +191,13 @@ func (h *Handshake) Server(conn *net.UDPConn, data []byte) *Peer { // Send that to client if _, err := conn.WriteTo( - append(encPub, - append(encRs, make([]byte, poly1305.TagSize)...)...), h.addr); err != nil { + append(encPub, append(encRs, idTag(id, encPub)...)...), h.addr); err != nil { panic(err) } h.LastPing = time.Now() - case 64: // ENC(K, RS + RC + SC) + NULLs - if (h.rNonce == nil) || (h.rClient != nil) { - log.Println("Invalid handshake stage from", h.addr) - return nil - } - + } else + // ENC(K, RS + RC + SC) + IDtag + if len(data) == 56 && h.rClient == nil { // Decrypted Rs compare rServer decRs := make([]byte, 8+8+32) salsa20.XORKeyStream(decRs, data[:8+8+32], h.rNonceNext(), h.key) @@ -232,7 +209,7 @@ func (h *Handshake) Server(conn *net.UDPConn, data []byte) *Peer { // Send final answer to client enc := make([]byte, 8) salsa20.XORKeyStream(enc, decRs[8:8+8], make([]byte, 8), h.key) - if _, err := conn.WriteTo(append(enc, make([]byte, poly1305.TagSize)...), h.addr); err != nil { + if _, err := conn.WriteTo(append(enc, idTag(id, enc)...), h.addr); err != nil { panic(err) } @@ -240,7 +217,7 @@ func (h *Handshake) Server(conn *net.UDPConn, data []byte) *Peer { peer := newPeer(h.addr, h.Id, 0, keyFromSecrets(h.sServer[:], decRs[8+8:])) h.LastPing = time.Now() return peer - default: + } else { log.Println("Invalid handshake message from", h.addr) } return nil @@ -248,15 +225,14 @@ func (h *Handshake) Server(conn *net.UDPConn, data []byte) *Peer { // Process handshake message on the client side. // This function is intended to be called on client's side. -// Our outgoing conn connection, authentication key and received data -// are required. Client does not work with identities, as he is the -// only one, so key is a requirement. +// Our outgoing conn connection, authentication +// key and received data are required. // If this is the final handshake message, then new Peer object // will be created and used as a transport. If no mutually // authenticated Peer is ready, then return nil. -func (h *Handshake) Client(conn *net.UDPConn, key *[KeySize]byte, data []byte) *Peer { +func (h *Handshake) Client(id *PeerId, conn *net.UDPConn, key *[KeySize]byte, data []byte) *Peer { switch len(data) { - case 88: // ENC(PSK, dh_server_pub) + ENC(K, RS + SS) + NULLs + case 80: // ENC(PSK, dh_server_pub) + ENC(K, RS + SS) + IDtag if h.key != nil { log.Println("Invalid handshake stage from", h.addr) return nil @@ -290,11 +266,11 @@ func (h *Handshake) Client(conn *net.UDPConn, key *[KeySize]byte, data []byte) * append(h.rClient[:], h.sClient[:]...)...), h.rNonceNext(), h.key) // Send that to server - if _, err := conn.WriteTo(append(encRs, make([]byte, poly1305.TagSize)...), h.addr); err != nil { + if _, err := conn.WriteTo(append(encRs, idTag(id, encRs)...), h.addr); err != nil { panic(err) } h.LastPing = time.Now() - case 24: // ENC(K, RC) + NULLs + case 16: // ENC(K, RC) + IDtag if h.key == nil { log.Println("Invalid handshake stage from", h.addr) return nil diff --git a/identify.go b/identify.go index c6fcc9d..e4baea1 100644 --- a/identify.go +++ b/identify.go @@ -60,6 +60,17 @@ func PeersInit(path string) { }() } +// Initialize dummy cache for client-side usage. It will consist only +// of single key. +func PeersInitDummy(id *PeerId) { + IDsCache = make(map[PeerId]*xtea.Cipher) + cipher, err := xtea.NewCipher(id[:]) + if err != nil { + panic(err) + } + IDsCache[*id] = cipher +} + // Refresh IDsCache: remove disappeared keys, add missing ones with // initialized ciphers. func (cc cipherCache) refresh() { @@ -103,13 +114,17 @@ func (cc cipherCache) refresh() { } // Try to find peer's identity (that equals to an encryption key) -// by providing cipher and plain texts. -func (cc cipherCache) Find(plaintext, ciphertext []byte) *PeerId { +// by taking first blocksize sized bytes from data at the beginning +// as plaintext and last bytes as cyphertext. +func (cc cipherCache) Find(data []byte) *PeerId { + if len(data) < xtea.BlockSize*2 { + return nil + } buf := make([]byte, xtea.BlockSize) cipherCacheLock.RLock() for pid, cipher := range cc { - cipher.Decrypt(buf, ciphertext) - if subtle.ConstantTimeCompare(buf, plaintext) == 1 { + cipher.Decrypt(buf, data[len(data)-xtea.BlockSize:]) + if subtle.ConstantTimeCompare(buf, data[:xtea.BlockSize]) == 1 { ppid := PeerId(pid) cipherCacheLock.RUnlock() return &ppid -- 2.44.0