Skip to content
Open
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
11 changes: 10 additions & 1 deletion docs/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,13 @@ encoding = require "lapis.util.encoding"
]]
}

Inside OpenResty, `encode_base64`, `decode_base64` and `hmac_sha1` use the
implementations built into `ngx`. Any other HMAC is calculated with
[luaossl](https://github.com/wahern/luaossl), or with
[lua-resty-openssl](https://github.com/fffonion/lua-resty-openssl) when luaossl
can't be loaded. Without either library the module still loads, but
calculating that HMAC raises an error.

### `encode_base64(str)`

Base64 encodes a string.
Expand Down Expand Up @@ -614,7 +621,9 @@ to have it encoded into the token. You can then use the `callback` parameter of
`validate_token` to verify data's value.

The random string is stored in a cookie named as your session name with
`_token` appended to the end.
`_token` appended to the end. Its random bytes come from luaossl. Inside
OpenResty, the `resty.random` module that ships with OpenResty is used when
luaossl can't be loaded.

### `csrf.validate_token(req, callback=nil)`

Expand Down
24 changes: 22 additions & 2 deletions lapis/csrf.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,34 @@ do
local _obj_0 = require("lapis.util.encoding")
encode_base64, encode_with_secret, decode_with_secret = _obj_0.encode_base64, _obj_0.encode_with_secret, _obj_0.decode_with_secret
end
local openssl_rand = require("openssl.rand")
local config = require("lapis.config").get()
local cookie_name = tostring(config.session_name) .. "_token"
local random_bytes
do
local has_luaossl, openssl_rand = pcall(require, "openssl.rand")
if has_luaossl then
random_bytes = openssl_rand.bytes
elseif ngx and ngx.config and pcall(function()
return require("resty.random")
end) then
local resty_random = require("resty.random")
random_bytes = function(n)
local bytes = resty_random.bytes(n, true)
assert(bytes, "lapis.csrf: resty.random failed to generate random bytes")
return bytes
end
else
local luaossl_err = openssl_rand
random_bytes = function()
return error("lapis.csrf: generating a token requires luaossl (or resty.random in OpenResty), but luaossl failed to load: " .. tostring(luaossl_err))
end
end
end
local generate_token
generate_token = function(req, data)
local key = req.cookies[cookie_name]
if not (key) then
key = encode_base64(openssl_rand.bytes(32))
key = encode_base64(random_bytes(32))
req.cookies[cookie_name] = key
end
local token = {
Expand Down
23 changes: 21 additions & 2 deletions lapis/csrf.moon
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
-- csrf protection

import encode_base64, encode_with_secret, decode_with_secret from require "lapis.util.encoding"
openssl_rand = require "openssl.rand"

config = require"lapis.config".get!
cookie_name = "#{config.session_name}_token"

-- luaossl is optional so it doesn't have to be installed for apps running in
-- OpenResty, which bundles resty.random. resty.random calls into the OpenSSL
-- that nginx is linked with, so it's only used inside of a real nginx process.
-- (The fake ngx of a simulated request has no ngx.config)
random_bytes = do
has_luaossl, openssl_rand = pcall require, "openssl.rand"

if has_luaossl
openssl_rand.bytes
elseif ngx and ngx.config and pcall -> require "resty.random"
resty_random = require "resty.random"
(n) ->
bytes = resty_random.bytes n, true
assert bytes, "lapis.csrf: resty.random failed to generate random bytes"
bytes
else
luaossl_err = openssl_rand
->
error "lapis.csrf: generating a token requires luaossl (or resty.random in OpenResty), but luaossl failed to load: #{luaossl_err}"

generate_token = (req, data) ->
key = req.cookies[cookie_name]

unless key
key = encode_base64 openssl_rand.bytes(32)
key = encode_base64 random_bytes 32
req.cookies[cookie_name] = key

token = {
Expand Down
35 changes: 25 additions & 10 deletions lapis/util/encoding.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
local encode_base64, decode_base64, hmac_sha1
local config = require("lapis.config").get()
local openssl_hmac = require("openssl.hmac")
local new_hmac
do
local has_luaossl, openssl_hmac = pcall(require, "openssl.hmac")
if has_luaossl then
new_hmac = openssl_hmac.new
elseif pcall(function()
return require("resty.openssl.hmac")
end) then
new_hmac = require("resty.openssl.hmac").new
else
local luaossl_err = openssl_hmac
new_hmac = function(secret, digest_type)
return error("lapis.util.encoding: hmac_" .. tostring(digest_type) .. " requires luaossl or lua-resty-openssl, but luaossl failed to load: " .. tostring(luaossl_err))
end
end
end
local hmac_for
hmac_for = function(digest_type)
return function(secret, str)
local hmac = assert(new_hmac(secret, digest_type))
return assert(hmac:final(str))
end
end
if ngx then
do
local _obj_0 = ngx
Expand All @@ -16,16 +38,9 @@ else
decode_base64 = function(...)
return (unb64(...))
end
hmac_sha1 = function(secret, str)
local hmac = openssl_hmac.new(secret, "sha1")
return hmac:final(str)
end
end
local hmac_sha256
hmac_sha256 = function(secret, str)
local hmac = openssl_hmac.new(secret, "sha256")
return hmac:final(str)
hmac_sha1 = hmac_for("sha1")
end
local hmac_sha256 = hmac_for("sha256")
local default_hmac
local _exp_0 = config.hmac_digest
if "sha256" == _exp_0 then
Expand Down
30 changes: 23 additions & 7 deletions lapis/util/encoding.moon
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,27 @@
local encode_base64, decode_base64, hmac_sha1

config = require"lapis.config".get!
openssl_hmac = require "openssl.hmac"

-- luaossl is optional so it doesn't have to be installed for apps running in
-- OpenResty, where lua-resty-openssl can be used instead. Both libraries
-- provide new(key, digest_type) to create an hmac. With neither installed,
-- calculating an hmac raises an error that includes why luaossl failed to load
new_hmac = do
has_luaossl, openssl_hmac = pcall require, "openssl.hmac"

if has_luaossl
openssl_hmac.new
elseif pcall -> require "resty.openssl.hmac"
require("resty.openssl.hmac").new
else
luaossl_err = openssl_hmac
(secret, digest_type) ->
error "lapis.util.encoding: hmac_#{digest_type} requires luaossl or lua-resty-openssl, but luaossl failed to load: #{luaossl_err}"

hmac_for = (digest_type) ->
(secret, str) ->
hmac = assert new_hmac secret, digest_type
assert hmac\final str

if ngx
{:encode_base64, :decode_base64, :hmac_sha1} = ngx
Expand All @@ -12,17 +32,13 @@ else
encode_base64 = (...) -> (b64 ...)
decode_base64 = (...) -> (unb64 ...)

hmac_sha1 = (secret, str) ->
hmac = openssl_hmac.new secret, "sha1"
hmac\final str
hmac_sha1 = hmac_for "sha1"

---Generate HMAC-SHA256 hash
---@param secret string Secret key for HMAC
---@param str string String to hash
---@return string hash Binary HMAC-SHA256 digest
hmac_sha256 = (secret, str) ->
hmac = openssl_hmac.new secret, "sha256"
hmac\final str
hmac_sha256 = hmac_for "sha256"

default_hmac = switch config.hmac_digest
when "sha256"
Expand Down
91 changes: 90 additions & 1 deletion spec/csrf_spec.moon
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
csrf = require "lapis.csrf"
import encode_with_secret, decode_with_secret from require "lapis.util.encoding"
import encode_base64, encode_with_secret, decode_with_secret from require "lapis.util.encoding"
ngx_stack = require "lapis.spec.stack"
import require_with_stubs from require "spec.helpers"

describe "lapis.csrf", ->
config = require"lapis.config".get!
Expand Down Expand Up @@ -192,3 +194,90 @@ describe "lapis.csrf", ->
csrf_token: token
}
}, (d) -> d.number == 5

describe "random bytes backend", ->
-- stands in for luaossl's openssl.rand and OpenResty's resty.random
fake_rand = (char, log) -> {
bytes: (n, strong) ->
table.insert log, { :n, :strong } if log
string.rep char, n
}

generate_key = (c) ->
r = { cookies: {} }
assert c.generate_token r
r.cookies.lapis_session_token

it "uses luaossl when it's installed", ->
ngx_stack.push { config: {} }
finally -> ngx_stack.pop!

c = require_with_stubs "lapis.csrf", {
"openssl.rand": fake_rand "a"
"resty.random": fake_rand "b"
}

assert.same encode_base64(string.rep "a", 32), generate_key c

it "falls back to resty.random with nginx", ->
ngx_stack.push { config: {} }
finally -> ngx_stack.pop!

calls = {}
c = require_with_stubs "lapis.csrf", {
"openssl.rand": false
"resty.random": fake_rand "b", calls
}

assert.same encode_base64(string.rep "b", 32), generate_key c
-- asks for cryptographically strong bytes
assert.same {{ n: 32, strong: true }}, calls

it "fails when resty.random can't generate bytes", ->
ngx_stack.push { config: {} }
finally -> ngx_stack.pop!

c = require_with_stubs "lapis.csrf", {
"openssl.rand": false
"resty.random": { bytes: -> nil }
}

ok, err = pcall generate_key, c
assert.same false, ok
assert.truthy err\find "resty.random failed to generate random bytes", 1, true

describe "without nginx", ->
assert_no_backend = ->
c = require_with_stubs "lapis.csrf", {
"openssl.rand": false
"resty.random": fake_rand "b"
}

ok, err = pcall generate_key, c
assert.same false, ok
assert.truthy err\find "generating a token requires luaossl (or resty.random in OpenResty)", 1, true
-- includes the reason luaossl couldn't be loaded
assert.truthy err\find "luaossl failed to load: module 'openssl.rand' not found (stubbed by spec)", 1, true

it "fails to generate a token without luaossl", ->
assert_no_backend!

it "doesn't use resty.random in a simulated request", ->
-- the fake ngx of a simulated request has no ngx.config
ngx_stack.push { _lapis_simulate: true }
finally -> ngx_stack.pop!
assert_no_backend!

it "validates tokens without a backend", ->
c = require_with_stubs "lapis.csrf", {
"openssl.rand": false
"resty.random": false
}

r = { cookies: {} }
token = csrf.generate_token r

assert c.validate_token {
cookies: r.cookies
params: { csrf_token: token }
}
92 changes: 92 additions & 0 deletions spec/encoding_spec.moon
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
encoding = require "lapis.util.encoding"
ngx_stack = require "lapis.spec.stack"
import require_with_stubs from require "spec.helpers"

to_hex = (str) -> (str\gsub ".", (c) -> string.format "%02x", c\byte!)

-- stands in for luaossl's openssl.hmac and lua-resty-openssl's
-- resty.openssl.hmac, which share the same interface
fake_hmac = (name) -> {
new: (secret, digest_type) -> {
final: (str) => "#{name}:#{digest_type}:#{secret}:#{str}"
}
}

describe "lapis.util.encoding", ->
config = require"lapis.config".get!
Expand Down Expand Up @@ -29,4 +41,84 @@ describe "lapis.util.encoding", ->
assert.same {encoding.decode_with_secret "hello.world"},
{nil, "invalid signature"}

-- RFC 2202 and RFC 4231 test case 2
it "calculates hmac_sha1", ->
assert.same "effcdf6ae5eb2fa2d27416d5f184df9c259a7c79",
to_hex encoding.hmac_sha1 "Jefe", "what do ya want for nothing?"

it "calculates hmac_sha256", ->
assert.same "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843",
to_hex encoding.hmac_sha256 "Jefe", "what do ya want for nothing?"

describe "hmac backend", ->
it "uses luaossl when it's installed", ->
e = require_with_stubs "lapis.util.encoding", {
"openssl.hmac": fake_hmac "luaossl"
"resty.openssl.hmac": fake_hmac "resty"
}

assert.same "luaossl:sha1:key:msg", e.hmac_sha1 "key", "msg"
assert.same "luaossl:sha256:key:msg", e.hmac_sha256 "key", "msg"

it "falls back to lua-resty-openssl", ->
e = require_with_stubs "lapis.util.encoding", {
"openssl.hmac": false
"resty.openssl.hmac": fake_hmac "resty"
}

assert.same "resty:sha1:key:msg", e.hmac_sha1 "key", "msg"
assert.same "resty:sha256:key:msg", e.hmac_sha256 "key", "msg"

it "loads without a backend and fails when an hmac is calculated", ->
e = require_with_stubs "lapis.util.encoding", {
"openssl.hmac": false
"resty.openssl.hmac": false
}

for fn_name in *{"hmac_sha1", "hmac_sha256"}
ok, err = pcall e[fn_name], "key", "msg"
assert.same false, ok
assert.truthy err\find "#{fn_name} requires luaossl or lua-resty-openssl", 1, true
-- includes the reason luaossl couldn't be loaded
assert.truthy err\find "luaossl failed to load: module 'openssl.hmac' not found (stubbed by spec)", 1, true

assert.has_error -> e.encode_with_secret { color: "red" }

describe "with nginx", ->
before_each ->
mime = require "mime"
ngx_stack.push {
config: {}
encode_base64: (str) -> (mime.b64 str)
decode_base64: (str) -> (mime.unb64 str)
hmac_sha1: (secret, str) -> "ngx:sha1:#{secret}:#{str}"
}

after_each ->
ngx_stack.pop!

it "signs with ngx.hmac_sha1 without a backend", ->
e = require_with_stubs "lapis.util.encoding", {
"openssl.hmac": false
"resty.openssl.hmac": false
}

assert.same "ngx:sha1:key:msg", e.hmac_sha1 "key", "msg"
assert.same { color: "red" }, e.decode_with_secret e.encode_with_secret { color: "red" }
assert.has_error -> e.hmac_sha256 "key", "msg"

it "signs with lua-resty-openssl when hmac_digest is sha256", ->
prev_digest = config.hmac_digest
config.hmac_digest = "sha256"
finally -> config.hmac_digest = prev_digest

e = require_with_stubs "lapis.util.encoding", {
"openssl.hmac": false
"resty.openssl.hmac": fake_hmac "resty"
}

assert.same "ngx:sha1:key:msg", e.hmac_sha1 "key", "msg"
assert.same "resty:sha256:key:msg", e.hmac_sha256 "key", "msg"

msg, signature = e.encode_with_secret({ color: "red" })\match "^(.*)%.(.*)$"
assert.same "resty:sha256:the-secret:#{msg}", e.decode_base64 signature
Loading