Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for HMAC-SHA256 and other SHA* algorithms #18

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,15 @@ Synopsis
ngx.say("AES 128 CBC (WITH IV) Encrypted HEX: ", str.to_hex(encrypted))
ngx.say("AES 128 CBC (WITH IV) Decrypted: ",
aes_128_cbc_with_iv:decrypt(encrypted))


-- example with HMAC-SHA256
-- there are other algorithms supported as well:
-- sha1, sha224, sha256, sha384, sha512
local resty_hmac_sha256 = require "resty.hmac"
local hmac_sha256 = resty_hmac_sha256:new()
local digest = hmac_sha256:digest("sha256","secret-key","Hello world")
ngx.say("hmac_sha256: ", digest)
```

[Back to TOC](#table-of-contents)
Expand Down
94 changes: 94 additions & 0 deletions lib/resty/hmac.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
-- Adds HMAC support to Lua with multiple algorithms, via OpenSSL and FFI
--
-- Author: ddragosd@gmail.com
-- Date: 16/05/14
--


local ffi = require "ffi"
local ffi_new = ffi.new
local ffi_str = ffi.string
local C = ffi.C
local resty_string = require "resty.string"
local setmetatable = setmetatable
local error = error


local _M = { _VERSION = '0.09' }


local mt = { __index = _M }

--
-- EVP_MD is defined in openssl/evp.h
-- HMAC is defined in openssl/hmac.h
--
ffi.cdef[[
typedef struct env_md_st EVP_MD;
typedef struct env_md_ctx_st EVP_MD_CTX;
unsigned char *HMAC(const EVP_MD *evp_md, const void *key, int key_len,
const unsigned char *d, size_t n, unsigned char *md,
unsigned int *md_len);
const EVP_MD *EVP_sha1(void);
const EVP_MD *EVP_sha224(void);
const EVP_MD *EVP_sha256(void);
const EVP_MD *EVP_sha384(void);
const EVP_MD *EVP_sha512(void);
]]

-- table definind the available algorithms and the length of each digest
-- for more information @see: http://csrc.nist.gov/publications/fips/fips180-4/fips-180-4.pdf
local available_algorithms = {
sha1 = { alg = C.EVP_sha1(), length = 160/8 },
sha224 = { alg = C.EVP_sha224(), length = 224/8 },
sha256 = { alg = C.EVP_sha256(), length = 256/8 },
sha384 = { alg = C.EVP_sha384(), length = 384/8 },
sha512 = { alg = C.EVP_sha512(), length = 512/8 }
}

-- 64 is the max lenght and it covers up to sha512 algorithm
local digest_len = ffi_new("int[?]", 64)
local buf = ffi_new("char[?]", 64)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this declaring a single buffer instance that gets reused across all instances of hmac? I'm new to Lua, but if this is the case, that would be dangerous (wouldn't it?).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ljwagerfield This is a temporary buffer that is used inside a single Lua function that never yields. Since nginx uses single threaded processes and it does not yield for Lua light threads, it is safe.



function _M.new(self)
return setmetatable({}, mt)
end

local function getDigestAlgorithm(dtype)
local md_name = available_algorithms[dtype]
if ( md_name == nil ) then
error("attempt to use unknown algorithm: '" .. dtype ..
"'.\n Available algorithms are: sha1,sha224,sha256,sha384,sha512")
end
return md_name.alg, md_name.length
end

---
-- Returns the HMAC digest. The hashing algorithm is defined by the dtype parameter.
-- The optional raw flag, defaulted to false, is a boolean indicating whether the output should be a direct binary
-- equivalent of the HMAC or formatted as a hexadecimal string (the default)
--
-- @param self
-- @param dtype The hashing algorithm to use is specified by dtype
-- @param key The secret
-- @param msg The message to be signed
-- @param raw When true, it returns the binary format, else, the hex format is returned
--
function _M.digest(self, dtype, key, msg, raw)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This creates a new context for every MAC which can get expensive if you're creating / verifying many MAC values. Also, it doesn't allow for a single MAC to be updated at a later time (e.g. for large values / stream values).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback. Line 84 with C.HMAC method creates a new context each time indeed.
This worked perfectly fine for my use case, but I also see your use case as another nice-to-have.
Would adding 3 new methods similar to hmac.h : init, update and final and maybe a 4th reset cover all scenarios ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think adding update, final and reset would cover our needs (init could just be done inside new).
We actually just published a standalone module which includes exactly this functionality: https://github.com/jkeys089/lua-resty-hmac

Had I known someone else was working on this I would have collaborated with you directly.
Perhaps we could just contribute the code we've published back into the official lua-resty-string? I'd happy to prepare a PR if you think it makes sense.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jkeys089 sounds great ! I'm all for collaboration also.
For some reason I'm thinking that the init method makes sense if we want to keep the exiting digest method, just to keep the API simple for the cases that don't need appending capabilities.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To use the existing digest method do we even need a new instance? I think it can just be called without reference to self and it will work in exactly the same way (i.e. remove self argument from the digest method and call it like hmac.digest("sha256", "sec-key", "some msg")) .

It doesn't make too much difference I think but it does keep the API a bit simpler and keeps the API similar other libs (e.g.

function _M.new(self, key, salt, _cipher, _hash, hash_rounds)
)

local evp_md, digest_length_int = getDigestAlgorithm(dtype)
if key == nil or msg == nil then
error("attempt to digest with a null key or message")
end

C.HMAC(evp_md, key, #key, msg, #msg, buf, digest_len)

if raw == true then
return ffi_str(buf,digest_length_int)
end

return resty_string.to_hex(ffi_str(buf,digest_length_int))
end


return _M
195 changes: 195 additions & 0 deletions t/hmac.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# vi:ft=

use Test::Nginx::Socket;

repeat_each(2);

plan tests => repeat_each() * (3 * blocks());

our $HttpConfig = <<'_EOC_';
#lua_code_cache off;
lua_package_path 'lib/?.lua;;';
lua_package_cpath 'lib/?.so;;';
_EOC_

no_long_string();

run_tests();

__DATA__

=== TEST 1: hello HMAC-SHA-1
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua '
local resty_hmac_sha1 = require "resty.hmac"
local hmac_sha1 = resty_hmac_sha1:new()

local digest = hmac_sha1:digest("sha1","secret-key","Hello world")
ngx.say("hmac_sha1: ", digest)

--test with an empty string
digest = hmac_sha1:digest("sha1","secret-key","")
ngx.say("hmac_sha1: ", digest)
';
}
--- request
GET /t
--- response_body
hmac_sha1: 3a20c85ba3af4c1b1eec24c672cfb3db803e3637
hmac_sha1: 0877fcf3af864ddf56157f9f4e39eb48dedd74fd
--- no_error_log
[error]

=== TEST 2: hello HMAC-SHA-224
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua '
local resty_hmac_sha224 = require "resty.hmac"
local hmac_sha224 = resty_hmac_sha224:new()

local digest = hmac_sha224:digest("sha224","secret-key","Hello world")
ngx.say("hmac_sha224: ", digest)

--test with an empty string
digest = hmac_sha224:digest("sha224","secret-key","")
ngx.say("hmac_sha224: ", digest)
';
}
--- request
GET /t
--- response_body
hmac_sha224: a38aa774b3d7f49e6f3a7006cdfac9f8aeab4427dd3fb47123be5874
hmac_sha224: a41ef5660a729abe83238c6921861ddd157a2314df03d98252a9ecac
--- no_error_log
[error]


=== TEST 3: hello HMAC-SHA-256
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua '
local resty_hmac_sha256 = require "resty.hmac"
local hmac_sha256 = resty_hmac_sha256:new()

local digest = hmac_sha256:digest("sha256","secret-key","Hello world")
ngx.say("hmac_sha256: ", digest)

--test with an empty string
digest = hmac_sha256:digest("sha256","secret-key","")
ngx.say("hmac_sha256: ", digest)
';
}
--- request
GET /t
--- response_body
hmac_sha256: 902dd133c19fef9216f144694b1b9cc9e06c7be3252019f7e12909ff07122220
hmac_sha256: 345fba21f06a4f75ed673fb93dc16cd47d8dc7a69f52e84e3016fcf69835fdb8
--- no_error_log
[error]


=== TEST 4: hello HMAC-SHA-384
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua '
local resty_hmac_sha384 = require "resty.hmac"
local hmac_sha384 = resty_hmac_sha384:new()

local digest = hmac_sha384:digest("sha384","secret-key","Hello world")
ngx.say("hmac_sha384: ", digest)

--test with an empty string
digest = hmac_sha384:digest("sha384","secret-key","")
ngx.say("hmac_sha384: ", digest)
';
}
--- request
GET /t
--- response_body
hmac_sha384: 7baab15202e53288e026c9b318c08527692ad27ef8903bcb405bcd097e4ed7611cb542d760234ef04536da3a16e906bf
hmac_sha384: 3e4c852a0ce874f8bef33bb899b7fa938f5ce8418bafc530e7c2df532b7be4ad49f5b57ca49c50d9080c16b74ef124dc
--- no_error_log
[error]



=== TEST 5: hello HMAC-SHA-512
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua '
local resty_hmac_sha512 = require "resty.hmac"
local hmac_sha512 = resty_hmac_sha512:new()

local digest = hmac_sha512:digest("sha512","secret-key","Hello world")
ngx.say("hmac_sha512: ", digest)

--test with an empty string
digest = hmac_sha512:digest("sha512","secret-key","")
ngx.say("hmac_sha512: ", digest)
';
}
--- request
GET /t
--- response_body
hmac_sha512: cbec52174d245ed147e57860e9317605895e1b4af43b080701bccb083f2194cd3ada40623420c2ab1b4c77ce6e6d26b149128867035eba259d495524fa230dee
hmac_sha512: 1560eeb87551d027de6007027af3faab5f644f8ef96519c4b519531a6620c755b61d210f179754f991607151b4b9a9db3377132b9e8587f803cdf8763499bcdc
--- no_error_log
[error]

=== TEST 6: test with an invalid HMAC algorithm
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua '
local resty_hmac_sha256 = require "resty.hmac"
local hmac_sha256 = resty_hmac_sha256:new()

local digest = hmac_sha256:digest("INVALID_SHA","secret-key","Hello world")
ngx.say("hmac_sha256: ", digest)

--test with an empty string
digest = hmac_sha256:digest("INVALID_SHA","secret-key","")
ngx.say("hmac_sha256: ", digest)
';
}
--- request
GET /t
--- response_body_like
.*500 Internal Server Error.*
--- error_code: 500
--- grep_error_log eval: qr/attempt to use unknown algorithm: 'INVALID_SHA'.*?/
--- grep_error_log_out
attempt to use unknown algorithm: 'INVALID_SHA'

=== TEST 7: test with null secret or message
--- http_config eval: $::HttpConfig
--- config
location /t {
content_by_lua '
local resty_hmac_sha256 = require "resty.hmac"
local hmac_sha256 = resty_hmac_sha256:new()

local digest = hmac_sha256:digest("sha256",nil,"Hello world")
ngx.say("hmac_sha256: ", digest)

digest = hmac_sha256:digest("sha256",nil,"")
ngx.say("hmac_sha256: ", digest)
';
}
--- request
GET /t
--- response_body_like
.*500 Internal Server Error.*
--- error_code: 500
--- grep_error_log eval: qr/attempt to digest with a null key or message.*?/
--- grep_error_log_out
attempt to digest with a null key or message