8

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
iamtheneal
  • 113
  • 1
  • 8
  • It's not very well documented, is it? It'd be educational to look more closely at the source to see what OpenSSL does with the values provided. It's supposed to use whatever OS-level random facilities are available, like `/dev/urandom` or `/dev/random` instead of something like that. – tadman Aug 25 '12 at 06:27
  • NOte that I found some discussion about how forking may result in vulnerabilities, which does not bode well for the way Ruby calls OpenSSL. – Maarten Bodewes Aug 25 '12 at 16:35
  • Could you point out where it states that Ruby uses the pid and current time as (only) seeds? – Maarten Bodewes Aug 25 '12 at 16:37
  • It has since been changed to favor /dev/urandom FWIW https://bugs.ruby-lang.org/issues/9569 not to mention the other patch submitted as a result of this question :) – rogerdpack Mar 06 '20 at 01:40

3 Answers3

6

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.

emboss
  • 38,880
  • 7
  • 101
  • 108
  • I agree. Admittedly, my rhetoric was unnecessary and counterproductive. At the end of the day, it seems to me that this is an issue of documentation. It *was* somewhat difficult to confirm that the Ruby's implementation of SecureRandom wasn't reducing security. Anyway, thanks for your input. Also, I've edited my answer to make it less inflammatory. – iamtheneal Aug 27 '12 at 17:29
  • @iamtheneal Thank you! I removed the comment and modified my answer as well. – emboss Aug 27 '12 at 17:41
3

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).

Maarten Bodewes
  • 90,524
  • 13
  • 150
  • 263
  • How secure depends on the actual implenetation and underlying platform, but that should be obvious. – Maarten Bodewes Aug 25 '12 at 15:32
  • In particular, it depends on whether or not the prng in question is seeded properly, and whether or not the caller is responsible for providing such a seed, or if the prng seeds itself. – iamtheneal Aug 25 '12 at 18:12
  • @iamtheneal if I look at the [SecureRandom.rb](http://uuidtools.rubyforge.org/coverage/lib-compat-securerandom_rb.html) code it relies on the standard seeding of the openssl library or the operating system, which should be absolutely fine if the library/operating system is fine. – Maarten Bodewes Aug 25 '12 at 18:46
  • 1
    It wasn't clear (to me) from the Ruby code that the seeding was appropriate. From the source, you can see that SecureRandom.rb provides its own seed to OpenSSL. I (well, my colleague) had to dig into the OpenSSL code to see that providing your own seed doesn't suppress OpenSSL's own internal seeding. – iamtheneal Aug 25 '12 at 18:51
  • There is some differences regarding seeding between libs. Most will mix in the seed into the pool. But simetimes the first seed is used as only random (so you can generate the same stream), sometimes a re-seed completely resets the RNG. I *think* all three mentioned here add the seed to the pool, so then you are safe. Most of the time this is not documented all to well. – Maarten Bodewes Aug 25 '12 at 18:55
  • Agreed. Things turn out to okay in this case, since the seed is added, instead of being used as a replacement. My concern came from not understanding what OpenSSL does under the covers. Also, I definitely agree with your point about documentation. It seems to me that all parties involved (Ruby and OpenSSL) could have documented things much better. – iamtheneal Aug 25 '12 at 18:59
  • I've added the documentation in question for the OpenSSL `rand` function. – Maarten Bodewes Aug 25 '12 at 19:07
2

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.

iamtheneal
  • 113
  • 1
  • 8