-
Notifications
You must be signed in to change notification settings - Fork 30.5k
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
crypto: add HMAC to crypto.timingSafeEqual() #38488
Conversation
f2a0d3a
to
a080a53
Compare
Here's a stress test of this PR: https://ci.nodejs.org/job/node-stress-single-test/294/ And here's one for current master branch: https://ci.nodejs.org/job/node-stress-single-test/295/ |
feede9c
to
b3c3a0c
Compare
Stress test results are enough for me to say "This is a good thing." At least, it fixes test flakiness (beyond the tiny bit of flakiness inherent in a probabilistic test). My C++ was never very good and it's only gotten rustier over the last several years, so apologies in advance. @nodejs/crypto @not-an-aardvark |
How much slower? Is the existing implementation not timing safe enough? Should this be a semver-major? Or better yet introduced as a flag to the existing method? |
src/crypto/crypto_timing.cc
Outdated
char key[kKeySize]; | ||
snprintf(key, sizeof(key), "%04x%04x%04x%04x%04x%04x%04x%04x", | ||
bufKey[0], | ||
bufKey[1], | ||
bufKey[2], | ||
bufKey[3], | ||
bufKey[4], | ||
bufKey[5], | ||
bufKey[6], | ||
bufKey[7]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just use a static random key here rather than generating one each time? The key can be generated randomly when the crypto
binding is loaded and just reused.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just
Because I am not particularly fluent at C++ and copied this implementation from the uuid generation in our inspector code. Happy to take informed comments to improve it, but the answer to any "why not just" queries is going to be "Because I'm really bad at this."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And I guess I should state, rather than imply: Thanks for the suggestion, and I'll look into doing that later. (Focused on something else at the moment, but not focused enough to ignore GitHub comments, go figure.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:-) ... ok, I just wanted to make sure there wasn't a specific technical reason. By avoiding generating the random key on every call we can save at least some of the additional performance cost introduced here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the main advantage generating a random key every time is that it would become much harder for someone to get the function to return true
as a result of a HMAC-sha256 collision, in the event that sha256 becomes less collision-resistant in the future.
As a secondary, smaller benefit, it also prevents an attacker from knowing either of the inputs to CRYPTO_memcmp
. More explicitly, if the computation is CRYPTO_memcmp(sha256hmac(someKey, a), sha256hmac(someKey, b))
where someKey
is a static constant, then an attacker could control at least the first few bytes of sha256hmac(someKey, a)
by changing a
via brute force. Given that this is intended as a defense-in-depth measure in the scenario that CRYPTO_memcmp
is insufficient to address side-channels, in this scenario the attacker could use a timing side-channel to discover the first few bytes of sha256hmac(someKey, b)
. It's not clear how this would be useful in most cases, but it would give a nonzero amount of information about b
.
std::array<unsigned char, EVP_MAX_MD_SIZE> hash1; | ||
std::array<unsigned char, EVP_MAX_MD_SIZE> hash2; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not just unsigned char hash1[EVP_MAX_MD_SIZE]
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I don't know the difference, so that works for me. 😬
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Afaik, C-style array lacks the higher-level functionalities of std::array, hence, they do not track their own size, so you need to manage size information manually. So for this particular case, I think it's better to keep std::array
, since we keeps track of arrays own size.
If this really isn't timing-safe, isn't that a bug in |
We don't have any formal benchmarks so I'd have to write one to say with certainty. Using test/pummel/test-crypto-timing-safe-equal-benchmarks.js as a proxy for a benchmark:
Short answer is "Yes, not timing safe enough." Longer answer: Unless there's a bug in test/pummel/test-crypto-timing-safe-equal-benchmarks.js, the answer would be "The current implementation fails a lot on some specific hosts in CI, so yes, not timing safe enough." You can see in https://ci.nodejs.org/job/node-stress-single-test/294/nodes=rhel7-s390x/console that with this implementation, the test passed 1000 times out of 1000 runs on LinuxONE (which is very fast in CI--might be our fastest CI host). In comparison, master branch (which you can see at https://ci.nodejs.org/job/node-stress-single-test/295/nodes=rhel7-s390x/console) is still running as of this writing but so far has failed more than 50% of the 700+ runs so far.
Good questions. I'm interested in what others think. |
I assumed we're using the OpenSSL memcmp under the hood and didn't even consider that there might be e bug in memcmp, but now that you mention it, I suppose this all warrants more investigation. |
Bug.... no.... but there could be something at play here with the use of |
I could be wrong, but I would think lost precision should make the test more robust. |
There's also several lines of code inside |
Here's a stress test on a branch where I removed Stress test: https://ci.nodejs.org/job/node-stress-single-test/296/nodes=rhel7-s390x/ If this fails a lot, then the timing issue doesn't involve If it succeeds 100%, then that strongly suggests that the issue is with |
So far, no failures in 50 runs. So that strongly suggests (to me at least) that the problem is indeed |
Looks like we're not even using a wrapper around OpenSSL's |
Maybe the way I modified the C++ function allowed the optimizer to remove some of the other code. So maybe the conclusion I draw here isn't correct. I should have thought of that before... |
Re. "Is the existing implementation not timing safe enough?", I think:
If we want to investigate further, there are a few routes we could try:
|
Perhaps using |
Avoid possible V8 optimizations that may invalidate benchmark. This is done by writing randm UUIDs to file and reading the files again as needed, rather than having hardcoded strings for the buffer contents. Refs: nodejs#38488 (comment)
I tried to do a lightweight version of this approach in #38493 |
Add HMAC to crypto.timingSafeEqual(). This makes things slower but also makes them far more timing safe. Refs: nodejs#38226 (comment) Fixes: nodejs#38226
Add HMAC to crypto.timingSafeEqual(). This makes things slower but also
makes them far more timing safe.
Refs: #38226 (comment)
Fixes: #38226