From eaf6ad66ab413765dd8d92e4fb3fcd06f138109e Mon Sep 17 00:00:00 2001 From: nahi Date: Thu, 16 Jun 2011 14:22:36 +0000 Subject: backport r32050 by akr * lib/securerandom.rb (SecureRandom.random_bytes): modify PRNG state to prevent random number sequence repeatation at forked child process which has same pid. reported by Eric Wong. [ruby-core:35765] backport r32124 by nahi * test/test_securerandom.rb: Add testcase. This testcase does NOT aim to test cryptographically strongness and randomness. It includes the test for PID recycle issue of OpenSSL described in #4579 but it's disabled by default. git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/branches/ruby_1_8_7@32128 b2dd03c8-39d4-4d8f-98ff-823fe69b080e --- ChangeLog | 14 ++++ lib/securerandom.rb | 8 +++ test/test_securerandom.rb | 178 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 200 insertions(+) create mode 100644 test/test_securerandom.rb diff --git a/ChangeLog b/ChangeLog index 92b1334eb7..99d5fded70 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,17 @@ +Thu Jun 16 22:55:02 2011 Hiroshi Nakamura + + * test/test_securerandom.rb: Add testcase. This testcase does NOT aim + to test cryptographically strongness and randomness. It includes + the test for PID recycle issue of OpenSSL described in #4579 but + it's disabled by default. + +Mon Jun 13 18:33:04 2011 Tanaka Akira + + * lib/securerandom.rb (SecureRandom.random_bytes): modify PRNG state + to prevent random number sequence repeatation at forked + child process which has same pid. + reported by Eric Wong. [ruby-core:35765] + Thu Jun 2 18:33:51 2011 URABE Shyouhei * variable.c (rb_const_get_0): Fix previous change. There were diff --git a/lib/securerandom.rb b/lib/securerandom.rb index 3fd63e535f..a957687a82 100644 --- a/lib/securerandom.rb +++ b/lib/securerandom.rb @@ -50,6 +50,14 @@ module SecureRandom def self.random_bytes(n=nil) n ||= 16 if defined? OpenSSL::Random + @pid = $$ if !defined?(@pid) + pid = $$ + if @pid != pid + now = Time.now + ary = [now.to_i, now.usec, @pid, pid] + OpenSSL::Random.seed(ary.to_s) + @pid = pid + end return OpenSSL::Random.random_bytes(n) end if !defined?(@has_urandom) || @has_urandom diff --git a/test/test_securerandom.rb b/test/test_securerandom.rb new file mode 100644 index 0000000000..d0ee254e62 --- /dev/null +++ b/test/test_securerandom.rb @@ -0,0 +1,178 @@ +require 'test/unit' +require 'securerandom' +require 'tempfile' + +# This testcase does NOT aim to test cryptographically strongness and randomness. +class TestSecureRandom < Test::Unit::TestCase + def setup + @it = SecureRandom + end + + def test_s_random_bytes + assert_equal(16, @it.random_bytes.size) + 65.times do |idx| + assert_equal(idx, @it.random_bytes(idx).size) + end + end + +# This test took 2 minutes on my machine. +# And 65536 times loop could not be enough for forcing PID recycle. +if false + def test_s_random_bytes_is_fork_safe + begin + require 'openssl' + rescue LoadError + return + end + SecureRandom.random_bytes(8) + pid, v1 = forking_random_bytes + assert(check_forking_random_bytes(pid, v1), 'Process ID not recycled?') + end + + def forking_random_bytes + r, w = IO.pipe + pid = fork { + r.close + w.write SecureRandom.random_bytes(8) + w.close + } + w.close + v = r.read(8) + r.close + Process.waitpid2(pid) + [pid, v] + end + + def check_forking_random_bytes(target_pid, target) + 65536.times do + pid = fork { + if $$ == target_pid + v2 = SecureRandom.random_bytes(8) + if v2 == target + exit(1) + else + exit(2) + end + end + exit(3) + } + pid, status = Process.waitpid2(pid) + case status.exitstatus + when 1 + raise 'returned same sequence for same PID' + when 2 + return true + end + end + false # not recycled? + end +end + + def test_s_random_bytes_without_openssl + begin + require 'openssl' + rescue LoadError + return + end + begin + load_path = $LOAD_PATH.dup + loaded_features = $LOADED_FEATURES.dup + openssl = Object.instance_eval { remove_const(:OpenSSL) } + + remove_feature('securerandom.rb') + remove_feature('openssl.rb') + Dir.mktmpdir do |dir| + open(File.join(dir, 'openssl.rb'), 'w') { |f| + f << 'raise LoadError' + } + $LOAD_PATH.unshift(dir) + require 'securerandom' + test_s_random_bytes + end + ensure + $LOADED_FEATURES.replace(loaded_features) + $LOAD_PATH.replace(load_path) + Object.const_set(:OpenSSL, openssl) + end + end + + def test_s_hex + assert_equal(16 * 2, @it.hex.size) + 33.times do |idx| + assert_equal(idx * 2, @it.hex(idx).size) + assert_equal(idx, @it.hex(idx).gsub(/(..)/) { [$1].pack('H*') }.size) + end + end + + def test_s_base64 + assert_equal(16, @it.base64.unpack('m*')[0].size) + 17.times do |idx| + assert_equal(idx, @it.base64(idx).unpack('m*')[0].size) + end + end + +if false # not in 1.8.7 + def test_s_urlsafe_base64 + safe = /[\n+\/]/ + 65.times do |idx| + assert_not_match(safe, @it.urlsafe_base64(idx)) + end + # base64 can include unsafe byte + 10001.times do |idx| + return if safe =~ @it.base64(idx) + end + flunk + end +end + + def test_s_random_number_float + 101.times do + v = @it.random_number + assert(0.0 <= v && v < 1.0) + end + end + + def test_s_random_number_float_by_zero + 101.times do + v = @it.random_number(0) + assert(0.0 <= v && v < 1.0) + end + end + + def test_s_random_number_int + 101.times do |idx| + next if idx.zero? + v = @it.random_number(idx) + assert(0 <= v && v < idx) + end + end + +if false # not in 1.8.7 + def test_uuid + uuid = @it.uuid + assert_equal(36, uuid.size) + uuid.unpack('a8xa4xa4xa4xa12').each do |e| + assert_match(/^[0-9a-f]+$/, e) + end + end +end + + def protect + begin + yield + rescue NotImplementedError + # ignore + end + end + + def remove_feature(basename) + $LOADED_FEATURES.delete_if { |path| + if File.basename(path) == basename + $LOAD_PATH.any? { |dir| + File.exists?(File.join(dir, basename)) + } + end + } + end + +end -- cgit v1.2.3