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

feat(utils) generate random strings via CSPRNG #2536

Merged
merged 1 commit into from
May 31, 2017
Merged

feat(utils) generate random strings via CSPRNG #2536

merged 1 commit into from
May 31, 2017

Conversation

p0pr0ck5
Copy link
Contributor

Summary

The current implementation of utils.random_string() leverages the LuaJIT math.random() under the hood to generate bytes; this is unsuitable for cases where cryptographically secure random data is needed, such as fallback values for authentication plugin secret values. To correct this, we introduce a wrapper around the kernel CSPRNG (via /dev/urandom) to read random bytes, and wrap utils.random_string around this. We also return these bytes in a modified Base64 encoding (replacing non-alphanumeric characters with random alphanumeric replacements); this serves to increase the size of the keyspace significantly while attempting to maintain some backwards compatibility with previous generated string
parameters (e.g. by generating a string of the same size and a somewhat matching pattern).

The underlying get_rand_bytes implementation is modified to read from /dev/urandom when explicitly requested, and falling back to OpenSSL's RAND_bytes when reading from urandom fails. The blocking read from urandom is acceptable when explicitly requested, as this is typically done in asynchronous environments (e.g. admin API requests), where the need for strong psuedorandomness outweighs the overhead of I/O and talking to the kernel.

Full changelog

  • Update lua_system_constants to 0.1.2 (for O_RDONLY)
  • Remove duplicate open(), close(), and strerror() cdefs from file-log plugin
  • Implement urandom handler in get_rand_bytes
  • Update random_string to use get_rand_bytes with urandom
  • Update search pattern in default JWT secret spec test
  • Update unrelated tests to use the proper util function

@Tieske
Copy link
Member

Tieske commented May 17, 2017

@p0pr0ck5 this contains changes to kong/plugins/file-log/handler.lua? did you mix branches?

@p0pr0ck5
Copy link
Contributor Author

@Tieske nope, it's noted in the PR summary, we remove duplicate and conflicting cdefs from this handler. Otherwise we attempt to redefine an already loaded symbol

@p0pr0ck5
Copy link
Contributor Author

BTW the Travis failures here are due to a hiccup with using the wrong build cache and the existing lua_system_constants shared object not being used in the test as part of the luarocks install. It should be ignored, or we can purge the caches to see the build through.

@thibaultcha
Copy link
Member

Since OpenSSL's RAND_bytes() is considered cryptographically secure, and itself gets its seed from /dev/urandom, what is the incentive for not using it instead?

I would go a little bit further, and would also like to ask why LuaJIT's math.random() isn't secure enough exactly, considering it is currently being seeded from OpenSSL (itself seeded from /dev/urandom)?

@p0pr0ck5
Copy link
Contributor Author

p0pr0ck5 commented May 17, 2017

Since OpenSSL's RAND_bytes() is considered cryptographically secure, and itself gets its seed from /dev/urandom, what is the incentive for not using it instead?

tl;dr: https://sockpuppet.org/blog/2014/02/25/safely-generate-random-numbers/

Another good thread: https://twitter.com/ircmaxell/status/653950793830297603

Userspace CSPRNGs aren't the best solution. We have access to the raw kernel entropy device, so let's use it while we can. For use cases that don't need/care about the best possible solution (or don't want to block on the read), this is why we provide a flag to ignore urandom. And, if we want a buffered solution for reading urandom bytes, we can look at something like lua-resty-urandom, which is getting a much-needed update now that lua_system_constants is updated.

I would go a little bit further, and would also like to ask why LuaJIT's math.random() isn't secure enough exactly, considering it is currently being seeded from OpenSSL (itself seeded from /dev/urandom)?

Tausworthe is not a CSPRNG, and thus is certainly not an appropriate source of randomness for generating what we consider secret values. At a higher level, the small search space provided by using a UUID is a weakness IMO; we should absolutely expand the character set use to build these values.

@p0pr0ck5
Copy link
Contributor Author

Another excellent resource I forgot to mention earlier: https://www.2uo.de/myths-about-urandom/

@thibaultcha
Copy link
Member

thibaultcha commented May 17, 2017

Yeah, I knew those articles and all the fuss about random/urandom. I would have thought OpenSSL's RAND_bytes() to still be a good enough source though, tbh. The only benefit I see is the non-blocking nature they provide...

Tausworthe is not a CSPRNG

That I wasn't sure of, but couldn't find any source to assert that. Do you have one?

we should absolutely expand the character set use to build these values.

Oh yeah for sure, using UUIDs for secret values was a flawed approach since day 1, definitely.

@bungle
Copy link
Member

bungle commented May 17, 2017

@p0pr0ck5
Copy link
Contributor Author

Tausworthe is not a CSPRNG

That I wasn't sure of, but couldn't find any source to assert that. Do you have one?

The kernel source is a good one :)

https://github.com/torvalds/linux/blob/1001354ca34179f3db924eb66672442a173147dc/lib/random32.c#L71-L77

@p0pr0ck5
Copy link
Contributor Author

@bungle yes the method by which OpenSSL seeds itself is not the issue, the problem is that userspace CSPRNGs are inherently flawed.

@bungle
Copy link
Member

bungle commented May 18, 2017

What do you think about this (it is a syscall but not file io):
https://github.com/justincormack/ljsyscall/blob/9a7b5843895d55cbc1b7ac06b9eafaae41ce947d/test/linux.lua#L344

The thing with userspace (like OpenSSL) is that they will certainly be faster.

@p0pr0ck5
Copy link
Contributor Author

@bungle yeah this is the ideal approach. But getrandom is only available in more recently kernels (3.17+), which by default is not used by some LTS systems that we support (such as Ubuntu Trusty). So we need to handle both cases anyway (I wouldn't be opposed to doing both). Ideally we would just use the libc wrapper, but this was only very very recently introduced, and is pretty much unavailable anywhere :/

Yes, userspace would be "faster". In our use case (infrequent, asynchronous uses) we are not concerned with pure speed. We are concerned with providing the most secure solution. Trading off to a userspace CSPRNG purely for speed gains is not a valid argument here IMO.

Having said that, I'm not ready to die on a urandom hill. If we're all truly opposed to a kernel-based solution, we can circle back to the argument, but at bare minimum we need to make random_string far more robust, even if that means not touching get_rand_bytes.

@bungle
Copy link
Member

bungle commented May 19, 2017

It looks like the new random_string is not asking urandom?

@p0pr0ck5
Copy link
Contributor Author

@bungle yep im a dummy and made a typo after doing some live testing. thanks!

fallback = true
end

local res = ffi.C.read(fd, buf, n_bytes)
Copy link
Member

Choose a reason for hiding this comment

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

If we fail opening /dev/urandom, we still try to read from it, and later on, to close the file descriptor? Wouldn't that leave use with 3 WARN logs instead of one, potentially?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, refactored approach to avoid this. let me know your thoughts.

@@ -135,9 +135,15 @@ describe("Utils", function()
describe("random_string()", function()
it("should return a random string", function()
local first = utils.random_string()
assert.truthy(first)
assert.falsy(first:find("-"))
assert.equals("string", type(first))
Copy link
Member

Choose a reason for hiding this comment

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

assert.is_string(first)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done, thanks

assert.equals(32, #first)

-- ensure we don't find anything that isnt alphanumeric
assert.is_nil(first:find("^[^%a%d]+$"))
Copy link
Member

@thibaultcha thibaultcha May 19, 2017

Choose a reason for hiding this comment

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

Something like assert.not_matches("^[^%a%d]+$", first) maybe

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done, thanks

@p0pr0ck5
Copy link
Contributor Author

(same issue with test failures here, this requires cache purging in order to pass because it tries to use the old version of the lua_system_constants object while running the tests as part of the local install).

The current implementation of utils.random_string() leverages
the LuaJIT math.random() under the hood to generate bytes; this is
unsuitable for cases where cryptographically secure random data
is needed, such as fallback values for authentication plugin
secret values. To correct this, we introduce a wrapper around
the kernel CSPRNG (via /dev/urandom) to read random bytes, and
wrap utils.random_string around this. We also return these bytes
in a modified Base64 encoding (replacing non-alphanumeric characters
with random alphanumeric replacements); this serves to increase
the size of the keyspace significantly while attempting to
maintain some backwards compatibility with previous generated string
parameters (e.g. by generating a string of the same size and a
somewhat matching pattern).

The underlying get_rand_bytes implementation is modified to read
from /dev/urandom when explicitly requested, and falling back to
OpenSSL's RAND_bytes when reading from urandom fails. The blocking
read from urandom is acceptable when explicitly requested, as this
is typically done in asynchronous environments (e.g. admin API
requests), where the need for strong psuedorandomness outweighs
the overhead of I/O and talking to the kernel.
@p0pr0ck5
Copy link
Contributor Author

(rebased following 0.10.3 release)

Copy link
Member

@thibaultcha thibaultcha left a comment

Choose a reason for hiding this comment

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

LGTM 👍 Was lua_system_constants released yet? It seems CI is failing due to it currently.

@thibaultcha thibaultcha added pr/ready (but hold merge) No more concerns, but do not merge yet (probably a conflict of interest with another PR or release) and removed pr/status/please review labels May 30, 2017
@p0pr0ck5
Copy link
Contributor Author

@thibaultcha yep lua_system_constants was deployed before this PR was pushed: http://luarocks.org:8080/modules/mashape/lua_system_constants/0.1.2-0

The CI is failing because, as a result of the travis cache, luarocks is using the 0.1.1 version of the shared object when trying to run the tests as part of the dep update process. With a clean cache this passes.

@p0pr0ck5 p0pr0ck5 merged commit c01752a into next May 31, 2017
@p0pr0ck5 p0pr0ck5 deleted the feat/urandom branch May 31, 2017 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pr/ready (but hold merge) No more concerns, but do not merge yet (probably a conflict of interest with another PR or release)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants