Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is Ruby's seed for OpenSSL::Random sufficient?

I know very little about Ruby, so please forgive me if the answer to this is obvious. I noticed at http://www.ruby-doc.org/stdlib-1.9.3/libdoc/securerandom/rdoc/SecureRandom.html that Ruby uses the pid and the current time to seed OpenSSL::Random when a call to random_bytes is made. Unless something else happens under the covers, isn't this pretty much the seed that Netscape used in their initial SSL implementation in the mid 90s? http://en.wikipedia.org/wiki/Random_number_generator_attack#Prominent_examples_of_random_number_generator_security_issues

Surely Ruby hasn't revived an 18 year old bug. What am I missing here?

Edit: Here's the source for random_bytes. Notice the first check to see if ruby was compiled with OpenSSL, in which case it seeds it with the pid and current time.

def self.random_bytes(n=nil)
  n = n ? n.to_int : 16

  if defined? OpenSSL::Random
    @pid = 0 if !defined?(@pid)
    pid = $$
    if @pid != pid
      now = Time.now
      ary = [now.to_i, now.nsec, @pid, pid]
      OpenSSL::Random.seed(ary.to_s)
      @pid = pid
    end
    return OpenSSL::Random.random_bytes(n)
  end

  if !defined?(@has_urandom) || @has_urandom
    flags = File::RDONLY
    flags |= File::NONBLOCK if defined? File::NONBLOCK
    flags |= File::NOCTTY if defined? File::NOCTTY
    begin
      File.open("/dev/urandom", flags) {|f|
        unless f.stat.chardev?
          raise Errno::ENOENT
        end
        @has_urandom = true
        ret = f.readpartial(n)
        if ret.length != n
          raise NotImplementedError, "Unexpected partial read from random device: only #{ret.length} for #{n} bytes"
        end
        return ret
      }
    rescue Errno::ENOENT
      @has_urandom = false
    end
  end

  if !defined?(@has_win32)
    begin
      require 'Win32API'

      crypt_acquire_context = Win32API.new("advapi32", "CryptAcquireContext", 'PPPII', 'L')
      @crypt_gen_random = Win32API.new("advapi32", "CryptGenRandom", 'LIP', 'L')

      hProvStr = " " * 4
      prov_rsa_full = 1
      crypt_verifycontext = 0xF0000000

      if crypt_acquire_context.call(hProvStr, nil, nil, prov_rsa_full, crypt_verifycontext) == 0
        raise SystemCallError, "CryptAcquireContext failed: #{lastWin32ErrorMessage}"
      end
      @hProv, = hProvStr.unpack('L')

      @has_win32 = true
    rescue LoadError
      @has_win32 = false
    end
  end
  if @has_win32
    bytes = " ".force_encoding("ASCII-8BIT") * n
    if @crypt_gen_random.call(@hProv, bytes.size, bytes) == 0
      raise SystemCallError, "CryptGenRandom failed: #{lastWin32ErrorMessage}"
    end
    return bytes
  end

  raise NotImplementedError, "No random device"
end
like image 239
iamtheneal Avatar asked Aug 25 '12 01:08

iamtheneal


3 Answers

The seed being used in SecureRandom prohibits predictable random numbers that occur whenever PIDs get recycled. Without the fix in SecureRandom, OpenSSL's random number generator will produce the exact same values in different processes that possess the same PID.

#4579 outlines how this can happen, and corresponding entry on OpenSSL's mailing list tells us more or less that this has to be dealt with in client code. This is why this seed was chosen in Ruby to prevent a security threat. If not convinced, run the script Eric Wong attached on a Ruby version prior to this fix to see what this was all about.

Adding to owlstead's explanation, seeding OpenSSL's RNG at this point doesn't compromise security, because an uninitialized random generator will always call RAND_poll first, which will gather enough entropy regardless of whether values have been previously seeded/added or not.

However, since the seed values in SecureRandom are clearly predictable, we should not assume that they add any entropy. OpenSSL's internal behavior could change at some point and it could skip the initial entropy gathering if the values already seeded are considered to contain enough entropy.

I therefore opened #6928, which would choose a more defensive approach of assuming no entropy for the values added to the entropy pool that distinguish different processes - this would force OpenSSL to reliably collect enough entropy in all cases.

In conclusion, the choice of values (PID and time) was a sensible one, it even adds to the overall security (by preventing the "recycled PID attack") instead of diminishing it.

like image 136
emboss Avatar answered Nov 11 '22 16:11

emboss


It depends on the configuration of Ruby which RNG is used:

Secure random number generator interface.

This library is an interface for secure random number generator which is suitable for generating session key in HTTP cookies, etc.

It supports following secure random number generators.

  • openssl

  • /dev/urandom

  • Win32

All three of the above are generally considered secure. However, it depends on the implementation of the SecureRandom class if it actually is secure. The only way of knowing this is an extensive research into the implementations.

Looking at the code in the question it is clear that Ruby directly uses bytes generated by OpenSSL, after additionally seeding the PID:

Whenever seed data is added, it is inserted into the 'state' as follows.

The input is chopped up into units of 20 bytes (or less for the last block). Each of these blocks is run through the hash function as follows: The data passed to the hash function is the current 'md', the same number of bytes from the 'state' (the location determined by in incremented looping index) as the current 'block', the new key data 'block', and 'count' (which is incremented after each use). The result of this is kept in 'md' and also xored into the 'state' at the same locations that were used as input into the hash function. I believe this system addresses points 1 (hash function; currently SHA-1), 3 (the 'state'), 4 (via the 'md'), 5 (by the use of a hash function and xor).

like image 3
Maarten Bodewes Avatar answered Nov 11 '22 14:11

Maarten Bodewes


A colleague of mine investigated this, and found that the choice of seed was introduced as a response to this bug:

http://bugs.ruby-lang.org/issues/4579

Fortunately, OpenSSL seeds itself with 256 bits of entropy from /dev/urandom (if available), or the egd ('entropy gathering daemon' -- a precursor to /dev/urandom) depending on the how it was compiled. The seeding happens automatically the first time RAND_status() or RAND_bytes() is called, and is not suppressed if RAND_seed() is called explicitly. Kudos to the OpenSSL folks for this decision. Here's a link to the specific OpenSSL code:

http://cvs.openssl.org/dir?d=openssl/crypto/rand

The interesting files are md_rand.c, rand_lib.c, and rand_unix.c.

like image 2
iamtheneal Avatar answered Nov 11 '22 14:11

iamtheneal