-
Notifications
You must be signed in to change notification settings - Fork 7.3k
Crypto library should have a constant-time equality function #8560
Comments
Good idea, do you have any exact proposal? I'm currently thinking about |
|
@jsha : this name makes me feel like we are writing Java, not JavaScript. May be |
That would be fine, or perhaps Also probably valuable in terms of API clarity to provide Hash.equal and Hmac.equal, since those are the most common use cases. |
OpenBSD has About constant time comparisons, I'm highly skeptical that you can write them in vanilla C or C++ unless the compiler is sufficiently stupid. Consider OpenBSD's int
timingsafe_bcmp(const void *b1, const void *b2, size_t n)
{
const unsigned char *p1 = b1, *p2 = b2;
int ret = 0;
for (; n > 0; n--)
ret |= *p1++ ^ *p2++;
return (ret != 0);
} The int
timingsafe_bcmp(const void *b1, const void *b2, size_t n)
{
const unsigned char *p1 = b1, *p2 = b2;
for (; n > 0; n--)
if (*p1++ ^ *p2++)
return 1; // Not constant time.
return 0;
} |
This is what OpenSSL is using: int CRYPTO_memcmp(const void *in_a, const void *in_b, size_t len)
{
size_t i;
const unsigned char *a = in_a;
const unsigned char *b = in_b;
unsigned char x = 0;
for (i = 0; i < len; i++)
x |= a[i] ^ b[i];
return x;
} I suggest that we should just stick to |
Not constant time either, not in the presence of whole program optimization. I think the only way to reliably do constant-time comparisons is in assembly. Even then you have to be wary of software and hardware optimizers. |
@bnoordhuis: Point well taken, and Brad Hill has a very cool fix for the problem called double HMAC: https://www.isecpartners.com/blog/2011/february/double-hmac-verification.aspx. However, by the "don't roll your own" principle, I think it makes sense to use OpenSSL's implementation and separately address any shortcomings there. |
@jsha Can you explain how that would work? I would summarize Hill's explanation as
I don't disagree with that but I would like to get it right from the start. A false sense of security can be more disastrous than having no security but at least being aware of that. |
Yes, memcmp(hmac(a, K), hmac(b, K)) is a good summary. The value of K in Brad's example is the same key you are using to generate the original HMAC that you are trying to validate. I believe it would also be valid to use a random K.
True, although in this case a number of Node users may already have a false sense of security from checking hmac(m, K) === b. At a certain point we do have to trust our libraries, though. If CRYPTO_memcmp in OpenSSL is broken under typical compilation scenarios, I believe that means a huge number of other primitives are also broken. |
I had a go at implementing a constant-time comparison function but I have to admit, it seems pretty hopeless. The interaction with the VM introduces many factors that turn a constant-time comparison into, for lack of a better word, arbitrary time. For example, comparing two strings is complicated by the fact that strings have several internal representations, may not be contiguous in memory, may need to be flattened first, etc. We could restrict it to buffers and typed arrays only but even then a determined attacker can probably glean information from inducing page faults of triggering garbage collections. |
@bnoordhuis: Is there any reason to implement it in JS instead of adding a binding to OpenSSL or another underlying library, as Node does for other crypto primitives? |
Implementing it in JS or C++ doesn't make much difference for the things I described above. You're going to end up dealing with JS objects either way and then it becomes nearly impossible to guarantee constant-time operation. |
Now I get it, thanks for clarifying. Java and Go address the representation issues by only handling byte arrays. Handling buffers might be appropriate in this case because that is what Hmac returns. All that said, the representation issues are enough to convince me that it's worthwhile to implement double-HMAC and use that. We can be more confident in doing the constant-time comparison because we know the representation that will be output from each HMAC step, and can do the comparison without ever converting types out of C++. |
I've filed a PR at node-forward/node#29. (Repo is private but ping me if you want access.) I'm still not sure if it's a great idea to add it because it's nearly impossible to make it truly constant time. I do make that abundantly clear in the documentation, though. |
Hi. Double-HMAC is not constant time, it is random-time-per-guess, which blinds the timing channel against iterative brute force. It allows you to stop the attack even in situations where you can never guarantee constant-time program semantics. Seems like the ideal choice in Node, honestly. Happy to answer any further questions you might have here. Re-using the same key for the comparison as that blog post recommends is fine against remote guessing, but it does reduce the strength of the HMAC to a little above the birthday bound for the hash. (so still infeasible for remote attacks) If you want the full strength of the HMAC, you can do a random key for the blinding step. This might be necessary against local attacks that can be performed very rapidly, but is certainly overkill for anything machine-to-machine. |
@hillbrad node-forward/node#29 is roughly the code below but in C++. function constTimeEqual(a, b) {
var key = crypto.randomBytes(32);
var ah = new crypto.Hmac('sha256', key).update(a).digest();
var bh = new crypto.Hmac('sha256', key).update(b).digest();
for (var c = 0, i = 0, n = ah.length; i < n; i += 1)
c |= ah[i] ^ bh[i];
c |= (a.length ^ b.length);
ah[0] = bh[0] = c; // Assign for side effect.
return c === 0;
} @jsha commented that it's allowed to bail out early if the inputs don't match in length, and that it's not necessary to compare the digests in constant time. That sounds reasonable but it seems to me that everything then hinges on the key's entropy. What would the ramifications be if, say, the PRNG is so broken that it returns only zeroes? Something that bugs me is that there is a (probably theoretical) chance of hash collisions. Maybe I should just stick with |
I would pick one strategy or the other: blind the timing channel, or do a constant-time compare. If you can get to a program abstraction layer where a reliable constant time compare is available, do that. Regarding bad keys for this particular use.
|
@bnoordhuis function constTimeEqual(a, b) {
var ah = crypto.createHash('sha1').update(a).digest('hex');
var bh = crypto.createHash('sha1').update(b).digest('hex');
return ah === bh;
} |
@Zolmeister There is a strategy called "Double HMAC" wherein you generate a nonce, HMAC both HMACs with said nonce as the key, then compare them using a normal string comparison strategy. It does prevent the leak of meaningful timing information. The reason "Double HMAC" works is because it uses a random nonce that the attacker cannot predict. If there is ever a RNG failure, this strategy becomes less robust. If there is a break in the hash function, we end up not much better off if we threw caution to the wind and left our code timing attack vulnerable. It's for this reason that most cryptographers employ a true constant-time comparison strategy rather than Double HMAC. Your simple hash-based comparison code is like a broken Double HMAC strategy. SHA-1 is a poor choice for a hash function in 2015, for starters. You don't use a nonce (nor an HMAC construct). It really offers no security against side-channels. The correct tool for the job would be, simply, function constTimeEqual(a, b) {
var key = crypto.randomBytes(32);
for (var c = 0, i = 0, n = ah.length; i < n; i += 1) {
c |= a[i] ^ b[i];
}
c |= (a.length ^ b.length);
return c === 0;
} |
@sarciszewski: The security of Double HMAC does not depend on the nonce being unpredictable. The security comes from the blinding property. Under Double HMAC the timing no longer indicates any information about the underlying data, and can't be used to guess the data. The only requirement for Double HMAC is that the nonce not repeat itself very frequently. It would actually be fine to use a non-CS PRNG. |
Sure, but I wouldn't recommend using a non-CS PRNG if you have a CSPRNG available for any crypto operation, personally. |
Sorry, I misspoke: It's not even required that the key be a nonce (i.e., not repeat itself). Like in Brad's example, it's fine for the key to be constant. My point was that @Zolmeister's code is not broken. See @hillbrad's earlier detailed comments on the topic. |
No it's not. If you send the same input 10000 times and always get the same output, then your underlying comparison operation is still dangerous. Blinding is helpful, but it doesn't actually remove the fact that some timing information is being leaked. Adding a nonce does achieve this. |
If we assume that However, compared to Double-HMAC ( All three methods require brute-forcing Since the comparison is byte-wise, the attacker finds 256 versions of From there, the attacker can offline attack The overall work done is the same in all regards, but in the single-hash version a large portion of it can happen offline in the datacenters of the attacker (on their hardware, using whatever resources they have at their disposal). How practical this is depends on the possible keyspace of Since there is no real added cost to doing double-hmac verification (pulling a random string and doing 2 more internal hash computations), the additional protection that it gives (especially guarding low-entropy secrets) is important. |
Untrue, unless the crpytographic properties of Edit: my point being that the quality of the 'password' should have no impact on the attack time due to the leak |
@Zolmeister I meant in the sense of a dictionary attack. Think along these lines: Assume you go through two rounds and find the first two "hash" characters are As far as the 64 bit part, I was more talking about if you knew some information about the source you could start enumerating the 64 bit key server-side, keeping only the guesses which match the prefix you found via timing attacks. So while you're executing the timing attack portion, the cluster could be building up a set of candidate keys that match the currently known key prefix. So after deducing a sufficiently large prefix size (say 10 bytes), the number of 64 bit keys that fit the space starts to drop significantly to the point where you can switch from timing attacking the full 20 byte sha1 and instead trying the prefix-filtered permutations. This style attack requires knowing something about |
+1, atm I need to use a separate module just for this. |
+1 |
Was reopened as nodejs/node#3043, the discussion should move there. |
When comparing HMACs, it's important to do a constant-time equality test between two digests to prevent timing attacks. See http://codahale.com/a-lesson-in-timing-attacks/.
According to http://nodejs.org/api/crypto.html, it looks like Node does not currently have a constant-time equality function. One is available as a module at https://www.npmjs.org/package/buffer-equal-constant-time, but this should be a core part of the crypto library since it's essential to correct use of HMACs.
The current library leads to incorrect implementations that use the
==
or===
operators, such as in this Stack Overflow question and answer: https://stackoverflow.com/questions/10305067/hmac-md5-validation-with-node-js-express-and-trialpayThe text was updated successfully, but these errors were encountered: