Golang : Simple client-server HMAC authentication without SSL example




Problem:

You are building a distributed system that involves multiple processes communicating across a cluster of computers. You want to make sure only authenticated processes are allowed to connect to one another. Your requirement is just to authenticate a connection handshake, not encrypt the connection and thus you do not need SSL connection between the processes. How to implement a simple inter processes authentication?

Solution:

Use hmac (Keyed-Hash Message Authentication Code) on both client and server to compute a hash known **only** to both. The hash digest will be computed from a **secret key** known only to client and server.

The basic idea is:

  1. The server will send the client a message of random bytes(string).
  2. The client will then compute a digest of the random bytes(string) received from the server with the common secret key.
  3. The client sends back the computed digest to the server.
  4. The server compares received digest from the client and decide whether or not to accept the connection request from client.

We will take the previous example of Golang client-server and add-on the connection handshake authentication process.

Let's enchance the server program first to include HMAC authentication mechanism

hmacserver.go


 package main

 import (
 "crypto/hmac"
 "crypto/md5"
 "crypto/rand"
 "encoding/base64"
 "fmt"
 "log"
 "net"
 )

 // both client and server MUST have the same secret key
 // to authenticate

 var secret = "GolangIsAwesome!"

 func randStr(strSize int, randType string) string {

 var dictionary string

 if randType == "alphanum" {
 dictionary = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
 }

 if randType == "alpha" {
 dictionary = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
 }

 if randType == "number" {
 dictionary = "0123456789"
 }

 var bytes = make([]byte, strSize)
 rand.Read(bytes)
 for k, v := range bytes {
 bytes[k] = dictionary[v%byte(len(dictionary))]
 }
 return string(bytes)
 }

 func serverSideAuthenticate(clientConn net.Conn, secretKey string) {
 // request client authentication
 // send client a random string as message

 message := randStr(16, "alphanum")

 _, err := clientConn.Write([]byte(message))

 if err != nil {
 clientConn.Close()
 }

 fmt.Println("Data send to client : ", message)

 // prepare server side hmac digest
 // with secret key and msg

 hasher := hmac.New(md5.New, []byte(secretKey))
 hasher.Write([]byte(message))
 serverHMACdigest := hasher.Sum(nil)
 fmt.Println("Server : ", base64.StdEncoding.EncodeToString(serverHMACdigest))

 // receive hmacDigest from client

 buffer := make([]byte, 4096)
 n, err := clientConn.Read(buffer)
 if err != nil || n == 0 {
 clientConn.Close()
 return
 }


 // don't over read n length
 clientHMACdigest := buffer[:n]

 fmt.Println("Client : ", base64.StdEncoding.EncodeToString(clientHMACdigest))

 // compare if the server and client HMAC digests are the same or not
 // the HANDSHAKING part!
 fmt.Println("Connection authenticated : ", hmac.Equal(serverHMACdigest, clientHMACdigest))

 // this is where you want to do stuff like disconnect client if the authentication failed
 // or proceed
 }

 func handleConnection(c net.Conn) {

 log.Printf("Client %v connected.", c.RemoteAddr())

 serverSideAuthenticate(c, secret)

 log.Printf("Connection from %v closed.", c.RemoteAddr())
 }

 func main() {
 ln, err := net.Listen("tcp", ":6000")
 if err != nil {
 log.Fatal(err)
 }

 fmt.Println("Server up and listening on port 6000")

 for {
 conn, err := ln.Accept()
 if err != nil {
 log.Println(err)
 continue
 }
 go handleConnection(conn)
 }
 }

and

hmacclient.go


 package main

 import (
 "crypto/hmac"
 "crypto/md5"
 "encoding/base64"
 "fmt"
 "net"
 )

 // both client and server MUST have the same secret key
 // to authenticate

 var secret = "GolangIsAwesome!"
 // change the secret to something else and the authentication will fail
 //var secret = "GolangIsTerrible!"

 func clientSideAuthenticate(serverConn net.Conn, secretKey string, message string) {

 // prepare client side hmac digest
 // with secret key and message received from server
 // if the secret key in client and server is the same
 // the digest should be the same.

 hasher := hmac.New(md5.New, []byte(secretKey))
 hasher.Write([]byte(message))
 clientHMACdigest := hasher.Sum(nil)
 fmt.Println("Digest send to server : ", base64.StdEncoding.EncodeToString(clientHMACdigest))

 // send hmacDigest back to server to authenticate

 n, err := serverConn.Write(clientHMACdigest)
 if err != nil || n == 0 {
 serverConn.Close()
 return
 }

 }

 func handleConnection(c net.Conn) {

 buffer := make([]byte, 4096)

 for {
 n, err := c.Read(buffer)
 if err != nil || n == 0 {
 c.Close()
 break
 }
 // don't over read n length
 msg := string(buffer[:n])

 fmt.Println("\nData received from server : ", msg)
 clientSideAuthenticate(c, secret, msg)
 }
 fmt.Printf("Connection from %v closed. \n", c.RemoteAddr())
 }

 func main() {
 hostName := "localhost" // change this to your server domain name 
 portNum := "6000"

 for {
 dialConn, err := net.Dial("tcp", hostName+":"+portNum)

 if err != nil {
 fmt.Println(err)
 continue
 }

 fmt.Printf("\nConnection established between %s and localhost.\n", hostName)
 fmt.Printf("Remote Address : %s \n", dialConn.RemoteAddr().String())
 fmt.Printf("Local Address : %s \n", dialConn.LocalAddr().String())

 go handleConnection(dialConn)
 }

 }

Run hmacserver on one terminal and hmacclient on another terminal/machine.

If the secret keys are the same in both client and server, you will see that the connection authenticated equals to TRUE

2016/07/03 18:27:19 Client [::1]:60825 connected.

Data send to client : iIFO2H2vHpSrwB6S

Server : G3lkRXxCYToGapXzdaoovQ==

Client : G3lkRXxCYToGapXzdaoovQ==

Connection authenticated : true

2016/07/03 18:27:19 Connection from [::1]:60825 closed.

and if not

2016/07/03 17:56:19 Client [::1]:58660 connected.

Data send to client : Zqm6axXwzkYP0UP0

Server : y4E8I4MTjdZFgjna6WRYDA==

Client : TM4Jicrrvxjs829JD0wWsw==

Connection authenticated : false

NOTE:

It is common for HMAC-based authentication to be used internally by software when it sets up communication with subprocesses. Just make sure you don't transmit the secret key along the unencrypted connection as anyone can easily sniff the traffic to pick it up.

By the way, authenticating a connection is not the same as encrypting a connection.

Happy coding!

References:

https://golang.org/pkg/crypto/hmac/

https://www.socketloop.com/references/golang-encoding-base64-encoding-encodetostring-function-example

https://www.socketloop.com/tutorials/golang-simple-client-server-example

https://www.socketloop.com/tutorials/golang-how-to-generate-random-string

  See also : Golang : Secure(TLS) connection between server and client





By Adam Ng

IF you gain some knowledge or the information here solved your programming problem. Please consider donating to the less fortunate or some charities that you like. Apart from donation, planting trees, volunteering or reducing your carbon footprint will be great too.


Advertisement