-
Notifications
You must be signed in to change notification settings - Fork 167
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 hashed secure compare #280
Conversation
In 1ade643 the Rails-like secure_compare naming was adopted and in original pull request introducing this functionality debate around timing of hash functions followed. This made me realize why Rails' default of hashing the values to protect users from making mistakes is a good idea.
@tenderlove @jeremyevans I am okay with this but since you have some background in this area, do you mind commenting? |
Thanks for this. Since this is functionality in excess of OpenSSL, I'm on the fence as to whether it belongs here or not. Because we start implementing functionality and features that are layered on top of OpenSSL. We also don't want to give users tools that are difficult to use correctly. So, I see the value in this. Carrying on the previous discussion from #269 There are places where this this interface might actually be worse in practice. Let's say you have some kind of IP address and/or user agent stored in a session, and that is considered secret. The web server might receive the session id from a cookie, and then need to Therefore, the more robust solution is to compute the hash of the user agent before saving it in the very first instance (non-hostile environment) and all subsequent checks are fixed length and therefore cannot possibly leak information. Unfortunately, this interface encourages users to do the former, not the latter. Thoughts? |
#!/usr/bin/env ruby
require 'digest/sha2'
class Secret
def initialize(string)
@secret = digest(string)
end
def digest(string)
Digest::SHA256.digest(string)
end
def == hostile
# OpenSSL.secure_compare(digest(hostile), @secret)
digest(hostile) == @secret
end
end
secret = Secret.new("Hello World!")
puts secret == "Hello World!"
puts secret == "Goodbye World!" |
Thanks for your continuous feedback @ioquatix!
Double checking if I understand you correctly by rewording it in another example: my API is secured with a key supplied via an environment variable. You're saying it's more secure to hash that key once during app boot, instead of doing it every time the controller code is called? I agree that's better in theory, but let's take a look at the benchmarks one more time - I forgot to post the comparison results in the previous PR. The difference in practice is very small with hashing:
Note that "large difference length" is the exact same length (32 bytes) of a prehashed SHA-256 digest. And we're working in a garbage collected language too so who knows when the GC kicks in in production 😄 While the direct comparison without hashing shows an order of magnitude difference:
If there's only one thing someone is going to remember, I think this is a good default. |
The real proof is whether you can write code, given everything except the secret string, can determine the length of the secret string. Something like: #!/usr/bin/env ruby
require 'benchmark'
require 'digest/sha2'
LETTERS = ('a'..'z').to_a
@secret = rand(0..250).times.map{LETTERS.sample}.join
puts "Secret: #@secret (#{@secret.bytesize} bytes)"
def digest(string)
Digest::SHA256.digest(string)
end
def test(hostile)
digest(hostile)
digest(@secret)
end
def attack
timings = []
base_case = Benchmark.measure do
10000.times do
test("")
end
end.utime
puts "Empty case: #{base_case}s"
250.times do |length|
puts "Checking #{length}..."
string = "." * length
duration = Benchmark.measure do
10000.times do
test(string)
end
end.utime
if duration >= (base_case*1.1)
puts "Duration: #{duration} **"
else
puts "Duration: #{duration}"
end
end
end
attack If we can show an attack against the digest approach, then it's instant failure. But if we can't show any attack, it might be feasible. That's my advice right now. |
The other question is, do we about leaking length of secret string. Maybe it's irrelevant. I'm okay if we accept that position. It just needs to be clearly documented. |
Doesn't make sense to me, is this using the constant time string comparison? It doesn't support different string lengths? |
I don't think leaking the size of the secret is a major issue. Either the secret is large enough to make brute force attacks impractical, or it is small enough that it is vulnerable to brute force attacks anyway and trying brute force attacks on a smaller keyspace (fewer characters) first would not result in a significant slowdown. That being said, I don't really like the fact that |
lib/openssl.rb
Outdated
# Constant time memory comparison. Inputs are hashed using SHA-256 to prevent | ||
# leaking the length of the secret. Returns +true+ if the strings are | ||
# identical, +false+ otherwise. | ||
def secure_compare(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.
Is there any reason why we can't just use def self.secure_compare
? rather than module_function
.
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.
module_function
also makes it a private instance method, so that if someone does include OpenSSL
, then they call secure_compare
(instead of OpenSSL.secure_compare
). FWIW, Rack::Utils.secure_compare
also uses module_function
.
Okay based on that feedback, I think this is a pretty reasonable implementation. Happy to merge. Just one minor feedback regarding the |
secure_compare is for user input, fixed_length_secure_compare for already processed data that is known to have the same length
&
Yes, it uses what this PR now calls
Rack can still return early (as it does today) before calling
Adressed 👍 |
c2a6aaf
to
9a91192
Compare
LGTM. |
These are some ruby code style adjustments, to make the code base better with how code is written in the ruby community. There should be zero functional changes in here, it's just white space and variable naming. There are also a few non-functional code changes I could suggest, such as another way to define private constants, a few more changes to the gemspec file and adding a minimum ruby version (I'd suggest 3.1 or 3.0 as the minimum[1]). However, I'll leave that for another PR, assuming you are open to such changes. There is also both `secure_compare` and `fixed_with_secure_compare` in the Ruby OpenSSL library[2]. The difference is that the former will hash to ensure fixed length. But if both have fixed length, then there is no need. What's the case here? Are we certain the two are fixed and identical length already? Otherwise we should consider switching to `secure_compare`. (note that this is already what the 'fallback' implementation guards against: `raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize`) Fixes #158 [1] #158 [2] ruby/openssl#280 --------- Co-authored-by: sandstrom <[email protected]>
Follow-up from the discussion over at #269 (comment) and 1ade643 where the Rails-like
secure_compare
naming was adopted. The debate around timing of hash functions made me realize why Rails' default of hashing the values to protect users from making mistakes is a good idea.