Project

General

Profile

Actions

Feature #10098

closed

[PATCH] Timing-safe string comparison for OpenSSL::HMAC

Added by arrtchiu (Matt U) over 9 years ago. Updated over 4 years ago.

Status:
Closed
Assignee:
-
Target version:
-
[ruby-core:64101]

Description

I could be totally wrong, but it seems the standard library doesn't provide a reliable way of comparing hashes in constant-time.

With this patch I propose to add an additional method, OpenSSL::HMAC#verify, which takes a binary string with a digest and compares it against the computed hash.


Files

hmac-timing.patch (2.5 KB) hmac-timing.patch arrtchiu (Matt U), 07/28/2014 02:58 PM
hmac-timing.patch (2.48 KB) hmac-timing.patch arrtchiu (Matt U), 07/28/2014 03:13 PM
tsafe_eql.patch (2.48 KB) tsafe_eql.patch arrtchiu (Matt U), 07/29/2014 03:56 AM
tsafe_inline.patch (3.51 KB) tsafe_inline.patch arrtchiu (Matt U), 07/29/2014 10:29 AM
0001-add-timing-safe-string-compare-method.patch (4.31 KB) 0001-add-timing-safe-string-compare-method.patch arrtchiu (Matt U), 08/23/2014 09:12 AM

Updated by arrtchiu (Matt U) over 9 years ago

Modified misleading error message - new patch attached.

Updated by nobu (Nobuyoshi Nakada) over 9 years ago

  • Indent style mismatch
  • Should try to convert the argument with StringValue()
  • Why HMAC only? Other digests don't need it?
  • Probably we should provide timing-safe binary compare function?

Updated by arrtchiu (Matt U) over 9 years ago

Thanks for the feedback!

Nobuyoshi Nakada wrote:

  • Indent style mismatch
  • Should try to convert the argument with StringValue()

Will fix - sorry, this is my first contribution!

  • Why HMAC only? Other digests don't need it?

Good point, I thought since HMAC is for both redundancy and message authentication it made most sense on HMAC rather than all digests - but - I can see there would be use-cases for comparing other digests or other strings in a timing-safe manner.

  • Probably we should provide timing-safe binary compare function?

I think this makes the most sense :)
How about moving this to a method on String - e.g. String#slow_eql? or String::timing_safe_eql?? I'm terrible at naming things :)

Updated by nobu (Nobuyoshi Nakada) over 9 years ago

Matt U wrote:

How about moving this to a method on String - e.g. String#slow_eql? or String::timing_safe_eql?? I'm terrible at naming things :)

slow is not the main concern here, IMHO.
The latter is more descriptive, but seems less pretty a little.

Updated by arrtchiu (Matt U) over 9 years ago

Nobuyoshi Nakada wrote:

slow is not the main concern here, IMHO.
The latter is more descriptive, but seems less pretty a little.

Cleaned up (hopefully correctly) and moved to String#tsafe_eql?. Any ideas for a better name?

Updated by nobu (Nobuyoshi Nakada) over 9 years ago

Seems correct.

I made a benchmark for it:

# bm-tsafe_eql.rb: [Feature #10098]

require 'benchmark'
a = "x"*1024_000
b = a+"y"
c = "y"+a
a << "x"
n = (ARGV.shift || 100000).to_i
Benchmark.bm(15) {|bm|
  bm.report("a==b") {n.times{a==b}}
  bm.report("a==c") {n.times{a==c}}
  bm.report("a.tsafe_eql?(b)") {n.times{a.tsafe_eql?(b)}}
  bm.report("a.tsafe_eql?(c)") {n.times{a.tsafe_eql?(c)}}
}

It seems quite slow.

           |      user|    system|     total|       real

---------------|----------|----------|----------|-----------
a==b | 4.660000| 0.000000| 4.660000|( 4.672629)
a==c | 0.010000| 0.000000| 0.010000|( 0.007154)
a.tsafe_eql?(b)| 34.330000| 0.020000| 34.350000|( 34.366759)
a.tsafe_eql?(c)| 34.450000| 0.020000| 34.470000|( 34.480267)

With the following patch,

           |      user|    system|     total|       real

---------------|----------|----------|----------|-----------
a==b | 4.660000| 0.000000| 4.660000|( 4.666683)
a==c | 0.000000| 0.000000| 0.000000|( 0.006657)
a.tsafe_eql?(b)| 7.660000| 0.010000| 7.670000|( 7.662584)
a.tsafe_eql?(c)| 7.660000| 0.000000| 7.660000|( 7.671189)

diff --git a/string.c b/string.c
index 5c2852f..90865c0 100644
--- a/string.c
+++ b/string.c
@@ -2504,7 +2504,7 @@ static VALUE
 rb_str_tsafe_eql(VALUE str1, VALUE str2)
 {
     long len, idx;
-    char result;
+    VALUE result;
     const char *buf1, *buf2;
 
     str2 = StringValue(str2);
@@ -2516,7 +2516,13 @@ rb_str_tsafe_eql(VALUE str1, VALUE str2)
     buf2 = RSTRING_PTR(str2);
 
     result = 0;
-    for (idx = 0; idx < len; idx++) {
+    idx = 0;
+    if (UNALIGNED_WORD_ACCESS || !((VALUE)buf1 % sizeof(VALUE)) && !((VALUE)buf2 % sizeof(VALUE))) {
+	for (; idx < len; idx += sizeof(VALUE)) {
+	    result |= *(const VALUE *)(buf1+idx) ^ *(const VALUE *)(buf2+idx);
+	}
+    }
+    for (; idx < len; idx++) {
         result |= buf1[idx] ^ buf2[idx];
     }
 

Updated by arrtchiu (Matt U) over 9 years ago

Nobuyoshi Nakada wrote:

According to notes on timingsafe_memcmp,
OpenBSD has timingsafe_memcmp(), and NetBSD has consttime_memequal().

Wow, thank you for such detailed and valuable feedback (and an awesome patch!)

What do you think about extracting this to an (inline) method like rb_timingsafe_memcmp(..) which can then use the system-provided ones if they exist? Since this is moving into distro/platform-specific territory I'm not sure how this fits with Ruby's coding guidelines.

Actions #9

Updated by nobu (Nobuyoshi Nakada) over 9 years ago

Agree to extract a function, I meant it by "provide timing-safe binary compare function".
But the name *_memcmp doesn't look nice, as it looks like returning the same result as memcmp.
The order on this comparison won't make sense, so *_equal or *_eql would be better.

Updated by arrtchiu (Matt U) over 9 years ago

What's your thoughts on this new patch?

At the moment I'm using OSX and Linux, unable to test timingsafe_memcmp() and consttime_memequal().

Updated by nobu (Nobuyoshi Nakada) over 9 years ago

rb_tsafe_eql() doesn't need to be VALUE, int is OK.
Tests for timing-safeness are desirable, but it would be fragile by noise.

Actions #12

Updated by cremno (cremno phobia) over 9 years ago

I don't like the proposed method name.

tsafe? It should be timingsafe. Saving some keystrokes isn't worth it to have a less obvious name. Even C programmers chose a longer name!

I'm also not happy about eql? either. Then people expect it to work like String#eql?. Same argument requirements/conversion, same result, and it's even mentioned in the proposed docs (“similarly” is vague)! But it doesn't. For example:

a = "\xFF".force_encoding(Encoding::CP437)
b = "\xFF".force_encoding(Encoding::CP850)
p a.eql?(b)  # => false
p a.tsafe_eql?(b)  # => true

If something is going to be added to String, then encodings need to be considered. Or:

require 'fiddle'
a = "\xFF"
(b = Fiddle::Pointer.malloc(1))[0] = 0xff
p a.eql?(b)  # => false
p a.tsafe_eql?(b)  # => true

Updated by arrtchiu (Matt U) over 9 years ago

Nobuyoshi Nakada wrote:

rb_tsafe_eql() doesn't need to be VALUE, int is OK.
Tests for timing-safeness are desirable, but it would be fragile by noise.

I'll get these done. Your benchmark code demonstrated this pretty well so (if it's ok with you) I'll use that as a starting point.

cremno phobia wrote:

I don't like the proposed method name.

tsafe? It should be timingsafe. Saving some keystrokes isn't worth it to have a less obvious name. Even C programmers chose a longer name!
I'm also not happy about eql? either. Then people expect it to work like String#eql?. Same argument requirements/conversion, same result, and it's even mentioned in the proposed docs (“similarly” is vague)! But it doesn't. For example:
...

Thanks for taking the time to test out this patch!
You have some really good points to consider. My thoughts in response:

  • Since Ruby has only the one String class for text and data, I think it does make sense to keep this method on the String class.
  • The behaviour you've demonstrated is intended, we care about the bytes in the buffer; not the encoding.
  • Name and documentation is terrible, I agree :)

I think the easiest way to resolve this would be to come up with a better name, and to explain more clearly in the documentation.

For a starting point, how about timingsafe_bytes_eq? ? I'll improve the documentation while making the fixes as suggested by Nobu :)

Updated by cremno (cremno phobia) over 9 years ago

Matt U wrote:

  • Since Ruby has only the one String class for text and data, I think it does make sense to keep this method on the String class.

I agree with you!

  • The behaviour you've demonstrated is intended, we care about the bytes in the buffer; not the encoding.
  • Name and documentation is terrible, I agree :)

Then that encoding will be ignored has to be mentioned in the documentation. What happens if the length of both strings differ, too.

I don't know yet how I feel about (silently) ignoring encoding. Maybe only ASCII-8BIT strings should be allowed? But having bytes in the name is good anyway! Naming things is hard. I don't have a better idea either (timingsafe_bytecmp?, but there's also casecmp…).

Are there any gems for this or is such a method part of one?

Updated by arrtchiu (Matt U) over 9 years ago

Changelog:

  • Renamed rb_tsafe_eql => rb_consttime_memequal.
  • Renamed rb_str_tsafe_eql => rb_str_consttime_bytes_eq.
  • Renamed tsafe_eql? => consttime_bytes_eq?.
  • rb_consttime_memequal now has return type int.
  • Updated documentation to reflect that encodings are ignored, and removed reference to eql?.
  • Added tests to ensure timing safety (delta of 0.25 sec allowed to account for GC/system noise).
  • Build on Travis passing: https://travis-ci.org/ruby/ruby/builds/33351019

Updated by arrtchiu (Matt U) over 9 years ago

Keen to hear feedback if any. Completely understand there are many more important tickets than this one, but it would be great to see this feature in MRI soon!

Devise, one of the most popular frameworks currently implements a timing-safe string compare in Ruby manually: https://github.com/plataformatec/devise/blob/66db52ce31b5d8629f5813a1d7f03a8bc17e5d52/lib/devise.rb#L480-L488

Updated by nagachika (Tomoyuki Chikanaga) over 9 years ago

  • Category changed from ext/openssl to core
  • Status changed from Open to Assigned
  • Assignee set to matz (Yukihiro Matsumoto)

The latest patch seems satisfy nobu, doesn't it?
At last we need to get approved from Matz.

Actions #18

Updated by zzak (zzak _) over 8 years ago

  • Assignee changed from matz (Yukihiro Matsumoto) to 7150

Updated by aledovsky (Aleksandrs Ļedovskis) about 8 years ago

Can someone clarify, what state is this feature in? Do we still need to get Matz's approval of String API change, or in light of Zachary's change "openssl" group gives the final call?

Updated by arrtchiu (Matt U) about 8 years ago

Aleksandrs Ļedovskis wrote:

Can someone clarify, what state is this feature in? Do we still need to get Matz's approval of String API change, or in light of Zachary's change "openssl" group gives the final call?

While still useful with OpenSSL, I'd say that this feature has changed since it was initially reported and no longer relates to OpenSSL. It seems this falls under Ruby's standard String API, which I assume is up to Matz.

Updated by naruse (Yui NARUSE) about 8 years ago

  • Assignee changed from 7150 to matz (Yukihiro Matsumoto)

Updated by shyouhei (Shyouhei Urabe) about 8 years ago

Anyways, this issue was discussed in this month's developer meeting. Attendees at there found that OpenSSL already provides a timing-safe binary comparison function in C (namely CRYPTO_memcmp). Just not currently usable from ruby. Why not export this to ruby-level so that application programmers can use?

Updated by naruse (Yui NARUSE) about 8 years ago

Following is a patch but I just found there's OPENSSL_memcmp, which is not timing safe...

diff --git a/ext/openssl/ossl.c b/ext/openssl/ossl.c
index d03dfa7..76333e2 100644
--- a/ext/openssl/ossl.c
+++ b/ext/openssl/ossl.c
@@ -551,6 +551,21 @@ static void Init_ossl_locks(void)
     CRYPTO_set_dynlock_destroy_callback(ossl_dyn_destroy_callback);
 }

+static VALUE
+ossl_memcmp(VALUE dummy, VALUE str1, VALUE str2)
+{
+    const unsigned char *p1 = (const unsigned char *)StringValuePtr(str1);
+    const unsigned char *p2 = (const unsigned char *)StringValuePtr(str2);
+    long len = RSTRING_LEN(str1);
+    long i;
+    unsigned char ret = 0;
+    if (len != RSTRING_LEN(str2)) return Qfalse;
+    for (i=0; i < len; i++) {
+       ret |= p1[i] ^ p2[i];
+    }
+    return ret ? Qfalse : Qtrue;
+}
+
 /*
  * OpenSSL provides SSL, TLS and general purpose cryptography.  It wraps the
  * OpenSSL[http://www.openssl.org/] library.
@@ -1088,6 +1103,7 @@ Init_openssl(void)
      */
     mOSSL = rb_define_module("OpenSSL");
     rb_global_variable(&mOSSL);
+    rb_define_singleton_method(mOSSL, "memcmp", ossl_memcmp, 2);

     /*
      * OpenSSL ruby extension version

Updated by arrtchiu (Matt U) almost 8 years ago

Yui, I'm a little confused. The patch you have in your comment looks timing-safe to me. Also I suggest taking a look at Nobu's improvements to my code, I definitely learned a lot more about speed after reading it.

Other things that don't use OpenSSL might benefit from this feature, so my vote is to add this to the String library rather than OpenSSL.

Updated by naruse (Yui NARUSE) over 7 years ago

  • Status changed from Assigned to Feedback

Even though OpenSSL uses the name memcmp, I re-considered it is a bad name.
Therefore this is back to naming issue.

Updated by shyouhei (Shyouhei Urabe) over 7 years ago

(Just to be clear) what is bad about the word "memcmp" is that the "cmp" part implies returning integers, rather than true/false. One of such example that returns integer is String#casecmp. But almost nobody wants integers for timing-safe comparisons, meseems at least. We should provide a method that returns true/false, and in doing so "memcmp" is inappropriate.

Updated by aledovsky (Aleksandrs Ļedovskis) over 7 years ago

Ok, here are couple suggestions, to keep the carriage rolling.

#safe_eql?
#secure_eql?
#timesafe_eql?
#timingsafe_eql?

Updated by nobu (Nobuyoshi Nakada) over 7 years ago

secure sounds vague, and compare doesn't address the point by shyouhei, I think.

Updated by shyouhei (Shyouhei Urabe) over 7 years ago

Are we going to add this method under OpenSSL namespace? If so "secure" describes almost nothing.

My suggestion is to add String#casecmp? (proposed in #12786) first, then introduce this one with OpenSSL.memcmp? naming.

Updated by shyouhei (Shyouhei Urabe) almost 6 years ago

String#casecmp? has already landed. It seems no blocker are there for implementing this one, except the name.

Updated by bdewater (Bart de Water) almost 6 years ago

I agree Rails' secure_compare name is not great. 'Secure' is a result of the fact it's a constant-time comparison. How about String#const_time_eql? or something along those lines?

Updated by shevegen (Robert A. Heiler) about 5 years ago

I think String#const_time_eql? is not an ideal name either.

A problem with "secure" is that it can mean different things in different contexts; on class String this
may be a bit more difficult since Strings in ruby can be so general purpose. Perhaps the OpenSSL
namespace could have somewhat more freedom to also accept names that are not absolutely
perfect? Just as comparison - although I don't think #const_time_eql? is a very good name, I think
it would fit a lot better into OpenSSL than it may fit towards class String as such.

'Secure' is a result of the fact it's a constant-time comparison.

Now I think that will surprise some people since they may wonder what secure has to do with any
constant-time comparison per se. :)

(By the way, I think the word "compare", also as method name, is easier to reason for than e. g.
"secure"; what would a "secure string" mean, for example? Would that be different or the same
to a "tainted" string or is it a separate aspect? But that is just a side comment, I think Bart would
like to see any forward decision towards the issue at hand considering he added it to the next
upcoming developer meeting.)

Updated by naruse (Yui NARUSE) about 5 years ago

  • Status changed from Feedback to Assigned
  • Assignee changed from matz (Yukihiro Matsumoto) to 7150

This feature is now considered up to @rhenium (Kazuki Yamaguchi).

Updated by bdewater (Bart de Water) over 4 years ago

The above PR has been merged into the OpenSSL gem 🎉

Updated by shyouhei (Shyouhei Urabe) over 4 years ago

  • Status changed from Assigned to Closed

Implemented in upstream. Closing.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0