Open twisted-trac opened 13 years ago
@zooko set owner to @exarkun @zooko set status to new |
---|
Okay I've reviewed this patch and it is okay.
There are a couple of things that are changed to use slowStringCompare
where I'm not sure that it is necessary:
Is it important to prevent someone who can submit matchesHost
requests from learning the hashed hostname?
Is it important to prevent someone who can submit checkKey
requests from learning the ssh public key? I think of ssh public keys as being public, so I don't think there's any harm in letting an attacker know the ssh public key.
@exarkun set owner to @mithrandi |
---|
Thanks for fixing this. I definitely messed up merging the previous version.
A couple simple things:
TestCase.flushWarnings
. If you can't find documentation talking about this, please also file a ticket for adding such. :) Or even if it exists but isn't easily discoverable, we should fix that too.This is going to result in deprecation warnings being emitted at the code in Twisted which directly calls slowStringCompare
, even though this code is fine. It's the code that constructs UsernamePassword
instances with unicode or calls checkPassword
or similar APIs that's a problem. So we may also want to add a layer of deprecation warnings to those APIs so that applications get a warning pointing at their code. So file a ticket for that as well.
Please merge when you've addressed those points.
ivank removed owner ivank set status to new |
---|
@jml set owner to @exarkun |
---|
Thanks for the work done so far everyone! Some questions:
I don't get why slowStringCompare
emits a DeprecationWarning when passed unicode strings. It's new, it can do whatever it wants, surely.
I would like to see _compare
in test_unicodeComparison
split up. I think the test is too big & too hard to read. Unfortunately, no ideas leap to mind. Thoughts?
Do the tests actually pass?
Reassigning to exarkun since he touched it last.
Automation removed owner |
---|
@exarkun set status to closed |
---|
(In [29835]) Merge password-comparison-4536-2
Author: ivank, exarkun Reviewer: exarkun, zooko Fixes: #4536
Change comparisons of sensitive materials to use a helper function which is resistent against leaking timing information.
@mithrandi set status to closed |
---|
(In [29879]) Merge password-comparison-4536-2
Author: ivank, exarkun, mithrandi Reviewer: exarkun, zooko Fixes: #4536
Change comparisons of sensitive materials to use a helper function which is resistent against leaking timing information.
@mithrandi set status to reopened |
---|
(In [29880]) Reopens #4536
Revert the merge again, sigh; u'\xff' == '\xff' is False, but slowStringCompare(u'\xff', '\xff') returns True in this form.
@mithrandi set status to closed |
---|
(In [29906]) Author: ivank, exarkun, mithrandi Reviewer: exarkun, zooko, Screwtape Fixes: #4536
Change comparisons of sensitive materials to use a helper function which is resistent against leaking timing information.
@glyph commented |
---|
Since this has come up again recently, it's worth pointing out that the right solution here might be to sidestep this issue entirely.
Secure (constant time) comparisons can only be done on secrets of a known length, which is why cryptography's bytes_eq
and hmac.compare_digest
only make promises about secrets of equivalent length. And there are numerous good reasons for that, but one is that if, at secret comparison time, you have to do data-dependent things based on both the secret and the input, you're in a bad situation.
The comparisons that are a problem here involve the credential checker having both the input and the plain-text secret in memory at the same time. We could possibly eventually have a proof of concept that conclusively verifies that comparison is timing-dependent, but this buries the lede which is that in this case we have a database full of plain text passwords.
We should really have every checker insist upon a digest algorithm to be used when storing shared secrets at rest (and then we can rely on the existing research and tools related specifically to digest comparisons). The thing to do with plain-text checkers is to deprecate them entirely, not to try to make their comparisons better.
@glyph commented |
---|
Oh also since I made some comments about "entropy depletion" earlier, I should say: that is a super dumb concern spurred on by the urban legends promulgated by the Linux /dev/urandom manpage. I was wrong. It is absolutely not a concern. See https://sockpuppet.org/blog/2014/02/25/safely-generate-random-numbers/
@glyph commented |
---|
In changeset 65f382f43dfe9bec9d25d99ce0cb05db115f5fac
#!CommitTicketReference repository="" revision="65f382f43dfe9bec9d25d99ce0cb05db115f5fac"
Merge pull request #1580 from wiml/8199-conch-constant-time-compare
Author: wiml
Reviewer: glyph
Fixes: ticket:8199
Refs: ticket:4536
Use constant-time compare_digest() for MACs and similar secret data.
@mithrandi commented |
---|
I filed #4607 to cover the latter issue.
@exarkun commented |
---|
#6783 was a duplicate of this.
chiiph commented |
---|
I've been trying to create a PoC so this issue will be taken more seriously, but I haven't been able to. That being said, I'm not a experienced hacker or however you want to call it, so this doesn't mean it's not possible to create one.
Being that this is a security issue, the policy should be to err on the side of caution, which right now means stop ignoring this issue and patch at hand. How can I help this get solved after all this time?
@zooko commented |
---|
In my opinion it would be a good idea to provide a function which compares two strings and guarantees not to leak information about their content in its timing. In my opinion, "regular bugs" should usually not be fixed without a unit test that exercises the bug and the alleged fix, but security vulnerabilities often should. In this case, in particular, there is a risk that someone out there can figure out a way to exploit this timing issue, and instead of submitting a unit test, they use it to steal other people's passwords.
@glyph commented |
---|
Replying to zooko:
In my opinion it would be a good idea to provide a function which compares two strings and guarantees not to leak information about their content in its timing. In my opinion, "regular bugs" should usually not be fixed without a unit test that exercises the bug and the alleged fix, but security vulnerabilities often should. In this case, in particular, there is a risk that someone out there can figure out a way to exploit this timing issue, and instead of submitting a unit test, they use it to steal other people's passwords.
As long as no-one can create one, then:
So we currently require PoCs for security issues for the same reason that we require them for regular bugs: every change carries risk, and should be justified by demonstrable reward. In the case of security, the risk is that much greater. So, zooko, I understand the emotional appeal of "we should do something!" in the case where users might be at risk, but unless we can demonstrate that they are at risk, then we may be doing more harm than good.
But I'm open to being convinced, if you disagree with my assessment of possible risks or have some other objective model for evaluating the validity of an attack rather than a PoC. (Can we have some kind of mathematical proof of anything related to the execution semantics of Python? Is that even possible?)
@glyph commented |
---|
For what it's worth I spent the better part of today trying to come up with a repeatable experiment, and the difference (if there is any) is way down below the threshold of noise, even if I disable frequency scaling and run tens of thousands of iterations per attempt, on both CPython and PyPy.
@zooko commented |
---|
I'm working on a PoC, but I really think that it should be fixed even if I can't come up with a working PoC. We know (from code inspection) that the timing information is there. So it is only a question of whether an attacker can exploit it. Requiring a PoC first is just saying "You have to be better at this than we are in order to harm our users." That's not a high-enough bar!
chiiph commented |
---|
I'm also working on a PoC, and while I can reproduce it reliably with the first letter, the attack PoC fails from the second letter on. Here's basically what I'm doing:
max(min((timeit.timeit("a==b", setup="a='H'+'ello'*40; b=chr(%i)+'ello'*40" % c, number=10**7), chr(c)) for j in range(5)) for c in range(256))
@exarkun commented |
---|
CPython 2.7 string equality is implemented like:
if (op == Py_EQ) {
/* Supporting Py_NE here as well does not save
much time, since Py_NE is rarely used. */
if (Py_SIZE(a) == Py_SIZE(b)
&& (a->ob_sval[0] == b->ob_sval[0]
&& memcmp(a->ob_sval, b->ob_sval, Py_SIZE(a)) == 0)) {
result = Py_True;
} else {
result = Py_False;
}
goto out;
}
Notice the special handling of the first byte and the use of memcmp
. I think a common implementation of memcmp
may be to compare bytes 4 (or even 8) at a time - so you may have to correctly guess blocks of 4 to see a timing difference. I haven't inspected any implementations of memcmp
to verify this though.
@exarkun commented |
---|
So... does that look like a proven timing leak to everyone? or just me? :)
Not to me, no. Without a demonstration that there's a leak there's still no way to evaluate a change to determine whether the leak has been fixed (or, say, made worse).
Requiring a PoC first is just saying "You have to be better at this than we are in order to harm our users." That's not a high-enough bar!
I'm not sure about this. Trying to fix it without a PoC seems much the same to me. If the fix is wrong and the attacker is smart enough to see it when we are not, the outcome is the same.
chiiph commented |
---|
Replying to exarkun:
So... does that look like a proven timing leak to everyone? or just me? :)
Not to me, no. Without a demonstration that there's a leak there's still no way to evaluate a change to determine whether the leak has been fixed (or, say, made worse).
I showed you a PoC that clearly shows how Python leaks the first char (at least) in a == comparison. You showed the reason why that's the case for the first char... and you still say there is no PoC for a leak? I'm not following. Does it need to be big enough for this to be a security issue?
@glyph commented |
---|
Replying to zooko:
I'm working on a PoC, but I really think that it should be fixed even if I can't come up with a working PoC. We know (from code inspection) that the timing information is there.
No, we don't. Timing information is leaked by hardware, not by algorithms. memcmp
implementations differ, and it's possible that all the ones our users are practically using a memcmp
implementation which does not leak for values within the range of the reasonable length of passwords. Maybe there's some crazy SIMD instruction that compares ten kilobytes at a time, maybe they're doing it on the GPU, nothing would surprise me at this point. CPUs are squirrely little bastards these days, and it seems entirely reasonable to me that even with a fairly straightforward byte- or word-at-a-time comparison algorithm, through a combination of instruction pipelining, variances in how microcode is executed, and noise introduced by operating system schedulers and network latency, there is no possible remote PoC.
So it is only a question of whether an attacker can exploit it. Requiring a PoC first is just saying "You have to be better at this than we are in order to harm our users." That's not a high-enough bar!
The thing is, if the attacker is better at this than we are, they do get to harm our users, whether we like it or not. They can as easily find a flaw that we can't recognize in the "fixed" version as they can find a flaw in the "broken" version that we can't prove.
For example, on Twitter you asked, "I don't understand why not to do this", with a link to a constant_time_compare function within Tahoe-LAFS.
os.urandom
. Is entropy depletion a concern? If not, why not? Do all platforms promise that it won't be, both now and in the future?SHA256
cache anything internally, so that hashing certain passwords might be faster than hashing others? Is promising never to do so part of its API contract?But the point is, if we don't have a test-case that demonstrates that at even the problem we're trying to fix here is fixed, we're taking the risk of introducing other issues and maybe not even fixing the timing attack because we're adding some new timing channel we hadn't previously considered.
All we have a PoC for right now is that we're leaking timing information about the first letter of the password. That suggests to me that the safest fix is to do this:
#!py
def safeBytesCompare(a, b):
return (b'\x00' + a) == (b'\x00' + b)
Now the first byte's always the same, and the only demonstrable attack has been defeated; we can much more plausibly estimate that +
is not risky than we can about the entire attack surface of kernel urandom
implementations, SHA256
and the Python wrappers for those things.
I'm still open to being convinced I'm in the wrong here, I am not a security expert, but just repeating "users are at risk, users are at risk" is not going to convince me, because empirically, science says that as far as we know, users are not at risk ("users are not at risk" is a falsifiable hypothesis and nobody's falsified it), and ethically, the precautionary principle says that the burden rests with those who say "we should do something" when both doing something and not-doing-something might be risky.
One thing that might be interesting to try would be to give this a spin on some alternate CPU architectures, like ARM, which, I am given to understand, are a bit more deterministic in their performance characteristics than Intel. Anybody have a raspberry pi or server-side ARM thing to look for a PoC on?
@glyph commented |
---|
I've been corresponding with Nate Lawson (author of the articles referenced in the ticket's description). While he hasn't entirely managed to convince me that we should fix it without a PoC, he has both provided some thoughts and provoked some of my own:
slowUnicodeCompare
that does something with the unicode data structure. This could normalize just the attacker-controlled input, and require the data store to have pre-normalized its own input. At that point we should be able to just hash the underlying bytes without encoding, but as far as I can tell, py3 has taken away the ability to access those bytes because unicode objects implement the old, but not the new, buffer interface. Anyone know a way around that?os.urandom
as zooko's implementation suggests, those bytes can be retrieved once on process startup. Given that sha hashing by itself ought to be constant time anyway, re-seeding the randomness on every comparison is overkill. (This significantly mitigates my entropy-leak concern.)Thanks everyone for your continued interest in making Twisted more secure.
@exarkun commented |
---|
Most recent build results look good. Merging.
@exarkun commented |
---|
There are a couple of things that are changed to use slowStringCompare where I'm not sure that it is necessary:
I think you're right about these. I had the same doubts as I was looking over the code. At the time, I left the changes in because I thought it couldn't hurt to be overly cautious. Taken to the extreme, that would leave Twisted with no uses of ==
though. ;) Since you also doubt the use of these, I'm going to go ahead and switch the code back to naive string comparison.
Thanks for the review!
@exarkun commented |
---|
(In [29826]) Remove two non-productive uses of slowStringCompare
refs #4536
@mithrandi commented |
---|
(In [29877]) Change test name and use flushWarnings. (refs #4536)
@mithrandi commented |
---|
Replying to jml:
- I don't get why
slowStringCompare
emits a DeprecationWarning when passed unicode strings. It's new, it can do whatever it wants, surely.
slowStringCompare
is new, but all of the places where it is used are not new; putting the DeprecationWarning in slowStringCompare
was easier than putting it in all of the places that it is now used. (Some of them probably aren't affected, but I think most of them are.)
Twisted has many timing attack vulnerabilities identical to the one found in Keyczar:
Originally reported by Ivan Kozik.
Attachments:
Searchable metadata
``` trac-id__4536 4536 type__defect defect reporter__exarkun exarkun priority__highest highest milestone__ branch__branches_password_comparison_4536_2 branches/password-comparison-4536-2 branch_author__ivank__mithrandi ivank, mithrandi status__new new resolution__ component__core core keywords__security security time__1277925946000000 1277925946000000 changetime__1626936906376695 1626936906376695 version__None None owner__ cc__zooko@... cc__ivank cc__warner ```