HAProxy & Lua: Store information in signed cookies

In some cases it is convenient to store data in cookie. This data must be signed by server for preventing modification by an attacker or something like that.

In other way, HAProxy require Lua-5.3. In many cases, this requirement implies usage of a Lua library specifically packaged because current Linux distributions doesn't embed Lua-5.3. Sometimes, usefull Lua libraries are not available with your distro, and you don't want to use heavy solutions like luarocks. This article explain also compile some modules:

  • base64
  • crypto
  • cjson

Lua compilation

The first step is compiling Lua. This library is very easy to compile, it doesn't have a lot of dependencies nor a ./configure which add compilation difficulties on manny Linux distributions.

The Lua dependencies are "libreadline". Note that this dependency is required for the command line Lua interpretor. If you not have this library, the Lua library will be compiled but not the command line interpretor. This condition doesn't impact the embedding with HAProxy.

Just download le last Lua version (here 5.3.4), extract data from the tarball, enter the directory and type "make":

That's all ! The installation is not required.  We considers that the Lua directory is store in the variable "".

base64 compilation

This extension is very useful, and very easy to compile. We download the archive and compile the library with right path. The command are:

That's done ! Le library is available in the source directory and it is called base64.so.

Note that the compilation embed Lua test of this new library. On my screen, it displays:

/src/lua test.lua
base64 library for Lua 5.3 / Aug 2012

0 true  
1 true TA== L
2 true THU= Lu
3 true THVh Lua
4 true THVhLQ== Lua-
5 true THVhLXM= Lua-s
6 true THVhLXNj Lua-sc
7 true THVhLXNjcg== Lua-scr
8 true THVhLXNjcmk= Lua-scri
9 true THVhLXNjcmlw Lua-scrip

testing prefix 0
testing prefix 1
testing prefix 2
testing prefix 3

THVhLXNjcmlwdGluZy1sYW5ndWFnZQ== Lua-scripting-language 22
???h???jcmlwd?lu?y1s??5nd??n??== nil
THV?LXN??????G??Z?1?YW5??WF?ZQ== nil
===h===jcmlwd=lu=y1s==5nd==n====  0
THV=LXN======G==Z=1=YW5==WF=ZQ== Lu 2

Note that, the website whoch provides the base64 library provides also a lot of useful libraries: http://webserver2.tecgraf.puc-rio.br/~lhf/ftp/lua/index.html

lua-cjson compilation

This library is also easy to compile. It provides very useful JSON converters functions. There is the way to compile:

Some warning will be display, but you can ignore it. The Lua library is available in the source directory with the name "cjson.so".

This library doesn't provides easy-to-use tests. You can test with this code written in the file "test.lua" stored in the source directory

local cjson = require("cjson")

local struct = {
  a = "a",
  b = {
  33, 34
  },
  c = "string"
}

enc = cjson.encode(struct)
print(enc)

dec = cjson.decode(enc)
print(dec['c'])

And executes the following command line:

user@host:~/build/lua-cjson-2.1.0$ /src/lua test.lua
{"b":[33,34],"c":"string","a":"a"}
string

crypto compilation

This library is very annoying to compile because it use all the compilation assistants: automake, pkg-config, autoconf, libtool, luarocks, and it very difficult to find the right option to compile with a Lua source installed in a non-standard directory.

In other way, look in the directory "src". You will find only one file ! All this crap is used for compiling only one file... :-(

One point for the library: It seems to be deprecated, But it provides required functions.

Default Lua build doesn't provide "*pkg*" file, the option LUA_CFLAGS seems to be ignored, ... After many time of research and try, I decide to use a non conventional way to compile this library. There is my solution:

First, we need to apply a little patch because this library is written for Lua version < 5.3.x. It easy: edit the file src/lcrypto.c and line 91, replace "luaL_checkint" by "luaL_checkinteger".

It works. I count about 25 various build files for an effective work of two fucking compilation lines. Great !

We will test the library, but only the load. We considers if the library is loaded, it will works perfectly.

local crypto = require("crypto")

And executes the Lua code:

user@host:~/build/luacrypto-0.3.2$ /src/lua test.lua 
table: 0x1823160

It works !

Now we have 3 .so files which contains basic functions for out main goal. Just copy these files in the same directory that the haproxy configuration and the Lua file. These library will be move in a conventional directory later.

Crypting cookies with HAProxy and Lua

The following Lua program shows how encoding Lua data and/or HAProxy variables in a signed cookie. The values will be serialised using json and them base64 encoded. The JSON string is signed  with an SHA-256 HMAC

We are just 4 functions:

  • cookie_encode: Get a Lua struct, encode it and generate signature, return cookie compatible string.
  • cookie_decode: decode the cookie and verify the signature, and return a Lua struct.
  • one HAProxy action for retrieving the cookie
  • one HAProxy action for generating the cookie

This is the code:

cjson  = require("cjson")
base64 = require("base64")
crypto = require("crypto")

-- serialize and sign this data
function cookie_encode(data, secret)
  local cookie_json = cjson.encode(cookie_data)
  local cookie_base64 = base64.encode(cookie_json)
  local sign_bin = crypto.hmac.digest("sha256", cookie_json, secret)
  local sign_base64 = base64.encode(sign_bin)
  return cookie_base64 .. "@" .. sign_base64
end

-- deserialize and check cookie
function cookie_decode(data, secret)
  local index = string.find(data, "@")
  if index == nil then return false, "bad-format" end
  local cookie_base64 = string.sub(data, 1, index - 1)
  local cookie_json = base64.decode(cookie_base64)
  if cookie_json == nil then return false, "bad-format" end
  local sign_base64 = string.sub(data, index + 1)
  local sign_bin = base64.decode(sign_base64)
  if cookie_json == nil then return false, "bad-format" end
  local sign_cmp = crypto.hmac.digest("sha256",cookie_json, secret)
  if sign_cmp ~= sign_bin then return false, "bad-sign" end
  local st, cookie_data = pcall(cjson.decode, cookie_json)
  if st == false then return false, "bad-format" end
  return true, cookie_data
end

-- Secret key
secret = "s3cr3t"

-- Create cookie and set it
core.register_action("set-cookie", {"http-res"}, function (txn)
  local cookie_data = {
  date = os.date("%Y-%m-%d %H:%M:%S"),
  var1 = txn:get_var("txn.var1"),
  var2 = txn:get_var("txn.var2")
  }
  -- Generate cookie
  local cookie = cookie_encode(cookie_data, secret)
  txn.http:req_add_header("Set-Cookie", "MYDATA="..cookie)
end)

-- Decode cookie ans restore vars
core.register_action("get-cookie", {"http-req"}, function(txn)
  local cookie = txn.sf:req_cook_val("MYDATA")
  local status, cookie_dec = cookie_decode(cookie, secret)
  if status == false then return end
  txn:set_var("txn.var1", cookie_dec.var1)
  txn:set_var("txn.var2", cookie_dec.var2)
end)

You must add tow lines in the HAProxy configuration:

  • http-request lua.get-cookie
  • http-response lua.set-cookie

The HAProxy transaction variables txn.var1 and txn.var2 will be encoded, signed and sent as cookie.

Do not hesitate to send feedback