Ruby中更快的常量字符串比较

时间:2014-08-12 22:21:16

标签: ruby

我试图将用户提供的身份验证令牌与存储在我服务器上的身份验证令牌进行比较。

最明显的方法是使用==,但这可能会造成时间攻击。

为了减轻我写这个安全比较功能:

# string comparison that leaks no information about the strings.
# loosely based on https://github.com/rack/rack/blob/master/lib/rack/utils.rb
# and http://security.stackexchange.com/questions/49849/timing-safe-string-comparison-avoiding-length-leak
def secure_compare(a, b)
  l = a.unpack("C*")

  i = 0
  r |= a.length - b.length # fail if the lengths are different
  b.each_byte do |v|
    r |= v ^ l[i]
    i = (i + 1) % a.length # make sure we compare on all bytes of b, even if a is shorter.
  end
  r == 0
end

唯一的问题是真的慢:它为60-80ms的页面加载增加了180ms。

有更快的方法进行恒定时间字符串比较吗?有更标准化的方法吗?

编辑:我使用以下脚本对不同的解决方案进行基准测试,fwiw - https://gist.github.com/daxtens/a3a59f163f08f9b447bb - 它显示了==如何尽早摆脱,泄露信息以及如何secure_compare==慢几个数量级。

编辑2:要完全清楚,我想要实现的是一个函数secure_compare(secret, untrusted_input),执行所需的时间完全取决于untrusted_input,而不是secret 。我还希望这个函数不会比==差几个数量级。提供的函数具有所需的时序依赖性,但它太慢了。

4 个答案:

答案 0 :(得分:5)

为了让事情变得简单,我保持简单,我已经重新实现了C中的函数和made it available as a gem

源代码位于GitHub(https://github.com/daxtens/fast_secure_compare)上,但其关键在于以下非常简单的C例程。

int secure_compare_bytes(const unsigned char * secret, unsigned int secret_len, 
                     const unsigned char * input, unsigned int input_len) {

    int input_pos;
    int secret_pos = 0;
    int result = secret_len - input_len;
    // make sure our time isn't dependent on secret_len, and only dependent
    // on input_len
    for (input_pos = 0; input_pos < input_len; input_pos++) {
        result |= input[input_pos] ^ secret[secret_pos];
        secret_pos = (secret_pos + 1) % secret_len;
    }

    return result;
}

```

还有一些FFI粘合剂可以让它与Ruby交谈。

它比原始纯Ruby快得多,并且比散列更快(也更简单)。我为了简洁而编辑了排练。这是在2008 MacBook上。您可以在演示目录中使用timing.rb复制它。

==== Long text ====
                                         user     system      total        real
==, early fail                       0.000000   0.000000   0.000000 (  0.000028)
==, late fail                        0.000000   0.000000   0.000000 (  0.000710)
Pure Ruby secure_compare, 'early'    1.730000   0.040000   1.770000 (  1.777258)
Pure Ruby secure_compare, 'late'     1.730000   0.050000   1.780000 (  1.774144)
C-based FastSecureCompare, 'early'   0.040000   0.000000   0.040000 (  0.047612)
C-based FastSecureCompare, 'late'    0.040000   0.000000   0.040000 (  0.045767)
SHA512-then-==, 'early'              0.050000   0.000000   0.050000 (  0.048569)
SHA512-then-==, 'late'               0.050000   0.000000   0.050000 (  0.046100)

==== Short text ====
                                         user     system      total        real
==, early fail                       0.000000   0.000000   0.000000 (  0.000028)
==, late fail                        0.000000   0.000000   0.000000 (  0.000031)
Pure Ruby secure_compare, 'early'    0.010000   0.000000   0.010000 (  0.010552)
Pure Ruby secure_compare, 'late'     0.010000   0.000000   0.010000 (  0.010805)
C-based FastSecureCompare, 'early'   0.000000   0.000000   0.000000 (  0.000556)
C-based FastSecureCompare, 'late'    0.000000   0.000000   0.000000 (  0.000516)
SHA512-then-==, 'early'              0.000000   0.000000   0.000000 (  0.000780)
SHA512-then-==, 'late'               0.000000   0.000000   0.000000 (  0.000812)

答案 1 :(得分:1)

secure_compare(Rails中的恒定时间字符串比较)


如果您使用的是Rails(或独立的ActiveSupport),则可以考虑使用ActiveSupport::SecurityUtils.secure_compare进行恒定时间字符串比较。

这里是一个例子:

ActiveSupport::SecurityUtils.secure_compare('foo', 'bar')

如果您对它的工作方式感兴趣,请查看source

答案 2 :(得分:0)

这样的事情可能有效(对pass来说是恒定的时间)

def check(x, pass)
  ((x.length > pass.length ? x : pass)
      .take(x.length)
      .zip(x)
      .reduce(true) { |r, (y, z)| r & (y == z) }) & 
  (pass.length == x.length)
end

在我的电脑上,1000字符长的密码需要0.5毫秒。

答案 3 :(得分:0)

  

是否有更快的方法进行恒定时间字符串比较?

很明显&#34;恒定时间字符串比较&#34;当输入大小不受限制时,理论上是不可能的。当您使用'The sly fox' * 1000作为测试输入而不是'abc'时,您几乎意识到了这一点。

  

我写了这个安全比较函数

我觉得这里有难闻的气味;为什么不使用现有的认证算法/实施来经受时间的考验?

  

编辑:我使用以下脚本来对不同的解决方案进行基准测试

仅供参考:因为模块初始化等可能会影响结果,所以使用Benchmark.bmbm会很好。

无论如何你喜欢这种方式?

require 'digest/sha2'

def secure_compare_kai(a, b)
    return Digest::SHA512.digest(a) == Digest::SHA512.digest(b) && a == b
end

实际上,计算哈希可能会向攻击者泄漏计时信息。您应该存储一对原始的,eh,auth令牌及其哈希值。还建议添加一粒盐:

您可能希望插入类似sleep(Random.rand(10e-3..30e-3))的内容,以免让您高枕无忧。

性能更新

在我的Atom D510(1.6 GHz)机器上,使用Digest::SHA512对1 MB输入进行散列仅需10 ms。

irb(main):009:0> x = "A" * 10**3; y = "A" * 10**6; 0
=> 0                   

irb(main):011:0> Benchmark.bmbm{|b| b.report("1 KB"){ Digest::SHA512.digest(x) }; b.report("1 MB"){ Digest::SHA512.digest(y) } }
Rehearsal ----------------------------------------
1 KB   0.000000   0.000000   0.000000 (  0.000049)
1 MB   0.010000   0.000000   0.010000 (  0.009677)
------------------------------- total: 0.010000sec

           user     system      total        real
1 KB   0.000000   0.000000   0.000000 (  0.000079)
1 MB   0.010000   0.000000   0.010000 (  0.009731)

irb(main):012:0> RUBY_VERSION
=> "2.1.2"