-
Notifications
You must be signed in to change notification settings - Fork 30k
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
Add crypto.timingSafeEqual(). #3073
Conversation
@mikeal Can I make node-forward/node public? I'd like to link to the previous discussion. |
// safety and benefits from never throwing an exception. | ||
// See discussion at | ||
// https://github.com/nodejs/node-v0.x-archive/issues/8560#issuecomment-60145816 | ||
exports.timingSafeEqual = function(a, b) { |
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 export this just for a test? Was it meant to be documented too?
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.
Thanks, I did indeed mean to document it as a new method. Fixed.
exports.timingSafeEqual = function(a, b) { | ||
var key = new Buffer(32); | ||
for (var i = 0; i < 32; i += 4) { | ||
key.writeFloatBE(Math.random(), i); |
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.
If you're generating for randomness, Math.random
is probably not the best option - I'd suggest var key = crypto.randomBytes(32)
instead.
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.
Ah, I see the (nodejs/node-v0.x-archive#8560 (comment)) discussion now. I'm not sure, but it makes sense just to use the system RNG regardless.. with Math.random
you might as well fill the buffer with 4
.
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 went back and forth on this a few times. The natural thing would be to just call crypto.randomBytes(32)
, and that's what I did the first time around. But crypto.randomBytes
is allowed to throw. So the options were: say that timingSafeEqual
is also allowed to throw (yuck), or catch the exception and do something next-best. And if we think the next-best is safe enough, why not just go with it directly?
As Brad said over on the linked issue, even if we just filled the buffer with four, that would only reduce the strength against guessing to the birthday bound for SHA256, which is still extremely strong. The counterintuitive thing about the Double-HMAC construction is that the strength is in the properties of an HMAC function, not in the random number. Where many things in crypto fall apart if your randomness is bad, this one (surprisingly) does not.
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.
timingSafeEqual
can still throw, even without crypto.randomBytes
(which I'm not even sure if that throws regularly). Next-best introduces a possible attack vector through Math.random
.
That is interesting, about the strength of this. Ultimately it is up to you whether or not you want to change it. Perhaps try a benchmark and see the difference?
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.
Well, you could make it sync/async just like randomBytes()
. That way an error is thrown for sync or it's passed as the err
argument for the async callback. Then use the async variation if you really don't want to wrap timingSafeEqual()
in a try-catch.
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.
timingSafeEqual can still throw
Ah, interesting! Under what conditions?
I was going off the fact that randomBytes
documents the possibility of an
exception, and was assuming we'd have to similarly cover the possibility
here, even if it was quite rare. All told, I'd prefer to use
crypto.randomBytes
to avoid the code smell. For me it's mainly a question
of what you'd like the Node API to look like. Is a tiny risk of exception
acceptable? I think, fwiw, that exception can never happen on Linux,
assuming OpenSSL is using /dev/urandom.
On Sep 26, 2015 13:52, "Brendan Ashworth" notifications@github.com wrote:
In lib/crypto.js
#3073 (comment):@@ -652,6 +672,23 @@ function filterDuplicates(names) {
}).sort();
}+// timingSafeEqual reuses the timing-safe Hmac.equals function (see above) for
+// comparison of non-hash inputs, like capability URLs or authentication tokens.
+// Note that the strength of the double-HMAC construction does not depend
+// on the unpredictability of the key. Since randomBytes can potentially
+// throw an exception, we use Math.random, which sacrifices nothing in timing
+// safety and benefits from never throwing an exception.
+// See discussion at
+// nodejs/node-v0.x-archive#8560 (comment)
+exports.timingSafeEqual = function(a, b) {
- var key = new Buffer(32);
- for (var i = 0; i < 32; i += 4) {
- key.writeFloatBE(Math.random(), i);
timingSafeEqual can still throw, even without crypto.randomBytes (which
I'm not even sure if that throws regularly). Next-best introduces a
possible attack vector through Math.random.That is interesting, about the strength of this. Ultimately it is up to
you whether or not you want to change it. Perhaps try a benchmark and see
the difference?—
Reply to this email directly or view it on GitHub
https://github.com/nodejs/node/pull/3073/files#r40498240.
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.
Under what conditions?
Really just under abnormal conditions, like running out of memory, but you could also cause an error from C++ by giving it a fake Buffer.
I might be wrong here, but I think the documentation for randomBytes
is out of date. Under normal circumstances, the only error thrown from randomBytes
is from RAND_bytes()
, which throws when it doesn't have enough entropy. However, with CheckEntropy
(see f68a116), it should now block until the system has enough entropy for the OpenSSL pool (see log in e5e5980). I think the documentation just needs to be updated - now that you bring it up, the docs are very confusing on that.
Edit: I've opened #3081.
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.
Alright, switched to randomBytes
, thanks for clarifying!
@@ -83,6 +83,7 @@ exports.createHmac = exports.Hmac = Hmac; | |||
function Hmac(hmac, key, options) { | |||
if (!(this instanceof Hmac)) | |||
return new Hmac(hmac, key, options); | |||
this.key = key; |
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.
What are the secondary security consequences of adding the secret key as an instance property in the constructor?
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.
None that I am aware of. I don't think Node makes any promises that Hmac keys are unextractable from memory after initialization, right?
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.
Should be either hidden, documented, or at least prefixed. Not sure if prefixing is a viable solution nowdays.
cc @nodejs/crypto |
@bnoordhuis I don't think that repo can be re-enabled now and even if it could we'd need to update the README and other cruft, perhaps you can just copypasta discussion if you can still find it? |
What's the status on this one? |
Looks like some merge conflicts cropped up. I'll fix those, then I think this is ready to go. |
6263650
to
1af1602
Compare
Okay, updated. |
@nodejs/crypto ... PTAL |
163bdde
to
5840d64
Compare
Pushed an additional change with a final equality check to address @indutny's concern about collisions, and added a type check. |
LGTM, good job @jsha ! |
@@ -0,0 +1,13 @@ | |||
'use strict'; | |||
var common = require('../common'); |
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.
const
for the requires
LGTM |
Oops, forgot to remove |
@@ -92,7 +92,6 @@ Hmac.prototype.digest = Hash.prototype.digest; | |||
Hmac.prototype._flush = Hash.prototype._flush; | |||
Hmac.prototype._transform = Hash.prototype._transform; | |||
|
|||
|
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.
Superfluous change.
Thank you! One more nit, CI: https://ci.nodejs.org/job/node-test-pull-request/2611/ . |
Done. On Thu, May 12, 2016 at 1:33 PM, Fedor Indutny notifications@github.com
|
New CI: https://ci.nodejs.org/job/node-test-pull-request/2617/ , Jenkins rebooted |
const h2 = crypto.createHmac('sha1', 'Node') | ||
.update('some data') | ||
.update('to hmac'); | ||
assert.ok(h2.validate( |
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.
Looks like a leftover from .validate()
;)
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.
Reverted, thanks. I can't check that CI URL because I'm getting 504's.
On Thu, May 12, 2016 at 5:21 PM, Fedor Indutny notifications@github.com
wrote:
In test/parallel/test-crypto-hmac.js
#3073 (comment):.update('some data')
.update('to hmac')
-assert.equal(h1, '19fd6e1ba73d9ed2224dd5094a71babe85d9a892', 'test HMAC');.digest('hex');
+assert.equal(h1.digest('hex'),.update('to hmac');
'19fd6e1ba73d9ed2224dd5094a71babe85d9a892',
'test HMAC');
+const h2 = crypto.createHmac('sha1', 'Node')
.update('some data')
+assert.ok(h2.validate(.update('to hmac');
Looks like a leftover from .validate() ;)
—
You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
https://github.com/nodejs/node/pull/3073/files/8f232da78b2bbb0628a1401f9b94a9d362d12191#r63118037
No problem, new CI: https://ci.nodejs.org/job/node-test-pull-request/2619/ |
Am I right in reading this CI output - just the Windows build failed? Any On Thu, May 12, 2016 at 8:39 PM, Fedor Indutny notifications@github.com
|
is very likely to introduce a | ||
[timing attack](http://codahale.com/a-lesson-in-timing-attacks/). | ||
Such a timing attack would allow someone to construct an | ||
HMAC value for a message of their choosing without posessing the key. |
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.
Typo: possessing
One more round of CI: https://ci.nodejs.org/job/node-test-pull-request/3020/ @nodejs/crypto any reason this should not land? |
@thealphanerd See the discussion here: #3073 (comment) I'm still -1. I suggest we close this. |
I'm happy to just call out to CRYPTO_memcmp if it would make everyone feel better, but I think it's a non-option to offer a cryptography library without a timing safe compare. |
About the compiler reordering things — we could deoptimize this function deliberately (e.g. adding a never-called The chance of a random collision here is negligible, there is no known attack on sha256 that breaks it, and we could always change the hashing function. This will by all means be better than what the ecosystem currently uses (e.g. https://github.com/hapijs/cryptiles/blob/master/lib/index.js#L48-L68). Still not sure about it, but I would prefer if we settled with some implementation and landed it, instead of saying «this is impossible in js». Perhaps this needs to be implemented on the C++ side, then? |
I think developers are more likely to find a safe verification method directly attached to the HMAC, but don't think this is worth bikeshedding any more after so long. This is substantially safer than the naive comparison that almost everyone uses, and provides an abstraction behind which future improvements can be iterated on. +1 to landing it. |
@ChALkeR ....
I was thinking the same. If this were implemented as native code, we ought to be able to bypass the timing concerns in JS entirely. We would need to verify, however, that there are no new timing concerns introduced by the JS-to-native boundary but I would be surprised. |
Closing in favor of the ongoing work in #8040 |
Fixes #3043.