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

Add crypto.timingSafeEqual(). #3073

Closed
wants to merge 18 commits into from
Closed

Conversation

jsha
Copy link
Contributor

@jsha jsha commented Sep 26, 2015

Fixes #3043.

@bnoordhuis
Copy link
Member

@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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@mscdex mscdex added the crypto Issues and PRs related to the crypto subsystem. label Sep 26, 2015
@brendanashworth brendanashworth added the semver-minor PRs that contain new features and should be released in the next minor version. label Sep 26, 2015
exports.timingSafeEqual = function(a, b) {
var key = new Buffer(32);
for (var i = 0; i < 32; i += 4) {
key.writeFloatBE(Math.random(), i);
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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;
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Member

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.

@brendanashworth
Copy link
Contributor

cc @nodejs/crypto

@rvagg
Copy link
Member

rvagg commented Sep 29, 2015

@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?

@jasnell
Copy link
Member

jasnell commented Feb 10, 2016

What's the status on this one?

@jsha
Copy link
Contributor Author

jsha commented Feb 11, 2016

Looks like some merge conflicts cropped up. I'll fix those, then I think this is ready to go.

@jsha jsha force-pushed the timing-safe-compare branch from 6263650 to 1af1602 Compare February 11, 2016 18:53
@jsha
Copy link
Contributor Author

jsha commented Feb 11, 2016

Okay, updated.

@jasnell
Copy link
Member

jasnell commented Feb 11, 2016

@nodejs/crypto ... PTAL

@jsha jsha force-pushed the timing-safe-compare branch from 163bdde to 5840d64 Compare February 19, 2016 19:13
@jsha
Copy link
Contributor Author

jsha commented Feb 19, 2016

Pushed an additional change with a final equality check to address @indutny's concern about collisions, and added a type check.

@indutny
Copy link
Member

indutny commented Feb 19, 2016

LGTM, good job @jsha !

@@ -0,0 +1,13 @@
'use strict';
var common = require('../common');
Copy link
Member

Choose a reason for hiding this comment

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

const for the requires

@jasnell
Copy link
Member

jasnell commented Feb 19, 2016

LGTM

@jsha
Copy link
Contributor Author

jsha commented May 12, 2016

Oops, forgot to remove .validate. Done.

@@ -92,7 +92,6 @@ Hmac.prototype.digest = Hash.prototype.digest;
Hmac.prototype._flush = Hash.prototype._flush;
Hmac.prototype._transform = Hash.prototype._transform;


Copy link
Member

Choose a reason for hiding this comment

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

Superfluous change.

@indutny
Copy link
Member

indutny commented May 12, 2016

Thank you! One more nit, CI: https://ci.nodejs.org/job/node-test-pull-request/2611/ .

@jsha
Copy link
Contributor Author

jsha commented May 12, 2016

Done.

On Thu, May 12, 2016 at 1:33 PM, Fedor Indutny notifications@github.com
wrote:

Thank you! One more nit, CI:
https://ci.nodejs.org/job/node-test-pull-request/2611/ .


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
#3073 (comment)

@indutny
Copy link
Member

indutny commented May 12, 2016

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(
Copy link
Member

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() ;)

Copy link
Contributor Author

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')
    
  •           .digest('hex');
    
    -assert.equal(h1, '19fd6e1ba73d9ed2224dd5094a71babe85d9a892', 'test HMAC');
  •           .update('to hmac');
    
    +assert.equal(h1.digest('hex'),
  •         '19fd6e1ba73d9ed2224dd5094a71babe85d9a892',
    
  •         'test HMAC');
    
    +const h2 = crypto.createHmac('sha1', 'Node')
  •           .update('some data')
    
  •           .update('to hmac');
    
    +assert.ok(h2.validate(

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

@indutny
Copy link
Member

indutny commented May 13, 2016

No problem, new CI: https://ci.nodejs.org/job/node-test-pull-request/2619/

@jsha
Copy link
Contributor Author

jsha commented May 13, 2016

Am I right in reading this CI output - just the Windows build failed? Any
idea why that would be?

On Thu, May 12, 2016 at 8:39 PM, Fedor Indutny notifications@github.com
wrote:

No problem, new CI: https://ci.nodejs.org/job/node-test-pull-request/2619/


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
#3073 (comment)

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.
Copy link
Member

Choose a reason for hiding this comment

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

Typo: possessing

@bnoordhuis bnoordhuis removed the stalled Issues and PRs that are stalled. label May 13, 2016
@MylesBorins
Copy link
Contributor

One more round of CI: https://ci.nodejs.org/job/node-test-pull-request/3020/

@nodejs/crypto any reason this should not land?

@bnoordhuis
Copy link
Member

@thealphanerd See the discussion here: #3073 (comment)

I'm still -1. I suggest we close this.

@jsha
Copy link
Contributor Author

jsha commented Jun 18, 2016

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.

@ChALkeR
Copy link
Member

ChALkeR commented Jun 18, 2016

About the compiler reordering things — we could deoptimize this function deliberately (e.g. adding a never-called eval after the return), but then it would need a test to ensure that no optimizations are being done there in the future. But I really doubt that's the best thing to do here.

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?

@hillbrad
Copy link
Contributor

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.

@jasnell
Copy link
Member

jasnell commented Jun 20, 2016

@ChALkeR ....

Perhaps this needs to be implemented on the C++ side, then?

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.

@not-an-aardvark not-an-aardvark mentioned this pull request Aug 9, 2016
4 tasks
@jasnell
Copy link
Member

jasnell commented Aug 22, 2016

Closing in favor of the ongoing work in #8040

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
crypto Issues and PRs related to the crypto subsystem. semver-minor PRs that contain new features and should be released in the next minor version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.