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:
- The server will send the client a message of random bytes(string).
- The client will then compute a digest of the random bytes(string) received from the server with the common secret key.
- The client sends back the computed digest to the server.
- 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/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
Tutorials
+17.7k Golang : How to remove certain lines from a file
+29.4k Golang : Get and Set User-Agent examples
+16.5k Golang : Get own process identifier
+16.1k Golang : Generate QR codes for Google Authenticator App and fix "Cannot interpret QR code" error
+16.7k Golang : How to save log messages to file?
+4.5k Golang : A program that contain another program and executes it during run-time
+8.7k Golang : Get curl -I or head data from URL example
+6.5k Get Facebook friends working in same company
+10.2k Golang : Allow Cross-Origin Resource Sharing request
+16.2k Golang : File path independent of Operating System
+9.9k Golang : How to check if a website is served via HTTPS
+22.1k Golang : Convert Unix timestamp to UTC timestamp