diff options
author | Bart de Water <bartdewater@gmail.com> | 2019-08-18 00:26:54 -0400 |
---|---|---|
committer | Samuel Williams <samuel.williams@oriontransfer.co.nz> | 2019-10-08 10:00:11 +1300 |
commit | 21681810c87e0f97979873284596104c87a6f780 (patch) | |
tree | 894a333873a83d5c9e11013cf4e1d9acbadb49c5 | |
parent | f7fc8c12807c523fc7e91429f83b6ee978ee26cb (diff) | |
download | ruby-openssl-21681810c87e0f97979873284596104c87a6f780.tar.gz |
Add `OpenSSL.memcmp?` for constant time/timing safe string comparison
Fixes https://bugs.ruby-lang.org/issues/10098
-rw-r--r-- | ext/openssl/ossl.c | 36 | ||||
-rw-r--r-- | test/test_ossl.rb | 28 |
2 files changed, 64 insertions, 0 deletions
diff --git a/ext/openssl/ossl.c b/ext/openssl/ossl.c index ab9a6bac..c6749267 100644 --- a/ext/openssl/ossl.c +++ b/ext/openssl/ossl.c @@ -605,6 +605,41 @@ static void Init_ossl_locks(void) #endif /* !HAVE_OPENSSL_110_THREADING_API */ /* + * call-seq: + * OpenSSL.memcmp?(string, string) -> boolean + * + * Constant time memory comparison. Inputs must be of equal length, otherwise + * an error is raised since timing attacks could leak the length of a + * secret. + * + * For securely comparing user input, it's recommended to use hashing and + * regularly compare after to prevent an unlikely false positive due to a + * collision. + * + * user_input = "..." + * expected = "..." + * hashed_input = OpenSSL::Digest::SHA256.digest(user_input) + * hashed_expected = OpenSSL::Digest::SHA256.digest(expected) + * OpenSSL.memcmp?(hashed_input, hashed_expected) && user_input == expected + */ +static VALUE +ossl_crypto_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 len1 = RSTRING_LEN(str1); + long len2 = RSTRING_LEN(str2); + + if(len1 != len2) + ossl_raise(rb_eArgError, "inputs must be of equal length"); + + switch (CRYPTO_memcmp(p1, p2, len1)) { + case 0: return Qtrue; + default: return Qfalse; + } +} + +/* * OpenSSL provides SSL, TLS and general purpose cryptography. It wraps the * OpenSSL[https://www.openssl.org/] library. * @@ -1125,6 +1160,7 @@ Init_openssl(void) */ mOSSL = rb_define_module("OpenSSL"); rb_global_variable(&mOSSL); + rb_define_singleton_method(mOSSL, "memcmp?", ossl_crypto_memcmp, 2); /* * OpenSSL ruby extension version diff --git a/test/test_ossl.rb b/test/test_ossl.rb new file mode 100644 index 00000000..657e1d0a --- /dev/null +++ b/test/test_ossl.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true +require_relative "utils" + +if defined?(OpenSSL) + +class OpenSSL::OSSL < OpenSSL::SSLTestCase + def test_memcmp? + assert_raises(ArgumentError) { OpenSSL.memcmp?("aaa", "a") } + assert_raises(ArgumentError) { OpenSSL.memcmp?("aaa", "aa") } + + assert OpenSSL.memcmp?("aaa", "aaa") + assert OpenSSL.memcmp?( + OpenSSL::Digest::SHA256.digest("aaa"), OpenSSL::Digest::SHA256.digest("aaa") + ) + + assert_raises(ArgumentError) { OpenSSL.memcmp?("aaa", "aaaa") } + refute OpenSSL.memcmp?("aaa", "baa") + refute OpenSSL.memcmp?("aaa", "aba") + refute OpenSSL.memcmp?("aaa", "aab") + assert_raises(ArgumentError) { OpenSSL.memcmp?("aaa", "aaab") } + assert_raises(ArgumentError) { OpenSSL.memcmp?("aaa", "b") } + assert_raises(ArgumentError) { OpenSSL.memcmp?("aaa", "bb") } + refute OpenSSL.memcmp?("aaa", "bbb") + assert_raises(ArgumentError) { OpenSSL.memcmp?("aaa", "bbbb") } + end +end + +end |